File size: 4,340 Bytes
cd35f72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import cv2
import numpy as np
import gradio as gr

def sift_ransac_matching(image, template, ratio_thresh=0.75, ransac_reproj_thresh=5.0, score_thresh=0.8, min_inliers=10):
    """
    Returns:
      - result_text (str)
      - match_score (float, inlier_ratio = inliers / good_matches)
      - detected (bool)
      - annotated_image (numpy array, BGR)
    """
    if image is None or template is None:
        return "Invalid input images.", 0.0, False, image

    # Convert to grayscale
    gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)

    # Initialize SIFT detector
    sift = cv2.SIFT_create()

    # Find keypoints and descriptors
    kp_img, des_img = sift.detectAndCompute(gray_image, None)
    kp_tmpl, des_tmpl = sift.detectAndCompute(gray_template, None)

    if des_img is None or des_tmpl is None or len(kp_img) == 0 or len(kp_tmpl) == 0:
        return "No features detected. Template not found.", 0.0, False, image

    # KNN matches with Lowe's ratio test
    bf = cv2.BFMatcher()
    knn = bf.knnMatch(des_img, des_tmpl, k=2)

    good_matches = []
    for pair in knn:
        if len(pair) < 2:
            continue
        m, n = pair
        if m.distance < ratio_thresh * n.distance:
            good_matches.append(m)

    if len(good_matches) < 4:
        return f"Not enough good matches ({len(good_matches)}). Template not found.", 0.0, False, image

    # Build point arrays
    src_pts = np.float32([kp_img[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)   # image -> src
    dst_pts = np.float32([kp_tmpl[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)  # template -> dst

    # Homography with RANSAC
    M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, ransac_reproj_thresh)
    if M is None or mask is None:
        return "Homography failed. Template not found.", 0.0, False, image

    inliers = int(mask.ravel().sum())
    match_score = float(inliers) / float(len(good_matches))  # inlier ratio

    # Detection decision (both quality and absolute inlier count can help stability)
    detected = (match_score >= score_thresh) and (inliers >= min_inliers)

    # Annotate image by projecting template corners back to image space
    annotated = image.copy()
    if detected:
        try:
            # M maps image -> template; invert to map template -> image
            Minv = np.linalg.inv(M)
            h, w = gray_template.shape[:2]
            tmpl_corners = np.float32([[0, 0], [w, 0], [w, h], [0, h]]).reshape(-1, 1, 2)
            proj = cv2.perspectiveTransform(tmpl_corners, Minv)
            proj = proj.astype(int)

            # Draw polygon
            cv2.polylines(annotated, [proj.reshape(-1, 2)], True, (0, 255, 0), 2)
            # Put label
            txt = f"Detected | score={match_score:.3f} | inliers={inliers}/{len(good_matches)}"
            x, y = proj.reshape(-1, 2)[0]
            cv2.putText(annotated, txt, (max(0, x), max(20, y)),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
        except np.linalg.LinAlgError:
            # If inversion fails, still return score/decision
            pass

    result_text = (
        f"Template {'found' if detected else 'not found'} | "
        f"score={match_score:.3f} | inliers={inliers}/{len(good_matches)} good matches"
    )
    return result_text, match_score, detected, annotated

# Gradio UI
iface = gr.Interface(
    fn=sift_ransac_matching,
    inputs=[
        gr.Image(type="numpy", label="Target Image"),
        gr.Image(type="numpy", label="Template Image"),
        gr.Slider(0.5, 0.95, value=0.75, step=0.01, label="Lowe ratio threshold"),
        gr.Slider(1.0, 10.0, value=5.0, step=0.5, label="RANSAC reprojection threshold"),
        gr.Slider(0.1, 1.0, value=0.8, step=0.01, label="Match score threshold"),
        gr.Slider(0, 100, value=10, step=1, label="Minimum inliers"),
    ],
    outputs=[
        gr.Text(label="Result"),
        gr.Number(label="Match score (inlier ratio)"),
        gr.Checkbox(label="Detected?"),
        gr.Image(label="Annotated result"),
    ],
    title="SIFT + RANSAC Template Search",
    description="Uploads a target and a template. Shows detection decision, match score, and an annotated result."
)

if __name__ == "__main__":
    iface.launch()