subapi / detect_crop_image.py
habulaj's picture
Update detect_crop_image.py
afb2ca8 verified
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.")