avans06 commited on
Commit
93497a7
·
1 Parent(s): a627433

Add an option to attempt border removal in the input parameters.

Browse files
Files changed (2) hide show
  1. app.py +11 -1
  2. image_processing/panel.py +160 -0
app.py CHANGED
@@ -14,7 +14,7 @@ import tempfile
14
  import shutil
15
  from tqdm import tqdm
16
 
17
- from image_processing.panel import generate_panel_blocks, generate_panel_blocks_by_ai
18
 
19
  # --- UI Description ---
20
  DESCRIPTION = """
@@ -31,6 +31,7 @@ def process_images(
31
  method,
32
  separate_folders,
33
  rtl_order,
 
34
  # Traditional method params
35
  merge_mode,
36
  split_joint,
@@ -98,6 +99,8 @@ def process_images(
98
 
99
  # Save each panel block
100
  for i, panel in enumerate(panel_blocks):
 
 
101
  if separate_folders:
102
  # e.g., /tmp/xyz/image1/panel_0.png
103
  panel_filename = f"panel_{i}{file_ext if file_ext else '.png'}"
@@ -177,6 +180,12 @@ def main():
177
  info="Check this for manga that is read from right to left. Uncheck for western comics."
178
  )
179
 
 
 
 
 
 
 
180
  # --- Shared Parameters ---
181
  gr.Markdown("### Shared Parameters")
182
  merge_mode = gr.Dropdown(
@@ -229,6 +238,7 @@ def main():
229
  method,
230
  separate_folders,
231
  rtl_order,
 
232
  merge_mode,
233
  split_joint,
234
  fallback,
 
14
  import shutil
15
  from tqdm import tqdm
16
 
17
+ from image_processing.panel import generate_panel_blocks, generate_panel_blocks_by_ai, remove_border
18
 
19
  # --- UI Description ---
20
  DESCRIPTION = """
 
31
  method,
32
  separate_folders,
33
  rtl_order,
34
+ remove_borders,
35
  # Traditional method params
36
  merge_mode,
37
  split_joint,
 
99
 
100
  # Save each panel block
101
  for i, panel in enumerate(panel_blocks):
102
+ if remove_borders:
103
+ panel = remove_border(panel)
104
  if separate_folders:
105
  # e.g., /tmp/xyz/image1/panel_0.png
106
  panel_filename = f"panel_{i}{file_ext if file_ext else '.png'}"
 
180
  info="Check this for manga that is read from right to left. Uncheck for western comics."
181
  )
182
 
183
+ remove_borders = gr.Checkbox(
184
+ label="Attempt to remove panel borders",
185
+ value=False,
186
+ info="Crops the image to the content area. May not be perfect for all images."
187
+ )
188
+
189
  # --- Shared Parameters ---
190
  gr.Markdown("### Shared Parameters")
191
  merge_mode = gr.Dropdown(
 
238
  method,
239
  separate_folders,
240
  rtl_order,
241
+ remove_borders,
242
  merge_mode,
243
  split_joint,
244
  fallback,
image_processing/panel.py CHANGED
@@ -525,6 +525,7 @@ def extract_panels_for_images_in_folder(
525
  num_panels += len(panel_blocks)
526
  return (num_files, num_panels)
527
 
 
528
  def extract_panels_for_images_in_folder_by_ai(
529
  input_dir: str,
530
  output_dir: str
@@ -547,3 +548,162 @@ def extract_panels_for_images_in_folder_by_ai(
547
  num_panels += len(panel_blocks)
548
  return (num_files, num_panels)
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  num_panels += len(panel_blocks)
526
  return (num_files, num_panels)
527
 
528
+
529
  def extract_panels_for_images_in_folder_by_ai(
530
  input_dir: str,
531
  output_dir: str
 
548
  num_panels += len(panel_blocks)
549
  return (num_files, num_panels)
550
 
551
+
552
+ # Ensure the thinning function is available
553
+ try:
554
+ # Attempt to import the thinning function from the contrib module
555
+ from cv2.ximgproc import thinning
556
+ except ImportError:
557
+ # If opencv-contrib-python is not installed, print a warning and provide a dummy function
558
+ print("Warning: cv2.ximgproc.thinning not found. Border removal might be less effective.")
559
+ print("Please install 'opencv-contrib-python' via 'pip install opencv-contrib-python'")
560
+ def thinning(src, thinningType=None): # Dummy function to prevent crashes
561
+ return src
562
+
563
+
564
+ def _find_best_border_line(roi_mask: np.ndarray, axis: int, scan_range: range) -> int:
565
+ """
566
+ A helper function to find the best border line along a single axis.
567
+ It scans from the inside-out and returns the index of the line with the highest score.
568
+
569
+ Parameters:
570
+ - roi_mask: The skeletonized mask of the panel's border area.
571
+ - axis: The axis to scan along (0 for vertical, 1 for horizontal).
572
+ - scan_range: The range of indices to scan (defines direction and search zone).
573
+
574
+ Returns:
575
+ - The index of the most likely border line.
576
+ """
577
+ best_index, max_score = scan_range.start, -1
578
+
579
+ # The total span of the search, used for normalizing the position weight.
580
+ total_span = abs(scan_range.stop - scan_range.start)
581
+ if total_span == 0:
582
+ return best_index
583
+
584
+ for i in scan_range:
585
+ # Calculate continuity score based on the scan axis
586
+ if axis == 1: # Horizontal scan (for top/bottom borders)
587
+ continuity_score = np.count_nonzero(roi_mask[i, :])
588
+ else: # Vertical scan (for left/right borders)
589
+ continuity_score = np.count_nonzero(roi_mask[:, i])
590
+
591
+ # Position weight increases as we move from the start (inner) to the end (outer) of the range.
592
+ # This prioritizes lines closer to the physical edge of the panel.
593
+ progress = abs(i - scan_range.start)
594
+ position_weight = progress / total_span
595
+
596
+ # Combine scores
597
+ score = continuity_score * (1 + position_weight)
598
+
599
+ # Update if we found a better candidate
600
+ if score >= max_score:
601
+ max_score, best_index = score, i
602
+
603
+ return best_index
604
+
605
+
606
+ def remove_border(panel_image: np.ndarray,
607
+ search_zone_ratio: float = 0.25,
608
+ padding: int = 5) -> np.ndarray:
609
+ """
610
+ Removes borders using skeletonization and weighted projection analysis.
611
+ This definitive version accurately finds the innermost border line by reducing
612
+ all contour lines to a single-pixel width, eliminating thickness bias from
613
+ speech bubble intersections.
614
+
615
+ Parameters:
616
+ - panel_image: The input panel image.
617
+ - search_zone_ratio: The percentage of the panel's width/height from the edge
618
+ to define the search area for a border (e.g., 0.25 = 25%).
619
+ - padding: Pixels to add inside the final detected border to avoid clipping art.
620
+
621
+ Returns:
622
+ - The cropped panel image, or the original if processing fails.
623
+ """
624
+ # Return original image if it's invalid or too small to process
625
+ if panel_image is None or panel_image.shape[0] < 30 or panel_image.shape[1] < 30:
626
+ return panel_image
627
+
628
+ # --- 1. Preparation ---
629
+ # Add a safe, white border to separate the panel's border from the image edge
630
+ pad_size = 15
631
+ padded_image = cv2.copyMakeBorder(
632
+ panel_image, pad_size, pad_size, pad_size, pad_size,
633
+ cv2.BORDER_CONSTANT, value=[255, 255, 255]
634
+ )
635
+
636
+ # Convert to grayscale and binarize to highlight non-white areas
637
+ gray = cv2.cvtColor(padded_image, cv2.COLOR_BGR2GRAY)
638
+ _, thresh = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)
639
+
640
+ # Find the outermost contour, which should now be the panel itself
641
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
642
+
643
+ # If no contours are found, there's nothing to process
644
+ if not contours:
645
+ return panel_image
646
+
647
+ # The largest contour is almost always the panel we want
648
+ largest_contour = max(contours, key=cv2.contourArea)
649
+ x, y, w, h = cv2.boundingRect(largest_contour)
650
+
651
+ # --- 2. Create Skeletonized Mask ---
652
+ # Create a mask by filling the largest contour
653
+ filled_mask = np.zeros_like(gray)
654
+ cv2.drawContours(filled_mask, [largest_contour], -1, 255, cv2.FILLED)
655
+
656
+ # Create a hollow version of the contour to provide a clean input for skeletonization.
657
+ # Use a fixed number of erosion iterations to define the thickness of the hollow ring.
658
+ erosion_iterations = 5
659
+ hollow_contour = cv2.subtract(filled_mask, cv2.erode(filled_mask, np.ones((3,3), np.uint8), iterations=erosion_iterations))
660
+
661
+ # Perform skeletonization to reduce varied-thickness lines to a single-pixel-wide skeleton
662
+ skeleton = thinning(hollow_contour)
663
+
664
+ # Crop the skeleton mask to the Region of Interest (ROI) for analysis
665
+ roi_mask = skeleton[y:y+h, x:x+w]
666
+
667
+ # --- 3. Find Borders using the Helper Function ---
668
+ # Define search zones and scan ranges for each border
669
+ top_search_end = int(h * search_zone_ratio)
670
+ bottom_search_start = h - top_search_end
671
+ left_search_end = int(w * search_zone_ratio)
672
+ right_search_start = w - left_search_end
673
+
674
+ # The scan_range determines the direction (inside-out)
675
+ top_range = range(top_search_end, -1, -1)
676
+ bottom_range = range(bottom_search_start, h)
677
+ left_range = range(left_search_end, -1, -1)
678
+ right_range = range(right_search_start, w)
679
+
680
+ # Call the common function for each border
681
+
682
+ # --- Find Top Border ---
683
+ best_top_y = _find_best_border_line(roi_mask, axis=1, scan_range=top_range)
684
+ # --- Find Bottom Border ---
685
+ best_bottom_y = _find_best_border_line(roi_mask, axis=1, scan_range=bottom_range)
686
+ # --- Find Left Border ---
687
+ best_left_x = _find_best_border_line(roi_mask, axis=0, scan_range=left_range)
688
+ # --- Find Right Border ---
689
+ best_right_x = _find_best_border_line(roi_mask, axis=0, scan_range=right_range)
690
+
691
+ # --- 4. Final Cropping ---
692
+ # Convert relative ROI coordinates back to the global coordinates of the padded image and apply padding
693
+ final_x1 = x + best_left_x + padding
694
+ final_y1 = y + best_top_y + padding
695
+ final_x2 = x + best_right_x - padding
696
+ final_y2 = y + best_bottom_y - padding
697
+
698
+ # If the calculated coordinates are invalid, return the original image
699
+ if final_x1 >= final_x2 or final_y1 >= final_y2:
700
+ return panel_image
701
+
702
+ # Crop the final result from the padded image
703
+ cropped = padded_image[final_y1:final_y2, final_x1:final_x2]
704
+
705
+ # Perform a final check to ensure the cropped image is not too small
706
+ if cropped.shape[0] < 10 or cropped.shape[1] < 10:
707
+ return panel_image
708
+
709
+ return cropped