How improve this Matplotlib animation? - matplotlib

I have written a program that plots a logarithm gradually. However, there are two issues:
When the animation is completed, and next I minimize the animation window and then maximize it again, the graph is gone.
When I close the animation window while it is still plotting, then the thread myDataLoop continues, and still prints 'done' after some time. The next time I run the program, it will take longer to start the animation, and the animation becomes glitchy. (This continues until I restart the kernel)
How can I solve this?
import numpy as np
import time
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import matplotlib
matplotlib.use("Qt5Agg")
from matplotlib.figure import Figure
from matplotlib.animation import TimedAnimation
from matplotlib.lines import Line2D
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import threading
#Time in seconds needed to construct the figure
Time=10
Modified_length=10**9
#We plot a log function
cumulative=[]
for i in range(1,int(Modified_length/8000)+1):
cumulative.append(np.log(i*8000))
class CustomMainWindow(QMainWindow):
def __init__(self):
super(CustomMainWindow, self).__init__()
# Define the geometry of the main window
self.setGeometry(0, 30, 1600, 830)
self.setWindowTitle(" ")
# Create FRAME_A
self.FRAME_A = QFrame(self)
self.FRAME_A.setStyleSheet("QWidget { background-color: %s }" % QColor(210,210,235,255).name())
self.LAYOUT_A = QGridLayout()
self.FRAME_A.setLayout(self.LAYOUT_A)
self.setCentralWidget(self.FRAME_A)
# Place the matplotlib figure
self.myFig = CustomFigCanvas()
self.LAYOUT_A.addWidget(self.myFig, *(0,1))
# Add the callbackfunc to ..
myDataLoop = threading.Thread(name = 'myDataLoop', target = dataSendLoop, daemon = True, args = (self.addData_callbackFunc,))
myDataLoop.start()
self.show()
return
def addData_callbackFunc(self, value):
# print("Add data: " + str(value))
self.myFig.addData(value)
return
''' End Class '''
class CustomFigCanvas(FigureCanvas, TimedAnimation):
def __init__(self):
self.addedData = []
# print(matplotlib.__version__)
# The data
self.xlim = int(Modified_length)
self.n = np.linspace(0, self.xlim - 1, int(self.xlim/8000))
self.y=[0]
# The window
self.fig = Figure(figsize=(5,5), dpi=120)
self.ax1 = self.fig.add_subplot(111)
# self.ax1 settings
self.line1 = Line2D([], [], color='blue')
self.ax1.add_line(self.line1)
self.ax1.set_xlim(0, self.xlim - 1)
self.ax1.set_ylim(0,100)
self.ax2 = self.ax1.twinx()
FigureCanvas.__init__(self, self.fig)
TimedAnimation.__init__(self, self.fig, interval = 50, blit = True)
return
def new_frame_seq(self):
return iter(range(self.n.size))
def _init_draw(self):
lines = [self.line1]
for l in lines:
l.set_data([], [])
return
def addData(self, value):
self.addedData.append(value)
return
def _step(self, *args):
# Extends the _step() method for the TimedAnimation class.
try:
TimedAnimation._step(self, *args)
except Exception:
TimedAnimation._stop(self)
pass
return
def _draw_frame(self, framedata):
global Q
while(len(self.addedData) > 0):
self.y=np.append(self.y,self.addedData[0])
del(self.addedData[0])
l=len(self.y)
self.line1.set_data(self.n[ 0 : l], self.y[ 0 : l ])
self._drawn_artists = [self.line1]
return
''' End Class '''
# You need to setup a signal slot mechanism, to
# send data to your GUI in a thread-safe way.
# Believe me, if you don't do this right, things
# go very very wrong..
class Communicate(QObject):
data_signal = pyqtSignal(float)
''' End Class '''
def dataSendLoop(addData_callbackFunc):
# Setup the signal-slot mechanism.
mySrc = Communicate()
mySrc.data_signal.connect(addData_callbackFunc)
# Use the log data
# n = np.linspace(0, int(Modified_length)-1, int(Modified_length))
totaly=cumulative
i = 0
while(True):
if(i > int(Modified_length/8000)-1):
print('done')
break
i = 0
time.sleep(100*Time/Modified_length)
for j in range(100):
mySrc.data_signal.emit(totaly[i]) # <- Here you emit a signal!
i += 1
###
###
if __name__== '__main__':
app = QApplication(sys.argv)
QApplication.setStyle(QStyleFactory.create('Plastique'))
myGUI = CustomMainWindow()
sys.exit(app.exec_())

Related

PyQt - not showing instance of FigureCanvasQTAgg on QtWidget of TabPane

I'm continuing project described more in that question: PyQt - can't read Excel file
Basically my code looks like this right now:
# This is a sample Python script.
# Press Shift+F10 to execute it or replace it with your code.
# Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings.
import csv
import sys
import numpy as np
from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QDialog, QApplication, QFileDialog, QTableWidget, QTableWidgetItem, QTabWidget, QWidget
from PySide6.QtCore import Slot, SIGNAL
from PyQt6.uic import loadUi
import pandas as pd
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
class MplCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=12, height=5, dpi=100):
fig = Figure(figsize=(width, height), dpi=100)
self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
class MainWindow(QDialog):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
self.initUI()
def initUI(self):
loadUi('gui.ui', self)
self.btnShow.setEnabled(False)
self.btnLoad.setEnabled(False)
self.btnBrowse.clicked.connect(self.browseFiles)
self.btnLoad.clicked.connect(self.loadExcelData)
self.btnClean.clicked.connect(self.cleanData)
self.btnShow.clicked.connect(self.showGraphs)
#Slot()
def browseFiles(self):
fname = QFileDialog.getOpenFileName(self, 'Open a file', 'C:\\', "Excel (*.xls *.xlsx)")
self.filename.setText(fname[0])
self.btnLoad.setEnabled(True)
#Slot()
def loadExcelData(self):
column_names = ["Action", "TimeOfFailure", "ReverseRankR", "S(i)", "Cdf", "Ppf", "LogTime"]
df = pd.read_excel(self.filename.text(), "Sheet1", names=column_names)
if df.size == 0:
return
self.tableExcelData.setRowCount(df.shape[0])
self.tableExcelData.setColumnCount(df.shape[1])
self.tableExcelData.setHorizontalHeaderLabels(df.columns)
for row in df.iterrows():
values = row[1]
for col_index, value in enumerate(values):
tableItem = QTableWidgetItem(str(value))
self.tableExcelData.setItem(row[0], col_index, tableItem)
self.btnLoad.setEnabled(False)
self.btnShow.setEnabled(True)
#Slot()
def cleanData(self):
self.btnLoad.setEnabled(True)
self.btnShow.setEnabled(False)
self.tableExcelData.setRowCount(0)
self.tableExcelData.setColumnCount(0)
#Slot()
def showGraphs(self):
timeOfDays = []
cdf = []
ppf = []
logTime = []
for row in range(self.tableExcelData.rowCount()):
isFailure = False
for column in range(self.tableExcelData.columnCount()):
value = self.tableExcelData.item(row, column)
if(column == 0 and str(value.text()) == 'F'):
isFailure = True
if isFailure == True:
if(column == 1): #TimeOfDays
value = int(value.text())
timeOfDays.append(value)
elif(column == 4): #CDF
value = float(value.text())
cdf.append(value)
elif(column == 5):
value = float(value.text())
ppf.append(value)
elif(column == 6):
value = float(value.text())
logTime.append(value)
print(timeOfDays)
print(cdf)
print(ppf)
print(logTime)
#fig = Figure(figsize=(12,5), dpi=100)
#firstSubplot = fig.add_subplot(111)
#firstSubplot.scatter(timeOfDays, ppf, '*')
#firstSubplot.plot(timeOfDays, ppf)
#fig.show()
#plt.plot(timeOfDays, ppf)
#plt.show()
try:
canvasFig = MplCanvas()
canvasFig.axes.scatter(timeOfDays, ppf, s=5, color='red')
canvasFig.axes.plot(timeOfDays, ppf)
canvasFig.draw()
self.tabFirstGraph.setCentralWidget(canvasFig)
except Exception as e:
print('Error: ' + str(e))
#canvas = FigureCanvasTkAgg(fig, master=self)
#canvas.get_tk_widget().pack()
#canvas.draw()
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWidget = QtWidgets.QStackedWidget()
mainWidget.addWidget(mainWindow)
mainWidget.show()
sys.exit(app.exec())
# See PyCharm help at https://www.jetbrains.com/help/pycharm/
I'm trying to generate two graphs (now it's code for only creation of one):
try:
canvasFig = MplCanvas()
canvasFig.axes.scatter(timeOfDays, ppf, s=5, color='red')
canvasFig.axes.plot(timeOfDays, ppf)
canvasFig.draw()
self.tabFirstGraph.setCentralWidget(canvasFig) #
except Exception as e:
print('Error: ' + str(e))
I tried to create another TabPane ("tabFirstGraph" as name of this object) and set canvas figure object to fill this QWidget instance. But I'm getting constantly this error:
Error: 'QWidget' object has no attribute 'setCentralWidget'
I assumed already that problem is with line above (QWidget, QTableWidget don't have this method). But how can I show my canvas figure graph on "First Graph" Tab Pane?
Thanks in advance for your all answers. :)

How to embed an interactive matplotlib widget in a Class using %matplotlib notebook

I've written an interactive GUI with the Matplotlib module widgets that runs directly inside my Jupyter Notebook using %matplotlib notebook (i.e. it does not open a separate window for the GUI as with qt or tk, but runs embedded within the notebook, similar to static plots with %matplotlib inline). The purpose of the GUI is to accept a list of images (2D numpy arrays, displayed using plt.imshow()), display them, and use a button to flip back and forth between images in the list.
I have written a block of code that works just fine based on the canonical example, but it fails when I try to wrap the whole thing within a Class.
My end goal is to execute the GUI with a command like:
GUI_object = interactive_plot_class(img_list), where img_list is the list of images.
Here I've given an example of the codeblock below, which works fine:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
from functools import partial
###############################
# CLASSES
# define a class to keep count of the index being displayed
class Counter:
def __init__(self, initial=0):
self.value = initial
def increment_back(self, amount=1):
self.value -= amount
return self.value
def increment_fwd(self, amount=1):
self.value += amount
return self.value
###############################
# GUI FUNCTIONS
# define function to update plot
def update_plot(counter,img):
ax1.imshow(img, cmap=plot_dict['cmap'], vmin=plot_dict['vmin'], vmax=plot_dict['vmax'], origin='lower', interpolation='nearest')
ax1.set_title('Index: {}'.format(counter.value))
fig.canvas.draw()
# define function for iterating backward through image list
def on_click_prev(button,counter=0):
if counter.value == 0: #minor error handling
pass
else:
counter.increment_back()
img_temp = img_list[counter.value]
update_plot(counter,img_temp)
# define function for iterating forward through image list
def on_click_next(button,counter=0):
if counter.value == (n_imgs-1): #minor error handling
pass
else:
counter.increment_fwd()
img_temp = img_list[counter.value]
update_plot(counter,img_temp)
###############################
# create random images stored in numpy arrays
img_size = 55
n_imgs = 20
img_list = [np.random.random((img_size,img_size)) for x in range(n_imgs)]
# define a dict to store parameters for plotting
plot_dict = {'cmap': 'gray_r', 'vmin': 0., 'vmax':1}
# define counter to track index
counter = Counter()
# Plotting
fig = plt.figure(figsize=(10,8))
ax1 = fig.subplots(1,1)
plt.subplots_adjust(left = 0.4, bottom = 0.3)
#-------BUTTONS--------
# create axes: [xposition, yposition, width, height]
ax_button_next = plt.axes([0.12, 0.05, 0.1, 0.10])
ax_button_prev = plt.axes([0.02, 0.05, 0.1, 0.10])
# properties of the button
next_button = Button(ax_button_next, 'Next', color='white', hovercolor='gainsboro')
prev_button = Button(ax_button_prev, 'Prev', color='white', hovercolor='gainsboro')
# triggering event is the clicking
next_button.on_clicked(partial(on_click_next,counter=counter))
prev_button.on_clicked(partial(on_click_prev,counter=counter))
# define an initial image to display
img_temp = img_list[counter.value].copy()
# display initial image
ax1.imshow(img_temp, cmap=plot_dict['cmap'], vmin=plot_dict['vmin'], vmax=plot_dict['vmax'], origin='lower', interpolation='nearest')
ax1.set_title('Index: {}'.format(counter.value))
plt.show(fig)
This little snippet of code works and does exactly what I want it to do. My problem is that when I try to re-write it inside of a class, nothing happens (i.e. after the initial image is displayed, the buttons are non-responsive and do not call the callback function. In fact, they don't do anything).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
from functools import partial
class interactive_plot_class():
### SUB-CLASSES
# define a class to keep count of the index being displayed
class Counter:
def __init__(self, initial=0):
self.value = initial
def increment_back(self, amount=1):
self.value -= amount
return self.value
def increment_fwd(self, amount=1):
self.value += amount
return self.value
### FUNCTIONS
# define initial state of class
def __init__(self, filter_dict):
self.img_list = filter_dict['images']
self.Index = self.Counter()
self.cmap = 'gray'
self.vmin = 0.
self.vmax = 1
#-------FIGURE--------
# create figure to display image
self.fig = plt.figure(figsize=(10,8))
# create axis for image display
self.ax1 = plt.axes([0.4, 0.3, 0.55, 0.55])
self.disp_img = self.img_list[self.Index.value]
self.ax1.imshow(self.disp_img,cmap=self.cmap, vmin=self.vmin, vmax=self.vmax, origin='lower', interpolation='nearest')
self.ax1.set_title('Index: {}'.format(self.Index.value))
#-------BUTTONS--------
# create axes for buttons: [xposition, yposition, width, height]
self.ax_button_next = plt.axes([0.12, 0.05, 0.1, 0.10])
self.ax_button_prev = plt.axes([0.02, 0.05, 0.1, 0.10])
# properties of the button
self.next_button = Button(self.ax_button_next, 'Next', color='white', hovercolor='gainsboro')
self.prev_button = Button(self.ax_button_prev, 'Prev', color='white', hovercolor='gainsboro')
# assign callback function, triggering event is the clicking
self.prev_button.on_clicked(self.on_click_prev)
self.next_button.on_clicked(self.on_click_next)
# display the figure interactively
self.fig.canvas.draw()
# GUI FUNCTIONS
# define function to update plot
def update_plot(img):
self.ax1.imshow(img, cmap=self.cmap, vmin=selfvmin, vmax=self.vmax, origin='lower', interpolation='nearest')
self.ax1.set_title('Index: {}'.format(self.Index.value))
self.fig.canvas.draw()
# define function for iterating forward through image list
def on_click_next(button_event):
if self.Index.value == (len(self.img_list)-1): #minor error handling
pass
else:
self.Index.increment_fwd()
self.disp_img = self.img_list[self.Index.value]
self.update_plot(self.disp_temp)
# define function for iterating backward through image list
def on_click_prev(button_event):
if self.Index.value == 0: #minor error handling
pass
else:
self.Index.increment_back()
self.disp_img = self.img_list[self.Index.value]
self.update_plot(self.disp_img)
###############
# END OF CLASS
###############
img_size = 55
n_imgs = 20
imgs = [np.random.random((img_size,img_size)) for x in range(n_imgs)]
# store relevant fields to intialize GUI
filter_dict['images'] = imgs
GUI_object = interactive_plot_class(filter_dict)
When I run this second block of code, the buttons don't do anything, nor do they increment the value of the Index counter.
Any help would be most appreciated.

Releasing pan function in NavigationToolbar2QT

I want to cancel pan function from other button. So far, my understanding is that when I want to pan&zoom image, I will click 'Pan' button. If I would like to do other function, e.g. 'Mark' function (in my case), I have to click 'Pan' Button again, then click whatever button I want to do.
I have searched for solving this and found something like 'release_pan', 'button_release_event', but I don't understand how to implement them correctly.
To be clear, I want to cancel pan function from 'Mark' button, and here is my code.
import sys
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Cursor
from matplotlib.backends.qt_compat import QtCore, QtWidgets
if QtCore.qVersion() >= "5.":
from matplotlib.backends.backend_qt5agg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
else:
from matplotlib.backends.backend_qt4agg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.figure import Figure
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.coor = [0,0] #temporary user selection
self.cid = None
self._main = QtWidgets.QWidget()
self.setCentralWidget(self._main)
layout = QtWidgets.QVBoxLayout(self._main)
butt = QtWidgets.QHBoxLayout()
self.static_canvas = FigureCanvas(Figure(figsize=(5, 5), dpi=100))
self.addToolBar = NavigationToolbar(self.static_canvas, self)
self.addToolBar.hide()
self.home = QtWidgets.QPushButton('Home')
self.pan = QtWidgets.QPushButton('Pan')
self.mark = QtWidgets.QPushButton('Mark')
butt.addWidget(self.home)
butt.addWidget(self.pan)
butt.addWidget(self.mark)
layout.addLayout(butt)
layout.addWidget(self.static_canvas)
self._static_ax = self.static_canvas.figure.subplots()
self.tar = plt.imread(r'my_image.tif').copy()
self._static_ax.imshow(self.tar)
# Set cursor
self.cursor = Cursor(self._static_ax, horizOn=True, vertOn=True, useblit=True,
color = 'r', linewidth = 1)
#trigger zone
self.home.clicked.connect(self.Home)
self.pan.clicked.connect(self.Pan)
self.mark.clicked.connect(self.Mark)
def coor_onclick(self, event):
"""
This function will get coordination from click and plot it on canvas
"""
#check out-figure click
if event.xdata == None or event.ydata == None:
pass
else:
self.coor[0] = int(event.xdata)
self.coor[1] = int(event.ydata)
# print(self.coor)
#show line marking on canvas
tar = self.tar.copy()
#NOTE:: self.coor = [x,y] = [col, row]
# x = self.coor[0]
# y = self.coor[1]
#marking line
for r in range(tar.shape[1]):
for c in range(tar.shape[0]):
tar[self.coor[1], c] = [255, 0, 0]
tar[r, self.coor[0]] = [255, 0, 0]
#set final mark on canvas
self._static_ax.clear()
self._static_ax.imshow(tar)
self._static_ax.axis('off')
# Set cursor
self.cursor = Cursor(self._static_ax, horizOn=True, vertOn=True, useblit=True,
color = 'r', linewidth = 1)
self.static_canvas.draw()
def Home(self):
self.cid = self.static_canvas.mpl_connect('button_press_event', self.coor_onclick)
self.addToolBar.home()
def Pan(self):
if self.cid is None:
pass
else:
#disconnect to self.coor_onclick
self.static_canvas.mpl_disconnect(self.cid)
self.addToolBar.pan()
def Mark(self):
self.cid = self.static_canvas.mpl_connect('button_press_event', self.coor_onclick)
if __name__ == "__main__":
# Check whether there is already a running QApplication (e.g., if running
# from an IDE).
qapp = QtWidgets.QApplication.instance()
if not qapp:
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
app.activateWindow()
app.raise_()
qapp.exec_()
I have modified from matplotlib documentation.
Check the current mode of NavigationToolbar and if the mode is "PAN", set the mode off by calling pan() again (which will uncheck the action (check out the source code for more details.)).
FYI:
You can check the current mode of the NavigationToolbar by using NavigationToolbar.mode.name, currently there are two modes: "ZOOM" and "PAN".
In your code, change function Mark like this:
def Mark(self):
# if the current mode is Pan, set the mode off by unchecking it.
if self.nav_toolbar.mode.name == "PAN":
self.nav_toolbar.pan()
self.cid = self.static_canvas.mpl_connect(
'button_press_event', self.coor_onclick)

Resize one subplot after removing another

Problem description:
I'm building an interface for my lab, I intergrarted matplotlib with pyqt5 widget, there is a real time video display widget working on multi-thread and queue. I managed to show single shot with cross-section plot by adding divider. However, when I remove the cross-section plots, and redraw the figure_idle, the video frame can never moved back to its initial position with its initial size. I adjust the figure with navigator_tool_bar (top, bottom...), However it seems that there are blank areas left after removing the cross-section plots. Do you have any idea?
The initial figure:
Display cross-sections:
Clear cross-section and redraw:
Video widget code:
class VideoViewer(FigureCanvas):
def __init__(self, parent=None, width=4, height=4, dpi=70):
self.figwidth = width
self.figheight = height
self.fig = Figure(figsize=(self.figwidth, self.figheight), dpi=dpi)
FigureCanvas.__init__(self, self.fig)
FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.axes = self.fig.add_subplot()
self.fig.subplots_adjust(top=0.975,bottom=0.048,left=0.029,right=0.983,hspace=0.2,wspace=0.2)
Cross section code:
#create cross-sections
self.divider = make_axes_locatable(self.axes)
self.top_ax = self.divider.append_axes("top", 1.05, pad=0.2,sharex=self.axes)
self.right_ax = self.divider.append_axes("right", 1.05,pad=0.2,sharey=self.axes)
#create lines
self.v_line = self.axes.axvline(xvlinepos, color='r')
self.h_line = self.axes.axhline(yhlinepos, color='g')
self.v_cross, = self.right_ax.plot(Norm_xvlinedata,np.arange(self.ImageData.shape[0]), 'r-')
self.h_cross, = self.top_ax.plot(np.arange(self.ImageData.shape[1]),Norm_yhlinedata, 'g-')
Clear cross-section code:
def ClearCrossSection(self):
self.fig.canvas.mpl_disconnect(self.pick_event_v)
self.fig.canvas.mpl_disconnect(self.pick_event_h)
self.h_line.remove()
self.v_line.remove()
self.v_cross.remove()
self.h_cross.remove()
self.right_ax.remove()
self.top_ax.remove()
self.fig.canvas.draw_idle()
What I did:
Light_layout + subplot_adjust -----> does not work.
Constrained_layout -----> does not work.
Constrained_layout + GridSpec by declaring at beginning self.axes takes all cols and rows.-----> Does not work.
An exemple of the problem:
import sys
# GUI
from PyQt5.QtWidgets import*
from PyQt5.QtCore import *
from PyQt5.QtGui import *
# Matplotlib
import matplotlib
matplotlib.use('Qt5Agg')
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
import matplotlib.gridspec as gridspec
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5 import NavigationToolbar2QT as NavigationToolbar
from mpl_toolkits.axes_grid1 import make_axes_locatable
from PIL import Image
import matplotlib.lines as lines
# Generate data
import numpy as np
'''
need to update video frame, I'm using blitting, so better not clear the whole figure.
the whole code could be too long to show here.
'''
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.resize(800,600)
self.setGeometry(350,250,950,600)
# Creat MainWidget
self.MainWidget = QWidget()
self.LayoutMainWidget = QGridLayout()
self.MainWidget.setLayout(self.LayoutMainWidget)
# Matplotlib widget
self.MatplotViewer = VideoViewer()
self.FigureTool = NavigationToolbar(self.MatplotViewer, self)
# Button plot image
self.ButtPltImg = QPushButton("Plot Image")
# BUtton plot cross
self.ButtPltCross = QPushButton("Cross View")
self.ButtPltCross.setCheckable(True)
self.ButtPltCross.setStyleSheet("background-color: Green")
# add widgets
self.LayoutMainWidget.addWidget(self.MatplotViewer,0,0,7,7)
self.LayoutMainWidget.addWidget(self.FigureTool, 7,0,1,7)
self.LayoutMainWidget.addWidget(self.ButtPltImg, 2,7,1,1)
self.LayoutMainWidget.addWidget(self.ButtPltCross, 3,7,1,1)
# Set central widget
self.setCentralWidget(self.MainWidget)
self.connection()
def GenerateImage(self, state):
if self.ButtPltCross.isChecked():
self.ButtPltCross.setChecked(False)
self.MatplotViewer.ClearCrossSection()
self.MatplotViewer.UpdateFrame()
else:
self.MatplotViewer.UpdateFrame()
def PlotCrossSection(self, state):
if self.ButtPltCross.isChecked():
self.MatplotViewer.PlotCrossSection()
def ClearCrossSection(self, state):
if not(self.ButtPltCross.isChecked()):
self.MatplotViewer.ClearCrossSection()
def connection(self):
self.ButtPltImg.clicked.connect(lambda state=True: self.GenerateImage(state))
self.ButtPltCross.clicked.connect(lambda state=True: self.PlotCrossSection(state))
self.ButtPltCross.clicked.connect(lambda state=True: self.ClearCrossSection(state))
class VideoViewer(FigureCanvas):
def __init__(self, parent=None, width=4, height=4, dpi=70):
# Figure
self.fig = Figure(figsize=(width, height), dpi=dpi)
# Init Parent
FigureCanvas.__init__(self, self.fig)
FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
# Ax
self.axes = self.fig.add_subplot(111)
self.fig.subplots_adjust(top=0.975,bottom=0.048,left=0.029,right=0.983,hspace=0.2,wspace=0.2)
# Plot init image
self.PlotInitFrame()
def PlotInitFrame(self):
self.ImageData = self.ImageGenerate()
self.image = self.axes.imshow(self.ImageData, cmap='Greys', interpolation='none')
self.fig.canvas.draw_idle()
def UpdateFrame(self):
self.ImageData = self.ImageGenerate()
self.image.set_data(self.ImageData)
self.fig.canvas.draw_idle()
def PlotCrossSection(self):
# create axes
self.divider = make_axes_locatable(self.axes)
self.top_ax = self.divider.append_axes("top", 1.05, pad=0.2,sharex=self.axes)
self.right_ax = self.divider.append_axes("right", 1.05, pad=0.2,sharey=self.axes)
self.top_ax.xaxis.set_tick_params(labelbottom=False)
self.right_ax.yaxis.set_tick_params(labelleft=False)
# set cross section limit
self.right_ax.set_xlim(right=1.05)
self.top_ax.set_ylim(top=1.05)
# some pars
xmin, xmax = self.axes.get_xlim()
ymin, ymax = self.axes.get_ylim()
v_mid = int((xmin + xmax)/2)
h_mid = int((ymin + ymax)/2)
# set line
self.v_line = lines.Line2D([v_mid, v_mid], [ymin, ymax], color='r', pickradius=5)
self.axes.add_line(self.v_line)
self.h_line = lines.Line2D([xmin, xmax], [h_mid, h_mid], color='g', pickradius=5)
self.axes.add_line(self.h_line)
# set cross section data
Norm_xvlinedata = self.NormalizeData(self.ImageData[:,v_mid])
self.v_cross, = self.right_ax.plot(Norm_xvlinedata, np.arange(self.ImageData.shape[0]), 'r-')
Norm_yhlinedata = self.NormalizeData(self.ImageData[h_mid,:])
self.h_cross, = self.top_ax.plot(np.arange(self.ImageData.shape[1]), Norm_yhlinedata, 'g-')
self.fig.canvas.draw_idle()
def NormalizeData(self, data_temp):
min_temp = np.min(data_temp)
max_temp = np.max(data_temp)
if min_temp != max_temp:
return (data_temp-min_temp)/(max_temp-min_temp)
else:
return data_temp/data_temp
def ClearCrossSection(self):
self.v_line.remove()
self.h_line.remove()
self.top_ax.remove()
self.right_ax.remove()
self.fig.canvas.draw_idle()
def ImageGenerate(self):
xx,yy = np.meshgrid(np.linspace(-502,502,1024),np.linspace(-502,502,1024))
r = np.sqrt(xx**2+yy**2)
AMP = np.random.randint(150,250)
SIG = np.random.randint(200,250)
T = np.random.randint(115,135)
return AMP*np.exp(-(r)**2/(2*SIG**2))*np.cos(2*np.pi/T*r)
if __name__ == '__main__':
app = QApplication(sys.argv)
MainWindow = Window()
MainWindow.showMaximized()
sys.exit(app.exec_())

Closing and deleteLater-ing PyQt tabs with Matplotlib figures on them doesn't free up memory

I've created a simple PyQt gui that can open/close files containing time-series data and display graphs using matplotlib. Each new file is displayed on a new tab. When a tab is closed, all references to the figure etc. should be deleted. To tell PyQt to destroy the Qt items, I'm calling deleteLater() on the closing tab.
However, someone's not letting go of the memory :(
I've tried overriding deleteLater() and clearing the figure/axes before calling deleteLater() on the parent but that only frees up a fraction of the memory.
Anyone?
Update: Managed to make a debug example that reproduces some of the behaviour:
#!/usr/bin/env python
# AbfView debug example
# Copyright 2014 Michael Clerx (michael#myokit.org)
import gc
import sys
# PyQt for python 2
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
# Matplotlib
import matplotlib
matplotlib.use('Qt4Agg')
import matplotlib.backends.backend_qt4agg as backend
import matplotlib.figure
class AbfView(QtGui.QMainWindow):
"""
Main window
"""
def __init__(self):
super(AbfView, self).__init__()
# Set size, center
self.resize(800,600)
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
self.create_toolbar()
# Add widget for Abf file tabs
self._tabs = QtGui.QTabWidget()
self._tabs.setTabsClosable(True)
self._tabs.tabCloseRequested.connect(self.action_close)
self.setCentralWidget(self._tabs)
def action_open(self, event):
"""
Mock-up file opening
"""
for i in xrange(1):
filename = 'file_' + str(i) + '.txt'
abf = AbfFile(filename)
self._tabs.addTab(AbfTab(self, abf), filename)
def action_close(self, index):
"""
Called when a tab should be closed
"""
tab = self._tabs.widget(index)
self._tabs.removeTab(index)
if tab is not None:
tab.deleteLater()
gc.collect()
del(tab)
def create_toolbar(self):
"""
Creates this widget's toolbar
"""
self._tool_open = QtGui.QAction('&Open', self)
self._tool_open.setShortcut('Ctrl+O')
self._tool_open.setStatusTip('Open a file')
self._tool_open.setIcon(QtGui.QIcon.fromTheme('document-open'))
self._tool_open.triggered.connect(self.action_open)
self._toolbar = self.addToolBar('tools')
self._toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self._toolbar.addAction(self._tool_open)
class AbfTab(QtGui.QTabWidget):
"""
A Qt widget displaying an ABF file.
"""
def __init__(self, parent, abf):
super(AbfTab, self).__init__(parent)
self.setTabsClosable(False)
self.setTabPosition(self.East)
self._abf = abf
self._abf.fold_sweeps()
self._abf.set_time_scale(1000)
self._figures = []
self._axes = []
for i in xrange(self._abf.count_data_channels()):
self.addTab(self.create_graph_tab(i), 'AD' + str(i))
for i in xrange(self._abf.count_protocol_channels()):
self.addTab(self.create_protocol_tab(i), 'DA' + str(i))
self.addTab(self.create_info_tab(), 'Info')
def create_graph_tab(self, channel):
"""
Creates a widget displaying the main data.
"""
widget = QtGui.QWidget(self)
# Create figure
figure = matplotlib.figure.Figure()
figure.suptitle(self._abf.filename())
canvas = backend.FigureCanvasQTAgg(figure)
canvas.setParent(widget)
axes = figure.add_subplot(1,1,1)
toolbar = backend.NavigationToolbar2QTAgg(canvas, widget)
# Draw lines
for i, sweep in enumerate(self._abf):
c = sweep[channel]
axes.plot(c.times(), c.values())
# Create a layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(canvas)
vbox.addWidget(toolbar)
widget.setLayout(vbox)
self._figures.append(figure)
self._axes.append(axes)
return widget
def create_protocol_tab(self, channel):
"""
Creates a widget displaying a stored D/A signal.
"""
widget = QtGui.QWidget(self)
# Create figure
figure = matplotlib.figure.Figure()
figure.suptitle(self._abf.filename())
canvas = backend.FigureCanvasQTAgg(figure)
canvas.setParent(widget)
axes = figure.add_subplot(1,1,1)
toolbar = backend.NavigationToolbar2QTAgg(canvas, widget)
# Draw lines
for i, sweep in enumerate(self._abf.protocol()):
c = sweep[channel]
axes.plot(c.times(), c.values())
# Create a layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(canvas)
vbox.addWidget(toolbar)
widget.setLayout(vbox)
self._figures.append(figure)
self._axes.append(axes)
return widget
def create_info_tab(self):
"""
Creates a tab displaying information about the file.
"""
widget = QtGui.QTextEdit(self)
widget.setText(self._abf.info(show_header=True))
widget.setReadOnly(True)
return widget
def deleteLater(self):
"""
Deletes this tab (later).
"""
for figure in self._figures:
figure.clear()
for axes in self._axes:
axes.cla()
del(self._abf, self._figures, self._axes)
gc.collect()
super(AbfTab, self).deleteLater()
class AbfFile(object):
"""
Mock-up for abf file class
"""
def __init__(self, filename):
import numpy as np
self._filename = filename
n = 500000
s = 20
self._time = np.linspace(0,6,n)
self._data = []
self._prot = []
for i in xrange(s):
self._data.append(AbfFile.Sweep(self._time,
np.sin(self._time + np.random.random() * 36)))
self._prot.append(AbfFile.Sweep(self._time,
np.cos(self._time + np.random.random() * 36)))
def count_data_channels(self):
return 1
def count_protocol_channels(self):
return 4
def info(self, show_header=False):
return 'fake info'
def fold_sweeps(self):
pass
def set_time_scale(self, scale):
pass
def __iter__(self):
return iter(self._data)
def protocol(self):
return iter(self._prot)
def filename(self):
return self._filename
class Sweep(object):
def __init__(self, time, data):
self._channel = AbfFile.Channel(time, data)
def __getitem__(self, index):
return self._channel
class Channel(object):
def __init__(self, time, data):
self._time = time
self._data = data
def times(self):
return self._time
def values(self):
return self._data
def load():
"""
Loads the Gui, and adds signal handling.
"""
import sys
import signal
app = QtGui.QApplication(sys.argv)
# Close with last window
app.connect(app, QtCore.SIGNAL('lastWindowClosed()'),
app, QtCore.SLOT('quit()'))
# Close on Ctrl-C
def int_signal(signum, frame):
app.closeAllWindows()
signal.signal(signal.SIGINT, int_signal)
# Create main window and show
window = AbfView()
window.show()
# For some reason, PyQt needs the focus to handle the SIGINT catching...
timer = QtCore.QTimer()
timer.start(500) # Flags timeout every 500ms
timer.timeout.connect(lambda: None)
# Wait for app to exit
sys.exit(app.exec_())
if __name__ == '__main__':
load()
To reproduce: Run the program, then click "Open" (or Ctrl-O), and then "Open" again. Now close all tabs. No memory is freed. I'm wondering if this is some kind of performance hack in matplotlib. If so, is there a way to tell it to let the memory go?