jon-fernandes commited on
Commit
ca056b9
·
verified ·
1 Parent(s): 10ad82f

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +0 -289
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)