A command-line tool that detects visually similar or duplicate images in a folder using perceptual hashing (PDQ). Unlike byte-level comparison, perceptual hashing finds images that look the same even if they differ in compression, resolution, or minor edits.
A regular checksum (MD5, SHA-256) only tells you if two files are byte-identical. Resize an image by one pixel, re-save it as a different quality JPEG, or strip its EXIF data, and the checksum changes completely even though the picture looks the same to a human.
Perceptual hashing solves this by hashing what the image looks like rather than its raw bytes. TwinFrame uses PDQ, which produces a 256-bit fingerprint that stays stable across resizing, re-encoding, and minor edits. Two images with a small Hamming distance between their hashes are very likely the same picture.
To find matches quickly across large collections, hashes are indexed in a BK-tree, which supports fast approximate nearest-neighbor lookups under a distance metric instead of requiring an exhaustive pairwise comparison.
- Recursively scans a folder and automatically detects image files by content, not extension
- Computes a PDQ perceptual hash for every image
- Groups likely duplicates using a tolerance-based Hamming distance search
- Prints a clear terminal report showing which file to keep and which are recoverable space
- Optional HTML report with a visual side-by-side of each duplicate group
- Fast: BK-tree indexing avoids comparing every image against every other image
Clone the repository and build with Cargo:
git clone https://github.com/mathieuemery/twinframe.git
cd twinframe
cargo build --releaseThe compiled binary will be at target/release/twinframe.
twinframe --folder-path <PATH> [--report] [--tolerance]Tolerance is the maximum Hamming distance between two hashes for them to be considered duplicates. Lower values mean stricter matching (fewer false positives, but you might miss heavily edited duplicates); higher values catch more variations at the risk of grouping unrelated images. The default is 15, which works well for resized or re-compressed copies of the same image.
Running with --report writes a report.html file to the current directory. It includes:
- A summary of total images scanned and potential space savings.
- Collapsible duplicate groups with image previews, dimensions, file size, and creation date.
- KEEP / DELETE labels, with the largest file in each group suggested as the one to keep.
- A "Copy path" button per file for quick access.
- Dark mode support.
Note: Image previews in the HTML report use absolute filesystem paths and will only render correctly when the report is opened locally on the same machine.
| Flag | Short | Description |
|---|---|---|
--folder-path |
-f |
Path to the folder to analyze (required) |
--report |
-r |
Generate an HTML report (report.html in the current directory) |
--tolerance |
-t |
Define the tolerance to use [default: 15] |
--help |
-h |
Show help |
--version |
-V |
Show version |
Both relative and absolute paths are accepted. The path is resolved to its absolute form before scanning, so all reported file paths are always fully qualified.
======================================================================
DUPLICATE DETECTION REPORT
======================================================================
GROUP #1: (Hash: e3b3cb37...)
[KEEP] -> "/home/user/Pictures/test.png" (282.54 KB)
[DELETE] -> "/home/user/Pictures/test copy.png" (282.54 KB)
[DELETE] -> "/home/user/Pictures/test_compressed.png" (65.15 KB)
----------------------------------------------------------------------
▶ Est. Recoverable Space: 0.34 MB
======================================================================
Within each group, the largest file is kept as the reference copy and the rest are flagged for deletion, TwinFrame never deletes anything itself, it only reports.
- Walk the folder:
walkdirrecursively traverses the directory, and each file is opened and checked to confirm it's actually a decodable image, regardless of its extension. - Hash each image: every image is passed through PDQ, producing a 256-bit hash that captures its visual structure.
- Index the hashes: hashes are inserted into a BK-tree, a structure built for fast lookups under a distance metric.
- Find matches: for each hash, TwinFrame queries the tree for every other hash within the configured tolerance, then groups those files together.
- Report: groups are sorted by file size, with the largest file suggested as the one to keep, and a terminal (and optionally HTML) report is generated.
- Single-threaded analysis: large folders with thousands of images will be slow. Parallel hashing via
rayonis a planned improvement. - HTML report output path: the report is always written to
report.htmlin the current working directory. A--outputflag is planned.
| Crate | Purpose |
|---|---|
pdqhash |
Perceptual hash generation |
bk-tree |
Nearest-neighbor search by Hamming distance |
image |
Image decoding |
walkdir |
Recursive directory traversal |
clap |
CLI argument parsing |
askama |
HTML report templating |
chrono |
Timestamp formatting |
anyhow |
Error handling |
hex |
Hash display |
MIT
Issues and pull requests are welcome. If you run into an image that isn't detected correctly, please include the file and its format in your report.
