stella-score-reader / score_system_split.py
CAY96
WIP: μ‹œμŠ€ν…œ λΆ„ν• (score_system_split), μŠ€νƒœν”„ κ²€μΆœ 보정, 첫 μŠ€νƒœν”„λ§Œ 클리프 검사
fe30d6e
"""
μ„Έλ‘œλ‘œ κΈ΄ 악보 νŽ˜μ΄μ§€λ₯Ό μ‹œμŠ€ν…œ(ν°λ³΄ν‘œ λ‹¨μœ„) κ²½κ³„μ—μ„œ 잘라 staff κ²€μΆœμ„ μ•ˆμ •ν™”ν•œλ‹€.
κ°€λ‘œ 투영(μ˜€μ„  κ°•μ‘°) ν›„ μž‰ν¬κ°€ 였래 λΉ„λŠ” ꡬ간을 μ°Ύμ•„ λΆ„ν• ν•œλ‹€.
"""
from __future__ import annotations
import os
from typing import List, Tuple
import cv2
import numpy as np
_DISABLE = os.environ.get("STELLA_SYSTEM_SPLIT_DISABLE", "").lower() in ("1", "true", "yes")
def horizontal_row_density(image_bgr: np.ndarray) -> np.ndarray:
gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3, 3), 0)
binary = cv2.adaptiveThreshold(
blur,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV,
31,
15,
)
h, w = binary.shape
kernel_width = max(25, w // 12)
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_width, 1))
horizontal = cv2.morphologyEx(binary, cv2.MORPH_OPEN, horizontal_kernel, iterations=1)
return np.sum(horizontal > 0, axis=1).astype(np.float64)
def _moving_average(arr: np.ndarray, window: int) -> np.ndarray:
window = max(3, window | 1)
pad = window // 2
padded = np.pad(arr, (pad, pad), mode="edge")
cumsum = np.cumsum(np.insert(padded, 0, 0))
out = (cumsum[window:] - cumsum[:-window]) / float(window)
return out[: len(arr)]
def _propose_cut_rows(row_density: np.ndarray, h: int) -> List[int]:
"""각 μ‹œμŠ€ν…œ μ‚¬μ΄μ˜ μ»· y(λ°΄λ“œ 경계)."""
if h < 200:
return []
win = max(21, min(71, h // 55))
sm = _moving_average(row_density, win)
mx = float(np.max(sm)) or 1.0
low_thresh = max(12.0, 0.11 * mx)
min_run = max(12, h // 100)
below = sm < low_thresh
runs: List[Tuple[int, int]] = []
i = 0
while i < len(below):
if not below[i]:
i += 1
continue
j = i
while j < len(below) and below[j]:
j += 1
if j - i >= min_run:
runs.append((i, j))
i = j
line_guess = max(10, h // 140)
min_band = max(8 * line_guess, int(h * 0.11))
cuts = [(a + b) // 2 for a, b in runs if (b - a) >= min_run * 0.35]
header = int(h * 0.06)
cuts = [c for c in cuts if c > header + min_band // 2]
merged: List[int] = []
for c in sorted(cuts):
if not merged or c - merged[-1] >= min_band:
merged.append(c)
elif (merged[-1] + c) // 2 != merged[-1]:
merged[-1] = (merged[-1] + c) // 2
filtered: List[int] = []
prev = 0
for c in merged:
if c - prev >= min_band and h - c >= min_band:
filtered.append(c)
prev = c
return filtered
def split_work_bgr_into_bands(work_bgr: np.ndarray) -> List[Tuple[np.ndarray, int, int]]:
"""
work μ’Œν‘œκ³„μ—μ„œ (λ°΄λ“œ 이미지, y0, x0) λͺ©λ‘. x0λŠ” 항상 0.
λΉ„ν™œμ„±Β·μ»· μ—†μŒΒ·λ„ˆλ¬΄ μž‘μ€ νŽ˜μ΄μ§€λ©΄ [(전체, 0, 0)].
"""
if _DISABLE:
return [(work_bgr, 0, 0)]
h, w = work_bgr.shape[:2]
rd = horizontal_row_density(work_bgr)
cuts = _propose_cut_rows(rd, h)
if not cuts:
return [(work_bgr, 0, 0)]
bounds = [0] + cuts + [h]
bands: List[Tuple[np.ndarray, int, int]] = []
for i in range(len(bounds) - 1):
y0, y1 = bounds[i], bounds[i + 1]
if y1 - y0 < 50:
continue
bands.append((work_bgr[y0:y1, 0:w], y0, 0))
return bands if bands else [(work_bgr, 0, 0)]