johnlockejrr Cursor commited on
Commit
398eee2
·
1 Parent(s): 5cd4b66

Mirror hardened BLLA polygonizer from main repo

Browse files

CLAHE + Sauvola fallback + median seam + convex-hull recovery + int32 fix.

Co-authored-by: Cursor <cursoragent@cursor.com>

regnetx_infer/serialization/blla_polygonal_environment.py CHANGED
@@ -23,7 +23,8 @@ from scipy.ndimage import affine_transform, binary_erosion, distance_transform_c
23
  from shapely.ops import nearest_points, unary_union
24
  from shapely.validation import explain_validity
25
  from skimage import draw
26
- from skimage.filters import sobel
 
27
  from skimage.transform import AffineTransform, warp
28
 
29
  logger = logging.getLogger(__name__)
@@ -286,7 +287,9 @@ def _extract_patch(
286
  polygon = np.concatenate(([end_points[-1]], upper_seam, [end_points[0]], bottom_seam))
287
  polygon = geom.Polygon(polygon)
288
  if not polygon.is_valid:
289
- raise Exception(f"Invalid bounding polygon computed: {explain_validity(polygon)}")
 
 
290
  inter = roi_polygon.intersection(polygon)
291
  if inter.is_empty:
292
  raise Exception("ROI ∩ polygon is empty")
@@ -366,8 +369,8 @@ def _calc_roi(line, bounds, baselines, suppl_obj, p_dir):
366
  )
367
  env_up.append(_coords_array(upper_limit)[0])
368
  env_bottom.append(_coords_array(bottom_limit)[0])
369
- env_up = np.array(env_up, dtype="uint")
370
- env_bottom = np.array(env_bottom, dtype="uint")
371
  return env_up, env_bottom
372
 
373
 
@@ -397,6 +400,7 @@ def calculate_polygonal_environment(
397
  if suppl_obj is not None:
398
  suppl_obj = [(np.array(bl) * scale_arr).astype("int").tolist() for bl in suppl_obj]
399
 
 
400
  if im_feats is None:
401
  assert im is not None
402
  bounds = np.array(im.size, dtype=float) - 1
@@ -405,6 +409,26 @@ def calculate_polygonal_environment(
405
  else:
406
  bounds = np.array(im_feats.shape[::-1], dtype=float) - 1
407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  polygons: list = []
409
  if suppl_obj is None:
410
  suppl_obj = []
@@ -445,6 +469,30 @@ def calculate_polygonal_environment(
445
  )
446
  )
447
  except Exception as e:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  if raise_on_error:
449
  raise
450
  logger.warning("Polygonizer failed on line %s: %s", idx, e)
 
23
  from shapely.ops import nearest_points, unary_union
24
  from shapely.validation import explain_validity
25
  from skimage import draw
26
+ from skimage.exposure import equalize_adapthist
27
+ from skimage.filters import sobel, threshold_sauvola
28
  from skimage.transform import AffineTransform, warp
29
 
30
  logger = logging.getLogger(__name__)
 
287
  polygon = np.concatenate(([end_points[-1]], upper_seam, [end_points[0]], bottom_seam))
288
  polygon = geom.Polygon(polygon)
289
  if not polygon.is_valid:
290
+ polygon = geom.MultiPoint(np.concatenate([upper_seam, bottom_seam])).convex_hull
291
+ if not polygon.is_valid or polygon.is_empty or polygon.geom_type != "Polygon":
292
+ raise Exception(f"Invalid bounding polygon computed: {explain_validity(polygon)}")
293
  inter = roi_polygon.intersection(polygon)
294
  if inter.is_empty:
295
  raise Exception("ROI ∩ polygon is empty")
 
369
  )
370
  env_up.append(_coords_array(upper_limit)[0])
371
  env_bottom.append(_coords_array(bottom_limit)[0])
372
+ env_up = np.array(env_up, dtype="int32")
373
+ env_bottom = np.array(env_bottom, dtype="int32")
374
  return env_up, env_bottom
375
 
376
 
 
400
  if suppl_obj is not None:
401
  suppl_obj = [(np.array(bl) * scale_arr).astype("int").tolist() for bl in suppl_obj]
402
 
403
+ im_arr: np.ndarray | None = None
404
  if im_feats is None:
405
  assert im is not None
406
  bounds = np.array(im.size, dtype=float) - 1
 
409
  else:
410
  bounds = np.array(im_feats.shape[::-1], dtype=float) - 1
411
 
412
+ # Fallback edge maps (computed lazily on first failure):
413
+ # 1) CLAHE-enhanced Sobel — helps faded ink with even background
414
+ # 2) Sauvola-binarized Sobel — helps extreme low-contrast / foxed pages
415
+ _fallback_cache: dict[str, np.ndarray] = {}
416
+
417
+ def _get_fallback_feats() -> list[np.ndarray]:
418
+ if im_arr is None:
419
+ return []
420
+ out: list[np.ndarray] = []
421
+ if "clahe" not in _fallback_cache:
422
+ clahe = (equalize_adapthist(im_arr, clip_limit=0.03) * 255).astype(np.uint8)
423
+ _fallback_cache["clahe"] = gaussian_filter(sobel(clahe), 0.5)
424
+ out.append(_fallback_cache["clahe"])
425
+ if "sauvola" not in _fallback_cache:
426
+ thresh = threshold_sauvola(im_arr, window_size=25)
427
+ binary = (im_arr < thresh).astype(float)
428
+ _fallback_cache["sauvola"] = gaussian_filter(sobel(binary), 0.5)
429
+ out.append(_fallback_cache["sauvola"])
430
+ return out
431
+
432
  polygons: list = []
433
  if suppl_obj is None:
434
  suppl_obj = []
 
469
  )
470
  )
471
  except Exception as e:
472
+ # Retry with fallback edge maps (CLAHE, then Sauvola) before giving up
473
+ recovered = False
474
+ for fb_feats in _get_fallback_feats():
475
+ try:
476
+ polygons.append(
477
+ _extract_patch(
478
+ env_up,
479
+ env_bottom,
480
+ line_arr.astype("int"),
481
+ offset_line.astype("int"),
482
+ end_points,
483
+ p_dir,
484
+ topline,
485
+ offset,
486
+ fb_feats,
487
+ bounds,
488
+ )
489
+ )
490
+ recovered = True
491
+ break
492
+ except Exception:
493
+ continue
494
+ if recovered:
495
+ continue
496
  if raise_on_error:
497
  raise
498
  logger.warning("Polygonizer failed on line %s: %s", idx, e)
regnetx_infer/serialization/polygon.py CHANGED
@@ -97,7 +97,7 @@ def _polygon_scale_tuple(polygon_scale: int, page_w: int, page_h: int) -> tuple[
97
  page already fits within ``polygon_scale`` (we never upscale small pages).
98
  """
99
  s = int(polygon_scale)
100
- if s <= 0 or max(page_w, page_h) <= s:
101
  return None
102
  return (s, 0)
103
 
 
97
  page already fits within ``polygon_scale`` (we never upscale small pages).
98
  """
99
  s = int(polygon_scale)
100
+ if s <= 0 or max(page_w, page_h) <= int(s * 1.3):
101
  return None
102
  return (s, 0)
103