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.
Architecture fit
The package is a library, not a service — it fits cleanly behind a single application-owned interface (ImageSimilarity with hash(string $path): string and distance(string $a, string $b): int). That interface is the entire architectural decision a TPM needs to sponsor: it keeps the perceptual-similarity primitive in-process today while preserving a one-line container-binding swap to a managed vision service or embedding store later. The library supplies the algorithm; the product team supplies two things it deliberately omits — threshold policy ("what counts as the same image") and a storage plus lookup strategy (a hash column, a duplicate-index scheme, and a backfill path).
Scope must be stated explicitly at kickoff. This solves perceptual similarity — resized, re-compressed, or lightly modified copies of the same image. It does not solve classification, OCR, face matching, NSFW verdicts, semantic search, or rotation invariance. If a stakeholder's mental model of "image AI" includes any of those, the gap gets named in the kickoff deck or it will quietly expand mid-sprint.
Integration feasibility
Low. composer require, no external accounts, no credentials, no data leaving the application, MIT license that clears legal and security review in days rather than a procurement cycle. The real prerequisite is not the package but the image driver: GD or Imagick must exist in every runtime that will hash — web FPM, queue workers/Horizon, Octane containers, and CI. That is an infrastructure checklist item, not a code item, and it belongs in the platform team's definition of done before the first sprint starts.
Technical risk
Ranked by likely impact:
intervention/image can shift stored hashes, making old and new rows incomparable with no error raised. Cheap insurance is a hash_algorithm and hash_version column pair so drift becomes a detectable backfill rather than a mystery bug report.intervention/image, not imagehash — imagehash wraps it, so its major-version bumps carry the real compatibility risk against the rest of the stack.Hasher interface is the seam that makes that survivable.Key questions for the kickoff
Stack fit
Native to Laravel/PHP. The library is framework-agnostic, so the integration work is ordinary Laravel plumbing: a service-provider singleton, a config file holding algorithm and thresholds, a queued job, an Eloquent observer, and one migration.
Migration path
Additive and non-breaking. Three nullable columns on the media/image table — image_hash (hex, indexed), hash_algorithm, and hash_version — plus, if needed later, a small banding table. Store hex, never raw bits: binary through JSON serialization or a UTF-8 column corrupts silently, and hex sidesteps PHP integer signedness issues on 32-bit builds. Column width must match the algorithm (BlockHash is 256-bit and will truncate in a char(16) field) — which is exactly why the algorithm is recorded per row.
Compatibility
Two compatibility axes to verify in the spike, not in the sprint: (a) what Composer resolves for intervention/image against the existing stack, and (b) driver parity — GD and Imagick decode edge-case files slightly differently and can shift a hash by a bit or two. The decision is to pick one driver and pin it everywhere — Dockerfile and composer.json config.platform alike — so hashes are comparable across environments.
Sequencing (the part a TPM actually owns)
Ship the capability dark, then earn the right to act on it:
ShouldBeUnique queued job dispatched from an observer — hashing stays off the request path.chunkById and batched, throttled dispatch, run off-peak with queue depth watched. Fail open on missing files.images:duplicates report command dumping candidate pairs with distances.That sequence converts threshold tuning from a guess into an evidence-based product decision, and it gives you a rollback story if the false-positive rate surprises you.
Maintenance
Low steady-state, with two named owners: platform owns the image driver and dependency pinning; product owns threshold policy and the review queue. The genuine maintenance hazard is drift — a GD/Imagick/intervention/image bump silently invalidates historical hashes. The hash_version column is the operational control: bump it, re-hash in the background, compare only within a version.
Support
Support volume should be near zero if the feature is advisory-first. It spikes the moment automated merging ships without an override — the support ticket is "your system deleted/merged my wrong image," and the only durable answer is the audit record of which pair and which threshold triggered it. Test assets belong as fixture files (original, resized, re-encoded, screenshot, solid colour, two genuinely different photos) with assertions on distance ranges, not exact hashes, so a dependency bump produces a readable test failure rather than a mystery.
Scaling
Hashing is CPU-bound but cheap — tens of milliseconds per image — and inherits the existing queue workers, so there is no new infrastructure to capacity-plan. The scaling cliff is the lookup, not the hash: 64-bit Hamming distance has no B-tree story, so past roughly 10k assets the naive full-scan comparison becomes the bottleneck. The documented escape hatch is banding — split the hex into four chunks in a side table, fetch candidates sharing at least one band, then compute exact distance over that reduced set. Past that, the interface seam is what lets you graduate to a vector store without a rewrite.
Failure modes
Class "Imagick" not found on the worker — works locally, dies in production; caught by verifying the extension in every container before sprint one.null; the dedupe check must never be the reason an upload fails.hash_version.Ramp-up
One sprint for a spike plus the queue job, per the build-vs-buy analysis — this is a well-understood primitive, not an ML research problem. The sprint's real output is not the code (which is small) but the tuning artifact: a few hundred real assets from the actual corpus, a distance histogram, a hand-reviewed sample, and a written recommendation for two threshold numbers. That artifact is what converts "someday, needs an ML team" into a shipped feature, and it frees ML capacity for genuinely hard problems. Budget the review time explicitly — it is product-ops hours, not engineering hours, and it is the step most likely to be quietly skipped under delivery pressure.
How can I help you explore Laravel packages today?