Give each ink run a single owner, by precedence not proximity
Feliz Navidad exposed two assignment faults. Each bracket was expanding independently, so a lyric line between two systems could be claimed by both, producing overlapping extents. And nearest-bracket is the wrong rule: engravers space lyrics generously, so a line sits 43px under its own system's bracket but only 10px above the next one's. Now one pass assigns every run exactly once: ink overlapping a bracket belongs to it, and otherwise the system above wins over the system below. Text under a staff belongs to that staff. Known limit, documented in _assign: where a lyric is printed tight enough that no blank row separates it from the next system's staves, the two fuse into one ink run and no row profile can split them. The lyric goes to the system below and the cut lands ~90px high. Dragging it is the fix. Feliz Navidad now reads 5 systems on every page, staff 70px, with no overlapping extents; Ketun joululaulu, Engel and Elaman nalka are unchanged.
This commit is contained in:
+66
-32
@@ -161,35 +161,70 @@ def staff_height(gray: np.ndarray, top: int, bottom: int) -> float | None:
|
|||||||
return float(np.median(intra) * 4) # 5 lines, 4 spaces
|
return float(np.median(intra) * 4) # 5 lines, 4 spaces
|
||||||
|
|
||||||
|
|
||||||
def _expand(
|
def _gap(run: tuple[int, int], span: tuple[int, int]) -> int:
|
||||||
|
"""Vertical distance between an ink run and a bracket span; 0 if they overlap."""
|
||||||
|
start, end = run
|
||||||
|
top, bottom = span
|
||||||
|
if end > top and start < bottom:
|
||||||
|
return 0
|
||||||
|
return top - end if end <= top else start - bottom
|
||||||
|
|
||||||
|
|
||||||
|
def _assign(
|
||||||
runs: list[tuple[int, int]],
|
runs: list[tuple[int, int]],
|
||||||
top: int,
|
anchors: list[tuple[int, int]],
|
||||||
bottom: int,
|
reaches: list[float],
|
||||||
max_gap: float,
|
) -> list[tuple[int, int]]:
|
||||||
others: list[tuple[int, int]],
|
"""Give every ink run to one system, and return each system's extent.
|
||||||
) -> tuple[int, int]:
|
|
||||||
"""Grow a bracket span over the ink close to it.
|
|
||||||
|
|
||||||
Lyrics printed below the last staff, and the tempo mark or section label
|
A run between two systems is resolved by **precedence, not proximity**: the
|
||||||
printed above the first, sit within about a staff height of the bracket and
|
system above wins if the run is within its reach. Text printed under a staff
|
||||||
get absorbed. A title block or a copyright footer is far further away and
|
belongs to that staff, and engravers space lyrics generously — on *Feliz
|
||||||
does not.
|
Navidad* a lyric line sits 43px under its own system's bracket but only 10px
|
||||||
|
above the next one's, so nearest-bracket gives it to the wrong system.
|
||||||
|
|
||||||
Distance is measured from the *bracket*, never from the growing extent: a
|
Distance is measured from the *bracket*, never from a growing extent — a
|
||||||
title block's credit lines are stacked closely enough that a chaining
|
title block's credit lines are stacked closely enough that a chaining
|
||||||
expansion hops from one to the next and walks the whole way up the page.
|
expansion hops from one to the next and walks the whole way up the page.
|
||||||
|
|
||||||
|
One pass over all systems, rather than each bracket expanding on its own, so
|
||||||
|
that a run has exactly one owner and extents cannot overlap.
|
||||||
|
|
||||||
|
Known limit: when a lyric line is printed tight enough under its system that
|
||||||
|
no blank row separates it from the *next* system's staves, the two fuse into
|
||||||
|
a single ink run and no row profile can split them — the lyric is then given
|
||||||
|
to the system below and the cut lands high. Dragging the cut is the fix;
|
||||||
|
separating them needs a signal this pass doesn't have.
|
||||||
"""
|
"""
|
||||||
lo, hi = top, bottom
|
bounds = [list(a) for a in anchors]
|
||||||
for start, end in runs:
|
|
||||||
if any(end > o[0] and start < o[1] for o in others):
|
def claim(index: int, run: tuple[int, int]) -> None:
|
||||||
continue # belongs to a different system
|
bounds[index][0] = min(bounds[index][0], run[0])
|
||||||
if end > top and start < bottom: # overlaps the bracket itself
|
bounds[index][1] = max(bounds[index][1], run[1])
|
||||||
lo, hi = min(lo, start), max(hi, end)
|
|
||||||
elif end <= top and top - end <= max_gap:
|
for run in runs:
|
||||||
lo = min(lo, start)
|
gaps = [_gap(run, a) for a in anchors]
|
||||||
elif start >= bottom and start - bottom <= max_gap:
|
|
||||||
hi = max(hi, end)
|
# Ink overlapping a bracket belongs to it — to the one it overlaps most,
|
||||||
return lo, hi
|
# whatever else is in reach.
|
||||||
|
inside = [
|
||||||
|
(min(run[1], anchors[i][1]) - max(run[0], anchors[i][0]), i)
|
||||||
|
for i, g in enumerate(gaps)
|
||||||
|
if g == 0
|
||||||
|
]
|
||||||
|
if inside:
|
||||||
|
claim(max(inside)[1], run)
|
||||||
|
continue
|
||||||
|
|
||||||
|
within = [i for i, g in enumerate(gaps) if g <= reaches[i]]
|
||||||
|
if not within:
|
||||||
|
continue # a title block or a footer: too far from any system
|
||||||
|
|
||||||
|
# Otherwise the system above wins, and only failing that the one below.
|
||||||
|
above = [i for i in within if anchors[i][1] <= run[0]]
|
||||||
|
claim(above[-1] if above else within[0], run)
|
||||||
|
|
||||||
|
return [(lo, hi) for lo, hi in bounds]
|
||||||
|
|
||||||
|
|
||||||
def detect_page(gray: np.ndarray, skew: float | None = None) -> PageDetection:
|
def detect_page(gray: np.ndarray, skew: float | None = None) -> PageDetection:
|
||||||
@@ -201,15 +236,14 @@ def detect_page(gray: np.ndarray, skew: float | None = None) -> PageDetection:
|
|||||||
anchors = system_anchors(straight)
|
anchors = system_anchors(straight)
|
||||||
|
|
||||||
if anchors:
|
if anchors:
|
||||||
systems = []
|
# Staff height is measured on the bracket span, before expansion, so a
|
||||||
for i, (top, bottom) in enumerate(anchors):
|
# swallowed title block can't distort it.
|
||||||
# Staff height is measured on the bracket span, before expansion,
|
heights = [staff_height(straight, top, bottom) for top, bottom in anchors]
|
||||||
# so a swallowed title block can't distort it.
|
reaches = [(h or gray.shape[0] * 0.02) * _EXPAND_REACH for h in heights]
|
||||||
height = staff_height(straight, top, bottom)
|
systems = [
|
||||||
others = [a for j, a in enumerate(anchors) if j != i]
|
System(top=lo, bottom=hi, staff_height=h)
|
||||||
reach = (height or gray.shape[0] * 0.02) * _EXPAND_REACH
|
for (lo, hi), h in zip(_assign(runs, anchors, reaches), heights)
|
||||||
lo, hi = _expand(runs, top, bottom, reach, others)
|
]
|
||||||
systems.append(System(top=lo, bottom=hi, staff_height=height))
|
|
||||||
else:
|
else:
|
||||||
# No bracket: a single-staff melody or lead sheet, where every ink run
|
# No bracket: a single-staff melody or lead sheet, where every ink run
|
||||||
# genuinely is its own system.
|
# genuinely is its own system.
|
||||||
|
|||||||
Reference in New Issue
Block a user