feat: per-image legend pipeline marker→class recovery (83 tests + CI) - #3
Open
vickysharma-prog wants to merge 6 commits into
Open
feat: per-image legend pipeline marker→class recovery (83 tests + CI)#3vickysharma-prog wants to merge 6 commits into
vickysharma-prog wants to merge 6 commits into
Conversation
- DotDetector class with HSV segmentation - Vegetation-adaptive thresholds - Rank-order species assignment - 29 focused tests (pipeline behavior) - Config updated with detection thresholds - AGENTS.md updated with detection rules - Notebook Cell 5, 6 refactored to use module
…sts + CI
Recovers, per screenshot and without the CSV, which marker (shape+color)
maps to which class — the mentor's ask. Same-color markers (e.g. red circle
"BRPE WBN" vs red plus "BRPE bird") are kept distinct by shape, which
color-only detection cannot do.
New modules
- src/legend.py: locate_dialog() finds the floating "Manual Point Count"
dialog as a BOX anywhere in the frame (top/right/bottom-right), replacing
the old ~50% vertical split that discarded half the aerial and its birds
(the boundary-cutting bug). parse_screenshot/parse_legend extract each
legend row's color, best-effort shape, and a canonical 24x24 glyph
template. attach_class_names() reads class text + counts via Tesseract OCR,
fuzzy-matched to the 98 species codes. Scale-adaptive (thumbnail->full-res).
- src/classify.py: detect_dots() finds aerial dots and splits merged clusters
via distance transform (recall on dense colonies); assign_classes() matches
color-first then shape (template correlation); select_by_count() is a
count-guided top-N precision filter.
- scripts/run_legend.py: run the pipeline over a folder of screenshots.
Supporting changes
- config.yaml: new legend: section; values reconciled to the module's actual
numbers so config is the single source of truth (all modules' tunables live
here, per mentor's good-practice ask). decompose: gains safe-boundary fallback
params; detect: keeps category metadata.
- decompose.py: low-confidence safe-boundary fallback + exposes the 3 raw
boundary candidates (for the localization figure).
- detect.py: optional per-category count metadata.
Tests + CI
- tests/test_legend.py (40) and tests/test_classify.py: synthetic-glyph shape
classification, color/template/OCR-parsing units, and real-screenshot
integration over the 4 study images (skip if fixtures absent).
- CI (tests.yml): rewritten as UTF-8, installs scipy, runs all four test
files. Full suite: 143 passed.
Fixtures: data/fixtures/{screenshots,dialogs} for the 4 study images.
Figures: results/figures/ (localization, marker->class, classified aerial,
recovered-vs-truth counts, color-vs-shape).
…ctions)
GitHub Actions could not parse the UTF-16-encoded workflow file
('invalid workflow file', no jobs ran). Re-encoded as plain UTF-8.
vickysharma-prog
force-pushed
the
feat/legend-pipeline
branch
from
June 29, 2026 19:24
8b2aa21 to
1b1ae37
Compare
…ta churn) The legend pipeline is run via scripts/run_legend.py; the notebook was not updated for it. The committed notebook diff was incidental (a saved SyntaxError output + kernel metadata), so restore it to the main version. Notebook will be updated once the module is further along.
Owner
Author
|
cc @jveitchmichaelis @bw4sz @henrykironde for review |
|
I see that in many cases in (2) the marker shape is incorrect, or duplicated? Can you share a full example of:
|
Owner
Author
|
@jveitchmichaelis Full worked example attached (image D, raccoon 2011); I can generate the same for any image. Answering each point:
On the incorrect / duplicated shapes correct, and worth clarifying the architecture:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Per-image legend pipeline recover which marker → which class, from the screenshot alone
This PR adds the legend-based annotation recovery pipeline: for each historical survey screenshot it figures out, without the annotation CSV, which marker (shape + colour) maps to which class, and then classifies every dot baked into the aerial photo accordingly.
This is the mentor's core ask. Colour alone is ambiguous a red ● can be
BRPE WBNwhile a red + isBRPE bird, and the convention differs per image. So the mapping is recovered per image and dots are separated by shape, which colour-only detection cannot do.It also fixes the boundary-cutting bug: the old stage assumed the dialog was a full-height right panel and split each screenshot at ~50% width, discarding ~half the aerial and its birds. The dialog is actually a floating window that sits in a different place in every image.
New modules
src/legend.pylocate_dialog()finds the floating "Manual Point Count" dialog as a box anywhere in the frame (works on 14/14 test images).parse_screenshot/parse_legendextract each legend row's colour, best-effort shape, and a canonical 24×24 glyph template.attach_class_names()reads the class text + Count via Tesseract OCR, fuzzy-matched to the 98 species codes (corrects e.g.BAPE→BRPE). Scale-adaptive: works at thumbnail and full-res.src/classify.pydetect_dots()finds aerial dots and splits merged clusters via distance transform (dense colonies otherwise undercount).assign_classes()matches colour first, then shape (template correlation) to separate same-colour classes.select_by_count()is a count-guided top-N precision filter.scripts/run_legend.pyFull automatic pipeline:
parse_screenshot→attach_class_names→detect_dots(exclude=bbox)→assign_classes→select_by_count.Figures
1. Dialog localization the boundary fix
The "Manual Point Count" dialog auto-located as a box (red outline) across six screenshots. It floats in a different place each time (top-right / right / bottom-right). The new locator keeps the entire aerial available for detection, instead of cutting it at 50%.
2. Per-image marker → class mapping
For each study image: the dialog + the recovered
colour/shape → SPECIES categorymapping, read entirely from the screenshot (shape analysis + OCR). Same-colour markers are kept distinct by shape. The mapping is recovered per image the shape→category convention differs between images.3. Aerial dots classified by recovered class
Each aerial with every detected dot coloured by its recovered class (dialog region dimmed). Overlapping dots in dense colonies are split via distance transform. Note the dots span the whole frame, including regions the old stage used to cut off.
4. Recovered vs ground-truth counts
Left: image D, recovered count vs the dialog's legend count per class zero-count categories (empty/pbn/brood) correctly stay near zero; same-colour classes are separated. Right: total recovered vs dialog total across the 4 images, with recall % (A 36%, B 77%, C 90%, D 69%).
5. Why colour alone is not enough
A synthetic scene of same-colour, different-shape markers. Colour-only collapses them into 2 classes; colour + shape separates them into 5 correct classes the simplest illustration of why the shape step exists.
Config centralization
All of
legend.py's tunables live in thelegend:section ofconfig.yamlone place for every module's config. The values were reconciled to the module's actual numbers; detection output is byte-for-byte unchanged (verified by snapshotting all 4 fixtures before/after).Supporting changes
decompose.pylow-confidence safe-boundary fallback; exposes the 3 raw boundary candidates (used by figure 1).detect.pyoptional per-category count metadata.Tests & CI
tests/test_legend.py(40) +tests/test_classify.pysynthetic-glyph shape classification, colour/template/OCR-parsing units, and real-screenshot integration over the 4 study images (skip cleanly if fixtures are absent).tests.yml) rewritten as UTF-8, now installsscipyand runs all four test files (previously onlytest_decompose.py).Honest status