Masks, stencil cutouts, and conditional region blending
In image processing, a mask is a single-band (1-band) 8-bit grayscale image: 255 (white) represents full selection or full opacity, 0 (black) represents zero coverage, and intermediate values (1..254) produce feathered transitions.
Across the libvips ecosystem, three patterns cover masking and selective region processing:
- Attaching a grayscale mask as the image’s alpha channel (
BandJoin/joinChannel); - Cutting out a base image with an RGBA shape using Porter-Duff
dest-in(BlendModeDestIn); - Blending two images pixel-by-pixel from a mask (
ifthenelseor alpha compositing).


1. Attaching a 1-band grayscale mask as alpha (BandJoin)
Section titled “1. Attaching a 1-band grayscale mask as alpha (BandJoin)”Given a 3-band RGB image and a matching 1-band grayscale mask (from an SVG rasterization, Pango text render, or segmentation model), join the grayscale mask as the fourth band to create a 4-band RGBA cutout:
// Ensure the base image has 3 RGB bands (strip any existing alpha first)if baseImg.Bands() > 3 { if err := baseImg.ExtractBand(0, 3); err != nil { return err }}
// Ensure the mask image is a single-band grayscale imageif maskImg.Bands() > 1 { if err := maskImg.ExtractBand(0, 1); err != nil { return err }}
// Append the 1-band mask as the 4th band (RGBA)if err := baseImg.BandJoin(maskImg); err != nil { return err}import sharp from 'sharp';
// Strip any prior alpha channel, then append the 1-band grayscale mask as band 3await sharp('input.jpg') .removeAlpha() .joinChannel('mask-gray.png') .png() .toFile('masked-cutout.png');import pyvips
base = pyvips.Image.new_from_file("input.jpg")[0:3]mask = pyvips.Image.new_from_file("mask-gray.png")[0]
rgba = base.bandjoin(mask)rgba.write_to_file("masked-cutout.png")vips bandjoin "input.jpg mask-gray.png" masked-cutout.png2. Rounded corners and SVG stencil cutouts with dest-in
Section titled “2. Rounded corners and SVG stencil cutouts with dest-in”When the stencil is already an RGBA graphic (such as a rounded rectangle, circular avatar frame, or badge rendered from SVG), ensure the base image has an alpha channel and composite the stencil using BlendModeDestIn (dest-in) — keeping the base image’s RGB colours while multiplying its alpha by the stencil’s alpha:
// Build a 500×500 rounded-rectangle SVG stencilsvgMask := []byte(`<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg"> <rect width="500" height="500" rx="64" ry="64" fill="#ffffff"/></svg>`)
mask, err := vips.NewImageFromBuffer(svgMask)if err != nil { return err}defer mask.Close()
if !cropImg.HasAlpha() { if err := cropImg.AddAlpha(); err != nil { return err }}
// BlendModeDestIn: retain base pixels only where the SVG stencil is opaqueif err := cropImg.Composite(mask, vips.BlendModeDestIn, 0, 0); err != nil { return err}import sharp from 'sharp';
const roundedMask = Buffer.from( '<svg width="500" height="500"><rect width="500" height="500" rx="64" ry="64" fill="#fff"/></svg>',);
await sharp('crop-500.webp') .ensureAlpha() .composite([{ input: roundedMask, blend: 'dest-in' }]) .png() .toFile('rounded-avatar.png');import pyvips
svg_mask = b'<svg width="500" height="500"><rect width="500" height="500" rx="64" ry="64" fill="#fff"/></svg>'mask = pyvips.Image.new_from_buffer(svg_mask, "")
crop = pyvips.Image.new_from_file("crop-500.webp")if not crop.hasalpha(): crop = crop.addalpha()
rounded = crop.composite2(mask, "dest-in")rounded.write_to_file("rounded-avatar.png")vips composite2 crop-500.png mask.svg rounded-avatar.png dest-in3. Selective region filtering and conditional blending (ifthenelse vs alpha over)
Section titled “3. Selective region filtering and conditional blending (ifthenelse vs alpha over)”Suppose you want a sharp subject in the foreground and a Gaussian-blurred background controlled by a soft mask:
- In
pyvipsandvips CLI, callmask.ifthenelse(sharp_img, blurred_img, blend=True)directly: mask values near255picksharp_img, values near0pickblurred_img, andblend=Trueinterpolates smoothly between them. - In
govipsandsharp, which do not export a standaloneIfthenelsemethod, join the 1-band mask onto the foreground image (BandJoin/joinChannel) and composite it over the blurred background withBlendModeOver(over) for the exact same mathematical blend.
// govips: attach the grayscale mask as foreground alpha, then composite Over the blurred backgroundif err := foreground.BandJoin(maskGray); err != nil { return err}if err := blurredBg.Composite(foreground, vips.BlendModeOver, 0, 0); err != nil { return err}// sharp: attach the grayscale mask to the foreground with joinChannel, then composite over the blurred baseconst maskedFg = await sharp('foreground.jpg') .removeAlpha() .joinChannel('mask-gray.png') .png() .toBuffer();
await sharp('input.jpg') .blur(8) .composite([{ input: maskedFg, blend: 'over' }]) .webp({ quality: 82 }) .toFile('selective-blur.webp');import pyvips
fg = pyvips.Image.new_from_file("input.jpg")bg = fg.gaussblur(8.0)mask = pyvips.Image.new_from_file("mask-gray.png")[0]
# blend=True smoothly interpolates between fg and bg using the 0..255 maskresult = mask.ifthenelse(fg, bg, blend=True)result.write_to_file("selective-blur.webp", Q=82)vips gaussblur input.jpg bg-blur.v 8.0vips ifthenelse mask-gray.png input.jpg bg-blur.v 'selective-blur.webp[Q=82]' --blend