Masks
Overview
Section titled “Overview”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.
Requesting masks
Section titled “Requesting masks”masks is a flag, not a metric. Add it to your ?components=... query alongside the metrics whose region shapes you want:
?components=masks,redness,wrinklesEach finding’s measurement_locations[] then carries a mask_shapes block. Without masks, you only get the AOI circles.
Two places to look
Section titled “Two places to look”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
valueandsum_measures.redness,texture,uneven_skin_tone,smoothnessandshineuse it for the full sampled skin area;hairuses 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.
The mask shape schema
Section titled “The mask shape schema”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"— allparamsvalues are in the input-image pixel coordinate system (origin top-left, X-right, Y-down).shapes[]— the draw program. Each entry hascolor,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 asshapes[].
Shape types
Section titled “Shape types”Four shape primitives. Each uses a different params layout:
type | params shape | Notes |
|---|---|---|
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. |
color — add or cut
Section titled “color — add or cut”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.
- Buffer A — apply the
shapes[]program. For each shape in order, draw its primitive onto A using the shape’scolor(1 fills with 1, 0 fills with 0). - Buffer B — draw the outer
maskshape (if present). Ifmaskis omitted, treat B as all-ones. Otherwise draw the singlemaskshape onto B with its color. - Final mask = A × B (elementwise).
A pixel ends up
1only 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.
Why most metrics’ masks look the same
Section titled “Why most metrics’ masks look the same”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.
Edge cases
Section titled “Edge cases”- Without
masks, fall back to AOIs. You can still draw approximate regions from each measurement location’sAOI_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 asidx: 401onhyperpigmentationandacne(the metrics that sample masks). Other mask-using metrics instead returnvalue: -1for the affected region rather than a code. Treat as advisory; the rest of the metric’s output is still usable.
Related
Section titled “Related”- Visualization data — the three-tier model and which metrics support which tier.
- Rendering recipes — paste-ready mask renderers for canvas / SVG / WebGL.
- Status codes —
401and other masking diagnostics.