Add detection: skew, system anchors, ink extents, cuts, staff height
Systems anchor on the vertical bracket and expand over nearby ink, per ADR 0006. Two corrections the corpus forced: - Expansion measures distance from the bracket, never from the growing extent. A chaining expansion hops between the closely-stacked lines of a title block and walks the whole way up the page — on Engel p1 it swallowed the title into system 1 and the copyright footer into system 3. - Reach is 1.5 staff heights, which takes in the lyrics below the last staff and the tempo mark and INTRO box above the first, while leaving the title block and footer out. Verified against the corpus. Ketun joululaulu: 2 systems per page except p7 (3) and p12 (1), staff 48px throughout, skew -2.7 to +1.2 per page, and p2 system 1 spans 179-1071 where its bracket is 177-994 — the difference being the bottom voice's lyric line. Engel: 2-3 systems per page, staff 70-72px. Elaman nalka: 3 systems per page, 0 skew, staff 118px at 600 DPI. The overlay dump is what made both bugs visible, and stays as the tool for diagnosing a page that comes out wrong. Closes #4, #5, #6, #7, #8, #9
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Runnable check for detection, on a synthetic page.
|
||||
|
||||
Draws the structure that matters — a bracket per system, staves, lyrics close
|
||||
below, and a title and footer far away — so the check is about the algorithm
|
||||
rather than about any one scan. Run with `python tests/test_detect.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from noteman_slicer.detect import deskew, deskew_angle, detect_page # noqa: E402
|
||||
|
||||
W, H = 1000, 1400
|
||||
STAFF_GAP = 15 # → staff height 60, so expansion reaches 90px past a bracket
|
||||
|
||||
|
||||
def _system(page: np.ndarray, top: int) -> tuple[int, int]:
|
||||
"""Two staves joined by a bracket, with a lyric line below. Returns its span."""
|
||||
bottom = top + 200
|
||||
page[top:bottom, 100:104] = 0 # the bracket
|
||||
for staff_top in (top, top + 140):
|
||||
for i in range(5):
|
||||
y = staff_top + i * STAFF_GAP
|
||||
page[y : y + 2, 110:900] = 0
|
||||
page[staff_top + 90 : staff_top + 105, 200:800] = 0 # lyrics under the staff
|
||||
return top, bottom
|
||||
|
||||
|
||||
def _page() -> np.ndarray:
|
||||
page = np.full((H, W), 255, np.uint8)
|
||||
page[50:70, 300:700] = 0 # title, far above system 1
|
||||
_system(page, 200)
|
||||
_system(page, 700)
|
||||
page[1350:1365, 100:600] = 0 # footer, far below system 2
|
||||
return page
|
||||
|
||||
|
||||
def main() -> int:
|
||||
page = _page()
|
||||
|
||||
det = detect_page(page)
|
||||
assert len(det.systems) == 2, f"expected 2 systems, got {len(det.systems)}"
|
||||
assert len(det.cuts) == 1, det.cuts
|
||||
|
||||
first, second = det.systems
|
||||
# The bracket spans 200–400; the lyric line under the lower staff reaches
|
||||
# ~445 and must be absorbed.
|
||||
assert first.top == 200, first.top
|
||||
assert 400 < first.bottom < 500, first.bottom
|
||||
assert second.top == 700, second.top
|
||||
|
||||
# The title and footer are far from any bracket and must not be swallowed —
|
||||
# the bug that a chaining expansion reintroduces.
|
||||
assert first.top > 70, "title block was swallowed"
|
||||
assert second.bottom < 1350, "footer was swallowed"
|
||||
|
||||
# The cut falls between the two systems, in the whitespace.
|
||||
assert first.bottom < det.cuts[0] < second.top, det.cuts
|
||||
|
||||
assert first.staff_height is not None
|
||||
assert abs(first.staff_height - STAFF_GAP * 4) < STAFF_GAP, first.staff_height
|
||||
|
||||
# Skew is recovered to within one fine step.
|
||||
for angle in (-1.5, 0.8):
|
||||
found = deskew_angle(deskew(page, angle))
|
||||
assert abs(found + angle) <= 0.15, f"skew {angle}: got {found}"
|
||||
|
||||
# No brackets: every ink run is its own system.
|
||||
bare = np.full((H, W), 255, np.uint8)
|
||||
for y in (200, 500, 800):
|
||||
bare[y : y + 20, 100:900] = 0
|
||||
assert len(detect_page(bare).systems) == 3
|
||||
assert detect_page(bare).bracketless
|
||||
|
||||
print("ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user