PySIFT / app.py
sivakumar
Convert to Gradio app with interactive SIFT matching demo
00c6d56
Raw
History Blame Contribute Delete
7.58 kB
"""PySIFT HuggingFace Space — static info + interactive matching demo."""
import time
import cv2
import numpy as np
import gradio as gr
HEADER_HTML = """
<div style="max-width:800px; margin:0 auto; font-family:system-ui,sans-serif; color:#333;">
<h1 style="margin-bottom:4px;">PySIFT</h1>
<p><strong>GPU-Resident Deterministic SIFT for Deep Learning Vision Pipelines</strong></p>
<div style="margin:16px 0;">
<a href="https://arxiv.org/abs/2605.17869" target="_blank" style="display:inline-block;padding:8px 16px;margin:4px;background:#b31b1b;color:white;text-decoration:none;border-radius:6px;font-weight:bold;">arXiv Paper</a>
<a href="https://github.com/SivaIITM/PySIFT" target="_blank" style="display:inline-block;padding:8px 16px;margin:4px;background:#2ecc71;color:white;text-decoration:none;border-radius:6px;font-weight:bold;">GitHub Code</a>
<a href="https://pypi.org/project/staysift/" target="_blank" style="display:inline-block;padding:8px 16px;margin:4px;background:#3775a9;color:white;text-decoration:none;border-radius:6px;font-weight:bold;">pip install staysift</a>
<a href="https://www.kaggle.com/code/sivakumarksce24d040/pysift-tutorial" target="_blank" style="display:inline-block;padding:8px 16px;margin:4px;background:#20BEFF;color:white;text-decoration:none;border-radius:6px;font-weight:bold;">Kaggle Tutorial</a>
<a href="https://www.kaggle.com/competitions/imc-2026-warm-up-landmark-matching-sprint" target="_blank" style="display:inline-block;padding:8px 16px;margin:4px;background:#FF6F00;color:white;text-decoration:none;border-radius:6px;font-weight:bold;">Kaggle Competition</a>
</div>
<p>A pure-Python, GPU-resident SIFT implementation that matches OpenCV SIFT accuracy while running
<strong>26% faster end-to-end</strong> with <strong>4x matching speedup</strong>.
Zero-copy DLPack interop keeps tensors on the GPU across the full pipeline.</p>
<table style="border-collapse:collapse; margin:16px 0; width:100%;">
<tr><th style="border:1px solid #ddd;padding:8px 14px;background:#f5f5f5;">Benchmark</th>
<th style="border:1px solid #ddd;padding:8px 14px;background:#f5f5f5;">Metric</th>
<th style="border:1px solid #ddd;padding:8px 14px;background:#f5f5f5;">PySIFT vs OpenCV</th></tr>
<tr><td style="border:1px solid #ddd;padding:8px 14px;">HPatches</td>
<td style="border:1px solid #ddd;padding:8px 14px;">MMA@10</td>
<td style="border:1px solid #ddd;padding:8px 14px;">+2.2pp</td></tr>
<tr><td style="border:1px solid #ddd;padding:8px 14px;">IMC Phototourism</td>
<td style="border:1px solid #ddd;padding:8px 14px;">Inliers/pair</td>
<td style="border:1px solid #ddd;padding:8px 14px;">303 vs 205 (+47%)</td></tr>
<tr><td style="border:1px solid #ddd;padding:8px 14px;">MegaDepth-1500</td>
<td style="border:1px solid #ddd;padding:8px 14px;">AUC@10</td>
<td style="border:1px solid #ddd;padding:8px 14px;">+5.6pp</td></tr>
<tr><td style="border:1px solid #ddd;padding:8px 14px;">ROxford5K</td>
<td style="border:1px solid #ddd;padding:8px 14px;">mAP</td>
<td style="border:1px solid #ddd;padding:8px 14px;">+7.5pp</td></tr>
</table>
<h3>Quick Start</h3>
<pre style="background:#f0f0f0;padding:12px;border-radius:6px;overflow-x:auto;"><code>pip install staysift
from pysift import PySIFT
sift = PySIFT()
keypoints, descriptors = sift.detectAndCompute(gray_image)</code></pre>
</div>
"""
DEMO_NOTE = (
"This demo runs **OpenCV SIFT on CPU** for compatibility. "
"For GPU-accelerated matching, install `staysift` locally with a CUDA GPU."
)
def match_images(img1, img2, max_keypoints, ratio_thresh):
"""Detect, match, and visualize SIFT correspondences between two images."""
if img1 is None or img2 is None:
return None, "Please upload both images."
gray1 = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
gray2 = cv2.cvtColor(img2, cv2.COLOR_RGB2GRAY)
sift = cv2.SIFT_create(nfeatures=int(max_keypoints))
t0 = time.perf_counter()
kp1, d1 = sift.detectAndCompute(gray1, None)
kp2, d2 = sift.detectAndCompute(gray2, None)
t_detect = time.perf_counter() - t0
if d1 is None or d2 is None or len(kp1) < 2 or len(kp2) < 2:
return None, "Too few keypoints detected. Try different images."
t0 = time.perf_counter()
bf = cv2.BFMatcher(cv2.NORM_L2)
raw = bf.knnMatch(d1, d2, k=2)
matches = [m for m, n in raw if m.distance < ratio_thresh * n.distance]
t_match = time.perf_counter() - t0
matches_sorted = sorted(matches, key=lambda x: x.distance)
draw_count = min(len(matches_sorted), 100)
# Resize both images to same height for clean side-by-side visualization
target_h = min(img1.shape[0], img2.shape[0], 600)
scale1 = target_h / img1.shape[0]
scale2 = target_h / img2.shape[0]
r_img1 = cv2.resize(img1, None, fx=scale1, fy=scale1)
r_img2 = cv2.resize(img2, None, fx=scale2, fy=scale2)
# Rescale keypoints to match resized images
r_kp1 = [cv2.KeyPoint(k.pt[0]*scale1, k.pt[1]*scale1, k.size*scale1, k.angle, k.response, k.octave, k.class_id) for k in kp1]
r_kp2 = [cv2.KeyPoint(k.pt[0]*scale2, k.pt[1]*scale2, k.size*scale2, k.angle, k.response, k.octave, k.class_id) for k in kp2]
# Draw with bright green, thicker lines
vis = cv2.drawMatches(
r_img1, r_kp1, r_img2, r_kp2,
matches_sorted[:draw_count], None,
matchColor=(0, 255, 0),
singlePointColor=None,
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
# Redraw lines thicker for visibility
w1 = r_img1.shape[1]
for m in matches_sorted[:draw_count]:
pt1 = (int(r_kp1[m.queryIdx].pt[0]), int(r_kp1[m.queryIdx].pt[1]))
pt2 = (int(r_kp2[m.trainIdx].pt[0]) + w1, int(r_kp2[m.trainIdx].pt[1]))
cv2.line(vis, pt1, pt2, (0, 255, 0), 2, cv2.LINE_AA)
cv2.circle(vis, pt1, 4, (0, 255, 0), -1, cv2.LINE_AA)
cv2.circle(vis, pt2, 4, (0, 255, 0), -1, cv2.LINE_AA)
total = t_detect + t_match
stats = (
f"**Keypoints:** {len(kp1)} + {len(kp2)} = {len(kp1)+len(kp2)} \n"
f"**Matches:** {len(matches)} (showing top {draw_count}) \n"
f"**Detection:** {t_detect*1000:.0f}ms | **Matching:** {t_match*1000:.0f}ms | "
f"**Total:** {total*1000:.0f}ms \n"
f"*On GPU with PySIFT, expect ~3-4x faster detection and ~4x faster matching.*"
)
return vis, stats
with gr.Blocks(title="PySIFT: GPU-Resident Deterministic SIFT", theme=gr.themes.Soft()) as demo:
gr.HTML(HEADER_HTML)
gr.Markdown("---")
gr.Markdown("## Try It: Interactive SIFT Matching")
gr.Markdown(DEMO_NOTE)
with gr.Row():
img1 = gr.Image(label="Image A", type="numpy")
img2 = gr.Image(label="Image B", type="numpy")
with gr.Row():
max_kp = gr.Slider(500, 8000, value=4000, step=500, label="Max Keypoints")
ratio = gr.Slider(0.5, 0.95, value=0.75, step=0.05, label="Ratio Test Threshold")
match_btn = gr.Button("Match", variant="primary", size="lg")
output_img = gr.Image(label="Matches", type="numpy")
output_stats = gr.Markdown(label="Stats")
match_btn.click(
fn=match_images,
inputs=[img1, img2, max_kp, ratio],
outputs=[output_img, output_stats],
)
gr.Markdown("---")
gr.Markdown(
"*Built by [Sivakumar K S](https://github.com/SivaIITM) at IIT Madras. "
"Read the [paper](https://arxiv.org/abs/2605.17869) for full details.*"
)
if __name__ == "__main__":
demo.launch()