Skip to content

Rendering recipes

This guide is paste-and-tune code for drawing the three feature types in advanced visualization data:

Feature typeMetricsRecipe
Spotsacne, freckles, dark spots, hyperpigmentation→ Spots
Polylineswrinkles + sub-region variants→ Polylines
Polygon areasredness, melasma, shine, texture, uneven skin tone, skin sagging, pore dilation→ Polygon areas

The renderers below are vanilla Canvas 2D, no dependencies. Same iteration shape, same render rules, simplified to the essentials of what works for these data types.

Every advanced-visualization render starts with the same nested iteration:

// 1. Find the metric in results[].
const finding = response.results.find((r) => r.description === "redness");
if (!finding) return;
// 2. Each measurement_locations[] entry can carry visualization_data[].
for (const loc of finding.measurement_locations ?? []) {
// 3. Sort features ascending by intensity before drawing.
// Higher-intensity polygons sit INSIDE lower-intensity ones —
// drawing low → high gives correct overpainting.
const features = (loc.visualization_data ?? [])
.slice()
.sort((a, b) => (a.intensity ?? 0) - (b.intensity ?? 0));
for (const f of features) {
// mean_intensity is the canonical color-decision input;
// fall back to intensity if absent.
const colorIntensity = f.mean_intensity ?? f.intensity;
drawFeature(ctx, f, colorIntensity);
}
}

Three rules from this loop:

  1. Sort features ascending by intensity before drawing. Required for area metrics, harmless for spot/line. Keeps render order deterministic.
  2. Prefer mean_intensity ?? intensity for color decisions — mean_intensity is more stable than the raw per-coordinate intensity on multi-point features.
  3. Stay in original-image pixel space. All x / y arrays are in input-image coordinates. Render onto a canvas of the same dimensions as the source image, no transform required.

A spot feature is a single point (length: 1, x and y are length-1 arrays, intensity is a number).

/**
* Draw a single spot — used for acne, freckles, dark spots,
* hyperpigmentation.
*
* `style` controls the marker shape:
* "dot" — two concentric circles (recommended for acne, freckles)
* "box" — square outline (dark spots / hyperpigmentation)
*/
function drawSpot(ctx, feature, intensity, { style, color, scale = 1 }) {
const x = feature.x[0];
const y = feature.y[0];
ctx.save();
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = 2 * scale;
if (style === "dot") {
// Inner solid disc + outer stroke ring.
ctx.beginPath();
ctx.arc(x, y, 6 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(x, y, 12 * scale, 0, Math.PI * 2);
ctx.lineWidth = 2 * scale;
ctx.stroke();
} else if (style === "box") {
const size = 18 * scale;
ctx.lineWidth = 4 * scale;
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
} else if (style === "cross") {
const size = 14 * scale;
ctx.lineWidth = 4 * scale;
ctx.beginPath();
ctx.moveTo(x - size / 2, y - size / 2);
ctx.lineTo(x + size / 2, y + size / 2);
ctx.moveTo(x + size / 2, y - size / 2);
ctx.lineTo(x - size / 2, y + size / 2);
ctx.stroke();
}
ctx.restore();
}

Marker size is fixed in viewBox-pixel terms (6/12 px for dots, 18 px for boxes, 14 px for crosses are workable defaults); pass a scale if you’re rendering on a downsized canvas.

A polyline feature is a connected line — one wrinkle, one fold. x and y are length-N arrays of points along the line. intensity is an array of length N (per-point intensity), and length_weighted_intensity is the canonical “how bad is this wrinkle” composite.

const FILTER_STEP = 3;
const FILTER_THRESHOLD = 20;
/**
* Decimate dense point arrays for performance — wrinkles can carry
* hundreds of points along a single line. Filtering every Nth point
* above a threshold gives a visually identical render at a fraction
* of the cost.
*/
function decimate(arr, step = FILTER_STEP, threshold = FILTER_THRESHOLD) {
if (arr.length <= threshold) return arr;
return arr.filter((_, i) => i % step === 0 || i === arr.length - 1);
}
/**
* Draw a polyline — used for wrinkles and its sub-region variants
* (nasolabial folds, marionette lines, etc.).
*/
function drawPolyline(ctx, feature, intensity, { color, strokeWidth = 4, scale = 1 }) {
const xs = decimate(feature.x);
const ys = decimate(feature.y);
if (xs.length < 2) return;
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = strokeWidth * scale;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(xs[0], ys[0]);
for (let i = 1; i < xs.length; i++) ctx.lineTo(xs[i], ys[i]);
ctx.stroke();
ctx.restore();
}

For ranking wrinkles (e.g., “show me the worst 5”), sort the features by length_weighted_intensity descending — that composite is built specifically for this use.

A polygon-area feature is an enclosed region whose intensity is a categorical integer indexing levels, counting up from 1. The top of the range varies by metric — 1..2 up to 1..9 — see the level table. The response carries one feature per level.

For most of these metrics the levels are increasing thresholds on one field, so higher levels sit inside lower ones and layered fills read as a gradient. shine (1..4) and skin_type (1..9) are different: their levels come from separate source fields, so treat the integer as a label and give each level its own colour rather than a position on one ramp.

/**
* Draw a polygon-area feature — used for redness, melasma, shine, skin type,
* texture, uneven skin tone, skin sagging, pore dilation.
*
* Caller provides a colorForLevel(intensity) → CSS color function.
* For the threshold-based metrics, higher levels should render denser/darker.
* For shine and skin type, map each level to its own colour instead.
*/
function drawPolygonArea(ctx, feature, intensity, { colorForLevel, opacity = 0.45 }) {
const xs = feature.x;
const ys = feature.y;
if (xs.length < 3) return;
ctx.save();
ctx.fillStyle = colorForLevel(intensity);
ctx.globalAlpha = opacity;
ctx.beginPath();
ctx.moveTo(xs[0], ys[0]);
for (let i = 1; i < xs.length; i++) ctx.lineTo(xs[i], ys[i]);
ctx.closePath(); // polygon types are closed shapes — connect last to first
ctx.fill();
ctx.restore();
}

Because the shared loop sorts features ascending by intensity, the highest level naturally lands on top. With opacity ≈ 0.45, the layered fill produces a heatmap-like gradient — outer (low) areas show through, inner (high) areas dominate. For shine and skin_type, keep the sort for deterministic draw order but pick colorForLevel per level rather than as a ramp, since their levels aren’t a severity sequence.

function renderMetric(ctx, response, metric, options) {
const finding = response.results.find((r) => r.description === metric);
if (!finding) return;
for (const loc of finding.measurement_locations ?? []) {
const features = (loc.visualization_data ?? [])
.slice()
.sort((a, b) => (a.intensity ?? 0) - (b.intensity ?? 0));
for (const f of features) {
const intensity = f.mean_intensity ?? f.intensity;
options.draw(ctx, f, intensity, options);
}
}
}
// Spot metrics
renderMetric(ctx, response, "acne", {
draw: drawSpot,
style: "dot",
color: "#FF5544",
});
renderMetric(ctx, response, "wrinkles", {
draw: drawPolyline,
color: "#7B3F00",
strokeWidth: 4,
});
renderMetric(ctx, response, "redness", {
draw: drawPolygonArea,
colorForLevel: (level) => ["#FFE0E0", "#FF8888", "#FF3333"][level - 1] ?? "#FF3333",
opacity: 0.45,
});
  • Server-side / Python. Use OpenCV (cv2.fillPoly, cv2.polylines, cv2.circle) or matplotlib.pyplot — both are workable for batch rendering and report generation.
  • PixiJS / WebGL. Revieve’s VTO product renders these features in real-time using PixiJS. The iteration logic in this guide carries over — replace Canvas API calls with PIXI.Graphics equivalents (drawCircle, drawPolygon, lineTo).
  • SVG. Build a <polygon>, <polyline>, or <circle> element per feature inside an <svg viewBox="0 0 W H"> matching the input image. Same iteration; different output target.
  • Native (iOS / Android). No first-party recommendation from Revieve at this time — partners typically use the platform’s native graphics APIs (Core Graphics on iOS, Canvas on Android) with the same iteration logic.
  • Visualization data — the three-tier model and feature-shape reference.
  • Masks — the tier-2 region-boundary format and its drawing program.
  • Sub-area divisions — face-region splits used by measurement_locations[].