Tasks description
Along with the source image, send a payload that describes which retouching operations should be applied.
Mode
This documentation only covers the "professional" mode. This parameter is required.
Tasks
For mode=professional, you must specify a list of tasks - which plugins should be applied and their settings.
Each task is a plugin name, the parameters for using this plugin, and a marker for whether to save the task result in a separate layer. Each plugin has no more than two main parameters. In API parameters they are mapped to "Alpha1" and "Alpha2" fields. The correspondence table with value ranges and parameter name mapping is given here.
- Plugin - plugin name.
- Alpha1 — first plugin parameter.
- Alpha2 — second plugin parameter.
- Scale — scale of the person in the photo, also can be interpreted as resolution.
For Clean Backdrop and Dust plugins, Scale defines dirt size: 1 targets the largest dirt particles, and 3 targets the finest / smallest dirt particles.
- 0 - auto detection
- 1 - close-up of the face
- 2 - half-body portrait
- 3 - full-body portrait
- Automask — Protects the person so changes apply only to the background. Available only for Clean Backdrop.
- Layer — flag that controls whether the task is returned as a separate layer.
- 0 - the task is applied directly to the flattened output image.
- 1 - the task is returned as a separate file. The final result is a ZIP archive with layers.
It is highly recommended to order tasks list by plugin name in same way as in the table below.
The numerical value of 1 corresponds to 100% value in the plugin.
| Plugin | Alpha1 | Alpha1 range | Alpha2 | Alpha2 range |
|---|---|---|---|---|
| Skin Mask | Sensitivity | [0, 2] | - | - |
| Clean Backdrop | Blend | [0, 1] | - | - |
| Remove Background | - | - | - | - |
| Heal | Sensitivity | [0, 1] | - | - |
| Fabric | Blend | [0, 2] | - | - |
| Dust | Blend | [0, 2] | - | - |
| Stray Hairs | Blend | [0, 1] | - | - |
| Eye Vessels | Blend | [0, 1] | - | - |
| Eye Brilliance | Blend | [0, 2] | - | - |
| White Teeth | Whiten | [0, 1] | Brighten | [0, 1] |
| Mattifier | Blend | [0, 1] | - | - |
| Dodge Burn | Blend | [0, 2] | Warmth | [0, 1] |
| Skin Tone | Blend | [0, 2] | - | - |
| Portrait Volumes | Blend | [0, 2] | - | - |
| Face Detection | - | - | - | - |
| Face Lifting | Named fields | Depends on parameter | - | - |
| Glasses Anti Glare | Glasses Glare Removal | [0, 1] | - | - |
| Color Correction | User Params | Named fields | - | - |
| Crop | User Params | Named fields | - | - |
Additional plugin notes
The plugins below are also supported in professional mode. Some of them use named fields or nested User Params instead of relying only on Alpha1 and Alpha2.
Remove Background
Remove Background automatically separates a wide range of foreground objects from the source image. It must be the only plugin in the tasks array. Use outputFormat: "png" with Layer: 0 for a ready transparent PNG. Add MaskOnly: 1 to return an 8-bit, single-channel grayscale mask with no RGB channels; BinaryMask: 0 preserves soft coverage, while BinaryMask: 1 returns a hard mask and is the default. Use outputFormat: "zip" with Layer: 1 for a foreground mask layer in an archive. See the AI Background Removal API guide for ecommerce, marketplace, DAM, PIM, CMS, direct alpha-mask, and custom compositing workflows.
{
"mode": "professional",
"outputFormat": "png",
"tasks": [
{
"Plugin": "Remove Background",
"version": 1,
"Layer": 0
}
]
}
Stray Hairs
Stray Hairs automatically removes detected loose and crossing hairs from portraits. Use Alpha1 from 0 to 1 for correction strength. With Layer: 0, the API returns a finished image. With Layer: 1 and ZIP output, it returns a full-size RGBA correction layer for normal blending. See the Stray Hairs Removal API guide for live before-and-after files, cURL, layered output, and integration guidance.
{
"Plugin": "Stray Hairs",
"Scale": 0,
"Layer": 0,
"Alpha1": 1.0
}
Crop
Crop supports AI image straightening, pose-aware portrait framing, subject-aware crop, document/passport portrait crop, and manual crop geometry through nested User Params. Use AutoStraightenMode: "AI" with Layer: 1 to write the predicted angle into ZIP result.json, or use Layer: 0 to return a flattened straightened image. Pose crop uses AutoMode: "Auto" or "Fixed" with AutoStraightenMode: "Person". Subject crop uses AutoMode: "Subject" and can return subjectMask RLE metadata in ZIP output. Manual crop uses AutoStraightenMode: "Manual", Angle, ManualCenterX, ManualCenterY, ManualArea, and aspect-ratio fields. See the Crop API overview, AI Straighten guide, Pose crop guide, Subject crop and mask guide, passport and ID photo crop API guide, and the Crop manual playground.
{
"Plugin": "Crop",
"Layer": 0,
"User Params": {
"AutoStraightenMode": "AI",
"AspectRatio": "Original"
}
}
Face Detection
Face Detection does not require user parameters. It detects faces and landmarks, writes face metadata to the result, and makes that data available to later face-aware plugins such as Face Lifting and Glasses Anti Glare. Landmark coordinates are normalized to the input image, where x=0/y=0 is the top-left corner and x=1/y=1 is the bottom-right corner.
{
"Plugin": "Face Detection"
}
Typical Face Detection metadata. The detector currently returns 10 landmark points plus face metadata. The source code does not expose semantic names for every landmark, so the comments below only describe the source-backed index behavior: 3/4 are used as the eye pair, 6/7 are used as the mouth-corner pair, 0/1, 3/4, and 6/7 are swapped during horizontal-flip averaging, and 2, 5, 8, 9 are centerline/non-swapped landmarks. Remove comments if you copy the object as strict JSON.
{
"metadata": {
"faces": [
{
"confidence": 0.8743641972541809,
"faceAge": 42.75107955932617,
"faceGenderFemale": -5.119027614593506,
"faceGenderMale": 5.1189422607421875,
"faceGlasses": 0.9999644756317139,
"landmarks": [
{ "x": 0.40930742025375366, "y": 0.2873525619506836 }, // 0: side-specific landmark; swaps with 1 on horizontal flip
{ "x": 0.6650311350822449, "y": 0.26288726925849915 }, // 1: side-specific landmark; swaps with 0 on horizontal flip
{ "x": 0.5250742435455322, "y": 0.3583020269870758 }, // 2: centerline/non-swapped landmark
{ "x": 0.47323837876319885, "y": 0.28001296520233154 }, // 3: eye landmark; swaps with 4 on horizontal flip
{ "x": 0.5855494737625122, "y": 0.2726733684539795 }, // 4: eye landmark; swaps with 3 on horizontal flip
{ "x": 0.5319857001304626, "y": 0.42925146222114563 }, // 5: centerline/non-swapped landmark
{ "x": 0.4922448694705963, "y": 0.4537167251110077 }, // 6: mouth-corner landmark; swaps with 7 on horizontal flip
{ "x": 0.576910138130188, "y": 0.4537167251110077 }, // 7: mouth-corner landmark; swaps with 6 on horizontal flip
{ "x": 0.5339804887771606, "y": 0.5705606341362 }, // 8: centerline/non-swapped landmark
{ "x": 0.526802122592926, "y": 0.23108233511447906 } // 9: forehead center / bindi-area reference; non-swapped
],
"x1": 0.40239596366882324,
"y1": 0.14056065678596497,
"x2": 0.6788541078567505,
"y2": 0.532005786895752
}
]
}
}
Dust
Dust uses the same parameter style as Fabric. Use Alpha1 for blend strength, Scale for dirt size, and optional Layer when you want a separate layer in ZIP output.
{
"Plugin": "Dust",
"Scale": 2,
"Alpha1": 1.0
}
Color Correction
Color Correction uses nested User Params. For AI-guided correction, set Basic.Enable Basic to true and choose AI.Color Correction Mode: None, Exposure only, Exposure & WB, or Full.
Defaults when parameters are omitted: the worker resets Color Correction for every task. IfUser Params,AI, orBasicvalues are omitted,AI.Color Correction ModeisNoneandBasic.Enable Basicisfalse. A flattened Color Correction task with no other enabled sections therefore leaves the pixels unchanged. AI adjustments are merged only when Basic is enabled. WithLayer: 1, the worker still runs prediction and writesAI predictedsettings toresult.json, but does not return an image layer.
{
"Plugin": "Color Correction",
"User Params": {
"AI": {
"Color Correction Mode": "Full"
},
"Basic": {
"Enable Basic": true
}
}
}
Face Lifting
Face Lifting uses named parameters rather than Alpha1 and Alpha2. Run Face Detection first to reuse its metadata.faces, or pass the same normalized face objects directly in faces. Use face_lifting_version: 3 for all new integrations.
{
"Plugin": "Face Lifting",
"face_lifting_version": 3,
"Face Lifting": 1.0,
"Face Masculinity": 0.0,
"Face Femininity": 0.0,
"Double Chin Correction": 0.0,
"Hair Volume": 0.0,
"Face Lifting Depth": 0.6,
"Flow Smoothing": 0.1,
"Layer": 0
}
| Exact field name | Accepted alias | Default | Range and behavior |
|---|---|---|---|
face_lifting_version |
- | Required by validation | Integer >= 2. Use exactly 3; it returns five fields per face. The server does not enforce an upper bound, but higher undocumented versions must not be sent. |
faces |
metadata.faces |
Prior Face Detection metadata | Array in input face order. Each face needs at least five normalized landmarks. null returns a 1x1 empty layer; a missing value is an error unless Face Detection ran earlier. |
face_lifting |
Face Lifting |
1.0 |
Beauty/lifting field strength. Recommended 0..1; numeric range is not validated by the server. |
face_masculinity |
Face Masculinity |
0.0 |
Masculinity field strength. Recommended 0..1; not server-validated. |
face_femininity |
Face Femininity |
0.0 |
Femininity field strength. Recommended 0..1; not server-validated. |
double_chin_correction |
Double Chin Correction |
0.0 |
Double-chin field strength. Recommended 0..1; not server-validated. |
hair_volume |
Hair Volume |
0.0 |
Hair-volume field strength. Recommended 0..1; not server-validated. |
face_lifting_depth |
Face Lifting Depth |
0.5 |
Global multiplier for all five fields in flattened output. Recommended 0..1; not server-validated. |
flow_smoothing |
Flow Smoothing |
0.1 |
Smooths every 256x256 flow field independently before flattened rendering. Recommended 0..1; not server-validated. |
Layer |
- | 0 |
0 returns a finished image. 1 returns the encoded flow roll and placement metadata in ZIP output. |
Strength values are linear. For an individual field, 0 excludes that deformation, 0.5 contributes half of it, and 1 contributes the full predicted field. Face Lifting Depth then multiplies every selected field: effective strength = field strength * Face Lifting Depth. With the defaults, Face Lifting is 1.0 * 0.5 = 0.5, while the other four fields are disabled.
Face Masculinity and Face Femininity are not mutually exclusive. If both are non-zero, their displacements are added; the worker does not normalize or choose between them. In flattened mode, one task-level set of strengths is applied identically to every face in faces. Use the v3 layered output when a group portrait needs different strengths for different people.
Flow Smoothing controls spatial smoothing, not effect strength. The worker computes radius = max(1.01, Flow Smoothing * 10 + 1.99) and smooths each field separately. The default 0.1 produces a radius of 2.99; 0 still produces 1.99, so it is not a literal no-smoothing switch.
With Layer: 1, the worker returns the full predicted flow layer. The numeric strength and smoothing fields do not alter that returned layer and Face Lifting is not flattened into the working image. Apply per-face strengths and smoothing in the client renderer.
Face Lifting flow layer version 3
Version 3 is designed for non-destructive editing and group portraits. The layer is a vertical flow atlas: every face occupies five consecutive 256x256 cells, and faces retain the order of the input faces array. For N faces, the PNG is 256 x (256 * 5 * N) pixels.
Face Lifting does not use a LayoutMode field. Its layered equivalent of compact patch output is this 256-pixel-wide flow atlas plus flowMatrices. Glasses Anti Glare uses the explicit LayoutMode: "patch" setting.
Two-face layered patch example
This live Cloud API example uses a two-person wedding portrait with well-separated faces. One request runs Face Detection, returns Face Lifting v3 as a flow atlas, and returns Glasses Anti Glare with LayoutMode: "patch". The detector found exactly two faces: face 0 is the man on the left, and face 1 is the woman on the right. The downloadable layers and full result.json all come from this source image.
3437x5155 lossless WebP. Download source
256x2560 PNG: five 256x256 fields for face 0, followed by five for face 1. Download layer
1024x1024 PNG: face 0 is the top half and face 1 is the bottom half. Neither person wears glasses, so this example demonstrates patch layout and face ordering rather than visible glare removal. Download layer
Download request JSON | Download full result.json | Download layered ZIP
Show two-face layered request JSON
{
"mode": "professional",
"outputFormat": "zip",
"tasks": [
{
"Plugin": "Face Detection"
},
{
"Plugin": "Face Lifting",
"Layer": 1,
"face_lifting_version": 3
},
{
"Plugin": "Glasses Anti Glare",
"Layer": 1,
"LayoutMode": "patch",
"Glasses Glare Removal": 1.0
}
]
}
| Cell within face | Atlas slot | Field | Placement matrix |
|---|---|---|---|
0 | 5 * faceIndex + 0 | Face Lifting | flowMatrices[0] |
1 | 5 * faceIndex + 1 | Face Masculinity | flowMatrices[1] |
2 | 5 * faceIndex + 2 | Face Femininity | flowMatrices[2] |
3 | 5 * faceIndex + 3 | Double Chin Correction | flowMatrices[3] |
4 | 5 * faceIndex + 4 | Hair Volume | flowMatrices[4] |
The first three fields use the face alignment transform, so their three matrices are normally identical. Double chin and hair use their own alignment transforms. result.json stores one faces[i] entry for each input face and its five matrices in the same field order.
Flow encoding: the useful values are the PNG red and green channels; PNG serialization writes a zero blue channel. The neutral value is 0.5. The worker encodes each predicted component as encoded = raw * 0.125 + 0.5, so raw = (encoded - 0.5) * 8. Read image channels as normalized 0..1 values before applying this formula.
Coordinates: each 2x3 flowMatrices[k] maps an image position in pixel coordinates to the corresponding 256x256 patch position. After sampling a flow, multiply the patch-space displacement by the inverse 2x2 linear part of that matrix to obtain an image-pixel displacement. If the output is rendered at the original originalWidth/originalHeight, use the matrix as returned. For a uniform resize that preserves aspect ratio, the reference renderer scales the matrix by originalWidth / outputWidth first.
Before rendering a layered flow, smooth every 256x256 atlas cell independently with the same radius formula described above. Do not blur across cell or face boundaries.
Show full result.json for this two-face example
{
"jsonVersion": "1",
"plugins": [
{
"metadata": {
"faces": [
{
"confidence": 0.8781260251998901,
"faceAge": 53.63501739501953,
"faceGenderFemale": -5.483673095703125,
"faceGenderMale": 5.483585357666016,
"faceGlasses": 0.0015793441561982036,
"landmarks": [
{
"x": 0.5217267274856567,
"y": 0.1506756842136383
},
{
"x": 0.7493013143539429,
"y": 0.13328248262405396
},
{
"x": 0.6517693400382996,
"y": 0.20829065144062042
},
{
"x": 0.5883735418319702,
"y": 0.16480764746665955
},
{
"x": 0.6891565918922424,
"y": 0.15176275372505188
},
{
"x": 0.6582714319229126,
"y": 0.22677092254161835
},
{
"x": 0.6030033826828003,
"y": 0.23438045382499695
},
{
"x": 0.7086629271507263,
"y": 0.21916139125823975
},
{
"x": 0.6715611219406128,
"y": 0.2893660068511963
},
{
"x": 0.6322629451751709,
"y": 0.1430661529302597
}
],
"x1": 0.505471408367157,
"x2": 0.7655566334724426,
"y1": 0.09740901738405228,
"y2": 0.2713409960269928
},
{
"confidence": 0.9013141393661499,
"faceAge": 26.086315155029297,
"faceGenderFemale": 3.2640063762664795,
"faceGenderMale": -3.263772964477539,
"faceGlasses": 2.816226754021045e-07,
"landmarks": [
{
"x": 0.278599351644516,
"y": 0.2978019416332245
},
{
"x": 0.49399930238723755,
"y": 0.33642369508743286
},
{
"x": 0.38786017894744873,
"y": 0.36356329917907715
},
{
"x": 0.3457167148590088,
"y": 0.31137174367904663
},
{
"x": 0.4502949118614197,
"y": 0.33224838972091675
},
{
"x": 0.3784949779510498,
"y": 0.38443994522094727
},
{
"x": 0.3254254162311554,
"y": 0.38026461005210876
},
{
"x": 0.4081515073776245,
"y": 0.3969659209251404
},
{
"x": 0.34727755188941956,
"y": 0.43454378843307495
},
{
"x": 0.4097123146057129,
"y": 0.3092840909957886
}
],
"x1": 0.2583080530166626,
"x2": 0.5080471038818359,
"y1": 0.26961851119995117,
"y2": 0.4366315007209778
}
]
},
"operationIndex": 0,
"pluginName": "Face Detection"
},
{
"blendingMode": "flow",
"faceLiftingVersion": 3,
"faces": [
{
"flowCount": 5,
"flowMatrices": [
[
[
0.1669235027996616,
-0.03240559662592008,
-212.0281041984988
],
[
0.03240559662592008,
0.1669235027996616,
-87.3473323491277
]
],
[
[
0.1669235027996616,
-0.03240559662592008,
-212.0281041984988
],
[
0.03240559662592008,
0.1669235027996616,
-87.3473323491277
]
],
[
[
0.1669235027996616,
-0.03240559662592008,
-212.0281041984988
],
[
0.03240559662592008,
0.1669235027996616,
-87.3473323491277
]
],
[
[
0.14263528162444472,
-0.0276904170079256,
-162.55228047057994
],
[
0.0276904170079256,
0.14263528162444472,
-81.17723776256105
]
],
[
[
0.07406062699730784,
-0.014377716523345985,
-22.86368409049345
],
[
0.014377716523345985,
0.07406062699730784,
-3.995873453637472
]
]
]
},
{
"flowCount": 5,
"flowMatrices": [
[
[
0.15319502661695983,
0.04586840131253076,
-157.65503005931123
],
[
-0.04586840131253076,
0.15319502661695983,
-71.39446927509431
]
],
[
[
0.15319502661695983,
0.04586840131253076,
-157.65503005931123
],
[
-0.04586840131253076,
0.15319502661695983,
-71.39446927509431
]
],
[
[
0.15319502661695983,
0.04586840131253076,
-157.65503005931123
],
[
-0.04586840131253076,
0.15319502661695983,
-71.39446927509431
]
],
[
[
0.13810352827100214,
0.04134982836779297,
-129.51467518712946
],
[
-0.04134982836779297,
0.13810352827100214,
-76.53987993054201
]
],
[
[
0.07170760121763572,
0.021470103190969425,
-5.709542885624899
],
[
-0.021470103190969425,
0.07170760121763572,
-1.5880145793199034
]
]
]
}
],
"operationIndex": 1,
"originalHeight": 5155,
"originalWidth": 3437,
"pluginName": "Face Lifting"
},
{
"amount": 1.0,
"blendingMode": "linearLight",
"faces": [
{
"glassesAntiGlarePatchMatrix": [
[
1.1520541977358996,
-0.22365336814093756,
-1834.7684191854532
],
[
0.22365336814093756,
1.1520541977358996,
-1213.046920389916
]
],
"patchHeight": 512,
"patchWidth": 1024
},
{
"glassesAntiGlarePatchMatrix": [
[
1.072365186318719,
0.32107880918771536,
-1487.5852104151788
],
[
-0.32107880918771536,
1.072365186318719,
-1121.7612849256604
]
],
"patchHeight": 512,
"patchWidth": 1024
}
],
"glassesAntiGlareVersion": 1,
"layoutMode": "patch",
"operationIndex": 2,
"originalHeight": 5155,
"originalWidth": 3437,
"pluginName": "Glasses Anti Glare"
}
]
}
Verified 60% application: set Face Lifting to 1.0, the other four field strengths to 0, and Face Lifting Depth to 0.6. The effective beauty strength is 0.6. For the encoded PNG, this gives a renderer multiplier of 8 * 0.6 = 4.8.
// Render one face at 60% Face Lifting strength.
const effectiveStrength = 1.0 * 0.6;
const encodedRG = bilinearSample(flowCell, patchPosition); // normalized 0..1
const patchDisplacement = [
(encodedRG[0] - 0.5) * 8 * effectiveStrength,
(encodedRG[1] - 0.5) * 8 * effectiveStrength
];
const imageDisplacement = multiplyByInverseLinear2x2(
flowMatrices[0],
patchDisplacement
);
const sourcePixel = add(outputPixel, imageDisplacement);
const resultPixel = bilinearSample(sourceImage, sourcePixel);
For multiple active fields, repeat the sample with its matching cell and matrix, convert each displacement back to image pixels, and add the displacements before sampling the source image. Render faces in array order, using the five cells and the faces[i].flowMatrices entry for the same faceIndex.
Apply Face Lifting in Python: before and after
This comparison was rendered locally by the Python script below from the downloadable Cloud API flow atlas and result.json. Face 0 is the man on the right: 30% Face Lifting, 60% Face Masculinity, Hair Volume off. Face 1 is the woman on the left: 60% Face Lifting, 30% Face Femininity, 30% Hair Volume. Double Chin Correction and Glasses Anti Glare are not applied. Both sides use the same source photograph; no Heal or Dodge Burn is applied in this comparison.
Download Python-rendered result PNG | Download complete Python script
Put the script, source WebP, flow PNG, and result JSON in one directory, then run:
python -m pip install numpy opencv-python
python apply_face_lifting.py --blur-radius 5
Edit FACE_SETTINGS in the script to choose strengths per face. Each entry has a depth multiplier and five field strengths in this order: beauty, masculinity, femininity, double chin, hair volume. Both depths are 0.6. For face 0, (0.5, 1.0, 0, 0, 0) gives effective beauty 0.3 and masculinity 0.6, with hair disabled. For face 1, (1.0, 0, 0.5, 0, 0.5) gives effective beauty 0.6, femininity 0.3, and hair 0.3. These hardcoded indices refer only to this photograph; inspect the returned faces and assign settings again for other images. This is client-side configuration, not an additional API request parameter.
The example preserves 8-bit or 16-bit PNG precision and reads red/green as numeric data. It uses an ordinary OpenCV box blur with radius 5 (an 11 x 11 window) on each flow cell separately, never across cell boundaries. This radius is measured in 256x256 flow-cell pixels, not source-image pixels. It intentionally differs from the server's IIR filter and is not equivalent to an API Flow Smoothing value. Set --blur-radius 0 to disable this local blur.
Each field uses its own matrix, including the separate hair transform. The script sums displacements in image coordinates before sampling once per face, then processes faces in array order. Changing settings reuses the saved layers without another API request. The source must match the metadata dimensions exactly; output is lossless PNG. Different smoothing and PNG flow quantization can produce differences from server-flattened output.
Show complete Python rendering script
"""Apply a Cloud API Face Lifting v3 atlas locally. Requires numpy and opencv-python."""
import argparse
import json
from pathlib import Path
import cv2
import numpy as np
def smooth_cell(cell, radius=5):
"""Box blur in flow-cell pixels; radius 5 means an 11x11 window."""
if radius < 0 or int(radius) != radius:
raise ValueError('Blur radius must be a non-negative integer')
size = 2 * int(radius) + 1
return cv2.blur(cell, (size, size), borderType=cv2.BORDER_REFLECT_101)
# These indices belong ONLY to the downloadable example, not arbitrary photos.
# Field order: beauty, masculinity, femininity, double chin, hair volume.
FACE_SETTINGS = {
0: {'depth': 0.6, 'fields': (0.5, 1.0, 0.0, 0.0, 0.0)}, # Man, right; hair off.
1: {'depth': 0.6, 'fields': (1.0, 0.0, 0.5, 0.0, 0.5)}, # Woman, left.
}
# Effective coefficients: man beauty 0.3, masculinity 0.6, hair 0;
# woman beauty 0.6, femininity 0.3, hair 0.3.
def bilinear(image, x, y):
"""Sample using array indices: (0, 0) is the first pixel's center."""
x = np.clip(x, 0, image.shape[1] - 1)
y = np.clip(y, 0, image.shape[0] - 1)
x0, y0 = np.floor(x).astype(int), np.floor(y).astype(int)
x1 = np.minimum(x0 + 1, image.shape[1] - 1)
y1 = np.minimum(y0 + 1, image.shape[0] - 1)
dx, dy = (x - x0)[..., None], (y - y0)[..., None]
top = image[y0, x0] * (1 - dx) + image[y0, x1] * dx
bottom = image[y1, x0] * (1 - dx) + image[y1, x1] * dx
return top * (1 - dy) + bottom * dy
def apply_lifting(source, atlas_rg, plugin, face_settings=None, blur_radius=5):
"""Sum selected fields per face, then render faces in array order."""
height, width = source.shape[:2]
if (width, height) != (plugin['originalWidth'], plugin['originalHeight']):
raise ValueError('Use the source at the original metadata dimensions')
faces = plugin['faces']
if plugin.get('faceLiftingVersion') != 3:
raise ValueError('This example requires Face Lifting version 3')
if atlas_rg.shape != (256 * 5 * len(faces), 256, 2):
raise ValueError('Atlas dimensions do not match five fields per face')
face_settings = FACE_SETTINGS if face_settings is None else face_settings
if set(face_settings) != set(range(len(faces))):
raise ValueError('Provide explicit settings for every face index')
output = source.astype(np.float32).copy()
for index, face in enumerate(faces):
if face['flowCount'] != 5 or len(face['flowMatrices']) != 5:
raise ValueError('Each face must have five fields and matrices')
settings = face_settings[index]
weights = np.asarray(settings['fields'], dtype=np.float32) * settings['depth']
if weights.shape != (5,) or not np.isfinite(weights).all():
raise ValueError('Provide five finite field strengths and a finite depth')
fields = []
for field_index, weight in enumerate(weights):
if weight == 0:
continue
matrix = np.asarray(face['flowMatrices'][field_index], dtype=np.float32)
inverse = np.linalg.inv(matrix[:, :2])
# PNG channels are data: do not apply an sRGB transfer function.
first_row = (index * 5 + field_index) * 256
cell = smooth_cell(atlas_rg[first_row:first_row + 256], blur_radius)
fields.append((matrix, inverse, cell, weight))
if not fields:
continue
previous = output.copy()
# Stripes bound temporary memory use for large source photographs.
for top in range(0, height, 128):
bottom = min(top + 128, height)
yy, xx = np.mgrid[top:bottom, :width].astype(np.float32)
# Match DisplacementFlow311 destination pixel centers.
dest_x, dest_y = xx + 0.5, yy + 0.5
dx, dy = np.zeros_like(xx), np.zeros_like(yy)
active = np.zeros(xx.shape, dtype=bool)
for matrix, inverse, cell, weight in fields:
px = matrix[0, 0] * dest_x + matrix[0, 1] * dest_y + matrix[0, 2]
py = matrix[1, 0] * dest_x + matrix[1, 1] * dest_y + matrix[1, 2]
inside = (px >= 1) & (px <= 255) & (py >= 1) & (py <= 255)
# Flow shader adds 0.5 before sampling: px/py are array indices here.
displacement = (bilinear(cell, px, py) - 0.5) * (8.0 * weight)
dx += np.where(inside, inverse[0, 0] * displacement[..., 0] + inverse[0, 1] * displacement[..., 1], 0)
dy += np.where(inside, inverse[1, 0] * displacement[..., 0] + inverse[1, 1] * displacement[..., 1], 0)
active |= inside
# Add image-space displacements BEFORE resampling, not separate image warps.
sx, sy = dest_x + dx, dest_y + dy
active &= (sx >= 0) & (sx <= width - 1) & (sy >= 0) & (sy <= height - 1)
sampled = bilinear(previous, sx - 0.5, sy - 0.5)
output[top:bottom] = np.where(active[..., None], sampled, previous[top:bottom])
return output
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', default='Face_Lifting_Group_Portrait_Source.webp')
parser.add_argument('--flow', default='Face_Lifting_Group_Portrait_Flow_v3.png')
parser.add_argument('--metadata', default='Face_Lifting_Group_Portrait_Result.json')
parser.add_argument('--output', default='Face_Lifting_Group_Portrait_After.png')
parser.add_argument('--blur-radius', type=int, default=5)
args = parser.parse_args()
source = cv2.imread(args.source, cv2.IMREAD_COLOR)
atlas = cv2.imread(args.flow, cv2.IMREAD_UNCHANGED)
if source is None or atlas is None:
raise ValueError('Could not read source or flow image')
if atlas.dtype not in (np.uint8, np.uint16) or atlas.ndim != 3 or atlas.shape[2] < 3:
raise ValueError('Expected an 8-bit or 16-bit RGB flow PNG')
# OpenCV loads BGR. Preserve PNG bit depth and select red then green.
rg = atlas[..., [2, 1]].astype(np.float32) / np.iinfo(atlas.dtype).max
metadata = json.loads(Path(args.metadata).read_text(encoding='utf-8'))
candidates = [p for p in metadata['plugins'] if p['pluginName'] == 'Face Lifting']
if len(candidates) != 1:
raise ValueError('Expected exactly one Face Lifting operation')
result = apply_lifting(source, rg, candidates[0], blur_radius=args.blur_radius)
pixels = np.clip(np.rint(result), 0, 255).astype(np.uint8)
if Path(args.output).suffix.lower() != '.png':
raise ValueError('Use a .png output to avoid lossy preview artifacts')
if not cv2.imwrite(args.output, pixels):
raise OSError('Could not save output')
print(f'Saved {args.output}: {len(candidates[0]["faces"])} faces, box blur radius {args.blur_radius}')
if __name__ == '__main__':
main()
Glasses Anti Glare
Glasses Anti Glare uses the named field Glasses Glare Removal. Provide faces directly or run Face Detection earlier in the task list; missing face data is an error, while faces: null skips the plugin. In flattened mode, you can omit Layer. In layered mode, LayoutMode controls the returned layer format: patch returns compact face patches with placement metadata, and full returns a full-size overlay.
{
"Plugin": "Glasses Anti Glare",
"Glasses Glare Removal": 1.0,
"LayoutMode": "patch",
"Layer": 1
}
With Layer: 1 and LayoutMode: "patch", the returned layer is exactly 1024 x (512 * N) pixels for N faces. Each face receives one 1024x512 cell. Cells are stacked from top to bottom in input faces order: face i occupies rows i * 512 through (i + 1) * 512 - 1.
The corresponding result.json entry is faces[i] at the same index. It contains patchWidth: 1024, patchHeight: 512, and glassesAntiGlarePatchMatrix. Use that matrix to place the cell back over the original image; it compensates for the detected face's original size, position, and alignment. The plugin also reports originalWidth and originalHeight for the coordinate space in which the matrix was generated.
See the live two-face layered example for the source portrait, the two-cell glasses atlas, the matching Face Lifting v3 atlas, request JSON, complete result.json, and downloadable ZIP.
Layered Task JSON Example
This sample shows a complete professional-mode task list that requests multiple retouching plugins as separate layers. Each task uses Layer: 1, so the response is expected to contain layered output instead of only a flattened final JPEG.
{
"mode": "professional",
"tasks": [
{
"Plugin": "Clean Backdrop",
"Scale": 0,
"Layer": 1,
"Alpha1": 1,
"Automask": true
},
{
"Plugin": "White Teeth",
"Scale": 0,
"Layer": 1,
"Alpha1": 0.3,
"Alpha2": 0.3
},
{
"Plugin": "Mattifier",
"Scale": 0,
"Layer": 1,
"Alpha1": 0.7,
"Alpha2": 0
},
{
"Plugin": "Dodge Burn",
"Scale": 0,
"Layer": 1,
"Alpha1": 2,
"Alpha2": 0
},
{
"Plugin": "Skin Tone",
"Scale": 0,
"Layer": 1,
"Alpha1": 2,
"Alpha2": 0
},
{
"Plugin": "Portrait Volumes",
"Scale": 0,
"Layer": 1,
"Alpha1": 1,
"Alpha2": 0
}
]
}