QChart - Bar graph not updating according to Y-axis range

1k Views Asked by At

I am new to Python and PyQt5. I am trying to plot a bar graph dynamically using the values coming from virtual port (pyserial). I am able to read all the values, and update the values to the set using self.set0.replace(0, int(dia)). I see that the value goes beyond the Y-axis limit even though it is updated below the limit. Also it rarely comes below the limit and as a result the graph looks like it is not updating.

Note that I used two separate threads, one for video capture ( which is working perfect ) and once is for reading the value from port( let me know if the threading part is wrong as well)

import random
import sys
import serial
import cv2
import numpy as np

from PyQt5 import QtGui, QtWidgets, QtSerialPort
from PyQt5.QtChart import QValueAxis, QChartView, QBarCategoryAxis, QChart, QBarSeries, QBarSet
from PyQt5.QtCore import pyqtSignal, Qt, QThread, QTimer, pyqtSlot
from PyQt5.QtGui import QPixmap
from PyQt5.QtMultimedia import QCameraInfo
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout


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

    def __init__(self, i, path):
        super().__init__()
        self._run_flag = True
        self.saveImg = False
        self.save_path = path
        self.cam_id = i
        print(self.cam_id)

    def run(self):
        # capture from web cam
        self._run_flag = True
        count = 0
        self.cap = cv2.VideoCapture(self.cam_id - 2)
        while self._run_flag:
            ret, cv_img = self.cap.read()
            if ret:
                self.change_pixmap_signal.emit(cv_img)
            if self.saveImg:
                cv2.imwrite("/home/ign/Pictures/frame%d.jpg" % count, cv_img)
                cv2.imwrite(os.path.join(self.save_path,
                                         "%04d.jpg" % count), cv_img)
                count += 1
        # shut down capture system
        self.cap.release()

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

    def proceed(self):
        self.saveImg = True


class GraphThread(QThread):
    set_data = pyqtSignal(int, int, int)

    def __init__(self):
        super(GraphThread, self).__init__()
        self._run_flag = True
        self.s = serial.Serial('/dev/pts/2', 9600, timeout=None, parity=serial.PARITY_NONE,
                               stopbits=serial.STOPBITS_ONE,
                               bytesize=serial.EIGHTBITS)

    def run(self):
        self._run_flag = True

        while self._run_flag:
            cc = self.s.read(15)
            ccread = cc.decode("utf-8")
            print(ccread)
            diamond = ccread[1:4]
            hexa = ccread[6:9]
            trep = ccread[11:14]

            self.set_data.emit(int(diamond), int(hexa), int(trep))


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('Lattice Object Detection Demo Dashboard')
        self.barThread = GraphThread()
        self.initWindow()

    def initWindow(self):
        widget = QWidget()
        self.setCentralWidget(widget)

        # self.s = serial.Serial('/dev/pts/2', 9600, timeout=None, parity=serial.PARITY_NONE,
        #                        stopbits=serial.STOPBITS_ONE,
        #                        bytesize=serial.EIGHTBITS)

        # creating a tool bar
        toolbar = QToolBar("Camera Tool Bar")

        # adding tool bar to main window
        self.addToolBar(toolbar)

        mainLayout = QHBoxLayout()
        leftLayout = QVBoxLayout()
        mainLayout.addLayout(leftLayout)
        leftLayout.addStretch()

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

        # path to save
        self.save_path = ""

        # 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(50, 50)

        leftLayout.addWidget(self.image_label, Qt.AlignCenter)
        leftLayout.addWidget(self.ss_video)
        rightLayout = QVBoxLayout()
        rightLayout.addStretch()

        self.set0 = QBarSet('Count')

        self.set0.append([random.randint(0, 10) for i in range(3)])

        self.series = QBarSeries()
        self.series.append(self.set0)

        self.chart = QChart()
        self.chart.addSeries(self.series)
        self.chart.setTitle('Bar Chart Demo')
        self.chart.setAnimationOptions(QChart.SeriesAnimations)

        months = ('Diamond', 'Hexagon', 'Trapezium')

        axisX = QBarCategoryAxis()
        axisX.append(months)

        axisY = QValueAxis()
        axisY.setRange(0, 10000)
        # axisY.setLabelFormat("%d")

        self.chart.addAxis(axisX, Qt.AlignBottom)
        self.chart.addAxis(axisY, Qt.AlignLeft)

        self.chart.legend().setVisible(True)
        self.chart.legend().setAlignment(Qt.AlignBottom)

        self.chartView = QChartView(self.chart)
        rightLayout.addWidget(self.chartView, Qt.AlignCenter)

        mainLayout.addLayout(leftLayout)
        mainLayout.addLayout(rightLayout)

        # self.timer = QTimer()
        # self.timer.timeout.connect(self.drawGraph)
        # self.timer.start(1000)

        self.drawGraph()

        # similarly creating action for changing save folder
        change_folder_action = QAction("Change save location",
                                       self)

        # adding status tip
        change_folder_action.setStatusTip("Change folder where picture will be saved saved.")

        # adding tool tip to it
        change_folder_action.setToolTip("Change save location")

        # setting calling method to the change folder action
        # when triggered signal is emitted
        change_folder_action.triggered.connect(self.change_folder)

        # adding this to the tool bar
        toolbar.addAction(change_folder_action)

        # creating a combo box for selecting camera
        self.camera_selector = QComboBox()

        # adding status tip to it
        self.camera_selector.setStatusTip("Choose camera to take pictures")

        # adding tool tip to it
        self.camera_selector.setToolTip("Select Camera")
        self.camera_selector.setToolTipDuration(2500)

        # adding items to the combo box
        self.camera_selector.addItem("Select Camera")
        self.camera_selector.addItems([camera.description()
                                       for camera in self.available_cameras])

        # create the video capture thread
        self.i = self.camera_selector.currentIndex()
        self.thread = VideoThread(self.i, self.save_path)

        # adding action to the combo box
        # calling the select camera method
        self.camera_selector.currentIndexChanged.connect(self.select_camera)

        # adding this to tool bar
        toolbar.addWidget(self.camera_selector)

        # setting tool bar stylesheet
        toolbar.setStyleSheet("background : white;")

        # comport selection
        comport = QComboBox()
        comport.setStatusTip("Select Comport")
        for info in QtSerialPort.QSerialPortInfo.availablePorts():
            comport.addItem(info.portName())
        toolbar.addSeparator()
        toolbar.addWidget(comport)

        widget.setLayout(mainLayout)

    # 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('Hold capture')
        self.thread.saveImg = True
        self.thread.change_pixmap_signal.connect(self.update_image)
        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('Resume capture')
        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.thread.proceed)
        self.ss_video.clicked.connect(self.ClickStartVideo)

    # method to select camera
    def select_camera(self, i):
        self.i = self.camera_selector.currentIndex()
        self.thread = VideoThread(self.i, self.save_path)
        self.thread.change_pixmap_signal.connect(self.update_image)

        # start the thread
        self.thread.start()

    def closeEvent(self, event):
        self.thread._run_flag = False
        self.thread.stop()
        self.barThread._run_flag = False
        event.accept()

    # Actions

    def update_image(self, cv_img):
        """Updates the image_label with a new opencv image"""
        qt_img = self.convert_cv_qt(cv_img)
        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)

    # change folder method
    def change_folder(self):

        # open the dialog to select path
        path = QFileDialog.getExistingDirectory(self,
                                                "Picture Location", "")

        # if path is selected
        if path:
            # update the path
            self.save_path = path

            # update the sequence
            self.save_seq = 0

    # method for alerts

    @pyqtSlot()
    def drawGraph(self):
        # cc = self.s.read(15)
        # ccread = cc.decode("utf-8")
        # print(ccread)
        # diamond = ccread[1:4]
        # hexa = ccread[6:9]
        # trep = ccread[11:14]
        # self.set0.replace(0, int(diamond))
        # self.set0.replace(1, int(hexa))
        # self.set0.replace(2, int(trep))

        self.barThread.set_data.connect(self.onDataFromThread)
        print("thread starting")
        self.barThread.start()

    def onDataFromThread(self, dia, hexa, trep):
        print(dia, hexa, trep)
        self.set0.replace(0, dia)
        self.set0.replace(1, hexa)
        self.set0.replace(2, trep)
        self.chartView.update()


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

I am able to read the pyserial values as below. However, when I update the graph, it always go beyond the window and I see the values are not matching the graph either. Not sure what I am doing wrong here. I don't see a lot of documentation or tutorials on this either. Any help would be deeply appreciated.

Value from port

initial graph

This is the initial graph ( used random values between 0-999)

graph after first updation using value

Graph after first update using received value

1

There are 1 best solutions below

0
On

With the help from this Qt Adjusting axes does not adjust the chart itself answer, I was able to update the graph successfully. As per the link, The series isn't attached to any axis, it will by default scale to utilize the entire plot area of the chart.

You should attach the series to axis created as :

# self.chart.addAxis(axisX, Qt.AlignBottom)
# self.chart.addAxis(axisY, Qt.AlignLeft) // Instead of this, do the below.

self.chart.setAxisX(axisX)
self.chart.setAxisY(axisY)
self.series.attachAxis(axisX)
self.series.attachAxis(axisY)