Silhouette Tracer

Pick any free-for-commercial-use silhouette from the PhyloPic catalogue and watch a RadialLineChart trace its outline — the creature redrawn as a closed polar curve.

Silhouette Tracer

Pick any free-for-commercial-use silhouette from the PhyloPic catalogue and watch a RadialLineChart trace its outline — the creature redrawn as a closed polar curve.

---

.. llms_copy::Silhouette Tracer

.. toc::

A creature as a polar curve

Browse the catalogue of free-for-commercial-use silhouettes from [PhyloPic](https://www.phylopic.org/images), pick one, and the RadialLineChart traces its outline: the elephant's trunk, the stegosaurus's plates, the seahorse's curl — each becomes a closed radial line. Search by name, dial the outline resolution, and toggle fill or smoothing.

.. admonition::Premium (preview) :color: yellow

RadialLineChart is MUI X Premium (preview) — set MUI_X_LICENSE_KEY to remove the watermark.

.. exec::docs.silhouette_tracer.silhouette_tracer :code: false

How the outline is traced

There is no shape data to plot — just a picture. So the whole trace happens client-side, from the pixels, the moment you pick a silhouette:

1. The chosen SVG is drawn to an offscreen <canvas> and its alpha channel read back (the silhouettes are vendored locally, so the canvas isn't tainted and getImageData works). 2. The centroid of the opaque pixels is found. 3. A ray is cast outward from the centroid at every angle, and the distance to the farthest opaque pixel — the boundary — is recorded as r(θ). 4. r(θ) is normalized and plotted as a single closed RadialLineChart line, so the chart literally draws the organism's outline in polar form.

The *Outline resolution* slider is the number of angles sampled; Smooth adds a small moving average and a catmullRom curve; Fill renders the line as a filled area.

.. admonition::Catalogue & licensing :color: blue

All silhouettes are free for commercial use (CC0, Public Domain Mark, CC BY or CC BY-SA — no NonCommercial), fetched from the PhyloPic v2 API and vendored under assets/phylopic/ with a manifest.json. The author and license of the selected image are shown under the preview; CC BY / CC BY-SA works require that attribution.

Source

```python

File: docs/silhouette_tracer/silhouette_tracer.py

import json import os from pathlib import Path

from dash import dcc, html, clientside_callback, Input, Output, State, ALL import dash_mantine_components as dmc from dash_iconify import DashIconify import dash_mui_scheduler as dms

/silhouette-tracer — pick any free-for-commercial-use silhouette from the

PhyloPic catalogue and the RadialLineChart traces its OUTLINE: the chosen

image is rasterized to an offscreen canvas, its centroid found, and a ray is

cast outward at every angle to read the boundary radius r(θ). Plotting r(θ) as

a closed radial line "draws" the organism as a polar curve. All of it (image

load, canvas ray-casting, plotting) runs client-side from the vendored SVGs.

_HERE = Path(__file__).resolve() _MANIFEST = _HERE.parents[2] / "assets" / "phylopic" / "manifest.json" _CATALOG = json.loads(_MANIFEST.read_text()) for _it in _CATALOG: _it["src"] = f"/assets/phylopic/{_it['slug']}.svg"

_LICENSE = os.environ.get("MUI_X_LICENSE_KEY", "") _LIC_COLOR = {"CC0": "teal", "PDM": "green", "CC BY": "blue", "CC BY-SA": "grape"}

TRACE_SX = { "& .MuiChartsRadialGrid-line": {"stroke": "rgba(51, 153, 255, 0.16)"}, "& .MuiChartsRadialAxis-line": {"stroke": "rgba(51, 153, 255, 0.30)"}, "& .MuiChartsRadialAxis-tick": {"stroke": "rgba(51, 153, 255, 0.30)"}, "& text": {"fill": "rgba(40, 90, 150, 0.7)", "fontFamily": "monospace", "fontSize": "10px"}, }

def _thumb(i, item): return dmc.UnstyledButton( dmc.Stack( [ html.Img(src=item["src"], style={"width": "100%", "height": 54, "objectFit": "contain"}), dmc.Text(item["name"], size="9px", ta="center", lineClamp=1, style={"width": "100%"}), ], gap=2, align="center", ), id={"type": "pp-pick", "index": i}, p=6, className="pp-thumb", style={"border": "1px solid var(--mantine-color-gray-3)", "borderRadius": "8px", "width": "100%", "background": "var(--mantine-color-body)"}, )

-------------------------------------------------------------- left: viewer ---

_viewer = dmc.Paper( [ dmc.Group( [ dmc.Text("SILHOUETTE", fw=700, c="#3399ff", style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Badge("PhyloPic", id="pp-license", color="teal", variant="light", styles={"root": {"fontFamily": "monospace"}}), ], justify="space-between", mb="xs", ), html.Div( html.Img(id="pp-preview", src=_CATALOG[0]["src"], style={"maxWidth": "100%", "maxHeight": 260, "objectFit": "contain"}), style={"height": 270, "display": "flex", "alignItems": "center", "justifyContent": "center", "background": "radial-gradient(circle at 50% 45%, #ffffff 0%, #eef4fb 70%, #dde8f5 100%)", "borderRadius": "12px"}, ), dmc.Text(_CATALOG[0]["name"], id="pp-name", fw=700, ta="center", mt="xs"), dmc.Text("", id="pp-attrib", size="xs", c="dimmed", ta="center"), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 320px", "maxWidth": 380}, )

------------------------------------------------------- right: radial trace ---

_trace = dmc.Paper( [ dmc.Text("RADIAL OUTLINE", fw=700, c="#3399ff", style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text("Distance from the silhouette's centre to its edge at every angle, " "drawn as one closed RadialLineChart line — the creature as a polar curve.", size="xs", c="dimmed", mb="xs"), dms.RadialLineChart( id="pp-chart", height=380, licenseKey=_LICENSE, series=[], rotationAxis=[{"data": [0, 360], "min": 0, "max": 360, "tickInterval": [0, 45, 90, 135, 180, 225, 270, 315]}], radiusAxis=[{"min": 0, "max": 100, "position": "none"}], grid={"rotation": True, "radius": True}, hideLegend=True, skipAnimation=True, margin=14, sx=TRACE_SX, ), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 380px", "maxWidth": 520}, )

----------------------------------------------------------------- controls ----

_controls = dmc.Paper( dmc.Group( [ dmc.Stack( [dmc.Text("Outline resolution", size="xs", c="dimmed"), dmc.Slider(id="pp-samples", min=60, max=360, step=30, value=240, marks=[{"value": v, "label": str(v)} for v in (60, 180, 360)], color="blue", style={"width": 220})], gap=4), dmc.Switch(id="pp-fill", label="Fill outline", checked=True, color="blue"), dmc.Switch(id="pp-smooth", label="Smooth", checked=True, color="blue"), dmc.TextInput(id="pp-search", placeholder="Search the catalogue…", leftSection=DashIconify(icon="mdi:magnify"), style={"width": 220}), ], gap="xl", align="flex-end", wrap="wrap", ), p="md", radius="md", withBorder=True, mt="md", )

------------------------------------------------------------------ gallery ----

_gallery = dmc.Paper( [ dmc.Group( [ dmc.Text("CATALOGUE", fw=700, c="#3399ff", style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text(f"{len(_CATALOG)} free-for-commercial-use silhouettes · PhyloPic", size="xs", c="dimmed"), ], justify="space-between", mb="xs", ), dmc.ScrollArea( dmc.SimpleGrid( cols={"base": 3, "xs": 4, "sm": 6, "md": 8}, spacing="xs", children=[_thumb(i, it) for i, it in enumerate(_CATALOG)], ), h=300, type="auto", offsetScrollbars=True, ), ], p="md", radius="md", withBorder=True, mt="md", )

component = dmc.Stack( [ dmc.Group([_viewer, _trace], align="flex-start", gap="xl", wrap="wrap"), _controls, _gallery, dcc.Store(id="pp-catalog", data=_CATALOG), dcc.Store(id="pp-selected", data={"index": 0}), ], gap="md", )

1) Gallery click -> selected index.

clientside_callback( """ function(clicks) { const ctx = window.dash_clientside.callback_context; const tid = ctx.triggered_id; if (!tid || tid.index == null) { return window.dash_clientside.no_update; } return {index: tid.index}; } """, Output("pp-selected", "data"), Input({"type": "pp-pick", "index": ALL}, "n_clicks"), prevent_initial_call=True, )

2) Search -> show/hide thumbnails.

clientside_callback( """ function(q, cat) { cat = cat || []; q = (q || '').trim().toLowerCase(); return cat.map(it => (!q || it.name.toLowerCase().indexOf(q) >= 0) ? {border: '1px solid var(--mantine-color-gray-3)', borderRadius: '8px', width: '100%'} : {display: 'none'}); } """, Output({"type": "pp-pick", "index": ALL}, "style"), Input("pp-search", "value"), State("pp-catalog", "data"), )

3) Trace the selected silhouette's outline (async: load image -> canvas -> ray-cast).

clientside_callback( """ async function(sel, nSamples, fill, smooth, cat) { const nu = window.dash_clientside.no_update; if (!sel || sel.index == null || !cat) { return [nu, nu, nu, nu, nu, nu]; } const item = cat[sel.index]; if (!item) { return [nu, nu, nu, nu, nu, nu]; } const N = nSamples || 240; const emptyRot = [{data: [0, 360], min: 0, max: 360}]; const lic = item.license; const attribTxt = (item.attribution ? item.attribution + ' · ' : '') + item.license + ' · PhyloPic';

const img = new Image(); await new Promise(res => { img.onload = res; img.onerror = res; img.src = item.src; });

const S = 320; const canvas = document.createElement('canvas'); canvas.width = S; canvas.height = S; const ctx = canvas.getContext('2d'); let iw = img.naturalWidth || 0, ih = img.naturalHeight || 0; if (!iw || !ih) { iw = ih = S; } const scale = Math.min(S / iw, S / ih) * 0.9; const w = iw * scale, h = ih * scale; ctx.clearRect(0, 0, S, S); ctx.drawImage(img, (S - w) / 2, (S - h) / 2, w, h); let data; try { data = ctx.getImageData(0, 0, S, S).data; } catch (e) { return [[], emptyRot, item.src, item.name, lic, attribTxt]; } const alpha = (x, y) => (x < 0 || y < 0 || x >= S || y >= S) ? 0 : data[(y * S + x) * 4 + 3];

// centroid of the filled (opaque) pixels let cx = 0, cy = 0, nn = 0; for (let y = 0; y < S; y++) { for (let x = 0; x < S; x++) { if (alpha(x, y) > 40) { cx += x; cy += y; nn++; } } } if (!nn) { return [[], emptyRot, item.src, item.name, lic, attribTxt]; } cx /= nn; cy /= nn;

// cast a ray outward at each angle (0 = up, clockwise) -> boundary radius const angles = [], radii = []; for (let k = 0; k < N; k++) { const v = k / N * 360, a = v * Math.PI / 180; const dx = Math.sin(a), dy = -Math.cos(a); let last = 0; for (let r = 2; r < S; r += 1) { if (alpha(Math.round(cx + dx * r), Math.round(cy + dy * r)) > 40) { last = r; } } angles.push(v); radii.push(last); } let mx = 1; for (let i = 0; i < N; i++) { if (radii[i] > mx) { mx = radii[i]; } } let norm = radii.map(r => r / mx * 100); if (smooth) { const src = norm.slice(), W = 2; for (let i = 0; i < N; i++) { let acc = 0, c = 0; for (let j = -W; j <= W; j++) { acc += src[(i + j + N) % N]; c++; } norm[i] = acc / c; } } const series = [{ data: norm, closePath: true, showMark: false, area: !!fill, curve: smooth ? 'catmullRom' : 'linear', color: '#3399ff', label: item.name, }]; const rot = [{data: angles, min: 0, max: 360, tickInterval: [0, 45, 90, 135, 180, 225, 270, 315]}]; return [series, rot, item.src, item.name, lic, attribTxt]; } """, Output("pp-chart", "series"), Output("pp-chart", "rotationAxis"), Output("pp-preview", "src"), Output("pp-name", "children"), Output("pp-license", "children"), Output("pp-attrib", "children"), Input("pp-selected", "data"), Input("pp-samples", "value"), Input("pp-fill", "checked"), Input("pp-smooth", "checked"), State("pp-catalog", "data"), ) ```

:defaultExpanded: false :withExpandedButton: true

---

*Source: /silhouette-tracer*

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: