Spaces:
Runtime error
Runtime error
File size: 13,368 Bytes
5c277fc 652f463 5c277fc 652f463 5c277fc 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 | import cv2
import numpy as np
from pathlib import Path
try:
import matplotlib.pyplot as plt
except Exception:
plt = None
class PanelColorExtractor:
"""
Extracts colored content from manga/manhwa panels by filtering out speech bubbles
using HSV color space analysis and finding the actual content boundaries.
"""
def __init__(self, saturation_threshold=20, value_range=(30, 225),
min_content_area=0.1, padding=5):
"""
Initialize the color extractor.
Args:
saturation_threshold (int): Minimum saturation to consider as colored content
value_range (tuple): Range of brightness values to consider (min, max)
min_content_area (float): Minimum area ratio to consider as valid content
padding (int): Padding to add around detected content boundaries
"""
self.saturation_threshold = saturation_threshold
self.value_min = value_range[0]
self.value_max = value_range[1]
self.min_content_area = min_content_area
self.padding = padding
# Create kernels for morphological operations
self.morph_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# Store intermediate results
self.original_panel = None
self.hsv_image = None
self.color_mask = None
self.content_box = None
self.refined_panel = None
def load_panel(self, image_path):
"""Load the panel image."""
try:
self.original_panel = cv2.imread(str(image_path))
if self.original_panel is None:
raise ValueError(f"Could not read image file: {image_path}")
# Convert to HSV for color analysis
self.hsv_image = cv2.cvtColor(self.original_panel, cv2.COLOR_BGR2HSV)
print(f"Loaded panel: {image_path}, size: {self.original_panel.shape}")
return self.original_panel
except Exception as e:
print(f"Error loading panel: {e}")
return None
def extract_colored_content(self):
"""
Extract colored content from the panel, excluding grayscale areas like speech bubbles.
"""
if self.hsv_image is None:
print("Error: Panel not loaded.")
return None
# Get HSV channels
h, s, v = cv2.split(self.hsv_image)
# 1. Create a mask for colored pixels (sufficient saturation)
saturation_mask = cv2.threshold(s, self.saturation_threshold, 255, cv2.THRESH_BINARY)[1]
# 2. Create a mask for reasonable brightness range (exclude pure white/black)
value_mask = cv2.inRange(v, self.value_min, self.value_max)
# 3. Combine masks to get colored content
self.color_mask = cv2.bitwise_and(saturation_mask, value_mask)
# 4. Clean up the mask with morphological operations
self.color_mask = cv2.morphologyEx(self.color_mask, cv2.MORPH_CLOSE, self.morph_kernel)
self.color_mask = cv2.morphologyEx(self.color_mask, cv2.MORPH_OPEN, self.morph_kernel)
# 5. Dilate to connect nearby regions
self.color_mask = cv2.dilate(self.color_mask, self.morph_kernel, iterations=2)
return self.color_mask
def find_content_boundaries(self):
"""
Find the bounding box of the colored content.
"""
if self.color_mask is None:
print("Error: Color mask not created.")
return None
# Find contours in the color mask
contours, _ = cv2.findContours(
self.color_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
if not contours:
print("Warning: No colored content detected.")
h, w = self.original_panel.shape[:2]
self.content_box = (0, 0, w, h)
return self.content_box
# Filter significant contours (by area)
img_area = self.original_panel.shape[0] * self.original_panel.shape[1]
min_area = img_area * self.min_content_area
significant_contours = [c for c in contours if cv2.contourArea(c) > min_area]
if not significant_contours:
print("Warning: No significant colored regions found.")
significant_contours = contours # Use all contours if none are significant enough
# Create a mask with all significant contours
content_mask = np.zeros_like(self.color_mask)
cv2.drawContours(content_mask, significant_contours, -1, 255, -1)
# Find the bounding rectangle containing all colored content
x, y, w, h = cv2.boundingRect(content_mask)
# Add padding (ensuring we stay within image bounds)
img_h, img_w = self.original_panel.shape[:2]
x = max(0, x - self.padding)
y = max(0, y - self.padding)
w = min(img_w - x, w + (self.padding * 2))
h = min(img_h - y, h + (self.padding * 2))
self.content_box = (x, y, w, h)
return self.content_box
def extract_refined_panel(self):
"""
Extract the portion of the panel containing the colored content.
"""
if self.content_box is None or self.original_panel is None:
print("Error: Content boundaries not determined.")
return None
x, y, w, h = self.content_box
# Create refined panel by cropping the original
self.refined_panel = self.original_panel[y:y+h, x:x+w].copy()
return self.refined_panel
def process_panel(self, image_path, output_path=None, visualize=False):
"""
Process a panel to extract colored content.
Args:
image_path: Path to the panel image
output_path: Optional path to save the refined panel
visualize: Whether to display visualization
Returns:
The refined panel image
"""
# Load the panel
if self.load_panel(image_path) is None:
return None
# Extract colored content
self.extract_colored_content()
# Find content boundaries
self.find_content_boundaries()
# Extract refined panel
self.extract_refined_panel()
# Save the result if path provided
if output_path:
output_path_obj = Path(output_path)
# Ensure output directory exists
if not output_path_obj.suffix:
# Directory path
output_dir = output_path_obj
output_dir.mkdir(parents=True, exist_ok=True)
# Create filename based on input
input_filename = Path(image_path).stem
output_filename = f"{input_filename}_refined.png"
output_path_full = output_dir / output_filename
else:
# File path with extension
output_path_obj.parent.mkdir(parents=True, exist_ok=True)
output_path_full = output_path_obj
# Save the refined panel
cv2.imwrite(str(output_path_full), self.refined_panel)
print(f"Saved refined panel to: {output_path_full}")
# Show visualization if requested
if visualize:
self.visualize()
return self.refined_panel
def visualize(self):
"""Visualize the extraction process in a 3x3 grid."""
if plt is None:
print("matplotlib is not installed, skipping visualization.")
return
if self.original_panel is None:
print("No panel to visualize.")
return
# Prepare figure with a 3x3 grid
fig, axes = plt.subplots(3, 3, figsize=(12, 12))
# 1. First row: Original Panel, HSV Image, Color Mask
stages = [
("Original Panel", self.original_panel, cv2.COLOR_BGR2RGB),
("HSV Image", self.hsv_image, cv2.COLOR_HSV2RGB),
("Color Mask", self.color_mask, None) # Grayscale
]
for i, (title, img, color_conversion) in enumerate(stages):
ax = axes[0, i]
if img is None:
ax.text(0.5, 0.5, "Not Available", ha="center", va="center")
elif len(img.shape) == 2: # Grayscale
ax.imshow(img, cmap="gray")
else: # Color
ax.imshow(cv2.cvtColor(img, color_conversion) if color_conversion else img)
ax.set_title(title)
ax.axis("off")
# 2. Second row: Individual HSV Channels
if self.hsv_image is not None:
h, s, v = cv2.split(self.hsv_image)
channels = [("Hue", h), ("Saturation", s), ("Value", v)]
for i, (title, channel) in enumerate(channels):
ax = axes[1, i]
ax.imshow(channel, cmap="gray")
ax.set_title(title)
ax.axis("off")
# 3. Third row: Significant Contours, Content Boundary, Refined Panel
if self.color_mask is not None:
ax = axes[2, 0]
debug_img = self.original_panel.copy()
contours, _ = cv2.findContours(self.color_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
img_area = self.original_panel.shape[0] * self.original_panel.shape[1]
min_area = img_area * self.min_content_area
significant_contours = [c for c in contours if cv2.contourArea(c) > min_area]
cv2.drawContours(debug_img, significant_contours, -1, (0, 255, 0), 2)
ax.imshow(cv2.cvtColor(debug_img, cv2.COLOR_BGR2RGB))
ax.set_title("Significant Contours")
ax.axis("off")
if self.content_box is not None:
ax = axes[2, 1]
debug_img = self.original_panel.copy()
x, y, w, h = self.content_box
cv2.rectangle(debug_img, (x, y), (x+w, y+h), (0, 255, 0), 2)
ax.imshow(cv2.cvtColor(debug_img, cv2.COLOR_BGR2RGB))
ax.set_title("Content Boundary")
ax.axis("off")
if self.refined_panel is not None:
ax = axes[2, 2]
ax.imshow(cv2.cvtColor(self.refined_panel, cv2.COLOR_BGR2RGB))
ax.set_title("Refined Panel")
ax.axis("off")
# Adjust layout
plt.tight_layout()
plt.show()
def process_panel_with_color(image_path, output_path=None, visualize=False,
saturation_threshold=20, value_range=(30, 225),
min_content_area=0.1, padding=5):
"""
Process a manga/manhwa panel to extract colored content.
Args:
image_path (str): Path to the input panel image
output_path (str, optional): Path to save the refined panel
visualize (bool): Whether to show visualization
saturation_threshold (int): Minimum saturation to consider as colored
value_range (tuple): Range of brightness values to consider (min, max)
min_content_area (float): Minimum area ratio to consider as valid content
padding (int): Padding to add around detected content
Returns:
numpy.ndarray: The refined panel image
"""
print(f"\n==== PROCESSING PANEL: {image_path} ====")
# Create the color extractor
extractor = PanelColorExtractor(
saturation_threshold=saturation_threshold,
value_range=value_range,
min_content_area=min_content_area,
padding=padding
)
# Process the panel
try:
refined_panel = extractor.process_panel(image_path, output_path, visualize)
print(f"==== COMPLETED PROCESSING PANEL: {image_path} ====\n")
return refined_panel
except Exception as e:
import traceback
print(f"ERROR during processing: {e}")
traceback.print_exc()
return None
# Example usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Manga Panel Color Extractor")
parser.add_argument("input_path", help="Path to input panel image")
parser.add_argument("--output", help="Path to save refined panel")
parser.add_argument("--visualize", action="store_true", help="Show visualization")
parser.add_argument("--saturation", type=int, default=20,
help="Minimum saturation to consider as colored")
parser.add_argument("--value-min", type=int, default=30,
help="Minimum brightness value")
parser.add_argument("--value-max", type=int, default=225,
help="Maximum brightness value")
parser.add_argument("--min-area", type=float, default=0.1,
help="Minimum content area ratio")
parser.add_argument("--padding", type=int, default=5,
help="Padding around content boundaries")
args = parser.parse_args()
process_panel_with_color(
args.input_path,
args.output,
args.visualize,
args.saturation,
(args.value_min, args.value_max),
args.min_area,
args.padding
)
|