Colour transfer for hair previews looks trivial until you try it. The naive approach - multiply the hair pixels by a colour - produces the flat, painted-on look that makes most virtual try-on tools unconvincing. What follows is the pipeline I settled on after a few iterations, and it runs entirely client side.
Work in a perceptual space, not RGB
RGB interpolation is perceptually uneven: shifting toward a mid brown moves through muddy grey. Convert to CIELAB first, keep the L and b channels from the original hair, and replace a with the target chroma. That single change preserves the shading, the highlights and the depth that make hair read as hair.
// rgb -> xyz -> lab, applied per pixel
const [L, aOrig, bOrig] = rgbToLab(r, g, b);
const [, aTarget] = hexToLab(targetHex);
const blended = labToRgb(L, mix(aOrig, aTarget, strength), bOrig);
Segment, then feather
A hard mask gives a visible cut-out line at the hairline. Two fixes do most of the work:
- Feather the alpha over 2-3 px at the boundary.
- Reintroduce the original luminance in a narrow band around the mask edge, so the transition follows the strands rather than the silhouette.
Keep the highlights
Real hair is not one colour. Sample the brightest 10% of hair pixels and hold their lightness fixed while the rest of the target colour is applied. Without this step, blonde to black works but dark to platinum collapses into a flat grey cap.
Performance
Run the transfer on a downscaled copy (400 px on the long edge) while dragging a colour picker, then recompute at full resolution once the user stops. In practice the interaction stays at 60 fps on integrated graphics if you keep the loop in a typed array and avoid allocating per pixel.
Known limits
- Very dark hair has little chroma to preserve, so a colour-brightening step is needed first.
- Strong backlighting breaks the luminance assumption; treat those regions with the original pixels.
- Curly hair with heavy occlusion still confuses segmentation more than it confuses the transfer itself.
The full implementation is running at AI Hair Color Changer if you want to compare the output against your own version.
Top comments (0)