DEV Community

hao jia
hao jia

Posted on

Where did the 254s go? A heatmap of non-255 pixels in a white JPEG

A white sneaker photo, cut out and flattened onto white, is exactly 255 in the PNG. Save it as a JPEG at quality 85 and it isn't any more. A percentage tells me how much drifted, but when a rule says "pure white" I want to know where, so I built a heatmap. On this one photo, 10,403 pixels that were exactly 255 in the PNG no longer were after the JPEG, and the map puts them along the outline of the shoes.

The photo is a Wikimedia Commons shot of white sneakers on beige tile (JustinLiang522, CC BY-SA 4.0). I made the white PNG in ImgIng (https://imging.ai/), a browser-based image tool, in desktop Chromium: cutout, background set to white in the refine step, exported at 1,280×1,707. Its export row lists PNG, WebP, AVIF, TIFF and GIF but no JPEG, so the JPEG comes from Pillow 11.3.0 and numpy 2.0.2 on an Apple M4, quality 85 with Pillow's 4:2:0 chroma. Amazon's US seller help page (viewed 2026-10-23) asks main images for RGB 255, 255, 255 and recommends JPEG, which is why I care about the last digit.

A percentage says how much but not where

My first count used the cutout's alpha channel: background is wherever alpha is 0. That gave 7,916 drifted pixels, 0.66% of the background. It needs the cutout file, and I'd guess an image reaches an upload step with the alpha long gone, though I haven't checked any pipeline. So I switched to what the finished files still tell me: which pixels were exactly 255 in the PNG master?

Use the master as the mask

Everything below compares two arrays, the PNG before saving and the JPEG decoded again.

png = Image.open("master.png").convert("RGB")
stream = io.BytesIO()
png.save(stream, "JPEG", quality=85, subsampling=2)
before = np.asarray(png)
after = np.asarray(Image.open(stream).convert("RGB"))

white = (before == 255).all(axis=2)
drift = white & (after < 255).any(axis=2)
print(f"{white.mean():.2%} white, {drift.sum():,} drifted")
# 55.59% white, 10,403 drifted
Enter fullscreen mode Exit fullscreen mode

I expected 7,916 again and got 10,403. The extra 2,487 sit on the shoe itself: 7,891 pixels of the shoe are pure 255 in the PNG, and some of them drift too. That's the price of not having a mask, and I'm keeping this version because it works on any pair of files.

The red hugs the outline

To see it, I widened each drifted pixel with a 5×5 maximum filter, faded the photo to a quarter of its strength and painted the dots red.

dots = Image.fromarray((drift * 255).astype("uint8")).filter(ImageFilter.MaxFilter(5))
canvas = (before * 0.25 + 191).astype("uint8")
canvas[np.asarray(dots) > 0] = (230, 40, 40)
Image.fromarray(canvas).save("drift.png")
Enter fullscreen mode Exit fullscreen mode

I'm not attaching the render. In mine the red is a thin line along the outline of both shoes and almost nothing across the open white. A few isolated specks sit away from the shoes, and I haven't traced them. A count by distance agrees: 7,731 of the 7,916 drifted pixels in the cutout's empty area are within 8 pixels of the subject, so a check that samples only far from the shoe would have seen none of it.

Every threshold shrinks the red

A heatmap with a threshold on it is the useful version. I counted a pixel as still red when any channel fell below the threshold, for five thresholds.

for t in (255, 254, 252, 250, 245):
    print(t, (white & (after < t).any(axis=2)).sum())
# 255 10403 | 254 5129 | 252 1091 | 250 293 | 245 32
Enter fullscreen mode Exit fullscreen mode

Half the red disappears at 254, and by 250 only 293 pixels are left, under 3% of the 10,403. So most of the drift is one or two levels off white. The darkest spots go further than that. A count on the same photo and quality found a few background pixels as low as 239.

Share of pixels counted as white at thresholds from 255 down to 240, for the whole image and for the background only, on three photos

The right panel is the part to read: background-only share on the sneakers climbs from 99.34% at 255 to 99.98% at 250 and is at or above 99.99% by 245. The left panel is the trap. Over the whole image the sneaker share moves only from 55.25% to 56.39% between 255 and 250, a 1.14-point shift that looks negligible even though the red count dropped by about 97% over the same range. They are different measures of the same JPEG, and the whole-image one also counts the shoe's own bright pixels: 2.66% of the shoe body is at or above 250. Photos by Dan Bollinger, Kritzolina and JustinLiang522, Wikimedia Commons, CC BY-SA 4.0; the chart is an adaptation under the same license.

If I had to put a line in a check today I'd start at all three channels at or above 250. That is my guess from one photo, not a validated value, and I don't know what any marketplace uses.

What I still can't explain

The specks away from the outline are the open one. The same photo's background count at quality 85 goes from 0.66% to 0.29% with 4:4:4 chroma and never reaches zero, so my next run is the same two blocks with subsampling=0, to see whether the specks go along with the rest of the red.

For your own export, run the first block before you read any percentage and look at where the red sits. Three photos were measured in the background figures above, only the sneakers went through the heatmap, and it was one encoder on one machine.

Top comments (0)