|
|
| |
| import gradio as gr |
| import cv2 |
| import numpy as np |
|
|
| |
| def run_visual_analysis(img1_rgb, img2_rgb, ratio_threshold, method_choice): |
| """ |
| A single, robust function to handle feature matching. It selects the matcher |
| based on the user's choice from the UI. |
| """ |
| |
| if img1_rgb is None or img2_rgb is None: |
| raise gr.Error("Please upload two images to compare.") |
|
|
| matcher_type = "FLANN" if "FLANN" in method_choice else "Brute-Force" |
|
|
| |
| img1_gray = cv2.cvtColor(img1_rgb, cv2.COLOR_RGB2GRAY) |
| img2_gray = cv2.cvtColor(img2_rgb, cv2.COLOR_RGB2GRAY) |
| sift = cv2.SIFT_create() |
| kp1, des1 = sift.detectAndCompute(img1_gray, None) |
| kp2, des2 = sift.detectAndCompute(img2_gray, None) |
|
|
| if des1 is None or des2 is None or len(kp1) < 2 or len(kp2) < 2: |
| return None, "Analysis Failed: Could not find enough features in one or both images. They may be too blurry, too small, or lack distinct details." |
|
|
| |
| if matcher_type == 'FLANN': |
| index_params = dict(algorithm=1, trees=5) |
| search_params = dict(checks=50) |
| matcher = cv2.FlannBasedMatcher(index_params, search_params) |
| else: |
| matcher = cv2.BFMatcher() |
|
|
| matches = matcher.knnMatch(np.float32(des1), np.float32(des2), k=2) |
|
|
| |
| good_matches = [] |
| |
| if matches and len(matches[0]) == 2: |
| for m, n in matches: |
| if m.distance < ratio_threshold * n.distance: |
| good_matches.append(m) |
| |
| if len(good_matches) < 4: |
| return None, f"Found only {len(good_matches)} high-quality matches. This is too few for a reliable geometric analysis. Try increasing the 'Match Sensitivity' slider." |
|
|
| |
| src_pts = np.float32([kp1[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2) |
| dst_pts = np.float32([kp2[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2) |
| |
| M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0) |
| |
| if M is None or mask is None: |
| return None, f"Found {len(good_matches)} initial matches, but could not establish a consistent geometric relationship. The scenes may be too different." |
| |
| inlier_mask = mask.ravel().tolist() |
| inlier_count = sum(inlier_mask) |
|
|
| if inlier_count < 4: |
| return None, f"Found {len(good_matches)} initial matches, but only {inlier_count} were geometrically consistent. The scenes might be too different or the sensitivity setting too low." |
| |
| |
| h1, w1 = img1_rgb.shape[:2] |
| h2, w2 = img2_rgb.shape[:2] |
| combined_img = np.zeros((max(h1, h2), w1 + w2, 3), dtype=np.uint8) |
| combined_img[:h1, :w1, :] = img1_rgb |
| combined_img[:h2, w1:w1 + w2, :] = img2_rgb |
| |
| |
| for i, match in enumerate(good_matches): |
| if inlier_mask[i]: |
| pt1 = tuple(map(int, kp1[match.queryIdx].pt)) |
| pt2_shifted = (int(kp2[match.trainIdx].pt[0] + w1), int(kp2[match.trainIdx].pt[1])) |
| |
| cv2.line(combined_img, pt1, pt2_shifted, (34, 139, 34), 1, cv2.LINE_AA) |
| |
| cv2.circle(combined_img, pt1, 4, (0, 255, 255), -1) |
| cv2.circle(combined_img, pt2_shifted, 4, (0, 255, 255), -1) |
|
|
| |
| summary = ( |
| f"**β
{matcher_type} Analysis Complete**\n\n" |
| f"- **Total Features Found:** Image 1: `{len(kp1)}`, Image 2: `{len(kp2)}`\n" |
| f"- **High-Quality Matches (Ratio Test):** `{len(good_matches)}`\n" |
| f"- **Geometrically Consistent Matches (Inliers):** `{inlier_count}`\n\n" |
| f"The visualization highlights **{inlier_count}** structural elements robustly identified in both images." |
| ) |
| return combined_img, summary |
|
|
| |
| with gr.Blocks(theme=gr.themes.Soft(primary_hue="slate", secondary_hue="orange")) as demo: |
| gr.Markdown( |
| "# πΌοΈ Visual Feature Comparator\n" |
| "Upload two images to find and visualize matching features. This tool uses SIFT and geometric verification to robustly compare scenes." |
| ) |
| |
| with gr.Row(variant="panel"): |
| |
| with gr.Column(scale=1, min_width=350): |
| gr.Markdown("### βοΈ Controls") |
| |
| |
| img1_input = gr.Image(type="numpy", label="Upload Image 1") |
| img2_input = gr.Image(type="numpy", label="Upload Image 2") |
| |
| with gr.Accordion("Analysis Settings", open=True): |
| method_selector = gr.Radio( |
| choices=['SIFT + FLANN (Fast, Recommended)', 'SIFT + Brute-Force (Classic, Slower)'], |
| value='SIFT + FLANN (Fast, Recommended)', |
| label="Analysis Method" |
| ) |
| ratio_slider = gr.Slider( |
| minimum=0.4, maximum=0.95, value=0.75, step=0.01, |
| label="Match Sensitivity", |
| info="Lower = fewer, higher-quality matches. Higher = more, potentially incorrect matches." |
| ) |
| |
| run_button = gr.Button("π Run Visual Analysis", variant="primary") |
|
|
| |
| with gr.Column(scale=3): |
| gr.Markdown("### π Results") |
| result_summary = gr.Markdown("Analysis results will be displayed here.", elem_id="summary_text") |
| result_image = gr.Image(label="Visual Comparison", interactive=False, show_download_button=True) |
|
|
| |
| run_button.click( |
| fn=run_visual_analysis, |
| inputs=[img1_input, img2_input, ratio_slider, method_selector], |
| outputs=[result_image, result_summary], |
| api_name="visual_analysis", |
| show_progress="full" |
| ) |
|
|
| |
| print("π Launching the Visual Feature Comparator...") |
| demo.launch(debug=True, share=True) |