Skip to content

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.

{
"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
}
]
}

The first thing your UI should branch on:

  • OK — clean run, results are reliable.
  • Warning — results returned, but status[] 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.

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.

Each entry has:

  • idx — a numeric code (see Status codes for the full reference).
  • description — human-readable.
  • isError — 1 blocks usable results, 0 is informational.

Common codes:

idxMeaningTreatment
100Image not in RGB color formatSurface, ask for retry
101Image resolution too smallSurface, ask for retry
102Image aspect ratio too extremeSurface, ask for retry
103/104Image too dark / overexposed globallyOptional nudge
201No face detectedSurface, ask for retry
308Face area overexposedOptional nudge
401Skin masking failure — acne/hyperpigmentation results unreliableInternal log; usable results

The full list lives in the API reference.

Each entry corresponds to one component you requested.

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).

Where on the face the score was computed. Each entry includes:

  • A description ("left cheek", "forehead", "Global", …).
  • A value for 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_shapes for visualization (polygons / ellipses).

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).

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, intensity is an array of per-point intensities; length_weighted_intensity is the canonical “how bad is this wrinkle” composite.
  • Polygon area — enclosed region whose intensity is 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; shine and skin_type draw their levels from separate source fields and don’t nest. skin_sagging also 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.

A 3-element array — not [r, g, b]. The elements are:

IndexMeaning
[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).

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.

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.