File size: 3,457 Bytes
fe30d6e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""
μ„Έλ‘œλ‘œ κΈ΄ 악보 νŽ˜μ΄μ§€λ₯Ό μ‹œμŠ€ν…œ(ν°λ³΄ν‘œ λ‹¨μœ„) κ²½κ³„μ—μ„œ 잘라 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)]