160 lines
6.4 KiB
Python
160 lines
6.4 KiB
Python
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
from scipy.signal import find_peaks, savgol_filter
|
|
from scipy.ndimage import sobel
|
|
|
|
def analyze_profile(
|
|
profile: np.ndarray,
|
|
prominence: float,
|
|
width: int,
|
|
direction: str
|
|
) -> int | None:
|
|
"""
|
|
Analyzes a 1D profile to find the most likely edge coordinate.
|
|
"""
|
|
if profile.size == 0:
|
|
return None
|
|
|
|
# 1. Smooth the profile to reduce noise while preserving peak shapes.
|
|
# The window length must be odd.
|
|
window_length = min(21, len(profile) // 2 * 2 + 1)
|
|
if window_length < 5: # savgol_filter requires window_length > polyorder
|
|
smoothed_profile = profile
|
|
else:
|
|
smoothed_profile = savgol_filter(profile, window_length=window_length, polyorder=2)
|
|
|
|
# 2. Find all significant peaks in the profile.
|
|
# Prominence is a measure of how much a peak stands out from the baseline.
|
|
peaks, properties = find_peaks(smoothed_profile, prominence=prominence, width=width)
|
|
|
|
if len(peaks) == 0:
|
|
return None
|
|
|
|
# 3. Select the best peak. We choose the one with the highest prominence.
|
|
most_prominent_peak_index = np.argmax(properties['prominences'])
|
|
best_peak = peaks[most_prominent_peak_index]
|
|
|
|
return best_peak
|
|
|
|
def find_film_edges_gradient(
|
|
image_path: str,
|
|
border_percent: int = 15,
|
|
prominence: float = 10.0,
|
|
min_width: int = 2
|
|
) -> tuple[int, int, int, int] | None:
|
|
"""
|
|
Detects film edges using a directional gradient method, which is robust
|
|
to complex borders and internal image features.
|
|
|
|
Args:
|
|
image_path (str): Path to the image file.
|
|
border_percent (int): The percentage of image dimensions to search for a border.
|
|
prominence (float): Required prominence of a gradient peak to be considered an edge.
|
|
This is the most critical tuning parameter. Higher values mean
|
|
the edge must be sharper and more distinct.
|
|
min_width (int): The minimum width (in pixels) of a peak to be considered.
|
|
Helps ignore single-pixel noise.
|
|
|
|
Returns:
|
|
A tuple (left, top, right, bottom) or None if detection fails.
|
|
"""
|
|
try:
|
|
with Image.open(image_path) as img:
|
|
image_gray = np.array(img.convert('L'), dtype=float)
|
|
height, width = image_gray.shape
|
|
except Exception as e:
|
|
print(f"Error opening or processing image: {e}")
|
|
return None
|
|
|
|
# 1. Calculate directional gradients for the entire image once.
|
|
grad_y = sobel(image_gray, axis=0) # For horizontal lines (top, bottom)
|
|
grad_x = sobel(image_gray, axis=1) # For vertical lines (left, right)
|
|
|
|
coords = {}
|
|
search_w = int(width * border_percent / 100)
|
|
search_h = int(height * border_percent / 100)
|
|
|
|
# 2. Find Left Edge (Dark -> Light transition, so positive grad_x)
|
|
left_band_grad = grad_x[:, :search_w]
|
|
# We only care about positive gradients (dark to light)
|
|
left_profile = np.sum(np.maximum(0, left_band_grad), axis=0)
|
|
left_coord = analyze_profile(left_profile, prominence, min_width, "left")
|
|
coords['left'] = left_coord if left_coord is not None else 0
|
|
|
|
# 3. Find Right Edge (Light -> Dark transition, so negative grad_x)
|
|
right_band_grad = grad_x[:, -search_w:]
|
|
# We want the strongest negative gradient, so we flip the sign.
|
|
right_profile = np.sum(np.maximum(0, -right_band_grad), axis=0)
|
|
# The profile is from right-to-left, so we analyze its reversed version.
|
|
right_coord = analyze_profile(right_profile[::-1], prominence, min_width, "right")
|
|
if right_coord is not None:
|
|
# Convert coordinate back to the original image space
|
|
coords['right'] = width - 1 - right_coord
|
|
else:
|
|
coords['right'] = width - 1
|
|
|
|
# 4. Find Top Edge (Dark -> Light transition, so positive grad_y)
|
|
top_band_grad = grad_y[:search_h, :]
|
|
top_profile = np.sum(np.maximum(0, top_band_grad), axis=1)
|
|
top_coord = analyze_profile(top_profile, prominence, min_width, "top")
|
|
coords['top'] = top_coord if top_coord is not None else 0
|
|
|
|
# 5. Find Bottom Edge (Light -> Dark transition, so negative grad_y)
|
|
bottom_band_grad = grad_y[-search_h:, :]
|
|
bottom_profile = np.sum(np.maximum(0, -bottom_band_grad), axis=1)
|
|
bottom_coord = analyze_profile(bottom_profile[::-1], prominence, min_width, "bottom")
|
|
if bottom_coord is not None:
|
|
coords['bottom'] = height - 1 - bottom_coord
|
|
else:
|
|
coords['bottom'] = height - 1
|
|
|
|
# 6. Sanity check and return
|
|
left, top, right, bottom = map(int, [coords['left'], coords['top'], coords['right'], coords['bottom']])
|
|
if not (left < right and top < bottom):
|
|
print("Warning: Detection failed, coordinates are illogical.")
|
|
return None
|
|
|
|
return (left, top, right, bottom)
|
|
|
|
|
|
# --- Example Usage ---
|
|
if __name__ == "__main__":
|
|
# Use the image you provided.
|
|
# NOTE: You must save your image as 'test_negative_v2.png' in the same
|
|
# directory as the script, or change the path here.
|
|
image_file = "test_negative_v2.png"
|
|
|
|
print(f"Attempting to detect edges on '{image_file}' with gradient method...")
|
|
|
|
# Run the detection. The 'prominence' parameter is the one to tune.
|
|
# Start with a moderate value and increase if it detects noise, decrease
|
|
# if it misses a subtle edge.
|
|
crop_box = find_film_edges_gradient(
|
|
image_file,
|
|
border_percent=15, # Increase search area slightly due to complex borders
|
|
prominence=5.0, # A higher value is needed for this high-contrast example
|
|
min_width=2
|
|
)
|
|
|
|
if crop_box:
|
|
print(f"\nDetected image box: {crop_box}")
|
|
|
|
with Image.open(image_file) as img:
|
|
# Add a white border for better visualization if the background is black
|
|
img_with_border = Image.new('RGB', (img.width + 2, img.height + 2), 'white')
|
|
img_with_border.paste(img, (1, 1))
|
|
|
|
draw = ImageDraw.Draw(img_with_border)
|
|
# Offset the crop box by 1 pixel due to the added border
|
|
offset_box = [c + 1 for c in crop_box]
|
|
draw.rectangle(offset_box, outline="red", width=2)
|
|
|
|
output_path = "test_negative_detected_final.png"
|
|
img_with_border.save(output_path)
|
|
print(f"Saved visualization to '{output_path}'")
|
|
try:
|
|
img_with_border.show(title="Detection Result")
|
|
except Exception:
|
|
pass
|
|
else:
|
|
print("\nCould not robustly detect the film edges.") |