Pillow、sharp 與 mozjpeg 比較白底產品照的 JPEG 編碼效果
Pillow, sharp or mozjpeg for a white-background product photo? Same quality, different stray pixels
作者以三張白底產品照比較 Pillow、sharp 和啟用 mozjpeg 的 sharp,發現同為品質 85、4:2:0 色度抽樣時,mozjpeg 輸出檔案較小,背景雜點比例也較低。
I needed JPEGs of three white-background product photos and had three encoders to hand: Pillow, sharp, and sharp with its mozjpeg flag. After saving the same three PNGs all three ways: Pillow and plain sharp leave identical pixels, and the mozjpeg: true flag gave smaller files with fewer stray background pixels at the same quality. None of them gets the background to 100% pure 255. Unless I say otherwise the numbers below are means over three photos on one Apple M4 Mac, with Pillow 11.3.0 and sharp 0.34.5. KB here means 1024 bytes.
What I fed them
The photos are three shots from Wikimedia Commons, not mine: a teddy bear on a blue knit blanket, a Bohemian glass against a grey wall, and a pair of white sneakers on beige tile. All three are CC BY-SA 4.0, with credits under the chart. I made the white-background PNGs in ImgIng (https://imging.ai/) in desktop Chromium: AI cutout on the quick setting, background set to white, exported as PNG. Its export row after a cutout lists WEBP, PNG, AVIF, TIFF and GIF with no visible JPG, so everything JPEG below happened afterwards.
"Background" means the pixels the cutout left fully transparent. In the PNG every one of them is 255, and I decoded each JPEG with Pillow and counted the ones where any channel is no longer 255. Amazon's US seller help page (viewed 2026-10-23) asks main images for a pure white background, RGB 255, 255, 255, and says JPEG is recommended, which is why I care about that last digit.
Pillow and plain sharp gave the same pixels
At quality 85 with 4:2:0 chroma sampling, which is the default for both, the stray share was identical for every photo: 0.656% on the sneakers, 0.286% on the teddy bear, 0.503% on the glass. Sharp's files came out a few KB smaller, 210 against 216 KB on the sneakers. The cjpeg on my machine is libjpeg-turbo, not mozjpeg, and matched Pillow byte for byte, so I left it out.
ImgIng's compress and convert tool is the other way I got JPEG. At 85 its file was within a few KB of sharp's default (211 against 210 KB on the sneakers), and the stray share matched Pillow's at the same quality. Its output measured 4:2:0.
The mozjpeg flag changed the result
Same quality, same sampling, only mozjpeg: true added. Three-photo means:
| Encoder | Quality | Sampling | Mean size | Stray background pixels |
|---|---|---|---|---|
| Pillow | 85 | 4:2:0 | 175 KB | 0.481% |
| sharp default | 85 | 4:2:0 | 165 KB | 0.481% |
| sharp mozjpeg | 85 | 4:2:0 | 125 KB | 0.347% |
| Pillow | 95 | 4:4:4 | 304 KB | 0.123% |
| sharp mozjpeg | 85 | 4:4:4 | 141 KB | 0.114% |
Against sharp's default at the same quality and sampling, the mozjpeg files were 38 to 41% smaller at quality 75, 23 to 28% at 85 and 13 to 20% at 95, per photo. The stray share dropped too. The last two rows are the pair I'd point to: mozjpeg at quality 85 with 4:4:4 ended at about the same stray share as Pillow at 95, in under half the bytes.
Look for the orange sharp mozjpeg line sitting lower and further left than every other one in both panels, meaning smaller files with fewer stray pixels. The ImgIng convert line sits on top of the sharp default line in the left panel. Photos by Dan Bollinger, Kritzolina and JustinLiang522 from Wikimedia Commons, CC BY-SA 4.0, adapted here under the same license.
Switching it on
In Pillow subsampling is 2 for 4:2:0 and 0 for 4:4:4. In sharp the mozjpeg switch is one extra key, and nothing else differs between the second and third rows of the table.
src.save("shoe_pil.jpg", quality=85, subsampling=2)
bg = cut == 0 # alpha of the cutout, 0 = left empty
share = 100 * (np.array(Image.open(p).convert("RGB"))[bg] != 255).any(axis=1).mean()
# pil 216 KB 0.656 % sharp 210 KB 0.656 % moz 162 KB 0.451 %
const opts = { quality: 85, chromaSubsampling: "4:2:0" };
await sharp("shoe_white.png").jpeg(opts).toFile("shoe_sharp.jpg");
await sharp("shoe_white.png").jpeg({ ...opts, mozjpeg: true }).toFile("shoe_moz.jpg");
The comment is the real output for the sneaker photo.
What I'd pick and what's left
For a white product photo I'd use sharp with mozjpeg: true, at quality 85 with 4:4:4 when the stray share matters and with 4:2:0 when the file size does. Chroma sampling mattered more than quality in my numbers: moving Pillow from 4:2:0 to 4:4:4 at quality 85 roughly halved the stray share, and going from 85 to 95 at 4:2:0 cut it by about a third.
Nothing reached zero. Pillow at quality 100 with 4:4:4 still had 0.035% stray pixels at an average of 538 KB, and mozjpeg at 95 with 4:4:4 had 0.045%. So I'd check the finished JPEG instead of trusting a quality number. That's three photos on one machine, so the numbers aren't a promise about your images. I haven't tried other mozjpeg builds, or checked whether any marketplace rejects a background that reads 254 in a few pixels.
來源:DEV Community · dev.to