File size: 14,217 Bytes
f0c23ec
 
 
 
 
 
 
 
 
 
bd1f76d
 
f0c23ec
bd1f76d
f0c23ec
 
 
 
 
bd1f76d
f0c23ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd1f76d
 
 
f0c23ec
 
 
 
 
 
 
bd1f76d
f0c23ec
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd1f76d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0c23ec
 
 
 
 
 
 
 
 
 
 
 
 
bd1f76d
 
f0c23ec
 
 
 
 
 
bd1f76d
f0c23ec
 
 
bd1f76d
f0c23ec
bd1f76d
f0c23ec
bd1f76d
 
 
f0c23ec
bd1f76d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0c23ec
 
 
 
 
 
bd1f76d
f0c23ec
 
 
bd1f76d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0c23ec
 
 
 
 
 
d28110d
f0c23ec
 
 
 
 
 
 
 
 
 
 
 
 
 
bd1f76d
f0c23ec
 
 
 
 
 
 
 
bd1f76d
 
f0c23ec
bd1f76d
 
 
 
 
 
 
f0c23ec
bd1f76d
f0c23ec
 
 
 
 
 
 
 
 
 
bd1f76d
f0c23ec
 
bd1f76d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0c23ec
bd1f76d
f0c23ec
 
 
 
 
 
 
 
 
 
 
 
 
 
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
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
import os
import numpy as np
from PIL import Image, ImageDraw
import imageio.v2 as imageio  # Fix for imageio warning
from skimage.color import rgb2gray
from skimage.feature import canny
from skimage import measure
from scipy import ndimage as ndi
import re
from skimage.morphology import remove_small_holes
from .image_processor import ImageProcessor
import cv2

pattern = re.compile(r"panel_\d+_\((\d+), (\d+), (\d+), (\d+)\)\.jpg")

def extract_fully_white_panels(
    original_image: np.ndarray,
    segmentation_mask: np.ndarray,
    output_dir: str = "panel_output",
    debug_region_dir: str = "temp_dir/panel_debug_regions",
    min_area_ratio: float = 0.05,
    min_width_ratio: float = 0.05,
    min_height_ratio: float = 0.05,
    save_debug: bool = True
):
    """
    Extract fully white panels from a segmented image.
    
    Args:
        original_image: Original RGB image as numpy array
        segmentation_mask: Binary segmentation mask
        output_dir: Directory to save extracted panels
        debug_region_dir: Directory to save debug images
        min_area_ratio: Minimum area ratio threshold
        min_width_ratio: Minimum width ratio threshold
        min_height_ratio: Minimum height ratio threshold
        save_debug: Whether to save debug images
    
    Returns:
        List of saved panel file paths
    """
    os.makedirs(output_dir, exist_ok=True)
    if save_debug:
        os.makedirs(debug_region_dir, exist_ok=True)

    img_h, img_w = segmentation_mask.shape
    image_area = img_h * img_w

    orig_pil = Image.fromarray(original_image)
    labeled_mask = measure.label(segmentation_mask)
    regions = measure.regionprops(labeled_mask)

    saved_panels = []
    accepted_boxes = []
    panel_idx = 0

    for idx, region in enumerate(regions):
        minr, minc, maxr, maxc = region.bbox
        w = maxc - minc
        h = maxr - minr
        area = w * h
        crop_box = (minc, minr, maxc, maxr)
        crop_name_prefix = f"region_{idx+1}"

        # Crops
        cropped_img = orig_pil.crop(crop_box)
        cropped_mask = segmentation_mask[minr:maxr, minc:maxc]
        # Fix for Pillow warning: Remove mode parameter
        mask_pil = Image.fromarray((cropped_mask * 255).astype('uint8'))

        # 1. Threshold check
        if (
            w < min_width_ratio * img_w or
            h < min_height_ratio * img_h
        ):
            # if save_debug:
            #     cropped_img.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_too_small_orig.jpg"))
            #     mask_pil.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_too_small_mask.jpg"))
            continue

        # 2. Check if region is mostly white (allow small % of black)
        black_pixel_count = np.count_nonzero(region.image == 0)
        total_pixels = region.image.size
        black_ratio = black_pixel_count / total_pixels

        if black_ratio > 0.1:  # Allow up to 1% black pixels
            print(f"❌ Black ratio panel #{idx} β€” {round(black_ratio * 100, 2)}% black")
            # Save debug info if desired
            if save_debug:
                debug_region_dir_specific = os.path.join(output_dir, f"region_{idx}_skipped_black_inside")
                os.makedirs(debug_region_dir_specific, exist_ok=True)
                
                # Save cropped mask
                cropped_mask = segmentation_mask[minr:maxr, minc:maxc]
                # Fix for Pillow warning: Remove mode parameter
                mask_pil = Image.fromarray((cropped_mask * 255).astype("uint8"))
                mask_pil.save(os.path.join(debug_region_dir_specific, f"region_{idx}_mask.jpg"))
                
                # Highlight black pixels in red and zoom
                highlighted = np.stack([cropped_mask]*3, axis=-1) * 255
                highlighted[cropped_mask == 0] = [255, 0, 0]
                highlighted_zoom = Image.fromarray(highlighted.astype('uint8')).resize(
                    (highlighted.shape[1]*4, highlighted.shape[0]*4), resample=Image.NEAREST
                )
                highlighted_zoom.save(os.path.join(debug_region_dir_specific, f"region_{idx}_highlight_black_zoomed.jpg"))
            
            continue

        # 3. Save valid panel with bbox coordinates in filename
        bbox_str = f"({minc}, {minr}, {maxc}, {maxr})"
        panel_idx = panel_idx + 1
        panel_path = os.path.join(output_dir, f"panel_{panel_idx}_{bbox_str}.jpg")
        cropped_img.save(panel_path)
        saved_panels.append(panel_path)
        accepted_boxes.append((minc, minr, maxc, maxr))

        if save_debug:
            cropped_img.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_saved_orig.jpg"))
            mask_pil.save(os.path.join(debug_region_dir, f"{crop_name_prefix}_saved_mask.jpg"))

    # 4. Debug image with accepted boxes
    if save_debug:
        debug_img = orig_pil.copy()
        draw = ImageDraw.Draw(debug_img)
        for (x1, y1, x2, y2) in accepted_boxes:
            draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
        debug_img.save(os.path.join(output_dir, "debug_all_saved_panels.jpg"))

    return saved_panels

def get_region_count(binary_seg):
    labeled_mask = measure.label(binary_seg)
    regions = measure.regionprops(labeled_mask)

    img_h, img_w = binary_seg.shape
    image_area = img_h * img_w
    count = 0
    for idx, region in enumerate(regions):
        minr, minc, maxr, maxc = region.bbox
        w = maxc - minc
        h = maxr - minr
        area = w * h

        if (
            area < 0.05 * image_area or
            w < 0.05 * img_w or
            h < 0.05 * img_h
        ):
            continue

        count += 1

    return count

def get_black_white_ratio(image_path, threshold=128):
    """
    Calculates the ratio of black and white pixels in a binary image.
    
    Parameters:
        image_path (str): Path to the image file.
        threshold (int): Threshold value for binarization (default: 128).
    
    Returns:
        dict: Dictionary with black_ratio, white_ratio, black_count, white_count, total_pixels.
    """
    # Load image in grayscale
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)

    if img is None:
        raise FileNotFoundError(f"Image not found: {image_path}")

    # Convert to binary using the given threshold
    _, binary = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)

    total_pixels = binary.size
    white_count = np.count_nonzero(binary == 255)
    black_count = total_pixels - white_count

    black_ratio = black_count / total_pixels
    white_ratio = white_count / total_pixels

    return {
        "black_ratio": black_ratio,
        "white_ratio": white_ratio,
        "black_count": black_count,
        "white_count": white_count,
        "total_pixels": total_pixels
    }


def create_segmentation_mask(image: np.ndarray, save_debug: bool = True) -> np.ndarray:
    """
    Create segmentation mask from image using edge detection and hole filling.
    
    Args:
        image: Input RGB image as numpy array
        save_debug: Whether to save intermediate processing steps
    
    Returns:
        Binary segmentation mask
    """
    if save_debug:
        os.makedirs("temp_dir/panel_debug_steps", exist_ok=True)
        Image.fromarray(image).save("temp_dir/panel_debug_steps/step1_original.jpg")

    # Convert to grayscale
    grayscale = rgb2gray(image)
    if save_debug:
        gray_uint8 = (grayscale * 255).astype('uint8')
        # Fix for Pillow warning: Remove mode parameter
        Image.fromarray(gray_uint8).save("temp_dir/panel_debug_steps/step2_grayscale.jpg")

    # Edge detection
    edges = canny(grayscale)
    edges_uint8 = (edges * 255).astype('uint8')
    if save_debug:
        Image.fromarray(edges_uint8).save("temp_dir/panel_debug_steps/step3_edges.jpg")

    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    seg = cv2.dilate(edges_uint8, kernel, iterations=2)
    seg = cv2.ximgproc.thinning(seg)
    # Fill holes in edges
    segmentation = ndi.binary_fill_holes(seg)
    # Ensure it's a NumPy boolean or 0/1 array
    binary_seg = segmentation.astype(np.uint8)

    # Count white and black pixels
    total_pixels = binary_seg.size
    white_pixels = np.count_nonzero(binary_seg)  # 1s

    # Ratios
    white_ratio = white_pixels / total_pixels

    region_count = get_region_count(binary_seg)
    if white_ratio > 0.8 or region_count == 1:
        print(f"⚠️ white is maximum hence reverting to only binary_fill_holes")
        # Fill holes in edges
        segmentation = ndi.binary_fill_holes(edges)

    # βœ… Remove small black clusters (holes in white regions)
    segmentation_cleaned = remove_small_holes(segmentation, area_threshold=500)  # adjust threshold as needed

    if save_debug:
        segmentation_uint8 = (segmentation_cleaned * 255).astype('uint8')
        Image.fromarray(segmentation_uint8).save("temp_dir/panel_debug_steps/step4_segmentation_filled.jpg")

    return segmentation_cleaned

def boxes_are_close(box1, box2, thresh):
    # Horizontal overlap or near
    horiz_close = (box1[2] >= box2[0] - thresh and box1[0] <= box2[2] + thresh)
    # Vertical overlap or near
    vert_close = (box1[3] >= box2[1] - thresh and box1[1] <= box2[3] + thresh)
    return horiz_close and vert_close

def merge_close_panels(saved_panels, draw, distance_thresh=20):
    """Merge panels with close bounding boxes and fill them on draw object."""
    # Step 1: Extract bounding boxes
    boxes = []
    for panel_path in saved_panels:
        panel_name = os.path.basename(panel_path)
        match = pattern.match(panel_name)
        if match:
            minc, minr, maxc, maxr = map(int, match.groups())
            boxes.append([minc, minr, maxc, maxr])

    # Step 2: Merge nearby boxes
    merged = []
    used = [False] * len(boxes)

    for i in range(len(boxes)):
        if used[i]:
            continue
        box1 = boxes[i]
        merged_box = box1.copy()

        for j in range(i + 1, len(boxes)):
            if used[j]:
                continue
            box2 = boxes[j]

            # Check if boxes are close (horizontal and vertical)
            if boxes_are_close(box1, box2, distance_thresh):
                # Merge boxes
                merged_box = [
                    min(merged_box[0], box2[0]),
                    min(merged_box[1], box2[1]),
                    max(merged_box[2], box2[2]),
                    max(merged_box[3], box2[3])
                ]
                used[j] = True

        used[i] = True
        merged.append(merged_box)

    # Step 3: Fill merged boxes
    for box in merged:
        draw.rectangle(box, fill=(0, 0, 0))

def create_image_with_panels_removed(
    original_image: np.ndarray,
    segmentation_mask: np.ndarray,
    output_folder: str,
    output_path: str,
    save_debug: True
) -> None:
    """
    Create a version of the original image with detected panels blacked out.
    
    Args:
        original_image: Original RGB image as numpy array
        segmentation_mask: Binary segmentation mask
        output_path: Path to save the modified image
    """
    # Get panel information
    saved_panels = extract_fully_white_panels(
        original_image=original_image,
        segmentation_mask=segmentation_mask,
        output_dir=output_folder,
        debug_region_dir="temp_dir/panel_debug_regions",
        save_debug=save_debug
    )
    
    # Create modified image
    im_no_panels = Image.fromarray(original_image.copy())
    draw = ImageDraw.Draw(im_no_panels)
    
    # Get regions and black them out
    # labeled_mask = measure.label(segmentation_mask)
    # regions = measure.regionprops(labeled_mask)
    
    # for panel_path in saved_panels:
    #     # Extract panel index from filename with bbox format
    #     panel_name = os.path.basename(panel_path)
    #     match = pattern.match(panel_name)
    #     minc, minr, maxc, maxr = map(int, match.groups())

    #     draw.rectangle([minc, minr, maxc, maxr], fill=(0, 0, 0))

    merge_close_panels(saved_panels, draw, distance_thresh=25)
    
    # Save the result
    im_no_panels.save(output_path)


def main(output_folder, input_image_path, original_image_path):
    """Main execution function."""
    # Load the input image
    image = imageio.imread(input_image_path)
    original_image = imageio.imread(original_image_path)
    save_debug = True
    # Create segmentation mask
    segmentation_mask = create_segmentation_mask(image, save_debug=save_debug)
    segmentation_mask_output_path = f"temp_dir/panel_debug_steps/step4_segmentation_filled.jpg"

    pixel_ratios = get_black_white_ratio(segmentation_mask_output_path)

    if pixel_ratios['black_ratio'] < 0.8:
        print(f"βœ… black is less hence applying other features")
        image_pros = ImageProcessor()
        new_path = image_pros.thick_black(segmentation_mask_output_path, file_name="step5_thick.jpg", output_folder="temp_dir/panel_debug_steps")

        new_path = image_pros.connect_horizontal_vertical_gaps(new_path, file_name="step6_continuity.jpg", output_folder="temp_dir/panel_debug_steps")

        pixel_ratios = get_black_white_ratio(new_path)
        if pixel_ratios['black_ratio'] < 0.8:
            new_path = image_pros.thin_image_borders(new_path, file_name="step7_thin.jpg", output_folder="temp_dir/panel_debug_steps")

            new_path = image_pros.remove_dangling_lines(new_path, file_name="step8_remove_dangling_lines.jpg", output_folder="temp_dir/panel_debug_steps")

            new_path = image_pros.thick_black(new_path, file_name="step9_thick.jpg", output_folder="temp_dir/panel_debug_steps")

            segmentation_mask = cv2.imread(new_path, cv2.IMREAD_GRAYSCALE)

    pre_process_path = f"{output_folder}/00_original_with_panels_removed.jpg"
    # Create image with panels removed
    create_image_with_panels_removed(
        original_image=original_image,
        segmentation_mask=segmentation_mask,
        output_folder=output_folder,
        output_path=pre_process_path,
        save_debug=save_debug
    )

    return pre_process_path


if __name__ == "__main__":
    main('panel_output', 'test7.jpg', 'test7.jpg')