| FIMGDUPES(1) | User Commands | FIMGDUPES(1) |
fimgdupes - find visually similar images using perceptual hashing
fimgdupes [OPTIONS] PATH...
fimgdupes --exact [OPTIONS] PATH...
fimgdupes --query FILE [OPTIONS] PATH...
fimgdupes scans directories (or individual image files) and groups visually similar images together using perceptual hashing. It compares images by their visual content, not filenames or metadata, so it can detect duplicates even across different resolutions, crops, quality levels, and file formats.
Matching is two-stage. Each image is reduced to a perceptual hash and to a small grayscale thumbnail (EXIF orientation is applied first, so a photo stored rotated with an orientation tag matches its upright duplicate). The hash is only a fast gate that decides which pairs are worth scoring; the similarity percentage shown and compared against --threshold is the normalized correlation of the two thumbnails. That score is continuous and independent of brightness and contrast: the same picture resized, recompressed, converted or brightened scores 99–100, small crops and rotations the low-to-mid 90s, and unrelated photos well under 85. Every match in a group is directly similar to its group head — there is no transitive chaining (see GROUPING below).
Two further modes reuse the same filters, output and actions. --exact reports only byte-identical files (same SHA-256) and never decodes an image, so it is much faster on a first run; think fdupes(1) restricted to images. --query asks "is this picture already in here?": the named file is compared against the scanned paths, and only its matches are printed. Byte-identical copies are marked (identical file) in every mode, because a visual 100% can still be a different resolution or format while an identical file is safe to drop unseen.
Each group has a head (the "best" file by the sort criterion) and one or more matches. Actions like --move-matches, --delete-matches, and --trash-matches operate on the matches, leaving heads untouched.
Files the filters leave out (below --min-size, above --max-size, outside the dimension limits, image-like extensions not in --extensions, and directories pruned by the skip list or hidden-directory rule) are counted and reported on standard error as a "Not scanned:" line, so a duplicate that was never examined is not mistaken for one the hash failed to match. --verbose also lists the skipped directories. The comparison step reports the thumbnail size and hash gate in use.
Hashes are cached to disk so repeat scans are fast. Byte-identical files are detected up front and share a single decoded hash, so exact copies are grouped at 100% without redundant decoding.
Following grep(1) convention:
Options can also be set in a config file (see CONFIG FILE). CLI flags always override config file values. For boolean options set in the config, use the corresponding --no- flag to override them back to off.
Similarity is the zero-mean normalized cross-correlation of the two images' scoring thumbnails (see --hash-size), mapped to 0–100. Measured on real photographs at the default size:
In short: 99 finds only copies of the same picture; the default 95 also catches small rotations and some light edits; crops and heavier edits need --hash-size 8 or a threshold of 90. 100 requires byte-identical thumbnails, which resized copies rarely are, so use 99 for "same picture". See CHOOSING A THRESHOLD below for recipes. Percentages from other tools (image viewers with duplicate search) are on different scales and not comparable.
Images are reduced to an N×N grid for the hash and to a 2N×2N grayscale thumbnail for scoring (16×16, 32×32 or 64×64 pixels). Larger sizes compare finer visual detail — and fine detail is exactly what differs between near-duplicates, so the same pair of images scores lower at a larger size. Copies of the same picture score 99–100 at every size; what changes is how far below that edits fall (worst case over the test set, original versus variant):
| variant | size 8 | size 16 | size 32 |
| 0.5° rotation | 98.8 | 96.4 | 90.1 |
| 4% border crop | 98.5 | 93.6 | 84.9 |
| closest unrelated photo | 83.4 | 77.1 | 72.1 |
See CHOOSING A THRESHOLD for which size and threshold to combine.
At most one action flag may be given. Without an action flag, fimgdupes prints results and exits without modifying any files.
Using both {match} and {matches} in the same template is an error.
Paths are passed as separate process arguments, not through a shell. This prevents filename injection attacks — filenames containing shell metacharacters (semicolons, backticks, etc.) are treated as opaque strings. Requires --yes.
Examples:
--exec "feh {head} {matches}" # view group in feh
--exec "diff {head} {match}" # diff each match against head
--exec "cp {match} /backup/" # copy each match somewhere
fimgdupes reads default option values from a TOML config file at:
The XDG_CONFIG_HOME environment variable is respected. All fields are optional; only set what you want to override from the built-in defaults. CLI flags always take precedence over config values.
If the config file contains syntax errors, unknown keys (including typos), or out-of-range values, fimgdupes will print a specific error message and exit without running. This prevents accidental data loss from running with unexpected defaults. Use --ignore-config to bypass a broken config file.
Example config:
recursive = true long = true threshold = 95 hash = "phash" hash-size = 8 min-size = "100K" extensions = "jpg,jpeg,png,gif,webp,tiff,tif,bmp,avif" sort = "size" cache-format = "bin" cache-local = false no-cache = false
Available config keys:
| Key | Type | Built-in default |
| recursive | bool | false |
| long | bool | false |
| hidden | bool | false |
| color | bool | false (auto-detect) |
| no-color | bool | false |
| threshold | integer | 95 |
| hash | string | "phash" |
| hash-size | integer | 16 |
| min-size | string | "50K" |
| max-size | string | (none) |
| min-dimensions | string | (none) |
| max-dimensions | string | (none) |
| extensions | string | "jpg,jpeg,..." |
| sort | string | "size" |
| no-cache | bool | false |
| cache-local | bool | false |
| cache-format | string | "bin" |
| no-min-size | bool | false |
| verbose | bool | false |
| quiet | bool | false |
To override a boolean config value for a single invocation, use the corresponding --no- flag (e.g. --no-recursive overrides recursive = true). To ignore the entire config file, use --ignore-config.
Used by --min-size and --max-size. Suffixes are case-insensitive. All sizes use powers of 1024 (binary prefixes). A bare number without a suffix is an error.
| B | bytes |
| K, KB | kibibytes (1024 B) |
| M, MB | mebibytes (1024 KB) |
| G, GB | gibibytes (1024 MB) |
Decimal values are accepted (e.g. 1.5M).
Used by --min-dimensions and --max-dimensions. Format is WxH where W and H are non-negative integers representing pixel counts. The separator "x" is case-insensitive. An optional "px" suffix is accepted and ignored.
Either W or H (but not both) may be omitted to leave that axis unconstrained:
| 2560x1440 | both axes constrained |
| 2560x | width only |
| x1440 | height only |
When --recursive is used, the following directory names are skipped by default. Use --include-dir to re-include specific entries, or --no-skip-dirs to disable the list entirely.
thumbs, thumbnails, thumb, .thumbnails, .cache, cache, .trash, Trash, tmp, temp, .tmp, @eaDir, __MACOSX
Hidden directories (names beginning with ".") are also skipped unless --hidden is given.
Directories named explicitly on the command line are always scanned, even if their own name is hidden or on the skip list; the rules above apply only to directories discovered during traversal.
The similarity percentage is printed next to every match, so the practical method is: run once with the defaults, look at the scores of the pairs you do and do not consider duplicates, then tighten or loosen. Scores cluster: copies of the same picture sit at 99–100, light edits in the 90s, unrelated images far below. Start from whichever of these fits the job:
Lowering the threshold at the default size and switching to --hash-size 8 both admit more edits; size 8 does it by comparing a coarser thumbnail, so it keeps "same picture" pairs near 100 while pulling crops up with them, which usually reads more cleanly than a lower threshold at size 16. Put the setting you settle on in the config file (see CONFIG FILE).
If an expected duplicate is missing, check the "Not scanned:" line first: a small copy under --min-size (50K by default), a large original over a configured --max-size, or a .heic file was never compared at all.
fimgdupes --exact and fdupes(1) agree on byte-identical files; both skip symbolic links and second hard links, both pair empty files, and both separate same-size files that differ in content. Every difference comes from scope, and each one is announced on the "Not scanned:" line (except the first, which is the tool's purpose):
With --no-min-size --no-skip-dirs --hidden and all image extensions enabled, the two tools produce the same groups for image files; the script tests/exact-vs-fdupes.sh in the source tree checks this against a tree built to exercise every case above. In the default visual mode fimgdupes also reports resized, recompressed and converted copies, which no checksum tool can find.
Groups are formed using a greedy star model. Every match in a group is directly above the --threshold to the group head — there is no transitive chaining.
Images are sorted by the --sort criterion (e.g. largest first). The first unclaimed image becomes a group head and claims all unclaimed images that are directly similar enough to it. This repeats until all images are either a head or claimed.
A claimed image (match) can still have its own direct matches that were claimed by a different group. These secondary relationships are shown in the display output so you can see the full picture. For example:
a.jpg
└─ 91% b.jpg b.jpg
└─ 92% c.jpg
Here, b is a match of a (91%), and c is a match of b (92%), but a and c are not directly similar enough to be grouped together.
Display vs. actions: when no action is specified, all groups are shown including secondary ones. When an action flag is used (e.g. --move-matches), only groups whose head is not itself a match in another group are acted upon. In the example above, only b would be moved (as a match of a). c would not be moved because its head (b) is itself a match.
fimgdupes --recursive --move-ab ~/photos feh a/ b/
fimgdupes --recursive --fdupes ~/photos > dupes.txt fimgdupes --recursive --fdupes --omit-first ~/photos | tr '\n' '\0' | xargs -0 ls -l
fimgdupes --query ~/Downloads/new.jpg --recursive ~/photos fimgdupes --query new.jpg --quiet --recursive ~/photos && echo already have it
Each cache entry holds the hash, the scoring thumbnail (about 2 KB at hash size 16) and the SHA-256 of the file bytes. Entries written by versions before 0.2.0 carry no thumbnail or checksum and are recomputed automatically on first use. A --exact run on a file that was never fingerprinted stores a checksum-only entry, which a later normal run completes. With --cache-format json the checksums are plain text, so sha256sum output can be grepped against the cache without running fimgdupes at all.
Hash results are cached to ~/.cache/fimgdupes.bin by default. If a fimgdupes.bin (or fimgdupes.json, per --cache-format) already exists in the current directory, it is preferred automatically, so a directory once scanned with --cache-local keeps using its local cache. The cache is keyed by file path, size, and modification time, so changed files are automatically re-hashed. Use --no-cache to disable, --cache-local to store the cache in the current directory, or --cache-format json for a human-readable format.
fdupes(1), feh(1), xargs(1)
Written using Rust.
| 2026-10-23 | fimgdupes 0.2.0 |