raspberrypi / picamera2

New libcamera based python library
BSD 2-Clause "Simplified" License
868 stars 182 forks source link

Make q_picamera2 and q_gl_picamera2 dynamic size #62

Closed jlprojects closed 2 years ago

jlprojects commented 2 years ago

I think the camera preview in the q_picamera2 and q_gl_picamera2 should be made (optionally) to automatically fill the size of its container. This problem can be seen in examples/app_capture.py - resizing or maximising the window doesn't look very good. The preview in many application cases I'd expect will need to fit the maximum space available.

My project is a cine film scanner using the HQ camera. It's original control UI was written using picamera and tkinter which sort of worked, but very clunky. I decided to start again using picamera2 and PyQt5, so I'm fumbling to implement a new GUI. The q_picamera2 and q_gl_picamera2 widget looks ideal for showing a live view for focussing and aligning the camera, and indeed I'm starting by building on app_capture.py for a live preview and basic camera settings.

I altered q_picamera2.py to implement a dynamic resize which seems to work - patch below. It implements a resize event, decouples the camera image size from the label size, centres the camera image in the label. A layout control fills the label to the size of the parent widget. The overlay is handled and resized as necessary. It can be tested in examples/app_capture.py by replacing theq_glpicamera2.py import to q_picamera2 and the qpicamera2 declaration to qpicamera2 = QPicamera2(picam2,resizeable=True)

I'm sure it can be optimised. Similar functionality should exist on the GL preview widget but I got lost trying to understand the GL code. Just getting up to speed with basic PyQt at the moment.

--- q_picamera2.py.orig 2022-04-10 09:53:31.931882445 +0100
+++ q_picamera2.py  2022-04-10 11:10:19.290102418 +0100
@@ -1,17 +1,29 @@
 from PyQt5 import QtCore, QtGui, QtWidgets
 from PyQt5.QtCore import pyqtSlot, QSocketNotifier
-from PyQt5.QtWidgets import QWidget, QApplication, QLabel
+from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QVBoxLayout
 from PIL import Image
 from PIL.ImageQt import ImageQt
 import numpy as np

 class QPicamera2(QWidget):
-    def __init__(self, picam2, parent=None, width=640, height=480):
+    def __init__(self, picam2, parent=None, width=640, height=480, resizeable=False):
         super().__init__(parent=parent)
         self.picamera2 = picam2
+        self.image_ratio = height / width
+        self.img_size = QtCore.QSize(width, height)
         self.label = QLabel(self)
-        self.label.resize(width, height)
+        self.label.setAlignment(QtCore.Qt.AlignCenter)
+        self.label.setMinimumWidth(80)
+        self.label.setMinimumHeight(60)
+        self.resizeable = resizeable
+        if resizeable:
+            self.vbox = QVBoxLayout()
+            self.vbox.addWidget(self.label)
+            self.setLayout(self.vbox)
+        else:
+            self.label.resize(width, height)
+        self.overlay_orig = None
         self.overlay = None
         self.painter = QtGui.QPainter()
         self.camera_notifier = QSocketNotifier(self.picamera2.camera_manager.efd,
@@ -19,17 +31,31 @@
                                                self)
         self.camera_notifier.activated.connect(self.handle_requests)

+    def resizeEvent(self, event):
+        if not self.resizeable: return
+        # Keep aspect ratio
+        size = event.size()
+        width,height = size.width(),size.height()
+        new_width, new_height = width, int(width*self.image_ratio)
+        if new_height > height:
+            # Fit to height
+            new_width, new_height = int(height/self.image_ratio), height
+        self.img_size = QtCore.QSize(new_width,new_height)
+        if self.overlay is not None:
+            # Resize overlay
+            self.overlay = self.overlay_orig.scaled(self.img_size)
+        
     def set_overlay(self, overlay):
         if overlay is not None:
             # Better to resize the overlay here rather than in the rendering loop.
-            orig = overlay
             overlay = np.ascontiguousarray(overlay)
             shape = overlay.shape
-            size = self.label.size()
-            if orig is overlay and shape[1] == size.width() and shape[0] == size.height():
+            size = self.img_size
+            if self.overlay_orig is overlay and shape[1] == size.width() and shape[0] == size.height():
                 # We must be sure to copy the data even when no one else does!
                 overlay = overlay.copy()
             overlay = QtGui.QImage(overlay.data, shape[1], shape[0], QtGui.QImage.Format_RGBA8888)
+            self.overlay_orig = overlay.copy()
             if overlay.size() != self.label.size():
                 overlay = overlay.scaled(self.label.size())

@@ -43,7 +69,7 @@

         if self.picamera2.display_stream_name is not None:
             # This all seems horribly expensive. Pull request welcome if you know a better way!
-            size = self.label.size()
+            size = self.img_size
             img = request.make_image(self.picamera2.display_stream_name, size.width(), size.height())
             qim = ImageQt(img)
             self.painter.begin(qim)
davidplowman commented 2 years ago

Hi, thank you for sending this. Would you be able to turn this into a pull request, that way it gets some automated testing and it's convenient for folks to pull your branch if they want to try it. Thanks! (I'm away for a few days but will get back to it after that.)

jlprojects commented 2 years ago

Hi, I'll have a go at doing a pull request today.

jlprojects commented 2 years ago

While reimplementing my app and learning a little more about PyQt5, it occurred to me it that QPicamera2 might be better implemented as a QGraphicsView object. The preview image stored as a pixmap in the scene. Qt then handles all the view scaling automatically. Will experiment and report back...

jlprojects commented 2 years ago

Looks like this is fixed now. :)