TriState Developers

Open the Tile-Snap tool →

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

fieldtyperequireddefaultmeaning
elementimageyesthe source image (base64 data-URL in JSON, or a file field in multipart)
compimageyesthe target image to search within
shapestringnosquaresquare | circle | hex — the tile mask (circle/hex avoid corner bias)
regionarraynonullfreeform polygon as [[fx,fy],…] fractions (0–1) of the element — match only inside it
threshfloatno0.60NCC match threshold, 0.30–0.99 (low = looser)
tileintno18block size in px, 5–48 (smaller = finer + slower)
detailintno5min 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

  1. Cut the element into small tiles (square/circle/hex mask).
  2. 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.
  3. 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.
  4. 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).
  5. 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.