File size: 5,485 Bytes
3f7c971
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#
# The Python Imaging Library.
# $Id$
#
# global image statistics
#
# History:
# 1996-04-05 fl   Created
# 1997-05-21 fl   Added mask; added rms, var, stddev attributes
# 1997-08-05 fl   Added median
# 1998-07-05 hk   Fixed integer overflow error
#
# Notes:
# This class shows how to implement delayed evaluation of attributes.
# To get a certain value, simply access the corresponding attribute.
# The __getattr__ dispatcher takes care of the rest.
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996-97.
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations

import math
from functools import cached_property

from . import Image


class Stat:
    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):
            self.h = image_or_list.histogram(mask)
        elif isinstance(image_or_list, list):
            self.h = image_or_list
        else:
            msg = "first argument must be image or list"  # type: ignore[unreachable]
            raise TypeError(msg)
        self.bands = list(range(len(self.h) // 256))

    @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]:
                    res_min = i
                    break
            for i in range(255, -1, -1):
                if histogram[i]:
                    res_max = i
                    break
            return res_min, res_max

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

    @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)]

    @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):
            layer_sum = 0.0
            for j in range(256):
                layer_sum += j * self.h[i + j]
            v.append(layer_sum)
        return v

    @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):
            sum2 = 0.0
            for j in range(256):
                sum2 += (j**2) * float(self.h[i + j])
            v.append(sum2)
        return v

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

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

        v = []
        for i in self.bands:
            s = 0
            half = self.count[i] // 2
            b = i * 256
            for j in range(256):
                s = s + self.h[b + j]
                if s > half:
                    break
            v.append(j)
        return v

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

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

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


Global = Stat  # compatibility