+1 (415) 360-7596

Colour you can trust: ColorChecker detection, correction matrices and drift checks in OpenCV

Most vision projects we inherit treat colour as a given. Then the same part looks "slightly redder" on line 2 than on line 1, a threshold tuned in the lab fails at the customer's site, and a classifier trained on one camera loses accuracy on another. The cause is rarely the model. It is that nobody calibrated colour.

This tutorial shows how to put a colour reference chart in the scene, detect it with OpenCV's mcc module, fit a colour correction matrix with the ccm module, and turn the result into a pass/fail check you can run on every deployment. Both modules ship in opencv-contrib, so confirm your build includes it before you start.

1. Why colour drifts

  • Illuminant: LED ageing, ambient daylight and different lamp batches all shift the white point.
  • Camera pipeline: auto white balance, vendor tone curves and different sensors map the same surface to different RGB values.
  • Encoding: a gamma-encoded sRGB frame treated as linear gives wrong averages, blends and resizes.

Fixing the camera (manual white balance, fixed exposure, fixed gain) removes most drift. A correction matrix removes what is left and makes different cameras comparable.

2. Put a reference in the scene

Use a 24-patch chart (X-Rite ColorChecker Classic or an equivalent). Mount it in the same plane as the product, lit the same way, and avoid glare on the patches. Matte charts only. If you cannot leave a chart in the scene, capture one reference frame per camera per lighting change and store it with the deployment record.

3. Detect the chart

import cv2

img = cv2.imread("chart_frame.png")          # BGR, 8-bit, not yet corrected

detector = cv2.mcc.CCheckerDetector_create()
found = detector.process(img, cv2.mcc.MCC24, 1)   # 1 = look for one chart
if not found:
    raise RuntimeError("No colour chart found: check framing, glare and focus")

checker = detector.getBestColorChecker()
cdraw = cv2.mcc.CCheckerDraw_create(checker)
preview = img.copy()
cdraw.draw(preview)
cv2.imwrite("chart_detected.png", preview)

measured = checker.getChartsRGB()   # N x 5 array; column 1 onward holds patch values

Always save the overlay image. Reviewers can check at a glance that the patch grid sits on the patches. A shifted grid gives a plausible-looking but wrong matrix.

4. Fit the correction model

import numpy as np

# Rows 0..23 of column 1 hold the mean RGB of each patch in the detected chart
src = measured[:, 1].copy().reshape(-1, 3) / 255.0   # check channel order for your build

model = cv2.ccm.ColorCorrectionModel(src, cv2.ccm.COLORCHECKER_MACBETH)
model.setColorSpace(cv2.ccm.COLOR_SPACE_SRGB)
model.setCCM_TYPE(cv2.ccm.CCM_3X3)
model.setDistance(cv2.ccm.DISTANCE_CIE2000)
model.setLinear(cv2.ccm.LINEARIZATION_GAMMA)
model.setLinearGamma(2.2)
model.run()

ccm = model.getCCM()
loss = model.getLoss()
print("CCM:\n", ccm, "\nloss:", loss)

Choices that matter:

  • CCM_3X3 vs CCM_4X3: the 4x3 type adds an offset term. Start with 3x3. Use 4x3 only if it clearly improves the validation error (section 6), because it can overfit 24 patches.
  • Distance: CIE2000 is closer to what people perceive than plain Euclidean RGB distance.
  • Linearisation: a simple gamma is a reasonable first step. If you have the camera's real response curve, use it instead.

Treat the exact channel order and value ranges as something to verify on your OpenCV version. Print the first patch values for a frame you can eyeball (the white patch should read high, the black patch low). Then write that check into a unit test.

5. Apply it

img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float64) / 255.0
corrected = model.infer(img_rgb)
out = np.clip(corrected * 255.0, 0, 255).astype(np.uint8)
out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)

In production, do this once per frame at the front of the pipeline, before any thresholding or model inference. For high frame rates, precompute a lookup table or run the matrix multiply on the GPU rather than calling infer per frame.

6. Prove it worked: a number you can quote

Fit on one capture and validate on a different one: another frame, another day, or a second chart position. Convert both the corrected patch values and the reference values to CIELAB, then report the per-patch colour difference:

def mean_delta_e(corrected_patches_rgb, reference_patches_rgb):
    a = cv2.cvtColor(corrected_patches_rgb.astype(np.float32).reshape(-1, 1, 3), cv2.COLOR_RGB2Lab)
    b = cv2.cvtColor(reference_patches_rgb.astype(np.float32).reshape(-1, 1, 3), cv2.COLOR_RGB2Lab)
    d = np.linalg.norm(a - b, axis=2).ravel()   # CIE76; use a CIEDE2000 library for strict reports
    return d.mean(), np.percentile(d, 95), d.max()

Report mean, 95th percentile and worst patch, before and after correction. Set an acceptance limit with the customer, for example "mean below 3, worst patch below 6", and write it down. Note that the formula above is CIE76, so state which delta-E you used.

7. Keep it honest in operation

  1. Store the matrix with the deployment, along with the camera serial, lens, lighting setup and date.
  2. Re-check on a schedule. Re-image the chart weekly or after any lamp or camera change, and alert if the validation error crosses the limit.
  3. Do not correct twice. If the camera already applies a matrix or tone curve, fit your model on frames captured exactly as the pipeline will receive them.
  4. Watch saturation. A clipped patch carries no information. Lower exposure until the white patch sits comfortably below full scale.
  5. Keep training and inference aligned. If you train a model on corrected images, run it on corrected images. Re-fit and retest when the matrix changes.

Common failure modes

SymptomLikely cause
Chart not detectedGlare, blur, chart too small in frame (aim for a clearly resolved patch grid), or wrong chart type
Low loss, wrong-looking imagesChannel order or value range mismatch between detector output and model input
Good on chart, bad on productChart not in the product plane or under different light
Error creeps up over weeksLamp ageing or auto white balance re-enabled

When to bring in help

Colour-critical inspection, such as paint matching, food grading, textile sorting or medical imaging, deserves a documented calibration procedure, validated limits and drift monitoring. If you want an experienced OpenCV engineer to set that up or review an existing pipeline, get in touch.