| import cv2 |
| import numpy as np |
| import os |
| from pathlib import Path |
| from PIL import Image |
|
|
| class AutomatedCollageSplitter: |
| def __init__(self): |
| self.min_segment_area_ratio = 0.01 |
| self.max_segment_area_ratio = 0.95 |
| self.min_aspect_ratio = 0.2 |
| self.max_aspect_ratio = 5.0 |
| self.min_solidity = 0.9 |
| self.nms_threshold = 0.3 |
|
|
| self.upscaler = None |
| self._initialize_upscaler() |
|
|
| def _initialize_upscaler(self): |
| if self.upscaler is not None: |
| return |
| try: |
| model_name = 'fsrcnn' |
| model_path = os.path.join('models', 'weights', 'FSRCNN-small_x4.pb') |
| scale = 4 |
| if not os.path.isfile(model_path): |
| raise FileNotFoundError(f"Model file not found at {model_path}") |
| self.upscaler = cv2.dnn_superres.DnnSuperResImpl_create() |
| self.upscaler.readModel(model_path) |
| self.upscaler.setModel(model_name, scale) |
| print(f"β OpenCV DNN upscaler model loaded: {model_name} with scale x{scale}") |
| except Exception as e: |
| print(f"β Could not initialize OpenCV DNN upscaler: {e}. Proceeding without upscaling.") |
| self.upscaler = None |
|
|
| def _upscale_image(self, image_array): |
| if not self.upscaler: |
| print("β Upscaling skipped because the upscaler is not available.") |
| return image_array |
| try: |
| return self.upscaler.upsample(image_array) |
| except Exception as e: |
| print(f"β Error during image upscaling: {e}") |
| return image_array |
|
|
| def preprocess_for_contours(self, image): |
| """Pre-process the image to make panel borders stand out for contour detection.""" |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) |
|
|
| binary = cv2.adaptiveThreshold( |
| gray, 255, |
| cv2.ADAPTIVE_THRESH_MEAN_C, |
| cv2.THRESH_BINARY_INV, |
| 21, |
| 8 |
| ) |
|
|
| kernel = np.ones((5, 5), np.uint8) |
| closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2) |
|
|
| return closed |
|
|
| def find_panel_contours(self, processed_image, original_shape): |
| """Find and filter contours that are likely to be comic panels.""" |
| contours, _ = cv2.findContours(processed_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) |
|
|
| height, width = original_shape[:2] |
| total_area = height * width |
| min_area = total_area * self.min_segment_area_ratio |
| max_area = total_area * self.max_segment_area_ratio |
|
|
| potential_panels = [] |
| for contour in contours: |
| area = cv2.contourArea(contour) |
|
|
| if not (min_area < area < max_area): |
| continue |
|
|
| x, y, w, h = cv2.boundingRect(contour) |
|
|
| if h == 0: continue |
| aspect_ratio = w / h |
| if not (self.min_aspect_ratio < aspect_ratio < self.max_aspect_ratio): |
| continue |
|
|
| hull = cv2.convexHull(contour) |
| hull_area = cv2.contourArea(hull) |
| if hull_area == 0: continue |
| solidity = float(area) / hull_area |
| if solidity < self.min_solidity: |
| continue |
|
|
| potential_panels.append([x, y, x + w, y + h, area]) |
|
|
| return np.array(potential_panels) |
|
|
| def apply_non_maximum_suppression(self, boxes): |
| """Apply NMS to merge overlapping bounding boxes.""" |
| if len(boxes) == 0: |
| return [] |
|
|
| boxes = boxes[boxes[:, 4].argsort()[::-1]] |
|
|
| picked_boxes = [] |
| while len(boxes) > 0: |
| best_box = boxes[0] |
| picked_boxes.append(best_box) |
|
|
| remaining_boxes = boxes[1:] |
|
|
| x1 = np.maximum(best_box[0], remaining_boxes[:, 0]) |
| y1 = np.maximum(best_box[1], remaining_boxes[:, 1]) |
| x2 = np.minimum(best_box[2], remaining_boxes[:, 2]) |
| y2 = np.minimum(best_box[3], remaining_boxes[:, 3]) |
|
|
| inter_w = np.maximum(0, x2 - x1) |
| inter_h = np.maximum(0, y2 - y1) |
| intersection_area = inter_w * inter_h |
|
|
| best_box_area = (best_box[2] - best_box[0]) * (best_box[3] - best_box[1]) |
| remaining_boxes_area = (remaining_boxes[:, 2] - remaining_boxes[:, 0]) * (remaining_boxes[:, 3] - remaining_boxes[:, 1]) |
|
|
| union_area = best_box_area + remaining_boxes_area - intersection_area |
| iou = intersection_area / union_area |
|
|
| boxes = remaining_boxes[iou < self.nms_threshold] |
|
|
| return np.array(picked_boxes) |
|
|
| def split_collage(self, image_path, output_dir=None, debug=False): |
| """Main function to automatically split collage using contour detection and NMS.""" |
| img = cv2.imread(str(image_path)) |
| if img is None: |
| raise ValueError(f"Could not load image from {image_path}") |
|
|
| print(f"Processing image: {image_path}") |
| print(f"Image dimensions: {img.shape[1]}x{img.shape[0]}") |
|
|
| processed_image = self.preprocess_for_contours(img) |
| print("β Preprocessed image for contour detection") |
|
|
| potential_panels = self.find_panel_contours(processed_image, img.shape) |
| print(f"β Found {len(potential_panels)} potential panel contours") |
|
|
| final_panels = self.apply_non_maximum_suppression(potential_panels) |
| print(f"β Refined to {len(final_panels)} panels after Non-Maximum Suppression") |
|
|
| if len(final_panels) > 0: |
| img_height = img.shape[0] |
| panel_heights = [box[3] - box[1] for box in final_panels] |
| if panel_heights: |
| max_panel_height = max(panel_heights) |
|
|
| if max_panel_height > 0: |
| bottom_margin = 10 |
| height_threshold_ratio = 0.8 |
|
|
| truly_final_panels = [] |
| for box in final_panels: |
| h = box[3] - box[1] |
| y2 = box[3] |
|
|
| is_at_bottom = y2 >= (img_height - bottom_margin) |
| is_too_short = h < (max_panel_height * height_threshold_ratio) |
|
|
| if is_at_bottom and is_too_short: |
| print(f"Skipping potentially incomplete panel at the bottom (h={h} vs max_h={max_panel_height})") |
| continue |
| truly_final_panels.append(box) |
|
|
| final_panels = truly_final_panels |
|
|
| if len(final_panels) < 4: |
| print("β Contour detection found too few panels. Creating fallback 2x2 grid...") |
| h, w = img.shape[:2] |
| final_panels = np.array([ |
| [0, 0, w//2, h//2, 0], |
| [w//2, 0, w, h//2, 0], |
| [0, h//2, w//2, h, 0], |
| [w//2, h//2, w, h, 0] |
| ]) |
|
|
| final_panels = sorted(final_panels, key=lambda b: (b[1], b[0])) |
|
|
| if output_dir is None: |
| output_dir = Path(image_path).parent / f"{Path(image_path).stem}_segments" |
| output_dir = Path(output_dir) |
| output_dir.mkdir(exist_ok=True) |
|
|
| saved_segments_info = [] |
| for i, box in enumerate(final_panels): |
| x1, y1, x2, y2, _ = map(int, box) |
|
|
| padding = 3 |
| x1 = max(0, x1 - padding) |
| y1 = max(0, y1 - padding) |
| x2 = min(img.shape[1], x2 + padding) |
| y2 = min(img.shape[0], y2 + padding) |
|
|
| segment = img[y1:y2, x1:x2] |
| original_dims = (segment.shape[1], segment.shape[0]) |
|
|
| upscaled_segment_np = self._upscale_image(segment) |
|
|
| final_image = Image.fromarray(cv2.cvtColor(upscaled_segment_np, cv2.COLOR_BGR2RGB)) |
|
|
| final_dims = (final_image.width, final_image.height) |
| output_path = output_dir / f"segment_{i+1:02d}.png" |
| final_image.save(str(output_path)) |
|
|
| caption = ( |
| f"Panel {i+1}<br>" |
| f"Original: {original_dims[0]}x{original_dims[1]}<br>" |
| f"Upscaled: {final_dims[0]}x{final_dims[1]}" |
| ) |
|
|
| saved_segments_info.append({ |
| "path": str(output_path), |
| "caption": caption |
| }) |
| print(f" Saved segment {i+1}: {final_dims[0]}x{final_dims[1]} pixels to {output_path}") |
|
|
| if debug: |
| debug_potential_panels = self.find_panel_contours(processed_image, img.shape) |
| self.create_debug_images(img, processed_image, debug_potential_panels, final_panels, output_dir) |
|
|
| print(f"\nπ Successfully split collage into {len(saved_segments_info)} segments!") |
| print(f"π Segments saved in: {output_dir}") |
| return saved_segments_info |
|
|
| def create_debug_images(self, original, processed, potential_boxes, final_boxes, output_dir): |
| """Create debug images showing the processing steps.""" |
| cv2.imwrite(str(output_dir / "debug_01_binary_closed.png"), processed) |
|
|
| potential_img = original.copy() |
| if len(potential_boxes) > 0: |
| for x1, y1, x2, y2, _ in potential_boxes: |
| cv2.rectangle(potential_img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 255), 2) |
| cv2.imwrite(str(output_dir / "debug_02_potential_boxes.png"), potential_img) |
|
|
| final_img = original.copy() |
| if len(final_boxes) > 0: |
| for x1, y1, x2, y2, _ in final_boxes: |
| cv2.rectangle(final_img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 3) |
| cv2.imwrite(str(output_dir / "debug_03_final_panels.png"), final_img) |
|
|
| print("π Debug images saved:") |
| print(" - debug_01_binary_closed.png (preprocessed)") |
| print(" - debug_02_potential_boxes.png (before NMS)") |
| print(" - debug_03_final_panels.png (after NMS)") |
|
|
| def main(): |
| """Example usage""" |
| splitter = AutomatedCollageSplitter() |
|
|
| image_path = "path/to/your/comic_image.png" |
|
|
| try: |
| if not Path(image_path).exists(): |
| print(f"β Image file not found: {image_path}") |
| print("Please update the image_path variable with the correct path to your collage image.") |
| return |
|
|
| segments = splitter.split_collage( |
| image_path=image_path, |
| debug=True |
| ) |
|
|
| print(f"\nπ Processing complete!") |
| print(f"Generated {len(segments)} separate images from the collage") |
|
|
| except Exception as e: |
| print(f"β Error processing image: {e}") |
| import traceback |
| traceback.print_exc() |
|
|
| if __name__ == "__main__": |
| main() |
|
|