Search code examples
pythonqtuser-interfacepyqt5

How to adjust exposure of the displayed image (openCV / PyQT)?


I use PyQT and OpenCV to get a thread from my webcam. I use a slider to adjust the max exposure. When I stop the thread, the last image stays displayed, but the slider doesn't adjust its exposure. I guess I should connect it to a function

self.slider_expo.valueChanger['int'].connect(self.expo_value_change)

but it already works well when the thread is started. How can I make something specific to handle the last image displayed when the thread is stopped? It's sometimes difficult to find the good value on a changing image; so I want to stop the thread to "pause" on a picture, adjust my value and then start the thread again.

import sys
from PyQt5.QtCore import QThread, pyqtSignal, Qt
from PyQt5.QtMultimedia import QCameraInfo
import cv2
from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, QStatusBar, QLabel
from PyQt5 import QtWidgets, QtGui
from PyQt5.QtGui import QPixmap
import numpy as np
from Process import Ui_MainWindow
import skimage.exposure

class VideoThread(QThread):
    change_pixmap_signal = pyqtSignal(np.ndarray)

    def __init__(self):
        super().__init__()
        self._run_flag = True

    def run(self):
        # capture from webcam
        self._run_flag = True
        self.cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
        while self._run_flag:
            ret, cv_img = self.cap.read()
            if ret:
                self.change_pixmap_signal.emit(cv_img)
        # shut down capture system
        self.cap.release()

    def stop(self):
        """Sets run flag to False and waits for thread to finish"""
        self._run_flag = False


class MyWindow(QMainWindow):
    def __init__(self):
        super(MyWindow, self).__init__()
        self.available_cameras = QCameraInfo.availableCameras()  # Getting available cameras

        cent = QDesktopWidget().availableGeometry().center()  # Finds the center of the screen
        self.setStyleSheet("background-color: white;")
        self.resize(1400, 800)
        self.frameGeometry().moveCenter(cent)
        self.setWindowTitle('S L A I T')
        self.initWindow()

########################################################################################################################
#                                                   Windows                                                            #
########################################################################################################################
    def initWindow(self):
        # create the video capture thread
        self.thread = VideoThread()

        # Button to start video
        self.ss_video = QtWidgets.QPushButton(self)
        self.ss_video.setText('Start video')
        self.ss_video.move(769, 100)
        self.ss_video.resize(300, 100)
        self.ss_video.clicked.connect(self.ClickStartVideo)

        #Slider to change max exposure
        self.slider_expo = QtWidgets.QSlider(self)
        self.slider_expo.setMinimum(52)
        self.slider_expo.setMaximum(255)
        self.slider_expo.setProperty("value",99)
        self.slider_expo.resize(10, 501)
        self.slider_expo.move(700, 0)

        # Status bar
        self.status = QStatusBar()
        self.status.setStyleSheet("background : lightblue;")  # Setting style sheet to the status bar
        self.setStatusBar(self.status)  # Adding status bar to the main window
        self.status.showMessage('Ready to start')

        self.image_label = QLabel(self)
        self.disply_width = 669
        self.display_height = 501
        self.image_label.resize(self.disply_width, self.display_height)
        self.image_label.setStyleSheet("background : black;")
        self.image_label.move(0, 0)

########################################################################################################################
#                                                   Buttons                                                            #
########################################################################################################################
    # Activates when Start/Stop video button is clicked to Start (ss_video
    def ClickStartVideo(self):
        # Change label color to light blue
        self.ss_video.clicked.disconnect(self.ClickStartVideo)
        self.status.showMessage('Video Running...')
        # Change button to stop
        self.ss_video.setText('Stop video')
        self.thread = VideoThread()
        self.thread.change_pixmap_signal.connect(self.update_image)

        # start the thread
        self.thread.start()
        self.ss_video.clicked.connect(self.thread.stop)  # Stop the video if button clicked
        self.ss_video.clicked.connect(self.ClickStopVideo)

    # Activates when Start/Stop video button is clicked to Stop (ss_video)
    def ClickStopVideo(self):
        self.thread.change_pixmap_signal.disconnect()
        self.ss_video.setText('Start video')
        self.status.showMessage('Ready to start')
        self.ss_video.clicked.disconnect(self.ClickStopVideo)
        self.ss_video.clicked.disconnect(self.thread.stop)
        self.ss_video.clicked.connect(self.ClickStartVideo)

########################################################################################################################
#                                                   Actions                                                            #
########################################################################################################################

    def update_image(self, cv_img):
        """Updates the image_label with a new opencv image"""
        min_expo = 50
        max_expo = self.slider_expo.value()
        img_expo = skimage.exposure.rescale_intensity(cv_img, in_range=(min_expo, max_expo),
                                                      out_range=(0, 255)).astype(np.uint8)
        qt_img = self.convert_cv_qt(img_expo)
        self.image_label.setPixmap(qt_img)

    def convert_cv_qt(self, cv_img):
        """Convert from an opencv image to QPixmap"""
        rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
        h, w, ch = rgb_image.shape
        bytes_per_line = ch * w
        convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888)
        p = convert_to_Qt_format.scaled(self.disply_width, self.display_height, Qt.KeepAspectRatio)
        #p = convert_to_Qt_format.scaled(801, 801, Qt.KeepAspectRatio)
        return QPixmap.fromImage(p)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = MyWindow()
    win.show()
    sys.exit(app.exec())

Solution

  • Your application updates image only when update_image is called. This method is only called when change_pixmap_signal signal is send by the VideoThread. To update image when the VideoThread is paused or stopped, you will have to first connect slider with a new slot as you guessed. Next, you will need to save the last image received in update_image as a class member variable.

    Also, make sure the slider value changed signal is not connected to a slot to update image when video is playing. Otherwise, you will have two different update methods running at the same time.

    import sys
    from PyQt5.QtCore import QThread, pyqtSignal, Qt
    from PyQt5.QtMultimedia import QCameraInfo
    import cv2
    from PyQt5.QtWidgets import QApplication, QMainWindow, QDesktopWidget, QStatusBar, QLabel
    from PyQt5 import QtWidgets, QtGui
    from PyQt5.QtGui import QPixmap
    import numpy as np
    import skimage.exposure
    
    
    class VideoThread(QThread):
        change_pixmap_signal = pyqtSignal(np.ndarray)
    
        def __init__(self):
            super().__init__()
    
        def run(self):
            # capture from webcam
            self._run_flag = True
            self.cap = cv2.VideoCapture(2)
            while self._run_flag:
                ret, cv_img = self.cap.read()
                if ret:
                    self.change_pixmap_signal.emit(cv_img)
            # shut down capture system
            self.cap.release()
    
        def stop(self):
            """Sets run flag to False and waits for thread to finish"""
            self._run_flag = False
    
    
    class MyWindow(QMainWindow):
        def __init__(self):
            super(MyWindow, self).__init__()
            self.available_cameras = QCameraInfo.availableCameras()  # Getting available cameras
    
            cent = QDesktopWidget().availableGeometry().center()  # Finds the center of the screen
            self.setStyleSheet("background-color: white;")
            self.resize(1400, 800)
            self.frameGeometry().moveCenter(cent)
            self.setWindowTitle('S L A I T')
            self.initWindow()
            self.cv_img = None
    
        ########################################################################################################################
        #                                                   Windows                                                            #
        ########################################################################################################################
        def initWindow(self):
            # create the video capture thread
            self.thread = VideoThread()
    
            # Button to start video
            self.ss_video = QtWidgets.QPushButton(self)
            self.ss_video.setText('Start video')
            self.ss_video.move(769, 100)
            self.ss_video.resize(300, 100)
            self.ss_video.clicked.connect(self.ClickStartVideo)
    
            # Slider to change max exposure
            self.slider_expo = QtWidgets.QSlider(self)
            self.max_expo = 99
            self.slider_expo.setMinimum(52)
            self.slider_expo.setMaximum(255)
            self.slider_expo.setProperty("value", 99)
            self.slider_expo.resize(10, 501)
            self.slider_expo.move(700, 0)
    
            # Status bar
            self.status = QStatusBar()
            self.status.setStyleSheet("background : lightblue;")  # Setting style sheet to the status bar
            self.setStatusBar(self.status)  # Adding status bar to the main window
            self.status.showMessage('Ready to start')
    
            self.image_label = QLabel(self)
            self.disply_width = 669
            self.display_height = 501
            self.image_label.resize(self.disply_width, self.display_height)
            self.image_label.setStyleSheet("background : black;")
            self.image_label.move(0, 0)
    
        ########################################################################################################################
        #                                                   Buttons                                                            #
        ########################################################################################################################
        # Activates when Start/Stop video button is clicked to Start (ss_video
        def ClickStartVideo(self):
            # Change label color to light blue
            self.ss_video.clicked.disconnect(self.ClickStartVideo)
            self.status.showMessage('Video Running...')
            # Change button to stop
            self.ss_video.setText('Stop video')
            self.thread = VideoThread()
            self.thread.change_pixmap_signal.connect(self.update_image)
    
            # start the thread
            self.thread.start()
            self.ss_video.clicked.connect(self.thread.stop)  # Stop the video if button clicked
            self.ss_video.clicked.connect(self.ClickStopVideo)
    
            # Disconnect updating image when slider is changed.
            if self.cv_img is not None:
                self.slider_expo.valueChanged.disconnect(self.update_image_when_video_paused)
    
        # Activates when Start/Stop video button is clicked to Stop (ss_video)
        def ClickStopVideo(self):
            self.thread.change_pixmap_signal.disconnect()
            self.ss_video.setText('Start video')
            self.status.showMessage('Ready to start')
            self.ss_video.clicked.disconnect(self.ClickStopVideo)
            self.ss_video.clicked.disconnect(self.thread.stop)
            self.ss_video.clicked.connect(self.ClickStartVideo)
    
            # Connect the slider signal to update image
            self.slider_expo.valueChanged.connect(self.update_image_when_video_paused)
    
        ########################################################################################################################
        #                                                   Actions                                                            #
        ########################################################################################################################
    
        def render_processed_image(self, cv_img, min_expo=50, max_expo=255):
            if min_expo != 50:
                print(min_expo)
            if max_expo != self.max_expo:
                print(f"max_expo changed to {max_expo}")
                self.max_expo = max_expo
            img_expo = skimage.exposure.rescale_intensity(cv_img, in_range=(min_expo, max_expo),
                                                          out_range=(0, 255)).astype(np.uint8)
            qt_img = self.convert_cv_qt(img_expo)
            self.image_label.setPixmap(qt_img)
    
        def update_image(self, cv_img):
            """Updates the image_label with a new opencv image"""
            self.cv_img = cv_img
            self.render_processed_image(self.cv_img, max_expo=self.slider_expo.value())
    
        def update_image_when_video_paused(self):
            self.render_processed_image(self.cv_img, max_expo=self.slider_expo.value())
    
        def convert_cv_qt(self, cv_img):
            """Convert from an opencv image to QPixmap"""
            rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
            h, w, ch = rgb_image.shape
            bytes_per_line = ch * w
            convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888)
            p = convert_to_Qt_format.scaled(self.disply_width, self.display_height, Qt.KeepAspectRatio)
            # p = convert_to_Qt_format.scaled(801, 801, Qt.KeepAspectRatio)
            return QPixmap.fromImage(p)
    
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        win = MyWindow()
        win.show()
        sys.exit(app.exec())