Edward Lukowsky
All work

Data / hardware 2026

LightSync

One scene controller for two brands of smart light that disagree about color, plus a calibration model that makes them match.

Stack
Next.js, TypeScript, Python, NumPy, Postgres
Method
Least squares regression, leave one out CV
Metric
CIELAB ΔE2000
Status
Personal, in daily use

The problem

Our apartment has Philips Hue bulbs and Govee ceiling lights. Setting a scene meant opening two apps, and worse, sending both brands the same RGB value produced visibly different colors on the wall. The Govee lights ran cooler and washed out at the top of their range.

The controller was simple: one app, one scene, both brands set in parallel. Most of the work went into making the same color look the same on both.

Getting clean measurements

Photographing light cleanly is hard. Room lighting gets into every shot, and the phone camera's auto white balance shifts the color it records.

So the capture wizard first takes an ambient reference frame with both bulbs off and subtracts it pixel by pixel from every later measurement. That removes the room's light and the camera's color cast from each sample before fitting.

Each anchor color is captured twice, once per bulb, with the other bulb off, so each measurement is one bulb and not the sum of both. The two brands also get separate brightness settings during capture, since they saturate the camera at very different levels and a blown out pixel has no usable color.

Picking the model

The correction is a least squares map from what the Govee light shows to what the reference light shows. Six candidates compete: linear, root polynomial, and second order polynomial feature sets, each with and without ridge regularization.

Selection uses leave one out cross validation on the inverse model, which is where the distortion is. For each held out anchor, the model is fit on the rest and scored on how well it recovers that anchor from its measured appearance. Lowest mean held out error wins, then the full model is refit on everything. Both mean and worst case error are reported for each candidate, since a model that does well on average but badly on one hue still looks wrong on the wall.

I used leave one out instead of a train and test split because there are only a handful of anchor colors, and holding out 20% of a sample that small makes the score mostly noise.

Scoring in ΔE2000

Models are scored in CIELAB ΔE2000 instead of mean squared error on RGB values. Equal distances in RGB are not equal differences to the eye, which is far more sensitive to a shift in a mid green than to the same numeric shift in a deep blue. A model tuned on RGB error can score well and still look wrong.

ΔE2000 measures perceived difference, so minimizing it means minimizing how different the two lights look to someone in the room.

The fitting runs inside the deployed app on real captured samples, so I can recalibrate from my phone after moving a lamp.