Olukainen juomukainen came out unusable, from two separate faults. The scanner left a dark line down the sheet edge, running the full height of every page but the first. Being taller than any bracket it won every overlap in anchor selection and swallowed the page into one system, so five pages of six proposed no cuts at all. A page's brackets and barlines are all about one system tall, so a stroke far taller than the typical one is not notation — relative to the page's own strokes, since a page holding one big system is legitimate. The trim then removed each system's bottom line of lyrics. It judged ink blobs by area, and a letter is nowhere near the threshold; a whole line of them is dozens of blobs, none of which qualifies. Measure ink per row and per column instead — a line of text carries plenty in total, and a fleck's row carries almost none, which is the case the filter was for. Detection now finds three systems on every page of that score, and every slice keeps all four voices' words.
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""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}"
|
||
|
||
# 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())
|