Image Thresholding
A bright seed on a dark tray gives us a first rule to try. Compare each pixel with a cutoff, keep the result as a mask, and inspect where that rule fails.
13 sections · 4 questions · 2 coding challenges
The arrow keys move between sections too.
Finding a Cutoff Without Inspecting Every Pixel
BOTIE
The useful intuition is to seek two reasonably compact intensity groups. That works only when intensity is a suitable way to separate the objects we care about. Shadows or similar foreground and background intensities can defeat a single global cutoff.
Can Brightness Separate the Seeds From the Tray?
We have numbers for the photograph, but Botie still needs to know which positions might belong to a seed. In this image the seeds are light and the tray is dark. That gives us a simple starting point: mark positions whose intensity exceeds a chosen cutoff.
Before asking a model to learn that rule, let's see what we get by writing it ourselves. We will also move a shadow across the tray, because a rule that works under one lamp may not work under another.
The invented tray image is small enough to count by eye.
Step 1 of 4
Inspect the intensities
Our invented seed contributes two bright pixels. The remaining positions belong to the dark tray.
Photograph
Step 2 of 4
Ask the same question at each position
With a strict cutoff of 100, mark one when the intensity exceeds 100 and zero otherwise.
Foreground mask
Step 3 of 4
A shadow can break the rule
Here a seed in shadow is darker than part of the lit tray. No single cutoff can separate these two observations in the intended direction.
Step 4 of 4
A pixel count is not yet a seed count
The mask tells us which positions passed the brightness test. Several selected pixels may belong to one seed, and two touching seeds may appear as one region. We will need spatial grouping before calling the result a count of objects.
Selected positions
A mask records a decision at every pixel
The comparison preserves positions. Adding the flattened mask counts selected pixels without pretending they are separate objects.
Which bright pixels belong in the mask?
Raise the cutoff through the image's brightness values. Watch selected positions disappear, then compare the number of selected pixels with the number of objects you can see.
BOTIE
Selected positions and their pixel count
| Column | |||
|---|---|---|---|
| Row | 0 | 1 | 2 |
| 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 0 |
| 2 | 0 | 1 | 0 |
Selected pixel count
2
Follow the operations
1. Tray intensities
10 20 10 20 220 30 10 200 20 2. Foreground cutoff
100
3. Strict threshold
0 0 0 0 1 0 0 1 0 4. Flatten a list
- 00
- 10
- 20
- 30
- 41
- 50
- 60
- 71
- 80
5. Add a list
2
6. Collect two results
[ [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 0, 1, 0 ] ], 2 ]7. Result
[ [ [ 0, 0, 0 ], [ 0, 1, 0 ], [ 0, 1, 0 ] ], 2 ]
The first result is a mask; the second is a pixel count. A global brightness cutoff can fail under shadows or similar foreground and background intensities.
1. Store a Decision at Every Position
A binary mask keeps the shape of the image but stores a yes-or-no decision at each position. In our convention, one means foreground and zero means background. NumPy comparisons first produce Boolean values; convert to integers when we want to display zeros and ones.
We must specify what happens at the cutoff. Here, a pixel equal to the cutoff remains background.
mask[row, column] = 1 if image[row, column] > cutoff else 0
100 > 100 is False
101 > 100 is TrueForeground means the rule selected that position. It does not establish that the position really belongs to a seed.
2. Inspect the Mask Before Summarising It
If we add the mask's ones, we count selected pixels. Taking its mean gives the selected fraction of the image. But Botie wanted seeds, and one seed can occupy many pixels.
Put the mask beside the photograph. Does the selected region follow each seed? That is the first check to make before we try to turn regions into an object count.
Selected pixels = 2
Total pixels = 9
Selected share = 2 / 9 ≈ 0.2222Two selected pixels in this tiny image describe one connected region. A later lesson will make that distinction explicit.
Create and inspect the mask
Compare all positions at once while preserving the grid.
import numpy as np
image = np.array([[10,20,10], [20,220,30], [10,200,20]])
mask = image > 100
print(mask.astype(int).tolist())
print(int(mask.sum()))[[0, 0, 0], [0, 1, 0], [0, 1, 0]]
2The sum measures area in pixels, not an object count.
3. Move the Cutoff and Look at the Tradeoff
Lowering the cutoff admits more pixels. This may recover a dim seed, but it can also include the tray. Raising it does the reverse. Choose a cutoff using representative training or development images, then check it on held-out images.
A histogram counts how many pixels have each intensity. A gap between dark and bright groups suggests possible cutoffs. Otsu's method formalises a particular criterion for selecting one from that histogram.
A histogram ignores where pixels are located. Two images with the same histogram receive the same Otsu threshold even if their objects are arranged differently.
4. Ask Whether One Reference Fits the Whole Image
Uneven illumination changes the background across a photograph. A local threshold compares a pixel with a reference estimated around that position, often a local mean or weighted mean with an offset.
The neighbourhood must be chosen carefully. If it is too small, the object itself can dominate the reference. If it is too large, the reference may fail to follow illumination changes. Border padding also affects these local estimates.
Example local reference = 70
Chosen offset = 5
Local cutoff = 70 - 5 = 65
Pixel intensity = 80
80 > 65, so this rule selects the pixelThe offset's sign here follows a mean-minus-offset convention. Local thresholding adds assumptions; it does not guarantee correct segmentation.
5. Decide What This Step Can Hand to the Next One
The mask is a useful intermediate result. We can clean small speckles, find connected regions, or measure selected area. But a brightness rule cannot recognise an object whose appearance overlaps with the background.
In the workshop, produce the mask and count the selected pixels. Try an equal-to-cutoff value and an entirely dark tray as well as the original example.
We will address shapes and connected regions next. A learned segmentation model becomes useful when the decision requires more than a brightness comparison.
6. A Bright Background Can Defeat One Global Cutoff
A shadow can make a seed darker than a well-lit patch of tray. No single intensity cutoff then separates both correctly. Moving the cutoff merely trades one kind of mistake for another.
Inspect the photograph and mask together. Local reference rules, controlled lighting or a learned segmenter can address different causes of failure. Do not keep adjusting a global threshold when the required distinction is absent from the intensity alone.
7. Compare Pixel Decisions with a Labelled Mask
A supervised assessment needs a mask that states which pixels belong to the seed. Count overlap and extra selected pixels against that reference. Intersection over union penalises both missed seed pixels and selected background.
An all-background prediction can have high pixel accuracy when seeds occupy little of the image. That is why the metric must reflect the foreground task. Specify how empty prediction and empty reference masks are handled.
Score a small seed mask
Each row below preserves its image position. The predicted mask misses one seed pixel and adds one background pixel.
import numpy as np
truth = np.array([[0,1,1],[0,1,0]], dtype=bool)
pred = np.array([[0,1,0],[0,1,1]], dtype=bool)
intersection = (truth & pred).sum()
union = (truth | pred).sum()
print(int(intersection), int(union))
print(float(intersection/union))2 4
0.5The overlap score describes segmentation on this labelled image. It is not the same as the accuracy of the later seed count.
Check yourself
Questions on Parts 1 to 7
Question 1 of 4
Which mask follows the rule?
Use the information below.
Intensities: [30, 100, 140]
Rule: foreground when intensity > 100.Try it with Botie
Build with Botie
Write and run Python