forked from Dan4ick/Lidar_Muxa
слил ветку Zhirik1337: его Docker, GPU-зависимости и ROS-узел; код детектора взят из выгрузки
This commit is contained in:
commit
6964e4cc4e
11 changed files with 1350 additions and 0 deletions
38
.dockerignore
Normal file
38
.dockerignore
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.github
|
||||
|
||||
# Python caches
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.pytest_cache
|
||||
.coverage
|
||||
htmlcov/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
.env
|
||||
|
||||
# Editor & OS artifacts
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Agent & temporary files
|
||||
scratch/
|
||||
.gemini/
|
||||
|
||||
# Raw lidar data (mounted via volume, never baked into image)
|
||||
data/
|
||||
96
Dockerfile
Normal file
96
Dockerfile
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# ============================================================================
|
||||
# FlyGuard: Bionic Obstacle Detection Core (LCT-2026, Case 05)
|
||||
# Hesai Pandar128 LiDAR (128 beams, 10 Hz / 100ms)
|
||||
# Universal Production Image: NVIDIA CUDA 12.x / Ada Lovelace / Graceful CPU Fallback
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Stage 1: Optional lightweight CPU-only image (build with: --target cpu)
|
||||
# ----------------------------------------------------------------------------
|
||||
FROM python:3.11-slim-bookworm AS cpu
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONPATH="/app:/app/tools" \
|
||||
FLYGUARD_DATA="/data" \
|
||||
FLYGUARD_DEVICE="cpu"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libgomp1 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN useradd -m -u 1000 -s /bin/bash flyguard && \
|
||||
mkdir -p /app /data /app/artifacts && \
|
||||
chown -R flyguard:flyguard /app /data
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=flyguard:flyguard requirements.txt /app/
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY --chown=flyguard:flyguard . /app/
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
USER flyguard
|
||||
VOLUME ["/data", "/app/artifacts"]
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["default"]
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Stage 2: Universal Production Image with NVIDIA GPU acceleration & CPU fallback
|
||||
# (Default target when building with: docker build -t flyguard:latest .)
|
||||
# ----------------------------------------------------------------------------
|
||||
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS production
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONPATH="/app:/app/tools" \
|
||||
FLYGUARD_DATA="/data" \
|
||||
FLYGUARD_DEVICE="auto" \
|
||||
NVIDIA_VISIBLE_DEVICES=all \
|
||||
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
|
||||
# Install Python 3.11, pip, and system runtime libraries (OpenMP for LightGBM/Torch)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common \
|
||||
ca-certificates \
|
||||
libgomp1 \
|
||||
curl \
|
||||
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-distutils \
|
||||
&& curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
||||
&& ln -sf /usr/bin/python3.11 /usr/bin/python3 \
|
||||
&& ln -sf /usr/bin/python3.11 /usr/bin/python \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user for container security compliance
|
||||
RUN useradd -m -u 1000 -s /bin/bash flyguard && \
|
||||
mkdir -p /app /data /app/artifacts && \
|
||||
chown -R flyguard:flyguard /app /data
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Cache layer: install Python dependencies with PyTorch CUDA 12.1+ wheels
|
||||
COPY --chown=flyguard:flyguard requirements-gpu.txt /app/
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
pip install --no-cache-dir -r requirements-gpu.txt
|
||||
|
||||
# Copy source code and artifacts
|
||||
COPY --chown=flyguard:flyguard . /app/
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
USER flyguard
|
||||
VOLUME ["/data", "/app/artifacts"]
|
||||
|
||||
# Container healthcheck: verifies Python runtime and graceful device detection
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python3 -c "import flyguard; from flyguard.device import get_device_info; print('healthy', get_device_info())" || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["default"]
|
||||
57
Dockerfile.gpu
Normal file
57
Dockerfile.gpu
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# ============================================================================
|
||||
# FlyGuard GPU: NVIDIA RTX / CUDA 12 Production Image (Кейс 05, ЛЦТ-2026)
|
||||
# Архитектура: Ada Lovelace (RTX 4070 Ti Super 16GB) / Ampere / Turing
|
||||
# Полная совместимость с Dockerfile (Stage: production)
|
||||
# ============================================================================
|
||||
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS production
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONPATH="/app:/app/tools" \
|
||||
FLYGUARD_DATA="/data" \
|
||||
FLYGUARD_DEVICE="auto" \
|
||||
NVIDIA_VISIBLE_DEVICES=all \
|
||||
NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
|
||||
# Install Python 3.11, pip, and system dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common \
|
||||
ca-certificates \
|
||||
libgomp1 \
|
||||
curl \
|
||||
&& add-apt-repository -y ppa:deadsnakes/ppa \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.11 \
|
||||
python3.11-distutils \
|
||||
&& curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \
|
||||
&& ln -sf /usr/bin/python3.11 /usr/bin/python3 \
|
||||
&& ln -sf /usr/bin/python3.11 /usr/bin/python \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user and directories
|
||||
RUN useradd -m -u 1000 -s /bin/bash flyguard && \
|
||||
mkdir -p /app /data /app/artifacts && \
|
||||
chown -R flyguard:flyguard /app /data
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Cache layer: copy only requirements first
|
||||
COPY --chown=flyguard:flyguard requirements-gpu.txt /app/
|
||||
|
||||
# Install python dependencies with PyTorch CUDA 12
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
pip install --no-cache-dir -r requirements-gpu.txt
|
||||
|
||||
# Copy application source code
|
||||
COPY --chown=flyguard:flyguard . /app/
|
||||
RUN chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
USER flyguard
|
||||
VOLUME ["/data", "/app/artifacts"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python3 -c "import flyguard; from flyguard.device import get_device_info; print('healthy', get_device_info())" || exit 1
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD ["default"]
|
||||
237
docker-compose.yml
Normal file
237
docker-compose.yml
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
services:
|
||||
# ============================================================================
|
||||
# FlyGuard: Универсальный производственный запуск (NVIDIA GPU / CUDA 12)
|
||||
# Поддерживает Ada Lovelace (RTX 4070 Ti Super 16GB) / Ampere / CPU Fallback
|
||||
# ============================================================================
|
||||
|
||||
# --- Главный сервис общего запуска конвейера по всем бэгам лидара ---
|
||||
pipeline:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-pipeline
|
||||
command: ["pipeline", "--all", "--verbose"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- FLYGUARD_DEVICE=auto
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Полный прогон тестового набора ядра и роутинга устройств ---
|
||||
test:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-test
|
||||
command: ["test"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- FLYGUARD_DEVICE=auto
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Диагностика доступности GPU и характеристик оборудования ---
|
||||
info:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-info
|
||||
command: ["info"]
|
||||
environment:
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
|
||||
# --- Оценка метрик детекции (Folds Cross-Validation / AUC) ---
|
||||
evaluate:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-evaluate
|
||||
command: ["evaluate", "--mbon-dir", "artifacts/mbon_folds", "--device", "auto"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Генерация синтетического бенчмарка с GPU DoG и MBON ---
|
||||
benchmark:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-benchmark
|
||||
command: [
|
||||
"benchmark",
|
||||
"--memory", "artifacts/mushroom_body.npz",
|
||||
"--mbon-dir", "artifacts/mbon_folds",
|
||||
"--augment",
|
||||
"--device", "auto",
|
||||
"--out", "artifacts/benchmark_gpu.json"
|
||||
]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Обучение MBON Readout на 50 000 клеток Кеньона на GPU ---
|
||||
train-mbon:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-train-mbon
|
||||
command: [
|
||||
"train-mbon",
|
||||
"--device", "auto",
|
||||
"--n-kc", "50000",
|
||||
"--active", "100",
|
||||
"--epochs", "100",
|
||||
"--save-folds", "artifacts/mbon_folds_50k",
|
||||
"--out", "artifacts/mbon_readout_50k.npz"
|
||||
]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Обучение классификатора треков TrackReadout ---
|
||||
train-track:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-train-track
|
||||
command: [
|
||||
"train-track",
|
||||
"--device", "auto",
|
||||
"--epochs", "300",
|
||||
"--baseline",
|
||||
"--save-folds", "artifacts/track_folds",
|
||||
"--out", "artifacts/track_readout.npz"
|
||||
]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
shm_size: '8gb'
|
||||
|
||||
# --- Интерактивная Bash-сессия разработчика ---
|
||||
shell:
|
||||
image: flyguard:latest
|
||||
container_name: flyguard-shell
|
||||
command: ["bash"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- NVIDIA_VISIBLE_DEVICES=all
|
||||
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: all
|
||||
capabilities: [gpu]
|
||||
stdin_open: true
|
||||
tty: true
|
||||
shm_size: '8gb'
|
||||
|
||||
# ============================================================================
|
||||
# СЕРВИСЫ ДЛЯ ЧИСТОГО CPU (Stand-alone без NVIDIA GPU)
|
||||
# ============================================================================
|
||||
cpu-test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: cpu
|
||||
image: flyguard:cpu
|
||||
container_name: flyguard-cpu-test
|
||||
command: ["test"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- FLYGUARD_DEVICE=cpu
|
||||
shm_size: '2gb'
|
||||
|
||||
cpu-pipeline:
|
||||
image: flyguard:cpu
|
||||
container_name: flyguard-cpu-pipeline
|
||||
command: ["pipeline", "--all"]
|
||||
volumes:
|
||||
- ./artifacts:/app/artifacts
|
||||
- ./data:/data:ro
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- FLYGUARD_DATA=/data
|
||||
- FLYGUARD_DEVICE=cpu
|
||||
shm_size: '2gb'
|
||||
119
docker-entrypoint.sh
Executable file
119
docker-entrypoint.sh
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# ==============================================================================
|
||||
# FlyGuard: Bionic Obstacle Detection Core (LCT-2026, Case 05)
|
||||
# Hesai Pandar128 LiDAR (128 beams, 10 Hz / 100ms)
|
||||
# Universal Entrypoint: NVIDIA GPU Acceleration (CUDA 12.x) with CPU Fallback
|
||||
# ==============================================================================
|
||||
|
||||
print_banner() {
|
||||
echo "============================================================================"
|
||||
echo " FlyGuard · Блиц-обнаружение препятствий в тоннеле метро (Кейс 05, ЛЦТ-2026)"
|
||||
echo " Hesai Pandar128 (128 лучей, 10 Гц / 100 мс) · NVIDIA CUDA 12 / CPU Fallback"
|
||||
echo "============================================================================"
|
||||
}
|
||||
|
||||
print_device_info() {
|
||||
python3 -c "
|
||||
try:
|
||||
import flyguard.device as dev
|
||||
info = dev.get_device_info()
|
||||
if info['cuda_available']:
|
||||
print(f'==> [HARDWARE] NVIDIA GPU: {info[\"device_name\"]} (Compute {info[\"compute_capability\"]}, {info[\"total_memory_gb\"]} GB VRAM)')
|
||||
print(f'==> [ACCELERATION] CUDA 12: АКТИВНО (PyTorch {info[\"torch_version\"]})')
|
||||
else:
|
||||
print('==> [HARDWARE] CPU Mode: CUDA недоступна или не передана в контейнер.')
|
||||
print('==> [FALLBACK] Активирован автоматический Graceful Fallback на CPU.')
|
||||
except Exception as e:
|
||||
print(f'==> Проверка окружения: {e}')
|
||||
"
|
||||
}
|
||||
|
||||
CMD="${1:-default}"
|
||||
|
||||
case "$CMD" in
|
||||
default|run)
|
||||
print_banner
|
||||
print_device_info
|
||||
DATA_DIR="${FLYGUARD_DATA:-/data}"
|
||||
FOUND_BAG=$(find "$DATA_DIR" -name '*.db3' 2>/dev/null | head -n 1 || true)
|
||||
if [ -d "$DATA_DIR" ] && [ -n "$FOUND_BAG" ]; then
|
||||
echo "==> Обнаружены бэги лидара в $DATA_DIR. Запуск конвейера FlyGuard по всем записям..."
|
||||
exec python3 tools/run_pipeline.py --all \
|
||||
--memory artifacts/mushroom_body.npz \
|
||||
--readout artifacts/mbon_readout.npz \
|
||||
--device auto \
|
||||
--verbose
|
||||
else
|
||||
echo "==> В $DATA_DIR не найдены файлы .db3 (датасет не смонтирован через -v /путь/к/data:/data)."
|
||||
echo "==> Выполняется системный прогон верификационных тестов и роутинга..."
|
||||
exec python3 tests/run_tests.py
|
||||
fi
|
||||
;;
|
||||
pipeline|run-pipeline)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tools/run_pipeline.py \
|
||||
--memory artifacts/mushroom_body.npz \
|
||||
--readout artifacts/mbon_readout.npz \
|
||||
--device auto "$@"
|
||||
;;
|
||||
benchmark|make-benchmark)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tools/make_benchmark.py \
|
||||
--memory artifacts/mushroom_body.npz \
|
||||
--mbon-dir artifacts/mbon_folds \
|
||||
--device auto "$@"
|
||||
;;
|
||||
evaluate)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tools/evaluate.py \
|
||||
--mbon-dir artifacts/mbon_folds \
|
||||
--device auto "$@"
|
||||
;;
|
||||
train-mbon)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tools/train_mbon.py \
|
||||
--device auto "$@"
|
||||
;;
|
||||
train-track)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tools/train_track.py \
|
||||
--device auto "$@"
|
||||
;;
|
||||
test|tests|run-tests)
|
||||
shift
|
||||
print_banner
|
||||
print_device_info
|
||||
exec python3 tests/run_tests.py "$@"
|
||||
;;
|
||||
pytest)
|
||||
shift
|
||||
exec pytest tests "$@"
|
||||
;;
|
||||
info|device|gpu|gpu-info)
|
||||
print_banner
|
||||
python3 -c "
|
||||
import json
|
||||
import flyguard.device as dev
|
||||
print(json.dumps(dev.get_device_info(), indent=2, ensure_ascii=False))
|
||||
"
|
||||
;;
|
||||
bash|sh)
|
||||
shift
|
||||
exec /bin/bash "$@"
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
134
docker-run.sh
Executable file
134
docker-run.sh
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# FlyGuard: Удобный скрипт сборки и запуска Docker-контейнера
|
||||
# Автоматически определяет наличие NVIDIA GPU и передает флаги --gpus all
|
||||
# ==============================================================================
|
||||
set -e
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
IMAGE_NAME="flyguard:latest"
|
||||
DATA_DIR="${FLYGUARD_DATA:-$DIR/data}"
|
||||
ARTIFACTS_DIR="$DIR/artifacts"
|
||||
|
||||
mkdir -p "$DATA_DIR" "$ARTIFACTS_DIR"
|
||||
|
||||
# Определение наличия драйвера NVIDIA
|
||||
GPU_FLAG=""
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
GPU_FLAG="--gpus all"
|
||||
fi
|
||||
|
||||
usage() {
|
||||
echo "Использование: $0 [команда]"
|
||||
echo ""
|
||||
echo "Команды:"
|
||||
echo " build Собрать универсальный образ с поддержкой NVIDIA GPU ($IMAGE_NAME)"
|
||||
echo " build-cpu Собрать облегченный CPU-образ (flyguard:cpu)"
|
||||
echo " run Общий запуск конвейера (по умолчанию обработка всех бэгов)"
|
||||
echo " pipeline Запуск конвейера с дополнительными аргументами"
|
||||
echo " test Запуск всех 40+ тестов верификации"
|
||||
echo " benchmark Генерация бенчмарка с ускорением на GPU"
|
||||
echo " evaluate Оценка качества моделей и кросс-валидация"
|
||||
echo " train-mbon Обучение клеток Кеньона (50 000 KC) на GPU"
|
||||
echo " train-track Обучение классификатора треков"
|
||||
echo " info Вывод телеметрии и статуса GPU из контейнера"
|
||||
echo " shell Интерактивная командная строка (bash) внутри контейнера"
|
||||
echo ""
|
||||
echo "Примеры:"
|
||||
echo " $0 build"
|
||||
echo " $0 run"
|
||||
echo " $0 pipeline --all --verbose"
|
||||
echo " $0 train-mbon --device cuda --n-kc 50000"
|
||||
}
|
||||
|
||||
CMD="${1:-run}"
|
||||
|
||||
case "$CMD" in
|
||||
build)
|
||||
echo "==> Сборка универсального образа FlyGuard ($IMAGE_NAME)..."
|
||||
docker build -t "$IMAGE_NAME" -f "$DIR/Dockerfile" "$DIR"
|
||||
;;
|
||||
build-cpu)
|
||||
echo "==> Сборка облегченного CPU-образа FlyGuard (flyguard:cpu)..."
|
||||
docker build --target cpu -t flyguard:cpu -f "$DIR/Dockerfile" "$DIR"
|
||||
;;
|
||||
run|default)
|
||||
shift 2>/dev/null || true
|
||||
echo "==> Запуск контейнера FlyGuard (GPU: ${GPU_FLAG:-CPU Fallback})..."
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" "$@"
|
||||
;;
|
||||
pipeline)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" pipeline "$@"
|
||||
;;
|
||||
test|tests)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" test "$@"
|
||||
;;
|
||||
benchmark)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" benchmark "$@"
|
||||
;;
|
||||
evaluate)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" evaluate "$@"
|
||||
;;
|
||||
train-mbon)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" train-mbon "$@"
|
||||
;;
|
||||
train-track)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" train-track "$@"
|
||||
;;
|
||||
info|gpu|device)
|
||||
docker run --rm $GPU_FLAG "$IMAGE_NAME" info
|
||||
;;
|
||||
shell|bash)
|
||||
shift
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" bash "$@"
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
docker run --rm -it $GPU_FLAG \
|
||||
-v "$DATA_DIR":/data:ro \
|
||||
-v "$ARTIFACTS_DIR":/app/artifacts \
|
||||
--shm-size=8g \
|
||||
"$IMAGE_NAME" "$@"
|
||||
;;
|
||||
esac
|
||||
106
docs/DOCKER.md
Normal file
106
docs/DOCKER.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Руководство по запуску FlyGuard в Docker (NVIDIA GPU + Graceful CPU Fallback)
|
||||
|
||||
FlyGuard поставляется в виде универсального производственного Docker-образа с поддержкой аппаратного ускорения **NVIDIA GPU (CUDA 12.x)** и автоматического гибридного переключения на **CPU (Graceful Fallback)** при отсутствии или сбое графического процессора.
|
||||
|
||||
---
|
||||
|
||||
## 1. Быстрый запуск через helper-скрипт `docker-run.sh`
|
||||
|
||||
Скрипт [`docker-run.sh`](file:///home/wpng1337/Brainrot_Muxa/docker-run.sh) автоматически проверяет наличие `nvidia-smi` и подключает флаги `--gpus all`:
|
||||
|
||||
```bash
|
||||
# Сборка универсального образа (flyguard:latest)
|
||||
./docker-run.sh build
|
||||
|
||||
# Общий запуск конвейера (по всем бэгам в data/)
|
||||
./docker-run.sh run
|
||||
|
||||
# Проверка видимости GPU и драйвера внутри контейнера
|
||||
./docker-run.sh info
|
||||
|
||||
# Прогон всех 41 верификационных unit-тестов
|
||||
./docker-run.sh test
|
||||
|
||||
# Обучение клеток Кеньона (50 000 KC) на GPU
|
||||
./docker-run.sh train-mbon --device cuda --n-kc 50000
|
||||
|
||||
# Генерация бенчмарка дальности с аугментациями
|
||||
./docker-run.sh benchmark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Сборка образов напрямую через `docker build`
|
||||
|
||||
### Универсальный образ с GPU-ускорением (по умолчанию)
|
||||
```bash
|
||||
docker build -t flyguard:latest .
|
||||
```
|
||||
* **База**: `nvidia/cuda:12.4.1-runtime-ubuntu22.04`
|
||||
* **Python**: 3.11, PyTorch CUDA 12.1+, cuDNN, OpenMP `libgomp1`
|
||||
* **Поддерживаемые архитектуры**: Ada Lovelace (RTX 4070 Ti Super 16GB), Ampere, Hopper, Turing.
|
||||
* **Отказоустойчивость**: если запущен без `--gpus all`, прозрачно работает на CPU.
|
||||
|
||||
### Облегченный CPU-образ (опционально, ~200 МБ)
|
||||
```bash
|
||||
docker build --target cpu -t flyguard:cpu .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Запуск через Docker Compose
|
||||
|
||||
В [`docker-compose.yml`](file:///home/wpng1337/Brainrot_Muxa/docker-compose.yml) настроены готовые сервисы с автоматической резервацией GPU:
|
||||
|
||||
```bash
|
||||
# 1. Главный сервис: сквозная обработка данных лидара
|
||||
docker compose up
|
||||
# или
|
||||
docker compose run --rm pipeline
|
||||
|
||||
# 2. Проверка оборудования
|
||||
docker compose run --rm info
|
||||
|
||||
# 3. Полный набор unit-тестов
|
||||
docker compose run --rm test
|
||||
|
||||
# 4. Высокопроизводительное обучение MBON на 50k клеток
|
||||
docker compose run --rm train-mbon
|
||||
|
||||
# 5. Оценка метрик детекции и ложных тревог
|
||||
docker compose run --rm evaluate
|
||||
|
||||
# 6. Интерактивная отладка в bash
|
||||
docker compose run --rm shell
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Прямой запуск через `docker run`
|
||||
|
||||
### Запуск с аппаратным ускорением NVIDIA GPU:
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
--gpus all \
|
||||
-v /path/to/lidar/data:/data:ro \
|
||||
-v $(pwd)/artifacts:/app/artifacts \
|
||||
--shm-size=8g \
|
||||
flyguard:latest pipeline --all --verbose
|
||||
```
|
||||
|
||||
### Запуск на CPU (стенд жюри без GPU / fallback):
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
-v /path/to/lidar/data:/data:ro \
|
||||
-v $(pwd)/artifacts:/app/artifacts \
|
||||
--shm-size=4g \
|
||||
flyguard:latest pipeline --all --verbose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Архитектура Graceful Fallback на CPU
|
||||
|
||||
Конвейер использует гибридную схему вычислений:
|
||||
1. **Статический выбор**: при инициализации `device = "cuda" if is_cuda_available() else "cpu"`.
|
||||
2. **Динамический рантайм-откат**: если в процессе работы видеокарта возвращает ошибку (CUDA OOM, отвал драйвера), модуль [`flyguard/device.py`](file:///home/wpng1337/Brainrot_Muxa/flyguard/device.py) фиксирует сбой через `notify_cuda_error(exc)`, текущий кадр мгновенно досчитывается на CPU через NumPy/SciPy, а конвейер переходит в режим CPU без падения программы.
|
||||
254
docs/IMPROVEMENT_PLAN.md
Normal file
254
docs/IMPROVEMENT_PLAN.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# Комплексный план развития FlyGuard · Хакатон ЛЦТ-2026 (Кейс 05)
|
||||
|
||||
Документ фиксирует согласованную архитектурную стратегию и детальный план доработок ядра `FlyGuard` для максимизации качества решения на хакатоне ЛЦТ-2026 («Обнаружение посторонних объектов в тоннеле метро по данным 3D-лидара»).
|
||||
|
||||
---
|
||||
|
||||
## 1. Контекст и текущее состояние
|
||||
|
||||
Система [`FlyGuard`](../flyguard/pipeline.py) построена на принципах зрительной системы и грибовидных тел дрозофилы:
|
||||
```
|
||||
облако точек 0.3–0.9 млн, 10 Гц
|
||||
│
|
||||
├─ RETINA омматидиальная решётка → дальностный образ 128 × N
|
||||
├─ HALTERES плоскость рельсов: крен, тангаж, высота сенсора
|
||||
├─ LAMINA диспаритет 1/R → ON/OFF, центр-окружение на 3 масштабах
|
||||
├─ MEDULLA / LP T4/T5 → LPTC: скорость поезда без одометрии
|
||||
├─ LOBULA LC11: кандидаты; разрез компоненты по контрасту
|
||||
├─ MUSHROOM BODY KC → APL → MBON: новизна формы (без меток)
|
||||
│ + обученное считывание MBON (с метками из физики)
|
||||
├─ FAN-SHAPED BODY накопление лучей в координатах пути
|
||||
├─ CENTRAL COMPLEX накопление улик в координатах пути, треки
|
||||
└─ DESCENDING два порога с гистерезисом → решение
|
||||
```
|
||||
|
||||
### Текущие измеренные показатели (Ryzen 7600X, Hesai Pandar128):
|
||||
* **Реальный объект на 55 м (`doubleT_obstacle`)**: **99.5 %** кадров с детекцией.
|
||||
* **Задержка кадра на CPU**: **32–33 мс** при лимите 100 мс (запас ~3×).
|
||||
* **Ложные тревоги (leave-one-bag-out)**: **8.0 трека/км** (11.0 % кадров).
|
||||
* **Дальность обнаружения стоящего человека**: рабочая дальность **100 м**, P@50 = 0.70, **P@100 = 0.53, P@150 = 0.33**.
|
||||
|
||||
---
|
||||
|
||||
### Главные узкие места, выявленные в ходе анализа:
|
||||
|
||||
1. **Застревание улики трека на 120–160 м**:
|
||||
На 150 м кандидат появляется в ~62 % кадров, но от предмета приходит лишь 4–6 лучей. Покадровое считывание MBON выдаёт малое $p \approx 0.05$ (из-за априорной редкости), улика прирастает на $+gain \cdot w \approx +0.017$, а утечка за промах отнимает $-leak = -0.12$. В итоге $evidence$ колеблется около нуля, и порог $0.5$ никогда не пробивается.
|
||||
2. **Артефакты синтетики в [`TrackReadout`](../flyguard/track_readout.py)**:
|
||||
Признаки $s_{std}$ и $u_{std}$ перевернулись на реальном объекте ($0.365$ м у человека против $0.013$ м у фона) из-за того, что в синтетике предмет спавнился строго неподвижно относительно оценки движения, а в реальности поезд стоял, а человек двигался. Признаки $closed$ и $age$ заучили фиксированный спавн с 200 м. В текущей версии `track_blend = 0.0` (выключен).
|
||||
3. **Неиспользуемая неопределённость коридора [`Corridor.sigma`](../flyguard/geometry.py)**:
|
||||
Ось коридора на кривой за горизонтом видимости (62–107 м) продолжается по касательной и расходится с истинным путем до 1.19 м на 150 м. Полуширина габарита жестко зафиксирована на $1.6$ м, поэтому крайние предметы выпадают из маски габарита ещё до кластеризации.
|
||||
4. **Потеря кандидатов вдали (38 % пропусков на 120–160 м)**:
|
||||
Порог $min\_rays\_far = 3$ отсекает кадры с 1–2 лучами. При этом накопитель [`FanBody`](../flyguard/fan_body.py) за 15 кадров аккумулирует в вокселе десятки попаданий, но никак не помогает формированию покадрового кандидата.
|
||||
5. **Скачок ложных тревог на новой линии ($8.0 \to 20.3$ треков/км)**:
|
||||
На незнакомом участке без обученной долговременной памяти штатные лотки и кронштейны получают высокую новизну. Габарит сейчас однороден и одинаково доверяет предметам на путях и конструкциям на стенах.
|
||||
6. **Слипание с платформой на 30–50 м**:
|
||||
При смещении предмета $+0.9$ м к кромке платформы он объединяется с ней в связную компоненту длиной $>15$ м и выбрасывается правилом $max\_depth$.
|
||||
7. **Отсутствие лежащего человека**:
|
||||
В [`catalogue`](../flyguard/synth.py) нет класса лежащего человека, а нижняя граница габарита $h_{lo} = 0.28$ м срезает упавшего на рельсы пассажира.
|
||||
|
||||
---
|
||||
|
||||
## 2. Поэтапный план улучшений
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ФАЗА 1: Устранение артефактов данных и синтетики │
|
||||
│ • Аугментация спавна, движения, стоянки в make_benchmark │
|
||||
│ • Добавление «человек_лежа» (1.8×0.5×0.3 м) │
|
||||
│ • Честное переобучение и валидация TrackReadout │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ФАЗА 2: Пробитие дальней зоны (100–160 м) │
|
||||
│ • Двухканальное решение в DescendingNeurons (soft-warning) │
|
||||
│ • Динамическое расширение габарита по Corridor.sigma(d) │
|
||||
│ • Обратная связь FanBody → Lobula (порог 2 луча по опоре) │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ФАЗА 3: Обобщаемость на новый тоннель и ближняя зона │
|
||||
│ • Иерархический габарит (опасная колея vs стены/лотки) │
|
||||
│ • Вычитание плоскости платформы (h~1.1 м, |u|>1.2 м) │
|
||||
│ • Понижение h_lo до 0.16 м в межрельсовой зоне │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ФАЗА 4: ROS2 Экспорт, Замеры и Протокол Стенда Жюри │
|
||||
│ • Модуль экспорта: 3D Bounding Boxes, Threat Level, RViz │
|
||||
│ • Сквозной Leave-one-bag-out замер и стресс-тест на CPU │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Фаза 1: Устранение артефактов данных и синтетики
|
||||
|
||||
**Цель:** сделать выборку для обучения [`TrackReadout`](../flyguard/track_readout.py) инвариантной к реальным условиям, исключить переобучение на искусственные признаки генератора.
|
||||
|
||||
#### 1.1. Аугментация генерации в [`tools/make_benchmark.py`](../tools/make_benchmark.py)
|
||||
* **Случайная дальность спавна**: вместо жесткой фиксации на $d_{start} = 200$ м задавать $d_{start} \sim \mathcal{U}(40, 200)$ м. Это разрушает фиктивную корреляцию между истинностью объекта и признаками $closed = \ln(d_{start}/d)$ и $age$.
|
||||
* **Поперечное и продольное блуждание объекта**:
|
||||
* Случайная скорость движения человека: $v_{lat} \sim \mathcal{U}(-0.4, 0.4)$ м/с.
|
||||
* Реалистичный шум положения $\delta s \sim \mathcal{N}(0, \sigma_s^2)$ с $\sigma_s = 0.15$ м (моделирует погрешность одометрии и скольжение лучей по телу).
|
||||
* Благодаря этому $s_{std}$ и $u_{std}$ перестают быть нулевыми для синтетических объектов.
|
||||
* **Сценарии со стоящим поездом**:
|
||||
* Генерация препятствий в кадры со стоящим поездом ($v = 0$, например, кадры бэга `doubleT_obstacle`), чтобы модель не считала нулевую скорость признаком отсутствия опасности.
|
||||
|
||||
#### 1.2. Добавление класса «человек_лежа» в [`catalogue`](../flyguard/synth.py)
|
||||
* Добавить геометрию:
|
||||
```python
|
||||
"человек_лежа": ObjectModel("человек_лежа", [
|
||||
Box(length=1.80, width=0.50, height=0.30, h_base=0.0)
|
||||
], reflectivity=0.35)
|
||||
```
|
||||
* В [`_quality`](../flyguard/central_complex.py) учесть, что вытянутость вдоль пути ($depth \approx 1.8$ м) при малой высоте ($h \le 0.35$ м) и расположении в межрельсовой зоне ($|u| \le 0.75$ м) не должна штрафоваться множителем $compact$.
|
||||
|
||||
#### 1.3. Переобучение [`TrackReadout`](../tools/train_track.py)
|
||||
* Собрать обновлённый кэш треков:
|
||||
```bash
|
||||
python tools/make_benchmark.py --memory artifacts/mushroom_body.npz \
|
||||
--mbon-dir artifacts/mbon_folds --tracks-out data/cache/track_set_aug.npz
|
||||
```
|
||||
* Обучить [`TrackReadout`](../tools/train_track.py) с контролем ROC AUC по всем полосам (целевой AUC $>0.90$ на полосе 110–160 м).
|
||||
* Проверить валидацию на `doubleT_obstacle`: оценка $p_{track}$ для реального объекта должна составлять $> 0.95$.
|
||||
|
||||
---
|
||||
|
||||
### Фаза 2: Пробитие дальней зоны (100–160 м)
|
||||
|
||||
**Цель:** гарантировать, что появляющиеся на 120–160 м кандидаты и треки превращаются в надежное раннее предупреждение, не застревая в утечке $leak$.
|
||||
|
||||
#### 2.1. Двухканальное решение в [`DescendingNeurons`](../flyguard/descending.py)
|
||||
Биологическая аналогия: разделение Giant Fiber (DNp01, жесткий аварийный порог) и DNp02/DNp11 (раннее предупреждение).
|
||||
|
||||
```python
|
||||
# 1. Аварийный канал (Giant Fiber / DNp01) — экстренное торможение
|
||||
is_emergency = (t.evidence >= self.emergency_evidence) and (d <= stopping_distance)
|
||||
|
||||
# 2. Канал раннего предупреждения (DNp02 / DNp11) — ранний soft-warning
|
||||
is_far_warning = (d > 90.0) and (t.hits >= 2) and (t.p_track >= 0.65)
|
||||
is_near_warning = (d <= 90.0) and (t.evidence >= self.warn_evidence)
|
||||
|
||||
decision.detected = is_emergency or is_far_warning or is_near_warning
|
||||
decision.emergency = is_emergency
|
||||
```
|
||||
|
||||
* **Безопасность**: далёкий ложный трек физически не может вызвать экстренное торможение (`emergency = False`), но даёт заблаговременное оповещение на 150 м.
|
||||
|
||||
#### 2.2. Динамический габарит по [`Corridor.sigma(d)`](../flyguard/geometry.py)
|
||||
В [`geometry.py`](../flyguard/geometry.py) уже заложен расчет неопределенности оси:
|
||||
$$\sigma(d) = base + rate \cdot d + 0.02 \cdot \max(d - d_{max\_seen}, 0)$$
|
||||
На 150 м $\sigma(d) \approx 1.1$ м.
|
||||
|
||||
В функции `gauge_mask` в [`flyguard/lobula.py`](../flyguard/lobula.py):
|
||||
$$W_{eff}(d) = half\_width + k_\sigma \cdot \sigma(d), \quad k_\sigma = 0.8$$
|
||||
* На 50 м: $W_{eff} = 1.6 + 0.8 \cdot 0.45 \approx 1.96$ м.
|
||||
* На 150 м: $W_{eff} = 1.6 + 0.8 \cdot 1.10 \approx 2.48$ м.
|
||||
* На кривых радиуса 1300 м объект у внешнего рельса больше не вываливается за границу габарита. Стены тоннеля, попадающие в расширенную зону, отсекаются признаками $containment$ и низким $p_{mbon}$.
|
||||
|
||||
#### 2.3. Обратная связь [`FanBody`](../flyguard/fan_body.py) $\to$ [`find_candidates`](../flyguard/lobula.py)
|
||||
[`FanBody`](../flyguard/fan_body.py) аккумулирует лучи в воксельной сетке пути $2.0 \times 0.2 \times 0.25$ м.
|
||||
В [`find_candidates`](../flyguard/lobula.py) вводится адаптивный порог числа лучей:
|
||||
```python
|
||||
# Если в данной точке пространства накопитель уже видит стабильную опору:
|
||||
support = fan.support_at(d_comp, u_comp) if fan is not None else 0.0
|
||||
need_rays = 2 if (d_comp >= 90.0 and support >= 0.6) else (3 if d_comp >= 90.0 else 4)
|
||||
```
|
||||
* Снижает пропуски кандидатов на 120–160 м с 38 % до $< 15$ % для слабых эхо.
|
||||
|
||||
---
|
||||
|
||||
### Фаза 3: Обобщаемость на незнакомой линии и ближняя зона
|
||||
|
||||
**Цель:** снизить ложные тревоги на незнакомой линии с 20.3 до $<10$ треков/км и закрыть потерю объектов возле платформ на 30–50 м.
|
||||
|
||||
#### 3.1. Иерархический двухзонный габарит (Core vs Peripheral)
|
||||
Сечение пути разделяется на две зоны ответственности:
|
||||
|
||||
$$\text{Зона 1 (Колея)}: |u| \le 0.90\,\text{м}, \quad h \in [0.16, 1.90]\,\text{м}$$
|
||||
$$\text{Зона 2 (Периферия)}: 0.90 < |u| \le 1.60\,\text{м} \quad \text{или} \quad h > 1.90\,\text{м}$$
|
||||
|
||||
1. **Для Зоны 1 (Колея)**:
|
||||
* Нижний порог $h_{lo}$ снижается до **$0.16$ м** (захватывает лежащего человека).
|
||||
* Множитель компактности в [`_quality`](../flyguard/central_complex.py) не штрафует протяженность $depth$ до $2.0$ м при малой высоте ($h \le 0.35$ м).
|
||||
* Максимальный приоритет подтверждения трека ($gain = 0.40$).
|
||||
2. **Для Зоны 2 (Периферия)**:
|
||||
* Пространство кабельных лотков, сигналов и кронштейнов.
|
||||
* Требование опоры на полотно: $grounded \le 0.3$ для висящих на стенах объектов подавляет их прирост улики.
|
||||
* Для формирования трека требуется $p_{mbon} \ge 0.50$.
|
||||
* **Результат**: на незнакомой линии ложные срабатывания от кабелей и ниш падают с $20.3$ до $< 10.0$ на км.
|
||||
|
||||
#### 3.2. Геометрическая сегментация платформы на 30–50 м
|
||||
Платформа в метро имеет стандартизованные параметры: высота $1.10 \pm 0.05$ м над рельсом, удаление $|u| \ge 1.35$ м.
|
||||
В [`find_candidates`](../flyguard/lobula.py):
|
||||
```python
|
||||
# Маска горизонтальной поверхности платформы:
|
||||
platform_mask = (np.abs(lat) > 1.30) & (tf.h >= 1.05) & (tf.h <= 1.25)
|
||||
# Исключаем лучи настила платформы из контекста связности:
|
||||
clustering_context = context & ~platform_mask
|
||||
```
|
||||
* Препятствие у края габарита ($u = +0.9$ м) больше не сшивается с настилом платформы в длинную 20-метровую стену и не отбрасывается по $max\_depth$.
|
||||
|
||||
---
|
||||
|
||||
### Фаза 4: ROS2-интеграция, API и валидация
|
||||
|
||||
**Цель:** подготовить модуль для интеграции с ребятами, делающими ROS2-узел, сохранив ядро 100 % независимым и легковесным (чистый CPU, NumPy/SciPy).
|
||||
|
||||
#### 4.1. Модуль экспорта для ROS2 ([`flyguard/export.py`](../flyguard/pipeline.py))
|
||||
Обеспечивает чистый интерфейс между ядром и ROS2-нодой:
|
||||
* Преобразование треков [`Track`](../flyguard/central_complex.py) в 3D Bounding Boxes в координатах сенсора `(x, y, z, dx, dy, dz, yaw)`.
|
||||
* Расчет метрик безопасности:
|
||||
* `distance_along_track` ($d$, м)
|
||||
* `lateral_offset` ($u$, м)
|
||||
* `ttc` (Time to Collision, с)
|
||||
* `threat_level`: `CLEAR`, `WARNING`, `EMERGENCY`.
|
||||
* Генератор структуры для публикации `visualization_msgs/MarkerArray`:
|
||||
* Bounding boxes с цветовым кодированием (красный — авария, желтый — предупреждение).
|
||||
* 3D линия оси коридора тоннеля.
|
||||
* Текстовые метки с расстоянием и скоростью сближения над объектами.
|
||||
|
||||
#### 4.2. Контроль рантайма на CPU
|
||||
* **Зависимости**: строго `numpy` и `scipy` в рантайме.
|
||||
* **Бюджет времени**:
|
||||
* Новые операции (lookup в FanBody, проверка 2 зон, маска платформы) выполняются на готовых массивах NumPy и добавляют $< 1.2$ мс.
|
||||
* Ожидаемая медиана на Ryzen 7600X: **$\approx 34$ мс**, на стенде жюри (i7-9700E): **$\approx 70–75$ мс** при лимите **100 мс**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Сводная таблица ожидаемых метрик
|
||||
|
||||
| Метрика | До улучшений | После реализации | Ключевой фактор роста |
|
||||
|---|:---:|:---:|---|
|
||||
| **Реальный объект на 55 м (`doubleT_obstacle`)** | 99.5 % | **> 99.5 %** | Сохранение надежности канала Giant Fiber |
|
||||
| **P@150 м (стоящий человек)** | 0.33 | **> 0.50** | Двухканальное решение по $p_{track}$ + $\sigma(d)$ |
|
||||
| **P@100 м (сидящий человек / чемодан)** | 0.34 / 0.21 | **> 0.55 / 0.40** | Порог 2 лучей при накоплении в [`FanBody`](../flyguard/fan_body.py) |
|
||||
| **P@50 м (лежащий человек)** | 0.0 % (срезался) | **> 0.75** | $h_{lo} = 0.16$ м в колее + класс в каталоге |
|
||||
| **Ложные тревоги (незнакомая линия)** | 20.3 на км | **< 10.0 на км** | Двухзонный габарит (периферия фильтрует лотки) |
|
||||
| **Обнаружение у платформ (30–50 м)** | 66 % (потеря 34 %) | **> 90 %** | Сегментация настила платформы до кластеризации |
|
||||
| **Время кадра (CPU)** | 33 мс | **< 36 мс** | Векторизованный NumPy/SciPy без оверхеда |
|
||||
|
||||
---
|
||||
|
||||
## 4. Дорожная карта выполнения (Action Checklist)
|
||||
|
||||
- [ ] **Шаг 1 (Данные и синтетика)**:
|
||||
* [x] В [`flyguard/synth.py`](../flyguard/synth.py) добавлен класс `человек_лежа`.
|
||||
* [x] В [`flyguard/central_complex.py`](../flyguard/central_complex.py) скорректирован расчет `compact` в `_quality` для лежащих на путях объектов.
|
||||
* [ ] В [`tools/make_benchmark.py`](../tools/make_benchmark.py) добавить случайный $d_{start}$, поперечный дрейф $v_{lat}$, шум $\delta s$ и статичные кадры.
|
||||
* [ ] Пересобрать кэш `track_set.npz` и переобучить [`TrackReadout`](../tools/train_track.py).
|
||||
- [x] **Шаг 2 (Дальняя зона)**:
|
||||
* [x] В [`flyguard/descending.py`](../flyguard/descending.py) имплементирован двухканальный триггер soft-warning (DNp02/DNp11) для $d > 90$ м.
|
||||
* [x] В [`flyguard/lobula.py`](../flyguard/lobula.py) применён динамический габарит $W_{eff}(d) = half\_width + k_\sigma \cdot \max(\sigma(d) - 0.25, 0.0)$.
|
||||
* [x] Оптимизированы параметры `warn_far = 0.35` и `leak_far = 0.06` для предотвращения вымывания улики на дальности.
|
||||
- [x] **Шаг 3 (Зонирование и платформы)**:
|
||||
* [x] Двухзонный габарит: колея ($|u| \le 0.85$ м, $h_{lo\_core} = 0.16$ м) и периферия ($h_{lo} = 0.28$ м).
|
||||
* [x] Геометрическая фильтрация настила платформ ($|u| \ge 1.30$ м, $h \in [1.05, 1.25]$ м) для устранения слипания объектов со стенкой.
|
||||
- [ ] **Шаг 4 (ROS2 и валидация)**:
|
||||
* [x] Создан модуль экспорта [`flyguard/export.py`](../flyguard/export.py): расчет `BoundingBox3D`, `ThreatLevel` (`CLEAR`/`WARNING`/`EMERGENCY`), `TTC` и генерация структур для `visualization_msgs/MarkerArray`.
|
||||
* [x] Добавлены и успешно пройдены 35 модульных тестов (`tests/test_pipeline.py`).
|
||||
* [ ] Запустить сквозную валидацию и зафиксировать метрики в `docs/EXPERIMENTS.md`.
|
||||
10
requirements-gpu.txt
Normal file
10
requirements-gpu.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Зависимости для запуска FlyGuard с поддержкой NVIDIA GPU (CUDA 12.x / Ada Lovelace RTX 4070 Ti Super)
|
||||
--extra-index-url https://download.pytorch.org/whl/cu121
|
||||
numpy>=1.24
|
||||
scipy>=1.10
|
||||
torch>=2.2.0
|
||||
torchvision>=0.17.0
|
||||
lightgbm>=4.0
|
||||
catboost>=1.2
|
||||
matplotlib>=3.7
|
||||
pytest>=7.0
|
||||
83
tests/run_tests.py
Normal file
83
tests/run_tests.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Автономный запуск тестов без внешних зависимостей (включая pytest)."""
|
||||
import inspect
|
||||
import math
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
class PytestMock:
|
||||
class raises:
|
||||
def __init__(self, exc_type, match=None):
|
||||
self.exc_type = exc_type
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if exc_type is None:
|
||||
raise AssertionError(f"Expected {self.exc_type} but nothing was raised")
|
||||
return issubclass(exc_type, self.exc_type)
|
||||
|
||||
@staticmethod
|
||||
def approx(expected, rel=1e-6, abs=1e-6):
|
||||
class Approx:
|
||||
def __eq__(self, other):
|
||||
return math.isclose(other, expected, rel_tol=rel, abs_tol=abs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"approx({expected})"
|
||||
|
||||
return Approx()
|
||||
|
||||
class mark:
|
||||
@staticmethod
|
||||
def skipif(cond, reason=""):
|
||||
def dec(f):
|
||||
f.__skip__ = cond
|
||||
return f
|
||||
|
||||
return dec
|
||||
|
||||
|
||||
sys.modules.setdefault("pytest", PytestMock())
|
||||
|
||||
import tests.test_pipeline as tp
|
||||
|
||||
|
||||
def main():
|
||||
passed = failed = skipped = 0
|
||||
test_names = sorted([n for n in dir(tp) if n.startswith("test_")])
|
||||
for name in test_names:
|
||||
fn = getattr(tp, name)
|
||||
if getattr(fn, "__skip__", False):
|
||||
print(f"SKIP {name}")
|
||||
skipped += 1
|
||||
continue
|
||||
try:
|
||||
sig = inspect.signature(fn)
|
||||
kwargs = {}
|
||||
if "tmp_path" in sig.parameters:
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
kwargs["tmp_path"] = Path(td)
|
||||
fn(**kwargs)
|
||||
else:
|
||||
fn()
|
||||
print(f"PASS {name}")
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f"FAIL {name}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\nИтог: {passed} passed, {failed} failed, {skipped} skipped")
|
||||
if failed > 0:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
216
tools/flyguard_ros2_node.py
Normal file
216
tools/flyguard_ros2_node.py
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
"""
|
||||
ROS 2 узел для биоинспирированного пайплайна FlyGuard (ЛЦТ-2026 Кейс 05).
|
||||
Связывает входные данные 3D-лидара с ядром FlyGuard и публикует детекции/маркеры.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
# Автоматически добавляем корень репозитория в sys.path
|
||||
ROOT_DIR = Path(__file__).resolve().parent.parent
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
|
||||
# Импорт сообщений ROS 2
|
||||
from sensor_msgs.msg import PointCloud2
|
||||
from std_msgs.msg import String
|
||||
from vision_msgs.msg import Detection3DArray, Detection3D, BoundingBox3D
|
||||
from visualization_msgs.msg import MarkerArray, Marker
|
||||
from geometry_msgs.msg import Vector3
|
||||
|
||||
import sensor_msgs_py.point_cloud2 as pc2
|
||||
|
||||
# Импорт ядра FlyGuard
|
||||
try:
|
||||
from flyguard.pipeline import FlyGuard, Params
|
||||
from flyguard.mushroom_body import MushroomBody
|
||||
except ImportError as e:
|
||||
FlyGuard = None
|
||||
Params = None
|
||||
MushroomBody = None
|
||||
|
||||
|
||||
class FlyGuardNode(Node):
|
||||
def __init__(self):
|
||||
super().__init__('flyguard_node')
|
||||
|
||||
# --- Объявление параметров ---
|
||||
self.declare_parameter('lidar_topic', '/pandar_points')
|
||||
self.declare_parameter('frame_id', 'pandar')
|
||||
self.declare_parameter('memory_path', '')
|
||||
self.declare_parameter('fov_deg', 30.0)
|
||||
|
||||
self.lidar_topic = self.get_parameter('lidar_topic').get_parameter_value().string_value
|
||||
self.frame_id = self.get_parameter('frame_id').get_parameter_value().string_value
|
||||
memory_path = self.get_parameter('memory_path').get_parameter_value().string_value
|
||||
fov_deg = self.get_parameter('fov_deg').get_parameter_value().double_value
|
||||
|
||||
# --- Инициализация ядра FlyGuard ---
|
||||
if FlyGuard is not None:
|
||||
self.get_logger().info("Инициализация ядра FlyGuard...")
|
||||
memory = MushroomBody.load(memory_path) if memory_path else None
|
||||
params = Params(fov_deg=fov_deg)
|
||||
self.fg = FlyGuard(params, memory=memory)
|
||||
else:
|
||||
self.get_logger().error("Модуль flyguard.pipeline не найден!")
|
||||
self.fg = None
|
||||
|
||||
# --- Подписчики и Публикаторы ---
|
||||
self.sub_cloud = self.create_subscription(
|
||||
PointCloud2,
|
||||
self.lidar_topic,
|
||||
self.pointcloud_callback,
|
||||
10
|
||||
)
|
||||
|
||||
self.pub_threat = self.create_publisher(String, 'flyguard/threat_level', 10)
|
||||
self.pub_boxes = self.create_publisher(Detection3DArray, 'flyguard/bounding_boxes', 10)
|
||||
self.pub_markers = self.create_publisher(MarkerArray, 'flyguard/markers', 10)
|
||||
|
||||
self.get_logger().info(f"FlyGuard ROS 2 Node запущен! Топик: {self.lidar_topic}")
|
||||
|
||||
def pointcloud_callback(self, msg: PointCloud2):
|
||||
"""Обработка каждого кадра лидара (10 Гц)"""
|
||||
start_time = self.get_clock().now()
|
||||
|
||||
# 1. Читаем точки из PointCloud2 в structured numpy array
|
||||
# (сохраняет стандартную структуру PointCloud)
|
||||
pc_data = pc2.read_points(msg, skip_nans=True)
|
||||
if pc_data.size == 0:
|
||||
return
|
||||
|
||||
# 2. Вызываем ядро FlyGuard
|
||||
if self.fg is None:
|
||||
return
|
||||
|
||||
res = self.fg.process(pc_data)
|
||||
if res is None:
|
||||
return
|
||||
|
||||
d = res.decision
|
||||
tracks = getattr(self.fg.cx, 'tracks', [])
|
||||
|
||||
# 3. Определяем уровень угрозы
|
||||
if d.detected:
|
||||
threat_level = "EMERGENCY" if getattr(d, 'emergency', False) else "WARNING"
|
||||
else:
|
||||
threat_level = "CLEAR"
|
||||
|
||||
# 4. Публикация статуса
|
||||
threat_msg = String()
|
||||
threat_msg.data = threat_level
|
||||
self.pub_threat.publish(threat_msg)
|
||||
|
||||
# 5. Публикация маркеров и рамок
|
||||
stamp = msg.header.stamp
|
||||
self.publish_detections(tracks, stamp)
|
||||
self.publish_rviz_markers(tracks, threat_level, d, stamp)
|
||||
|
||||
elapsed_ms = (self.get_clock().now() - start_time).nanoseconds / 1e6
|
||||
self.get_logger().debug(f"Кадр за {elapsed_ms:.1f} мс | Статус: {threat_level} | Треков: {len(tracks)}")
|
||||
|
||||
def publish_detections(self, tracks, stamp):
|
||||
"""Формирование vision_msgs/Detection3DArray"""
|
||||
det_array = Detection3DArray()
|
||||
det_array.header.stamp = stamp
|
||||
det_array.header.frame_id = self.frame_id
|
||||
|
||||
for trk in tracks:
|
||||
det = Detection3D()
|
||||
det.header = det_array.header
|
||||
|
||||
bbox = BoundingBox3D()
|
||||
bbox.center.position.x = float(getattr(trk, 'x', 0.0))
|
||||
bbox.center.position.y = float(getattr(trk, 'y', 0.0))
|
||||
bbox.center.position.z = float(getattr(trk, 'z', 0.0))
|
||||
|
||||
bbox.size.x = float(getattr(trk, 'dx', 0.5))
|
||||
bbox.size.y = float(getattr(trk, 'dy', 0.5))
|
||||
bbox.size.z = float(getattr(trk, 'dz', 1.5))
|
||||
|
||||
det.bbox = bbox
|
||||
det_array.detections.append(det)
|
||||
|
||||
self.pub_boxes.publish(det_array)
|
||||
|
||||
def publish_rviz_markers(self, tracks, threat_level: str, decision, stamp):
|
||||
"""Генерация маркеров для RViz2"""
|
||||
marker_array = MarkerArray()
|
||||
|
||||
delete_all = Marker()
|
||||
delete_all.action = Marker.DELETEALL
|
||||
marker_array.markers.append(delete_all)
|
||||
|
||||
for i, trk in enumerate(tracks):
|
||||
# Куб вокруг объекта
|
||||
box_marker = Marker()
|
||||
box_marker.header.stamp = stamp
|
||||
box_marker.header.frame_id = self.frame_id
|
||||
box_marker.ns = "flyguard_boxes"
|
||||
box_marker.id = i
|
||||
box_marker.type = Marker.CUBE
|
||||
box_marker.action = Marker.ADD
|
||||
|
||||
x = float(getattr(trk, 'x', 0.0))
|
||||
y = float(getattr(trk, 'y', 0.0))
|
||||
z = float(getattr(trk, 'z', 0.0))
|
||||
|
||||
box_marker.pose.position.x = x
|
||||
box_marker.pose.position.y = y
|
||||
box_marker.pose.position.z = z
|
||||
|
||||
dx = float(getattr(trk, 'dx', 0.5))
|
||||
dy = float(getattr(trk, 'dy', 0.5))
|
||||
dz = float(getattr(trk, 'dz', 1.5))
|
||||
box_marker.scale = Vector3(x=dx, y=dy, z=dz)
|
||||
|
||||
if threat_level == "EMERGENCY":
|
||||
box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 1.0, 0.0, 0.0, 0.6
|
||||
elif threat_level == "WARNING":
|
||||
box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 1.0, 0.9, 0.0, 0.5
|
||||
else:
|
||||
box_marker.color.r, box_marker.color.g, box_marker.color.b, box_marker.color.a = 0.0, 1.0, 0.0, 0.4
|
||||
|
||||
marker_array.markers.append(box_marker)
|
||||
|
||||
# Текст с дистанцией над объектом
|
||||
text_marker = Marker()
|
||||
text_marker.header.stamp = stamp
|
||||
text_marker.header.frame_id = self.frame_id
|
||||
text_marker.ns = "flyguard_labels"
|
||||
text_marker.id = 1000 + i
|
||||
text_marker.type = Marker.TEXT_VIEW_FACING
|
||||
text_marker.action = Marker.ADD
|
||||
|
||||
text_marker.pose.position.x = x
|
||||
text_marker.pose.position.y = y
|
||||
text_marker.pose.position.z = z + dz / 2.0 + 0.3
|
||||
text_marker.scale.z = 0.4
|
||||
|
||||
dist = getattr(trk, 'distance', np.hypot(x, y))
|
||||
text_marker.text = f"D: {dist:.1f}m"
|
||||
|
||||
text_marker.color.r, text_marker.color.g, text_marker.color.b, text_marker.color.a = 1.0, 1.0, 1.0, 1.0
|
||||
marker_array.markers.append(text_marker)
|
||||
|
||||
self.pub_markers.publish(marker_array)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
node = FlyGuardNode()
|
||||
try:
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in a new issue