code: #!/usr/bin/env python
  """ ViewGeoTIFF.py: PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
  
  """
  
  import os.path
  try:
      from PyQt5.QtCore import Qt, QRectF, pyqtSignal, QT_VERSION_STR
      from PyQt5.QtGui import QImage, QPixmap, QPainterPath
      from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QFileDialog
  except ImportError:
      try:
          from PyQt4.QtCore import Qt, QRectF, pyqtSignal, QT_VERSION_STR
          from PyQt4.QtGui import QGraphicsView, QGraphicsScene, QImage, QPixmap, QPainterPath, QFileDialog
      except ImportError:
          raise ImportError("ViewGeoTIFF: Requires PyQt5 or PyQt4.")
  import rasterio
  import numpy as np
  from pathlib import Path
  
  __author__ = "Konrad Heidler <k.heidler@tum.de>, Marcel Goldschen-Ohm <marcel.goldschen@gmail.com>"
  __version__ = '0.1.0'
  
  
  class ViewGeoTIFF(QGraphicsView):
      """ PyQt image viewer widget for a QPixmap in a QGraphicsView scene with mouse zooming and panning.
  
      Displays a QImage or QPixmap (QImage is internally converted to a QPixmap).
      To display any other image format, you must first convert it to a QImage or QPixmap.
  
      Some useful image format conversion utilities:
          qimage2ndarray: NumPy ndarray <==> QImage    (https://github.com/hmeine/qimage2ndarray)
          ImageQt: PIL Image <==> QImage  (https://github.com/python-pillow/Pillow/blob/master/PIL/ImageQt.py)
  
      Mouse interaction:
          Left mouse button drag: Pan image.
          Right mouse button drag: Zoom box.
          Right mouse button doubleclick: Zoom to show entire image.
      """
  
      # Mouse button signals emit image scene (x, y) coordinates.
      # !!! For image (row, column) matrix indexing, row = y and column = x.
      leftMouseButtonPressed = pyqtSignal(float, float)
      rightMouseButtonPressed = pyqtSignal(float, float)
      leftMouseButtonReleased = pyqtSignal(float, float)
      rightMouseButtonReleased = pyqtSignal(float, float)
      leftMouseButtonDoubleClicked = pyqtSignal(float, float)
      rightMouseButtonDoubleClicked = pyqtSignal(float, float)
  
      def __init__(self, app):
          QGraphicsView.__init__(self)
          self.app = app
  
          # Image is displayed as a QPixmap in a QGraphicsScene attached to this QGraphicsView.
          self.scene = QGraphicsScene()
          self.setScene(self.scene)
  
          # Store a local handle to the scene's current image pixmap.
          self._pixmapHandle = None
  
          # Image aspect ratio mode.
          # !!! ONLY applies to full image. Aspect ratio is always ignored when zooming.
          #   Qt.IgnoreAspectRatio: Scale image to fit viewport.
          #   Qt.KeepAspectRatio: Scale image to fit inside viewport, preserving aspect ratio.
          #   Qt.KeepAspectRatioByExpanding: Scale image to fill the viewport, preserving aspect ratio.
          self.aspectRatioMode = Qt.KeepAspectRatio
  
          # Scroll bar behaviour.
          #   Qt.ScrollBarAlwaysOff: Never shows a scroll bar.
          #   Qt.ScrollBarAlwaysOn: Always shows a scroll bar.
          #   Qt.ScrollBarAsNeeded: Shows a scroll bar only when zoomed.
          self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
          self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  
          # Stack of QRectF zoom boxes in scene coordinates.
          self.zoomStack = []
  
          # Flags for enabling/disabling mouse interaction.
          self.canZoom = True
          self.canPan = True
  
          # Path of current file
          self.currentFile = None
  
  
      def hasImage(self):
          """ Returns whether or not the scene contains an image pixmap.
          """
          return self._pixmapHandle is not None
  
      def clearImage(self):
          """ Removes the current image pixmap from the scene if it exists.
          """
          if self.hasImage():
              self.scene.removeItem(self._pixmapHandle)
              self._pixmapHandle = None
  
      def pixmap(self):
          """ Returns the scene's current image pixmap as a QPixmap, or else None if no image exists.
          :rtype: QPixmap | None
          """
          if self.hasImage():
              return self._pixmapHandle.pixmap()
          return None
  
      def image(self):
          """ Returns the scene's current image pixmap as a QImage, or else None if no image exists.
          :rtype: QImage | None
          """
          if self.hasImage():
              return self._pixmapHandle.pixmap().toImage()
          return None
  
      def setImage(self, image):
          """ Set the scene's current image pixmap to the input QImage or QPixmap.
          Raises a RuntimeError if the input image has type other than QImage or QPixmap.
          :type image: QImage | QPixmap
          """
          if type(image) is QPixmap:
              pixmap = image
          elif type(image) is QImage:
              pixmap = QPixmap.fromImage(image)
          else:
              raise RuntimeError("ImageViewer.setImage: Argument must be a QImage or QPixmap.")
          if self.hasImage():
              self._pixmapHandle.setPixmap(pixmap)
          else:
              self._pixmapHandle = self.scene.addPixmap(pixmap)
          self.setSceneRect(QRectF(pixmap.rect()))  # Set scene size to image size.
          self.updateViewer()
  
      def loadImageFromFile(self, fileName=None):
          """ Load an image from file.
          Without any arguments, loadImageFromFile() will popup a file dialog to choose the image file.
          With a fileName argument, loadImageFromFile(fileName) will attempt to load the specified image file directly.
          """
          if fileName is None:
              if QT_VERSION_STR[0] == '4':
                  fileName = QFileDialog.getOpenFileName(self, "Open image file.")
              elif QT_VERSION_STR[0] == '5':
                  fileName, _ = QFileDialog.getOpenFileName(self, "Open image file.")
          self.currentFile = Path(fileName)
          self.updateFolderListing()
          self.loadImage()
  
      def loadImage(self):
          if self.currentFile.exists():
              with rasterio.open(self.currentFile) as raster:
                  r = raster.read(4)
                  g = raster.read(3)
                  b = raster.read(2)
  
              img = np.stack([r, g, b], axis=-1)
              img = (255 * np.clip(img / 3000, 0, 1)).astype(np.uint8)
  
              height, width = img.shape[:2]
              image = QImage(img, width, height, 3*width, QImage.Format_RGB888)
  
              self.setImage(image)
              self.setWindowTitle(f'{self.currentFile.name} ({self.folderIndex+1} / {len(self.folderListing)})')
          else:
              print(f"Trying to load Image at {self.currentFile}, which doesn't exist!")
  
  
      def updateFolderListing(self):
          self.folderListing = list(sorted(self.currentFile.parent.glob('*.tif')))
          self.folderIndex = self.folderListing.index(self.currentFile)
  
      def updateViewer(self):
          """ Show current zoom (if showing entire image, apply current aspect ratio mode).
          """
          if not self.hasImage():
              return
          if len(self.zoomStack) and self.sceneRect().contains(self.zoomStack[-1]):
              self.fitInView(self.zoomStack[-1], Qt.IgnoreAspectRatio)  # Show zoomed rect (ignore aspect ratio).
          else:
              self.zoomStack = []  # Clear the zoom stack (in case we got here because of an invalid zoom).
              self.fitInView(self.sceneRect(), self.aspectRatioMode)  # Show entire image (use current aspect ratio mode).
  
      def resizeEvent(self, event):
          """ Maintain current zoom on resize.
          """
          self.updateViewer()
  
      def mousePressEvent(self, event):
          """ Start mouse pan or zoom mode.
          """
          scenePos = self.mapToScene(event.pos())
          if event.button() == Qt.LeftButton:
              if self.canPan:
                  self.setDragMode(QGraphicsView.ScrollHandDrag)
              self.leftMouseButtonPressed.emit(scenePos.x(), scenePos.y())
          elif event.button() == Qt.RightButton:
              if self.canZoom:
                  self.setDragMode(QGraphicsView.RubberBandDrag)
              self.rightMouseButtonPressed.emit(scenePos.x(), scenePos.y())
          QGraphicsView.mousePressEvent(self, event)
  
      def mouseReleaseEvent(self, event):
          """ Stop mouse pan or zoom mode (apply zoom if valid).
          """
          QGraphicsView.mouseReleaseEvent(self, event)
          scenePos = self.mapToScene(event.pos())
          if event.button() == Qt.LeftButton:
              self.setDragMode(QGraphicsView.NoDrag)
              self.leftMouseButtonReleased.emit(scenePos.x(), scenePos.y())
          elif event.button() == Qt.RightButton:
              if self.canZoom:
                  viewBBox = self.zoomStack[-1] if len(self.zoomStack) else self.sceneRect()
                  selectionBBox = self.scene.selectionArea().boundingRect().intersected(viewBBox)
                  self.scene.setSelectionArea(QPainterPath())  # Clear current selection area.
                  if selectionBBox.isValid() and (selectionBBox != viewBBox):
                      self.zoomStack.append(selectionBBox)
                      self.updateViewer()
              self.setDragMode(QGraphicsView.NoDrag)
              self.rightMouseButtonReleased.emit(scenePos.x(), scenePos.y())
  
      def mouseDoubleClickEvent(self, event):
          """ Show entire image.
          """
          scenePos = self.mapToScene(event.pos())
          if event.button() == Qt.LeftButton:
              self.leftMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
          elif event.button() == Qt.RightButton:
              if self.canZoom:
                  self.zoomStack = []  # Clear zoom stack.
                  self.updateViewer()
              self.rightMouseButtonDoubleClicked.emit(scenePos.x(), scenePos.y())
          QGraphicsView.mouseDoubleClickEvent(self, event)
  
      def closeEvent(self, event):
          self.app.quit()
  
      def nextImage(self):
          self.folderIndex = (self.folderIndex + 1) % len(self.folderListing)
          self.currentFile = self.folderListing[self.folderIndex]
          self.loadImage()
  
      def previousImage(self):
          self.folderIndex -= 1
          if self.folderIndex < 0:
              self.folderIndex += len(self.folderListing)
          self.currentFile = self.folderListing[self.folderIndex]
          self.loadImage()
  
      def keyPressEvent(self, event):
          if event.key() == Qt.Key_Right:
              self.nextImage()
          elif event.key() == Qt.Key_Left:
              self.previousImage()
          event.accept()
  
  
  if __name__ == '__main__':
      import sys
      try:
          from PyQt5.QtWidgets import QApplication
      except ImportError:
  
          try:
              from PyQt4.QtGui import QApplication
          except ImportError:
              raise ImportError("ViewGeoTIFF: Requires PyQt5 or PyQt4.")
      print('Using Qt ' + QT_VERSION_STR)
  
      def handleLeftClick(x, y):
          row = int(y)
          column = int(x)
          print("Clicked on image pixel (row="+str(row)+", column="+str(column)+")")
  
      # Create the application.
      app = QApplication(sys.argv)
  
      # Create image viewer and load an image file to display.
      viewer = ViewGeoTIFF(app)
  
      filename = None
      if len(sys.argv) > 1:
          filename = sys.argv[1]
  
      viewer.loadImageFromFile(filename)
      # Handle left mouse clicks with custom slot.
      viewer.leftMouseButtonPressed.connect(handleLeftClick)
  
      # Show viewer and run application.
      viewer.show()
      sys.exit(app.exec_())
  
apis: numpy.stack numpy.clip