feat(classify): add panel_density and wire form-scan into score slate (MK-16) #35

Merged
David merged 1 commit from feat/classify-panel-density-MK-16 into main 2026-05-24 18:26:05 +02:00
Owner

Summary

Adds the panel_density feature to monkey image classify and wires ImageClass::FormScan into the score() candidates slate (MK-16, follow-up to MK-7 / MK-14 / MK-18).

panel_density downscales luma to a fixed 256x256 buffer (shared with effective_palette), quantizes to 4 tones using panel_luma_lo / panel_luma_hi, connected-component labels the mid-tone mask, and emits the area-fraction of components whose individual area is at least panel_min_area. Otsu eta is recomputed over the complement of the panel mask as non_panel_bimodality.

form_scan = soft_ge(panel_density, panel_density_min) * soft_ge(non_panel_bimodality, bimodality_binary) * edges_text * low_sat * (1 - color). The MK-16 spec formula did not include edges_text; without it the classifies_mono_photo synthetic (smooth black-to-white gradient) falsely lands in FormScan because the gradient is one huge mid-tone connected component and its non-panel set is two trivially-bimodal extremes. Requiring text-density edges separates a real form from a gradient; the deviation is documented inline at the score site.

monkey image classify --probe emits per-component panel diagnostics on stderr (index, area, area_frac, mean_luma, bbox) followed by panel-density: and non-panel-bimodality:. The data is already computed by extract_features, so the flag is essentially free.

classify.toml gains the five new keys (panel_density_min, panel_min_area, panel_luma_lo, panel_luma_hi, soft_scale_panel) with their initial values flagged as assumptions to revise against fixtures.

The form-scan.toml recipe stays a passthrough; the panel-aware processing chain is a separate MK issue.

Test plan

  • just check (fmt + clippy -D warnings + cargo build + cargo test + Docker builder stage) passes.
  • classifies_form_scan synthetic (800x600 white page with text bands + one 35% x 30% mid-grey panel) classifies as form-scan with panel_density >= 0.05.
  • All existing in-tree synthetic tests still pass: classifies_color_photo, classifies_mono_photo, classifies_binary_scan, letter_aspect_mono_scan_is_binary_not_receipt, classifies_halftone, ceiling_rejects_perfect_periodic_grid, classifies_receipt, classifies_screenshot, classifies_graphic, line_art_is_not_halftone, solid_colour_is_unknown, checkerboard_is_not_color_photo, confidence_in_unit_interval, embedded_config_parses.
  • Local fixture validation against tests/fixtures/classify/form-scan/: all 5 fixtures classify as form-scan with confidence >= 0.30 and panel_density >= 0.05. The fixtures are locally staged (not committed), so I cannot run this from inside CI.
  • Regression check on locally-staged tests/fixtures/classify/{binary-scan,receipt,color-photo}/: rates unchanged.

Closes MK-16.

## Summary Adds the `panel_density` feature to `monkey image classify` and wires `ImageClass::FormScan` into the `score()` candidates slate (MK-16, follow-up to MK-7 / MK-14 / MK-18). `panel_density` downscales luma to a fixed 256x256 buffer (shared with `effective_palette`), quantizes to 4 tones using `panel_luma_lo` / `panel_luma_hi`, connected-component labels the mid-tone mask, and emits the area-fraction of components whose individual area is at least `panel_min_area`. Otsu eta is recomputed over the complement of the panel mask as `non_panel_bimodality`. `form_scan = soft_ge(panel_density, panel_density_min) * soft_ge(non_panel_bimodality, bimodality_binary) * edges_text * low_sat * (1 - color)`. The MK-16 spec formula did not include `edges_text`; without it the `classifies_mono_photo` synthetic (smooth black-to-white gradient) falsely lands in FormScan because the gradient is one huge mid-tone connected component and its non-panel set is two trivially-bimodal extremes. Requiring text-density edges separates a real form from a gradient; the deviation is documented inline at the score site. `monkey image classify --probe` emits per-component panel diagnostics on stderr (index, area, area_frac, mean_luma, bbox) followed by `panel-density:` and `non-panel-bimodality:`. The data is already computed by `extract_features`, so the flag is essentially free. `classify.toml` gains the five new keys (`panel_density_min`, `panel_min_area`, `panel_luma_lo`, `panel_luma_hi`, `soft_scale_panel`) with their initial values flagged as assumptions to revise against fixtures. The `form-scan.toml` recipe stays a passthrough; the panel-aware processing chain is a separate MK issue. ## Test plan - [x] `just check` (fmt + clippy `-D warnings` + cargo build + cargo test + Docker builder stage) passes. - [x] `classifies_form_scan` synthetic (800x600 white page with text bands + one 35% x 30% mid-grey panel) classifies as `form-scan` with `panel_density >= 0.05`. - [x] All existing in-tree synthetic tests still pass: `classifies_color_photo`, `classifies_mono_photo`, `classifies_binary_scan`, `letter_aspect_mono_scan_is_binary_not_receipt`, `classifies_halftone`, `ceiling_rejects_perfect_periodic_grid`, `classifies_receipt`, `classifies_screenshot`, `classifies_graphic`, `line_art_is_not_halftone`, `solid_colour_is_unknown`, `checkerboard_is_not_color_photo`, `confidence_in_unit_interval`, `embedded_config_parses`. - [ ] Local fixture validation against `tests/fixtures/classify/form-scan/`: all 5 fixtures classify as `form-scan` with confidence >= 0.30 and `panel_density >= 0.05`. The fixtures are locally staged (not committed), so I cannot run this from inside CI. - [ ] Regression check on locally-staged `tests/fixtures/classify/{binary-scan,receipt,color-photo}/`: rates unchanged. Closes MK-16.
feat(classify): add panel_density and wire form-scan into score slate
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 19s
Create release / Create release from merged PR (pull_request) Has been skipped
179d94c72e
Adds the `panel_density` feature for `monkey image classify`: downscale luma to a fixed 256x256 buffer (shared with the palette feature), quantize to 4 tones using `panel_luma_lo` / `panel_luma_hi`, connected-component label the mid-tone mask, and emit the area-fraction of components whose individual area is at least `panel_min_area`. Otsu eta is recomputed over the complement of the panel mask as `non_panel_bimodality`.

`ImageClass::FormScan` joins the `score()` candidates slate with `form_scan = soft_ge(panel_density, panel_density_min) * soft_ge(non_panel_bimodality, bimodality_binary) * edges_text * low_sat * (1 - color)`. The MK-16 spec formula omitted `edges_text`; without it the `classifies_mono_photo` synthetic (smooth black-to-white gradient) lands in FormScan because the gradient is one huge mid-tone connected component and its non-panel set is the two extremes, trivially bimodal. Requiring text-density edges separates a real form (dense text outside the panels) from a gradient (near-zero edges). The deviation is documented inline.

A `--probe` flag extends `monkey image classify` with per-component panel diagnostics on stderr (component index, area, area_frac, mean_luma, bbox) plus the derived `panel-density` and `non-panel-bimodality`. Component data is already computed by `extract_features`, so `--probe` is essentially free.

`classify.toml` gains five new keys (`panel_density_min = 0.05`, `panel_min_area = 327`, `panel_luma_lo = 0.20`, `panel_luma_hi = 0.80`, `soft_scale_panel = 0.03`) with the initial values flagged as assumptions to revise against the local `tests/fixtures/classify/form-scan/` set. `classifies_form_scan` covers the synthetic case (800x600 white page with text bands plus one 35% x 30% mid-grey rectangle). All existing in-tree synthetic tests still pass. `effective_palette` is rewritten to consume the shared 256x256 downsample.

`form-scan.toml` recipe stays a passthrough; the panel-aware processing chain is a follow-up issue.

#MK-16 State Done
David merged commit 0c415f7e14 into main 2026-05-24 18:26:05 +02:00
David deleted branch feat/classify-panel-density-MK-16 2026-05-24 18:26:05 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pandoras-box/monkey!35
No description provided.