Spaces:
Running
Running
easy panel ext added
Browse files
comic_panel_extractor/config.py
CHANGED
|
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
|
| 4 |
class Config:
|
| 5 |
"""Configuration settings for the comic-to-video pipeline."""
|
| 6 |
input_path: str = ""
|
|
|
|
| 7 |
output_folder: str = "temp_dir"
|
| 8 |
distance_threshold: int = 70
|
| 9 |
vertical_threshold: int = 30
|
|
|
|
| 4 |
class Config:
|
| 5 |
"""Configuration settings for the comic-to-video pipeline."""
|
| 6 |
input_path: str = ""
|
| 7 |
+
black_overlay_input_path: str = ""
|
| 8 |
output_folder: str = "temp_dir"
|
| 9 |
distance_threshold: int = 70
|
| 10 |
vertical_threshold: int = 30
|
comic_panel_extractor/image_processor.py
CHANGED
|
@@ -11,11 +11,11 @@ class ImageProcessor:
|
|
| 11 |
def __init__(self, config: Config):
|
| 12 |
self.config = config
|
| 13 |
|
| 14 |
-
def mask_text_regions(self, bboxes: List[List[int]], output_filename: str = "1_text_removed.jpg", color: Tuple[int, int, int] = (0, 0, 0)) -> str:
|
| 15 |
"""Mask text regions in the image to reduce panel extraction noise."""
|
| 16 |
-
image = cv2.imread(
|
| 17 |
if image is None:
|
| 18 |
-
raise FileNotFoundError(f"Could not load image: {
|
| 19 |
|
| 20 |
for bbox in bboxes:
|
| 21 |
x1, y1, x2, y2 = bbox
|
|
@@ -26,16 +26,17 @@ class ImageProcessor:
|
|
| 26 |
print(f"β
Text-masked image saved to: {output_path}")
|
| 27 |
return str(output_path)
|
| 28 |
|
| 29 |
-
def preprocess_image(self,
|
| 30 |
"""Preprocess image for panel extraction."""
|
| 31 |
-
image = cv2.imread(
|
| 32 |
if image is None:
|
| 33 |
-
raise FileNotFoundError(f"Could not load image: {
|
| 34 |
|
| 35 |
# Convert to grayscale and binary
|
| 36 |
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 37 |
_, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
if not is_inverted:
|
| 41 |
# Dilate to strengthen borders
|
|
|
|
| 11 |
def __init__(self, config: Config):
|
| 12 |
self.config = config
|
| 13 |
|
| 14 |
+
def mask_text_regions(self, input_path, bboxes: List[List[int]], output_filename: str = "1_text_removed.jpg", color: Tuple[int, int, int] = (0, 0, 0)) -> str:
|
| 15 |
"""Mask text regions in the image to reduce panel extraction noise."""
|
| 16 |
+
image = cv2.imread(input_path)
|
| 17 |
if image is None:
|
| 18 |
+
raise FileNotFoundError(f"Could not load image: {input_path}")
|
| 19 |
|
| 20 |
for bbox in bboxes:
|
| 21 |
x1, y1, x2, y2 = bbox
|
|
|
|
| 26 |
print(f"β
Text-masked image saved to: {output_path}")
|
| 27 |
return str(output_path)
|
| 28 |
|
| 29 |
+
def preprocess_image(self, processed_image_path) -> Tuple[str, str, str]:
|
| 30 |
"""Preprocess image for panel extraction."""
|
| 31 |
+
image = cv2.imread(processed_image_path)
|
| 32 |
if image is None:
|
| 33 |
+
raise FileNotFoundError(f"Could not load image: {processed_image_path}")
|
| 34 |
|
| 35 |
# Convert to grayscale and binary
|
| 36 |
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
| 37 |
_, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
|
| 38 |
+
is_inverted = False
|
| 39 |
+
# binary, is_inverted = self.invert_if_black_dominates(binary)
|
| 40 |
|
| 41 |
if not is_inverted:
|
| 42 |
# Dilate to strengthen borders
|
comic_panel_extractor/main.py
CHANGED
|
@@ -3,6 +3,7 @@ from .config import Config
|
|
| 3 |
from .image_processor import ImageProcessor
|
| 4 |
from .panel_extractor import PanelData
|
| 5 |
from .panel_extractor import PanelExtractor
|
|
|
|
| 6 |
|
| 7 |
from typing import List, Tuple
|
| 8 |
from pathlib import Path
|
|
@@ -26,13 +27,16 @@ class ComicPanelExtractor:
|
|
| 26 |
def extract_panels_from_comic(self) -> Tuple[List[np.ndarray], List[PanelData]]:
|
| 27 |
"""Complete pipeline to extract panels from a comic image."""
|
| 28 |
print(f"Starting panel extraction for: {self.config.input_path}")
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
| 30 |
# Step 1: Detect and mask text regions
|
| 31 |
text_bubbles = self._detect_text_bubbles()
|
| 32 |
-
|
| 33 |
-
|
| 34 |
# Step 2: Preprocess image
|
| 35 |
-
_, _, processed_image_path, is_inverted = self.image_processor.preprocess_image(
|
| 36 |
|
| 37 |
if is_inverted:
|
| 38 |
# Step 3: Remove Inner Sketch
|
|
|
|
| 3 |
from .image_processor import ImageProcessor
|
| 4 |
from .panel_extractor import PanelData
|
| 5 |
from .panel_extractor import PanelExtractor
|
| 6 |
+
from .panel_segmentation import main as main_panel_segmentation
|
| 7 |
|
| 8 |
from typing import List, Tuple
|
| 9 |
from pathlib import Path
|
|
|
|
| 27 |
def extract_panels_from_comic(self) -> Tuple[List[np.ndarray], List[PanelData]]:
|
| 28 |
"""Complete pipeline to extract panels from a comic image."""
|
| 29 |
print(f"Starting panel extraction for: {self.config.input_path}")
|
| 30 |
+
|
| 31 |
+
processed_image_path = main_panel_segmentation(self.config.output_folder, self.config.input_path, self.config.input_path)
|
| 32 |
+
self.config.black_overlay_input_path = processed_image_path
|
| 33 |
+
|
| 34 |
# Step 1: Detect and mask text regions
|
| 35 |
text_bubbles = self._detect_text_bubbles()
|
| 36 |
+
processed_image_path = self.image_processor.mask_text_regions(processed_image_path, [bubble["bbox"] for bubble in text_bubbles])
|
| 37 |
+
|
| 38 |
# Step 2: Preprocess image
|
| 39 |
+
_, _, processed_image_path, is_inverted = self.image_processor.preprocess_image(processed_image_path)
|
| 40 |
|
| 41 |
if is_inverted:
|
| 42 |
# Step 3: Remove Inner Sketch
|
comic_panel_extractor/panel_extractor.py
CHANGED
|
@@ -4,6 +4,7 @@ from .config import Config
|
|
| 4 |
import numpy as np
|
| 5 |
import cv2
|
| 6 |
from dataclasses import dataclass
|
|
|
|
| 7 |
|
| 8 |
@dataclass
|
| 9 |
class PanelData:
|
|
@@ -218,49 +219,98 @@ class PanelExtractor:
|
|
| 218 |
|
| 219 |
return [(x1, y1, x2, y2) for x1, y1, x2, y2 in panels
|
| 220 |
if (x2 - x1) >= min_allowed_width and (y2 - y1) >= min_allowed_height]
|
| 221 |
-
|
| 222 |
-
def
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
"""Save panel images and return panel data."""
|
| 225 |
visual_output = original.copy()
|
| 226 |
panel_images = []
|
| 227 |
panel_data = []
|
| 228 |
all_panel_path = []
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
for idx, (x1, y1, x2, y2) in enumerate(panels, 1):
|
| 231 |
-
# Extract panel image
|
| 232 |
-
panel_img =
|
| 233 |
|
| 234 |
-
# Check
|
| 235 |
gray = cv2.cvtColor(panel_img, cv2.COLOR_BGR2GRAY)
|
| 236 |
-
black_pixels = np.sum(gray < 30)
|
| 237 |
total_pixels = gray.size
|
| 238 |
black_ratio = black_pixels / total_pixels
|
| 239 |
|
| 240 |
-
if black_ratio > 0.
|
| 241 |
print(f"β οΈ Skipping panel #{idx} β {round(black_ratio * 100, 2)}% black")
|
| 242 |
continue
|
|
|
|
|
|
|
| 243 |
|
| 244 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
panel_images.append(panel_img)
|
| 246 |
-
|
| 247 |
-
# Create panel data
|
| 248 |
panel_info = PanelData.from_coordinates(x1, y1, x2, y2)
|
| 249 |
panel_data.append(panel_info)
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
panel_path = f'{self.config.output_folder}/panel_{
|
| 253 |
cv2.imwrite(str(panel_path), panel_img)
|
| 254 |
all_panel_path.append(panel_path)
|
| 255 |
-
|
| 256 |
-
# Draw visualization
|
| 257 |
cv2.rectangle(visual_output, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 258 |
cv2.putText(visual_output, f"#{idx}", (x1+5, y1+25),
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
visual_path = f'{self.config.output_folder}/panels_visualization.jpg'
|
| 263 |
cv2.imwrite(str(visual_path), visual_output)
|
| 264 |
-
|
| 265 |
print(f"β
Extracted {len(panel_images)} panels after filtering.")
|
| 266 |
-
return panel_images, panel_data, all_panel_path
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
import cv2
|
| 6 |
from dataclasses import dataclass
|
| 7 |
+
import os
|
| 8 |
|
| 9 |
@dataclass
|
| 10 |
class PanelData:
|
|
|
|
| 219 |
|
| 220 |
return [(x1, y1, x2, y2) for x1, y1, x2, y2 in panels
|
| 221 |
if (x2 - x1) >= min_allowed_width and (y2 - y1) >= min_allowed_height]
|
| 222 |
+
|
| 223 |
+
def count_panel_files(self, folder_path: str) -> int:
|
| 224 |
+
"""
|
| 225 |
+
Count the number of files in a folder that start with 'panel_'.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
folder_path: Path to the folder to search.
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
Number of files starting with 'panel_'.
|
| 232 |
+
"""
|
| 233 |
+
if not os.path.exists(folder_path):
|
| 234 |
+
print(f"Folder does not exist: {folder_path}")
|
| 235 |
+
return 0
|
| 236 |
+
|
| 237 |
+
return len([
|
| 238 |
+
fname for fname in os.listdir(folder_path)
|
| 239 |
+
if fname.startswith("panel_") and os.path.isfile(os.path.join(folder_path, fname))
|
| 240 |
+
])
|
| 241 |
+
|
| 242 |
+
def _save_panels(self, panels: List[Tuple[int, int, int, int]], original: np.ndarray, width: int, height: int) -> Tuple[List[np.ndarray], List[PanelData], List[str]]:
|
| 243 |
"""Save panel images and return panel data."""
|
| 244 |
visual_output = original.copy()
|
| 245 |
panel_images = []
|
| 246 |
panel_data = []
|
| 247 |
all_panel_path = []
|
| 248 |
+
|
| 249 |
+
panel_idx = self.count_panel_files(self.config.output_folder)
|
| 250 |
+
black_overlay_input = cv2.imread(self.config.black_overlay_input_path)
|
| 251 |
+
|
| 252 |
+
image_area = width * height
|
| 253 |
+
maybe_full_page_panel = None # Store panel that is β₯90% of the page
|
| 254 |
+
|
| 255 |
for idx, (x1, y1, x2, y2) in enumerate(panels, 1):
|
| 256 |
+
# Extract panel image from black_overlay_input
|
| 257 |
+
panel_img = black_overlay_input[y1:y2, x1:x2]
|
| 258 |
|
| 259 |
+
# Check for mostly black content
|
| 260 |
gray = cv2.cvtColor(panel_img, cv2.COLOR_BGR2GRAY)
|
| 261 |
+
black_pixels = np.sum(gray < 30)
|
| 262 |
total_pixels = gray.size
|
| 263 |
black_ratio = black_pixels / total_pixels
|
| 264 |
|
| 265 |
+
if black_ratio > 0.8:
|
| 266 |
print(f"β οΈ Skipping panel #{idx} β {round(black_ratio * 100, 2)}% black")
|
| 267 |
continue
|
| 268 |
+
else:
|
| 269 |
+
print(f"β
Black ratio panel #{idx} β {round(black_ratio * 100, 2)}% black")
|
| 270 |
|
| 271 |
+
# Check if this panel is β₯90% of the full image
|
| 272 |
+
panel_area = (x2 - x1) * (y2 - y1)
|
| 273 |
+
if panel_area >= 0.9 * image_area:
|
| 274 |
+
print(f"β οΈ Panel #{idx} covers β₯90% of the image β marked for potential use only")
|
| 275 |
+
maybe_full_page_panel = (idx, (x1, y1, x2, y2))
|
| 276 |
+
continue # Skip for now
|
| 277 |
+
|
| 278 |
+
# Save valid smaller panel
|
| 279 |
+
panel_img = visual_output[y1:y2, x1:x2]
|
| 280 |
panel_images.append(panel_img)
|
|
|
|
|
|
|
| 281 |
panel_info = PanelData.from_coordinates(x1, y1, x2, y2)
|
| 282 |
panel_data.append(panel_info)
|
| 283 |
+
|
| 284 |
+
panel_idx += 1
|
| 285 |
+
panel_path = f'{self.config.output_folder}/panel_{panel_idx}_{(x1, y1, x2, y2)}.jpg'
|
| 286 |
cv2.imwrite(str(panel_path), panel_img)
|
| 287 |
all_panel_path.append(panel_path)
|
| 288 |
+
|
|
|
|
| 289 |
cv2.rectangle(visual_output, (x1, y1), (x2, y2), (0, 255, 0), 2)
|
| 290 |
cv2.putText(visual_output, f"#{idx}", (x1+5, y1+25),
|
| 291 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
|
| 292 |
+
|
| 293 |
+
# If no valid panels were saved, and we had a full-page one, save it
|
| 294 |
+
if not panel_images and maybe_full_page_panel and panel_idx == 0:
|
| 295 |
+
idx, (x1, y1, x2, y2) = maybe_full_page_panel
|
| 296 |
+
panel_img = visual_output[y1:y2, x1:x2]
|
| 297 |
+
panel_images.append(panel_img)
|
| 298 |
+
panel_info = PanelData.from_coordinates(x1, y1, x2, y2)
|
| 299 |
+
panel_data.append(panel_info)
|
| 300 |
+
|
| 301 |
+
panel_idx += 1
|
| 302 |
+
panel_path = f'{self.config.output_folder}/panel_{panel_idx}_{(x1, y1, x2, y2)}.jpg'
|
| 303 |
+
cv2.imwrite(str(panel_path), panel_img)
|
| 304 |
+
all_panel_path.append(panel_path)
|
| 305 |
+
|
| 306 |
+
cv2.rectangle(visual_output, (x1, y1), (x2, y2), (255, 0, 0), 2)
|
| 307 |
+
cv2.putText(visual_output, f"#full", (x1+5, y1+25),
|
| 308 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2)
|
| 309 |
+
print(f"β
Saved full-page panel as fallback")
|
| 310 |
+
|
| 311 |
+
# Save final visualization
|
| 312 |
visual_path = f'{self.config.output_folder}/panels_visualization.jpg'
|
| 313 |
cv2.imwrite(str(visual_path), visual_output)
|
| 314 |
+
|
| 315 |
print(f"β
Extracted {len(panel_images)} panels after filtering.")
|
| 316 |
+
return panel_images, panel_data, all_panel_path
|
comic_panel_extractor/panel_segmentation.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image, ImageDraw
|
| 4 |
+
import imageio.v2 as imageio # Fix for imageio warning
|
| 5 |
+
from skimage.color import rgb2gray
|
| 6 |
+
from skimage.feature import canny
|
| 7 |
+
from skimage import measure
|
| 8 |
+
from scipy import ndimage as ndi
|
| 9 |
+
import re
|
| 10 |
+
from skimage.morphology import remove_small_holes
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def extract_fully_white_panels(
|
| 14 |
+
original_image: np.ndarray,
|
| 15 |
+
segmentation_mask: np.ndarray,
|
| 16 |
+
output_dir: str = "panel_output",
|
| 17 |
+
debug_region_dir: str = "panel_debug_regions",
|
| 18 |
+
min_area_ratio: float = 0.05,
|
| 19 |
+
min_width_ratio: float = 0.05,
|
| 20 |
+
min_height_ratio: float = 0.05,
|
| 21 |
+
save_debug: bool = True
|
| 22 |
+
):
|
| 23 |
+
"""
|
| 24 |
+
Extract fully white panels from a segmented image.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
original_image: Original RGB image as numpy array
|
| 28 |
+
segmentation_mask: Binary segmentation mask
|
| 29 |
+
output_dir: Directory to save extracted panels
|
| 30 |
+
debug_region_dir: Directory to save debug images
|
| 31 |
+
min_area_ratio: Minimum area ratio threshold
|
| 32 |
+
min_width_ratio: Minimum width ratio threshold
|
| 33 |
+
min_height_ratio: Minimum height ratio threshold
|
| 34 |
+
save_debug: Whether to save debug images
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
List of saved panel file paths
|
| 38 |
+
"""
|
| 39 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 40 |
+
if save_debug:
|
| 41 |
+
os.makedirs(debug_region_dir, exist_ok=True)
|
| 42 |
+
|
| 43 |
+
img_h, img_w = segmentation_mask.shape
|
| 44 |
+
image_area = img_h * img_w
|
| 45 |
+
|
| 46 |
+
orig_pil = Image.fromarray(original_image)
|
| 47 |
+
labeled_mask = measure.label(segmentation_mask)
|
| 48 |
+
regions = measure.regionprops(labeled_mask)
|
| 49 |
+
|
| 50 |
+
saved_panels = []
|
| 51 |
+
accepted_boxes = []
|
| 52 |
+
panel_idx = 0
|
| 53 |
+
|
| 54 |
+
for idx, region in enumerate(regions):
|
| 55 |
+
minr, minc, maxr, maxc = region.bbox
|
| 56 |
+
w = maxc - minc
|
| 57 |
+
h = maxr - minr
|
| 58 |
+
area = w * h
|
| 59 |
+
crop_box = (minc, minr, maxc, maxr)
|
| 60 |
+
crop_name_prefix = f"region_{idx+1}"
|
| 61 |
+
|
| 62 |
+
# Crops
|
| 63 |
+
cropped_img = orig_pil.crop(crop_box)
|
| 64 |
+
cropped_mask = segmentation_mask[minr:maxr, minc:maxc]
|
| 65 |
+
# Fix for Pillow warning: Remove mode parameter
|
| 66 |
+
mask_pil = Image.fromarray((cropped_mask * 255).astype('uint8'))
|
| 67 |
+
|
| 68 |
+
# 1. Threshold check
|
| 69 |
+
if (
|
| 70 |
+
area < min_area_ratio * image_area or
|
| 71 |
+
w < min_width_ratio * img_w or
|
| 72 |
+
h < min_height_ratio * img_h
|
| 73 |
+
):
|
| 74 |
+
if save_debug:
|
| 75 |
+
cropped_img.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_too_small_orig.jpg"))
|
| 76 |
+
mask_pil.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_too_small_mask.jpg"))
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
# 2. Check if region is mostly white (allow small % of black)
|
| 80 |
+
black_pixel_count = np.count_nonzero(region.image == 0)
|
| 81 |
+
total_pixels = region.image.size
|
| 82 |
+
black_ratio = black_pixel_count / total_pixels
|
| 83 |
+
|
| 84 |
+
if black_ratio > 0.02: # Allow up to 1% black pixels
|
| 85 |
+
print(f"β Black ratio panel #{idx} β {round(black_ratio * 100, 2)}% black")
|
| 86 |
+
# Save debug info if desired
|
| 87 |
+
if save_debug:
|
| 88 |
+
debug_region_dir_specific = os.path.join(output_dir, f"region_{idx}_skipped_black_inside")
|
| 89 |
+
os.makedirs(debug_region_dir_specific, exist_ok=True)
|
| 90 |
+
|
| 91 |
+
# Save cropped mask
|
| 92 |
+
cropped_mask = segmentation_mask[minr:maxr, minc:maxc]
|
| 93 |
+
# Fix for Pillow warning: Remove mode parameter
|
| 94 |
+
mask_pil = Image.fromarray((cropped_mask * 255).astype("uint8"))
|
| 95 |
+
mask_pil.save(os.path.join(debug_region_dir_specific, f"region_{idx}_mask.jpg"))
|
| 96 |
+
|
| 97 |
+
# Highlight black pixels in red and zoom
|
| 98 |
+
highlighted = np.stack([cropped_mask]*3, axis=-1) * 255
|
| 99 |
+
highlighted[cropped_mask == 0] = [255, 0, 0]
|
| 100 |
+
highlighted_zoom = Image.fromarray(highlighted.astype('uint8')).resize(
|
| 101 |
+
(highlighted.shape[1]*4, highlighted.shape[0]*4), resample=Image.NEAREST
|
| 102 |
+
)
|
| 103 |
+
highlighted_zoom.save(os.path.join(debug_region_dir_specific, f"region_{idx}_highlight_black_zoomed.jpg"))
|
| 104 |
+
|
| 105 |
+
continue
|
| 106 |
+
|
| 107 |
+
# 3. Save valid panel with bbox coordinates in filename
|
| 108 |
+
bbox_str = f"({minc}, {minr}, {maxc}, {maxr})"
|
| 109 |
+
panel_idx = panel_idx + 1
|
| 110 |
+
panel_path = os.path.join(output_dir, f"panel_{panel_idx}_{bbox_str}.jpg")
|
| 111 |
+
cropped_img.save(panel_path)
|
| 112 |
+
saved_panels.append(panel_path)
|
| 113 |
+
accepted_boxes.append((minc, minr, maxc, maxr))
|
| 114 |
+
|
| 115 |
+
if save_debug:
|
| 116 |
+
cropped_img.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_saved_orig.jpg"))
|
| 117 |
+
mask_pil.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_saved_mask.jpg"))
|
| 118 |
+
|
| 119 |
+
# 4. Debug image with accepted boxes
|
| 120 |
+
if save_debug:
|
| 121 |
+
debug_img = orig_pil.copy()
|
| 122 |
+
draw = ImageDraw.Draw(debug_img)
|
| 123 |
+
for (x1, y1, x2, y2) in accepted_boxes:
|
| 124 |
+
draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
|
| 125 |
+
debug_img.save(os.path.join(output_dir, "debug_all_saved_panels.jpg"))
|
| 126 |
+
|
| 127 |
+
return saved_panels
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def create_segmentation_mask(image: np.ndarray, save_debug: bool = True) -> np.ndarray:
|
| 131 |
+
"""
|
| 132 |
+
Create segmentation mask from image using edge detection and hole filling.
|
| 133 |
+
|
| 134 |
+
Args:
|
| 135 |
+
image: Input RGB image as numpy array
|
| 136 |
+
save_debug: Whether to save intermediate processing steps
|
| 137 |
+
|
| 138 |
+
Returns:
|
| 139 |
+
Binary segmentation mask
|
| 140 |
+
"""
|
| 141 |
+
if save_debug:
|
| 142 |
+
os.makedirs("panel_debug_steps", exist_ok=True)
|
| 143 |
+
Image.fromarray(image).save("panel_debug_steps/step1_original.jpg")
|
| 144 |
+
|
| 145 |
+
# Convert to grayscale
|
| 146 |
+
grayscale = rgb2gray(image)
|
| 147 |
+
if save_debug:
|
| 148 |
+
gray_uint8 = (grayscale * 255).astype('uint8')
|
| 149 |
+
# Fix for Pillow warning: Remove mode parameter
|
| 150 |
+
Image.fromarray(gray_uint8).save("panel_debug_steps/step2_grayscale.jpg")
|
| 151 |
+
|
| 152 |
+
# Edge detection
|
| 153 |
+
edges = canny(grayscale)
|
| 154 |
+
if save_debug:
|
| 155 |
+
edges_uint8 = (edges * 255).astype('uint8')
|
| 156 |
+
# Fix for Pillow warning: Remove mode parameter
|
| 157 |
+
Image.fromarray(edges_uint8).save("panel_debug_steps/step3_edges.jpg")
|
| 158 |
+
|
| 159 |
+
# Fill holes in edges
|
| 160 |
+
segmentation = ndi.binary_fill_holes(edges)
|
| 161 |
+
|
| 162 |
+
# β
Remove small black clusters (holes in white regions)
|
| 163 |
+
segmentation_cleaned = remove_small_holes(segmentation, area_threshold=500) # adjust threshold as needed
|
| 164 |
+
|
| 165 |
+
if save_debug:
|
| 166 |
+
segmentation_uint8 = (segmentation_cleaned * 255).astype('uint8')
|
| 167 |
+
Image.fromarray(segmentation_uint8).save("panel_debug_steps/step4_segmentation_filled.jpg")
|
| 168 |
+
|
| 169 |
+
return segmentation_cleaned
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def create_image_with_panels_removed(
|
| 173 |
+
original_image: np.ndarray,
|
| 174 |
+
segmentation_mask: np.ndarray,
|
| 175 |
+
output_folder: str,
|
| 176 |
+
output_path: str,
|
| 177 |
+
save_debug: True
|
| 178 |
+
) -> None:
|
| 179 |
+
"""
|
| 180 |
+
Create a version of the original image with detected panels blacked out.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
original_image: Original RGB image as numpy array
|
| 184 |
+
segmentation_mask: Binary segmentation mask
|
| 185 |
+
output_path: Path to save the modified image
|
| 186 |
+
"""
|
| 187 |
+
# Get panel information
|
| 188 |
+
saved_panels = extract_fully_white_panels(
|
| 189 |
+
original_image=original_image,
|
| 190 |
+
segmentation_mask=segmentation_mask,
|
| 191 |
+
output_dir=output_folder,
|
| 192 |
+
debug_region_dir="panel_debug_regions",
|
| 193 |
+
save_debug=save_debug
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Create modified image
|
| 197 |
+
im_no_panels = Image.fromarray(original_image.copy())
|
| 198 |
+
draw = ImageDraw.Draw(im_no_panels)
|
| 199 |
+
|
| 200 |
+
# Get regions and black them out
|
| 201 |
+
labeled_mask = measure.label(segmentation_mask)
|
| 202 |
+
regions = measure.regionprops(labeled_mask)
|
| 203 |
+
pattern = re.compile(r"panel_\d+_\((\d+), (\d+), (\d+), (\d+)\)\.jpg")
|
| 204 |
+
|
| 205 |
+
for panel_path in saved_panels:
|
| 206 |
+
# Extract panel index from filename with bbox format
|
| 207 |
+
panel_name = os.path.basename(panel_path)
|
| 208 |
+
match = pattern.match(panel_name)
|
| 209 |
+
minc, minr, maxc, maxr = map(int, match.groups())
|
| 210 |
+
|
| 211 |
+
draw.rectangle([minc, minr, maxc, maxr], fill=(0, 0, 0))
|
| 212 |
+
|
| 213 |
+
# Save the result
|
| 214 |
+
im_no_panels.save(output_path)
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def main(output_folder, input_image_path, original_image_path):
|
| 218 |
+
"""Main execution function."""
|
| 219 |
+
# Load the input image
|
| 220 |
+
image = imageio.imread(input_image_path)
|
| 221 |
+
original_image = imageio.imread(original_image_path)
|
| 222 |
+
save_debug = True
|
| 223 |
+
# Create segmentation mask
|
| 224 |
+
segmentation_mask = create_segmentation_mask(image, save_debug=save_debug)
|
| 225 |
+
|
| 226 |
+
pre_process_path = f"{output_folder}/original_with_panels_removed.jpg"
|
| 227 |
+
# Create image with panels removed
|
| 228 |
+
create_image_with_panels_removed(
|
| 229 |
+
original_image=original_image,
|
| 230 |
+
segmentation_mask=segmentation_mask,
|
| 231 |
+
output_folder=output_folder,
|
| 232 |
+
output_path=pre_process_path,
|
| 233 |
+
save_debug=save_debug
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
return pre_process_path
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
main('panel_output', 'test7.jpg', 'test7.jpg')
|