CropCatcher
retrieve the image source of any visual crop
CropCatcher is a Python package for determining whether two images show the same physical content.
It orchestrates classical OpenCV-based local feature matching methods such as SIFT, AKAZE and ORB, with descriptor matching, RANSAC and homography estimation for geometric verification and localization and exposes with a simple API.
CropCatcher is not a semantic similarity tool (not using visual embeddings): It is designed to identify the exact source image, rank candidates and, when relevant, locate the query inside the matching image.
Typical Cultural Heritage use case: you've found an old, low-resolution or cropped image somewhere like a screenshot, a thumbnail, a scan on a blog or in a PDF and you suspect it comes from a page held in a digital library such as BnF-Gallica. CropCatcher searches across one or more IIIF manifests, tells you which page it's actually contained in and where, and hands back that page's IIIF service. So, you can fetch the original at full resolution and read off its metadata (manuscript, shelfmark, folio) straight from the manifest.

A real query crop (left) matched with SIFT against real folios from the same manuscript scrapped from BnF-Gallica: the true source scores clearly highest (green) and is accepted, while other pages sharing the same script, style and parchment (red) score far lower and are rejected. See Benchmarks for how this holds up across methods and distortions.
Install
Requires
Python >= 3.13
use uv:
or pip:
Install (for development only)
git clone git@github.com:odil-ai/cropcatcher.git && cd IIIF_image_matcher/
uv sync # add --group notebook dev docs for the notebooks, lint tests, mkdocs etc.
Quickstart
from cropcatcher import Matcher
matcher = Matcher(method="sift") # or any feature extraction method: "akaze", "orb", "star_brief"
results = matcher.search(
query="my_query_image.jpg",
images="local_images_folder/", # a directory, a list of paths, or a single path
)
best = results.best
print(best.source, best.score, best.inliers, best.bbox)
Output: Each result is a MatchResult:
MatchResult(
source="image_142.jpg",
score=0.87,
matches=94,
inliers=61,
inlier_ratio=0.65,
bbox=(412, 580, 1034, 1280),
homography=...,
)
Against a digital library instead of local files:
results = matcher.search_iiif(
query="my_query_image.jpg",
manifests=["https://gallica.bnf.fr/iiif/ark:/12148/btv1b8427253m/manifest.json"],
)
How it works
flowchart TD
Q["Query image"] --> FD1["Feature detection & description"]
C["Candidate image"] --> FD2["Feature detection & description"]
FD1 --> DM["Descriptor matching + Lowe's ratio test"]
FD2 --> DM
DM --> RA["RANSAC homography"]
RA --> SC["Score + localization"]
- Feature detection & description: extracts local keypoints and descriptors with SIFT, AKAZE, ORB, or optional STAR+BRIEF / SURF.
- Descriptor matching: pairs similar descriptors using L2 or Hamming distance, then filters ambiguous matches with Lowe's ratio test.
mutual_check=Trueadds a bidirectional consistency check to reduce false positives. - RANSAC homography: uses RANSAC to keep geometrically consistent matches and estimate a homography between the query and candidate.
- Score + localization: combines inlier ratio and count into a confidence score, then projects the query into the candidate to estimate its bounding box.
SURF requires a custom OpenCV build (see above).
Where to go next
| If you want to… | See |
|---|---|
Match against local images, understand MatchResult |
Matching local images |
| Search IIIF manifests, tune cost | Searching IIIF |
| Reconcile a whole CSV of folios | Batch reconciliation |
| Know how well it actually works | Benchmarks |
Citation
If you use CropCatcher in your research, please cite it. Metadata also lives
in CITATION.cff at the repository root, which GitHub exposes through its
"Cite this repository" button.