I was putting a few cutout photos onto dark cards for a side project, and every one of them came with a thin grey-white rim. My first guess was a bad mask, and I was ready to try a heavier model. Before doing that I loaded the exported PNG into NumPy and looked at what the edge pixels actually store. The outline was fine. The rim is the old white background, still sitting inside the colour of the semi-transparent pixels.
Measuring the semi-transparent ring
The test images are two white-background photos I found on Wikimedia Commons: a puppy (George Hodan, CC0) and a plant cutting in a glass bottle (Biusch, CC BY-SA 3.0). For the cutouts I used ImgIng (https://imging.ai/) in its fast AI mode, ISNet INT8, running in the browser (I used the Chinese UI, so the labels quoted below are my translations); I watched the Network panel during export and there were zero non-GET requests. Its refine panel says the default uses the AI output pixel by pixel without changing transparency or edge colour, so whatever I read from the file is the raw model output. This is roughly what I ran:
img = np.asarray(Image.open("puppy_cutout.png").convert("RGBA"), dtype=float)
a = img[..., 3:] / 255
ring = (img[..., 3] > 0) & (img[..., 3] < 255)
luma = lambda rgb: (rgb * [0.2126, 0.7152, 0.0722]).sum(-1)
print(ring.sum(), luma(img[..., :3][ring]).mean()) # 113438, 160.1
print(luma((img[..., :3] * a)[ring]).mean()) # on black: 75.4
The puppy has 113,438 pixels with alpha between 1 and 254. Their own RGB averages 160.1 in Rec.709 luma. For comparison I took the opaque pixels within 3 px inside the edge, which average 108.6. So the ring is about 50 luma brighter than the fur right next to it, and 39.9% of ring pixels are above 200. The plant is worse: 200.8 in the ring against 123.6 inside, with 57.8% above 200. The model assigned a "half subject, half background" alpha but kept the original pixel colour, which already had white mixed in.
This crop is the puppy's front leg and belly at 3x. Left is the original on white. In the middle, the default export on black, the lower edge of the leg brightens for a moment before going dark. On the right, with "remove background colour fringe" turned on, the grey rim is gone but the leg–belly boundary has picked up a jagged step. A WebP export had exactly the same semi-transparent pixel count and an average colour within 0.2, so the format is not the cause.
The error does not depend on the background
The straight-alpha formula is rgb * a + bg * (1 - a). On black the puppy ring comes out at 75.4, and on a dark navy #1A2A4A at 92.1. If the ring pixels held the subject's own colour (estimated from that inner band), they would be 64.3 and 81.0. That is about 11 too bright on both. The plant showed the same gap of about 11 on both backgrounds, and I first assumed that was a coincidence. It is not. Subtract the ideal composite from the actual one and the bg * (1 - a) term cancels, leaving a * (F_stored - F_subject). The error lives in the pixel, not in the backdrop. The "ideal" value is my approximation from nearby opaque pixels, so I only use it for relative comparisons.
So why does the same picture look fine on white? I did not composite onto white and measure it, so this part is reasoning from the formula. The extra light in the ring is white, and the background around it is white too, so it reads as a soft transition. On a dark background the backdrop term is close to zero, everything around the ring is dark, and that leftover white becomes the highest-contrast thing on screen. The rim was always there; the dark card just exposed it.
The glass bottle is the extreme case. The glass is itself semi-transparent and the white backdrop shows through it, so the default export leaves a grey-white halo about 20 px wide. With the fringe option on, the halo mostly disappears but the bottle outline turns into visible steps. Original by Biusch on Wikimedia Commons, CC BY-SA 3.0; the image above is a composite made from the cutout and is shared under CC BY-SA 3.0 as well.
That option fixes the rim by rewriting alpha, not colour. On the plant it changed alpha on 1,369,864 pixels while no opaque pixel shifted colour by more than 30. A leaf with pale patches lost 41,644 pixels to full transparency and showed black holes on the dark background. I have not found a setting that keeps both the clean rim and the pale leaf, so for now I treat it as a trade-off. What I do now: composite the export on black and zoom in, compare the ring's average brightness with the pixels just inside it, and only turn on fringe removal after checking whether the subject has areas close to the old background colour.

Top comments (0)