imgtrail
Find out where else on the web your own photos show up.
Point it at your Instagram data export. It hashes every photo, collapses the near-duplicates so you never pay to search the same picture twice, runs each unique one through reverse image search, and then downloads every candidate and compares it against your original before putting it in the report. What you get back is a list you can trust, not a pile of URLs.
imgtrail scan ~/Downloads/instagram-export.zip --dry-run
imgtrail scan ~/Downloads/instagram-export.zip
imgtrail report --open
What it finds, and what it doesn't
It searches Google's index, so it finds your photos on blogs, news sites, Pinterest, Tumblr, forums, scraper mirrors and shops that lifted your pictures.
It will not find a repost on another Instagram account. Instagram blocks crawling of post images, so they aren't in anyone's index — the only way such a repost surfaces here is indirectly, via one of the many "Instagram viewer" mirror sites that are indexed. Telegram, WhatsApp, TikTok, Facebook and private accounts are invisible to it too. If your question is "is someone reposting me inside Instagram", this is the wrong tool and there isn't a good one.
Install
pip install imgtrail
Getting your photos
Instagram → Settings → Accounts Centre → Your information and permissions → Download your
information. Ask for JSON, high quality. You'll get a ZIP; hand it straight to imgtrail scan.
No scraping, nothing against the terms of service, no rate limits.
A plain folder of images works just as well.
Getting an API key
imgtrail uses Google Cloud Vision's WEB_DETECTION. Create a project at
console.cloud.google.com, enable the Cloud Vision API,
then Credentials → Create credentials → API key.
export IMGTRAIL_API_KEY=AIza...
The first 1,000 images each month are free, then $3.50 per 1,000. A typical profile costs
nothing. Run --dry-run first and it will tell you exactly how many searches it would make and
what they would cost before spending anything.
How the verification works
Reverse image search returns a lot of near-misses. For every candidate, imgtrail downloads the image and compares perceptual hashes against your original:
| Hamming distance | Verdict | Meaning |
|---|---|---|
| ≤ 8 | confirmed |
The same image, possibly recompressed |
| ≤ 16 | likely |
Cropped, filtered or heavily edited |
| > 16 | rejected |
Not your photo |
Only confirmed and likely reach the report. visuallySimilarImages is dropped entirely —
it means "semantically alike", not "this is your photo", and it drowns the report in noise.
Commands
imgtrail scan SOURCE index, dedupe, search and verify — resumable
--dry-run count the searches and their cost, call nothing
--limit N search at most N unique photos
--threshold N pHash distance for "same photo" (default 6)
--ignore-domain DOMAIN exclude a domain from results (repeatable)
--no-verify skip the download-and-compare pass
imgtrail report --open build the HTML report and open it
imgtrail status what's in the database so far
State lives in ./imgtrail-data. Everything is idempotent: re-running scan searches only
what it hasn't searched before, so an interrupted run costs nothing to resume.
Privacy
Your photos are sent to Google Cloud Vision, and nowhere else. Nothing is uploaded to any server of mine — there isn't one. The database, the extracted export and the report all stay on your machine.
Architecture
Ports and adapters, sized to the problem: the rules sit in the middle and know nothing about Google, SQLite or HTTP, so swapping a search backend touches exactly one file.
domain.py fingerprints, grouping, verdicts, what counts as "your own platform"
— pure; no I/O, no SQL, no network
ports.py the boundaries: PhotoSource, ImageLoader, SearchEngine, ImageFetcher,
PhotoRepository, MatchRepository, ReportWriter
services.py the use cases: index, plan, search, verify, report
adapters/ the details: sqlite_repository, vision, http_fetcher, local_files, html_report
cli.py the composition root — the one module that knows every layer
Adding TinEye or Yandex means writing one SearchEngine and wiring it in cli.py. Nothing
in domain.py or services.py changes.
Development
uv sync --all-groups
uv run pytest # 85 tests, no network, no mocks
uv run ruff check .
uv run ruff format .
uv run mypy # strict, and it passes on the tests too
The test doubles are real implementations, not mocks: an in-memory DictPhotoSource, a
FakeSearchEngine that records what it was asked, and — where the wire itself is what needs
testing — a real local HTTP server speaking Vision's JSON.
Licence
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file imgtrail-0.1.2.tar.gz.
File metadata
- Download URL: imgtrail-0.1.2.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6d3b2eb9b894d6d148ed4513990e047fa0af39a579cca1342b8648f4a096944
|
|
| MD5 |
1047da14e017b813b1b103dda77d987b
|
|
| BLAKE2b-256 |
44592e3ec5c2c91d07a6dd0d2405771cd814d72e173081c491b1cc0101e1e017
|
Provenance
The following attestation bundles were made for imgtrail-0.1.2.tar.gz:
Publisher:
release.yml on Endika/imgtrail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
imgtrail-0.1.2.tar.gz -
Subject digest:
a6d3b2eb9b894d6d148ed4513990e047fa0af39a579cca1342b8648f4a096944 - Sigstore transparency entry: 2617813443
- Sigstore integration time:
-
Permalink:
Endika/imgtrail@9af2f1d3c597a8a9ba49abf9dd81672f30136c2d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Endika
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9af2f1d3c597a8a9ba49abf9dd81672f30136c2d -
Trigger Event:
push
-
Statement type:
File details
Details for the file imgtrail-0.1.2-py3-none-any.whl.
File metadata
- Download URL: imgtrail-0.1.2-py3-none-any.whl
- Upload date:
- Size: 23.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e0951d20ae41b6d68843837cae0183ebbd92a5a38c73479baaf8faf018a101d
|
|
| MD5 |
23c8ae8f38b1a8c6c400525fde8797ef
|
|
| BLAKE2b-256 |
b0405e4e54985cae4afde063e0c85745970c1ba4e2d716f51ffb751def7d0c65
|
Provenance
The following attestation bundles were made for imgtrail-0.1.2-py3-none-any.whl:
Publisher:
release.yml on Endika/imgtrail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
imgtrail-0.1.2-py3-none-any.whl -
Subject digest:
8e0951d20ae41b6d68843837cae0183ebbd92a5a38c73479baaf8faf018a101d - Sigstore transparency entry: 2617813454
- Sigstore integration time:
-
Permalink:
Endika/imgtrail@9af2f1d3c597a8a9ba49abf9dd81672f30136c2d -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Endika
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9af2f1d3c597a8a9ba49abf9dd81672f30136c2d -
Trigger Event:
push
-
Statement type: