| import os |
| import numpy as np |
| from PIL import Image, ImageDraw |
| import imageio.v2 as imageio |
| 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}" |
|
|
| |
| cropped_img = orig_pil.crop(crop_box) |
| cropped_mask = segmentation_mask[minr:maxr, minc:maxc] |
| |
| mask_pil = Image.fromarray((cropped_mask * 255).astype('uint8')) |
|
|
| |
| if ( |
| w < min_width_ratio * img_w or |
| h < min_height_ratio * img_h |
| ): |
| |
| |
| |
| continue |
|
|
| |
| 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: |
| print(f"❌ Black ratio panel #{idx} — {round(black_ratio * 100, 2)}% black") |
| |
| 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) |
| |
| |
| cropped_mask = segmentation_mask[minr:maxr, minc:maxc] |
| |
| mask_pil = Image.fromarray((cropped_mask * 255).astype("uint8")) |
| mask_pil.save(os.path.join(debug_region_dir_specific, f"region_{idx}_mask.jpg")) |
| |
| |
| 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 |
|
|
| |
| 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")) |
|
|
| |
| 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. |
| """ |
| |
| img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) |
|
|
| if img is None: |
| raise FileNotFoundError(f"Image not found: {image_path}") |
|
|
| |
| _, 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") |
|
|
| |
| grayscale = rgb2gray(image) |
| if save_debug: |
| gray_uint8 = (grayscale * 255).astype('uint8') |
| |
| Image.fromarray(gray_uint8).save("temp_dir/panel_debug_steps/step2_grayscale.jpg") |
|
|
| |
| 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) |
| |
| segmentation = ndi.binary_fill_holes(seg) |
| |
| binary_seg = segmentation.astype(np.uint8) |
|
|
| |
| total_pixels = binary_seg.size |
| white_pixels = np.count_nonzero(binary_seg) |
|
|
| |
| 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") |
| |
| segmentation = ndi.binary_fill_holes(edges) |
|
|
| |
| segmentation_cleaned = remove_small_holes(segmentation, area_threshold=500) |
|
|
| 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): |
| |
| horiz_close = (box1[2] >= box2[0] - thresh and box1[0] <= box2[2] + thresh) |
| |
| 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.""" |
| |
| 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]) |
|
|
| |
| 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] |
|
|
| |
| if boxes_are_close(box1, box2, distance_thresh): |
| |
| 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) |
|
|
| |
| 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 |
| """ |
| |
| 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 |
| ) |
| |
| |
| im_no_panels = Image.fromarray(original_image.copy()) |
| draw = ImageDraw.Draw(im_no_panels) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
|
|
| merge_close_panels(saved_panels, draw, distance_thresh=25) |
| |
| |
| im_no_panels.save(output_path) |
|
|
|
|
| def main(output_folder, input_image_path, original_image_path): |
| """Main execution function.""" |
| |
| image = imageio.imread(input_image_path) |
| original_image = imageio.imread(original_image_path) |
| save_debug = True |
| |
| 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( |
| 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') |
|
|