Summary
An opt-in "misalignment safety net" for the ChromIQ chart-reading engine: after a strip is read, if it fits its expected colours badly, probe small ±1/±2 assignment offsets and — only if a shift is dramatically better — surface a friendly dialog offering Re-measure this strip / Use it anyway / Stop. Default off, so nothing changes unless the user enables it.
Status: DEFERRED — design preserved, not currently worth building
This was investigated after a tester reported that "many patches are highlighted at the ΔE 20 threshold" and that one strip's measured half looked shifted (last patch measured as paper white).
Analysis of the tester's real .ti2 + .ti3 overturned the misalignment theory for that measurement:
- The saved measurement is correctly aligned: offset 0 mean ΔE ≈ 15.5–15.9 per strip; every shift (±1, ±2) and the reversal are 3× worse (40–51).
- Neutral/dark patches are low ΔE (near-black ≈ 2–4, gray ≈ 9.5) while only saturated patches are high (pure green ≈ 41). That is the textbook signature of a correctly-aligned measurement with genuine print-vs-sRGB deviation — a real misalignment would push the neutrals high too.
- So the mass ΔE-20 flags are expected print-vs-sRGB deviation, i.e. the live flag threshold is too low for printer profiling (tracked separately), not a misread.
Why this is probably a non-problem for the i1Pro family:
- Argyll deliberately gates its ±1 offset-recovery (
loff/hoff) to the DTP51 only (which has a known ±1 square-misalignment quirk). Other instruments' transition-detection is considered reliable.
- On an i1Pro a miscounted swipe generally makes chartread error out ("wrong number of patches") rather than silently shift.
Conclusion: build only if a genuine hardware misalignment (neutrals also high ΔE, and a ±k shift that dramatically lowers the mean) is ever confirmed on a non-DTP51 instrument.
Design (ready to implement if revived)
Approach
Reactive, symptom-triggered, opt-in, detection-only (never a silent rewrite — a real one-patch misread physically loses a patch that re-indexing can't restore).
Engine (native helper, chromiq_chartread.c)
- New
cq_safenet flag (declared in chromiq_ext.h, defined in chromiq_json.c), enabled by a --safenet CLI flag parsed in the CHROMIQ_EXT arg loop.
- Two small pure functions:
cq_strip_mean_de_at(vals, navail, scb, stipa, skipp, boff, off, bdir, *n_valid) — mean ΔE of assigning scb[i] ← vals[i+skipp+boff+off] (mirrored when bdir), over in-range patches only, returning the overlap count. navail = stipa + nextrap.
cq_detect_misalign(...) — base mean at off 0; only if that's poor (≥ TRIGGER_DE, e.g. 12) probe off ∈ [−k..k]; accept an offset only if its mean is both absolutely good (≤ GOOD_DE, e.g. 6) and ≤ RATIO (e.g. 0.5) of the base. Requires a real overlap so a 1-patch coincidence can't win.
- After the transfer block, if
cq_safenet and the detector returns k != 0, emit a strip_misaligned JSON event (strip, offset, base_de, best_de).
#define CQ_MIS_TRIGGER_DE 12.0
#define CQ_MIS_GOOD_DE 6.0
#define CQ_MIS_RATIO 0.5
#define CQ_MIS_MAX_OFF 2
static double cq_strip_mean_de_at(ipatch *vals, int navail, chcol **scb,
int stipa, int skipp, int boff, int off,
int bdir, int *n_valid) {
double sum = 0.0; int i, j, cnt = 0;
for (i = 0; i < stipa; i++) {
double labm[3], labe[3], xyz[3], de; int ix = i + skipp + boff + off;
if (bdir != 0) ix = stipa - 1 - ix;
if (ix < 0 || ix >= navail || vals[ix].XYZ_v == 0) continue;
for (j = 0; j < 3; j++) xyz[j] = vals[ix].XYZ[j] / 100.0;
icmXYZ2Lab(&icmD50, labm, xyz);
for (j = 0; j < 3; j++) xyz[j] = scb[i]->eXYZ[j] / 100.0;
icmXYZ2Lab(&icmD50, labe, xyz);
de = icmLabDE(labm, labe); sum += de; cnt++;
}
if (n_valid != NULL) *n_valid = cnt;
return cnt > 0 ? sum / (double)cnt : 1e9;
}
static int cq_detect_misalign(ipatch *vals, int navail, chcol **scb, int stipa,
int skipp, int boff, int bdir,
double *base_mean, double *best_mean) {
int off, nv, best_off = 0; double m0, m;
m0 = cq_strip_mean_de_at(vals, navail, scb, stipa, skipp, boff, 0, bdir, &nv);
*base_mean = *best_mean = m0;
if (m0 < CQ_MIS_TRIGGER_DE) return 0;
for (off = -CQ_MIS_MAX_OFF; off <= CQ_MIS_MAX_OFF; off++) {
if (off == 0) continue;
m = cq_strip_mean_de_at(vals, navail, scb, stipa, skipp, boff, off, bdir, &nv);
if (nv < stipa - CQ_MIS_MAX_OFF - 1) continue;
if (m < *best_mean) { *best_mean = m; best_off = off; }
}
if (best_off != 0 && *best_mean <= CQ_MIS_GOOD_DE && *best_mean <= m0 * CQ_MIS_RATIO)
return best_off;
return 0;
}
GUI (Python)
- Settings → Beta: "Detect misaligned strips (safety net)" checkbox (persisted, migration), with a tooltip (friendly, extensive, plain-language).
- Pass
--safenet to the helper when enabled.
measure_manager parses strip_misaligned → Qt signal → tab_measure shows a friendly dialog: Re-measure this strip / Use it anyway / Stop (reusing the existing wrong-strip prompt pattern).
- i18n for the new strings (German + placeholders).
Tests (replay-reproducible)
- Aligned strip → returns 0.
- Shifted +1 (script readings for p1…pN then a paper reading) → returns −1.
- Saturated but aligned (high ΔE at every offset) → returns 0 (false-positive guard).
- Gray ramp → no trigger.
- Shifted +2 with MAX_OFF=2 → −2; with MAX_OFF=1 → 0 (documented limit).
- Short strip → overlap floor prevents a fluke.
Notes / open questions before building
- Confirm whether a real i1Pro misread leaves an extra reading in
vals (auto-apply safe) or truly drops a patch (re-scan only).
- Calibrate the thresholds on real misaligned vs. saturated strips; consider a robust statistic (median / count > X ΔE) instead of the mean.
🤖 Generated with Claude Code
Summary
An opt-in "misalignment safety net" for the ChromIQ chart-reading engine: after a strip is read, if it fits its expected colours badly, probe small ±1/±2 assignment offsets and — only if a shift is dramatically better — surface a friendly dialog offering Re-measure this strip / Use it anyway / Stop. Default off, so nothing changes unless the user enables it.
Status: DEFERRED — design preserved, not currently worth building
This was investigated after a tester reported that "many patches are highlighted at the ΔE 20 threshold" and that one strip's measured half looked shifted (last patch measured as paper white).
Analysis of the tester's real
.ti2+.ti3overturned the misalignment theory for that measurement:Why this is probably a non-problem for the i1Pro family:
loff/hoff) to the DTP51 only (which has a known ±1 square-misalignment quirk). Other instruments' transition-detection is considered reliable.Conclusion: build only if a genuine hardware misalignment (neutrals also high ΔE, and a ±k shift that dramatically lowers the mean) is ever confirmed on a non-DTP51 instrument.
Design (ready to implement if revived)
Approach
Reactive, symptom-triggered, opt-in, detection-only (never a silent rewrite — a real one-patch misread physically loses a patch that re-indexing can't restore).
Engine (native helper,
chromiq_chartread.c)cq_safenetflag (declared inchromiq_ext.h, defined inchromiq_json.c), enabled by a--safenetCLI flag parsed in the CHROMIQ_EXT arg loop.cq_strip_mean_de_at(vals, navail, scb, stipa, skipp, boff, off, bdir, *n_valid)— mean ΔE of assigningscb[i] ← vals[i+skipp+boff+off](mirrored whenbdir), over in-range patches only, returning the overlap count.navail = stipa + nextrap.cq_detect_misalign(...)— base mean at off 0; only if that's poor (≥TRIGGER_DE, e.g. 12) probe off ∈ [−k..k]; accept an offset only if its mean is both absolutely good (≤GOOD_DE, e.g. 6) and ≤RATIO(e.g. 0.5) of the base. Requires a real overlap so a 1-patch coincidence can't win.cq_safenetand the detector returnsk != 0, emit astrip_misalignedJSON event (strip,offset,base_de,best_de).GUI (Python)
--safenetto the helper when enabled.measure_managerparsesstrip_misaligned→ Qt signal →tab_measureshows a friendly dialog: Re-measure this strip / Use it anyway / Stop (reusing the existing wrong-strip prompt pattern).Tests (replay-reproducible)
Notes / open questions before building
vals(auto-apply safe) or truly drops a patch (re-scan only).🤖 Generated with Claude Code