Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -105,222 +105,6 @@ def supports_keyword(callable_obj, keyword):
|
|
| 105 |
return keyword in signature.parameters
|
| 106 |
|
| 107 |
|
| 108 |
-
def _order_points(points):
|
| 109 |
-
import numpy as np
|
| 110 |
-
|
| 111 |
-
rect = np.zeros((4, 2), dtype="float32")
|
| 112 |
-
point_sum = points.sum(axis=1)
|
| 113 |
-
point_diff = np.diff(points, axis=1)
|
| 114 |
-
|
| 115 |
-
rect[0] = points[np.argmin(point_sum)]
|
| 116 |
-
rect[2] = points[np.argmax(point_sum)]
|
| 117 |
-
rect[1] = points[np.argmin(point_diff)]
|
| 118 |
-
rect[3] = points[np.argmax(point_diff)]
|
| 119 |
-
return rect
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def _warp_largest_page(image: Image.Image) -> Image.Image:
|
| 123 |
-
try:
|
| 124 |
-
import cv2
|
| 125 |
-
import numpy as np
|
| 126 |
-
except ImportError:
|
| 127 |
-
return image
|
| 128 |
-
|
| 129 |
-
rgb = np.array(image)
|
| 130 |
-
height, width = rgb.shape[:2]
|
| 131 |
-
max_side = max(width, height)
|
| 132 |
-
scale = 1200 / max_side if max_side > 1200 else 1.0
|
| 133 |
-
|
| 134 |
-
if scale != 1.0:
|
| 135 |
-
scan = cv2.resize(rgb, (int(width * scale), int(height * scale)))
|
| 136 |
-
else:
|
| 137 |
-
scan = rgb
|
| 138 |
-
|
| 139 |
-
gray = cv2.cvtColor(scan, cv2.COLOR_RGB2GRAY)
|
| 140 |
-
gray = cv2.GaussianBlur(gray, (5, 5), 0)
|
| 141 |
-
edges = cv2.Canny(gray, 50, 150)
|
| 142 |
-
edges = cv2.dilate(edges, np.ones((5, 5), dtype=np.uint8), iterations=1)
|
| 143 |
-
|
| 144 |
-
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 145 |
-
image_area = scan.shape[0] * scan.shape[1]
|
| 146 |
-
for contour in sorted(contours, key=cv2.contourArea, reverse=True)[:8]:
|
| 147 |
-
area = cv2.contourArea(contour)
|
| 148 |
-
if area < image_area * 0.25:
|
| 149 |
-
continue
|
| 150 |
-
|
| 151 |
-
perimeter = cv2.arcLength(contour, True)
|
| 152 |
-
approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
|
| 153 |
-
if len(approx) != 4:
|
| 154 |
-
continue
|
| 155 |
-
|
| 156 |
-
points = approx.reshape(4, 2).astype("float32") / scale
|
| 157 |
-
rect = _order_points(points)
|
| 158 |
-
top_left, top_right, bottom_right, bottom_left = rect
|
| 159 |
-
|
| 160 |
-
target_width = int(max(
|
| 161 |
-
np.linalg.norm(bottom_right - bottom_left),
|
| 162 |
-
np.linalg.norm(top_right - top_left),
|
| 163 |
-
))
|
| 164 |
-
target_height = int(max(
|
| 165 |
-
np.linalg.norm(top_right - bottom_right),
|
| 166 |
-
np.linalg.norm(top_left - bottom_left),
|
| 167 |
-
))
|
| 168 |
-
if target_width < 200 or target_height < 200:
|
| 169 |
-
continue
|
| 170 |
-
|
| 171 |
-
destination = np.array([
|
| 172 |
-
[0, 0],
|
| 173 |
-
[target_width - 1, 0],
|
| 174 |
-
[target_width - 1, target_height - 1],
|
| 175 |
-
[0, target_height - 1],
|
| 176 |
-
], dtype="float32")
|
| 177 |
-
matrix = cv2.getPerspectiveTransform(rect, destination)
|
| 178 |
-
warped = cv2.warpPerspective(rgb, matrix, (target_width, target_height))
|
| 179 |
-
return Image.fromarray(warped)
|
| 180 |
-
|
| 181 |
-
return image
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
def _crop_to_music_content(image: Image.Image) -> Image.Image:
|
| 185 |
-
staff_crop = _crop_to_staff_band(image)
|
| 186 |
-
if staff_crop is not None:
|
| 187 |
-
return staff_crop
|
| 188 |
-
|
| 189 |
-
try:
|
| 190 |
-
import numpy as np
|
| 191 |
-
except ImportError:
|
| 192 |
-
return image
|
| 193 |
-
|
| 194 |
-
gray = np.array(image.convert("L"))
|
| 195 |
-
content = gray < 235
|
| 196 |
-
row_density = content.mean(axis=1)
|
| 197 |
-
col_density = content.mean(axis=0)
|
| 198 |
-
rows = np.where(row_density > 0.01)[0]
|
| 199 |
-
cols = np.where(col_density > 0.004)[0]
|
| 200 |
-
if len(rows) == 0 or len(cols) == 0:
|
| 201 |
-
return image
|
| 202 |
-
|
| 203 |
-
width, height = image.size
|
| 204 |
-
left, right = int(cols[0]), int(cols[-1])
|
| 205 |
-
top, bottom = _select_music_row_band(row_density, rows)
|
| 206 |
-
content_width = right - left + 1
|
| 207 |
-
content_height = bottom - top + 1
|
| 208 |
-
if (
|
| 209 |
-
content_width * content_height < width * height * 0.12
|
| 210 |
-
and (content_width < width * 0.35 or content_height < 48)
|
| 211 |
-
):
|
| 212 |
-
return image
|
| 213 |
-
|
| 214 |
-
pad_x = max(int(content_width * 0.04), 24)
|
| 215 |
-
pad_y = max(int(content_height * 0.18), 24)
|
| 216 |
-
return image.crop((
|
| 217 |
-
max(left - pad_x, 0),
|
| 218 |
-
max(top - pad_y, 0),
|
| 219 |
-
min(right + pad_x, width),
|
| 220 |
-
min(bottom + pad_y, height),
|
| 221 |
-
))
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
def _crop_to_staff_band(image: Image.Image):
|
| 225 |
-
try:
|
| 226 |
-
import cv2
|
| 227 |
-
import numpy as np
|
| 228 |
-
except ImportError:
|
| 229 |
-
return None
|
| 230 |
-
|
| 231 |
-
rgb = np.array(image)
|
| 232 |
-
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
| 233 |
-
height, width = gray.shape[:2]
|
| 234 |
-
if width < 80 or height < 40:
|
| 235 |
-
return None
|
| 236 |
-
|
| 237 |
-
# Normalize uneven camera lighting before looking for staff lines.
|
| 238 |
-
background = cv2.medianBlur(gray, 31)
|
| 239 |
-
normalized = cv2.divide(gray, background, scale=255)
|
| 240 |
-
thresholded = cv2.adaptiveThreshold(
|
| 241 |
-
normalized,
|
| 242 |
-
255,
|
| 243 |
-
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
| 244 |
-
cv2.THRESH_BINARY_INV,
|
| 245 |
-
31,
|
| 246 |
-
12,
|
| 247 |
-
)
|
| 248 |
-
|
| 249 |
-
horizontal_kernel = cv2.getStructuringElement(
|
| 250 |
-
cv2.MORPH_RECT,
|
| 251 |
-
(max(width // 18, 35), 1),
|
| 252 |
-
)
|
| 253 |
-
staff_lines = cv2.morphologyEx(thresholded, cv2.MORPH_OPEN, horizontal_kernel)
|
| 254 |
-
row_strength = staff_lines.mean(axis=1) / 255.0
|
| 255 |
-
candidate_rows = np.where(row_strength > max(0.03, row_strength.max() * 0.25))[0]
|
| 256 |
-
if len(candidate_rows) < 3:
|
| 257 |
-
return None
|
| 258 |
-
|
| 259 |
-
top, bottom = _select_music_row_band(row_strength, candidate_rows)
|
| 260 |
-
line_span = bottom - top + 1
|
| 261 |
-
if line_span < 10:
|
| 262 |
-
return None
|
| 263 |
-
|
| 264 |
-
band_pad = max(int(line_span * 1.6), 48)
|
| 265 |
-
top = max(top - band_pad, 0)
|
| 266 |
-
bottom = min(bottom + band_pad, height - 1)
|
| 267 |
-
|
| 268 |
-
band_mask = thresholded[top:bottom + 1, :]
|
| 269 |
-
col_strength = band_mask.mean(axis=0) / 255.0
|
| 270 |
-
active_cols = np.where(col_strength > max(0.006, col_strength.max() * 0.08))[0]
|
| 271 |
-
if len(active_cols) < width * 0.2:
|
| 272 |
-
return None
|
| 273 |
-
|
| 274 |
-
left, right = int(active_cols[0]), int(active_cols[-1])
|
| 275 |
-
content_width = right - left + 1
|
| 276 |
-
content_height = bottom - top + 1
|
| 277 |
-
if content_width < 120 or content_height < 40:
|
| 278 |
-
return None
|
| 279 |
-
|
| 280 |
-
pad_x = max(int(content_width * 0.03), 20)
|
| 281 |
-
pad_y = max(int(content_height * 0.08), 12)
|
| 282 |
-
return image.crop((
|
| 283 |
-
max(left - pad_x, 0),
|
| 284 |
-
max(top - pad_y, 0),
|
| 285 |
-
min(right + pad_x, width),
|
| 286 |
-
min(bottom + pad_y, height),
|
| 287 |
-
))
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def _select_music_row_band(row_density, active_rows):
|
| 291 |
-
try:
|
| 292 |
-
import numpy as np
|
| 293 |
-
except ImportError:
|
| 294 |
-
return int(active_rows[0]), int(active_rows[-1])
|
| 295 |
-
|
| 296 |
-
active = row_density > 0.01
|
| 297 |
-
runs = []
|
| 298 |
-
start = None
|
| 299 |
-
for index, is_active in enumerate(active):
|
| 300 |
-
if is_active and start is None:
|
| 301 |
-
start = index
|
| 302 |
-
elif not is_active and start is not None:
|
| 303 |
-
runs.append((start, index - 1))
|
| 304 |
-
start = None
|
| 305 |
-
if start is not None:
|
| 306 |
-
runs.append((start, len(active) - 1))
|
| 307 |
-
|
| 308 |
-
if not runs:
|
| 309 |
-
return int(active_rows[0]), int(active_rows[-1])
|
| 310 |
-
|
| 311 |
-
first, last = int(active_rows[0]), int(active_rows[-1])
|
| 312 |
-
total_height = last - first + 1
|
| 313 |
-
if total_height < len(row_density) * 0.55:
|
| 314 |
-
return first, last
|
| 315 |
-
|
| 316 |
-
smoothed = np.convolve(row_density, np.ones(9) / 9, mode="same")
|
| 317 |
-
best = max(
|
| 318 |
-
runs,
|
| 319 |
-
key=lambda run: float(smoothed[run[0]:run[1] + 1].sum()) * max(run[1] - run[0] + 1, 1),
|
| 320 |
-
)
|
| 321 |
-
return int(best[0]), int(best[1])
|
| 322 |
-
|
| 323 |
-
|
| 324 |
def _ensure_horizontal_music(image: Image.Image) -> Image.Image:
|
| 325 |
width, height = image.size
|
| 326 |
if height > width:
|
|
@@ -328,75 +112,6 @@ def _ensure_horizontal_music(image: Image.Image) -> Image.Image:
|
|
| 328 |
return image
|
| 329 |
|
| 330 |
|
| 331 |
-
def _deskew_image(image: Image.Image) -> Image.Image:
|
| 332 |
-
"""Correct slight rotation by aligning detected staff lines to horizontal."""
|
| 333 |
-
try:
|
| 334 |
-
import cv2
|
| 335 |
-
import numpy as np
|
| 336 |
-
except ImportError:
|
| 337 |
-
return image
|
| 338 |
-
|
| 339 |
-
rgb = np.array(image)
|
| 340 |
-
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
| 341 |
-
height, width = gray.shape[:2]
|
| 342 |
-
if width < 120 or height < 40:
|
| 343 |
-
return image
|
| 344 |
-
|
| 345 |
-
background = cv2.medianBlur(gray, 31)
|
| 346 |
-
normalized = cv2.divide(gray, background, scale=255)
|
| 347 |
-
edges = cv2.Canny(normalized, 30, 100)
|
| 348 |
-
|
| 349 |
-
lines = cv2.HoughLines(edges, 1, np.pi / 360, threshold=max(width // 5, 40))
|
| 350 |
-
if lines is None:
|
| 351 |
-
return image
|
| 352 |
-
|
| 353 |
-
angles = []
|
| 354 |
-
for line in lines:
|
| 355 |
-
rho, theta = line[0]
|
| 356 |
-
angle_from_horizontal = theta - np.pi / 2
|
| 357 |
-
if abs(angle_from_horizontal) < np.radians(10):
|
| 358 |
-
angles.append(angle_from_horizontal)
|
| 359 |
-
|
| 360 |
-
if len(angles) < 2:
|
| 361 |
-
return image
|
| 362 |
-
|
| 363 |
-
median_angle = float(np.median(angles))
|
| 364 |
-
if abs(median_angle) < np.radians(0.3):
|
| 365 |
-
return image
|
| 366 |
-
|
| 367 |
-
angle_deg = max(-10.0, min(10.0, float(np.degrees(median_angle))))
|
| 368 |
-
return image.rotate(-angle_deg, resample=Image.Resampling.BICUBIC, expand=False, fillcolor=(255, 255, 255))
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
def _binarize_to_training_style(image: Image.Image) -> Image.Image:
|
| 372 |
-
"""Convert to clean black-on-white to match training image style."""
|
| 373 |
-
try:
|
| 374 |
-
import cv2
|
| 375 |
-
import numpy as np
|
| 376 |
-
except ImportError:
|
| 377 |
-
return image
|
| 378 |
-
|
| 379 |
-
gray = np.array(image.convert("L"))
|
| 380 |
-
height, width = gray.shape[:2]
|
| 381 |
-
|
| 382 |
-
# Normalize uneven camera lighting before thresholding.
|
| 383 |
-
blur_size = max(min(width, height) // 10, 15) | 1
|
| 384 |
-
background = cv2.medianBlur(gray, blur_size)
|
| 385 |
-
normalized = cv2.divide(gray, background, scale=255)
|
| 386 |
-
|
| 387 |
-
block_size = max(min(width, height) // 20, 11) | 1
|
| 388 |
-
binary = cv2.adaptiveThreshold(
|
| 389 |
-
normalized,
|
| 390 |
-
255,
|
| 391 |
-
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
| 392 |
-
cv2.THRESH_BINARY,
|
| 393 |
-
block_size,
|
| 394 |
-
6,
|
| 395 |
-
)
|
| 396 |
-
binary = cv2.medianBlur(binary, 3)
|
| 397 |
-
return Image.fromarray(binary).convert("RGB")
|
| 398 |
-
|
| 399 |
-
|
| 400 |
def _score_training_orientation(image: Image.Image):
|
| 401 |
try:
|
| 402 |
import cv2
|
|
@@ -526,10 +241,7 @@ def _normalize_to_training_orientation(image: Image.Image):
|
|
| 526 |
candidates = []
|
| 527 |
for rotation in (0, 90, 180, 270):
|
| 528 |
candidate = image.rotate(rotation, expand=True) if rotation else image.copy()
|
| 529 |
-
candidate = _warp_largest_page(candidate)
|
| 530 |
-
candidate = _crop_to_music_content(candidate)
|
| 531 |
candidate = _ensure_horizontal_music(candidate)
|
| 532 |
-
candidate = _deskew_image(candidate)
|
| 533 |
score = _score_training_orientation(candidate)
|
| 534 |
candidates.append((score, rotation, candidate))
|
| 535 |
|
|
@@ -555,7 +267,6 @@ def preprocess_sheet_music_image(image_path):
|
|
| 555 |
original_size = original.size
|
| 556 |
image, rotation, orientation_score = _normalize_to_training_orientation(original)
|
| 557 |
image = _limit_image_size(image)
|
| 558 |
-
image = _binarize_to_training_style(image)
|
| 559 |
processed_size = image.size
|
| 560 |
|
| 561 |
temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
|
|
|
| 105 |
return keyword in signature.parameters
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def _ensure_horizontal_music(image: Image.Image) -> Image.Image:
|
| 109 |
width, height = image.size
|
| 110 |
if height > width:
|
|
|
|
| 112 |
return image
|
| 113 |
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
def _score_training_orientation(image: Image.Image):
|
| 116 |
try:
|
| 117 |
import cv2
|
|
|
|
| 241 |
candidates = []
|
| 242 |
for rotation in (0, 90, 180, 270):
|
| 243 |
candidate = image.rotate(rotation, expand=True) if rotation else image.copy()
|
|
|
|
|
|
|
| 244 |
candidate = _ensure_horizontal_music(candidate)
|
|
|
|
| 245 |
score = _score_training_orientation(candidate)
|
| 246 |
candidates.append((score, rotation, candidate))
|
| 247 |
|
|
|
|
| 267 |
original_size = original.size
|
| 268 |
image, rotation, orientation_score = _normalize_to_training_orientation(original)
|
| 269 |
image = _limit_image_size(image)
|
|
|
|
| 270 |
processed_size = image.size
|
| 271 |
|
| 272 |
temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|