DEV Community

hao jia
hao jia

Posted on

On a 7 px trimap band, alpha matte MSE grows about 6x and SAD stays put

If you build your own evaluation for video matting, you have to decide what "unknown region" means before you compute a single metric. I had to make that choice for a synthetic test, and it turned out to move one metric by about six times while leaving another almost untouched. In one case it also hid more than half of a method's error.

The setup is synthetic so that ground truth exists. I took 90 frames of an alpha matte at 540×960, composited the foreground over two backgrounds (a flat green screen, and a cluttered one that shifts 1 px per frame), and scored several methods against the known alpha: FFmpeg chromakey at three settings, a hard green threshold, an AI matte, and two constructed controls. The AI matte in these numbers is the compatible tier of ImgIng https://imging.ai/ , exported as transparent WebM and decoded back with libvpx-vp9. One caveat before any numbers. The ground truth is itself the output of a larger model from the same family (RVM ResNet50), not a hand-refined alpha. Scoring against it tells you how close a method gets to that model, not to the real edge, and I'd expect the AI row to look better than it deserves.

The band comes from eroding the ground truth

A synthetic clip has no trimap, so I derived one from the ground truth for every frame. Pixels with alpha ≥ 0.99 count as definite foreground and pixels ≤ 0.01 as definite background. Each of those two masks is eroded with an elliptical kernel 15 px across, which pulls its edge back by about 7 px. Whatever neither eroded mask still covers is the band: the true semi-transparent edge plus roughly 7 px on each side. This is the whole script, run over the 90 ground-truth frames:

import sys, glob, cv2, numpy as np
k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
counts = []
for f in sorted(glob.glob(sys.argv[1] + "/*.png")):
    alpha = cv2.imread(f, cv2.IMREAD_GRAYSCALE).astype(np.float64) / 255
    fg = cv2.erode((alpha >= 0.99).astype(np.uint8), k)
    bg = cv2.erode((alpha <= 0.01).astype(np.uint8), k)
    band = (fg == 0) & (bg == 0)
    counts.append(band.sum())
print("frames", len(counts), "pixels", alpha.size)
print("band mean %.2f  share %.2f%%" % (np.mean(counts), 100 * np.mean(counts) / alpha.size))
Enter fullscreen mode Exit fullscreen mode
$ python3 band.py gt_alpha/
frames 90 pixels 518400
band mean 81816.97  share 15.78%
Enter fullscreen mode Exit fullscreen mode

The band averages 81,817 pixels out of 518,400, which is 15.78% of the frame. Keep that share in mind for the rest of this post, because MSE divides by it and SAD does not. I only ran the 7 px radius. How the numbers move at a tighter or wider band is something I have not measured.

SAD barely notices the region

SAD in this test is the sum of |predicted − true| over the region, divided by 1000. It is a total, so pixels outside the band only add to it when they are wrong, and for most methods they are not. Whole frame against band: chromakey at similarity 0.10 gives 3.88 and 3.88, the hard threshold 4.25 and 4.25, the AI matte on green 2.89 and 2.89. The AI matte on the cluttered background is the widest normal gap at 7.44 against 6.97. Apart from one setting covered below, the difference stays under 0.5. A near-zero gap is already a finding: whatever these methods get wrong, they get wrong within a few pixels of the true edge.

MSE grows about six times for the same errors

MSE is the mean of the squared difference, so its denominator is the pixel count of the region. Same error mass, roughly one sixth as many pixels, and the number scales up. For the AI matte on green, MSE×1000 goes from 1.41 over the whole frame to 9.03 over the band. Chromakey 0.10 goes from 2.75 to 17.54, the threshold from 3.47 to 21.98, and the cluttered-background AI run from 8.06 to 47.15. None of these methods changed between the two columns, only the denominator did.

So I don't put my MSE next to numbers from papers. A table that says "MSE" without the region, the thresholds that define it and the resolution gives me a value I can't line up against mine. Even inside one test, whole-frame MSE and band MSE are two different measurements that share a name.

One chroma key hides most of its error outside the band

Chromakey at similarity 0.25 with blend 0.10 is the exception, and it is the most useful row in the table. Whole-frame SAD is 93.37 and band SAD is 40.65. Subtract one from the other and over half of the total is left, all of it in pixels the band never touches. At that setting the key treats large dark patches of clothing and the shoes as green and removes them, well away from the edge. Its MSE goes from 171.15 to 461.24, a much smaller jump than the other rows, for the same reason: most of its error was never in the band. Scored on the band alone, this setting still looks bad, just less than half as bad as it is. A band-only report is blind to exactly the failure that an aggressive key setting produces, which is why I keep both columns. When whole-frame SAD and band SAD agree, the method fails at edges. When they split, something large broke elsewhere, and the gap tells you how much.

The line I now keep next to these metrics is the region definition: thresholds 0.99 and 0.01, erosion kernel 15 px, band share 15.78%, frame 540×960. Without it nobody can reproduce my MSE. The numbers here hold only for this synthetic clip, these 90 frames and this implementation. The band share depends on how much edge the subject has, so 15.78% belongs to this clip and not to matting in general.

The ground-truth frames come from "Filming with the green screen" (VOA Learning English, Wikimedia Commons, public domain), cut to 3 seconds and recomposited, so modified. The two backgrounds were generated for this test.

Top comments (0)