ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-07-07Advanced

The Thumbnails Had Been Drifting: Catching Image-Asset Regressions with pHash and SSIM in Your Agent Workflow

After letting an agent rewrite an image pipeline, visual quality can quietly slip in ways tests never catch. This guide shows how to measure that drift with two numbers — pHash and SSIM — and block it in CI, from threshold calibration to a ready-to-run gate script and Antigravity integration.

antigravity420image-regressionphashssimci-cd15premium16

Premium Article

import RelatedArticles from "@/components/RelatedArticles";

The thumbnails in my wallpaper app had gone slightly sleepy without my noticing.

It started when I asked an Antigravity agent to make thumbnail generation a little faster. It swapped the Pillow resampling filter, tuned the JPEG quality parameter, and shaved about thirty percent off the processing time. The diff looked reasonable. The tests passed. I merged it.

A few days later, glancing at the store's "new arrivals" grid, I noticed faint banding along the edges of a gradient. No user complained. But it was undeniably coarser than before, and the culprit was that resampling change. Nothing was broken — only the look had quietly regressed. That kind of regression never trips an assert.

This piece is about turning that visual regression into a single number and stopping the agent's change at the CI gate. Using two rulers — pHash and SSIM — and building on my own day-to-day practice, we'll assemble a way to catch image-asset regressions mechanically.

Why exact-match and eyeballing both fail

The obvious first idea is to hash the output and compare for an exact match. It breaks down immediately. A single JPEG encoder version bump changes the byte stream; so do EXIF timestamps, an embedded color profile, or a slightly different quantization table — all of them produce "looks identical, bytes differ" situations. An md5 gate goes red on meaningless diffs until nobody looks at it anymore.

Eyeballing doesn't last either. Dozens of thumbnails might be manageable, but once you include resolution variants you're at hundreds. The human eye notices a 5% degradation when two images sit side by side, yet misses 15% when shown one in isolation — and the focus required to line them all up every release doesn't survive two weeks.

What you need is a ruler for perceptual closeness — something closer to "does a person see these as the same" than to byte equality. That's where pHash (perceptual hash) and SSIM (structural similarity) come in.

MethodCatchesMissesGood for
Byte match (md5)Exact identityFlags visually identical files as differentCache keys, dedup
pHashBig changes in composition and tonal layoutLocal noise, banding"Turned into a different image"
SSIMLocal texture and gradient degradationWeak against global shifts"Same composition, lower quality"

As the table shows, pHash and SSIM are strong in different places. I ended up using both, because either one alone would have missed the banding that first tripped me up.

Turning the drift into one number with pHash

pHash shrinks an image to a small grayscale version and builds a 64-bit fingerprint from the low-frequency components of a discrete cosine transform (DCT). If two images are perceptually close, their fingerprint bits mostly agree, and the size of the difference is the Hamming distance — how many bits differ.

In Python it's a few lines with imagehash and Pillow.

from PIL import Image
import imagehash
 
def phash_distance(path_a: str, path_b: str) -> int:
    """Hamming distance between the perceptual hashes of two images. 0 = near-identical."""
    hash_a = imagehash.phash(Image.open(path_a))
    hash_b = imagehash.phash(Image.open(path_b))
    return hash_a - hash_b  # subtracting two ImageHash objects gives the Hamming distance
 
if __name__ == "__main__":
    d = phash_distance("baseline/thumb_001.jpg", "candidate/thumb_001.jpg")
    print(f"pHash distance = {d}")

The ImageHash object returned by imagehash.phash becomes a Hamming distance directly through the - operator. That's the pleasant part of imagehash — you never count bits yourself.

pHash is strong at "turned into a different image" because the low-frequency DCT components describe the overall composition and the layout of light and dark. Swap the subject of a thumbnail or shift the crop, and those components move a lot, so the Hamming distance jumps. A slight change in compression ratio, on the other hand, barely moves them — which is exactly why it doesn't go red on meaningless diffs.

Measuring "re-encode with no change" across 300 real wallpaper thumbnails, the distances stayed at roughly 0–2. On test images where I swapped the subject, they opened up to 18–30. That gap is the room you have to draw a threshold.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You can detect the quiet visual drift that follows an agent's change to your image pipeline using a single number — the Hamming distance of a perceptual hash
You'll take home a practical pattern for the middle ground between exact-match (too strict) and eyeballing (doesn't scale), using SSIM and calibrated thresholds
You get a Python gate script and a GitHub Actions snippet you can run tomorrow against your own generated thumbnails and OGP images
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Integrations2026-06-17
Feeding axe-core Findings to an Antigravity Agent: An Accessibility Fix Loop in CI
Detect accessibility violations with axe-core, hand them to an Antigravity agent as fixable tasks, and wire detection, triage, fixing, and re-verification into a single CI loop. Includes a diff gate that blocks only new violations and a scoping design that keeps the agent from breaking screen readers.
Integrations2026-05-01
Antigravity × Lighthouse CI: Catching Web Performance Regressions Automatically— Budgets, PR Comments, and Progressive Blocking
Wire Antigravity's AI to Lighthouse CI inside GitHub Actions and stop performance regressions before they reach production. This guide covers budget design, PR comments, progressive blocking, RUM integration, and cost controls — all in a shape that holds up in real teams.
Integrations2026-03-27
Automate GitHub Pull Request Reviews with Antigravity: A Practical Guide
Learn how to automate GitHub Pull Request code reviews using Antigravity's AI agents. From environment setup to workflow design, this step-by-step guide covers everything you need.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →