Interpreting results
The Skin Analysis response is one JSON document with five top-level fields: message, face_area, inverse_color_balance_rb, status[], and results[]. This page explains each and how to render it.
The shape
Section titled “The shape”{ "message": "OK" | "Warning" | "Error", "face_area": { "bounding_box": [x_min, y_min, x_max, y_max], "rotation": -2.4 }, "inverse_color_balance_rb": [1.05, 0.98, 1.02], "status": [ { "idx": 308, "description": "Image is overexposed at the face area. Find more diffuse lighting or reduce exposure.", "isError": 0 } ], "results": [ { "description": "redness", "value": 0.23, "value_raw": 3.21, // deprecated "message": "Ok", "local_warnings": [], "measurement_locations": [ /* per-region scores; advanced visualization features live here, on each location's own visualization_data[] */ ], "sum_measures": [/* derived/aggregate */], "visualization_data": null // always null at finding level — see below } ]}message — the global verdict
Section titled “message — the global verdict”The first thing your UI should branch on:
OK— clean run, results are reliable.Warning— results returned, butstatus[]flags one or more quality issues. Show the score, optionally nudge the user to retry for better fidelity.Error— no usable results.status[]explains why; show that and ask for a new image.
face_area — geometry
Section titled “face_area — geometry”bounding_box is [x_min, y_min, x_max, y_max] in pixels of the original image. rotation is degrees clockwise from vertical. Use them when overlaying landmarks or masks back onto the source frame.
status — quality codes
Section titled “status — quality codes”Each entry has:
idx— a numeric code (see Status codes for the full reference).description— human-readable.isError—1blocks usable results,0is informational.
Common codes:
idx | Meaning | Treatment |
|---|---|---|
| 100 | Image not in RGB color format | Surface, ask for retry |
| 101 | Image resolution too small | Surface, ask for retry |
| 102 | Image aspect ratio too extreme | Surface, ask for retry |
| 103/104 | Image too dark / overexposed globally | Optional nudge |
| 201 | No face detected | Surface, ask for retry |
| 308 | Face area overexposed | Optional nudge |
| 401 | Skin masking failure — acne/hyperpigmentation results unreliable | Internal log; usable results |
The full list lives in the API reference.
results[] — per-component findings
Section titled “results[] — per-component findings”Each entry corresponds to one component you requested.
value vs value_raw
Section titled “value vs value_raw”value is normalized to 0..1 for nearly every metric — what you should display. value_raw is the pre-normalization value and is deprecated: ignore it for new integrations.
skin_age and skin_type are exceptions: they don’t populate the top-level value at all — their integer result is carried in sum_measures (see below).
measurement_locations[]
Section titled “measurement_locations[]”Where on the face the score was computed. Each entry includes:
- A
description("left cheek","forehead","Global", …). - A
valuefor that region. - AOI (area of interest) coordinates:
AOI_x_o,AOI_y_o,AOI_radius_o(in original image pixels). ut_zone—"u-zone"|"t-zone"|"none"|"mixed".- Optional
mask_shapesfor visualization (polygons / ellipses).
sum_measures[]
Section titled “sum_measures[]”Aggregated or categorical sub-findings. Shape varies by metric:
- Makeup → face shape (1–6), eye color (1–3), skin undertone (−1 / 0 / +1), foundation RGB, Fitzpatrick type.
- Hair → color array, volume (0–1), type (0–4), frizziness (0–1).
- Skin Age → integer 0–100.
- Skin Type → integer 1–8 (normal, dry, oily, combination, sensitive variants).
visualization_data[]
Section titled “visualization_data[]”Optional per-metric advanced visualization — the third tier of visualization data (the first two being AOI circles and mask shapes). Returned only when the corresponding <metric>_visualization opt-in is in your ?components=... query (e.g. acne_normalized_visualization, wrinkles_visualization, redness_visualization).
The field appears at two levels, and only one of them is ever populated: read it from each measurement_locations[] entry. The finding-level visualization_data is always null — don’t look for features there.
const features = (finding.measurement_locations ?? []).flatMap( (loc) => loc.visualization_data ?? []);Each feature is one of three implicit types — determined by the metric, not by a kind field:
- Spot — single point with intensity. Used by acne, freckles, dark spots, hyperpigmentation.
length: 1,x[0]/y[0]is the centroid. - Polyline — connected line of N points (one wrinkle, one fold). Used by wrinkles.
length: N,intensityis an array of per-point intensities;length_weighted_intensityis the canonical “how bad is this wrinkle” composite. - Polygon area — enclosed region whose
intensityis a level index (1,2,3, …), not a normalized score. Used by redness, melasma, shine, skin type, texture, uneven skin tone, pore dilation. For the threshold-based metrics higher levels nest inside lower ones;shineandskin_typedraw their levels from separate source fields and don’t nest.skin_saggingalso uses polygons but carries a continuous intensity instead of levels. See Visualization data for per-metric ranges.
All coordinates are in original-image pixel space. For paste-ready render code, see Rendering recipes.
inverse_color_balance_rb
Section titled “inverse_color_balance_rb”A 3-element array — not [r, g, b]. The elements are:
| Index | Meaning |
|---|---|
[0] | inverse red white-balance multiplier (1 / red_correction) |
[1] | inverse blue white-balance multiplier (1 / blue_correction) |
[2] | inverse luminance factor (1 / luminance_correction) |
Green is the reference channel (multiplier 1), so it has no entry — hence the rb (“red/blue”) in the name, plus the luminance term. When white balance can’t be estimated the array is [1, 1, 1] (a no-op).
What it’s for
Section titled “What it’s for”The analysis runs in a white-balanced (“true colour”) space — the engine internally removes the image’s colour cast before measuring. These factors are the inverse of that correction, so they let you take a true-colour RGB value and re-apply the original image’s colour cast. The point is efficiency: instead of colour-correcting the whole user photo (expensive, and a global approximation can make the photo itself look off), you leave the photo untouched and only re-bias the handful of colours you draw on top of it, so they sit naturally on the original image.
So use it when you composite a CV-measured colour (e.g. a foundation/skin-tone swatch the API returned in true-colour space) onto the original, uncorrected photo. Apply per channel:
const [iR, iB, iL] = response.inverse_color_balance_rb;
// trueColor = [R, G, B] in 0..255 (a colour the API measured in true-colour space)function toImageCast([R, G, B]) { return [ R * iR * iL, // red channel: inverse red WB × luminance G * iL, // green channel: luminance only (green is the reference) B * iB * iL, // blue channel: inverse blue WB × luminance ];}The result is how that colour appears under the photo’s own lighting/cast, so it blends with the un-corrected image.
A minimal renderer
Section titled “A minimal renderer”function summarize(response) { if (response.message === "Error") { return { ok: false, reason: response.status?.[0]?.description }; }
const scores = Object.fromEntries( response.results .filter((r) => typeof r.value === "number") .map((r) => [r.description, Math.round(r.value * 100)]) );
return { ok: true, scores, warnings: response.status ?? [] };}That’s enough for a percentage-bar dashboard. Mask overlays and AR rendering are documented in their respective concept pages.