Repository: python-pillow/Pillow

Issue:
ZeroDivisionError in ImageStat
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

Called `ImageState.Stat.rms` and for some images, we get a ZeroDivisionError. We do not have access to the images that triggered it and were not able to reproduce yet.

### What did you expect to happen?

I expect Pillow to validate before dividing by zero.

### What actually happened?

An error was thrown

### What are your OS, Python and Pillow versions?

* OS: Windows 11, and Ubuntu
* Python: 3.10.15
* Pillow: 10.4.0

```
  File "/usr/local/lib/python3.10/functools.py", line 981, in __get__
    val = self.func(instance)
  File "/usr/local/lib/python3.10/site-packages/PIL/ImageStat.py", line 144, in rms
    return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
  File "/usr/local/lib/python3.10/site-packages/PIL/ImageStat.py", line 144, in <listcomp>
    return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
ZeroDivisionError: float division by zero
```

```python
    diff = ImageChops.difference(img1, img2)
    stat = ImageStat.Stat(diff, mask=binary_mask)
    rms = sum([v**2 for v in stat.rms]) ** 0.5
```



Discussion:
Hi. As you will have gathered from the traceback, `self.count[i]` is zero.

`count` is the https://pillow.readthedocs.io/en/stable/reference/ImageStat.html#PIL.ImageStat.Stat.count
> Total number of pixels for each band in the image.

The problem can be replicated quite simply by generating an image that is zero width by zero height.
```pycon
>>> from PIL import Image, ImageStat
>>> im = Image.new("RGB", (0, 0))
>>> stat = ImageStat.Stat(im)
>>> stat.count
[0, 0, 0]
>>> stat.rms
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "functools.py", line 993, in __get__
    val = self.func(instance)
  File "PIL/ImageStat.py", line 144, in rms
    return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
  File "PIL/ImageStat.py", line 144, in <listcomp>
    return [math.sqrt(self.sum2[i] / self.count[i]) for i in self.bands]
ZeroDivisionError: float division by zero
```

> I expect Pillow to validate before dividing by zero.

Are you saying that Pillow should throw a custom error when `ImageStat.Stat()` is called with a zero by zero image? I would have thought a neater option would just be to return zero values as the `rms`.

Also, you have said that you are using Pillow 10.4.0. I don't know what reason you have for not upgrading to the latest version, but be aware that if you wanted any fix we make for this, you would have to upgrade to at least Pillow 12.0.0, as that is the next release we have planned.
I've created https://github.com/python-pillow/Pillow/pull/9105 to return zero values.

If you do find the image that triggered this, please let us know.



Patch:
diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py
index 8bc504526f0..3a1044ba449 100644
--- a/src/PIL/ImageStat.py
+++ b/src/PIL/ImageStat.py
@@ -120,7 +120,7 @@ def sum2(self) -> list[float]:
     @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]
+        return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
 
     @cached_property
     def median(self) -> list[int]:
@@ -141,13 +141,20 @@ def median(self) -> list[int]:
     @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]
+        return [
+            math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
+            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]
+            (
+                (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+                if self.count[i]
+                else 0
+            )
             for i in self.bands
         ]
 


Test patch:
diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py
index 0dfbc5a2abb..0baab7ce21e 100644
--- a/Tests/test_imagestat.py
+++ b/Tests/test_imagestat.py
@@ -57,3 +57,13 @@ def test_constant() -> None:
     assert st.rms[0] == 128
     assert st.var[0] == 0
     assert st.stddev[0] == 0
+
+
+def test_zero_count() -> None:
+    im = Image.new("L", (0, 0))
+
+    st = ImageStat.Stat(im)
+
+    assert st.mean == [0]
+    assert st.rms == [0]
+    assert st.var == [0]