| """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) |
|
|
| |
| 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) |
|
|
| |
| 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] |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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() |
|
|