Spaces:
Build error
Build error
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') | |