Spaces:
Runtime error
Runtime error
File size: 28,251 Bytes
8c82386 652f463 8c82386 652f463 8c82386 652f463 | 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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 | import numpy as np
import cv2
import os
import argparse
from pathlib import Path
from skimage.feature import canny
from skimage.morphology import dilation
from scipy import ndimage as ndi
from skimage.measure import label, regionprops
from skimage.color import rgb2gray, label2rgb
from PIL import Image, ImageDraw
try:
import matplotlib.pyplot as plt
except Exception:
plt = None
class MangaPanelSegmenter:
"""
A class for segmenting manga/manhwa pages into individual artwork panels,
excluding speech bubbles and text elements.
"""
def __init__(self, min_panel_area_ratio=0.01, edge_detection_sigma=1.0,
low_threshold=0.1, high_threshold=0.2):
"""
Initialize the segmenter with configurable parameters.
Args:
min_panel_area_ratio: Minimum panel area as a ratio of page area
edge_detection_sigma: Sigma parameter for Canny edge detection
low_threshold: Low threshold for Canny edge detection
high_threshold: High threshold for Canny edge detection
"""
self.min_panel_area_ratio = min_panel_area_ratio
self.edge_detection_sigma = edge_detection_sigma
self.low_threshold = low_threshold
self.high_threshold = high_threshold
# Initialize attributes to None
self.original_image = None
self.grayscale = None
self.masked_grayscale = None
self.segmentation = None
self.labels = None
self.panels = []
self.extracted_panels = []
self.ordered_panels = []
self.height = 0
self.width = 0
def load_image(self, image_path):
"""Load the manga page image."""
try:
self.original_image = cv2.imread(image_path)
if self.original_image is None:
raise ValueError(f"Could not read image file: {image_path}")
self.original_image = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB)
self.height, self.width = self.original_image.shape[:2]
return self.original_image
except Exception as e:
print(f"Error loading image {image_path}: {e}")
return None
def preprocess_for_edge_detection(self):
"""
Preprocess the image to highlight panel borders and reduce noise from text.
Apply techniques to ignore speech bubbles extending outside panels.
"""
if self.original_image is None: return None
# Convert to grayscale
grayscale = rgb2gray(self.original_image)
self.grayscale = grayscale
# Create a binary threshold to identify potential speech bubbles (white areas)
speech_bubble_mask = grayscale > 0.9 # White/near-white areas
# Dilate the speech bubble mask to ensure we capture the entire bubbles
speech_bubble_mask = dilation(speech_bubble_mask, np.ones((5, 5)))
# Create an edge detection mask that excludes speech bubbles
# edge_mask = ~speech_bubble_mask # This wasn't actually used, removing
# Apply the mask to the grayscale image to suppress speech bubbles
masked_grayscale = grayscale.copy()
masked_grayscale[speech_bubble_mask] = 0.5 # Set speech bubble areas to mid-gray
self.masked_grayscale = masked_grayscale
return masked_grayscale
def detect_edges(self):
"""
Detect edges in the image using Canny edge detection.
Focus on panel boundaries while ignoring speech bubbles.
"""
masked_grayscale = self.preprocess_for_edge_detection()
if masked_grayscale is None: return None
# Apply Canny edge detection on the masked grayscale
edges = canny(
masked_grayscale,
sigma=self.edge_detection_sigma,
low_threshold=self.low_threshold,
high_threshold=self.high_threshold
)
# Thicken edges through dilation to ensure panel borders are continuous
# Applying dilation twice for potentially thicker lines
thick_edges = dilation(dilation(edges))
# Fill holes to create solid regions
self.segmentation = ndi.binary_fill_holes(thick_edges)
return self.segmentation
def identify_panels(self):
"""
Identify individual artwork panels from the segmented image.
Ignore speech bubbles and focus on the main panel boundaries.
"""
if self.segmentation is None or self.grayscale is None: return []
# Label each region
labels = label(self.segmentation)
self.labels = labels
# Extract region properties
regions = regionprops(labels)
# Find panel regions (excluding speech bubbles)
initial_panels = []
# Create a binary mask for potential speech bubbles
speech_bubble_mask = self.grayscale > 0.9 # White areas
# --- Simplified Panel Identification ---
# Instead of complex overlap and merging, treat large non-background regions as potential panels
min_area = self.min_panel_area_ratio * self.height * self.width
for region in regions:
# Check if region is large enough (ignore tiny artifacts and background)
if region.area < min_area or region.label == 0: # region.label==0 is background
continue
y_min, x_min, y_max, x_max = region.bbox
# --- Refined Check: Avoid primarily white regions (likely background gaps or large bubbles) ---
region_mask_in_bbox = (labels == region.label)[y_min:y_max, x_min:x_max]
region_pixels_in_bbox = self.grayscale[y_min:y_max, x_min:x_max][region_mask_in_bbox]
white_ratio_in_region = np.sum(region_pixels_in_bbox > 0.9) / region.area
# Skip regions that are primarily white (adjust threshold as needed)
if white_ratio_in_region > 0.85:
# print(f"Skipping region {region.label} due to high white ratio: {white_ratio_in_region:.2f}")
continue
initial_panels.append(region.bbox)
# --- Merge Overlapping Bounding Boxes ---
# This step helps combine regions that might be fragmented parts of the same panel
if not initial_panels:
self.panels = []
return []
merged = True
while merged:
merged = False
merged_panels = []
used = [False] * len(initial_panels)
for i in range(len(initial_panels)):
if used[i]:
continue
current_bbox = list(initial_panels[i]) # Make mutable
used[i] = True
for j in range(i + 1, len(initial_panels)):
if used[j]:
continue
# Check for significant overlap (IoU or simple overlap ratio)
if self._do_bboxes_overlap(current_bbox, initial_panels[j]):
# Merge overlapping boxes
current_bbox = self._merge_bboxes(current_bbox, initial_panels[j])
used[j] = True
merged = True # Indicate that a merge happened in this pass
merged_panels.append(tuple(current_bbox)) # Add the potentially merged box
initial_panels = merged_panels # Update list for next potential merge pass
# --- Final Filtering (Optional: Remove panels fully contained within others) ---
final_panels = []
initial_panels.sort(key=lambda b: (b[2]-b[0])*(b[3]-b[1]), reverse=True) # Sort by area desc
for i, p1 in enumerate(initial_panels):
is_contained = False
for j, p2 in enumerate(initial_panels):
if i != j and self._is_bbox_contained(p1, p2):
is_contained = True
break
if not is_contained:
# Further check: ensure it's not too small after merging/filtering
y_min, x_min, y_max, x_max = p1
if (y_max - y_min) * (x_max - x_min) >= min_area:
final_panels.append(p1)
# Assign to self.panels
self.panels = final_panels
# print(f"Identified {len(self.panels)} panels.")
return self.panels
def extract_artwork_panels(self):
"""
Extract the artwork panels from the original image based on identified bounding boxes.
"""
if self.original_image is None: return []
self.extracted_panels = []
for i, bbox in enumerate(self.panels):
# Ensure bbox coordinates are valid
y_min, x_min, y_max, x_max = bbox
y_min = max(0, y_min)
x_min = max(0, x_min)
y_max = min(self.height, y_max)
x_max = min(self.width, x_max)
# Check if coordinates are still valid after clipping
if y_min >= y_max or x_min >= x_max:
# print(f"Skipping invalid bbox after clipping: {bbox}")
continue
# Extract the panel
panel = self.original_image[y_min:y_max, x_min:x_max].copy()
# Store the panel and its bounding box
self.extracted_panels.append({
'bbox': bbox,
'panel': panel,
'panel_id': i + 1 # Temporary ID, will be updated after ordering
})
return self.extracted_panels
def order_panels(self, reading_order='ltr'):
"""
Order the panels according to typical reading order.
Args:
reading_order (str): 'ltr' (left-to-right) or 'rtl' (right-to-left).
Default is 'ltr'.
"""
if not self.extracted_panels:
self.ordered_panels = []
return []
# Sort panels primarily by their vertical position (top-to-bottom).
# For panels roughly on the same horizontal line, sort by horizontal position.
# A tolerance is used to group panels that are vertically close.
vertical_tolerance = (self.height * 0.05) # e.g., 5% of page height tolerance
# Group panels by approximate vertical position
rows = []
sorted_by_y = sorted(self.extracted_panels, key=lambda p: p['bbox'][0]) # Sort by y_min
if not sorted_by_y:
self.ordered_panels = []
return []
current_row = [sorted_by_y[0]]
last_y_min = sorted_by_y[0]['bbox'][0]
for panel in sorted_by_y[1:]:
y_min = panel['bbox'][0]
# If the current panel's top is close to the last panel's top, consider it in the same row
if abs(y_min - last_y_min) < vertical_tolerance:
current_row.append(panel)
# Update last_y_min to the average or min y of the current row for better grouping?
# For simplicity, let's keep comparing with the first element's top in the row group
else:
# Start a new row
rows.append(current_row)
current_row = [panel]
last_y_min = y_min
rows.append(current_row) # Add the last row
# Sort panels within each row based on reading order
self.ordered_panels = []
for row in rows:
if reading_order == 'rtl':
# Sort right-to-left based on the left edge (x_min)
sorted_row = sorted(row, key=lambda p: p['bbox'][1], reverse=True)
else: # Default ltr
# Sort left-to-right based on the left edge (x_min)
sorted_row = sorted(row, key=lambda p: p['bbox'][1])
self.ordered_panels.extend(sorted_row)
# Reassign panel IDs based on the final order
for i, panel in enumerate(self.ordered_panels):
panel['panel_id'] = i + 1
return self.ordered_panels
def save_panels(self, output_dir, base_filename, original_extension):
"""
Save the extracted artwork panels as individual image files using
the specified naming convention.
Args:
output_dir (str): Directory to save the panels.
base_filename (str): The original image filename without extension (e.g., "page001").
original_extension (str): The original image file extension (e.g., ".jpg").
"""
# This function assumes output_dir already exists
if not self.ordered_panels:
print("No panels to save.")
return 0
saved_count = 0
for panel_data in self.ordered_panels:
panel = panel_data['panel']
panel_id = panel_data['panel_id']
# Construct the output filename: base_filename_panelID.original_extension
# Use 2-digit padding for panel ID
output_filename = f"{base_filename}_{panel_id:02d}{original_extension}"
output_path = os.path.join(output_dir, output_filename)
try:
# Convert back to BGR for saving with OpenCV/PIL if needed
if panel.ndim == 3 and panel.shape[2] == 3: # Check if it's an RGB image
panel_bgr = cv2.cvtColor(panel, cv2.COLOR_RGB2BGR)
else:
panel_bgr = panel # Assume grayscale or already BGR
# Save the panel using PIL to handle various formats potentially better
panel_img = Image.fromarray(panel) # Use original RGB panel for PIL
panel_img.save(output_path)
saved_count += 1
except Exception as e:
print(f"Error saving panel {output_path}: {e}")
return saved_count
def visualize_results(self, show_original=True, show_edges=True, show_panels=True):
"""
Visualize the extraction results, showing the journey from source image to final panels.
Displays multiple stages of the process with clear explanations.
"""
if plt is None:
print("matplotlib is not installed, skipping visualization.")
return
if self.original_image is None:
print("No image loaded to visualize.")
return
num_stages = 0
if show_original: num_stages += 1
if show_edges and self.segmentation is not None: num_stages +=1
# Add more stages if needed
plt.figure(figsize=(15, 5 * num_stages)) # Adjust figure size
plot_index = 1
# --- Plot 1: Original Image ---
if show_original:
plt.subplot(num_stages, 3, plot_index)
plt.imshow(self.original_image)
plt.title('1. Original Image')
plt.axis('off')
plot_index += 1
# --- Plot 2: Edge Detection / Segmentation ---
if show_edges and self.segmentation is not None:
plt.subplot(num_stages, 3, plot_index)
plt.imshow(self.segmentation, cmap='gray')
plt.title('2. Edge Detection & Segmentation')
plt.axis('off')
plot_index += 1
# --- Plot 3: Identified Panel Boundaries ---
if self.panels:
panel_boundary_img = self.original_image.copy()
for bbox in self.panels:
y_min, x_min, y_max, x_max = [int(c) for c in bbox] # Ensure int
cv2.rectangle(panel_boundary_img, (x_min, y_min), (x_max, y_max), (255, 0, 0), 2) # Red boxes
plt.subplot(num_stages, 3, plot_index)
plt.imshow(panel_boundary_img)
plt.title('3. Identified Panel Boundaries')
plt.axis('off')
plot_index += 1
# --- Plot 4: Final Panels with Order ---
if self.ordered_panels:
img_with_boxes = self.original_image.copy()
for panel_data in self.ordered_panels:
bbox = panel_data['bbox']
panel_id = panel_data['panel_id']
y_min, x_min, y_max, x_max = [int(c) for c in bbox] # Ensure int
# Draw rectangle
cv2.rectangle(img_with_boxes, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) # Green boxes
# Add panel number (position inside the box)
text_x = x_min + 10
text_y = y_min + 30
# Basic check to keep text within image bounds
if text_y > y_max: text_y = y_min + 15
if text_x > x_max - 20 : text_x = x_min + 5
cv2.putText(img_with_boxes, str(panel_id), (text_x, text_y),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) # Blue text
plt.subplot(num_stages, 3, plot_index) # Or adjust layout as needed
plt.imshow(img_with_boxes)
plt.title('4. Final Ordered Panels')
plt.axis('off')
plot_index += 1
plt.tight_layout()
plt.suptitle('Manga Panel Segmentation Stages', fontsize=16, y=1.02) # Adjust title pos
plt.show()
# --- Figure 2: Display Extracted Panels ---
if show_panels and self.ordered_panels:
num_panels = len(self.ordered_panels)
if num_panels > 0:
max_cols = 4 # Increased columns for potentially more panels
rows = int(np.ceil(num_panels / max_cols))
cols = min(max_cols, num_panels)
fig_panels, panel_axes = plt.subplots(rows, cols, figsize=(3 * cols, 3 * rows + 1)) # Adjust size
# Handle case of single panel or single row/column
if num_panels == 1:
panel_axes = np.array([panel_axes]) # Make iterable
panel_axes = panel_axes.flatten() # Flatten for easy iteration
for i, panel_data in enumerate(self.ordered_panels):
panel_axes[i].imshow(panel_data['panel'])
panel_axes[i].set_title(f'Panel {panel_data["panel_id"]}')
panel_axes[i].axis('off')
# Hide any unused subplots
for i in range(num_panels, len(panel_axes)):
panel_axes[i].axis('off')
plt.tight_layout()
plt.suptitle('Extracted Panels', fontsize=16, y=1.0) # Adjust title pos
plt.show()
def _do_bboxes_overlap(self, a, b, threshold=0.1):
"""Check if two bounding boxes overlap significantly."""
y_min_a, x_min_a, y_max_a, x_max_a = a
y_min_b, x_min_b, y_max_b, x_max_b = b
# Calculate intersection coordinates
intersect_ymin = max(y_min_a, y_min_b)
intersect_xmin = max(x_min_a, x_min_b)
intersect_ymax = min(y_max_a, y_max_b)
intersect_xmax = min(x_max_a, x_max_b)
# Calculate intersection area
intersect_height = max(0, intersect_ymax - intersect_ymin)
intersect_width = max(0, intersect_xmax - intersect_xmin)
intersection_area = intersect_height * intersect_width
if intersection_area == 0:
return False
# Calculate area of bounding boxes
area_a = (y_max_a - y_min_a) * (x_max_a - x_min_a)
area_b = (y_max_b - y_min_b) * (x_max_b - x_min_b)
# Check if intersection area is a significant portion of either box
if area_a > 0 and (intersection_area / area_a) > threshold:
return True
if area_b > 0 and (intersection_area / area_b) > threshold:
return True
return False
def _merge_bboxes(self, a, b):
"""Merge two overlapping bounding boxes."""
return (
min(a[0], b[0]),
min(a[1], b[1]),
max(a[2], b[2]),
max(a[3], b[3])
)
def _is_bbox_contained(self, inner_bbox, outer_bbox):
"""Check if inner_bbox is fully contained within outer_bbox."""
y_min_i, x_min_i, y_max_i, x_max_i = inner_bbox
y_min_o, x_min_o, y_max_o, x_max_o = outer_bbox
return (y_min_o <= y_min_i and x_min_o <= x_min_i and
y_max_o >= y_max_i and x_max_o >= x_max_i)
# %% Processing Function
def process_image(image_path, output_dir, visualize=True,
edge_detection_sigma=1.0, low_threshold=0.1, high_threshold=0.2,
min_panel_area=0.01, reading_order='ltr'):
"""
Process a single manga/manhwa image and extract panels.
Args:
image_path (str): Path to the input image.
output_dir (str): Directory to save extracted panels.
visualize (bool): Whether to show visualization of results.
edge_detection_sigma (float): Sigma parameter for Canny edge detection.
low_threshold (float): Low threshold for Canny edge detection.
high_threshold (float): High threshold for Canny edge detection.
min_panel_area (float): Minimum panel area as a ratio of page area.
reading_order (str): 'ltr' or 'rtl' for panel ordering.
Returns:
int: Number of panels extracted, or 0 if processing failed.
"""
print(f"Processing: {image_path}")
# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)
# Extract base filename and extension for saving panels later
path_obj = Path(image_path)
base_filename = path_obj.stem
original_extension = path_obj.suffix # Includes the dot (e.g., ".jpg")
# Initialize segmenter with parameters
segmenter = MangaPanelSegmenter(
min_panel_area_ratio=min_panel_area,
edge_detection_sigma=edge_detection_sigma,
low_threshold=low_threshold,
high_threshold=high_threshold
)
# Process the image
if segmenter.load_image(image_path) is None:
return 0 # Image loading failed
if segmenter.detect_edges() is None:
print(f" Edge detection failed for {image_path}")
return 0
segmenter.identify_panels()
segmenter.extract_artwork_panels()
segmenter.order_panels(reading_order=reading_order)
# Save panels
num_panels = segmenter.save_panels(output_dir, base_filename, original_extension)
if num_panels > 0:
print(f"-> Successfully extracted {num_panels} panels to {output_dir}/")
else:
print(f"-> No panels extracted or saved for {image_path}")
# Visualize results if requested
if visualize:
print(f" Visualizing results for {base_filename}{original_extension}...")
segmenter.visualize_results(show_panels=(num_panels > 0)) # Only show panels if any were extracted
return num_panels
# %% Main Function
def main():
"""
Main function to run the manga panel segmentation from command line.
Handles both single image and directory processing.
"""
parser = argparse.ArgumentParser(description='Manga/Manhwa Panel Segmentation Tool')
parser.add_argument('input_path', type=str,
help='Path to the manga/manhwa page image OR a directory containing images.')
parser.add_argument('--output', type=str, default=None, # Default is None, logic will handle it
help='Output directory base. If input is a file, panels go here.'
' If input is a directory, a sibling directory named <input_dir>_output is created,'
' and this argument is ignored.')
parser.add_argument('--visualize', action='store_true',
help='Visualize the segmentation results for each image.')
parser.add_argument('--min-panel-area', type=float, default=0.01,
help='Minimum panel area as a ratio of total page area (e.g., 0.01 = 1%%). Default: 0.01')
parser.add_argument('--edge-sigma', type=float, default=1.5, # Slightly higher default sigma might help
help='Sigma for Gaussian filter in Canny edge detection. Default: 1.5')
parser.add_argument('--low-threshold', type=float, default=0.1,
help='Low threshold for Canny edge detection (as fraction of max intensity). Default: 0.1')
parser.add_argument('--high-threshold', type=float, default=0.2,
help='High threshold for Canny edge detection (as fraction of max intensity). Default: 0.2')
parser.add_argument('--reading-order', type=str, default='ltr', choices=['ltr', 'rtl'],
help='Reading order for panel numbering (ltr or rtl). Default: ltr')
args = parser.parse_args()
input_path = Path(args.input_path)
output_base = args.output
if not input_path.exists():
print(f"Error: Input path does not exist: {args.input_path}")
return
# --- Determine if input is directory or file ---
if input_path.is_dir():
# --- Directory Processing ---
print(f"Processing directory: {input_path}")
# Create the output directory path (sibling with _output suffix)
output_dir_name = f"{input_path.name}_output"
output_dir_path = input_path.parent / output_dir_name
print(f"Output will be saved in: {output_dir_path}")
os.makedirs(output_dir_path, exist_ok=True) # Create the main output dir for the batch
# Find image files (adjust extensions as needed)
image_extensions = ["*.jpg", "*.jpeg", "*.png", "*.webp", "*.bmp"]
image_files = []
for ext in image_extensions:
image_files.extend(input_path.glob(ext))
if not image_files:
print(f"No image files found in directory: {input_path}")
return
total_panels_extracted = 0
num_files_processed = 0
for img_file_path in sorted(image_files): # Process in sorted order
# Output for each image's panels goes directly into the derived output_dir_path
try:
num_panels = process_image(
str(img_file_path),
output_dir=str(output_dir_path), # Panels saved directly here
visualize=args.visualize,
edge_detection_sigma=args.edge_sigma,
low_threshold=args.low_threshold,
high_threshold=args.high_threshold,
min_panel_area=args.min_panel_area,
reading_order=args.reading_order
)
total_panels_extracted += num_panels
num_files_processed += 1
except Exception as e:
print(f"!! Failed to process {img_file_path}: {e}")
# Optionally add more robust error handling here (e.g., log errors)
print("\n" + "="*30)
print("Batch Processing Summary:")
print(f" Processed {num_files_processed} image files from '{input_path}'.")
print(f" Total panels extracted: {total_panels_extracted}")
print(f" Output saved in: '{output_dir_path}'")
print("="*30)
elif input_path.is_file():
# --- Single File Processing ---
print(f"Processing single file: {input_path}")
# Determine output directory for single file
if output_base is None:
# If --output is not specified, create output dir next to the file
output_dir_path = input_path.parent / f"{input_path.stem}_output"
else:
output_dir_path = Path(output_base)
print(f"Output will be saved in: {output_dir_path}")
os.makedirs(output_dir_path, exist_ok=True)
try:
num_panels = process_image(
str(input_path),
output_dir=str(output_dir_path),
visualize=args.visualize,
edge_detection_sigma=args.edge_sigma,
low_threshold=args.low_threshold,
high_threshold=args.high_threshold,
min_panel_area=args.min_panel_area,
reading_order=args.reading_order
)
print(f"\nFinished processing. Extracted {num_panels} panels.")
except Exception as e:
print(f"!! Failed to process {input_path}: {e}")
else:
print(f"Error: Input path is neither a file nor a directory: {args.input_path}")
if __name__ == "__main__":
main()
|