Tile-Snap
Find where one image sits inside another — or how similar two images are — by cutting the source into small tiles, template-matching each into the target, and letting them vote on an offset.
One good small-tile hit is astronomically unlikely by chance (a 5×5 patch lives in a 256³-per-pixel space), so the real tiles out-vote every false positive. Same-source images lock to an exact x/y/scale; unrelated images return a similarity %. It's occlusion-robust (covered tiles just abstain), brightness/contrast-invariant (normalized cross-correlation), and needs no training.
Free tool tristate.digital/tool.html REST API https://api.tristate.digital/match
The API
Base URL https://api.tristate.digital. Two POST endpoints, JSON or multipart/form-data. CORS is open. Light rate limit (40 req/min/IP).
POST /match — one-shot result
Returns the full match as a single JSON object.
POST /stream — live progress
Streams newline-delimited JSON (chunked): one {"meta":…}, then one {"t":{tile}} per tile as it's scanned, then a final {"done":…, summary}. Used by the live tool UI.
Parameters
| field | type | required | default | meaning |
|---|---|---|---|---|
element | image | yes | — | the source image (base64 data-URL in JSON, or a file field in multipart) |
comp | image | yes | — | the target image to search within |
shape | string | no | square | square | circle | hex — the tile mask (circle/hex avoid corner bias) |
region | array | no | null | freeform polygon as [[fx,fy],…] fractions (0–1) of the element — match only inside it |
thresh | float | no | 0.60 | NCC match threshold, 0.30–0.99 (low = looser) |
tile | int | no | 18 | block size in px, 5–48 (smaller = finer + slower) |
detail | int | no | 5 | min unique tones per tile; flatter tiles are skipped so solid colours can't false-match |
Response
{
"tile": 18, "cols": 14, "rows": 22,
"el_w": 255, "el_h": 405, "comp_w": 1536, "comp_h": 1024,
"shape": "square", "region": false,
"x": 820, "y": 55, // best offset of the element in the comp
"match_pct": 100, // % of textured tiles that matched
"matched": 160, "textured": 160, "agree": 160,
"locked": true, // true => exact same-source registration
"tiles": [ {"r":0,"c":0,"m":true,"x":820,"y":55,"ncc":0.99,"u":214}, … ]
}
Per-tile flags: m matched, flat skipped (below detail), outside outside the region, ncc best correlation, u unique tones, x/y where it landed in the comp.
Examples
JSON (base64 data-URLs):
curl -s https://api.tristate.digital/match \
-H "Content-Type: application/json" \
-d '{"element":"data:image/png;base64,iVBOR…","comp":"data:image/png;base64,iVBOR…","shape":"circle","thresh":0.6}'
Multipart (raw files):
curl -s https://api.tristate.digital/match \
-F element=@face.jpg -F comp=@celebrity.jpg -F shape=hex -F thresh=0.55
JS / fetch:
const r = await fetch("https://api.tristate.digital/match", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ element: elDataURL, comp: compDataURL, shape: "square" })
});
const { x, y, match_pct, locked } = await r.json();
Errors & limits
400— bad request, or a rejected upload (only PNG/JPG/GIF/BMP/WEBP pass; anything that isn't a decodable image is refused before it touches disk).413— image over 8 MB or request over 20 MB.429— rate limit (40 req / 60 s / IP).500— match failed.
How it works
- Cut the element into small tiles (square/circle/hex mask).
- Skip flat tiles — a tile with fewer unique tones than the detail threshold is uninformative (a white block matches every white region) and is dropped.
- Match each remaining tile against the comp with normalized cross-correlation (
TM_CCOEFF_NORMED), which subtracts the mean — so a small brightness/contrast shift doesn't break the match. - Vote: each matched tile implies an offset
(tile.x − tile_col·T, tile.y − tile_row·T). Identical votes stack. The dominant offset is the placement; scattered votes mean no true correspondence (just similarity). - Lock: when enough tiles agree on one offset, it's an exact same-source registration and you get x/y/width/height.
Why it's robust: occluded tiles simply don't find a match and abstain — they don't poison the vote. And because one solid small-tile hit is ~1-in-trillions by chance, even a handful of agreeing tiles is conclusive (you don't need to count thousands of inliers).
The tool
tristate.digital/tool.html drives the same API with a live UI:
- Drop or browse any two images (element + comp).
- Freeform lasso a region on the element to match just that part.
- Pick the tile shape (square / circle / hex) and tune block size, threshold, detail.
- Watch the scan fill live — tiles turn green/red/grey/white and matched pixels light up on the target.
- Hover any tile for its score; the big number is your similarity % (and it locks when it's the same source).
Self-hosting
The API is a single Python file (snap_api.py) with one dependency: opencv-python-headless + numpy. Run it behind any reverse proxy:
pip install opencv-python-headless numpy
SNAP_API_PORT=8771 python3 snap_api.py
# POST to http://127.0.0.1:8771/match
It validates every upload (magic-byte sniff + cv2.imdecode), rate-limits per IP, and optionally saves validated images to a no-exec folder.