File size: 14,070 Bytes
d5daafd
 
 
 
 
 
c01fc99
97aa4e5
0c179d9
d5daafd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830245b
d5daafd
 
 
 
 
 
 
 
 
 
830245b
d5daafd
 
 
 
 
 
 
 
 
830245b
d5daafd
 
 
953dc3a
d5daafd
 
 
953dc3a
d5daafd
830245b
cbb6e50
d5daafd
cbb6e50
 
d5daafd
cbb6e50
 
 
 
 
 
 
97aa4e5
 
cbb6e50
97aa4e5
cbb6e50
d5daafd
cbb6e50
 
97aa4e5
 
 
 
 
 
 
 
 
cbb6e50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830245b
cbb6e50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830245b
cbb6e50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d5daafd
 
 
 
 
 
20bec6b
d5daafd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830245b
d5daafd
830245b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c01fc99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97aa4e5
 
 
 
 
 
 
 
 
 
 
 
c01fc99
d5daafd
861d58c
d5daafd
 
 
953dc3a
c01fc99
 
 
 
 
97aa4e5
 
 
 
c01fc99
d5daafd
c01fc99
 
18a5a4e
97aa4e5
18a5a4e
 
97aa4e5
 
18a5a4e
97aa4e5
18a5a4e
c01fc99
18a5a4e
 
97aa4e5
 
 
c01fc99
97aa4e5
18a5a4e
c01fc99
 
 
 
97aa4e5
 
 
 
c01fc99
36f1695
 
 
97aa4e5
 
 
36f1695
0c179d9
36f1695
 
 
 
97aa4e5
861d58c
d5daafd
 
 
c01fc99
 
 
d5daafd
953dc3a
c01fc99
d5daafd
 
97aa4e5
c01fc99
97aa4e5
c01fc99
 
861d58c
c01fc99
 
 
 
 
 
 
 
 
 
 
97aa4e5
c01fc99
 
 
d5daafd
 
c01fc99
18a5a4e
c01fc99
97aa4e5
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
from typing import List, Tuple
from .config import Config

import numpy as np
import cv2
from dataclasses import dataclass
import os
import re
from .utils import remove_duplicate_boxes, count_panels_inside

@dataclass
class PanelData:
    """Represents an extracted comic panel."""
    x_start: int
    y_start: int
    x_end: int
    y_end: int
    width: int
    height: int
    area: int
    
    @classmethod
    def from_coordinates(cls, x1: int, y1: int, x2: int, y2: int) -> 'PanelData':
        """Create PanelData from coordinates."""
        return cls(
            x_start=x1,
            y_start=y1,
            x_end=x2,
            y_end=y2,
            width=x2 - x1,
            height=y2 - y1,
            area=(x2 - x1) * (y2 - y1)
        )

class PanelExtractor:
    """Handles comic panel extraction using black percentage analysis."""
    
    def __init__(self, config: Config):
        self.config = config
    
    def extract_panels(self, dilated_path: str, row_thresh: int = 20, col_thresh: int = 20) -> Tuple[List[np.ndarray], List[PanelData]]:
        """Extract comic panels using black percentage scan."""
        dilated = cv2.imread(dilated_path, cv2.IMREAD_GRAYSCALE)
        original = cv2.imread(self.config.input_path)
        
        if dilated is None or original is None:
            raise FileNotFoundError("Could not load dilated or original image")

        height, width = dilated.shape
        
        # Find row gutters and panel rows
        panel_rows = self._find_panel_rows(dilated, row_thresh)
        
        # Extract panels from each row
        all_panels = []
        for y1, y2 in panel_rows:
            row_panels = self._extract_panels_from_row(dilated, y1, y2, col_thresh)
            all_panels.extend(row_panels)
        
        # Filter panels by size
        filtered_panels = self._filter_panels_by_size(
            all_panels, width, height
        )
        
        # Extract panel images and save
        panel_images, panel_data, all_panel_path = self._save_panels(
            filtered_panels, original, width, height
        )
        
        return panel_images, panel_data, all_panel_path
    
    def _find_panel_rows(self, dilated: np.ndarray, row_thresh: int) -> List[Tuple[int, int]]:
        """Find panel rows where consecutive rows meet the threshold and height constraint."""
        height, width = dilated.shape

        # Calculate black percentage for each row
        row_black_percentage = np.sum(dilated == 0, axis=1) / width * 100

        # Find all rows meeting threshold
        black_rows = [y for y, p in enumerate(row_black_percentage) if p >= row_thresh]

        # Forcefully include first and last row
        if 0 not in black_rows:
            black_rows.insert(0, 0)
        if (height) not in black_rows:
                black_rows.append(height)

        print(f'πŸ“„ Row Points:: {black_rows}')
        # Group consecutive rows into gutters
        row_gutters = []
        if black_rows:
            start_row = black_rows[0]
            for i, end_row in enumerate(black_rows):
                # Only extend if combined height meets min_height_ratio
                combined_height = end_row - start_row
                if combined_height / height >= self.config.min_height_ratio:
                    print(f'πŸ“„ {i+1}) Start: {start_row:04d} | End: {end_row:04d} | Total: {combined_height:04d} | Ratio: {(combined_height / height):04f}')
                    row_gutters.append((start_row, end_row))
                    start_row = end_row
                elif len(black_rows) == i + 1:
                    row_gutters[-1] = (row_gutters[-1][0], end_row)

        print(f"βœ… Detected panel row gutters: {row_gutters}")

        # ⚑ Draw detected rows on a color copy
        visual = cv2.cvtColor(dilated, cv2.COLOR_GRAY2BGR)
        for (y1, y2) in row_gutters:
            cv2.line(visual, (0, y1), (width, y1), (0, 255, 0), thickness=5)
            cv2.line(visual, (0, y2), (width, y2), (0, 0, 255), thickness=5)

        # Save visualization
        output_path = f"{self.config.output_folder}/row_gutters_visualization.jpg"
        cv2.imwrite(output_path, visual)
        print(f"πŸ“„ Saved row gutter visualization: {output_path}")

        return row_gutters

    def _find_panel_columns(self, dilated: np.ndarray, col_thresh: int) -> List[Tuple[int, int]]:
        """
        Find panel columns where consecutive columns meet the threshold and width constraint.
        """
        height, width = dilated.shape

        # Calculate black percentage for each column
        col_black_percentage = np.sum(dilated == 0, axis=0) / height * 100

        # Find all columns meeting threshold
        black_cols = [x for x, p in enumerate(col_black_percentage) if p >= col_thresh]

        # Forcefully include first and last column
        if 0 not in black_cols:
            black_cols.insert(0, 0)
        if (width - 1) not in black_cols:
            black_cols.append(width - 1)

        # Group consecutive columns into gutters
        col_gutters = []
        if black_cols:
            start_col = black_cols[0]
            prev_col = black_cols[0]
            for x in black_cols:
                if x != start_col:
                    # Only extend if combined width meets min_width_ratio
                    combined_width = x - start_col + 1
                    if combined_width / width >= self.config.min_width_ratio:
                        prev_col = x
                        col_gutters.append((start_col, prev_col))
                        start_col = x

            if start_col != prev_col:
                col_gutters.append((start_col, prev_col))  # Add last gutter

        print(f"βœ… Detected panel column gutters: {col_gutters}")

        # ⚑ Draw detected columns on a color copy
        visual = cv2.cvtColor(dilated, cv2.COLOR_GRAY2BGR)
        for (x1, x2) in col_gutters:
            cv2.line(visual, (x1, 0), (x1, height), (255, 0, 0), thickness=5)
            cv2.line(visual, (x2, 0), (x2, height), (0, 255, 255), thickness=5)

        # Save visualization
        output_path = f"{self.config.output_folder}/col_gutters_visualization.jpg"
        cv2.imwrite(output_path, visual)
        print(f"πŸ“„ Saved column gutter visualization: {output_path}")

        return col_gutters

    def _extract_panels_from_row(self, dilated: np.ndarray, y1: int, y2: int, 
                                col_thresh: int) -> List[Tuple[int, int, int, int]]:
        """Extract panels from a single row."""
        width = dilated.shape[1]
        row_slice = dilated[y1:y2, :]
        col_black_percentage = np.sum(row_slice == 0, axis=0) / (y2 - y1) * 100

        # Find column gutters
        col_gutters = []
        in_gutter = False
        for x, percent_black in enumerate(col_black_percentage):
            if percent_black >= col_thresh and not in_gutter:
                start_col = x
                in_gutter = True
            elif percent_black < col_thresh and in_gutter:
                end_col = x
                col_gutters.append((start_col, end_col))
                in_gutter = False
        
        # Convert gutters to panel columns
        panel_cols = []
        prev_end = 0
        for start, end in col_gutters:
            if start - prev_end > 10:  # Minimum column width
                panel_cols.append((prev_end, start))
            prev_end = end
        
        if width - prev_end > 10:
            panel_cols.append((prev_end, width))
        
        return [(x1, y1, x2, y2) for x1, x2 in panel_cols]
    
    def _filter_panels_by_size(self, panels: List[Tuple[int, int, int, int]], width: int, height: int) -> List[Tuple[int, int, int, int]]:
        """Filter panels by size constraints."""
        new_panel = []
        image_area = width * height

        for x1, y1, x2, y2 in panels:
            w = x2 - x1  # Corrected
            h = y2 - y1  # Corrected
            area = w * h

            if (
                area >= self.config.min_area_ratio * image_area and
                w >= self.config.min_width_ratio * width and
                h >= self.config.min_height_ratio * height
            ):
                new_panel.append((x1, y1, x2, y2))

        return new_panel


    def count_panel_files(self, folder_path: str) -> int:
        """
        Count the number of files in a folder that start with 'panel_'.

        Args:
            folder_path: Path to the folder to search.

        Returns:
            Number of files starting with 'panel_'.
        """
        if not os.path.exists(folder_path):
            print(f"Folder does not exist: {folder_path}")
            return 0

        return len([
            fname for fname in os.listdir(folder_path)
            if fname.startswith("panel_") and os.path.isfile(os.path.join(folder_path, fname))
        ])

    def load_existing_panels_from_folder(self, folder: str) -> List[Tuple[int, int, int, int]]:
        """
        Parses filenames like 'panel_1_(1006, 176, 1757, 1085).jpg' and extracts coordinates.
        """
        pattern = re.compile(r"panel_\d+_\((\d+), (\d+), (\d+), (\d+)\)\.jpg")
        coords = []
        for fname in os.listdir(folder):
            match = pattern.match(fname)
            if match:
                coords.append(tuple(map(int, match.groups())))
        return coords

    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]]:
        """Save panel images and return panel data."""
        original_image = cv2.imread(self.config.input_path)
        visual_output = original.copy()
        panel_images = []
        panel_data = []
        all_panel_path = []

        panel_idx = self.count_panel_files(self.config.output_folder)
        black_overlay_input = cv2.imread(self.config.black_overlay_input_path)

        image_area = width * height
        maybe_full_page_panel = None

        # Load existing panels from disk
        existing_coords = self.load_existing_panels_from_folder(self.config.output_folder)

        for idx, (x1, y1, x2, y2) in enumerate(panels, 1):
            # Extract panel image from black_overlay_input
            panel_img = black_overlay_input[y1:y2, x1:x2]

            # Check for mostly black/white
            gray = cv2.cvtColor(panel_img, cv2.COLOR_BGR2GRAY)
            total_pixels = gray.size
            black_pixels = np.sum(gray < 30)
            white_pixels = np.sum(gray > 240)
            black_ratio = black_pixels / total_pixels
            white_ratio = white_pixels / total_pixels

            if black_ratio > 0.8:
                print(f"⚠️ Skipping panel #{idx} β€” {round(black_ratio * 100, 2)}% black")
                continue
            elif white_ratio > 0.9:
                print(f"⚠️ Skipping panel #{idx} β€” {round(white_ratio * 100, 2)}% white")
                continue
            else:
                print(f"βœ… Panel #{idx} β€” {round(black_ratio * 100, 2)}% black, {round(white_ratio * 100, 2)}% white")

            panel_area = (x2 - x1) * (y2 - y1)
            if panel_area >= 0.9 * image_area:
                print(f"⚠️ Panel #{idx} covers β‰₯90% of the image β€” marked for potential use only")
                maybe_full_page_panel = (idx, (x1, y1, x2, y2))
                continue

            # Check for full containment in existing and current session
            already_saved_coords = existing_coords + [ (pd.x_start, pd.y_start, pd.x_end, pd.y_end) for pd in panel_data ]

            # 1. Skip if duplicate
            is_duplicate, _ = remove_duplicate_boxes(already_saved_coords, (x1, y1, x2, y2))
            if is_duplicate:
                print(f"⚠️ Skipping panel #{idx} β€” fully contained in existing panel")
                continue

            # 2. Skip if this panel contains β‰₯1 other panels
            contained_count = count_panels_inside((x1, y1, x2, y2), already_saved_coords)
            if contained_count >= 1:
                print(f"⚠️ Skipping panel #{idx} β€” contains {contained_count} other panels inside")
                continue

            # Save panel
            panel_img = original_image[y1:y2, x1:x2]
            panel_images.append(panel_img)
            panel_info = PanelData.from_coordinates(x1, y1, x2, y2)
            panel_data.append(panel_info)

            panel_idx += 1
            panel_path = f'{self.config.output_folder}/panel_{panel_idx}_{(x1, y1, x2, y2)}.jpg'
            cv2.imwrite(str(panel_path), panel_img)
            all_panel_path.append(panel_path)

            cv2.rectangle(visual_output, (x1, y1), (x2, y2), (0, 255, 0), 2)
            cv2.putText(visual_output, f"#{idx}", (x1+5, y1+25),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)

        # If no valid panels and full-page backup exists
        if not panel_images and maybe_full_page_panel and panel_idx == 0:
            idx, (x1, y1, x2, y2) = maybe_full_page_panel
            panel_img = original_image[y1:y2, x1:x2]
            panel_images.append(panel_img)
            panel_info = PanelData.from_coordinates(x1, y1, x2, y2)
            panel_data.append(panel_info)

            panel_idx += 1
            panel_path = f'{self.config.output_folder}/panel_{panel_idx}_{(x1, y1, x2, y2)}.jpg'
            cv2.imwrite(str(panel_path), panel_img)
            all_panel_path.append(panel_path)

            cv2.rectangle(visual_output, (x1, y1), (x2, y2), (255, 0, 0), 2)
            cv2.putText(visual_output, f"#full", (x1+5, y1+25),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 0, 0), 2)
            print(f"βœ… Saved full-page panel as fallback")

        # Save final visualization
        visual_path = f'{self.config.output_folder}/panels_visualization.jpg'
        cv2.imwrite(str(visual_path), visual_output)

        print(f"βœ… Extracted {len(panel_images)} panels after filtering.")
        return panel_images, panel_data, all_panel_path