|
| 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