What is a good way to draw a waveform with pyqt6? - performance

Currently making an application which allows me to make a lightshow with some custom build LED-Controllers and for that i need to draw the waveform of the song on a widget.
Although I managed to do this it is still VERY slow (especially with .wav files longer than a few seconds). The thing is I don't know how to optimise this or if my approach is correct since i cant find anything on the web.
So my question is: what is the right way to go about this? How do audio editors display the waveform and are able to zoom in and out without lag?
So my current attempt at this is by using QGraphicsView and a QGraphicsScene, the latter one supposedly being made to represent a lot of custom graphics items.
The main function to look at here is drawWav() in class WavDisplay
Showcreator.py:
from PyQt6 import uic
from PyQt6.QtCore import (
QSize,
Qt
)
from PyQt6.QtGui import (
QAction,
QPen,
QPixmap,
QPainter,
QColor,
QImage
)
from PyQt6.QtWidgets import (
QMainWindow,
QWidget,
QStatusBar,
QFileDialog,
QGraphicsScene,
QGraphicsView,
QGridLayout
)
import sys
import wave
import pyaudio
import numpy as np
import threading
import soundfile as sf
import threading
class MainWindow(QMainWindow):
# audio chunk rate
CHUNK = 1024
def __init__(self):
super().__init__()
# set window title
self.setWindowTitle("LED Music Show")
# create file button
button_action = QAction("Open .wav file", self)
button_action.setStatusTip("Open a Wave file to the Editor.")
button_action.triggered.connect(self.openWav)
# set status bar
self.setStatusBar(QStatusBar(self))
# create menubar
menu = self.menuBar()
# add file button to status bar
file_menu = menu.addMenu("&File")
file_menu.addAction(button_action)
# create layout
layout = QGridLayout()
layout.setContentsMargins(0,0,0,0)
# create Wave display object
self.waveformspace = WavDisplay()
# add widget to layout
layout.addWidget(self.waveformspace, 0, 1)
self.centralWidget = QWidget()
self.centralWidget.setLayout(layout)
self.setCentralWidget(self.centralWidget)
def openWav(self):
# file selection window
self.filename, check = QFileDialog.getOpenFileName(self,"QFileDialog.getOpenFileName()", "","Wave files (*.wav)")
self.file = None
# try to open .wav with two methods
try:
try:
self.file = wave.open(self.filename, "rb")
except:
print("Failed to open with wave")
try:
self.file, samplerate = sf.read(self.filename, dtype='float32')
except:
print("Failed to open with soundfile")
# read file and convert it to array
self.signal = self.file.readframes(-1)
self.signal = np.fromstring(self.signal, dtype = np.int16)
# set file for drawing
self.waveformspace.setWavefile(self.signal)
self.waveformspace.drawWav()
# return file cursor to start
self.file.rewind()
# start thread for the player
# self.player = threading.Thread(target = self.playWav)
# try:
# self.player.daemon = True
# except:
# print("Failed to set player to Daemon")
# self.player.start()
except:
print("Err opening File")
def playWav(self):
lastFile = None
lastpos = None
p = pyaudio.PyAudio()
data = None
sampwidth = None
fps = None
chn = None
farmes = None
currentpos = 0
framespersec = None
while True:
if self.file != lastFile:
# get file info
sampwidth = self.file.getsampwidth()
fps = self.file.getframerate()
chn = self.file.getnchannels()
frames = self.file.getnframes()
lastFile = self.file
# open audio stream
stream = p.open(format = p.get_format_from_width(sampwidth), channels = chn, rate = fps, output = True)
# read first frame
data = self.file.readframes(self.CHUNK)
framespersec = sampwidth * chn * fps
print("file changed")
if self.pos != lastpos:
# read file for offset
self.file.readframes(int(self.pos * framespersec))
lastpos = self.pos
frames = self.file.getnframes()
print("pos changed")
while data and self.running:
# writing to the stream
stream.write(data)
data = self.file.readframes(self.CHUNK)
currentpos = currentpos + self.CHUNK
# cleanup stuff.
self.file.close()
stream.close()
p.terminate()
return
class WavDisplay(QGraphicsView):
file = None
maxAmplitude = 0
fileset = False
def __init__(self):
super().__init__()
def setWavefile(self, externFile):
self.file = externFile
self.fileset = True
# find the max deviation from 0 db to set draw borders
if max(self.file) > abs(min(self.file)):
self.maxAmplitude = max(self.file) * 2
else:
self.maxAmplitude = abs(min(self.file)) * 2
def drawWav(self):
# only draw when there is a set file
if self.fileset:
width = self.frameGeometry().width()
height = self.frameGeometry().height()
vStep = height / self.maxAmplitude
scene = QGraphicsScene(self)
# to draw on the middle of the widget
h = height / 2
# method 1 of drawing: looks at sections of the file and determines the max and min amplitude that would be visible on a single "column" of pixels and draws a vertical line between them
if width < len(self.file):
hStep = len(self.file) / width
drawArray = np.empty((width, 3))
for i in range(width - 1):
buffer = self.file[int(np.ceil(i * hStep)) : int(np.ceil((i + 1) * hStep))]
drawArray[i][0] = (min(buffer) * vStep) + h
drawArray[i][1] = (max(buffer) * vStep) + h
for i in range(width - 1):
self.line = scene.addLine(i, drawArray[i][0], i, drawArray[i][1])
# method 2 of drawing: this only happens when the amount of samples to draw is less than the windows width (e.g. when zoomed in and you can see the individual samples)
else:
hStep = width / len(self.file)
for i in range(len(self.file) - 1):
self.line = scene.addLine(i * hStep, int(self.file[i] * vStep + h), (i + 1) * hStep, int(self.file[i + 1] * vStep + h))
self.setScene(scene)
self.setContentsMargins(0,0,0,0)
self.show()
def resizeEvent(self, event) -> None:
# has to redraw the wave file if the window gets resized
self.drawWav()
# class not used yet
class effectList(QGraphicsView):
bpm = 130
trackBeats = 0
def __init__(self):
super().__init__()
def setBeatsAndBpm(self, trackLenght, Bpm):
self.bpm = Bpm
self.trackBeats = (trackLenght / 60) * self.bpm
main.py:
from PyQt6 import QtCore, QtGui, QtWidgets
from Showcreator import MainWindow
app = QtWidgets.QApplication([])
window = MainWindow()
window.show()
app.exec()
In essence: Where do i need to start to make this wave file view like one in for example Audacity? (Aka a fast rendering view which doesnt take ages)
Btw i have looked at seemingly duplicates of this question and as you can see in the code i have an algorythem that is only drawing as many lines as the window is wide and not all the 100000+ lines for each sample so the main problem i have should be the rendering method i guess.
Edit: I have all the data preloaded as im loading a wave file and convert it into a numpy array. And i need to display the file as a whole but be able to zoom in dynamically-

Related

Open a video with alpha and the webcam input in the same window, using pyQT

Open a video with alpha and the input from the webcam in the same window, using pyQT I have a video with alpha channel, it is in .flv or .mov format.
The code below can open this video and control its execution:
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QSizePolicy, QFileDialog, QLabel, QSlider, QStyle
from PyQt5.QtGui import QIcon, QPalette, QPixmap
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5.QtCore import Qt, QUrl
import sys
class Window(QWidget):
def __init__(self, windowsize):
super().__init__()
self.setWindowTitle("Rodolfo Nogueira Player")
# Posição e tamanho da janela
self.setGeometry(350, 100, 700, 500)
self.setWindowIcon(QIcon('player.png'))
self.windowsize = windowsize
p = self.palette()
p.setColor(QPalette.Window, Qt.black)
self.setPalette(p)
self.init_ui()
self.show()
def init_ui(self):
# Criação do objeto Media Player
self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)
#Criação do objeto videoObject
videowidget = QVideoWidget()
#Criação do botão de abrir
openBtn = QPushButton('Abrir Video')
openBtn.clicked.connect(self.open_file)
# Criação do botão de play
self.playBtn = QPushButton()
self.playBtn.setEnabled(False)
self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.playBtn.clicked.connect(self.play_video)
# Criação do slider
self.slider =QSlider(Qt.Horizontal)
self.slider.setRange(0, 0)
self.slider.sliderMoved.connect(self.set_position)
# Criando um rótulo - Receberá o erro, caso ocorra
self.label = QLabel()
# Definindo a política de tamanho
self.label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
# Criando um Layout hBox
hboxLayout = QHBoxLayout()
hboxLayout.setContentsMargins(0, 0, 0, 0)
# Configurando os elementos do bBox
hboxLayout.addWidget(openBtn)
hboxLayout.addWidget(self.playBtn)
hboxLayout.addWidget(self.slider)
# Criando um vBox layout
vboxLayout = QVBoxLayout()
vboxLayout.addWidget(videowidget)
vboxLayout.addLayout(hboxLayout)
vboxLayout.addWidget(self.label) # Receberá o erro, caso ocorra
self.setLayout(vboxLayout)
self.mediaPlayer.setVideoOutput(videowidget)
# Sinais do mediaPlayer
self.mediaPlayer.stateChanged.connect(self.media_state_change)
self.mediaPlayer.positionChanged.connect(self.position_changed)
self.mediaPlayer.durationChanged.connect(self.duration_changed)
def open_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Open Video")
filename = str(filename).replace('/', '\\')
if filename != '':
self.mediaPlayer.setMedia(QMediaContent(QUrl.fromLocalFile(filename)))
self.playBtn.setEnabled(True)
def play_video(self):
print('tentando o Play')
if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
print('tentando o Play')
self.mediaPlayer.pause()
else:
print('tentando o Pause')
self.mediaPlayer.play()
def media_state_change(self, state):
if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
else:
self.playBtn.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
def position_changed(self, position):
self.slider.setValue(position)
def duration_changed(self, duration):
self.slider.setRange(0, duration)
def set_position(self, position):
self.mediaPlayer.setPosition(position)
def handre_errors(self):
self.playBtn.setEnabled(False)
self.label.setText("Erro: " + self.mediaPlayer.errorString())
app = QApplication(sys.argv)
screensize = app.desktop().availableGeometry().size()
window = Window(screensize)
sys.exit(app.exec_())
Now I want to open the input of a webcam in the background, in the same window. Preferably when I play the video I want the cam input to open in the background.
I even got this code that uses pyQT to open the cam, but I can't get the two together in the same window:
class MainWindow(QMainWindow):
# constructor
def __init__(self):
super().__init__()
# setting geometry
self.setGeometry(100, 100,
800, 600)
# setting style sheet
self.setStyleSheet("background : lightgrey;")
# getting available cameras
self.available_cameras = QCameraInfo.availableCameras()
# if no camera found
if not self.available_cameras:
# exit the code
sys.exit()
# creating a status bar
self.status = QStatusBar()
# setting style sheet to the status bar
self.status.setStyleSheet("background : white;")
# adding status bar to the main window
self.setStatusBar(self.status)
# path to save
self.save_path = ""
# creating a QCameraViewfinder object
self.viewfinder = QCameraViewfinder()
# showing this viewfinder
self.viewfinder.show()
# making it central widget of main window
self.setCentralWidget(self.viewfinder)
# Set the default camera.
self.select_camera(0)
# creating a tool bar
toolbar = QToolBar("Camera Tool Bar")
# adding tool bar to main window
self.addToolBar(toolbar)
# creating a photo action to take photo
click_action = QAction("Click photo", self)
# adding status tip to the photo action
click_action.setStatusTip("This will capture picture")
# adding tool tip
click_action.setToolTip("Capture picture")
# adding action to it
# calling take_photo method
click_action.triggered.connect(self.click_photo)
# adding this to the tool bar
toolbar.addAction(click_action)
# 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
camera_selector = QComboBox()
# adding status tip to it
camera_selector.setStatusTip("Choose camera to take pictures")
# adding tool tip to it
camera_selector.setToolTip("Select Camera")
camera_selector.setToolTipDuration(2500)
# adding items to the combo box
camera_selector.addItems([camera.description()
for camera in self.available_cameras])
# adding action to the combo box
# calling the select camera method
camera_selector.currentIndexChanged.connect(self.select_camera)
# adding this to tool bar
toolbar.addWidget(camera_selector)
# setting tool bar stylesheet
toolbar.setStyleSheet("background : white;")
# setting window title
self.setWindowTitle("PyQt5 Cam")
# showing the main window
self.show()
# method to select camera
def select_camera(self, i):
# getting the selected camera
self.camera = QCamera(self.available_cameras[i])
# setting view finder to the camera
self.camera.setViewfinder(self.viewfinder)
# setting capture mode to the camera
self.camera.setCaptureMode(QCamera.CaptureStillImage)
# if any error occur show the alert
self.camera.error.connect(lambda: self.alert(self.camera.errorString()))
# start the camera
self.camera.start()
# creating a QCameraImageCapture object
self.capture = QCameraImageCapture(self.camera)
# showing alert if error occur
self.capture.error.connect(lambda error_msg, error,
msg: self.alert(msg))
# when image captured showing message
self.capture.imageCaptured.connect(lambda d,
i: self.status.showMessage("Image captured : "
+ str(self.save_seq)))
# getting current camera name
self.current_camera_name = self.available_cameras[i].description()
# initial save sequence
self.save_seq = 0
# method to take photo
def click_photo(self):
# time stamp
timestamp = time.strftime("%d-%b-%Y-%H_%M_%S")
# capture the image and save it on the save path
self.capture.capture(os.path.join(self.save_path,
"%s-%04d-%s.jpg" % (
self.current_camera_name,
self.save_seq,
timestamp
)))
# increment the sequence
self.save_seq += 1
# 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
def alert(self, msg):
# error message
error = QErrorMessage(self)
# setting text to the error message
error.showMessage(msg)
Can anyone give me a hint on how to put these two codes together?

Display text on another process' screen (overlay)

I have a question, its more an OS-based one.
I'm playing a video game and I want to be able to put a textual timer ontop of the game's screen as if it was a part of the game itself.
Now, I can write a program in any language that displays a TextBox with a timer on the screen, but if I run it, the game's process (lets call it game.exe) "loses" its focus and I get my TextBox focused and interactive by the OS.
Is there any option to display that text "ontop" of the game.exe that comes from an entire different process? as if there were "layers" to the screen. Also, this text shouldn't be intractable, clickable or make the game.exe process lose its focus.
Here's a very simple example I drew:
Thanks a lot!
Solved this using a window trick with python and tkinter with some windows api stuff.
The trick is to create a transparent non-clickable window and keep it always on top.
I've basically combined this answer with a bunch of simpler stuff like removing window's border and set to auto fullscreen.
from tkinter import *
import time
import win32gui
import win32api
from win32api import GetSystemMetrics
# WIDTH = 500
# HEIGHT = 500
WIDTH = GetSystemMetrics(0)
HEIGHT = GetSystemMetrics(1)
LINEWIDTH = 1
TRANSCOLOUR = 'gray'
title = 'Virtual whiteboard'
global old
old = ()
global HWND_t
HWND_t = 0
tk = Tk()
# tk.title(title)
tk.lift()
tk.wm_attributes("-topmost", True)
tk.wm_attributes("-transparentcolor", TRANSCOLOUR)
tk.attributes('-fullscreen', True)
state_left = win32api.GetKeyState(0x01) # Left button down = 0 or 1. Button up = -127 or -128
canvas = Canvas(tk, width=WIDTH, height=HEIGHT, highlightthickness=0)
canvas.pack()
canvas.config(cursor='tcross')
canvas.create_rectangle(0, 0, WIDTH, HEIGHT, fill=TRANSCOLOUR, outline=TRANSCOLOUR)
canvas.create_text(WIDTH/2,HEIGHT/2,fill="white",font="Arial 20", text="TEXT GOES HERE")
def putOnTop(event):
event.widget.unbind('<Visibility>')
event.widget.update()
event.widget.lift()
event.widget.bind('<Visibility>', putOnTop)
def drawline(data):
global old
if old !=():
canvas.create_line(old[0], old[1], data[0], data[1], width=LINEWIDTH)
old = (data[0], data[1])
def enumHandler(hwnd, lParam):
global HWND_t
if win32gui.IsWindowVisible(hwnd):
if title in win32gui.GetWindowText(hwnd):
HWND_t = hwnd
win32gui.EnumWindows(enumHandler, None)
tk.bind('<Visibility>', putOnTop)
tk.focus()
running = 1
while running == 1:
try:
tk.update()
time.sleep(0.01)
if HWND_t != 0:
windowborder = win32gui.GetWindowRect(HWND_t)
cur_pos = win32api.GetCursorPos()
state_left_new = win32api.GetKeyState(0x01)
if state_left_new != state_left:
if windowborder[0] < cur_pos[0] and windowborder[2] > cur_pos[0] and windowborder[1] < cur_pos[1] and windowborder[3] > cur_pos[1]:
drawline((cur_pos[0] - windowborder[0] - 5, cur_pos[1] - windowborder[1] - 30))
else:
old = ()
except Exception as e:
running = 0
print("error %r" % (e))

GUI plot refreshing incorrectly on wxpython panel

Ok, so I've been tasked with creating a VERY simple GUI at work (I'm an intern). The task is to eventually connect to a machine and process real data, but right now I'm working on randomly generated sine data with noise. I've chose to work in Python 3.0, and use wxpython to create my GUI components.
As I want everything to appear on the same window, I'm using panels (hence wx.lib.plot.PlotCanvas rather than something like matplotlib.pyplot)
The problem that I have is that over time, the plot seems to 'expand' off of the panel. This is temporarily solved when I manually resize the window, but resumes again immediately after (you need to run the code to see what I mean).
Expansion over time in panel
Another problem (that has bugged me since I have started writing the code) is that sometimes when I resize the window (manually) or minimize it and then maximize it again, the timer randomly starts and stops.
I have tried all sorts of things (changing padding in sizers, extra arguments, changing time between refreshes GetBestSize()) but I believe that I simply don't understand wxpython well enough to identify where the problem is
I would really appreciate any help you can shed on either of these problems (I don't know, they might even be linked to each other).
FYI: I am not an experienced coder, and my code is not finished (I have more functions to code, but I feel like I should resolve this first). I have constructed this code by looking at different techniques from various tutorials and websites like stackoverflow, so I know it's not formatted well and could definitely be made more efficient. Also, I have removed some parts just to be safe about confidentiality - nothing important, just strings in messages.
PS: If you do have an easier way to do the whole plot/update thing that doesn't have this problem (preferably still in wx) I would be thrilled to hear that as well
And here's my code:
EDIT: Solved the expanding problem by using self.p2.SetSize((W+0,L+0)) instead of (self.p2.GetBestSize())
EDIT: Made transitions much smoother by just regenerating data and redrawing it on existing canvas in the evt_timer function (instead of recreating the whole canvas, which gave a blink-y appearance if you know what I mean)
import wx
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import leastsq
import wx.lib.plot as plot
import time
import os
wildcard = "Text File (*.txt)|*.txt|"\
"Picture (*.png)|*.png|"\
"All files (*.*)|*.*"#This wildcard shows the options for file endings in the "SAVE" tab - see OnSave(self,event)
wildcard2 = "Picture (*.png)|*.png|"\
"Text File (*.txt)|*.txt|"\
"All files (*.*)|*.*"
class PlotCanvas(plot.PlotCanvas):
def __init__(self,parent,id,size,accepted):
"""
This randomly generates sine data (with noise) and plots it to a panel.
Incorporated as a separate class instead of instatiating it as a plot.PlotCanvas object
to overcome an issue of the size of the plot in the panel.
"""
plot.PlotCanvas.__init__(self,parent,id,style=wx.BORDER_SUNKEN,size = size)
N = 100 # number of data points
self.t = np.linspace(0, 4*np.pi, N)
f = 1.15247 # Optional!! Advised not to use
self.data = 3.0*np.sin(f*self.t+0.001) + 0.5 + np.random.randn(N) # create artificial data with noise
guess_mean = np.mean(self.data)
guess_phase = 0
guess_freq = 1
guess_amp = 1
optimize_func = lambda x: x[0]*np.sin(x[1]*self.t+x[2]) + x[3] - self.data
est_amp, est_freq, est_phase, est_mean = leastsq(optimize_func, [guess_amp, guess_freq, guess_phase, guess_mean])[0]
fine_t = np.arange(0,max(self.t),0.1)
data_fit=est_amp*np.sin(est_freq*fine_t+est_phase)+est_mean
multiplier = 1
dataset1 = [(x,[d for d in self.data][[td for td in self.t].index(x)])for x in [td for td in self.t]]
fitdata1 = [(x,[df for df in data_fit][[tf for tf in fine_t].index(x)]) for x in [tf for tf in fine_t]]
dataset =[(x,y*multiplier) for (x,y) in dataset1]
fitdata = [(x,y*multiplier) for (x,y) in fitdata1]
self.data = dataset
self.data2 = fitdata
line = plot.PolyLine(self.data,legend = 'random',colour = 'light blue', width =2)
line2 = plot.PolyLine(self.data2,legend = 'sineline',colour ='black',width =2)
a = []
if "D" in accepted:
a.append(line)
if "S" in accepted:
a.append(line2)
if "G" in accepted:
pass
if "L" in accepted:
pass
gc = plot.PlotGraphics(a,'Line Graph','X','Y')
xmin = self.t[0]-0.01*(self.t[-1]-self.t[0])
xmax = self.t[-1]+0.01*(self.t[-1]-self.t[0])
self.Draw(gc,xAxis=(xmin,xmax),yAxis=(min([x[1] for x in dataset])-0.01*(max([x[1] for x in dataset])-min([x[1] for x in dataset])),
max([x[1] for x in dataset])+0.01*(max([x[1] for x in dataset])-min([x[1] for x in dataset]))))
#self.showLegend = True
#self.enableZoom = True
def Dialog(self, parent, message, c):# Will be used to notify the user of errors/processes
if c == "W":
caption = "Warning!"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
elif c == "I":
caption = "Information"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()#Destroys dialog on close
class Frame(wx.Frame):
"""
This is the main class. In it, we declare the separate panels, canvas, menubar, buttons and sizers.
"""
def __init__(self,parent,id,title):
wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition)
self.CurrentDirectory = os.getcwd()
self.timer=wx.Timer(self)#Instantiating the timer
self.count=0
self.Bind(wx.EVT_TIMER,self.evt_timer)#Binding it to itself so that it is always triggered
self.Bind(wx.EVT_PAINT,self.paint)
menubar = wx.MenuBar()
fileMenu = wx.Menu() #Creating the Menubar at the top
#Creating 3 menus: fileMenu,fit,and help
save = wx.Menu()
z = wx.MenuItem(save,wx.ID_ANY,'Save Raw Data\tCtrl+D')
self.Bind(wx.EVT_MENU,self.OnSave,z)
save.Append(z)
z= wx.MenuItem(save,wx.ID_ANY,'Save Image\tCtrl+I')
self.Bind(wx.EVT_MENU,self.OnSaveImage,z)
save.Append(z)
fileMenu.AppendSubMenu(save,'&Save')
fileMenu.AppendSeparator()
z = wx.MenuItem(fileMenu, wx.ID_EXIT, '&Quit\tCtrl+W')
self.Bind(wx.EVT_MENU, self.OnQuit, z)
fileMenu.Append(z)
fit = wx.Menu()#Making a check menu
self.gaussian = fit.Append(wx.ID_ANY,'Gaussian',kind = wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleGaussian,self.gaussian)
fit.Check(self.gaussian.GetId(),False)
self.sine = fit.Append(wx.ID_ANY,'Sine',kind = wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleSine,self.sine)
fit.Check(self.sine.GetId(),False)
self.linear = fit.Append(wx.ID_ANY,'Linear',kind=wx.ITEM_CHECK)
#self.Bind(wx.EVT_MENU,self.ToggleLinear,self.linear)
fit.Check(self.linear.GetId(),False)
help = wx.Menu()
z = wx.MenuItem(help,wx.ID_ANY,'&About\tCtrl+H')
self.Bind(wx.EVT_MENU,self.OnHelp,z)
help.Append(z)
menubar.Append(fileMenu, '&File')
menubar.Append(fit, '&Fit')
menubar.Append(help, '&Help')#adding menus to menubar
self.SetMenuBar(menubar)#formatting the frame with menubar
self.sp = wx.SplitterWindow(self)#Splitting the window into 2 panels
self.p1 = wx.Panel(self.sp,style = wx.SUNKEN_BORDER)#For buttons and user events
self.p2 = wx.Panel(self.sp,style = wx.SUNKEN_BORDER)#For display of the plot
self.sp.SplitVertically(self.p1,self.p2,300)
sizer = wx.GridBagSizer(3, 3)#Versatile sizer for layout of first panel self.p1
bitmappath = self.CurrentDirectory + "\\BITMAPS"
bmp = wx.Bitmap(bitmappath+"\\SAVE.BMP",wx.BITMAP_TYPE_BMP)
self.saveBtn = wx.BitmapButton(self.p1,wx.ID_ANY,bitmap = bmp,size =(bmp.GetWidth()+10,bmp.GetHeight()+10))
self.Bind(wx.EVT_BUTTON,self.OnSave,self.saveBtn)
sizer.Add(self.saveBtn, (0, 0), wx.DefaultSpan, wx.ALL,5)
bmp = wx.Bitmap(bitmappath +"\\START.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn = wx.BitmapButton(self.p1,-1,bitmap = bmp,size =(bmp.GetWidth()+10,bmp.GetHeight()+10))# A button that starts and stops the plotting
self.startBtn.startval = "START"
self.Bind(wx.EVT_BUTTON,self.paint,self.startBtn)
sizer.Add(self.startBtn, (0, 1), wx.DefaultSpan,wx.ALL,5)
sizer1 = wx.BoxSizer(wx.VERTICAL)
W,L = self.p2.GetSize()
self.p2.canvas = PlotCanvas(self.p2,wx.ID_ANY,(W,L),["D"])
sizer1.Add(self.p2.canvas,1,wx.ALL,0,0)
self.p2.SetSizerAndFit(sizer1)
self.p1.SetSizerAndFit(sizer)
self.p2.SetSizerAndFit(sizer1)
self.p2.SetSize(W,L)
self.Maximize(True)
self.Centre()
self.Show()
############### event methods ###########
def paint(self,event):
"""
Updates the canvas based on the value of the startbtn(not the image). Bound to self.timer.
"""
bitmappath = self.CurrentDirectory + "\\BITMAPS"
if self.startBtn.startval == "START":
self.timer.Start(1)# increase the value for more time
bmp = wx.Bitmap(bitmappath + "\\STOP.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn.SetBitmap(bmp)
self.startBtn.startval = "STOP"
elif self.startBtn.startval == "STOP":
self.timer.Stop()
bmp = wx.Bitmap(bitmappath+ "\\START.BMP",wx.BITMAP_TYPE_BMP)
self.startBtn.SetBitmap(bmp)
self.startBtn.startval = "START"
def evt_timer(self,event):
self.count +=1
if self.count== 10:# By increasing count (or the number in self.timer.Start()) you can increase the interval between updates
#self.p2.canvas.Clear()
sizer1 = wx.BoxSizer(wx.VERTICAL)
W,L = self.p2.GetSize()
a = ["D"]
if self.sine.IsChecked():
a.append("S")
elif self.linear.IsChecked():
a.append("L")
elif self.gaussian.IsChecked():
a.append("G")
self.p2.canvas = PlotCanvas(self.p2,wx.ID_ANY,(W,L),a)
sizer1.Add(self.p2.canvas,1,wx.ALL,0,0)
self.p2.SetSizerAndFit(sizer1)
self.p2.SetSize(self.p2.GetBestSize())
self.count=0 # reset the count
def Dialog(self, parent, message, c):# Will be used to notify the user of errors/processes
if c == "W":
caption = "Warning!"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_WARNING)
elif c == "I":
caption = "Information"
dlg = wx.MessageDialog(parent, message, caption, wx.OK | wx.ICON_INFORMATION)
dlg.ShowModal()
dlg.Destroy()#Destroys dialog on close
def OnSave(self,event):#Triggered by menubar and button
try:
rawdata = self.p2.canvas.data
raw_X =[x[0] for x in rawdata]
raw_Y =[x[1] for x in rawdata]
dlg = wx.FileDialog(#Code for this from http://www.blog.pythonlibrary.org
self, message="Save file as ...",
defaultDir=self.CurrentDirectory,
defaultFile=str(time.ctime()), wildcard=wildcard, style=wx.FD_SAVE
)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
dlg.Destroy()
f = open(path+".txt","w+")
for i in range(len(raw_X)):
f.write(str(raw_X[i])+"\t"+str(raw_Y[i])+"\n")
f.close()
self.Dialog(None,"File successfully saved","I")
except UnboundLocalError:#Catch error when user closes save window without selecting any directory or filename
pass
def OnSaveImage(self,event):
try:
rawdata = self.p2.canvas.data
raw_X = [x[0] for x in rawdata]
raw_Y = [x[1] for x in rawdata]
dlg = wx.FileDialog(
self, message="Save file as ...",
defaultDir=self.CurrentDirectory,
defaultFile=str(time.ctime()), wildcard=wildcard2, style=wx.FD_SAVE
)
if dlg.ShowModal() == wx.ID_OK:
path = dlg.GetPath()
dlg.Destroy()
fig1 = plt.figure()
plt.plot(raw_X,raw_Y)
plt.title("Raw Data")
fig1.savefig(path+".png")
self.Dialog(None,"File successfully saved","I")
except UnboundLocalError:
pass
def OnMultiply(self,e):
try:
factor = self.x.GetValue()
factor = float(factor)
self.IntegrationTime = factor
except ValueError as e:
self.Dialog(None,str(e),"W")
def OnQuit(self, e):
self.Close()
def OnHelp(self,e):
self.Dialog(None,"N/A","I")
def ToggleSine(self,e):
pass
def ToggleLinear(self,e):
self.Dialog(None,"Not added yet","W")
def ToggleGaussian(self,e):
self.Dialog(None,"Not added yet","W")
if __name__ =="__main__":
app=wx.App()
Frame(None,-1,"N/A")
app.MainLoop()

pyqt4 pixel information on rotated image

I have adapted an image viewer (see code below) to allow me to get pixel information from a loaded image. You load an image using the 'Load image' button, then you can zoom in and out using the scroll wheel, and pan using mouse left click and drag. When you press the button 'Enter pixel info mode', the dragging is disabled (you can still zoom) and clicking on the image will give the pixel coordinate (integer pixel indices) and grayscale value of the pixel.
The problem is that if you rotate the image, by pressing the 'Rotate image' button, using the pixel info button no longer gives the correct pixel info. I imagine that the mapToScene method is not the right thing to use on a rotated image but can find no other way to do it. I have tried various things, such as using toImage() on the rotated pixmap and then replacing the original image with this, but nothing seems to work. What would be the best way to resolve this?
The code:
from PyQt4 import QtCore, QtGui
class PhotoViewer(QtGui.QGraphicsView):
photoClicked = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, parent):
super(PhotoViewer, self).__init__(parent)
self._zoom = 0
self._empty = True
self._scene = QtGui.QGraphicsScene(self)
self._photo = QtGui.QGraphicsPixmapItem()
self._scene.addItem(self._photo)
self.setScene(self._scene)
self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30)))
self.setFrameShape(QtGui.QFrame.NoFrame)
def fitInView(self):
rect = QtCore.QRectF(self._photo.pixmap().rect())
if not rect.isNull():
unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
self.scale(1 / unity.width(), 1 / unity.height())
viewrect = self.viewport().rect()
scenerect = self.transform().mapRect(rect)
factor = min(viewrect.width() / scenerect.width(),
viewrect.height() / scenerect.height())
self.scale(factor, factor)
self.centerOn(rect.center())
self._zoom = 0
def hasPhoto(self):
return not self._empty
def toggleDragMode(self):
if self.dragMode() == QtGui.QGraphicsView.ScrollHandDrag:
self.setDragMode(QtGui.QGraphicsView.NoDrag)
elif self.hasPhoto():
self.setDragMode(QtGui.QGraphicsView.ScrollHandDrag)
def setPhoto(self, pixmap=None):
self._zoom = 0
if pixmap and not pixmap.isNull():
self._empty = False
self.setDragMode(QtGui.QGraphicsView.ScrollHandDrag)
self._photo.setPixmap(pixmap)
self.fitInView()
else:
self._empty = True
self.setDragMode(QtGui.QGraphicsView.NoDrag)
self._photo.setPixmap(QtGui.QPixmap())
def wheelEvent(self, event):
if not self._photo.pixmap().isNull():
if event.delta() > 0:
factor = 1.25
self._zoom += 1
else:
factor = 0.8
self._zoom -= 1
if self._zoom > 0:
self.scale(factor, factor)
elif self._zoom == 0:
self.fitInView()
else:
self._zoom = 0
def mousePressEvent(self, event):
if (self.hasPhoto() and
self.dragMode() == QtGui.QGraphicsView.NoDrag and
self._photo.isUnderMouse()):
self.photoClicked.emit(QtCore.QPoint(event.pos()))
super(PhotoViewer, self).mousePressEvent(event)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.viewer = PhotoViewer(self)
# 'Load image' button
self.btnLoad = QtGui.QToolButton(self)
self.btnLoad.setText('Load image')
self.btnLoad.clicked.connect(self.loadImage)
# Button to change from drag/pan to getting pixel info
self.btnPixInfo = QtGui.QToolButton(self)
self.btnPixInfo.setText('Enter pixel info mode')
self.btnPixInfo.clicked.connect(self.pixInfo)
self.editPixInfo = QtGui.QLineEdit(self)
self.editPixInfo.setReadOnly(True)
# Button to rotate image by 10 degrees
self.btnRotate = QtGui.QToolButton(self)
self.btnRotate.setText('Rotate image')
self.btnRotate.clicked.connect(self.rotateImage)
self.viewer.photoClicked.connect(self.photoClicked)
# Arrange layout
VBlayout = QtGui.QVBoxLayout(self)
VBlayout.addWidget(self.viewer)
HBlayout = QtGui.QHBoxLayout()
HBlayout.setAlignment(QtCore.Qt.AlignLeft)
HBlayout.addWidget(self.btnLoad)
HBlayout.addWidget(self.btnRotate)
HBlayout.addWidget(self.btnPixInfo)
HBlayout.addWidget(self.editPixInfo)
VBlayout.addLayout(HBlayout)
def loadImage(self):
self.viewer.setPhoto(QtGui.QPixmap('pic.jpg'))
def pixInfo(self):
self.viewer.toggleDragMode()
def rotateImage(self):
self.viewer._photo.setRotation(10)
def photoClicked(self, pos):
pos = self.viewer.mapToScene(pos)
# p.s. I realise the following lines are probably a very convoluted way of getting
# a grayscale value from RGB, but I couldn't make it work any other way I tried
rot_image = self.viewer._photo.pixmap().toImage().pixel(pos.x(), pos.y())
colour = QtGui.QColor.fromRgb(rot_image)
gsval = QtGui.qGray(colour.red(), colour.green(), colour.blue())
self.editPixInfo.setText('X:%d, Y:%d Grayscale: %d' % (pos.x(), pos.y(), gsval))
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 600)
window.show()
sys.exit(app.exec_())
You need to map the scene coordinates to item coordinates:
pos = self.viewer._photo.mapFromScene(self.viewer.mapToScene(pos))

Python GUI (tkinter.ttk) application slow

I've got (working) application done with ttk. It uses self-created module for showing a comport-related controls and a canvas which draws a few graphs on it. When I crate an instance of my object, it starts a thread in which processes serial input and appends this to a list (one list per graph). When I have 3-6 graphs, the application gets noticeably slow. It also has got a few bugs, but I will adress them when I'm done with the general concept.
Things that may help you help me:
comport is an instance of a self-written object that derives from
LabelFrame and Serial.Serial
coordinates for graphs are stored in a dictionary of lists:
self.graphs = {} self.graphs['name1']=[] number of coordinates stored
is up to the width of canvas, so about 1000-2000 per graph. Have six
graphs - please multiply by 6
With every new coordinate arriving I pop(0) from the list and
append() the new coordinate
I forgot, I also store timing of each new set of coordinates arriving
in a separate list
I use a preiodic call function to process the lists: self.after(100,
func=self.periodicCall)Thus every 100ms I delete(ALL) from the canvas
and I draw every graph with theset of lines. So if I have 1000 coords
in 6 graps, I draw 6000 small lines
Plus some service info of course such as a few rulers
So I guess the idea is clear. I want to figure out what would be the better approach. I'm just a started in python and in programming as well, so I'm asking for your excuse for the code that I'm going to post and for the pain in your eyes it's gonna cause. I don't have any programming style and I want to fix it. At least a bit. So any other comments on anything you'll se in the code are welcome.
#-------------------------------------------------------------------------------
# Name: dataVisualizer
# Purpose:
#
# Author: dccharacter
#
# Created: 23.03.2012
# Copyright: (c) dccharacter 2012
# Licence: <your licence>
#-------------------------------------------------------------------------------
#!/usr/bin/env python
from tkinter import *
from tkinter.ttk import *
from robowidgets.serialPortGui import *
import threading
import re
import atexit
import random
from datetime import datetime
import time
class dataVisualizer(LabelFrame):
def __init__(self, master, comport , cnf={}, **kw):
self.master = master
self.comport = comport
LabelFrame.__init__(self, *cnf, **kw)
self.messageVar = StringVar()
Label(self, text="Message format regexp:").pack()
self.messagePattern = Entry(self, width = 20, text = 234234, textvariable = self.messageVar);
self.messageVar.set(r'(-*\d+),(-*\d+),(-*\d+),(-*\d+),(-*\d+),(-*\d+)')
self.messagePattern.pack()
Button(self, text = "Pause", command = self.pause).pack()
self.pauseFlag = TRUE
self.canvWidth, self.canvHeight = 1000, 700
self.density = 1 ##width of pixel - the bigger, the wider graph
self.numOfDots = self.canvWidth//self.density
self.graphs = {}
self.graphs['name1']=[]
self.graphs['name2']=[]
self.graphs['name3']=[]
self.graphs['name4']=[]
self.graphs['name5']=[]
self.graphs['name6']=[]
self.timings = []
self.zeroTiming = datetime.now()
self.colors = ['red', 'blue', 'green', 'orange', 'violet', 'black', 'cyan']
self.canv = Canvas(self, width = self.canvWidth, height = self.canvHeight)
self.canv.pack()
self.thread = threading.Thread(target = self.workerThread)
self.thread.start()
self.serialData = []
self.periodicCall()
def pause(self):
self.pauseFlag = ~self.pauseFlag
def redraw(self):
self.canv.delete(ALL)
colorIndex = 0
for graphName in self.graphs:
runningAverage = sum(self.graphs[graphName][-10:])//10
text = str(runningAverage)
self.canv.create_text(self.canvWidth-60, 20*(colorIndex+1), text = text,
fill = self.colors[colorIndex], anchor = W)
prev_xxx, prev_yyy = 0, 0
for yyy in self.graphs[graphName]:
self.canv.create_line(prev_xxx, prev_yyy, prev_xxx+self.density, self.canvHeight//2 - yyy,
width = 1.4, fill = self.colors[colorIndex])
prev_xxx, prev_yyy = prev_xxx+self.density, self.canvHeight//2 - yyy
colorIndex = colorIndex + 1
self.drawMesh()
def drawMesh(self):
self.canv.create_rectangle(3, 3, self.canvWidth,
self.canvHeight, outline = 'black', width = 2)
self.canv.create_line(0, self.canvHeight/2, self.canvWidth,
self.canvHeight/2, fill="black", width = 1)
mouseX = self.canv.winfo_pointerx() - self.canv.winfo_rootx()
mouseY = self.canv.winfo_pointery() - self.canv.winfo_rooty()
if mouseY < 60: aaa = -1
else: aaa = 1
if mouseX > self.canvWidth - 200 : bbb = -12
else: bbb = 1
try:
self.canv.create_rectangle(mouseX + 10*bbb - 5, mouseY - 20*aaa +10,
mouseX + 10*bbb + 115, mouseY - 20*aaa - 30, outline = "black",
fill = "red")
self.canv.create_text(mouseX + 10*bbb, mouseY - 40*aaa,
text = "t="+str(self.timings[mouseX//self.density]),
anchor = W)
self.canv.create_text(mouseX + 10*bbb, mouseY - 20*aaa,
text = "value="+str(self.canvHeight//2 - mouseY),
anchor = W)
except IndexError:
pass
self.canv.create_line(mouseX, 0, mouseX,
self.canvHeight, fill="blue", dash = [4, 1, 2, 1], width = 1)
self.canv.create_line(0, mouseY, self.canvWidth,
mouseY, fill="blue", dash = [4, 1, 2, 1], width = 1)
def periodicCall(self):
self.redraw()
self.after(100, func=self.periodicCall)
def workerThread(self):
while (1):
try:
if self.comport.isOpen() and (self.pauseFlag == TRUE):
comLine = self.comport.readline()
if len(self.timings) == self.numOfDots:
self.timings.pop(0)
td = datetime.now() - self.zeroTiming
## b'271;-3:-50\r\n'
parsedLine = re.search(self.messagePattern.get(), str(comLine))
index = 1
if parsedLine:
self.timings.append(td)
for graphName in self.graphs:
if len(self.graphs[graphName]) == self.numOfDots:
self.graphs[graphName].pop(0)
try:
self.graphs[graphName].append(int(parsedLine.group(index)))
except IndexError:
self.graphs[graphName].append(0)
index = index + 1
else:
self.comport.flush();
time.sleep(1)
except TclError:
self.thread._stop()
def main():
root = Tk()
mainWindow = Frame(root)
mainWindow.pack()
port = comPortWidget(mainWindow)
port.pack()
dv = dataVisualizer(mainWindow, port)
dv.pack()
root.mainloop()
if __name__ == '__main__':
main()
And the serial part - may lag as well (used to lag when I used to reenumerate ports evey second or so...)
#-------------------------------------------------------------------------------
# Name: robowidgets
# Purpose:
#
# Author: dccharacter
#
# Created: 10.03.2012
# Copyright: (c) dccharacter 2012
# Licence: <your licence>
#-------------------------------------------------------------------------------
#!/usr/bin/env python
import serial
from serial.tools.list_ports_windows import comports
from tkinter import *
from tkinter.ttk import *
class comPortWidget(LabelFrame, serial.Serial):
commonComPortSpeeds = ["1200", "2400", "4800", "9600", "14400", "19200", "38400", "57600", "115200"]
def __init__(self, master=None, cnf={}, **kw):
"""Construct a comPortWidget widget with the parent MASTER.
STANDARD OPTIONS
borderwidth, cursor, font, foreground,
highlightbackground, highlightcolor,
highlightthickness, padx, pady, relief,
takefocus, text, background, class, colormap, container,
height, labelanchor, labelwidget,
visual, width
WIDGET-SPECIFIC OPTIONS
"""
self.master = master
LabelFrame.__init__(self, master, text="Serial settings", *cnf, **kw)
serial.Serial.__init__(self)
self.parent = master
self.draw()
def draw(self):
self.strVarComPort = StringVar()
self.comboComport = Combobox(self,
textvariable=self.strVarComPort)
self.comboComport.grid(row=0, column=1)
self.labelComportName = Label(self, text="Com port:")
self.labelComportName.grid(row=0, column=0)
self.strVarComSpeed = StringVar()
self.comboComSpeed = Combobox(self,
textvariable=self.strVarComSpeed, values=self.commonComPortSpeeds)
self.comboComSpeed.current(len(self.commonComPortSpeeds)-1)
self.comboComSpeed.grid(row=1, column=1)
self.labelComSpeed = Label(self, text="Com speed:")
self.labelComSpeed.grid(row=1, column=0)
self.buttonComOpen = Button(self, text="Open port", command=self.openPort)
self.buttonComOpen.grid(row=0, column=2)
self.buttonComClose = Button(self, text="Close port", command=self.closePort)
self.buttonComClose.grid(row=1, column=2)
self.buttonRefreshPorts = Button(self, text="Re", width=3, command=self.refreshComPortsCombo)
##self.buttonRefreshPorts.grid(row=0, column=2)
self.refreshComPortsCombo()
def refreshComPortsCombo(self):
listComs = self.enumerateComPorts()
if not listComs:
listComs.append("No com ports found")
self.disableControls(~self.isOpen())
self.buttonComClose.configure(state=DISABLED)
else:
self.disableControls(self.isOpen())
self.buttonRefreshPorts.configure(state=NORMAL)
self.comboComport.config(values=listComs)
self.comboComport.current(len(listComs)-1)
##self.after(500, func=self.refreshComPortsCombo)
def enumerateComPorts(self):
"""
Returns the list ofcom port names in the system or an empty list if
no ports found
"""
listComs = []
for port, desc, hwid in sorted(comports()):
listComs.append(port)
return listComs
def openPort(self):
if self.isOpen():
return
self.port = self.comboComport.get()
self.baudrate = int(self.comboComSpeed.get())
self.timeout = 1
try:
self.open()
self.disableControls(self.isOpen())
except IOError:
pass
def closePort(self):
if self.isOpen():
self.flush()
self.close()
self.disableControls(self.isOpen())
def disableControls(self, isConnected):
if isConnected:
self.labelComportName.configure(state=DISABLED)
self.labelComSpeed.configure(state=DISABLED)
self.comboComport.configure(state=DISABLED)
self.comboComSpeed.configure(state=DISABLED)
self.buttonComClose.configure(state=NORMAL)
self.buttonComOpen.configure(state=DISABLED)
self.buttonRefreshPorts.configure(state=DISABLED)
else:
self.labelComportName.configure(state=NORMAL)
self.labelComSpeed.configure(state=NORMAL)
self.comboComport.configure(state=NORMAL)
self.comboComSpeed.configure(state=NORMAL)
self.buttonComClose.configure(state=DISABLED)
self.buttonComOpen.configure(state=NORMAL)
self.buttonRefreshPorts.configure(state=NORMAL)
def main():
pass
if __name__ == '__main__':
main()
UPDATE: I did as Brian advised. Now I have two screen redraw functions. Difference between them is that first moves all the lines to the left adding new to the right and deleting those that fall off the canvas. The second one moves lines to the left and re-deploys elements that fall off the canvas to the right (without creating new ones). There's a huge improvement with any of these in respect to my initial variant, but I don't see big difference between the two wit the naked eye - mayme if I had more elements I would. The latter though works better specifically for my application as I don't have to track those who fall off the cliff.
Here the functions:
def drawGraph(self): ###needed for self.updateGraph2() only as it is creates the lines
for graphNum in range(0, self.numOfGraphs):
self.graphLines.append([])
self.graphData.append([0,]*self.numOfDots)
for iii in range(0,self.numOfDots):
self.graphLines[graphNum].append(
self.canv.create_line(0,0,0,0,fill=self.colors[graphNum],
width=1.2, tags=('graphLines', 'graph'+str(graphNum)))
)
def updateGraph2(self):
while not self.queue.empty():
iTuple = self.queue.get()
self.canv.move('graphLines', -self.density,0)
for graphNum in range(0, self.numOfGraphs):
try: self.graphData[graphNum].append(iTuple[graphNum])
except IndexError:
self.graphData[graphNum].append(0)
self.graphData[graphNum].pop(0)
self.graphLines[graphNum].append(self.graphLines[graphNum].pop(0))
self.canv.coords(self.graphLines[graphNum][-1],
self.canv.winfo_width()-self.density,
int(int(self.graphData[graphNum][-2])+int(self.canv.winfo_height()//2)),
self.canv.winfo_width(),
int(int(self.graphData[graphNum][-1])+int(self.canv.winfo_height()//2))
)
def updateGraph(self):
while not self.queue.empty():
self.timingIndex = self.timingIndex + 1
self.canv.move('graphLines', -self.density, 0)
iTuple = self.queue.get()
for iii in range(0, len(iTuple)):
yyy = int(iTuple[iii])+self.canv.winfo_height()//2
if yyy < 0: yyy = 0
if yyy > self.canv.winfo_height(): yyy = self.canv.winfo_height()
prev_yyy = int(self.prevTuple[iii])+self.canv.winfo_height()//2
if prev_yyy < 0: prev_yyy = 0
if prev_yyy > self.canv.winfo_height(): prev_yyy = self.canv.winfo_height()
self.canv.create_line(
self.canv.winfo_width()-self.density, prev_yyy,
self.canv.winfo_width(), yyy,
width = 1.4, fill = self.colors[iii], tags=('graphLines','graph'+str(iii)))
self.prevTuple = iTuple
self.canv.addtag_overlapping('todelete',-1,-1,-3,self.canv.winfo_height()+1)
self.canv.dtag('preserve','todelete')
self.canv.delete('todelete')
My understanding of the canvas is that the more element ids that have been allocated, the slower it gets. It can handle tens of thousands without much problem (and maybe even 100's of thousands), but if you're creating and deleting 6000 items every 100ms, that is likely your problem. Even though you are deleting the items, it still affects performance especially when you are creating 60,000 per second.
Instead of deleting all items every 100ms, simply move the items off screen and remember them, then reuse them by using the coords method to change their coordinates for the new graph.

Resources