Detects foreign objects — coins, buttons, jewellery, wires, surgical items — in frontal chest radiographs from the object-CXR dataset, with two interchangeable detectors behind one interface:
| Detector | Backbone | Input | Notes |
|---|---|---|---|
| Faster R-CNN | ResNet50-FPN (torchvision) | 600×600 | Two-stage; better recall on small, low-contrast objects |
| YOLO | ultralytics | 480×480 | Single-stage; much faster on CPU, simpler to deploy |
This is a production restructuring of the original research notebook
(notebooks/foreign-objects-in-chest-x-rays.ipynb): an installable Python package with a CLI,
a FastAPI service, and a React front end.
Research and educational use only. Not a medical device. Predictions must not be used for diagnosis or any clinical decision.
┌──────────────────────────────────────────────────────────────────────────┐
│ Foreign Object Detection Detect Compare Batch Metrics demo · cpu │
├──────────────────┬───────────────────────────────────────────────────────┤
│ Upload / samples │ X-ray with detection overlay (zoom, pan, hover) │
│ Detector picker │ ┌────┐ 94% │
│ Confidence 0.50 │ └────┘ │
│ [Detect] │ Verdict: FOREIGN OBJECT · 94.0% · 1 object · 412 ms │
└──────────────────┴───────────────────────────────────────────────────────┘
- Detect — drop an X-ray, pick a detector, drag the confidence slider; boxes are drawn in original-image coordinates and stay aligned at any zoom.
- Compare — the same image through both detectors, side by side with synchronised zoom.
- Batch — screen many images at once, sortable table, CSV export.
- Metrics — ROC curve, threshold sweep, confusion matrix, FROC and a model card, read from
whatever
cxr evaluatelast wrote.
# 1. backend
pip install -e backend # add [yolo] for ultralytics, [dev] for tests
python -m cxr.cli info # environment, dataset and checkpoint report
python -m cxr.cli serve # http://127.0.0.1:8000 (docs at /api/docs)
# 2. front end (second terminal)
cd frontend && npm install && npm run dev # http://localhost:5173pip install -e backend also installs a cxr console script; use it in place of
python -m cxr.cli if your Python scripts directory is on PATH.
No checkpoints yet? That is the expected first-run state. The app starts, reports
demo mode, and serves recorded detections for five bundled synthetic phantom images so the
whole UI is usable. Those responses are tagged source: "demo" and badged in the interface —
they are never presented as real inference.
docker compose up --build # SPA on :8080, API on :8000./models, ./artifacts and ./data are bind-mounted, so checkpoints you drop into models/
are picked up on restart.
The dataset (~9 000 images) and training are not bundled. On a GPU machine — Kaggle, Colab, or your own box:
# point the config at your copy of the dataset
export CXR_DATA__ROOT=/path/to/object-CXR # <root>/train, <root>/dev, train.csv, dev.csv
cxr train frcnn --config configs/frcnn.yaml # ~5 epochs, keeps the best-AUC checkpoint
cxr train yolo --config configs/yolo.yaml # exports the YOLO tree, then trains
cxr evaluate --model frcnn # writes the CSVs + metrics.json the UI readsA one-minute smoke test without a GPU:
cxr train frcnn --epochs 1 --limit 20Copy the resulting model.pt / best.pt into models/frcnn/ and models/yolo/
(see models/README.md), restart the API, and the UI leaves demo mode.
backend/cxr/
config.py layered settings: defaults -> YAML -> CXR_* env vars
device.py auto CPU / CUDA / MPS selection
data/ annotation parsing, dataset, transforms, YOLO export
models/ Detector interface + Faster R-CNN and YOLO backends + registry
training/ train loops, meters, per-model training entry points
evaluation/ accuracy/AUC/ROC, centre-point localization, FROC, report writer
inference/ upload validation, model cache, batch + demo mode
api/ FastAPI app and routes
cli.py cxr info | prepare-yolo | train | evaluate | predict | serve
frontend/src/
api/ typed client and DTOs
lib/scale.ts image <-> canvas coordinate maths (unit-tested)
components/ canvas overlay, charts, controls, result panels
pages/ Detect, Compare, Batch, Metrics
configs/ default.yaml, frcnn.yaml, yolo.yaml
docker/ backend + frontend images, nginx config
scripts/ synthetic demo asset generator
notebooks/ the original research notebook, unchanged
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
device, versions, which models are ready |
| GET | /api/models |
per-detector status (weights present, dependencies, loaded) |
| POST | /api/models/{name}/load |
warm a model up |
| POST | /api/predict |
one image → detections, verdict, timing |
| POST | /api/predict/batch |
many images; ?format=csv streams a report |
| POST | /api/compare |
one image through every detector |
| GET | /api/metrics[/{name}] |
stored evaluation artefacts |
| GET | /api/samples |
bundled synthetic phantoms |
Interactive docs: http://127.0.0.1:8000/api/docs.
Three layers, later wins: defaults in config.py → YAML (--config, or CXR_CONFIG) →
environment variables prefixed CXR_ (nested keys use __, e.g. CXR_DATA__ROOT).
Relative paths resolve against the repository root. See .env.example.
pip install -e "backend[dev]"
cd backend && pytest -q # 49 tests, no dataset or weights required
cd frontend && npm test # coordinate-mapping unit tests
cd frontend && npm run build # type-check + production bundleTests use synthetic fixtures and a stub detector throughout, so CI needs neither the dataset nor a GPU.
The port is not a copy-paste. Beyond the restructuring, these defects were found and fixed:
- YOLO trained without labels. Labels were written to
/kaggle/working/data/<split>/labelswhiledata.yamlpointedtrain/valat the read-only Kaggle image folders. Ultralytics resolves labels by swapping/images/for/labels/in the image path, which could not match, so the run saw empty targets.data/yolo_export.pynow builds a realimages/{train,val}+labels/{train,val}tree and adata.yamlthat usespath:with relative subdirectories. - Wrong checkpoint path. Predictions loaded
runs/train/.../best.pt; ultralytics ≥ 8 writes toruns/detect/.... The trainer's ownsave_diris used instead. - Crash on negative images.
torch.as_tensor([])is 1-D, soboxes[:, 3]raised; the notebook only avoided it by dropping every un-annotated training image. Boxes are now reshaped to(0, 4), and training on positives only is an explicit config flag. - Batch-size-dependent evaluation. The eval loops indexed
outputs[-1],label[-1],width[-1]— correct only at batch size 1. Both loops are batch-aware. - Shape codes ignored. Annotations were parsed with
anno[2:], assuming a single-character shape code and skipping validation. Rectangles, ellipses and polygons are now parsed explicitly, and malformed or degenerate entries are dropped instead of producing silent garbage coordinates. - Deprecated / unsafe APIs.
pretrained=True→ weights enum,torch.load→weights_only=True, hardcodedcuda:0→ auto device,sys.exit(1)on divergence → a raised exception, and the vendored distributed-training helpers (unused on one GPU) removed.
Localization output keeps the object-CXR submission format (score x y triples per image), and
cxr evaluate still emits classification.csv and localization.csv alongside the JSON the
dashboard reads.
- Trained on annotated images only, so the detector never saw a clean radiograph in training.
- The image-level score is the maximum box confidence — one spurious box drives the verdict.
- Frontal radiographs only; behaviour on other views or modalities is untested.
- The bundled sample images are synthetic phantoms, not patient data.