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.
This is the initial graph ( used random values between 0-999)
Graph after first update using received value
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 :