Skip to content

Commit d99674e

Browse files
kwagyemanclaude
andcommitted
docs: add Image Processing chapter
33 leaf pages organised into 11 captioned subsections (Foundations, Drawing, Pixel math, Thresholding, Filters and morphology, Tonal and statistical analysis, Geometric transforms, Finding shapes, Decoding, Matching, Image I/O) plus a wrap-up. 14 SVG figures with light/dark theme support via currentColor. Covers the image module end to end: pixel formats, coordinate system, regions and masks, drawing primitives and image composition, arithmetic and bitwise operations, frame differencing, LAB thresholding, linear / morphological / custom-kernel filters with a standard-kernel catalogue, histograms / statistics / tonal corrections, regression and SSIM, scale / flip / crop / lens / perspective / polar transforms, blob / line / circle / rectangle / QR / AprilTag / barcode / DataMatrix detection, template and displacement matching, save and ImageIO + mjpeg/gif recording paths. Closes with an outline of the classical CV pipeline and hands off to the ulab.numpy chapter. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5b6039a commit d99674e

52 files changed

Lines changed: 9790 additions & 13 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
Histograms and statistics
2+
=========================
3+
4+
The sections so far have all *operated* on
5+
images: filtering them, thresholding them,
6+
combining them with other images. The remaining
7+
family of operations does something different.
8+
It *measures* the image -- summarises the
9+
distribution of pixel values, returns the
10+
mean and median brightness, finds the optimal
11+
cutoff between dark and bright pixels, reports
12+
the spread of the colour channels. Those
13+
measurements feed back into the operating side
14+
in two ways: as inputs to the algorithm that
15+
decides what threshold to use, what gain to set,
16+
what the scene's tonal profile looks like; and as
17+
diagnostic signals -- "is the scene bright
18+
enough?" -- that the application can act on
19+
without making a decision about any particular
20+
pixel.
21+
22+
The starting point for almost every measurement
23+
is the *histogram*.
24+
25+
The histogram
26+
-------------
27+
28+
A histogram of an image is a count of how many
29+
pixels have each possible brightness value.
30+
For a grayscale image, that is a list of
31+
counts indexed by the values ``0`` through
32+
``255``. For a colour image, it is three such
33+
lists -- one per channel.
34+
35+
:meth:`~image.Image.get_histogram` computes
36+
one:
37+
38+
::
39+
40+
h = img.get_histogram()
41+
42+
The returned object is a :class:`histogram`
43+
result that exposes the per-channel bin lists
44+
and a few high-level queries on them. The bin
45+
counts are *normalised* so that they sum to
46+
``1.0`` -- the histogram describes the
47+
profile of the distribution rather than the
48+
absolute pixel count, which makes the
49+
measurements comparable across images of
50+
different sizes.
51+
52+
For grayscale images the histogram has one
53+
channel of bins, available as
54+
``h.bins()`` (or equivalently ``h[0]``). For
55+
RGB565 images the histogram is computed in
56+
the *LAB* colour space introduced on the
57+
binary-thresholding page, with three bin
58+
channels available as ``h.l_bins()``,
59+
``h.a_bins()``, ``h.b_bins()`` (or
60+
``h[0]``, ``h[1]``, ``h[2]``). LAB is the
61+
same choice the threshold and tracking
62+
methods use; histograms agree with
63+
thresholds about what space colour is being
64+
measured in.
65+
66+
Bins and the bin count
67+
----------------------
68+
69+
The default histogram has one bin per
70+
possible pixel value -- ``256`` bins for an
71+
8-bit channel. Sometimes that is finer
72+
resolution than the application needs. A
73+
classifier that only cares about the rough
74+
profile of the distribution might be better
75+
served by a smaller bin count -- ``32`` or
76+
even ``8`` bins -- which both runs faster
77+
and produces a cleaner result against noise.
78+
The ``bins`` keyword (and the per-channel
79+
``l_bins``, ``a_bins``, ``b_bins`` for colour)
80+
sets the count:
81+
82+
::
83+
84+
h = img.get_histogram(bins=32)
85+
86+
ROI and threshold scoping work the same way
87+
as on every other measurement method. Pass
88+
an ``roi`` to confine the histogram to a
89+
rectangle of pixels; pass a ``thresholds``
90+
list to include only pixels that match those
91+
ranges. The threshold form is what makes
92+
"compute the histogram of the matching
93+
pixels only" a one-call operation -- a
94+
common pattern when an application wants
95+
to characterise the texture of an
96+
already-detected region without having to
97+
walk the pixels itself.
98+
99+
.. figure:: ../figures/histogram-with-overlays.svg
100+
:alt: A grayscale histogram drawn as a row
101+
of bars across the brightness range 0
102+
to 255. The distribution has two
103+
peaks -- a smaller dark peak and a
104+
larger bright peak -- separated by a
105+
clear valley. Three vertical lines
106+
are overlaid: the Otsu threshold in
107+
the valley, the mean shifted toward
108+
the larger bright peak, and the
109+
median further right where cumulative
110+
pixel count reaches one-half.
111+
112+
A grayscale histogram with three summary
113+
measurements overlaid: Otsu's threshold
114+
(the cutoff that best splits the dark and
115+
bright clusters), the mean, and the
116+
median. Each measurement says something
117+
different about the same distribution.
118+
119+
Statistics
120+
----------
121+
122+
A histogram is a description of every value's
123+
prevalence; *statistics* are the numerical
124+
summaries derived from it. The
125+
:class:`statistics` object returned by
126+
:meth:`~image.Image.get_statistics` carries
127+
the standard set:
128+
129+
* ``mean`` -- the arithmetic mean of pixel
130+
values.
131+
* ``median`` -- the value below which half
132+
the pixels lie.
133+
* ``mode`` -- the most common single value.
134+
* ``stdev`` -- the standard deviation, a
135+
measure of the spread around the mean.
136+
* ``min`` and ``max`` -- the brightest
137+
and darkest pixel values present.
138+
* ``lq`` and ``uq`` -- the lower and upper
139+
quartile cutoffs.
140+
141+
For an RGB565 image the per-channel forms
142+
(``l_mean``, ``a_median``, ``b_mode``,
143+
and so on) deliver the same measurements
144+
channel by channel.
145+
146+
Most of those numbers come up in specific
147+
contexts. ``mean`` and ``stdev`` together
148+
give a noise estimate: a scene that should
149+
be uniform has small stdev, while a noisy
150+
sensor gives the same scene a larger stdev.
151+
``min`` and ``max`` give the *contrast* of
152+
the image: the closer they are, the flatter
153+
the scene; the further apart, the more
154+
dynamic range the algorithm has to work
155+
with. ``median`` is the robust centre when
156+
the distribution has outliers (a few very
157+
bright pixels do not pull the median the
158+
way they pull the mean). ``mode`` is the
159+
single most-common value, useful for finding
160+
the background level of an image whose
161+
background covers most of the pixels.
162+
163+
:meth:`~image.Image.get_statistics` runs the
164+
histogram pass internally and then summarises
165+
it; passing the same ``thresholds`` and
166+
``roi`` arguments as a previously-computed
167+
histogram produces the statistics for the
168+
same set of pixels.
169+
170+
Percentiles and CDF lookups
171+
---------------------------
172+
173+
The :class:`histogram` object exposes a
174+
``get_percentile`` method that turns a
175+
fraction into a pixel value -- the value
176+
below which the requested fraction of pixels
177+
lies. ``h.get_percentile(0.5)`` is the
178+
median; ``h.get_percentile(0.05)`` and
179+
``h.get_percentile(0.95)`` together give a
180+
robust min/max that ignores the bottom and
181+
top 5% as outliers.
182+
183+
That is the form an application uses when it
184+
wants to characterise the *range* of pixel
185+
values without letting a handful of stray
186+
bright or dark pixels skew the answer. The
187+
robust min/max from the 5th and 95th
188+
percentiles is also the natural input to a
189+
contrast-stretching pass -- the per-pixel
190+
remap that Tonal corrections covers.
191+
192+
Otsu's method
193+
-------------
194+
195+
Histograms answer another question worth
196+
calling out on its own: given an image whose
197+
pixels split into a "dark" and a "bright"
198+
cluster, what is the cutoff between them?
199+
The thresholding page already named the
200+
mechanism by its result -- a single global
201+
threshold the application can hand to
202+
:meth:`~image.Image.binary` -- but deferred
203+
the *how*. The how is *Otsu's method*, and it
204+
lives on the histogram.
205+
206+
The intuition: an image with a clear
207+
foreground and background has *two* clusters
208+
in its brightness histogram, with a valley
209+
between them. The right place to threshold is
210+
the bottom of the valley -- the value where
211+
the two clusters are best separated. Otsu's
212+
method searches every possible cutoff and
213+
picks the one where the within-cluster
214+
variances are smallest (which is the same as
215+
saying the between-cluster variance is
216+
largest), and the result is the optimal
217+
binary split for that particular image's
218+
distribution.
219+
220+
The :class:`histogram` object exposes Otsu
221+
through ``get_threshold``:
222+
223+
::
224+
225+
h = img.get_histogram()
226+
t = h.get_threshold()
227+
228+
The returned :class:`threshold` object has
229+
``value`` (for grayscale) or
230+
``l_value`` / ``a_value`` / ``b_value``
231+
(for colour) attributes carrying the chosen
232+
cutoff. Feeding the result straight back into
233+
:meth:`~image.Image.binary` gives a
234+
self-tuning global threshold whose cutoff is
235+
chosen by the image itself:
236+
237+
::
238+
239+
img.binary([(t.value, 255)])
240+
241+
That pattern does not solve the
242+
uneven-illumination problem the
243+
filter-based adaptive threshold solves; what
244+
it solves is the "what value should I cut
245+
at?" question when global thresholding is
246+
already the right approach. For a scene
247+
whose foreground / background distinction
248+
is well-defined, the value Otsu picks is
249+
usually within a few units of what a human
250+
would pick by eye.
251+
252+
Computing on a difference image
253+
-------------------------------
254+
255+
A useful detail on
256+
:meth:`~image.Image.get_histogram` and
257+
:meth:`~image.Image.get_statistics`: both
258+
accept a ``difference`` keyword that takes
259+
another :class:`Image` and computes the
260+
histogram (or statistics) of the per-pixel
261+
difference between the source and that image,
262+
*without* allocating a separate difference
263+
image. That is the cheap way to ask "how
264+
much has the scene changed since the
265+
reference frame?" without paying for an
266+
explicit :meth:`~image.Image.difference`
267+
call to produce an image whose only purpose
268+
is to be measured. For a continuously
269+
running motion-detection script, the saving
270+
adds up.
271+
272+
With histograms describing distributions,
273+
statistics summarising them, percentiles for
274+
robust range estimates, and Otsu's method
275+
for the optimal binary split, the
276+
measurement side of the toolkit is ready.
277+
The remaining tonal work is about *acting*
278+
on what the measurements say -- adjusting the
279+
image's contrast, brightness, or colour to
280+
correct for what the histogram revealed
281+
about it.

0 commit comments

Comments
 (0)