Using the NIX for data colour calibration
  • JavaScript 62.3%
  • Go 26.9%
  • HTML 10.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
hum3 1cb53367d8 Model 2a: dyes vs printer library (1.1); M1.n equation numbering,
hot-linkable; the composition as nested functions

New section 1.1 compares this sheet's dye readings — peak wavelength
and density per ink — against the printer library's stored dyes for the
same printer (preferring the same paper and results carrying model-2
spectra), with ΔD per ink and the source result named; agreement means
the printer behaves as recorded, drift means re-store. On the ET-2850
the match is exact, as it should be — the library entry came from this
sheet.

All equations take the M1. page prefix, (M1.1)-(M1.13), each an anchor
link matching the transforms page's T.n convention, with every
cross-reference renumbered. And M1.3 is now a proper nested equation:
(r,g,b)_s = F₁(x) = encode(sense(mix(gcr(ink(x))))), each function
defined below by its own equations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:08:45 +01:00
cmd/godatacolor Tartan page reads and saves palettes in tartan-weaver's records.yaml 2026-07-18 18:14:34 +01:00
color Model 2 (/model2): ramps fitted as one dye plus a gamma, scored in XYZ 2026-07-20 11:18:54 +01:00
datacolor Remove bogus per-LED interpretation from Measurement 2026-06-08 13:00:47 +01:00
dataset Add host-side reference calibration (von Kries white balance) 2026-06-07 23:54:14 +01:00
examples Add SpyderCheckr 24 reference target; fix CSV comment handling 2026-06-08 10:54:34 +01:00
internal/usbfs Initial godatacolor: Go/Ubuntu rework of pydatacolor (hardware-validated) 2026-06-07 23:48:59 +01:00
model Initial godatacolor: Go/Ubuntu rework of pydatacolor (hardware-validated) 2026-06-07 23:48:59 +01:00
packaging Initial godatacolor: Go/Ubuntu rework of pydatacolor (hardware-validated) 2026-06-07 23:48:59 +01:00
profile Add guided profiling to the web UI 2026-06-08 11:32:18 +01:00
spydercheckr Initial godatacolor: Go/Ubuntu rework of pydatacolor (hardware-validated) 2026-06-07 23:48:59 +01:00
web Model 2a: dyes vs printer library (1.1); M1.n equation numbering, 2026-07-22 19:08:45 +01:00
.gitignore Add Taskfile: 'task' builds and serves the web UI 2026-07-16 14:19:15 +01:00
go.mod Experiment file: one YAML records the whole characterisation run 2026-07-18 01:06:30 +01:00
go.sum Experiment file: one YAML records the whole characterisation run 2026-07-18 01:06:30 +01:00
Makefile Initial godatacolor: Go/Ubuntu rework of pydatacolor (hardware-validated) 2026-06-07 23:48:59 +01:00
README.md Menu web UI: Nix spectro, OKLab CMS, ArgyllCMS ICC workflow, direct printing 2026-07-16 14:19:40 +01:00
sc24.json Add guided profiling to the web UI 2026-06-08 11:32:18 +01:00
Taskfile.yml Add Taskfile: 'task' builds and serves the web UI 2026-07-16 14:19:15 +01:00

godatacolor

A Go rework — for Ubuntu/Linux — of pydatacolor, driving the Datacolor SpyderPRINT spectrocolorimeter (USB 085c:0007).

It talks to the device, runs the connect handshake, calibrates against the white tile, takes measurements, and turns the raw CIELAB readings into correct sRGB — fixing the clipping / out-of-gamut problems the original project ran into.

What's improved over pydatacolor

  • No dependencies, no libusb, no cgo. The USB layer talks to the Linux usbfs ioctl interface directly (internal/usbfs), so the whole thing is pure Go and builds with just the Go toolchain.
  • Correct colour pipeline (color): CIELAB → XYZ → Bradford chromatic adaptation → sRGB primaries → sRGB transfer function, with a selectable reference white (D50 for print, D65). Out-of-gamut colours are handled with hue-preserving gamut mapping (chroma is compressed toward the boundary with L* and hue fixed) instead of per-channel clipping, which shifts hue.
  • SpyderCheckr charts (spydercheckr): render measured patches to a PNG, optionally drawing the naively-clipped colour beside the mapped one so you can see the correction.
  • Robust protocol layer (datacolor): the device transport is behind an interface (testable without hardware), handshake/calibration mismatches are lenient by default (firmware responses vary) but can be made -strict, timeouts are typed and tolerated where appropriate.
  • Data export: every run saves to JSON and CSV.
  • Two front-ends: a scriptable CLI and a lofigui-style web UI.

Install

Requires Go 1.24+.

make build                 # -> ./bin/godatacolor
sudo make install          # -> /usr/local/bin/godatacolor   (optional)
sudo make install-udev     # udev rule for non-root device access

The udev rule (packaging/99-datacolor-spyderprint.rules) grants the active local user access to the device. After installing it, unplug and replug the SpyderPRINT. Note: like the original, using this driver path means the device is no longer claimed by Datacolor's own software while the rule is active.

CLI

godatacolor info                      # connect, print serial + one reading
godatacolor calibrate                 # white-tile calibration
godatacolor measure -n 100 -name sky -white D50 -o run.json
godatacolor capture -patches 24 -o checker.png   # interactive patch capture -> chart
godatacolor chart -in run.json -o chart.png      # render a saved dataset
godatacolor serve -addr 127.0.0.1:1340           # web UI

Common flags: -white D50|D65, -n <readings to average>, -strict. Device progress goes to stderr; data goes to stdout.

Interactive capture

godatacolor capture calibrates once, then prompts you to place each patch and press Enter. It averages -n readings per patch, writes a SpyderCheckr-style PNG and a dataset JSON. Press q to finish early.

Calibration overlay (profiling)

The device returns raw CIELAB with a systematic bias (a freshly calibrated white tile reads ~L*102.7, slightly blue). Two host-side corrections sit on top of the raw numbers:

  • White balance (-host-cal, or the web "Set white ref" button): a von Kries diagonal correction from the white tile alone — quick, neutralises the white point.
  • Fitted overlay (profile): a 3×3-matrix + offset fitted by least squares from a known-reference target, correcting scale, offset and hue across the whole range.

Build an overlay from a target whose patches have known L*a*b* (a .json or .csv — see examples/target.example.csv):

godatacolor profile build -target mychart.csv -o mychart.json -n 50
godatacolor profile show -in mychart.json          # matrix + residual ΔE
godatacolor measure -profile mychart.json -name patch1
godatacolor capture -profile mychart.json -patches 24 -o chart.png

profile build reports the ΔE to reference before vs after correction, so you can see the improvement. After measuring all patches it re-reads the first row to measure session drift (-row, default 6) and fails if the drift or the fit residual exceeds -tolerance (default 1.0 ΔE) — so a run where the device drifted out of your limits is flagged rather than silently trusted.

Repeatability characterisation

godatacolor noise recalibrates in situ N times, takes a timed run of readings each, and reports single-reading σ, the standard error of the mean, an averaging-convergence table, the systematic within-run drift, and the between-calibration spread — so you can choose how long to average:

godatacolor noise -repeats 3 -n 200

On the validated unit, per-reading noise is ~0.06 ΔE (gone by ~50 reads), while drift (~0.1) and calibration repeatability (~0.17 ΔE) dominate — i.e. averaging past ~50 readings is wasted; warm-up and recalibration are the real levers.

Web UI

godatacolor serve starts a menu-driven UI on http://127.0.0.1:1340, built around printing a small palette of colours (a tartan swatch) accurately:

  • Spectro (/) — connect a Nix Spectro 2 (via the sibling gonixcolour module; clone it next to this repo, see the replace in go.mod) and measure sample by sample into a ticker-tape feed showing Lab, OkLCH and the spectral curve. JSON/CSV export.
  • Calibrate (/calibrate) — characterise a printer: print a chart of known sRGB patches (grey ramp, primaries/secondaries/tertiaries + the palette) with pinned CUPS options, measure it patch by patch, then fit an affine intended → measured map in OKLab space. Exports the session as CGATS .ti3 so ArgyllCMS's colprof can build a real ICC from the same data.
  • Argyll (/argyll) — real ICC profiles for arbitrary colours (an unknown tartan): targen chart → print uncorrected → measure with the Nix (journalled, resumable) → colprof → .icc, then xicclu (absolute colorimetric) seeds the palette's device values from the profile. Charts and profiles live in the -argyll dir (default argyll/); profiles also work in any colour-managed app. /docs explains the .ti1/.ti3/.icc pipeline.
  • CMS (/cms) — the palette: each target colour (entered as hex or OkLCH) gets a device RGB seeded from the inverted fit, then refined in a closed loop — print a check sheet, measure it, each colour is nudged by its OKLab error until the print measures within tolerance. Palette, fit and device values persist (-cms cms.json) as the recipe for the final swatch.
  • Measure (/measure) — the dedicated chart-measuring page: whichever chart session is active (built-in or targen) is walked patch by patch here, journalled and resumable.
  • Verify (/verify) — how good is the match, really? Prints a colour set (built-in greys + hue wheel + palette, or a targen set like the Argyll chart) through the active calibration — via the ICC when one is applied — then measures it back: target vs measured OkLCH and ΔEok side by side, an interactive a*b* vector plot (hover a point for details), and per-run stats. Completed runs join a persistent history — the honest accuracy estimate per printer + paper.
  • TI3 (/ti3) — visualise any CGATS .ti3: sent-vs-measured swatch pairs, summary ΔE, and the a*b* error-vector plot that makes the printer's character visible (chroma compression, hue twists, grey-axis cast).
  • SpyderPRINT (/spyder) — the original single-page SpyderPRINT UI: Connect / Calibrate / Measure gated on the device state machine, guided profiling, match-sheet printing.

All pages share the scrolling log and auto-refresh while an operation runs.

Guided profiling in the browser. Pass a reference target to serve and the page adds a profiling panel that walks you through the chart patch by patch — showing the next patch's reference swatch and L*a*b*, and, as each reading lands, a live table comparing measured vs reference (swatches + ΔE). It then re-reads the first row for drift, fits the overlay (raw vs corrected ΔE, with a within/out-of-tolerance badge), applies it, and offers the profile for download:

godatacolor serve -target examples/spydercheckr24.csv

Direct printing (CUPS). The print form can bypass the browser's print dialog entirely: the server renders the solid sheets / tuning grids as a PDF of plain device-RGB rectangles (no ICC tag, no browser or OS colour management) and submits it with lp, with the driver options — paper size and type, input slot, colour space, resolution, quality, colour mapping — chosen on the page and logged to the terminal panel, so a print is reproducible from its log line. Only the options that matter for colour targets are surfaced (e.g. Gutenprint's MediaType, StpQuality, StpInkSet, StpColorCorrection); everything else keeps its PPD default. For profiling runs on a Gutenprint queue, set Color Correction to Uncorrected so the patch values map deterministically to ink. An as PDF checkbox on the preview buttons shows exactly what would be sent.

Layout

Package Role
internal/usbfs Dependency-free Linux USB transport (usbfs ioctls)
datacolor SpyderPRINT protocol: handshake, calibrate, measure
color CIELAB → sRGB with adaptation, gamut mapping, von Kries + affine corrections
model Thread-safe connect→calibrate→measure state machine
profile Fitted calibration overlay (3×3+offset) + drift QA
dataset Sample persistence + JSON/CSV export
spydercheckr Chart (PNG) rendering
web lofigui-style HTTP UI
cmd/godatacolor CLI entry point

Protocol notes

The device speaks a command-framed bulk protocol: an OUT transfer whose first byte is the command, then an IN transfer that echoes the command byte followed by a length byte; the payload is length - 2 bytes. Measurements come back as a 13-byte frame — a mode byte plus three big-endian signed 16.16 fixed-point values interpreted as raw L*a*b*. The reset handshake and the non-standard two-write calibration frames are ported from pydatacolor's reverse engineering; see comments in datacolor/datacolor.go. Calibration is expected to be applied on the host (this is why the raw values need the color pipeline).

Testing

make test

Tests cover the protocol framing/parsing (against a scripted fake transport, no hardware needed), the state machine transitions, the colour conversions (white, black, sRGB primaries, D50 vs D65, gamut mapping) and chart rendering.

Status

The protocol, colour pipeline, charting, CLI and web UI are implemented and tested. The reset/calibration sequences are ported faithfully from pydatacolor but, like the original, were captured from one unit — run with -strict to see exactly where your device's responses differ. Hardware-in-the-loop verification is the remaining step.

Credit

Based on drummonds/pydatacolor and its USB reverse-engineering work.