Files
noteman-slicer/tests/test_detect.py
T
Esa Kataja 38cb6ce09a Stop an edge artefact and a speck filter from destroying a song
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.
2026-07-29 12:22:08 +03:00

95 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 200400; 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())