A screen that prints class 1, class 2 or class 3 for every scratch is not a bigger threshold. It is three numbers per region — area, length and shape — compared against cuts you set in millimetres. This guide builds that grader end to end: calibrate the image scale, pull each connected scratch out of the frame, measure it, and grade it with rules you can defend in an audit. The worked example uses a 12 MP camera at 50 px/mm with cuts at 0.10 mm² and 5.0 mm, and shows every conversion. It starts where lighting guides stop: the image already shows the scratches cleanly, and you need the measurement and the decision.
What you need
- An area scan camera and lens framing the whole part with the smallest class comfortably above three pixels wide. The pixel rule for defect detection sizes this.
- Dark-field bar lights for shiny surfaces, diffuse light for matte ones, and a strobed light controller for moving parts.
- A calibration target of known length, for example a 10.00 mm line, plus the marker-based calibration method.
- Your library's bottom-hat filter, adaptive threshold and region-feature function. The code below uses the common names imbothat, imbinarize and regionprops; map them to your equivalents.
The build is five moves:
- Set the class cuts in millimetres.
- Calibrate pixels per millimetre.
- Extract scratch pixels.
- Measure each region's features.
- Grade with rules; train a classifier only if the rules misgrade.
Step 1. Set the class cuts in millimetres, not pixels
Write the classes from the quality spec and golden parts before touching code. A workable three-class set for a machined face:
| Grade | Rule on measured features | Action |
|---|---|---|
| Class 3 | area ≥ 0.50 mm² or length ≥ 5.0 mm | reject |
| Class 2 | area ≥ 0.10 mm² or length ≥ 2.5 mm | flag for review |
| Class 1 | area ≥ 0.008 mm² (20 px at 50 px/mm) | accept, log |
| Ignore | below 20 px | noise |
These cuts are this guide's worked example, not a standard; place your own between the clusters your golden parts show. One habit worth breaking: total scratch area per part, which a first-pass script usually computes, is a shift KPI, not a class. A class 3 verdict must attach to a specific region on a specific part, so the grader works per region.
Step 2. Calibrate the scale in pixels per millimetre
Image a line of known length in the part plane. In the worked setup a 10.00 mm line spans 500 px:
Every cut from Step 1 converts through this scale, and the scale moves whenever you refocus, swap a lens or change the working distance — recalibrate after any such change, or lean on the marker-based camera calibration for length measurement routine.
The scale also fixes what you can see. Measuring a line's width takes about three pixels across it, so 50 px/mm resolves 3 / 50 = 0.06 mm. If class 1 must catch a 0.04 mm line, you need 3 / 0.04 = 75 px/mm; an 80 mm field then demands 75 × 80 = 6000 px across. That is more than the 4000 px of the setup below, so narrow the field to 4000 / 75 = 53 mm or step up the camera. The camera resolution calculator runs this budget the other way, from spec to pixels:
The LS-CMR-0166 gives you 4000 × 3000 pixels at 30 fps over USB3, and the camera finder filters the area scan cameras by resolution.
- Sensor format
- 1/1.7"
- Interface
- USB3
- Resolution
- 12 MP
- Shutter
- Rolling
Step 3. Make the scratch a bright object on a dark background
A scratch is gradable only if the pixels say so. On shiny surfaces, dark-field illumination — bar lights mounted at a low grazing angle from both sides — sends flat areas dark and scratch scatter bright, which is exactly what the bottom-hat threshold in Step 4 wants. Matte surfaces take diffuse light, and polished parts that fight glare do better with cross polarization. The scratch lighting and dynamic thresholding walkthrough covers the illumination half of this build, and the glossy-panel lighting guide covers polished surfaces.
Expose so the background sits near 160 grey levels on an 8-bit frame with the brightest scratch below 255: roughly 60 percent of full scale, leaving headroom on both sides. A stable threshold wants about 25 grey levels of separation; dark-field usually delivers several times that. For moving parts, strobe the lights through a controller so the frame freezes motion regardless of shutter type. The light finder filters bars by lit length.
For the worked setup's field of roughly 300 mm, bars from 290 to 312 mm long sit alongside the part. All three run from a standard 24 V supply.
- Size
- 200-400 mm
- Type
- Standard
- Size
- 200-400 mm
- Type
- Standard
- Size
- 200-400 mm
- Type
- Standard
Step 4. Extract scratch pixels: bottom-hat filter, then adaptive threshold
Two filters handle the two enemies: slow shading across the part, and uneven background brightness. The bottom-hat morphological filter keeps structures thinner than its element and brighter than their surroundings, so shading gradients vanish and scratches survive. An adaptive local threshold then sets the cut per neighbourhood instead of globally.
% Inputs, declared before use
gray = camera_frame % uint8 mono frame, background near 160 grey levels
px_per_mm = 50.0 % double, from Step 2: 500 px / 10.00 mm
se = disk_mask(11) % logical 11 x 11 disk: twice the widest scratch, 5 px
min_px = 20 % integer; regions below this are specks
bg = imbothat(gray, se) % bright thin marks; slow shading removed
bw = imbinarize(bg, 'adaptive') % local threshold absorbs leftover unevenness
disk_mask(11) stands for your library's disk-element constructor. Set the element first: its diameter is about twice the widest scratch that must count, here 11 px for 5 px lines. Then raise the adaptive threshold's sensitivity until known class 1 scratches survive, and back off until clean parts stay clean. Where a scratch is a raised ridge rather than a brightness dip, a local standard-deviation filter (stdfilt in the same function family) followed by the same threshold isolates it too. For parts whose shading defeats any global cut, the local mean adaptive thresholding walkthrough shows the mechanics.
Step 5. Measure four features per region with regionprops
Label the binary image and measure each connected region. Four features decide the grade; more can feed a trained model later but do not add decisiveness now:
stats = regionprops(bw, bg, {'Area', 'MajorAxisLength', 'Circularity', 'MeanIntensity'})
% one struct per region:
% Area px, physical size of the mark
% MajorAxisLength px, tip-to-tip length of the best-fit ellipse
% Circularity unitless, 1.0 for a circle, below 0.4 for a hairline
% MeanIntensity grey levels of bottom-hat response, the contrast proxy
Area alone cannot do the job. Counting blob area by pixel counting is the single-feature special case this guide generalizes. Length catches the hairline whose area stays small; circularity rejects round dirt; MeanIntensity separates a bright deep gouge from a faint smudge.
Step 6. Grade each region: the rules and the arithmetic
for k = 1 : numel(stats)
a_mm2 = stats(k).Area / (px_per_mm * px_per_mm) % mm²
l_mm = stats(k).MajorAxisLength / px_per_mm % mm
if stats(k).Circularity > 0.70
grade(k) = 0 % round blob: dirt, not a scratch
elseif a_mm2 >= 0.50 || l_mm >= 5.0
grade(k) = 3
elseif a_mm2 >= 0.10 || l_mm >= 2.5
grade(k) = 2
elseif stats(k).Area >= min_px
grade(k) = 1
else
grade(k) = -1 % speck: ignore
end
end
Worked example on one frame at 50 px/mm, so 1 mm² = 2500 px:
- Region A: Area 1000 px → 1000 / 2500 = 0.40 mm²; MajorAxisLength 280 px → 280 / 50 = 5.6 mm → class 3 on length.
- Region B: Area 400 px → 400 / 2500 = 0.16 mm²; axis 90 px → 90 / 50 = 1.8 mm → class 2 on area.
- Region C: Area 45 px → 45 / 2500 = 0.018 mm²; axis 30 px → 0.6 mm → class 1.
- Region D: Area 700 px → 700 / 2500 = 0.28 mm², but Circularity 0.82 > 0.70 → dirt gate, never graded as a scratch.
| Region | Area (px) | Area (mm²) | Axis (px) | Axis (mm) | Circularity | Grade |
|---|---|---|---|---|---|---|
| A | 1000 | 0.40 | 280 | 5.6 | 0.11 | 3 — reject |
| B | 400 | 0.16 | 90 | 1.8 | 0.19 | 2 — flag |
| C | 45 | 0.018 | 30 | 0.6 | 0.24 | 1 — log |
| D | 700 | 0.28 | 84 | 1.7 | 0.82 | dirt |
Decision for this frame: the part rejects. Region A alone is class 3, and the dirt in region D never reached the scratch grader.
Step 7. Prove the grader before production
Run three checks on golden parts before trusting any grade:
- Synthetic test: set a 250 × 4 px block true in a blank logical image, measure it, and confirm the conversions come back as class 3.
- Replay: grade 20 operator-labeled parts. Every class 3 must match; investigate any miss before the line runs.
- Repeatability: grade one part ten times and again rotated 90°. The classes must hold, or the threshold is riding on lighting drift.
bw_test(150:153, 100:349) = true % 250 x 4 px block = 1000 px in a blank frame
s = regionprops(bw_test, {'Area', 'MajorAxisLength'})
% expect s.Area == 1000 px -> 0.40 mm²; s.MajorAxisLength ~ 250 px -> 5.0 mm -> grade 3
Then repeat the replay weekly with the same golden parts. When grades wander, the lamps or the calibration moved first — check those before touching the thresholds.
When to train a classifier instead of writing rules
Rules win when the classes separate in the feature table, and they are auditable: you can print the exact reason for every reject. Train a small classifier — a decision tree is enough — when the scatter overlaps, for example class 2 and class 3 scratches sharing area and length ranges but differing in contrast or shape. Build the training set from the same regionprops output, one row per labeled region from golden parts, roughly 30 to 50 regions per class, and keep the circularity gate ahead of the model so dirt never reaches it.
Common mistakes
- Cuts set in pixels: they break the day you change the field of view. Millimetres only.
- Grading from total part area instead of per-region features: no class, no location, no audit trail.
- Element diameter smaller than the scratch width: the bottom-hat erases the marks you are chasing.
- Background exposed near 255: contrast headroom is gone before you start.
- Skipping the circularity gate: dirt turns into phantom class 2 and class 3 rejects.
Next step
Size the camera in the camera finder, pick bars in the bar lights category, then configure the station from the camera step. More build-order guides sit on the systems topic page, and for a surface that resists these fixes — curved, black or mirrored — /contact/ with a sample part beats a week of threshold tuning.
Questions
How many pixels do I need across a scratch to grade it?
Two pixels tell you something is there; three to five let you measure width and keep the threshold stable. At the worked example's 50 px/mm, three pixels correspond to a 0.06 mm line. If your smallest class is finer, raise the scale: a 75 px/mm setup resolves about 0.04 mm, reached by shortening the field of view or moving to a higher-resolution camera.
Can I grade scratches from area alone?
No. A 0.28 mm² round dirt dot and a 0.28 mm² hairline 12 mm long would land in the same grade, yet only one is a scratch and only one may matter to your spec. Measure at least area, major-axis length and circularity per region; the circularity gate keeps round contaminants out of the scratch classes entirely.
How much contrast does a scratch need for a stable threshold?
As a working rule, keep at least 25 grey levels between the scratch response and the local background on an 8-bit frame, about 10 percent of full scale. Dark-field bar lighting usually delivers several times that on shiny surfaces. Plot a profile across a known scratch to check the separation, and re-check it after any lamp change.
Where do the class cut values come from?
From your quality spec and a golden set. Image parts carrying operator-agreed class 1, 2 and 3 scratches, tabulate the measured features, and place each cut between the two classes it must separate. The 0.10 and 0.50 mm² and 2.5 and 5.0 mm cuts in this guide are an example of that placement, not a standard; review them whenever the spec or surface finish changes.
When is a trained classifier worth it instead of rules?
When the feature scatter overlaps: class 2 and class 3 scratches share area and length ranges but differ in contrast or shape in ways that are hard to write as rules. You need a labeled set, roughly 30 to 50 regions per class, and a repeatable imaging setup. After any lighting or optics change, re-validate on golden parts before trusting the grades again.



