"""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 ( # noqa: E402 deskew, deskew_angle, detect_page, ink_levels, ) 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}" # Levels are proposed too. A grey scan left at 0–255 ships its wash to the # tablet, and the downscale to the song's width only blends it further. grey = np.full((H, W), 210, np.uint8) # paper, not white grey[200:400, 100:900] = 70 # ink, not black black, white = ink_levels(grey) assert black < 70 < white < 210, (black, white) # A page already bilevel has nothing between ink and paper to stretch. assert ink_levels(_page()) == (0, 255) # A scanner's edge line runs the whole height of the sheet. Being taller # than every bracket it used to win each overlap and swallow the page into # one system — Olukainen juomukainen, where five pages of six came out as a # single slice each. scanned = _page() scanned[10 : H - 10, W - 8 : W - 4] = 0 assert len(detect_page(scanned).systems) == 2, "an edge artefact is not a bracket" # 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())