Virtual hairstyle try-on looks like a straightforward image generation task:
- Upload a portrait.
- Choose a hairstyle.
- Generate a new image.
However, a useful hairstyle preview is not simply a portrait with different hair.
The system must change one visually complex region while preserving nearly everything else:
- Facial identity
- Expression
- Skin tone
- Head position
- Camera angle
- Lighting
- Clothing
- Background
- Image composition
If any of these elements drift too much, the user is no longer comparing hairstyles. They are comparing different people, poses, or photographs.
While building an AI hairstyle changer, I found that the most difficult part was not producing attractive hair. It was constraining the transformation so that the result remained useful as a before-and-after reference.
This article describes how I would structure a production hairstyle try-on pipeline, from upload validation and hairstyle normalization to identity preservation, failure detection, and honest user experience.
1. Define the real product goal
A generic image generator tries to create a visually pleasing image.
A hairstyle try-on product has a narrower goal:
Change the hairstyle while preserving the person and the original photographic context.
That distinction affects the entire architecture.
A generated result may look impressive but still fail as a preview if it:
- Changes the user's face shape
- Makes the eyes larger
- Smooths the skin aggressively
- Alters the expression
- Changes the camera angle
- Replaces the clothing
- Adds professional studio lighting
- Changes the background
- Makes the person appear significantly younger
The system should therefore optimize for controlled transformation rather than unrestricted creativity.
A simple internal request type might look like this:
type HairstyleRequest = {
sourceImageId: string;
hairstyle: string;
hairColor?: string;
hairLength?: "buzzed" | "short" | "medium" | "long";
hairTexture?: "straight" | "wavy" | "curly" | "coily";
bangs?: "none" | "curtain" | "wispy" | "blunt" | "side";
preserveIdentity: true;
preserveExpression: true;
preserveBackground: true;
outputAspectRatio: "1:1" | "3:4" | "4:3";
};
The user does not need to see every parameter, but the backend benefits from turning a vague hairstyle idea into explicit constraints.
2. Validate whether the photo is suitable
Not every portrait is a good input for hairstyle preview.
A system should inspect the image before spending generation resources.
Useful checks include:
- Exactly one primary face is visible
- The face is large enough in the frame
- The head is not heavily cropped
- The current hair is visible
- Lighting is not extremely dark
- The face is not heavily blurred
- The image does not contain a large beauty filter
- Hats or hands are not covering most of the hair
- The file format and size are supported
A basic validation result could look like this:
type PortraitValidation = {
valid: boolean;
faceCount: number;
faceCoverage: number;
headFullyVisible: boolean;
hairVisibility: number;
blurScore: number;
brightnessScore: number;
warnings: string[];
};
The application should distinguish between blocking problems and warnings.
For example:
function evaluatePortrait(
result: PortraitValidation
): "accept" | "warn" | "reject" {
if (result.faceCount !== 1) {
return "reject";
}
if (!result.headFullyVisible || result.faceCoverage < 0.12) {
return "reject";
}
if (
result.hairVisibility < 0.5 ||
result.blurScore > 0.65 ||
result.brightnessScore < 0.2
) {
return "warn";
}
return "accept";
}
A warning might say:
Your current hair is partly covered. The preview may be less accurate around the hairline and sides.
This is more useful than allowing the request to fail silently.
3. Separate editable and protected regions
The hairstyle region is not a simple rectangle.
Hair can overlap:
- Forehead
- Ears
- Cheeks
- Neck
- Shoulders
- Clothing
- Background
A reliable pipeline needs a concept of editable and protected regions.
Editable regions
- Existing hair
- Hairline boundary
- Areas where longer hair may be added
- Small neighboring regions needed for natural blending
Protected regions
- Eyes
- Nose
- Mouth
- Core facial geometry
- Skin tone
- Clothing
- Background
- Accessories that should remain unchanged
A segmentation result might contain multiple masks:
type PortraitMasks = {
hair: Uint8Array;
face: Uint8Array;
skin: Uint8Array;
ears: Uint8Array;
neck: Uint8Array;
clothing: Uint8Array;
background: Uint8Array;
};
Using only the original hair mask is often insufficient.
If the user selects long hair but currently has a buzz cut, the editable region must expand beyond the original hair boundary. The system needs a hairstyle-specific expected region.
function calculateEditRegion(
masks: PortraitMasks,
targetLength: HairstyleRequest["hairLength"]
) {
const baseRegion = masks.hair;
switch (targetLength) {
case "buzzed":
return expandMask(baseRegion, 4);
case "short":
return expandMask(baseRegion, 12);
case "medium":
return expandMask(baseRegion, 40);
case "long":
return extendTowardShoulders(baseRegion, masks.clothing);
default:
return baseRegion;
}
}
The exact implementation depends on the image model, but the principle is important:
The allowed edit region should depend on the requested hairstyle, not only on the current hairstyle.
4. Preserve identity explicitly
“Keep the same person” is too vague as a model instruction.
Identity preservation should be expressed as a collection of constraints.
Preserve:
- Face width and height
- Eye shape and spacing
- Eyebrow shape
- Nose structure
- Mouth shape
- Jawline
- Skin tone
- Apparent age
- Expression
- Head angle
Do not unintentionally introduce:
- Face slimming
- Larger eyes
- Smaller nose
- Smoother skin
- Different makeup
- Different ethnicity
- Different age
- Different gender presentation
The generation prompt should state these constraints directly.
For example:
const identityConstraints = `
Preserve the same person's facial identity exactly.
Keep the original face shape, eyes, eyebrows, nose, lips,
jawline, skin tone, apparent age, expression, head angle,
camera perspective, clothing, lighting, and background.
Modify only the hairstyle and requested hair color.
Do not beautify, reshape, retouch, or replace the face.
`;
Identity preservation should also be evaluated after generation.
A lightweight evaluation pipeline could compare:
- Face embedding similarity
- Facial landmark displacement
- Pose difference
- Expression difference
- Background difference
type IdentityEvaluation = {
faceSimilarity: number;
landmarkDrift: number;
poseDifference: number;
backgroundDifference: number;
};
function passesIdentityCheck(result: IdentityEvaluation) {
return (
result.faceSimilarity >= 0.82 &&
result.landmarkDrift <= 0.08 &&
result.poseDifference <= 0.1 &&
result.backgroundDifference <= 0.12
);
}
These thresholds are illustrative. They should be calibrated using actual user images and reviewed outputs.
5. Normalize hairstyle selections
Users describe hairstyles inconsistently.
The same idea might be entered as:
- Curtain bangs
- Korean curtain bangs
- Soft middle-parted fringe
- Long face-framing bangs
- Bardot bangs
Sending raw user text directly into the generation pipeline creates unpredictable results.
Instead, map selections into a normalized hairstyle specification.
type NormalizedHairstyle = {
category: string;
length: string;
silhouette: string;
parting: string;
fringe: string;
texture: string;
volume: string;
layering: string;
sideShape: string;
backShape: string;
};
A curtain-bangs preset might become:
{
"category": "curtain bangs",
"length": "medium to long",
"silhouette": "soft face-framing shape",
"parting": "center part",
"fringe": "long parted fringe opening around the eyes",
"texture": "natural straight to soft wave",
"volume": "moderate",
"layering": "soft front layers",
"sideShape": "blended into the cheek and jaw area",
"backShape": "preserve existing overall length"
}
This structure provides more control than a single label.
It also helps the product support:
- Preset hairstyle cards
- Custom text instructions
- Localization
- Hairstyle comparison
- Analytics by hairstyle attribute
6. Separate hairstyle and color instructions
A haircut and a hair color transformation are related but different operations.
Haircut instructions affect:
- Silhouette
- Length
- Volume
- Layering
- Bangs
- Hairline visibility
- Ear visibility
- Neck and shoulder overlap
Color instructions affect:
- Hue
- Warmth
- Lightness
- Root depth
- Highlight placement
- Saturation
- Contrast with skin tone
Combining everything into one vague prompt can reduce control.
A color specification could use:
type HairColorSpec = {
baseColor: string;
temperature: "cool" | "neutral" | "warm";
rootDepth: "same" | "slightly darker" | "dark root";
highlightStyle?: "none" | "balayage" | "full highlights";
intensity: "subtle" | "natural" | "vivid";
};
For example:
{
"baseColor": "copper brown",
"temperature": "warm",
"rootDepth": "slightly darker",
"highlightStyle": "none",
"intensity": "natural"
}
The prompt can then describe the haircut and color in separate sections.
function buildHairPrompt(
style: NormalizedHairstyle,
color?: HairColorSpec
) {
return `
TARGET HAIRSTYLE
- Category: ${style.category}
- Length: ${style.length}
- Silhouette: ${style.silhouette}
- Parting: ${style.parting}
- Fringe: ${style.fringe}
- Texture: ${style.texture}
- Volume: ${style.volume}
- Layers: ${style.layering}
TARGET HAIR COLOR
${
color
? `
- Base color: ${color.baseColor}
- Temperature: ${color.temperature}
- Root treatment: ${color.rootDepth}
- Highlight style: ${color.highlightStyle ?? "none"}
- Intensity: ${color.intensity}
`
: "- Preserve the original hair color"
}
`;
}
7. Handle hair boundaries carefully
Most visible hairstyle-generation errors happen near boundaries.
Common problem areas include:
- Hairline against the forehead
- Hair behind the ears
- Bangs crossing the eyebrows
- Long hair crossing the neck
- Hair overlapping shoulders
- Strands against a complex background
- Fade transitions near the temple
- Sideburns and facial hair
A hard-edged mask can produce obvious seams.
A heavily feathered mask can allow changes to spread into the face or background.
The mask should generally use:
- A protected facial core
- A narrow transition zone
- A wider hairstyle-dependent generation region
Conceptually:
Protected face
↓
Narrow blending boundary
↓
Editable hair region
↓
Optional expansion region for longer styles
For hairstyles with bangs, the forehead cannot remain completely protected because part of it must be covered. However, the underlying facial geometry should remain unchanged.
For fades and buzz cuts, the model must reconstruct scalp and hairline details without changing the head shape.
For long hair, the system must allow new strands to overlap the shoulders while preserving the clothing underneath wherever it remains visible.
These cases may require different mask-generation strategies rather than one universal setting.
8. Use layered prompts instead of one paragraph
A production prompt is easier to maintain when separated into layers.
Layer 1: Identity constraints
Preserve the same person, face, expression, pose, age,
skin tone, lighting, clothing, background, and framing.
Layer 2: Hairstyle specification
Apply a chin-length blunt bob with a center part,
slightly curved ends, controlled side volume,
and no bangs.
Layer 3: Hair color specification
Use a natural medium copper-brown color with slightly
darker roots and realistic strand-level variation.
Layer 4: Rendering constraints
Maintain realistic hair density, strand direction,
hairline integration, ear interaction, neck overlap,
and lighting consistent with the original image.
Layer 5: Negative constraints
Do not change the face, eyes, eyebrows, nose, mouth,
jawline, expression, makeup, body, clothing, background,
camera position, or image composition.
In code:
function createGenerationPrompt(input: {
identity: string;
hairstyle: string;
color: string;
rendering: string;
negative: string;
}) {
return [
"IDENTITY PRESERVATION",
input.identity,
"",
"HAIRSTYLE",
input.hairstyle,
"",
"HAIR COLOR",
input.color,
"",
"REALISM REQUIREMENTS",
input.rendering,
"",
"DO NOT CHANGE",
input.negative,
].join("\n");
}
This makes prompt changes easier to test and version.
9. Keep comparisons visually consistent
A user may generate five different hairstyles from the same photograph.
If every result changes lighting, crop, background, or facial expression, comparing them becomes difficult.
For comparison workflows, keep these variables fixed:
- Source image
- Crop
- Aspect ratio
- Resolution
- Head position
- Face identity
- Background
- Lighting
- Output framing
Where supported, reuse stable generation settings or deterministic seeds.
Store the source and variant relationship explicitly:
type HairstyleVariant = {
id: string;
sourceImageId: string;
comparisonGroupId: string;
hairstylePresetId?: string;
customInstruction?: string;
generationSeed?: number;
outputImageId?: string;
status: "queued" | "processing" | "completed" | "failed";
};
The frontend can then display all variants as one comparison group rather than unrelated generations.
10. Detect common generation failures
A successful API response does not guarantee a useful result.
The system should look for predictable failure patterns.
Identity drift
The face no longer looks like the uploaded person.
Hair clipping
Hair stops unnaturally at the neck, ears, or image boundary.
Background mutation
Objects or colors behind the head change noticeably.
Facial obstruction
Bangs cover both eyes or create artificial eyebrow shapes.
Hairline artifacts
The hairline appears painted, duplicated, or detached from the scalp.
Color leakage
Hair color spreads onto the forehead, skin, clothing, or background.
Geometry errors
The model creates extra ears, duplicated hair sections, impossible braids, or uneven head shape.
A result evaluator could combine several scores:
type ResultQuality = {
identityScore: number;
backgroundConsistency: number;
hairBoundaryScore: number;
artifactScore: number;
styleMatchScore: number;
};
function calculateResultScore(q: ResultQuality) {
return (
q.identityScore * 0.35 +
q.backgroundConsistency * 0.15 +
q.hairBoundaryScore * 0.2 +
q.artifactScore * 0.1 +
q.styleMatchScore * 0.2
);
}
Low-quality outputs can be:
- Automatically retried
- Marked for review
- Shown with a warning
- Excluded from the user's paid generation count
The correct policy depends on cost and product economics, but the system should not treat every generated image as equally successful.
11. Build an honest before-and-after interface
A hairstyle preview is a decision-support tool, not a physical simulation.
The interface should help users compare practical differences:
- Hair length
- Bangs
- Face framing
- Side volume
- Hair color warmth
- Root depth
- Overall maintenance level
Useful interface patterns include:
- Before-and-after slider
- Side-by-side comparison
- Same-source variant grid
- Favorite and shortlist actions
- Downloadable reference image
- Notes for a stylist or barber
- Clear display of the selected hairstyle attributes
The result page should avoid claiming:
This is exactly how you will look after your haircut.
A more accurate statement is:
Use this preview as a visual reference and discuss the final cut or color with a stylist who can evaluate your real hair texture, length, density, and condition.
This protects user trust and better reflects what the technology can actually provide.
12. Explain maintenance, not only appearance
A hairstyle can look good in one generated image but still be unsuitable for the user's routine.
The interface can add non-generative information such as:
- Typical trim frequency
- Daily styling requirements
- Color maintenance
- Heat styling needs
- Whether the style grows out easily
- Whether it depends on natural texture
For example:
type HairstyleMaintenance = {
trimIntervalWeeks: [number, number];
dailyStyling: "low" | "medium" | "high";
colorMaintenance?: "low" | "medium" | "high";
recommendedProducts: string[];
notes: string[];
};
This information should be presented as general guidance rather than a personalized professional assessment.
It gives the preview more practical value without asking the image model to invent advice.
13. Protect user photos
Portrait images are personal data.
The product should clearly define:
- How long uploads are stored
- How long generated results are retained
- Whether users can delete results
- Whether guest results are persisted
- Whether images are sent to external processors
- Whether images are used for training
- Whether generated images are public or private
Avoid storing full image URLs in application logs.
Prefer logging:
type SafeGenerationLog = {
taskId: string;
userId?: string;
inputSizeBytes: number;
inputWidth: number;
inputHeight: number;
hairstylePresetId?: string;
processingTimeMs: number;
status: string;
errorCode?: string;
};
Do not log:
- Raw image bytes
- Permanent public image URLs
- Face embeddings
- Full custom prompts containing personal details
- Unredacted provider responses
Retention rules should be enforced by scheduled deletion, not only stated in a privacy policy.
14. Use explicit task states
Image generation can take long enough that users refresh the page or close the browser.
Use persistent task states:
type GenerationStatus =
| "created"
| "validating"
| "uploading"
| "queued"
| "generating"
| "evaluating"
| "completed"
| "failed";
A minimal task record:
type GenerationTask = {
id: string;
userId?: string;
sourceImageId: string;
request: HairstyleRequest;
status: GenerationStatus;
providerTaskId?: string;
outputImageId?: string;
qualityScore?: number;
errorCode?: string;
createdAt: Date;
updatedAt: Date;
};
The frontend can poll a status endpoint:
export async function GET(
request: Request,
context: { params: Promise<{ taskId: string }> }
) {
const { taskId } = await context.params;
const task = await getGenerationTask(taskId);
if (!task) {
return Response.json(
{ error: "Task not found" },
{ status: 404 }
);
}
return Response.json({
id: task.id,
status: task.status,
outputUrl:
task.status === "completed"
? await createSignedOutputUrl(task.outputImageId!)
: null,
errorCode: task.errorCode ?? null,
});
}
The task should survive browser refreshes and temporary frontend failures.
15. Measure whether the preview is useful
Generation speed is only one metric.
A fast but inaccurate hairstyle preview does not solve the user's problem.
Useful product metrics include:
- Percentage of images passing input validation
- Generation completion rate
- Identity-preservation pass rate
- Automatic retry rate
- Regeneration rate
- Before-and-after interaction rate
- Number of variants generated per source image
- Favorite or shortlist rate
- Download rate
- User-reported face-change rate
- User-reported hairstyle-mismatch rate
- Cost per accepted result
It is useful to separate technical success from product success.
Technical success
- The task completed
- The output file exists
- The image has valid dimensions
- No provider error occurred
Visual success
- Identity was preserved
- The hairstyle matched the request
- The background remained consistent
- There were no obvious artifacts
Product success
- The user compared the result
- The user saved or downloaded it
- The user did not immediately regenerate
- The user considered it useful as a reference
These are three different levels of quality.
Suggested architecture
A complete workflow might look like this:
Portrait upload
↓
File and face validation
↓
Image normalization
↓
Hair, face, clothing, and background segmentation
↓
Hairstyle specification normalization
↓
Editable-region construction
↓
Identity-constrained generation
↓
Automated quality evaluation
↓
Optional retry
↓
Private result storage
↓
Before-and-after comparison
The most important architectural principle is simple:
A hairstyle preview should preserve the user more strongly than it transforms the image.
The visual change should be concentrated in the hair. Everything else should remain stable enough that the result functions as a meaningful comparison rather than a new portrait of a different person.
That requires more than a good image model.
It requires validation, masking, structured hairstyle definitions, identity checks, failure recovery, privacy controls, and an interface that communicates both the value and limitations of the preview.
Disclosure: This article was created with AI assistance for structure and English-language editing. The technical content was reviewed and edited before publication.
Top comments (0)