feat: hole detection + per-nest snips on the public dashboard (#165)#188
Conversation
Replace the random stub_classify with a real OpenCV HoleDetector: locate nest holes (HoughCircles + normalized fixed-grid fallback), label bee type by measured diameter, classify empty/sealed by brightness+texture, and crop a per-nest snip. Snips are stored with full history (nest_detections, duckdb-service sole writer) and served publicly at /api/snips/:filename — the crop is the privacy mechanism, so no auth (#154). The existing add_progress contract is unchanged; only the sealed values become real. Detection degrades gracefully — a miss falls back to the stub and never 500s. Frontend: NestSnipGrid renders a 4x4 grid mirroring the block, one row per bee type, each cell a snip with an empty/sealed badge (gated by the existing dashboard-images flag). Adds opencv-python-headless + libglib2.0-0, a one-shot backfill script, contracts (NestSnip), full test coverage (unit + component + Playwright), ADR-026, and arc42 doc updates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEJZA4eWELoGpvZ7Ym41K6
- Replace the tautological diameter test with one feeding synthetic circles whose vertical order disagrees with their diameter order, proving bee-type labelling follows measured diameter, not position. - Backend /api/modules/:id/snips: extend the wire-shape guard to validate confidence + nest_index, and annotate the response as NestSnipsResponse so the producer-side mapping is compile-checked (ADR-004). - Document the detector's mock-calibration limitation (no 4th row / distinct diameters) loudly; fix a wrong path in the backfill script docstring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEJZA4eWELoGpvZ7Ym41K6
…#165) Real ESP captures (added under dev-tools/real_captures/) exposed that the mock-tuned HoughCircles params find no circles on real low-contrast images, so detect() hit the fixed-grid fallback and emitted 16 wood-sampled "sealed" snips — confident garbage on the public dashboard, invisible because the only test inputs were the high-contrast mocks. Remove the grid-fabricating fallback and gate on a circle quorum: below it the detector returns no detection and the pipeline falls back to the stub (honest blank), never invented values. Real captures degrade gracefully today; robust real-image detection (grid-fitting + a labelled corpus) is tracked as follow-up. Adds the three real captures as regression fixtures + a test that forbids a full all-sealed grid on unreadable input, a real_captures/README, and a chapter-11 lesson ("a fallback may degrade but must never fabricate"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEJZA4eWELoGpvZ7Ym41K6
Sets up the "calibrate against real captures, not mocks" loop: a JSON label format (<image>.labels.json: grid + sealed [row,col] list) and dev-tools/calibrate_holes.py, which runs the detector over labelled real captures and reports whether detection fired, holes kept, and predicted-vs- labelled sealed counts (with --overlay to draw detected circles). README documents the workflow. Recalibration becomes measurable the moment labelled 4x4 captures are added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEJZA4eWELoGpvZ7Ym41K6
Next steps — recalibrate hole detection against real capturesReal ESP captures exposed that the mock-tuned What's not done: tuning detection to actually read real captures. There is no single Hough config that fits both the high-contrast synthetic mocks and the low-contrast real images, so this needs a grid-fitting/candidate-selection stage plus a labelled real-capture corpus. The loop is set up to make that measurable rather than guesswork. To do (on a PC with the module +
|
… ONNX) Replace the OpenCV HoughCircles detector — which found no holes on real ESP captures — with the trained YOLO26n-seg model, exported to ONNX and run through the lean onnxruntime (no torch/ultralytics in the service image). The model localizes every nest hole and crops a real per-nest snip from each; empty/sealed classification is deferred, so snips carry state="undetermined" and stub_classify() still drives the species progress bars. image-service - ONNX inference (conf 0.25, NMS 0.7 dedup), diameter-based bee-type labelling, no per-row cap so the irregular 7/5/5/4 (21) and 4x4 (16) blocks both keep every hole; ~50 ms CPU, graceful degradation preserved. - onnxruntime + the committed 11 MB ONNX (models/hole_detector.onnx); libgomp1 added to the Dockerfile. duckdb / wire - GET /detections folds to the latest capture and dedups per nest, so a vanished nest no longer latches a stale crop and a re-upload stays idempotent. - new neutral "undetermined" state threaded through duckdb, backend, contracts, and the NestSnipGrid badge (+ i18n). dev-tools/ml_hole_detection - export_onnx.py (with an end2end-shape guard that rejects a non-YOLO26 fallback) and verify_onnx_parity.py (runtime vs ultralytics — committed proof of the bit-for-bit claim). docs: arc42 chapter + ADR-027, image-service.md, glossary, runtime-view, api-reference/contracts, and a troubleshooting entry for onnxruntime's libgomp1 need. Refs #165. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Phase 2 of the engagement story (#154). Today
image-service'sstub_classify()returns random 0/1 per nest, and those random values feed the sealed-% progress bars users already see. This makes detection real and adds snips — cropped images of individual nest holes — to the public dashboard. The crop is the privacy mechanism (#154): a snip shows only the hole, no garden/house background, so per-nest imagery stays public without auth. The stored snips also become labelled training patches for the planned ML model (#112).Addresses #165.
Approach (decisions taken with the maintainer)
dev-tools/circle.txt(4×3) is annotated as superseded.HoughCircles, snap to grid, fall back to a normalized fixed grid. All in resolution-independent fractions (works at VGA/UXGA).sealed = ratio ≥ R OR std ≥ S): catches both shadowed-textured and bright-smooth plugs.nest_detections, duckdb-service sole writer per ADR-001) → enables phase-3 time-lapse (Engagement features on top of nest snips (#154 phase 3) #166)./uploadnever 500s.Changes
services/hole_detection.py(HoleDetector), wired intoUploadPipeline(stub kept as the fallback); new/snips/:filenameserving route;opencv-python-headless+numpy(+libglib2.0-0in the Dockerfile).nest_detectionstable +POST /record_detectionsandGET /detections(latest-per-nest fold).GET /api/snips/:filenameandGET /api/modules/:id/snips(snake→camel, validated wire shape).NestSnip/NestSnipsResponse.NestSnipGrid(4×4 grid mirroring the block, empty/sealed badges),getSnips/getSnipUrl, i18n. Gated by the existing dashboard-images flag.backfill_detections.pyfor existing uploads.Tests
test_hole_detection.py(incl. a diameter-vs-position test on synthetic circles),test_detections.py,snips-route.test.ts,NestSnipGrid.test.tsx, Playwrightmodule-nest-snips.spec.ts(real chain: detector → duckdb → backend → nginx → decoded<img>pixels).make check-citationsclean.Known limitation
The Hough params and fallback-grid constants are calibrated against the 791×528 mock fixtures (uniform 3×4), which don't exercise the real block's 4th row or four distinct diameters. Recalibrate against a real UXGA capture (one sealed, one empty) before relying on it in the field — documented in the detector docstring, ADR-026, and
circle.txt. The diameter logic itself is unit-pinned independently of the mock.Out of scope
MaskRCNN/ML (#112) · phase-3 engagement extras (#166) · the small top-row camera/lighting fix (#12, #155 — detection degrades gracefully there).
🤖 Generated with Claude Code
https://claude.ai/code/session_01MEJZA4eWELoGpvZ7Ym41K6
Generated by Claude Code