Repository: pyqtgraph/pyqtgraph

Issue:
setImage(..) autoLevels not considering all data when calculating min,max
<!-- In the following, please describe your issue in detail! -->
<!-- If some of the sections do not apply, just remove them. -->

### Short description

Since `setImage` skips pixels for large images, the max and min values are off. Therefore the `levels` determined from the data is incorrect, causing an apparent empty image.

Please see `setImage` in `ImageItem.py` and the screenshot below.

![image](https://user-images.githubusercontent.com/4085287/109031439-05a0ef00-76c5-11eb-8326-3f5844b7ed31.png)


### Expected behavior

The levels should have been determined correctly.



### Real behavior

Since only a subset of the image is considered, the min and max values are off resulting in a wrong look up table. Note that in my field (physics) single pixel 'hits' are very common, and this behavior results in empty images _sometimes_, depending on whether the pixel happens to be part of the image considered for the minimum and maximum values. I spent multiple hours debugging this..



In any case, thanks for all the work you all have put into this project.




Discussion:

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py
--- a/pyqtgraph/graphicsItems/ImageItem.py
+++ b/pyqtgraph/graphicsItems/ImageItem.py
@@ -341,8 +341,14 @@ def setImage(self, image=None, autoLevels=None, **kargs):
             or dark points in the data set.
             
             If False, the search will be omitted.
-            
+
             The default is `False` if a ``levels`` keyword argument is given, and `True` otherwise.
+            
+        levelSamples: int, default 65536
+            When determining minimum and maximum values, ImageItem
+            only inspects a subset of pixels no larger than this number.
+            Setting this larger than the total number of pixels considers all values.
+            
         """
         profile = debug.Profiler()
 
@@ -378,10 +384,8 @@ def setImage(self, image=None, autoLevels=None, **kargs):
             else:
                 autoLevels = True
         if autoLevels:
-            img = self.image
-            while img.size > 2**16:
-                img = img[::2, ::2]
-            mn, mx = self._xp.nanmin(img), self._xp.nanmax(img)
+            level_samples = kargs.pop('levelSamples', 2**16) 
+            mn, mx = self.quickMinMax( targetSize=level_samples )
             # mn and mx can still be NaN if the data is all-NaN
             if mn == mx or self._xp.isnan(mn) or self._xp.isnan(mx):
                 mn = 0
@@ -457,11 +461,15 @@ def quickMinMax(self, targetSize=1e6):
         Returns (`min`, `max`).
         """
         data = self.image
-        while data.size > targetSize:
-            ax = self._xp.argmax(data.shape)
-            sl = [slice(None)] * data.ndim
-            sl[ax] = slice(None, None, 2)
-            data = data[sl]
+        if targetSize < 2: # keep at least two pixels
+            targetSize = 2
+        while True:
+            h, w = data.shape[:2]
+            if h * w <= targetSize: break
+            if h > w:
+                data = data[::2, ::] # downsample first axis
+            else:
+                data = data[::, ::2] # downsample second axis
         return self._xp.nanmin(data), self._xp.nanmax(data)
 
     def updateImage(self, *args, **kargs):


Test patch: