Training, validation and test data for the pillbox pill-presence detectors, plus the model registry. The Raspberry Pi pushes new photos and labels here automatically (hourly); people and training jobs pull.
raw/YYYY-MM-DD/photo_*.jpg every capture, filed by date — the source of truth
labels/labels.json per-cell ground truth: "<photo stem>/<DAY>_<SLOT>": "pill" | "empty"
splits/{train,valid,test}.txt photo stems per split, assigned BY CAPTURE SCENE
references/<set-id>/ empty-box reference photos (for the 6-channel CNN)
models/<detector>/<version>/ trained models + card.json (see "Contributing a model")
legacy/<set-name>/ pre-existing datasets that can't be regenerated
from raw/ (e.g. ad-hoc-named crops); frozen as-is
export/ generated train folders — gitignored, never committed
Only sources of truth are stored. Cropped cell images are never committed — they are derived from the raw photos and go stale whenever the crop calibration changes, so they get regenerated on demand (next section).
You need both repos side by side, plus opencv-python and numpy:
git clone https://github.com/tarun101/pillbox
git clone https://github.com/tarun101/pillbox-data
pip install opencv-python-headless numpy
python3 pillbox/detect/export_dataset.py --data pillbox-data --cleanThat writes the familiar Ultralytics-classify layout (same shape as the
Roboflow export you trained on) to pillbox-data/export/:
export/
Train/Full/photo_20260713_144724_SAT_NIGHT.jpg # <photo stem>_<DAY>_<SLOT>
Train/Empty/…
Valid/Full/… Valid/Empty/…
Test/Full/… Test/Empty/…
Filenames keep provenance, so any crop traces back to its raw photo. Re-run the export any time — it always reflects the latest photos and label corrections. Train on it e.g. with:
yolo classify train data=pillbox-data/export model=yolov8n-cls.pt imgsz=224
yolo export model=runs/classify/train/weights/best.pt format=onnx imgsz=224imgsz must match between train and export. The shipped YOLO is a 224 model — exporting a 224-trained model at 640 runs without error but returns near-random predictions. Keep both commands on the same size.
The original YOLO trained on a Roboflow export that expanded these ~1k cells to
~2,700 "training images" by augmentation (flips, rotations, exposure). That was
never a separate dataset — it's the same labelled cells — so it isn't stored
here; regenerate it with --augment N:
python3 pillbox/detect/export_dataset.py --data pillbox-data --clean --augment 3--augment N writes N images per Train crop (original + N-1 augmented
copies); Valid/Test always stay 1× so evaluation is never inflated. Every
copy is seeded from its filename, so the export is byte-identical run to run.
--augment 3 yields ~2.4k images (Roboflow's exact 2,700 sat between 3× and
4×). Omit the flag (default 1) for the plain, unaugmented crops.
All three detectors — the classical DoG baseline, the reference-CNN, and
the YOLO classifier — run through shared harnesses in pillbox/detect/, so
their numbers are directly comparable. The DoG baseline is fully classical (no
weights, no training) and lives in pillbox/detect/classify_cells.py
(dog_response() is the difference-of-Gaussians; its one threshold is fitted by
calibrate_dog.py). You never run a model by hand — use these:
Accuracy / F1 across all models on a labelled split:
python3 -m detect.paper_stats --data ~/pillbox-data --split test # or: allWrites the model-comparison table (accuracy / F1 / macro-F1 / RMSE / params) and
the bar chart. Use --split test for the frozen held-out number, --split all
to sanity-check on everything labelled.
Camouflage (Figure 5) — recall on Full cells binned by pill-to-lid colour difference (ΔE), one line per model:
python3 -m detect.camouflage_eval --images <photos_dir> \
--labels <labels.json> --out dataset/camoWrites figure5.png + bins.json. Each cell is scored against the empty-box
reference, so include one empty-box reference photo when you shoot a new box
or setup.
Latency + power — run ON the Pi:
python3 -m detect.paper_stats --data ~/pillbox-data --hardwareLatency (ms/photo) is measured on whatever device runs it, so run it on the
actual Pi. --hardware also reads the Pi 5 PMIC for real power draw (net of
idle baseline) → Figure 6. Power needs a Pi 5 (the Pi 4 has no PMIC to
query — latency still works there, power does not). Add --latency-reps 20 for
tighter timing averages.
Both harnesses need ground-truth labels for the cells you evaluate. Label new captures in the app's Analyze modal ("Your labels" grid); they sync here automatically, then the commands above pick them up.
- Never train on
Test/. The test split is frozen — photos are never moved between splits — so everyone's accuracy numbers stay comparable over time. Report test accuracy only for a final, chosen model. - Don't hand-edit
splits/*.txtor file images into Train/Valid/Test yourself. Splits are assigned by capture scene (shots taken seconds apart are near-duplicates; letting them straddle splits inflates accuracy).pillbox/detect/make_splits.pyhandles it, and the Pi runs it automatically. - Don't commit
export/(it's gitignored). If you want to pin exactly what a model trained on, note this repo's commit hash in the model'scard.jsoninstead — the export is reproducible from any commit. - Labels come from the app. Corrections are made in the pillbox web app's
Analyze modal ("Your labels" grid) and sync here hourly. If you spot a wrong
label while training, say so / fix it in the app rather than editing
labels.jsonby hand, so the two never diverge.
Drop trained models into the registry with a small metadata card:
models/
yolo/
dylan-2026-07-16/
best.onnx # what the Pi runs (onnxruntime)
best.pt # the source checkpoint (kept for re-export)
card.json
cnn/
2026-07-13-v1/
pill_classifier.onnx
card.json
card.json — a few lines so "which model is live and why" stays answerable:
{
"trained_by": "Dylan P",
"date": "2026-07-16",
"data_commit": "<git rev-parse HEAD of this repo when exported>",
"base_model": "yolov8n-cls",
"imgsz": 640,
"val_accuracy": 0.97,
"test_accuracy": 0.95,
"notes": "trained on Roboflow export + July 13 set"
}Promotion (making a model live on the Pi): copy the winner into the app
repo — detect/yolo/best.onnx for YOLO, detect/pill_classifier.onnx for the
CNN — and open a PR there. Merging auto-deploys to the Pi within ~2 minutes;
the PR diff is the audit log and rollback is git revert. Models must be
ONNX to run on the Pi (onnxruntime only — no PyTorch there); classify models
should name their classes so the "pill present" one contains full or pill.
If you have data from before this repo (e.g. your original Roboflow export):
if the filenames still identify the source photo + cell, it can be absorbed
into labels.json/splits losslessly; if not (Roboflow-renamed or augmented
crops), it gets preserved as a frozen snapshot under legacy/<set-name>/ and
concatenated at training time. Post a file listing (find <dataset> -type f | head -20) in the group chat and we'll wire it in.
Pi camera → web app (capture + Analyze labeling) → ~/photos + labels.json
→ hourly sync (deploy/sync-data.sh) → this repo (raw/, labels/, splits/)
→ export_dataset.py → export/ (Train|Valid|Test / Full|Empty)
→ training → models/<detector>/<version>/ → PR to pillbox → Pi