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
    )