Quick answer: The threshold color and tolerance must actually match pixels in the surface. Sample a real pixel with get_at first, and widen the per-channel threshold.

A code path builds a collision mask from a sprite’s background color via mask.from_threshold, but the resulting mask has zero set bits. The threshold didn’t match anything.

Sample the Actual Color

# find out what the pixels actually are
print(surface.get_at((0, 0)))   # e.g. (12, 200, 14, 255)

Don’t assume the green is (0, 255, 0) — compression, anti-aliasing, or art tools shift it. Use the real value.

from_threshold Signature

mask = pygame.mask.from_threshold(
    surface,
    (12, 200, 14),         # the color to match
    (30, 30, 30, 255))      # per-channel tolerance

A pixel is set in the mask if it’s within tolerance of the color on every channel. Too-tight tolerance over anti-aliased edges = empty or sparse mask.

Invert for Collision

from_threshold marks pixels matching the color. For a collision mask you usually want the opposite — everything that isn’t the background:

bg_mask = pygame.mask.from_threshold(surface, bg_color, tol)
collision_mask = bg_mask
collision_mask.invert()

Prefer from_surface for Alpha Sprites

If your sprite already has a proper alpha channel, mask.from_surface(surface, alpha_threshold) is simpler and more reliable than color thresholding.

Verifying

mask.count() returns a sensible non-zero number. Overlaying the mask (mask.to_surface()) visually matches the sprite’s solid area.

“Threshold matches real pixels, not assumed ones. Sample first, widen tolerance, invert for collision.”

If you control the art pipeline, ship sprites with a clean alpha channel and use from_surface — color thresholding is a last resort.