Take a scheme's colours from a photo (#17) #131
No reviewers
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED!131
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/17-palette"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #17.
Pick an image in the scheme editor and its dominant colours fill the segment slots. Nothing is written until Save, so the result is an ordinary editable scheme on the ordinary path.
Two deviations from the issue's sketch — flagging both
Extraction is client-side, where the sketch proposed a Pillow endpoint
Server-side extraction means a self-hosted box parsing arbitrary uploaded images with several megabytes of native code. That's a meaningful addition to the attack surface #60 spent a release shrinking, on the one code path that consumes attacker-chosen bytes.
The browser already decodes JPEG, PNG, WebP and HEIC in a sandbox hardened far beyond anything we would add. Doing it there means: no new backend dependency, no upload endpoint, no size caps or temp files to get wrong, and the photo never leaves the machine it's on — which matters for a self-hosted app where someone might drop in a family photo.
Every acceptance criterion is met either way — image → palette, preview before save, a standard editable scheme. Only the mechanism differs. Say the word if you'd rather have the endpoint and I'll add it.
Median cut rather than k-means
The issue asks for "deterministic, offline color sourcing", and k-means is not deterministic: it depends on how its centroids are seeded, so the same photo can give two different palettes on two runs. A palette you can't reproduce isn't worth saving. Median cut has no random component at all.
The split point was found by a failing test, not reasoned in advance
Textbook median cut splits at the median index. My first test — equal parts red, green and blue, asking for three colours — failed with a bucket 120 units from any colour in the image.
Sorting by red puts 200 non-red pixels before 100 red ones, so the median lands inside the non-red run: the first bucket averages green with half the blues and returns a colour present nowhere in the image. A flag or a logo comes back as mud.
Cutting at the widest gap along the widest channel finds the boundary between clusters instead. The tie-break matters as much: in a photo the values are near-continuous and every gap is equal, so preferring the gap nearest the middle restores median cut's balance exactly where balance is the right instinct — rather than peeling off one outlier pixel at a time.
Smaller decisions
Verification
14 new vitest cases asserting properties rather than fixture values — that the colours are ones actually present, that the dominant one comes first, that the same input gives the same output — because median cut always returns something, and eyeballing swatches won't tell you whether they came from the photo or from the order the buckets split in.
Frontend in
node:22:tsc -bclean,eslintclean, 95 vitest passing. No backend change at all.🤖 Generated with Claude Code
cd5ee38ceee1d252c9f2e1d252c9f2f25f4d7b76