Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 3 additions & 64 deletions docs/reference/ImageStat.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,67 +7,6 @@
The :py:mod:`~PIL.ImageStat` module calculates global statistics for an image, or
for a region of an image.

.. py:class:: Stat(image_or_list, mask=None)

Calculate statistics for the given image. If a mask is included,
only the regions covered by that mask are included in the
statistics. You can also pass in a previously calculated histogram.

:param image: A PIL image, or a precalculated histogram.

.. note::

For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.

:param mask: An optional mask.

.. py:attribute:: extrema

Min/max values for each band in the image.

.. note::

This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
simply returns the low and high bins used. This is correct for
images with 8 bits per channel, but fails for other modes such as
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
return per-band extrema for the image. This is more correct and
efficient because, for non-8-bit modes, the histogram method uses
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.

.. py:attribute:: count

Total number of pixels for each band in the image.

.. py:attribute:: sum

Sum of all pixels for each band in the image.

.. py:attribute:: sum2

Squared sum of all pixels for each band in the image.

.. py:attribute:: mean

Average (arithmetic mean) pixel level for each band in the image.

.. py:attribute:: median

Median pixel level for each band in the image.

.. py:attribute:: rms

RMS (root-mean-square) for each band in the image.

.. py:attribute:: var

Variance for each band in the image.

.. py:attribute:: stddev

Standard deviation for each band in the image.
.. autoclass:: Stat
:members:
:special-members: __init__
102 changes: 68 additions & 34 deletions src/PIL/ImageStat.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,61 @@
from __future__ import annotations

import math
from functools import cached_property

from . import Image


class Stat:
def __init__(self, image_or_list, mask=None):
try:
def __init__(
self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None
) -> None:
"""
Calculate statistics for the given image. If a mask is included,
only the regions covered by that mask are included in the
statistics. You can also pass in a previously calculated histogram.

:param image: A PIL image, or a precalculated histogram.

.. note::

For a PIL image, calculations rely on the
:py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
grouped into 256 bins, even if the image has more than 8 bits per
channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
of more than 255.

:param mask: An optional mask.
"""
if isinstance(image_or_list, Image.Image):
if mask:
self.h = image_or_list.histogram(mask)
else:
self.h = image_or_list.histogram()
except AttributeError:
self.h = image_or_list # assume it to be a histogram list
else:
self.h = image_or_list
if not isinstance(self.h, list):
msg = "first argument must be image or list"
msg = "first argument must be image or list" # type: ignore[unreachable]
raise TypeError(msg)
self.bands = list(range(len(self.h) // 256))

def __getattr__(self, id):
"""Calculate missing attribute"""
if id[:4] == "_get":
raise AttributeError(id)
# calculate missing attribute
v = getattr(self, "_get" + id)()
setattr(self, id, v)
return v

def _getextrema(self):
"""Get min/max values for each band in the image"""

def minmax(histogram):
@cached_property
def extrema(self) -> list[tuple[int, int]]:
"""
Min/max values for each band in the image.

.. note::
This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
simply returns the low and high bins used. This is correct for
images with 8 bits per channel, but fails for other modes such as
``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
return per-band extrema for the image. This is more correct and
efficient because, for non-8-bit modes, the histogram method uses
:py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
"""

def minmax(histogram: list[int]) -> tuple[int, int]:
res_min, res_max = 255, 0
for i in range(256):
if histogram[i]:
Expand All @@ -65,12 +91,14 @@ def minmax(histogram):

return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]

def _getcount(self):
"""Get total number of pixels in each layer"""
@cached_property
def count(self) -> list[int]:
"""Total number of pixels for each band in the image."""
return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]

def _getsum(self):
"""Get sum of all pixels in each layer"""
@cached_property
def sum(self) -> list[float]:
"""Sum of all pixels for each band in the image."""

v = []
for i in range(0, len(self.h), 256):
Expand All @@ -80,8 +108,9 @@ def _getsum(self):
v.append(layer_sum)
return v

def _getsum2(self):
"""Get squared sum of all pixels in each layer"""
@cached_property
def sum2(self) -> list[float]:
"""Squared sum of all pixels for each band in the image."""

v = []
for i in range(0, len(self.h), 256):
Expand All @@ -91,12 +120,14 @@ def _getsum2(self):
v.append(sum2)
return v

def _getmean(self):
"""Get average pixel level for each layer"""
@cached_property
def mean(self) -> list[float]:
"""Average (arithmetic mean) pixel level for each band in the image."""
return [self.sum[i] / self.count[i] for i in self.bands]

def _getmedian(self):
"""Get median pixel level for each layer"""
@cached_property
def median(self) -> list[int]:
"""Median pixel level for each band in the image."""

v = []
for i in self.bands:
Expand All @@ -110,19 +141,22 @@ def _getmedian(self):
v.append(j)
return v

def _getrms(self):
"""Get RMS for each layer"""
@cached_property
def rms(self) -> list[float]:
"""RMS (root-mean-square) for each band in the image."""
return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]

def _getvar(self):
"""Get variance for each layer"""
@cached_property
def var(self) -> list[float]:
"""Variance for each band in the image."""
return [
(self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
for i in self.bands
]

def _getstddev(self):
"""Get standard deviation for each layer"""
@cached_property
def stddev(self) -> list[float]:
"""Standard deviation for each band in the image."""
return [math.sqrt(self.var[i]) for i in self.bands]


Expand Down