I zoomed into a blurred-background export from a browser video matting tool and found a dark ring around the speaker's head. My first suspect was the matte, and it turned out not to be the cause. At the graphics-tool company I worked at before, dark fringes around resized transparent PNG icons came in as bug reports every few months. This ring is the same bug in a different place: a blur filter is averaging pixels that belong to another layer. Both standard fixes from texture work carry over, and on one clip I measured how far each gets.
The ring is the person blurred into the background
I used ImgIng's video matting (https://imging.ai/) for the export, a Beta that runs in desktop Chrome or Edge and not on phones. I only saw its output, not its code. The clip is an 8-second, 478×850 sign language video from Wikimedia Commons (Sayvhior, CC BY-SA 4.0, signing by JudithNG12), one woman in a headscarf before a light curtain. I exported it with blur strength 18 (the default) and 40 in Chromium 149 on a desktop machine that had no GPU acceleration. In its blur-original-background mode the ring appears because what gets blurred is the original frame with the person still in it, so the dark of her clothes and skin spreads past her outline. To put numbers on it I compared brightness in rings outside her edge, on four frames, against an empty-curtain reference rebuilt from the clip itself; it covers 62.5% of the frame. Right at her edge, in the first 4 px, the background read 67.8 grey levels (0 to 255) darker than it should at strength 18, and 63.1 darker at 40. The visible part stretched about 16 to 32 px out at 18 and beyond 32 px at 40.
Filtered transparent textures get the same dark fringe
Take a sprite stored with straight alpha, where the fully transparent pixels still hold some colour, usually black. Scale it and the filter averages those invisible black pixels into the edge, leaving a dark outline that was never in the artwork. Background blur does the same thing with the layers swapped. The kernel should only see background, but it reaches across her edge and pulls her colour in. Real-time depth of field hits it too, where a sharp foreground leaks into the blurred background unless samples are weighted by depth. Texture pipelines settled on two fixes: premultiply, so excluded pixels carry zero weight, or bleed the edge colours outward into the empty area before filtering.
Both texture fixes work on video frames
I reproduced both with OpenCV on the same frames, taking the matte from a transparent export of the clip (WebM only in that tool). Here is the core, trimmed. It's a generic compositing order, not a feature of the tool.
# r is the blur strength, 18 or 40
keep = 1.0 - matte # how much of each pixel is background
smear = cv2.GaussianBlur(img, (0, 0), r) # blur everything, person included
num = cv2.GaussianBlur(img * keep[..., None], (0, 0), r)
den = cv2.GaussianBlur(keep, (0, 0), r)[..., None]
weighted = num / np.maximum(den, 1e-3) # premultiplied-style blur
grow4 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
fill_zone = cv2.dilate((matte > 0.02).astype(np.uint8), grow4) # fill area grown 4 px
plate = cv2.GaussianBlur(cv2.inpaint(img_u8, fill_zone * 255, 9, cv2.INPAINT_TELEA), (0, 0), r)
out = img * matte[..., None] + plate * keep[..., None] # sharp person on top
The weighted version is premultiplication written out: blur colour times weight, blur the weight, divide. Averaged over the four frames on the original matte, it left −0.1 to −1.6 grey levels across the rings at strength 18, and −1.3 to −3.3 at 40. Filling first, which is edge bleeding by inpainting, landed within ±1 at both strengths. I haven't pinned down where the weighted version's leftover comes from; at 40 it is still a few levels, which I would not ship as a regression baseline without knowing why.
Filling has to reach past the matte
Texture bleeding runs a few pixels past the visible edge, and the fill here needs that margin too. With a matte shrunk 2 px to a hard edge and the fill area taken straight from it, her outer 2 px stayed in the background, got blurred, and at strength 18 the 4 to 16 px rings came back at −14.1. Growing the fill 4 px past the matte brought them to +0.3. The plain blur-everything version sat at −15.4 on the original matte. Filling does cost an inpainting pass on every frame, which the weighted blur avoids.
Read the last group first: with the hard edge, the fill taken straight from the matte sinks almost as low as blurring everything, while the grown fill sits back at zero. Data: my own measurements on this clip, four frames.
A thin dark line is a different member of the family
My filled frames still show a very thin dark outline right on her edge. I read it as the other straight-alpha problem: edge pixels already contain the old background mixed into their colour, so the curtain or the dark wall rides along when she is pasted onto anything new. Removing it is colour decontamination, a separate step I didn't measure. So in a compositing pipeline I'd place it this way: the blur stage only ever receives background pixels, through weights or a fill, and decontaminating the edge is its own stage between matting and the final paste.
Top comments (0)