A threshold separates objects from the background
Image thresholding is the simplest way to separate an object from its background. Pick a grey level and call every pixel brighter than it part of the object. A histogram, a count of how many pixels have each grey level, tells you where to put that level.
In this lesson you read a histogram, choose a threshold and count what it keeps.
By the end of this lesson
Note
I can use a histogram to choose a threshold that separates objects from the background.
Key words
| Word | What it means | Example |
|---|---|---|
| Histogram | a count of the pixels at each grey level | |
| Threshold | a grey level that splits pixels in two | 100 |
| Background | the part of a picture that is not an object | |
| Foreground | the objects you want to find | |
| Mask | a picture of 0 and 1 that marks the pixels you keep |
The cells students fill in are left blank in this preview.
A histogram counts pixels at each grey level
Go through every pixel and tally its grey level. The result is a histogram. Usually the levels are grouped into bands, such as 0 to 63 and 64 to 127, so the counts are easy to read.
A picture with a dark background and bright objects has two clear heaps: one at the dark end and one at the bright end.
Worked example: a histogram of twelve pixels
| Band of grey levels | Pixels in the band | Which pixels |
|---|---|---|
| 0 to 63 | 4 | 10, 60, 30, 20 |
| 64 to 127 | 2 | 90, 70 |
| 128 to 191 | 3 | 130, 190, 140 |
| 192 to 255 | 3 | 200, 250, 255 |
The cells students fill in are left blank in this preview.
Count the road picture in bands of 50
docker exec jupyter python3 -c "import numpy as np; from PIL import Image; a = np.array(Image.open('images/road.png')); print(np.bincount(a.ravel() // 50).tolist())"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Two heaps in a histogram show two groups
The Run reads road.png, a grey road with white lane lines. It divides every grey level by 50, so each pixel lands in a band of 50 levels, and counts them.
The six numbers are [0, 28819, 44273, 0, 3403, 305]. Almost every pixel is in the second and third bands: the dark verge and the road. A few thousand are in the last two: the bright lane lines. The empty band between them is where a threshold belongs.
A threshold turns a picture into a mask
A threshold asks one question of every pixel: is it brighter than this level? If yes, the mask holds 1. If no, it holds 0.
The mask is a binary image. Counting its 1s tells you how much of the picture is object.
Worked example: the same twelve pixels, threshold 100
| Row | Grey levels | Mask |
|---|---|---|
| 0 | 10, 200, 60, 130 | 0, 1, 0, 1 |
| 1 | 250, 30, 190, 90 | 1, 0, 1, 0 |
| 2 | 70, 140, 20, 255 | 0, 1, 0, 1 |
The cells students fill in are left blank in this preview.
Count the pixels above a threshold
grid = [
[10, 200, 60, 130],
[250, 30, 190, 90],
[70, 140, 20, 255],
]
level = 100
kept = 0
for row in grid:
for pixel in row:
if pixel > level:
kept = kept + 1
print("pixels above", level, ":", kept, "of 12")
# Change level to 60 and press Run.
# Then try 255. Why does nothing pass?
Try two thresholds on the road picture
docker exec jupyter python3 -c "import numpy as np; from PIL import Image; a = np.array(Image.open('images/road.png')); print(int((a > 200).sum()), int((a > 90).sum()))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Choose the level in the gap between heaps
The Run prints 3708 48240. A level of 200 keeps only the lane lines, 3708 pixels. A level of 90 keeps the road and the lines, 48240 pixels, because the road is brighter than 90.
Neither is wrong. They answer different questions, so the level must match what you want to find.
Let scikit-image pick a level for the coins
docker exec jupyter python3 -c "import numpy as np; from PIL import Image; from skimage import filters; a = np.array(Image.open('images/coins.png').convert('L')); print(int(filters.threshold_otsu(a)))"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Otsu picks the level that splits two heaps
Otsu's method tries every level and keeps the one that separates the pixels into two groups most cleanly. For the coin picture it prints 63.
That sits in the empty stretch between the dark background and the bright coins, so it is a sound choice. It still only helps when the histogram really has two heaps.
A common mistake
Warning
Many people think one threshold works for a whole picture. It does only if the light is even. On a page lit from one side, the dark ink at the bright edge can be brighter than the plain paper at the dark edge. Look at the mask before you trust a count.
Going further
Open scan.png in a notebook and print a.min(), a.max() and the mean of the left half and the right half.
The light is uneven across the page. Predict what a single threshold would do, then check it by drawing the mask.