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.