ApplicationsHow-to

Automatic threshold segmentation in Lab color with Otsu's method

No hand-set gray values: pick the Lab color channel, let Otsu's between-class variance find the automatic threshold, and gate the segmentation every frame.

LumaSpec Vision10 min read Share on LinkedIn

On this page 11 sections
  1. 01What you need
  2. 02Choose the Lab channel before the threshold
  3. 03Otsu's rule in one pass
  4. 04Worked example you can check by hand
  5. 05The steps
  6. 06Test that the mask holds
  7. 07Common mistakes
  8. 08When a better threshold is the wrong fix
  9. 09Parts that do the job
  10. 10Where to take it next
  11. 11Questions

A threshold tuned by hand on Monday fails by Thursday: someone swaps the belt, an LED ages, a dome collects dust, and the fixed gray value lands in the wrong place. Computing the threshold from the image itself removes that failure mode, because the split follows the histogram instead of your last tuning session. This guide shows you how to threshold one channel of Lab color with Otsu's maximum between-class variance rule, the standard automatic threshold. You will type a single-pass loop that runs on any smart camera or vision library, check a worked example by hand, choose the channel with one number, and gate the frames where the split cannot be trusted.

What you need

  • A color camera whose library exposes per-channel pixels or a histogram; the smart cameras category is built for this on-device job.
  • Regulated lighting on a light controller, with auto exposure and auto gain off — step 1 explains why.
  • One ROI of 100×100 pixels or more holding the part and its immediate background.
  • Twenty saved frames of good parts and ten empty frames, for the channel test and the frame gates.

Everything below needs only a 256-bin histogram per frame: no GPU, no training set, no PC.

Choose the Lab channel before the threshold

Lab expresses color as L* for lightness, a* for red versus green and b* for yellow versus blue, in a color-opponent layout (CIELAB color space reference). That structure is the reason to threshold here: L tracks how much light reached the sensor, while a and b describe color largely independently of brightness. Dim the line by a quarter and the L histogram slides; a and b stay close to where they were.

Libraries print the scaling differently. Some keep L on 0–100 with a and b on −128…+127; others store all three in 8-bit, with a and b shifted by 128 so neutral gray reads 128. The algorithm is identical; bin whatever scaling your library hands you.

Pick the channel with one number, the separation score S = |μ_part − μ_bg| / (σ_part + σ_bg), measured inside a part region and a background region of your saved frames. A channel scoring under about 1.5 will not hold a clean valley — that is roughly the bimodality bar for two classes of similar spread. Aim for 3 or better so drift cannot close the gap. If no channel clears 1.5, part and background are too alike for any threshold; change the light, not the code.

Otsu's rule in one pass

Otsu's method tries every candidate split t and keeps the one that maximizes the between-class variance, the share of the spread the two classes explain together (Otsu's method reference):

ω0(t) = (pixels with level ≤ t) / N ω1(t) = 1 − ω0(t) μ0(t) = mean level of the class at or below t μ1(t) = mean level of the class above t σ²_b(t) = ω0(t) × ω1(t) × (μ0(t) − μ1(t))² t* = the candidate t with the largest σ²_b(t)

With running sums, the whole search is one pass over 256 bins after one pass over the pixels — fast enough for every frame on a small camera. If your current job runs on a hand-typed gray value, the threshold basics without a vision library guide shows why that constant breaks first; Otsu replaces exactly that number.

# Pseudocode: names are ours, not any vendor's API.
h = [0] * 256          # int list, histogram of the chosen Lab channel
total = 0              # int, pixels in the ROI
for i in range(256):
    total = total + h[i]
sum_all = 0            # int, sum of level times count
for i in range(256):
    sum_all = sum_all + i * h[i]

w_b = 0                # int, pixels below the split so far
sum_b = 0              # int, their level sum
best = -1.0            # float, best between-class variance found
t_best = -1            # int, winning split
for t in range(255):
    w_b = w_b + h[t]
    sum_b = sum_b + t * h[t]
    w_f = total - w_b
    if w_b == 0 or w_f == 0:
        continue
    m_b = sum_b / w_b                 # float
    m_f = (sum_all - sum_b) / w_f     # float
    v = (w_b / total) * (w_f / total) * (m_b - m_f) ** 2
    if v > best:
        best = v
        t_best = t
# mask: a pixel joins the dark class where its level <= t_best

A test that pins the code down: load the worked-example histogram below — h[50] = 1500, h[60] = 1500, h[105] = 3500, h[115] = 3500, all other bins 0 — and the loop returns t_best = 60 with best = 635.25, the lowest maximizer, exactly as the hand calculation gives. On a PC-side library the same histogram discipline applies; the color threshold mask set-up for highlights page shows the equivalent configuration there.

Worked example you can check by hand

Scene: a dark cap on a light gray belt, ROI of 100×100 = 10,000 pixels, channel L. Four occupied bins: 1,500 px at level 50, 1,500 at 60 (the cap), 3,500 at 105 and 3,500 at 115 (the belt).

Total level sum: 50×1,500 + 60×1,500 + 105×3,500 + 115×3,500 = 75,000 + 90,000 + 367,500 + 402,500 = 935,000.

t where it splits ω0 μ0 μ1 σ²_b (levels²)
52 through the cap mode 0.15 50 101.2 334
80 clean cap/belt split 0.30 55 110 635.25
110 through the belt mode 0.65 81.9 115 249

Arithmetic for the winner, t = 80: the lower class holds 3,000 px with mean 165,000/3,000 = 55; the upper class holds 7,000 px with mean (935,000 − 165,000)/7,000 = 110. So σ²_b = 0.30 × 0.70 × (55 − 110)² = 0.21 × 3,025 = 635.25. The neighbors lose: at t = 52 the classes are 0.15 and 0.85 with means 50 and (935,000 − 75,000)/8,500 = 101.2, giving 0.1275 × 51.2² ≈ 334; at t = 110 they are 0.65 and 0.35 with means (165,000 + 367,500)/6,500 = 81.9 and 115, giving 0.2275 × 33.1² ≈ 249.

Every split from t = 60 to t = 104 yields the same two classes, because no pixels live in the gap; the maximum is a plateau and implementations return the lowest maximizer, t = 60. The mask is then level ≤ 60: the 3,000 cap pixels, 30% of the ROI. Separation score for the record: each class spreads σ = 5 about its mean (50 and 60 about 55; 105 and 115 about 110), so S = 55/(5 + 5) = 5.5, well past the ≥3 bar.

The steps

  1. Lock the imaging. Manual exposure and gain, white balance locked. Auto modes rescale the histogram every frame, and the threshold starts adapting to the camera's own reaction; auto exposure and auto gain on color cameras covers which pixels that disturbs, and contour position jitter under automatic exposure shows the downstream instability.
  2. Fix the ROI. Part plus roughly 10–20 pixels of background ring; exclude vignette corners, dirt and anything that changes without the part.
  3. Pick the channel with the score. Compute μ and σ per channel inside the part and background regions of the saved frames; take the largest S and drop channels under 1.5.
  4. Strobe when parts move. Keep exposure short enough to freeze motion; when 50/60 Hz mains light beats against your exposure time, camera flicker under artificial lighting explains the beat pattern.
  5. Build the histogram of the chosen channel over the ROI each frame — 256 bins, one pass.
  6. Run the loop and clean the mask. Apply the threshold — dark part: level ≤ t*; bright part: level > t* — then one 3×3 erode followed by one 3×3 dilate to remove single-pixel noise.
  7. Gate every frame on two numbers from set-up. Reference values from the worked example: part class 30% of the ROI, σ²_b = 635.25. Accept frames with the part class between 15% and 45% of the ROI (half to 1.5 times the reference share) and σ²_b at or above 318 (half the reference). Outside that band, report no-part or scene-changed; never route a mask the gate refuses to the result.

That last step is the one teams skip and then chase for weeks: an automatic threshold with no gate will confidently segment an empty belt.

Test that the mask holds

  • Dim test. Run the light at nominal, then 20% lower from the controller. On a* or b* the mask area should stay within about 5%; chroma barely moves when only intensity changes, so a bigger jump means the spectrum changed — aging LED or new belt — and the fix is hardware.
  • Empty-frame test. The ten saved empty frames must trip the gate in step 7. If one slips through, raise the σ²_b floor.
  • Full-shift log. Record t* and σ²_b per frame across one shift. A threshold that walks is a lighting problem; closed-loop lighting control for repeatable vision images shows how to hold brightness at the source.

Common mistakes

  • Full-frame Otsu. The histogram then describes the whole station — conveyor texture, shadows, neighboring fixtures — and the winning split has nothing to do with your part. Keep the ROI.
  • Auto exposure on top of per-frame thresholds. Two adaptive loops fighting each other; lock the camera and adapt only the threshold.
  • Three populations in one channel. Otsu is a two-class rule; it merges the two nearest and cuts off the third. Crop tighter or run two thresholds — the FAQ covers multi-Otsu.
  • Reading the gate as a defect. A refused frame means no-part or scene-changed; routing it to the reject bin teaches operators to distrust the station.
  • Uneven illumination across the field. One global threshold cannot serve a bright center and a dark corner; that is local adaptive thresholding territory, and adaptive thresholding for uneven part shading walks through it.

When a better threshold is the wrong fix

If part and background score under 1.5 on every channel, no threshold algorithm will rescue the image: the classes genuinely overlap. Move the physics instead — darkfield geometry for gloss contrast, a colored light to force chroma separation, a different wavelength. The lighting guides collect those geometry decisions. And when recipes legitimately change the part's appearance line by line, per-recipe reference frames beat per-frame magic; past that point it becomes a retraining question, which when lighting changes force retraining a vision model answers.

Parts that do the job

Two cameras that run this loop on-device, no PC attached:

Smart cameras & vision sensorsHikrobot MV-SC3050XC-16M-ATN-H 5 MP color vision sensorMV-SC3050XC-16M-ATN-H
Interface
Fast Ethernet
Resolution
5 MP
$1,809.00View part
Smart cameras & vision sensorsHikrobot MV-SC6050C-08M-XBN SC6000 Series Smart Camera, 5 MP, Color, 8 mm focal length, M12-mount, Multi-color light sourceMV-SC6050C-08M-XBN
Interface
GigE
Resolution
5 MP
$3,449.00 · quotedView and quote

The second card is an SC6000-series unit with a built-in multi-color light, so you can force chroma contrast from the camera itself when part and belt tie on every channel. Hold the light side steady with a digital controller; the light controller finder filters on channels, strobing and output type. This one drives four channels at 16-24 V with 8 A total output:

Light controllers4-Channel Digital LED light controller, 16-24V, AC100-240V 50/60 Hz, 8 A total output (LS-CTL-0322)LS-CTL-0322
Output
Constant voltage 24 V
Channels
4 channels
Control
Digital
$539.00View partBuild a system with it

Where to take it next

Point the configurator at your camera and light to match channel counts and strobe wiring, or open the smart camera finder if the current camera cannot expose per-channel histograms. When the mask is stable, color blob tracking with segmentation and PID control is the natural next read — it starts exactly where this guide ends, at a clean binary mask. The applications guides hold the rest of the downstream set.

Questions

Which Lab channel should I use for a red part on a gray belt?

Use a*. Red pushes a* strongly positive while a gray belt stays near neutral, so the classes separate on chroma alone; L often fails because a red part and a mid-gray belt can share the same lightness. Measure the separation score on both channels from saved frames first, and if a* scores under 1.5, add a colored light to force the gap.

Does the Otsu threshold still work when the light dims over a shift?

On L the threshold follows the light down, because both modes shift and the gap changes shape; on a* and b* it barely moves, since chroma is normalized against the white point. A threshold that walks across a shift is the classic symptom of an aging LED or an unregulated supply, and the fix lives in the controller, not in the code.

Why does my automatic threshold jump when no part is present?

An all-background frame still holds two populations, belt texture, shading, vignette, and Otsu splits them with a confident-looking variance value. Gate every frame on the two numbers you measured at set-up: the part-class share of the ROI and the between-class variance. When either leaves the band you derived from reference frames, report no part and skip the mask entirely.

Can Otsu split a channel into more than two classes?

Plain Otsu is a two-class rule: one threshold, two means. For three populations, run multi-Otsu, which searches pairs of thresholds and maximizes the sum of all pairwise between-class variances, or apply plain Otsu twice and split the larger class again. Both run in a single pass at 256 bins; gate each resulting class on its own reference share, exactly as you would for two.

Should I lock exposure and white balance when thresholds are computed per frame?

Yes. Auto exposure and auto gain rescale the histogram every frame, so the threshold adapts to the camera's reaction instead of the part; auto white balance does the same to a and b. Lock both, hold brightness at the light controller, and strobe when parts move. Re-reference per recipe only when the illuminant color genuinely changes between products.