VisualDedup 🔍✨
VisualDedup is a high-performance Python toolkit designed to find exact and visually similar images in large collections, cluster them into duplicate groups, explain why they match, recommend which copy to retain, and safely clean datasets without destructive surprises.
Existing libraries such as imagededup and Perception provide basic perceptual hashing. VisualDedup goes beyond basic hashing to solve the real operational challenges of large-scale dataset management:
- Explains Why Images Match: Classifies transformations (resizing, JPEG recompression, format conversion, and cropping).
- Sub-Quadratic Indexing: Uses BK-Trees and Multi-Index Hamming Hashing to eliminate the $O(N^2)$ pairwise comparison bottleneck, scaling to hundreds of thousands of images.
- Intelligent Keeper Recommendation: Recommends the optimal file to retain based on resolution, lossless format preservation, compression quality, and original timestamps.
- Safe Dataset Cleanup: Non-destructive by default with strict dry-run modes, quarantine isolation, and full JSON audit logs.
- Interactive Visual Reports: Generates standalone HTML dashboards and image contact sheets with side-by-side previews, difference metrics, and keeper badges.
- Incremental SQLite Cache: Avoids recomputing hashes on subsequent scans by tracking file modification times and byte sizes.
⚡ Quickstart
1. Installation
# Clone the repository
git clone https://github.com/your-org/visualdedup.git
cd visualdedup
# Create virtual environment and install
python -m venv .venv
# Windows:
.venv\Scripts\activate
# Linux/macOS:
source .venv/bin/activate
pip install -e ".[dev]"
💻 Command-Line Interface (CLI)
Scan a Folder and Generate Reports
# Scan with pHash (DCT) and export JSON + interactive HTML reports
visualdedup scan ./photos --output report.json --html report.html
# Scan with custom Hamming threshold and contact sheets
visualdedup scan ./dataset --hash phash --threshold 8 --contact-sheet sheet.png
# Fast candidate indexing with Multi-Index Hashing for large collections
visualdedup scan ./large_collection --index multi_index --keeper best_quality
# Output structured tabular CSV for data pipelines / Excel
visualdedup scan ./dataset --csv report.csv
# Output pure pipeable JSON directly to stdout
visualdedup scan ./dataset --json | jq .
# Store duplicate images in a separate folder organized by cluster subfolders
visualdedup scan ./dataset --store-duplicates ./duplicates_output
# Move duplicate discards into a separate folder (cleaning the source directory)
visualdedup scan ./dataset --move-duplicates ./quarantine_folder
Inspect Two Images Side-by-Side
# Terminal table view:
visualdedup inspect original.png compressed_downscaled.jpg
# Structured JSON view for scripts and CI:
visualdedup inspect original.png compressed_downscaled.jpg --json
Outputs a detailed comparison table with SHA-256, dHash, pHash, aHash, wHash distances, resolution differences, and an automated diagnosis (e.g. Resized copy: downscaled ~2.0x; JPEG recompression).
Safe Dataset Cleanup
# Dry run simulation (default: does not touch files)
visualdedup clean ./dataset --mode quarantine --dry-run
# Execute quarantine move with confirmation
visualdedup clean ./dataset --mode quarantine --target-dir ./quarantine --confirm
🐍 Python Library API
from visualdedup import VisualDedup, KeeperStrategy, CleanMode
# 1. Initialize engine with incremental caching and sub-quadratic indexing
vd = VisualDedup(
cache_path=".visualdedup_cache.db",
hash_type="phash", # "phash", "dhash", "ahash", "whash", "exact"
threshold=8, # Max Hamming distance
index_type="bktree", # "bktree" or "multi_index"
)
# 2. Scan folders
clusters, summary = vd.scan(["./dataset"], recursive=True)
print(f"Scanned {summary.total_scanned} files.")
print(f"Found {summary.total_clusters} duplicate clusters.")
print(f"Reclaimable space: {summary.reclaimable_bytes / (1024 * 1024):.2f} MB")
# 3. Inspect keeper recommendations
for cluster in clusters:
print(f"\nCluster {cluster.cluster_id} ({cluster.match_category}):")
print(f" Keeper: {cluster.keeper_decision.keeper_path}")
print(f" Reason: {cluster.keeper_decision.reason}")
print(f" Discards: {cluster.keeper_decision.discard_paths}")
# 4. Export visual reports
vd.export_html(clusters, summary, "report.html")
vd.export_json(clusters, summary, "report.json")
# 5. Execute safe cleanup (e.g. quarantine mode with dry run)
clean_result = vd.clean(
clusters,
mode=CleanMode.QUARANTINE,
target_dir="./quarantine_folder",
dry_run=True,
audit_log_path="audit.json"
)
🚀 Performance & Sub-Quadratic Indexing
Naive deduplication compares every image pair ($O(N^2)$ comparisons). For 5,000 images, that requires 12.5 million comparisons.
VisualDedup implements metric BK-Trees and Multi-Index Hamming Hashing (Pigeonhole block search):
| Dataset Size | Pairwise Checks $N(N-1)/2$ | Naive Linear $O(N^2)$ | BK-Tree $O(N \log N)$ | Multi-Index Hash | Speedup |
|---|---|---|---|---|---|
| 500 | 124,750 | 31 ms | 38 ms | 4 ms | 7.8x |
| 1,000 | 499,500 | 116 ms | 124 ms | 8 ms | 14.5x |
| 2,500 | 3,123,750 | 697 ms | 619 ms | 20 ms | 34.8x |
| 5,000 | 12,497,500 | 3,000 ms | 2,088 ms | 59 ms | 50.5x |
Run the benchmarks yourself:
python benchmarks/benchmark_indexing.py
🛡️ Keeper Selection Strategies
| Strategy | Criteria | Best For |
|---|---|---|
best_quality (default) |
Resolution > Lossless format (PNG > JPEG) > File size > Original timestamp | General dataset cleaning |
highest_resolution |
Maximizes pixel dimensions ($W \times H$) | Upscaled/downscaled collections |
smallest_file |
Minimizes storage space | Bandwidth / disk-constrained archives |
largest_file |
Preserves least compressed files | Photography & print workflows |
oldest |
Retains earliest file by modification date | Digital asset origin preservation |
newest |
Retains most recently modified file | Continuous syncing backups |
🧪 Testing
pytest -v
All 22 unit and integration tests validate hashing invariance, tree search pruning, cluster disjoint sets, transformation explanations, safe deletion guards, and report exports.
📄 License
VisualDedup is distributed under the MIT License.
Metadata
Release files for visualdedup 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| visualdedup-0.2.0.tar.gz | 41.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| visualdedup-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 81.6 kB
Release files / visualdedup-0.2.0.tar.gz
| Download URL | visualdedup-0.2.0.tar.gz |
|---|---|
| Size | 41.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8731eb6a33cd0d3cb1f17d6e9ea306e94c33fb6c831e3adae8a7067580f8eff3
|
|
BLAKE2b-256 checksum How to use checksums |
137670731b62dff9320c2ed4033f417382438e61d86b2d7ab42756316a6ec043
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / visualdedup-0.2.0-py3-none-any.whl
| Download URL | visualdedup-0.2.0-py3-none-any.whl |
|---|---|
| Size | 40.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
edfe609b3d67fe0d09c6fd98bf45185b8795ba3fee5d19e9c342bfcfe18386a5
|
|
BLAKE2b-256 checksum How to use checksums |
c2e992d29714f797751fdbd939956eee1dca7ca3fd37069af0441d67e9213d79
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for visualdedup 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.