A complete pipeline for multi-object detection and tracking of laparoscopic surgical instruments using the CholecTrack20 dataset, YOLOv10, BoT-SORT, and a custom Memory Bank for out-of-body re-identification.
Forschungsprojekt/
├── requirements.txt # Python dependencies
├── prepare_data.py # JSON → YOLO labels, image symlinks
├── cholectrack20.yaml # Ultralytics dataset config
├── train.py # Multi-GPU YOLOv10 training
├── track.py # Inference + BoT-SORT + Memory Bank ReID
├── README.md # This file
│
├── logs/ # nohup output logs (created on first run)
│ ├── prepare_data_trainval.log
│ ├── prepare_data_test.log
│ ├── train.log
│ ├── track_VID01.log
│ └── ...
│
├── data/ # Generated by prepare_data.py
│ ├── images/
│ │ ├── train/ # Symlinks to scratch3 PNG frames
│ │ ├── val/ # Symlinks to scratch3 PNG frames
│ │ └── test/ # Extracted frames from .mp4 (test split)
│ └── labels/
│ ├── train/ # YOLO-format .txt label files
│ ├── val/
│ └── test/
│
└── runs/ # Created by Ultralytics during training
└── detect/
└── cholectrack20/
└── weights/
├── best.pt
└── last.pt
| ID | Name | Description |
|---|---|---|
| 0 | grasper | Most common; can appear ≥2 at once |
| 1 | bipolar | Cauterisation forceps |
| 2 | hook | Electrosurgical hook |
| 3 | scissors | Curved / straight micro-scissors |
| 4 | clipper | Clip applier (haemostatic clips) |
| 5 | irrigator | Irrigation / suction cannula |
| 6 | specimen_bag | Endobag / specimen retrieval bag |
- 3× NVIDIA RTX 4090 (24 GB VRAM each) on a shared cluster
- CUDA ≥ 12.1, cuDNN ≥ 8.9
- Python ≥ 3.10 (cluster has Python 3.13.11 – confirmed compatible)
# Navigate to the project root
cd /graphics/scratch2/students/eichler/Forschungsprojekt
# Create a Python 3.10+ virtual environment
python3 -m venv .venv
# Activate it (must be done in every new terminal session)
source .venv/bin/activate
# Verify Python version
python --versionNote: All subsequent commands assume the virtual environment is activated.
# Upgrade pip first
pip install --upgrade pip setuptools wheel
# Step 1: install everything except boxmot
pip install -r requirements.txt
# Step 2: install boxmot WITHOUT dependency resolution
# (boxmot 12.x metadata pins numpy==1.26.4 which has no cp313 wheel;
# its runtime code works fine with numpy 2.x that was installed above)
pip install boxmot --no-deps
# Optional: verify GPU access
python -c "import torch; print(torch.cuda.device_count(), 'GPU(s) found')"
# Expected output: 3 GPU(s) foundTroubleshoot: If
torchcannot find CUDA, install the correct CUDA-specific wheel:pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
Hinweis zu
setsid: Alle langen Prozesse werden mitsetsid python ...gestartet.setsiderstellt eine neue Session ohne kontrollierende TTY, so dass kein SIGHUP beim SSH-Disconnect gesendet wird – weder an den Hauptprozess noch an gespawnte Subprozesse (z. B.torchrunbeim DDP-Training).
prepare_data.py reads the read-only CholecTrack20 JSON annotations,
converts bounding boxes to YOLO format, writes them as .txt label files
in your working directory, and creates symbolic links for the images so
that no data is copied.
mkdir -p logs
setsid python prepare_data.py \
--dataset_root /graphics/scratch3/datasets/cholectrack20 \
--work_dir /graphics/scratch2/students/eichler/Forschungsprojekt \
--splits train val \
> logs/prepare_data_trainval.log 2>&1 &
echo "PID $! – tail -f logs/prepare_data_trainval.log"Follow progress:
tail -f logs/prepare_data_trainval.logExpected console output (abbreviated):
Processing split: 'train' (Training)
[VID02] frames= 2545 boxes= 3812 skipped= 0
[VID04] frames= 2565 boxes= 4100 skipped= 0
...
Processing split: 'val' (Validation)
[VID110] frames= 2421 boxes= 3500 skipped= 0
[VID30] frames= 1800 boxes= 2700 skipped= 0
...
Total frames processed : <N>
Total boxes written : <M>
Done.
The test split has no pre-extracted Frames – the script decodes them from .mp4:
mkdir -p logs
setsid python prepare_data.py \
--dataset_root /graphics/scratch3/datasets/cholectrack20 \
--work_dir /graphics/scratch2/students/eichler/Forschungsprojekt \
--splits test \
--extract_test_fps 1 \
> logs/prepare_data_test.log 2>&1 &
echo "PID $! – tail -f logs/prepare_data_test.log"Follow progress:
tail -f logs/prepare_data_test.logDisk space: Each extracted frame is ~300–500 KB (PNG).
8 test videos × ~2 500 frames each ≈ 10–20 GB.
# Count label and image files per split
echo "Train images: $(ls data/images/train | wc -l)"
echo "Train labels: $(ls data/labels/train | wc -l)"
echo "Val images: $(ls data/images/val | wc -l)"
echo "Val labels: $(ls data/labels/val | wc -l)"
# Verify that symlinks resolve correctly
ls -la data/images/train | head -5mkdir -p logs
setsid python train.py \
--model yolov10x.pt \
--data cholectrack20.yaml \
--epochs 100 \
--imgsz 640 \
--batch 48 \
--devices 0,1,2 \
--workers 8 \
--project runs/detect \
--name cholectrack20 \
> logs/train.log 2>&1 &
echo "PID $! – tail -f logs/train.log"Follow progress:
tail -f logs/train.logArgument reference:
| Argument | Default | Description |
|---|---|---|
--model |
yolov10x.pt |
Pre-trained YOLO weights (auto-downloaded) |
--data |
cholectrack20.yaml |
Dataset config |
--epochs |
500 |
Total epochs |
--imgsz |
640 |
Input resolution (px²) |
--batch |
48 |
Global batch (16 per GPU) |
--devices |
0,1,2 |
GPU device IDs |
--workers |
8 |
DataLoader workers per GPU |
--patience |
50 |
Early-stopping epochs |
--fl_gamma |
2.0 |
Focal loss gamma (higher = more focus on rare/hard classes) |
--lr0 |
0.01 |
Initial learning rate |
--lrf |
0.001 |
Final LR (fraction of lr0) |
--warmup_epochs |
3 |
Cosine warmup duration |
--name |
cholectrack20 |
Sub-folder inside --project |
--resume |
(none) | Resume from a checkpoint |
setsid python train.py \
--resume runs/detect/cholectrack20/weights/last.pt \
--devices 0,1,2 \
> logs/train_resume.log 2>&1 &
echo "PID $! – tail -f logs/train_resume.log"# Watch training metrics (Ultralytics writes CSV + PNG plots)
watch -n 30 "tail -5 runs/detect/cholectrack20/results.csv"
# Or open TensorBoard (if tensorboard is installed)
tensorboard --logdir runs/detect/cholectrack20After 100 epochs on 3× RTX 4090, expect:
| Metric | Approx. target |
|---|---|
| mAP50 | 0.70 – 0.82 |
| mAP50-95 | 0.50 – 0.65 |
| Training time | 8 – 14 hours |
Tip: Use
--model yolov10l.ptfor faster iterations during debugging.
# boxmot can download them automatically on first run, or manually:
python -c "
from boxmot import BoTSORT
from pathlib import Path
# triggers automatic download of osnet_x0_25_msmt17.pt
tracker = BoTSORT(reid_weights=Path('osnet_x0_25_msmt17.pt'), device='cuda:0')
print('ReID model ready:', Path('osnet_x0_25_msmt17.pt').exists())
"mkdir -p logs
setsid python track.py \
--weights runs/detect/cholectrack20/weights/best.pt \
--source /graphics/scratch3/datasets/cholectrack20/Testing/VID01/VID01.mp4 \
--out_dir results/VID01 \
--device 0 \
--conf 0.25 \
--iou 0.45 \
--save_video \
> logs/track_VID01.log 2>&1 &
echo "PID $! – tail -f logs/track_VID01.log"Follow progress:
tail -f logs/track_VID01.logArgument reference:
| Argument | Default | Description |
|---|---|---|
--weights |
(required) | Path to best.pt |
--source |
(required) | .mp4 video or image folder |
--out_dir |
results |
Output directory |
--device |
0 |
Single GPU for inference |
--conf |
0.25 |
Detection confidence threshold |
--iou |
0.45 |
NMS IoU threshold |
--reid_weights |
osnet_x0_25_msmt17.pt |
BoT-SORT appearance model |
--trocar_margin |
0.12 |
Border fraction for trocar zone |
--match_thresh |
0.72 |
Histogram similarity for Memory Bank ReID |
--max_age |
600 |
Memory Bank expiry (frames) |
--save_video |
True |
Write annotated video |
Outputs in results/VID01/:
VID01_tracked.mp4– annotated video with bounding boxes and track IDsVID01.txt– CholecTrack20 evaluation format:frameid,-1,x_norm,y_norm,w_norm,h_norm,score,class_idVID01_summary.json– tracking statistics (total detections, unique IDs, Memory Bank remaps)
Runs all 8 test videos sequentially in the background, one log file per video:
mkdir -p logs
for VID in VID01 VID06 VID07 VID111 VID12 VID25 VID39 VID92; do
echo "=== Queuing $VID ==="
setsid python track.py \
--weights runs/detect/cholectrack20/weights/best.pt \
--source /graphics/scratch3/datasets/cholectrack20/Testing/${VID}/${VID}.mp4 \
--out_dir results/${VID} \
--device 0 \
--conf 0.25 \
--save_video \
> logs/track_${VID}.log 2>&1
echo " done → results/${VID}/ (log: logs/track_${VID}.log)"
done &
echo "Batch PID $!"Watch a specific video's progress:
tail -f logs/track_VID06.logCheck all logs at once:
tail -n 3 logs/track_*.log# Run YOLO val (detection mAP only, no tracking)
mkdir -p logs
setsid python -c "
from ultralytics import YOLO
m = YOLO('runs/detect/cholectrack20/weights/best.pt')
r = m.val(data='cholectrack20.yaml', imgsz=640, device=0)
print('mAP50:', r.box.map50)
print('mAP50-95:', r.box.map)
" > logs/val.log 2>&1 &
echo "PID $! – tail -f logs/val.log"To make the detector robust to laparoscopic-specific artefacts, two levels of augmentation are applied:
-
YOLO built-in parameters (applied at the mosaic/copy-paste level):
- Mosaic + copy-paste → rare instrument class oversampling
- Geometric:
degrees=10,scale=0.5,shear=2,perspective=0.0001 - Colour:
hsv_s=0.7,hsv_v=0.4
-
Albumentations pipeline (injected via Ultralytics callback):
RandomFog→ surgical smoke / CO₂ insufflation hazeMotionBlur + GaussianBlur→ camera shake, fast instrument movementRandomBrightnessContrast + CLAHE→ variable cavity illuminationHueSaturationValue + RGBShift→ blood and bile colour castsGaussNoise→ camera sensor noiseCoarseDropout→ partial occlusion by tissue
The MemoryBank in track.py maintains intraoperative trajectories:
Instrument exits near border
│
▼
Save HSV colour histogram + class to MemoryBank
(keyed by canonical track ID)
Instrument re-enters near border (new BoT-SORT ID)
│
▼
Compute HSV histogram of new detection
│
▼
Compare with bank entries of same class
│
correlation > 0.72?
│ │
Yes No
│ │
Restore old ID Keep new ID
The histogram comparison uses OpenCV's compareHist with HISTCMP_CORREL.
| Problem | Solution |
|---|---|
CUDA out of memory |
Reduce --batch (e.g. --batch 24) or use --model yolov10l.pt |
FileNotFoundError: osnet...pt |
Run the ReID download cell in §5a |
| Symlinks not resolving | Check ls -la data/images/train – source must be readable |
Low mAP on grasper |
Increase mosaic=1.0 and copy_paste=0.3; class is present in nearly every frame |
| Tracking ID fragmentation | Lower --trocar_margin or tune --match_thresh |
boxmot import error |
pip install boxmot>=10.0.0 lapx>=0.5.5 |