Skip to content

feat: staff-finding pipeline improvements — line following, coord fix, stave QA boxes#53

Open
kyrieb-ekat wants to merge 15 commits into
mainfrom
kyrie/staff-finding
Open

feat: staff-finding pipeline improvements — line following, coord fix, stave QA boxes#53
kyrieb-ekat wants to merge 15 commits into
mainfrom
kyrie/staff-finding

Conversation

@kyrieb-ekat

Copy link
Copy Markdown
Collaborator

Summary

  • Line-following refinement (fit_centerline.py): when a bounding box contains multiple arching stafflines (page curvature), the initial Huber fit averages across them and produces high residuals. A new sliding-window trace walks left-to-right within a ±1.5h band from the box vertical center, isolating one line before refitting with a cubic polynomial. Handles both C-shaped arches and S-shaped parchment waves. Flagged as line_following_applied:deg3/deg2.

  • Page-coordinate offset fix (fit_centerline.py, run_page.py, group_staves.py): FitResult coordinates are crop-local. Adds x_page_offset / y_page_offset fields (populated from YOLO bounding box ulx/uly in run_page.py). Fixes two bugs:

    • Stave grouping was seeing all y-gaps as ~0 px → every line assigned to one stave (mode=96)
    • Diagnostic lines were drawn at crop-local coords on the page canvas → all appeared in the top-left corner
  • Stave bounding boxes (group_staves.py): red rectangles + Sn labels drawn around each stave group in the diagnostic PNG for visual QA.

  • Test fix (test_fit_centerline.py): fixed hardcoded /home/claude sys.path; added test_two_line_box_line_following verifying the line-following trigger and refit quality.

Test plan

  • python -m pytest staff-finding/scripts/script_tests/test_fit_centerline.py staff-finding/scripts/script_tests/test_group_staves.py -v — 15 tests pass
  • Run run_page.py on a real page; confirm grouping diagnostic shows colored centerlines at correct positions, red stave boxes, and a reasonable mode lines/stave (~4–5 for square notation)
  • Check line_following_applied flag appears in fit diagnostics for boxes with high initial residuals

🤖 Generated with Claude Code

kyrieb-ekat and others added 15 commits May 27, 2026 11:42
fit_centerline.py
- Add x_page_offset / y_page_offset fields to FitResult (default 0.0)
  so crop-local coords can be converted to page-absolute downstream
- Add line-following refinement triggered when initial Huber residual
  mean > 1.0 * scale_unit (multi-line box signal):
    * _trace_line() slides left-to-right within a 1.5h band, emitting
      one median-y per window — naturally follows any curve shape
    * Refit uses cubic polynomial (LINE_FOLLOW_POLY_DEGREE=3) when
      ≥ 8 trace points available, else falls back to quadratic; handles
      both C-shaped page arches and S-shaped parchment waves
    * Only accepted if refit residual improves over initial fit
    * Flagged as line_following_applied:deg3/deg2 in FitResult.flags
- Diagnostic overlay now shows cyan scatter dots for trace points when
  line-following was applied; legend distinguishes from fitted line

group_staves.py
- _y_at_fit_center: add fit.y_page_offset so gap analysis uses
  page-absolute y-positions (fixes all-gaps-near-zero bug that put
  every line in one giant stave)
- _save_grouping_diagnostic drawing loop: add x/y page offsets to
  xs/ys before cv2.polylines (fixes lines-at-origin visualization bug)
- After polylines loop, draw a red bounding rectangle + Sn label
  around each stave group for quick visual QA

run_page.py
- Populate fit_result.x_page_offset / y_page_offset from actual_box
  (ulx, uly) immediately after each fit_centerline call

test_fit_centerline.py
- Fix sys.path (was hardcoded /home/claude)
- Add test_two_line_box_line_following: constructs a ComponentFilterResult
  directly with two-line pixel data, verifies line_following_applied flag
  and tight residual (< 3 px) after the refit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds GentAnt1475 folio 17 (left and right crops) with YOLO inference
outputs (raw + corrected) as the primary test page for the implicit
neural and interpolation experiments documented in
experiments/implicit_neural/NOTES.md.

Moves the Ordo Virtutum page from image-sets/ root into its own
image-sets/ordo_virt_000/ subdirectory, matching the layout convention
used for gent/.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Introduces staff-finding/experiments/ — a family of standalone runner
scripts that plug in after YOLO detection and exercise different
curve-fitting approaches. Each runner produces identical JSOMR JSON and
stave-grouping diagnostic PNGs so results are directly comparable.

shared_utils.py: common helpers (YOLO parsing, ExperimentFitResult
bridge, run_grouping_and_save) shared by every runner.

Runners added:
- dp_tracing/        Dynamic-programming horizontal-emphasis tracer
- gp_centerlines/    Gaussian-process Matern fit on ink-pixel coords
- implicit_neural/   Test-time MLP on pixel brightness (best result
                     on Gent ms: mode=8, see implicit_neural/NOTES.md)
- periodicity/       Autocorrelation comb (limited on per-box crops)
- heatmap_regression/ Planned; NOTES.md stubs the design

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…terpolate_staves

group_staves.py additions
  - Stage 7: gap-based missing-line synthesis with two triggers:
      (A) in-stave gap fill — fires when consecutive detected lines in
          the same stave have a y-gap in [cut_threshold, max_threshold],
          inserts n_missing lines evenly spaced between them
      (B) edge extrapolation — grid-anchored at the bottommost detected
          line, fills empty slots; requires >= mode_n//2 lines detected
          (safety against over-synthesis on sparse/split staves)
  - Adaptive thresholds: cut_threshold (Otsu/median), min_threshold
      (noise floor = 0.5 * scale_unit), max_threshold (mean of
      inter-stave gaps) — all three shown on the gap distribution chart
  - Territory boundaries: interpolated lines are constrained to each
      stave's y-range; top/bottom staves cannot extrapolate outside
      their own detected extent
  - Stage 6c _check_stave_rhythm: classifies each stave as normal /
      under_populated / over_populated by comparing detected intra-stave
      gap count to mode_n-1; also reports gap_cv (spacing consistency)
  - Rhythm gate in Stage 7: staves flagged under/over_populated are
      skipped entirely — their detected assignments are preserved as-is
  - Diagnostic chart: per-bar colouring by rhythm status (red = under,
      purple = over), annotated labels showing observed/expected counts,
      shaded noise and missing-line trigger zones
  - StaveGroupingResult gains min_threshold_px, interpolation_max_gap_px,
      rhythm_anomalies fields; summary print shows stave count + avg

interpolate_staves.py (new)
  Extracted from group_staves Stage 7 to keep grouping and synthesis
  concerns separate. Contains InterpolatedLine dataclass,
  _compute_interpolation_max_gap, _interpolate_between, and the public
  interpolate_missing_lines() API. group_staves.py re-exports
  InterpolatedLine for backward-compatible imports.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
eval_page.py evaluates a single page run against ground-truth
annotations, reporting per-stave line-count accuracy and flagging
rhythm anomalies vs. expected stave structure.

eval_batch.py runs eval_page across an image-set directory and
aggregates results into a summary CSV, enabling cross-method and
cross-manuscript comparison once all five experiment runners are
complete.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
staff-finding/README.md gives an overview of the pipeline stages,
experiment variants, and output formats for new contributors.

.gitignore updated to cover Python cache dirs and experiment output
folders that should not be tracked.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…le test fix

Stage 1 (component_filter.py — already committed): loosen merge thresholds
(y-distance 0.5→1.0h, x-gap 5→10h) and add companion-score-floor retention
so high-scoring non-overlapping fragments are kept alongside the merge winner.
Diagnostics updated with cyan coloring for companions in panels 3–5.

Stage 2 (run_page.py): add centerline_page section to each JSOMR record with
page-absolute x_start/x_end/y_values (crop-local + x_page_offset/y_page_offset).
Old centerline key preserved for backwards compatibility.

Tests: fix stale assertion in test_grouping_with_missing_line_and_interpolation
— interpolation was implemented in 572c025 but the test still expected [].

E2E data:
- 28may_stave-fulldata/: Ordo Virtutum 8-page run (8.6MB, all files)
- 29may/: Gent right/left multi-variant run (JSON/CSV/TXT only, no PNGs)
- pitch_finding_sample/: Gent right, 86 lines, 17 staves — stafflines JSON,
  stave grouping PNGs, summary CSV, stave grouping report for pitch-finding
  collaborator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant