Skip to content

Masks

masks is the second tier of visualization data — between the cheap AOI circles and the rich advanced/concern visualization. It returns the exact region boundary that a metric was measured over, encoded as a small drawing program: an ordered list of shapes to add to (or cut out of) a buffer, optionally followed by an outer mask the result is multiplied against.

It’s the right tier when you need pixel-accurate region rendering — diagnostic dashboards, before/after panels, “highlight the cheek when discussing redness” UX. For markers/spots/lines on top of the photo, use Tier 3 instead. For a coarse “this is roughly the area” zoom, AOI circles are enough.

masks is a flag, not a metric. Add it to your ?components=... query alongside the metrics whose region shapes you want:

?components=masks,redness,wrinkles

Each finding’s measurement_locations[] then carries a mask_shapes block. Without masks, you only get the AOI circles.

mask_shapes appears at two levels, and several metrics populate both:

  • Per measurement location — the boundary of that one region. This is the common case.
  • On the finding itself — a whole-metric boundary, next to value and sum_measures. redness, texture, uneven_skin_tone, smoothness and shine use it for the full sampled skin area; hair uses it for the hair outline.

For hair the finding-level block is the only mask — hair has no measurement_locations at all, so a renderer that walks locations will find nothing. Check both levels.

Each mask_shapes block has two parts:

{
"coordinates": "original", // pixels of the input image
"shapes": [ // ordered draw program
{ "color": 1, "type": "polygon", "params": [[x0, y0], [x1, y1], ...] },
{ "color": 0, "type": "ellipse", "params": [cx, cy, rx, ry, rot, a1, a2] }
],
"mask": { // optional outer mask, multiplied at the end
"color": 1,
"type": "polygon",
"params": [[x0, y0], [x1, y1], ...]
}
}
  • coordinates: "original" — all params values are in the input-image pixel coordinate system (origin top-left, X-right, Y-down).
  • shapes[] — the draw program. Each entry has color, type, params.
  • mask — optional. If present, the final result is multiplied by this single shape (so anything outside it gets zeroed out). It’s a full shape object (color, type, params) — same vocabulary as shapes[].

Four shape primitives. Each uses a different params layout:

typeparams shapeNotes
polygon[[x0, y0], [x1, y1], ...]Guaranteed convex. Connect first point to last explicitly when drawing — the API omits the closing edge.
unrestricted_polygon[[x0, y0], [x1, y1], ...]Not guaranteed convex. Same closing-edge requirement.
ellipse[cx, cy, rx, ry, rotation, startAngle, endAngle]Standard 7-parameter ellipse — see OpenCV’s ellipse function for the canonical interpretation.
polyline[[x0, y0], [x1, y1], ...]A line, not an enclosed area. Don’t connect first to last.

Each shape’s color is an integer:

  • color: 1 — add the shape’s pixels to the buffer (set them to 1).
  • color: 0 — cut the shape’s pixels from the buffer (set them to 0).

Order matters: shapes are applied left-to-right, each on top of the result so far. So [ellipse@1, polygon@0] means “draw the ellipse, then erase the polygon’s interior from it.”

Drawing a mask — the buffer-A / buffer-B walkthrough

Section titled “Drawing a mask — the buffer-A / buffer-B walkthrough”

Two single-channel image buffers A and B, both starting empty (all zeros), both the size of the input image.

  1. Buffer A — apply the shapes[] program. For each shape in order, draw its primitive onto A using the shape’s color (1 fills with 1, 0 fills with 0).
  2. Buffer B — draw the outer mask shape (if present). If mask is omitted, treat B as all-ones. Otherwise draw the single mask shape onto B with its color.
  3. Final mask = A × B (elementwise). A pixel ends up 1 only if both buffer A and buffer B mark it as 1.

Pseudocode:

function buildMask({ shapes, mask }, width, height) {
const A = new Uint8Array(width * height); // zeros
for (const { type, color, params } of shapes) {
drawShape(A, type, params, color);
}
const B = new Uint8Array(width * height).fill(mask ? 0 : 1);
if (mask) {
drawShape(B, mask.type, mask.params, mask.color);
}
const final = new Uint8Array(width * height);
for (let i = 0; i < final.length; i++) final[i] = A[i] && B[i] ? 1 : 0;
return final;
}

drawShape is wherever your renderer’s primitives live — <canvas> 2D context, OpenCV’s polylines / fillPoly / ellipse, an SVG path builder, etc. Python + matplotlib.pyplot or OpenCV is a common choice for server-side / batch rendering.

Most metrics’ mask_shapes start from the same shared origin — the facial skin area — and then add or cut metric-specific regions. So if you compare mask_shapes across redness, texture, and uneven_skin_tone, large parts of their shapes[] arrays will be identical. That’s expected; it’s the shared face-area scaffold.

  • Without masks, fall back to AOIs. You can still draw approximate regions from each measurement location’s AOI_x_o / AOI_y_o / AOI_radius_o — circles around each area of interest. Less precise, much smaller payload.
  • local_warnings[] — a skin-mask sampling failure surfaces as idx: 401 on hyperpigmentation and acne (the metrics that sample masks). Other mask-using metrics instead return value: -1 for the affected region rather than a code. Treat as advisory; the rest of the metric’s output is still usable.