import cv2 import numpy as np import os import argparse def detect_and_crop_image(image_path, output_image_path=None): if not os.path.exists(image_path): print(f"Error: Image file not found at {image_path}") return None # Read the image img = cv2.imread(image_path) if img is None: print("Error: Could not open image.") return None # Convert to grayscale gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Identify "mid-tones" to separate the real photo from pure white or black backgrounds/text. # JPEG artifacts mean pure white/black might vary. We use 20 to 235 as the "mid-tone" photo range. mask = cv2.inRange(gray, 20, 235) # 1. MORPH_OPEN (Erode then Dilate) # This removes thin structures, such as text anti-aliasing, thin lines, or small icons. # A 15x15 kernel removes anything thinner than 15 pixels. kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_open) # 2. MORPH_CLOSE (Dilate then Erode) # This merges nearby blobs and fills holes (e.g., if the photo has pure white/black areas inside). # A large kernel ensures the entire main image forms one single solid block. kernel_close = cv2.getStructuringElement(cv2.MORPH_RECT, (51, 51)) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_close) # Find contours contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) print(f"📊 Encontrados {len(contours)} contornos potenciais na imagem.") if not contours: print("Error: No significant non-background regions detected.") return None # Find the contour with the largest bounding box area max_area = 0 best_bbox = None for c in contours: x, y, w, h = cv2.boundingRect(c) area = w * h if area > max_area: max_area = area best_bbox = (x, y, w, h) if best_bbox is None or max_area < 500: print(f"❌ Aviso: Nenhum conteúdo significativo detectado (max_area={max_area} < 500).") return None x, y, w, h = best_bbox print(f"✅ Melhor região de conteúdo: {w}x{h} @ ({x},{y}) | Área: {max_area}px") x, y, w, h = best_bbox # --- Smart Zoom for Rounded Corners --- # If the corners of our bounding box still touch the background (white/black), # it's likely a rounded corner. We "zoom in" (inset) until the corners are safe. img_h, img_w = img.shape[:2] def check_corners(cx, cy, cw, ch, m): # Check the 4 corner pixels in the mask # We use a small 3x3 average or just the point? Point is simpler. coords = [ (cy, cx), (cy, cx + cw - 1), (cy + ch - 1, cx), (cy + ch - 1, cx + cw - 1) ] for py, px in coords: if m[py, px] == 0: return False return True zoom_inset = 0 max_zoom = min(w, h) // 4 # Prevent zooming more than 25% of the image size while not check_corners(x, y, w, h, mask) and zoom_inset < max_zoom: x += 1 y += 1 w -= 2 h -= 2 zoom_inset += 1 if w <= 20 or h <= 20: break if zoom_inset > 0: print(f"Smart Zoom applied: {zoom_inset}px inset to clear rounded corners.") # --- Validate Crops --- # Only crop if the excluded region is genuinely a white/black background prop_x_min = x prop_y_min = y prop_x_max = x + w prop_y_max = y + h def validate_crop(region, border_region, edge_thresh=0.80, region_thresh=0.60): if region.size == 0 or border_region.size == 0: return False dark_edge = np.count_nonzero(border_region < 20) / border_region.size light_edge = np.count_nonzero(border_region > 235) / border_region.size dark_region = np.count_nonzero(region < 20) / region.size light_region = np.count_nonzero(region > 235) / region.size is_dark_bg = (dark_edge >= edge_thresh) and (dark_region >= region_thresh) is_light_bg = (light_edge >= edge_thresh) and (light_region >= region_thresh) return is_dark_bg or is_light_bg # Validate Top Crop if prop_y_min > 0: top_region = gray[0:prop_y_min, :] top_border = gray[0:min(3, prop_y_min), :] if not validate_crop(top_region, top_border): prop_y_min = 0 # Validate Bottom Crop if prop_y_max < img_h: bottom_region = gray[prop_y_max:img_h, :] bottom_border = gray[max(img_h-3, prop_y_max):img_h, :] if not validate_crop(bottom_region, bottom_border): prop_y_max = img_h # Validate Left Crop if prop_x_min > 0: left_region = gray[:, 0:prop_x_min] left_border = gray[:, 0:min(3, prop_x_min)] if not validate_crop(left_region, left_border): prop_x_min = 0 # Validate Right Crop if prop_x_max < img_w: right_region = gray[:, prop_x_max:img_w] right_border = gray[:, max(img_w-3, prop_x_max):img_w] if not validate_crop(right_region, right_border): prop_x_max = img_w # Inset Logic (2px) - additional fixed safety margin ONLY for valid crops inset = 2 x_min = prop_x_min + inset if prop_x_min > 0 else 0 y_min = prop_y_min + inset if prop_y_min > 0 else 0 x_max = prop_x_max - inset if prop_x_max < img_w else img_w y_max = prop_y_max - inset if prop_y_max < img_h else img_h final_w = x_max - x_min final_h = y_max - y_min if final_w <= 0 or final_h <= 0: print("Error: Invalid crop dimensions after zoom.") return None # Ensure crop dimensions are even if final_w % 2 != 0: final_w -= 1 if final_h % 2 != 0: final_h -= 1 x_max = x_min + final_w y_max = y_min + final_h print(f"Proposed Crop: w={final_w}, h={final_h}, x={x_min}, y={y_min}") # Crop the original image cropped_img = img[y_min:y_max, x_min:x_max] if output_image_path is None: filename, ext = os.path.splitext(image_path) output_image_path = f"{filename}_cropped{ext}" cv2.imwrite(output_image_path, cropped_img) print(f"Successfully created cropped image at {output_image_path}") return output_image_path if __name__ == "__main__": import sys input_image = "image.png" output_image = "image_cropped.png" if len(sys.argv) > 1: input_image = sys.argv[1] if len(sys.argv) > 2: output_image = sys.argv[2] print(f"Processing: {input_image} -> {output_image}") result = detect_and_crop_image(input_image, output_image) if result and os.path.exists(result): print(f"\n✅ Done! Cropped image saved as: {result}") else: print(f"\n❌ Failed to create cropped image.")