jenssegers/imagehash
Perceptual image hashing for PHP and Laravel. Generate hashes to detect duplicate images and find visually similar pictures using aHash, dHash, pHash and more. Works with Intervention Image in any PHP project. Fast, lightweight, and great for deduplication and content moderation.
A feature you can ship next sprint instead of next quarter. Perceptual hashing turns "duplicate image detection" — a line item that usually gets parked behind "we need an ML team" — into a queue job and a column. The package (jenssegers/imagehash, MIT, ~2.1k stars, actively released) is pure PHP, installs via Composer, and runs in-process: no Python service, no vector database, no cloud vision contract, no vendor DPA, no per-image API spend, no user media leaving the app.
ImageSimilarity interface in front of it (hash → string, distance → int). Swapping to a managed service later becomes a container binding change, not a rewrite. That is the single most defensible reason to approve it.The decision this package hands back to you — deliberately. The library returns a distance number; the cutoffs are product policy. Commit explicitly to two tiers in config: "duplicate" (merge, block, or badge) and "similar" (nudge or suggest). Thresholds are domain-specific — product photography, screenshots, and scanned documents are three different numbers — so tuning is a product-policy exercise with a real spike (a few hundred real assets, a distance histogram, a hand-reviewed sample), not a library setting you copy from a blog post.
Explicitly out of scope — say it in the kickoff or scope will creep: object detection, face recognition, OCR, captioning, NSFW classification, semantic search, and rotation invariance. If a stakeholder's mental model of "image AI" includes any of those, name the gap on day one.
Sequencing that keeps you out of trouble: schema → backfill command → offline images:duplicates report → hand-review the report → advisory badge in UI → feature-flagged merge with manual override. Ship dedupe dark first. Systematic false positives (solid colours, gradients, heavy letterboxing, watermarks) are the real UX risk, and you want the audit trail of which pair triggered a merge before anyone can trigger one.
Success metrics worth defining up front: duplicate groups surfaced per 1,000 uploads, review-queue precision from the hand-reviewed sample, storage reclaimed from consolidation, and the observed distance distribution over the first few weeks of production.
Adopt when:
Class "Imagick" not found on the worker is the classic day-one failure. Pick one driver and pin it everywhere (Dockerfile + composer.json config.platform), because GD and Imagick decode edge cases slightly differently and can shift a hash by a bit or two.hash_version).Look elsewhere when:
Two operational commitments that come with adoption — treat these as part of the cost, not optional polish: (1) fail open on decode errors (corrupt files, CMYK JPEGs, animated GIFs, HEIC, 0-byte uploads) so the similarity check is never the reason an upload fails; (2) version your hashes, because a GD/Imagick/intervention/image bump can silently shift hashes and make old and new rows incomparable. hash_version is how you detect that and turn a data-loss event into a backfill.
One dependency to watch: the package wraps intervention/image, so its major-version bumps are your real upgrade risk, not this package's small public API. Pin the imagehash version explicitly.
One line: We can ship duplicate and near-duplicate image detection next sprint using an MIT-licensed PHP library that runs inside our existing app — no new service, no vendor contract, no per-image fees, and no user media leaving our infrastructure.
For executives. This replaces a "needs an ML team" roadmap item with a one-sprint spike plus a queue job, and frees ML capacity for genuinely hard problems rather than a well-understood primitive. Costs: an engineer-sprint to spike and integrate, plus one for the backfill and review tooling. There's no procurement cycle, no recurring API spend, and no data-processing agreement to negotiate — it's a Composer install. The measurable returns are concrete: storage reclaimed from consolidating redundant assets, manual cleanup toil removed from content ops, and a fraud signal (recycled listing photos) routed to human review instead of paying out on bad listings. The risk is bounded and reversible: an internal interface keeps the similarity primitive swappable for a managed service later, so the exit cost is a container binding, not a rewrite.
For engineering. It's a small, actively maintained, MIT library with a tiny public API: hash(), distance(), compare(), plus four algorithms — DifferenceHash (default, 64-bit), PerceptualHash (more robust to re-encoding/resizing, slower), AverageHash (fastest, least accurate), BlockHash (256-bit). Start with dHash; move to pHash only if the spike shows re-encoded or resized copies slipping past threshold. Bind it once behind our own ImageSimilarity interface — never instantiate it in a controller — so the algorithm choice lives in one place. Store hex, not bits, in a char(16) column (or char(64) for BlockHash — the wrong size truncates silently), alongside hash_algorithm and hash_version. Hash in a queued, idempotent, ShouldBeUnique job off the request path. Never call compare() in production — it decodes both files every time; store hashes and use distance(). Handle S3 by streaming to a temp file or building an in-memory image. Past ~10k assets, add a banded index as a cheap prefilter. Test against 8–10 fixture files (original, resized, re-encoded, screenshot, solid colour, two genuinely different photos) and assert distance ranges, not exact hashes — exact-hash assertions break on a GD bump. And don't hash user-supplied URLs; "paste an image link" is an SSRF vector.
What we are explicitly not promising. This is not image classification, moderation verdicts, OCR, face recognition, or semantic search, and it is not rotation-invariant. It tells us two images look alike. Everything about what to do when they do — thresholds, merge rules, review routing, overrides — is a product decision we own, which is precisely why it should be explicit in config and revisited with real data rather than buried in a magic number.
How can I help you explore Laravel packages today?