object oriented architecture and pickling problems and multiprocessing in Tkinter/matplotlib GUI - oop

I know that several questions have been created with people asking about non-responsive GUIs and the ultimate answer is that Tkinter is not thread safe. However, it is my understanding that queues can be utilized to overcome this problem. Therefore, I have been looking into using the multiprocessing module with queues such that my code can be utilized on hyperthreaded and multicore systems.
What I would like to do is to try and do a very complex least squares fitting of multiple imported spectra in different tabs whenever a button is pressed.
The problem is that my code is still hanging up on the long process that I initialize by a button in my GUI. I have knocked the code down to something that still may run and has most of the objects of my original program, yet still suffers from the problem of not being responsive.
I believe my problem is in the multiprocessing portion of my program.
Therefore my question is regarding the multiprocessing portion of the code and if there is a better way to organize the process_spectra() function shown here:
def process_spectra(self):
process_list = []
queue = mp.Queue()
for tab in self.tab_list:
process_list.append(mp.Process(target=Deconvolution(tab).deconvolute(), args=(queue,)))
process_list[-1].start()
process_list[-1].join()
return
At the moment it appears that this is not actually making the deconvolution process into a different thread. I would like the process_spectra function to process all of the spectra with the deconvolution function simultaneously while still being able to interact with and see the changes in the spectra and GUI.
Here is the full code which can be run as a .py file directly to reproduce my problem:
from Tkinter import *
import Tkinter
import tkFileDialog
import matplotlib
from matplotlib import *
matplotlib.use('TKAgg')
from matplotlib import pyplot, figure, backends
import numpy as np
import lmfit
import multiprocessing as mp
# lots of different peaks can appear
class peak:
def __init__(self, n, m):
self.n = n
self.m = m
def location(self, i):
location = i*self.m/self.n
return location
def NM(self):
return str(self.n) + str(self.m)
# The main function that is given by the user has X and Y data and peak data
class Spectra:
def __init__(self, spectra_name, X, Y):
self.spectra_name = spectra_name
self.X = X
self.Y = Y
self.Y_model = Y*0
self.Y_background_model = Y*0
self.Y_without_background_model = Y*0
self.dYdX = np.diff(self.Y)/np.diff(self.X)
self.peak_list = self.initialize_peaks(3, 60)
self.params = lmfit.Parameters()
def peak_amplitude_dictionary(self):
peak_amplitude_dict = {}
for peak in self.peak_list:
peak_amplitude_dict[peak] = self.params['P' + peak.NM() + '_1_amp'].value
return peak_amplitude_dict
def peak_percentage_dictionary(self):
peak_percentage_dict = {}
for peak in self.peak_list:
peak_percentage_dict[peak] = self.peak_amplitude_dictionary()[peak]/np.sum(self.peak_amplitude_dictionary().values())
return peak_percentage_dict
# Function to create all of the peaks and store them in a list
def initialize_peaks(self, lowestNM, highestNM):
peaks=[]
for n in range(0,highestNM+1):
for m in range(0,highestNM+1):
if(n<lowestNM and m<lowestNM): break
elif(n<m): break
else: peaks.append(peak(n,m))
return peaks
# This is just a whole bunch of GUI stuff
class Spectra_Tab(Frame):
def __init__(self, parent, spectra):
self.spectra = spectra
self.parent = parent
Frame.__init__(self, parent)
self.tab_name = spectra.spectra_name
self.canvas_frame = Frame(self, bd=3, bg= 'WHITE', relief=SUNKEN)
self.canvas_frame.pack(side=LEFT, fill=BOTH, padx=0, pady=0, expand=1)
self.results_frame = Frame(self, bd=3, bg= 'WHITE', relief=SUNKEN, width=600)
self.results_frame.pack(side=RIGHT, fill=BOTH, padx=0, pady=0, expand=1)
self.top_canvas_frame = Frame(self.canvas_frame, bd=0, bg= 'WHITE', relief=SUNKEN)
self.top_canvas_frame.pack(side=TOP, fill=BOTH, padx=0, pady=0, expand=1)
self.original_frame = Frame(self.top_canvas_frame, bd=1, relief=SUNKEN)
self.original_frame.pack(side=LEFT, fill=BOTH, padx=0, pady=0, expand=1)
self.scrollbar = Scrollbar(self.results_frame)
self.scrollbar.pack(side=RIGHT, fill=BOTH,expand=1)
self.sidebar = Listbox(self.results_frame)
self.sidebar.pack(fill=BOTH, expand=1)
self.sidebar.config(yscrollcommand=self.scrollbar.set)
self.scrollbar.config(command=self.sidebar.yview)
self.original_fig = figure.Figure()
self.original_plot = self.original_fig.add_subplot(111)
init_values = np.zeros(len(self.spectra.Y))
self.original_line, = self.original_plot.plot(self.spectra.X, self.spectra.Y, 'r-')
self.original_background_line, = self.original_plot.plot(self.spectra.X, init_values, 'k-', animated=True)
self.original_canvas = backends.backend_tkagg.FigureCanvasTkAgg(self.original_fig, master=self.original_frame)
self.original_canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
self.original_canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
self.original_canvas.show()
self.original_canvas.draw()
self.original_canvas_BBox = self.original_plot.figure.canvas.copy_from_bbox(self.original_plot.bbox)
ax1 = self.original_plot.figure.axes[0]
ax1.set_xlim(self.spectra.X.min(), self.spectra.X.max())
ax1.set_ylim(0, self.spectra.Y.max() + .05*self.spectra.Y.max())
self.step=0
self.update()
# This just refreshes the GUI stuff everytime that the parameters are fit in the least squares method
def refreshFigure(self):
self.step=self.step+1
if(self.step==1):
self.original_canvas_BBox = self.original_plot.figure.canvas.copy_from_bbox(self.original_plot.bbox)
self.original_plot.figure.canvas.restore_region(self.original_canvas_BBox)
self.original_background_line.set_data(self.spectra.X, self.spectra.Y_background_model)
self.original_plot.draw_artist(self.original_line)
self.original_plot.draw_artist(self.original_background_line)
self.original_plot.figure.canvas.blit(self.original_plot.bbox)
# show percentage of peaks on the side bar
self.sidebar.delete(0, Tkinter.END)
peak_dict = self.spectra.peak_percentage_dictionary()
for peak in sorted(peak_dict.iterkeys()):
self.sidebar.insert(0, peak.NM() + ' ' + str(peak_dict[peak]) + '%' )
return
# just a tab bar
class TabBar(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.tabs = {}
self.buttons = {}
self.current_tab = None
def show(self):
self.pack(side=BOTTOM, expand=0, fill=X)
def add(self, tab):
tab.pack_forget()
self.tabs[tab.tab_name] = tab
b = Button(self, text=tab.tab_name, relief=RAISED, command=(lambda name=tab.tab_name: self.switch_tab(name)))
b.pack(side=LEFT)
self.buttons[tab.tab_name] = b
def switch_tab(self, name):
if self.current_tab:
self.buttons[self.current_tab].config(relief=RAISED)
self.tabs[self.current_tab].pack_forget()
self.tabs[name].pack(side=BOTTOM)
self.current_tab = name
self.buttons[name].config(relief=SUNKEN)
class Deconvolution:
def __init__(self, spectra_tab):
self.spectra_tab = spectra_tab
self.spectra = spectra_tab.spectra
self.model = [0 for x in self.spectra.X]
self.model_without_background = [0 for x in self.spectra.X]
self.residual_array = [0 for x in self.spectra.X]
# Amplitudes for backgrounds
self.pi_plasmon_amp = np.interp(4.3, self.spectra.X, self.spectra.Y)
self.graphite_amp = np.interp(5, self.spectra.X, self.spectra.Y)
self.spectra.params.add('PPAmp', value=self.pi_plasmon_amp, vary=True, min=0.0, max=None)
self.spectra.params.add('PPCenter', value=4.3, vary=True)
self.spectra.params.add('PPFWHM', value=.4, vary=True)
self.spectra.params.add('GLAmp', value=self.graphite_amp, vary=True, min=0.0, max=None)
self.spectra.params.add('GLCenter', value=5, vary=True)
self.spectra.params.add('GLFWHM', value=.4, vary=True)
self.background_model = self.pseudoVoigt(self.spectra.X, self.spectra.params['PPAmp'].value, self.spectra.params['PPCenter'].value, self.spectra.params['PPFWHM'].value, 1)+\
self.pseudoVoigt(self.spectra.X, self.spectra.params['GLAmp'].value, self.spectra.params['GLCenter'].value, self.spectra.params['GLFWHM'].value, 1)
for peak in self.spectra.peak_list:
for i in range(1,4):
param_prefix = 'P' + peak.NM() + '_' + str(i)
center = peak.location(i)
amp = np.interp(center, self.spectra.X, self.spectra.Y - self.background_model)
width = 0.02
self.spectra.params.add(param_prefix + '_amp', value = 0.8*amp, vary=False, min=0.0, max=None)
self.spectra.params.add(param_prefix + '_center', value = center, vary=False, min=0.0, max=None)
self.spectra.params.add(param_prefix + '_width', value = width, vary=False, min=0.0, max=None)
self.model_without_background += self.pseudoVoigt(self.spectra.X, self.spectra.params[param_prefix + '_amp'].value, self.spectra.params[param_prefix + '_center'].value, self.spectra.params[param_prefix + '_width'].value, 1)
def deconvolute(self):
for State in range(0,3):
# Make each voigt profile for each tube
for peak in self.spectra.peak_list:
for i in range(1,4):
param_prefix = 'P' + peak.NM() + '_' + str(i)
if(State==1):
self.spectra.params[param_prefix + '_amp'].vary = True
if(State==2):
self.spectra.params[param_prefix + '_width'].vary = True
result = lmfit.Minimizer(self.residual, self.spectra.params, fcn_args=(State,))
result.prepare_fit()
result.leastsq()#lbfgsb()
def residual(self, params, State):
self.model = self.background_model
if(State>0):
self.model += self.model_without_background
for x in range(0, len(self.spectra.X)):
if(self.background_model[x]>self.spectra.Y[x]):
self.residual_array[x] = -999999.-9999.*(self.spectra.Y[x]-self.background_model[x])
else:
self.residual_array[x] = self.spectra.Y[x]-self.model[x]
self.spectra.Y_model = self.model
self.spectra.Y_background_model = self.background_model
self.spectra.Y_without_background_model = self.model_without_background
self.spectra_tab.refreshFigure()
return self.residual_array
def pseudoVoigt(self, x, amp, center, width, shapeFactor):
LorentzPortion = (width**2/((x-center)**2+width**2))
GaussianPortion = 1/(np.sqrt(2*np.pi*width**2))*np.e**(-(x-center)**2/(2*width**2))
try:
Voigt = amp*(shapeFactor*LorentzPortion+(1-shapeFactor)*GaussianPortion)
except ZeroDivisionError:
width = width+0.01
LorentzPortion = (width**2/((x-center)**2+width**2))
GaussianPortion = 1/(np.sqrt(2*np.pi*width**2))*np.e**(-(x-center)**2/(2*width**2))
Voigt = amp*(shapeFactor*LorentzPortion+(1-shapeFactor)*GaussianPortion)
return Voigt
class MainWindow(Tk):
def __init__(self, parent):
Tk.__init__(self, parent)
self.parent = parent
self.wm_state('zoomed')
self.spectra_list = []
self.tab_list = []
self.button_frame = Frame(self, bd=3, relief=SUNKEN)
self.button_frame.pack(side=TOP, fill=BOTH)
self.tab_frame = Frame(self, bd=3, relief=SUNKEN)
self.tab_frame.pack(side=BOTTOM, fill=BOTH, expand=1)
open_spectra_button = Button(self.button_frame, text='open spectra', command=self.open_spectra)
open_spectra_button.pack(side=LEFT, fill=Y)
process_spectra_button = Button(self.button_frame, text='process spectra', command=self.process_spectra)
process_spectra_button.pack(side=LEFT, fill=Y)
self.tab_bar = TabBar(self.tab_frame)
self.tab_bar.show()
self.resizable(True,False)
self.update()
def open_spectra(self):
# This will prompt user for file input later, but here is an example
file_name_list = ['spectra_1', 'spectra_2']
for file_name in file_name_list:
# Just make up functions that may be imported
X_values = np.arange(1240.0/1350.0, 1240./200., 0.01)
if(file_name=='spectra_1'):
Y_values = np.array(np.e**.2*X_values + np.sin(10*X_values)+np.cos(4*X_values))
if(file_name=='spectra_2'):
Y_values = np.array(np.e**.2*X_values + np.sin(10*X_values)+np.cos(3*X_values)+.3*np.cos(.5*X_values))
self.spectra_list.append(Spectra(file_name, X_values, Y_values))
self.tab_list.append(Spectra_Tab(self.tab_frame, self.spectra_list[-1]))
self.tab_bar.add(self.tab_list[-1])
self.tab_bar.switch_tab(self.spectra_list[0].spectra_name)
self.tab_bar.show()
return
def process_spectra(self):
process_list = []
queue = mp.Queue()
for tab in self.tab_list:
process_list.append(mp.Process(target=Deconvolution(tab).deconvolute(), args=(queue,)))
process_list[-1].start()
process_list[-1].join()
return
if __name__ == "__main__":
root = MainWindow(None)
root.mainloop()
EDIT:
I am editing this question because I realized that my question did not regard the real problem. I think the code I have supplied has problems with having a Tkinter Frame passed as a parameter to something that needs to be pickled, ? and it can't because it's not thread safe?? It gives a pickle error that points to Tkinter in some way.
However, I am not sure how to reorganize this code such that the only part that is pickled is the data part since the threads or processes must access the Tkinter frames in order to update them via refreshFigure().
Does anyone have any ideas regarding how to do this? I have researched it but everyone's examples are usually simple with only one figure or that only refreshes after the process is completed.

The segment target=Deconvolution(tab).deconvolute() will actually be evaluated instead of passed to a subprocess. You could replace this with a wrapper function
def mp_deconvolute(tab):
return Deconvolution(tab).deconvolute()
I'm not sure if your queue is actually be used at all but I believe that would be more appropriate for a worker Pool scenario.
Edit:
Oh, and you would call it like so
process_list.append(mp.Process(target=mp_deconvolute, args=(tab)))
Edit again:
You could just define that as a lambda function too unless you to to add more complexity
mp_deconv = lambda x: Deconvolution(tab).deconvolute()
process_list.append(mp.Process(target=mp_deconv, args=(tab)))

Related

MicroPython Thread not exiting (Pi Pico)

I'm attempting to write a fairly simple class to handle some relays for a robot costume. The user should be able to push some buttons to activate different sets of lights/EL wire. A basic thread seemed the best way to handle this, but maybe I'm missing something about the micropython implementation...
Here's the relevant two functions:
def run(self):
"""Start the default state"""
self.current_state.start_new_thread(self.activate_emotion, (self.state,))
def change_state(self):
"""Kill the old state (if applicable) and start a new one"""
print('Exiting thread...')
self.current_state.exit()
print('Starting new thread...')
self.current_state.start_new_thread(self.activate_emotion, (self.state,))
print('Done.')
When my script starts, run() throws "OSError: core1 in use", but the "default" state begins to run. It does this even after a fresh startup. When my button press is detected and activates change_state(), I get the output: "Exiting thread...", and it just hangs there indefinitely. What am I missing here? Any help would be greatly appreciated.
Here's the entirety of my script:
from machine import Pin
import utime
import _thread
import random
class LowRelay(Pin):
def turn_on(self):
self.low()
def turn_off(self):
self.high()
def test(self):
self.turn_on()
utime.sleep(0.5)
self.turn_off()
class Ariel():
def __init__(self):
self.happy_button = Pin(10, Pin.IN, Pin.PULL_DOWN)
self.sad_button = Pin(11, Pin.IN, Pin.PULL_DOWN)
self.scared_button = Pin(12, Pin.IN, Pin.PULL_DOWN)
self.thinking_button = Pin(13, Pin.IN, Pin.PULL_DOWN)
self.happy_relay = LowRelay(2, Pin.OUT)
self.sad_relay = LowRelay(3, Pin.OUT)
self.scared_relay = LowRelay(4, Pin.OUT)
self.thinking_relay = LowRelay(5, Pin.OUT)
self.group1_relay = LowRelay(6, Pin.OUT)
self.group2_relay = LowRelay(7, Pin.OUT)
self.group3_relay = LowRelay(8, Pin.OUT)
self.group4_relay = LowRelay(9, Pin.OUT)
self.led_groups = ['group1', 'group2',
'group3', 'group4']
self.off_led_groups = ['group1', 'group2',
'group3', 'group4']
self.on_led_groups = []
self.state = 'default'
self.current_state = _thread
def full_test(self):
""" Self test """
print('Testing....')
self.group1_relay.test()
self.group2_relay.test()
self.group3_relay.test()
self.group4_relay.test()
self.happy_relay.test()
self.sad_relay.test()
self.scared_relay.test()
self.thinking_relay.test()
print('Test complete.')
utime.sleep(1)
def test(self):
"""Quick Test"""
self.all_relays_off()
utime.sleep(1)
self.all_relays_on()
utime.sleep(1)
self.all_relays_off()
def run(self):
"""Start the default state"""
self.current_state.start_new_thread(self.activate_emotion, (self.state,))
def change_state(self):
"""Kill the old state (if applicable) and start a new one"""
print('Exiting thread...')
self.current_state.exit()
print('Starting new thread...')
self.current_state.start_new_thread(self.activate_emotion, (self.state,))
print('Done.')
def watch_and_wait(self, seconds):
"""Sleep while an effect processes, watching for a state change."""
start_state = self.state
ticks = int(float(seconds)/0.25)
for x in range(ticks):
self.check_buttons()
if self.state != start_state:
print('Changing state...')
self.change_state()
continue
utime.sleep(0.25)
def check_buttons(self):
"""Set states based on button presses"""
if self.happy_button.value():
self.state = 'happy'
if self.sad_button.value():
self.state = 'sad'
if self.scared_button.value():
self.state = 'scared'
if self.thinking_button.value():
self.state = 'thinking'
def swap_groups(self):
"""
Pick one of the two LED groups that is not on, and one that is.
Swap them. If none are on, turn on two.
"""
off_lg = self.off_led_groups
on_lg = self.on_led_groups
on_candidate = off_lg.pop(random.randrange(len(off_lg)))
off_candidate = on_lg.pop(random.randrange(len(on_lg))) if on_lg else None
on_lg.append(on_candidate)
if off_candidate:
off_lg.append(off_candidate)
if len(on_lg) < 2:
on_lg.append(off_lg.pop(random.randrange(len(off_lg))))
for x in self.led_groups:
r = getattr(self, '%s_relay' % x)
if x in on_lg:
r.turn_on()
else:
r.turn_off()
def all_relays_on(self):
"""Turn off all relays"""
self.happy_relay.turn_on()
self.sad_relay.turn_on()
self.scared_relay.turn_on()
self.thinking_relay.turn_on()
self.group1_relay.turn_on()
self.group2_relay.turn_on()
self.group3_relay.turn_on()
self.group4_relay.turn_on()
def all_relays_off(self):
"""Turn off all relays"""
self.happy_relay.turn_off()
self.sad_relay.turn_off()
self.scared_relay.turn_off()
self.thinking_relay.turn_off()
self.group1_relay.turn_off()
self.group2_relay.turn_off()
self.group3_relay.turn_off()
self.group4_relay.turn_off()
####################
# define states
# default - turn off EL wire, swap LED groups every second
# emotions - turn off other LEDs, and turn on relevant EL wire
####################
def activate_emotion(self, emotion):
"""Activates the selected emotional state"""
print('Entering %s state...' % emotion)
self.all_relays_off()
if self.state == 'default':
while self.state == 'default':
self.swap_groups()
self.watch_and_wait(1)
else:
r = getattr(self, '%s_relay' % emotion)
r.turn_on()
self.watch_and_wait(5)
self.state = 'default'
print('Changing state to default.')
self.change_state()
ariel = Ariel()
ariel.test()
while True:
ariel.run()
pass

How to open context menu for an object that is empty, but has a size?

I am creating a GUI for a dependency graphing software... And am not able to figure out how to get a context menu to open for my lines.
What I want to do, right click on/near a MyLine widget and open a context menu... What is happening right clicks are not detected.
It is currently not detecting right clicks on the line widgets location to open a context menu (Purpose of this is to allow the user to delete/edit lines by right clicking on them).
What am I doing wrong here?
class MyLine(QWidget):
def __init__(self, destination: Node, source: Node, parent=None):
super().__init__(parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
self.destination = destination
self.source = source
self.setAutoFillBackground(True)
p = self.palette()
p.setColor(self.backgroundRole(), Qt.red)
self.setPalette(p)
def update_line_size(self):
origin = self.source.get_line_draw_pos(self.destination.pos())
destination = self.destination.get_line_draw_pos(self.source.pos())
leftcornerX = origin.x() if origin.x() < destination.x() else destination.x()
leftcornerY = origin.y() if origin.y() < destination.y() else destination.y()
sizeX = abs(origin.x() - destination.x())
sizeY = abs(origin.y() - destination.y())
self.setGeometry(leftcornerX, leftcornerY, sizeX, sizeY)
def showMenu(self, _):
menu = QMenu()
menu.addAction("Delete", self.remove)
menu.exec_(self.cursor().pos())
def draw(self, painter: QPainter):
origin = self.source.get_line_draw_pos(self.destination.pos())
destination = self.destination.get_line_draw_pos(self.source.pos())
painter.drawLine(origin, destination)
# DRAW ARROW HEAD
ARROW_SIZE = 10 # Might change
line_angle = calculate_line_angle(destination, origin)
draw_arrow_head(destination, painter, line_angle, ARROW_SIZE)
def remove(self):
self.parent().delete_line(self)
self.deleteLater()
Edit:
required types for reproducibility
class Node(QLabel):
def __init__(self, text: str, parent=None):
super().__init__(text, parent)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.showMenu)
def get_line_draw_pos(self, other_side: QPoint):
x = self.pos().x() if other_side.x() < self.pos().x() else (self.pos().x() + self.width())
y = self.pos().y() if other_side.y() < self.pos().y() else (self.pos().y() + self.height())
return QPoint(x, y)
def showMenu(self, _):
pass #purposefully left as a stub
def calculate_line_angle(destination: QPoint, origin: QPoint):
return math.atan2(destination.y() - origin.y(), destination.x() - origin.x())
def draw_arrow_head(destination: QPoint, painter: QPainter, line_angle: float, arrow_size: float = 10):
angle1 = math.radians(22.5) + line_angle
angle2 = math.radians(-22.5) + line_angle
arrow1 = QPoint( int(destination.x() - arrow_size * math.cos(angle1)), int(destination.y() - arrow_size * math.sin(angle1)))
arrow2 = QPoint( int(destination.x() - arrow_size * math.cos(angle2)), int(destination.y() - arrow_size * math.sin(angle2)))
painter.drawLine(destination, arrow1)
painter.drawLine(destination, arrow2)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setAcceptDrops(True) # add a drop rule
self.setMouseTracking(True)
self.track_origin = None
self.track_mouse = QPoint(0,0)
self.lines = []
def paintEvent(self, event):
painter = QPainter(self)
for line in self.lines:
line.draw(painter)
line.update_line_size()
def connectNodes(self, destination: Node, source: Node):
self.lines.append(MyLine(destination, source))
self.update()
def delete_line(self, line: MyLine):
self.lines.remove(line)
self.update()
app = QApplication([])
window = MainWindow()
window.setWindowTitle("Right Click to remove label")
window.setGeometry(100, 100, 400, 200)
window.move(60,15)
nodes = []
for index, node_name in enumerate(["hello.txt", "not_a_villain.txt", "nope.txt"]):
node = Node(node_name, window)
node.move(50 + index*100, 50 + (index%2) * 50)
nodes.append(node)
window.connectNodes(nodes[0], nodes[1])
window.connectNodes(nodes[0], nodes[2])
window.connectNodes(nodes[1], nodes[2])
window.show()
sys.exit(app.exec_())

PyQt5 - How to calculate corner points of a QGraphicsRectItem after rotation by its center point?

My problem is that I couldn't find the pixel values of each corner points of a HighwayItem (which is a QGraphicsRectItem) after rotation it by angle theta about the center point of it.
I used the Rotation Matrix which explained here and I also looked thisexplanation. But, I cannot find the true values.
Any help will be great. Thanks.
Here is MapViewer() class. A HighwayItem is created in this view.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import Qt, QPoint, QPointF, QRectF
from PyQt5.QtWidgets import QGraphicsScene, \
QGraphicsView, QGraphicsPixmapItem, \
from class_graphical_items import HighwayItem
class MapViewer(QGraphicsView):
def __init__(self, parent, ui):
super(MapViewer, self).__init__(parent)
self.ui = ui
# Attributes for highway
self.add_highway_control = False
self.current_highway = None
self.start = QPointF()
self.hw_counter = 0
self._scene = QGraphicsScene(self)
self._map = QGraphicsPixmapItem()
self._scene.addItem(self._map)
self.setScene(self._scene)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
def mousePressEvent(self, event):
if self._map.isUnderMouse():
if self.add_highway_control:
# Create a yellow highway
self.current_highway = HighwayItem(self._scene, self.ui)
self.hw_counter += 1
self.start = self.mapToScene(event.pos()).toPoint()
r = QRectF(self.start, self.start)
self.current_highway.setRect(r)
self._scene.addItem(self.current_highway)
# When adding HW, set drag mode NoDrag
self.setDragMode(QGraphicsView.NoDrag)
super(MapViewer, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.add_highway_control and self.current_highway is not None:
# When adding HW, set drag mode NoDrag
self.setDragMode(QGraphicsView.NoDrag)
r = QRectF(self.start, self.mapToScene(event.pos()).toPoint()).normalized()
self.current_highway.setRect(r)
super(MapViewer, self).mouseReleaseEvent(event)
def mouseReleaseEvent(self, event):
if self.add_highway_control:
if self.current_highway is not None:
# When finish the adding HW, set drag mode ScrollHandDrag
self.setDragMode(QGraphicsView.ScrollHandDrag)
self.update_item_dict(self.current_highway)
self.update_item_table(self.current_highway)
self.current_highway = None
self.add_highway_control = False
super(MapViewer, self).mouseReleaseEvent(event)
This is the HighwayItem class. It has some specs like color, opacity etc.
By doubleclicking on created HighwayItem, I'm activating a spinbox which was in a QTreeWidget in main window (ui).
By changing the spinbox value, the user can rotate the item.
class HighwayItem(QGraphicsRectItem):
def __init__(self, scene, ui):
QGraphicsRectItem.__init__(self)
self.scene = scene
self.ui = ui
self.setBrush(QtCore.Qt.yellow)
self.setOpacity(0.5)
self.setZValue(4.0)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.setAcceptHoverEvents(True)
# Here, I'm activating the spinbox by double clicking
# on HighwayItem. In spinbox, I'm entering the rotation angle
# of HighwayItem.
def mouseDoubleClickEvent(self, event):
selected_item = self.scene.selectedItems()
if selected_item:
for i in range(self.ui.treeWidget_objects.topLevelItemCount()):
toplevel_item = self.ui.treeWidget_objects.topLevelItem(i)
heading_item = toplevel_item.child(2)
spinbox = self.ui.treeWidget_objects.itemWidget(heading_item, 2)
if str(toplevel_item.text(2)) == str(selected_item[0]):
if 'HighwayItem' in str(selected_item[0]):
spinbox.setEnabled(True)
else:
spinbox.setEnabled(False)
This is the HWHeadingSpinBox() class which sets the rotation angle of HWItem. My problem starts here. In rotate_hw() method, I am transforming the created HighwayItem by its center point and giving it a rotation by its center point.
BUT, when I try to calculate new corners of hw in calc_rotated_coords() method, I'm messing up.
class HWHeadingSpinBox(QSpinBox):
def __init__(self, viewer, selected_hw):
QSpinBox.__init__(self)
self.selected_hw = selected_hw
self.viewer = viewer
# First coords of HW
tl = self.selected_hw.rect().topLeft()
tr = self.selected_hw.rect().topRight()
br = self.selected_hw.rect().bottomRight()
bl = self.selected_hw.rect().bottomLeft()
self.temp_list = [tl, tr, br, bl]
self.setRange(-180, 180)
self.setSuffix('°')
self.setEnabled(False)
self.valueChanged.connect(self.rotate_hw)
def heading_val(self):
return self.value()
def rotate_hw(self):
angle = self.heading_val()
self.selected_hw.prepareGeometryChange()
offset = self.selected_hw.boundingRect().center()
self.selected_hw.sceneBoundingRect().center()
transform = QTransform()
transform.translate(offset.x(), offset.y())
transform.rotate(-angle)
transform.translate(-offset.x(), -offset.y())
self.selected_hw.setTransform(transform)
# br_rect = self.selected_hw.sceneBoundingRect()
# sbr_rect = self.selected_hw.sceneBoundingRect()
# r_rect = self.selected_hw.sceneBoundingRect()
#
# rectitem = QtWidgets.QGraphicsRectItem(br_rect)
# rectitem.setBrush(Qt.red)
# self.viewer._scene.addItem(rectitem)
#
# rectitem = QtWidgets.QGraphicsRectItem(sbr_rect)
# rectitem.setBrush(Qt.green)
# self.viewer._scene.addItem(rectitem)
#
# rectitem = QtWidgets.QGraphicsRectItem(r_rect)
# rectitem.setBrush(Qt.blue)
# self.viewer._scene.addItem(rectitem)
def calc_rotated_coords(self):
# center point
cx = self.selected_hw.rect().center().x()
cy = self.selected_hw.rect().center().y()
# rotation angle
theta = math.radians(angle)
rotated_corners = []
for item in self.temp_list:
x = item.x()
y = item.y()
temp_x = x - cx
temp_y = y - cy
rot_x = temp_x * math.cos(theta) + temp_y * math.sin(theta)
rot_y = -temp_x * math.sin(theta) + temp_y * math.cos(theta)
rotated_corners.append([rot_x, rot_y])
self.temp_list = rotated_corners
print("\nPIXEL VALUES OF HW: \n{}".format(self.temp_list))
Here is the solution:
I added the itemChange(self, change, value) event in to HighwayItem and if change is ItemPositionHasChanged, I calculated all items' corners as such:
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionHasChanged:
top_left = self.mapToScene(self.rect().topLeft())
top_right = self.mapToScene(self.rect().topRight())
bottom_left = self.mapToScene(self.rect().bottomLeft())
bottom_right = self.mapToScene(self.rect().bottomRight())
changed_pos = [top_left, top_right, bottom_right, bottom_left]
return super(HighwayItem, self).itemChange(change, value)

Python multiprocessing how to update a complex object in a manager list without using .join() method

I started programming in Python about 2 months ago and I've been struggling with this problem in the last 2 weeks.
I know there are many similar threads to this one but I can't really find a solution which suits my case.
I need to have the main process which is the one which interacts with Telegram and another process, buffer, which understands the complex object received from the main and updates it.
I'd like to do this in a simpler and smoother way.
At the moment objects are not being updated due to the use of multi-processing without the join() method.
I tried then to use multi-threading instead but it gives me compatibility problems with Pyrogram a framework which i am using to interact with Telegram.
I wrote again the "complexity" of my project in order to reproduce the same error I am getting and in order to get and give the best help possible from and for everyone.
a.py
class A():
def __init__(self, length = -1, height = -1):
self.length = length
self.height = height
b.py
from a import A
class B(A):
def __init__(self, length = -1, height = -1, width = -1):
super().__init__(length = -1, height = -1)
self.length = length
self.height = height
self.width = width
def setHeight(self, value):
self.height = value
c.py
class C():
def __init__(self, a, x = 0, y = 0):
self.a = a
self.x = x
self.y = y
def func1(self):
if self.x < 7:
self.x = 7
d.py
from c import C
class D(C):
def __init__(self, a, x = 0, y = 0, z = 0):
super().__init__(a, x = 0, y = 0)
self.a = a
self.x = x
self.y = y
self.z = z
def func2(self):
self.func1()
main.py
from b import B
from d import D
from multiprocessing import Process, Manager
from buffer import buffer
if __name__ == "__main__":
manager = Manager()
lizt = manager.list()
buffer = Process(target = buffer, args = (lizt, )) #passing the list as a parameter
buffer.start()
#can't invoke buffer.join() here because I need the below code to keep running while the buffer process takes a few minutes to end an instance passed in the list
#hence I can't wait the join() function to update the objects inside the buffer but i need objects updated in order to pop them out from the list
import datetime as dt
t = dt.datetime.now()
#library of kind of multithreading (pool of 4 processes), uses asyncio lib
#this while was put to reproduce the same error I am getting
while True:
if t + dt.timedelta(seconds = 10) < dt.datetime.now():
lizt.append(D(B(5, 5, 5)))
t = dt.datetime.now()
"""
#This is the code which looks like the one in my project
#main.py
from pyrogram import Client #library of kind of multithreading (pool of 4 processes), uses asyncio lib
from b import B
from d import D
from multiprocessing import Process, Manager
from buffer import buffer
if __name__ == "__main__":
api_id = 1234567
api_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
app = Client("my_account", api_id, api_hash)
manager = Manager()
lizt = manager.list()
buffer = Process(target = buffer, args = (lizt, )) #passing the list as a parameter
buffer.start()
#can't invoke buffer.join() here because I need the below code to run at the same time as the buffer process
#hence I can't wait the join() function to update the objects inside the buffer
#app.on_message()
def my_handler(client, message):
lizt.append(complex_object_conatining_message)
"""
buffer.py
def buffer(buffer):
print("buffer was defined")
while True:
if len(buffer) > 0:
print(buffer[0].x) #prints 0
buffer[0].func2() #this changes the class attribute locally in the class instance but not in here
print(buffer[0].x) #prints 0, but I'd like it to be 7
print(buffer[0].a.height) #prints 5
buffer[0].a.setHeight(10) #and this has the same behaviour
print(buffer[0].a.height) #prints 5 but I'd like it to be 10
buffer.pop(0)
This is the whole code about the problem I am having.
Literally every suggestion is welcome, hopefully constructive, thank you in advance!
At last I had to change the way to solve this problem, which was using asyncio like the framework was doing as well.
This solution offers everything I was looking for:
-complex objects update
-avoiding the problems of multiprocessing (in particular with join())
It is also:
-lightweight: before I had 2 python processes 1) about 40K 2) about 75K
This actual process is about 30K (and it's also faster and cleaner)
Here's the solution, I hope it will be useful for someone else like it was for me:
The part of the classes is skipped because this solution updates complex objects absolutely fine
main.py
from pyrogram import Client
import asyncio
import time
def cancel_tasks():
#get all task in current loop
tasks = asyncio.Task.all_tasks()
for t in tasks:
t.cancel()
try:
buffer = []
firstWorker(buffer) #this one is the old buffer.py file and function
#the missing loop and loop method are explained in the next piece of code
except KeyboardInterrupt:
print("")
finally:
print("Closing Loop")
cancel_tasks()
firstWorker.py
import asyncio
def firstWorker(buffer):
print("First Worker Executed")
api_id = 1234567
api_hash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
app = Client("my_account", api_id, api_hash)
#app.on_message()
async def my_handler(client, message):
print("Message Arrived")
buffer.append(complex_object_conatining_message)
await asyncio.sleep(1)
app.run(secondWorker(buffer)) #here is the trick: I changed the
#method run() of the Client class
#inside the Pyrogram framework
#since it was a loop itself.
#In this way I added another task
#to the existing loop in orther to
#let run both of them together.
my secondWorker.py
import asyncio
async def secondWorker(buffer):
while True:
if len(buffer) > 0:
print(buffer.pop(0))
await asyncio.sleep(1)
The resources to understand the asyncio used in this code can be found here:
Asyncio simple tutorial
Python Asyncio Official Documentation
This tutorial about how to fix classical Asyncio errors

Trying to take pictures with Coral camera with Coral edgeTPU dev board but it is really slow

To start with, I am not a developer, but a mere automation engineer that have worked a bit with coding in Java, python, C#, C++ and C.
I am trying to make a prototype that take pictures and stores them using a digital pin on the board. Atm I can take pictures using a switch, but it is really slow(around 3 seconds pr image).
My complete system is going to be like this:
A product passes by on a conveyor and a photo cell triggers the board to take an image and store it. If an operator removes a product(because of bad quality) the image is stored in a different folder.
I started with the snapshot function shipped with Mendel and have tried to get rid off the overhead, but the Gstream and pipeline-stuff confuses me a lot.
If someone could help me with how to understand the supplied code, or how to write a minimalistic solution to take an image i would be grateful :)
I have tried to understand and use project-teachable and examples-camera from Google coral https://github.com/google-coral, but with no luck. I have had the best luck with the snapshot tool that uses snapshot.py that are referenced here https://coral.withgoogle.com/docs/camera/datasheet/#snapshot-tool
from periphery import GPIO
import time
import argparse
import contextlib
import fcntl
import os
import select
import sys
import termios
import threading
import gi
gi.require_version('Gst', '1.0')
gi.require_version('GstBase', '1.0')
from functools import partial
from gi.repository import GLib, GObject, Gst, GstBase
from PIL import Image
GObject.threads_init()
Gst.init(None)
WIDTH = 2592
HEIGHT = 1944
FILENAME_PREFIX = 'img'
FILENAME_SUFFIX = '.png'
AF_SYSFS_NODE = '/sys/module/ov5645_camera_mipi_v2/parameters/ov5645_af'
CAMERA_INIT_QUERY_SYSFS_NODE = '/sys/module/ov5645_camera_mipi_v2/parameters/ov5645_initialized'
HDMI_SYSFS_NODE = '/sys/class/drm/card0/card0-HDMI-A-1/status'
# No of initial frames to throw away before camera has stabilized
SCRAP_FRAMES = 1
SRC_WIDTH = 2592
SRC_HEIGHT = 1944
SRC_RATE = '15/1'
SRC_ELEMENT = 'v4l2src'
SINK_WIDTH = 2592
SINK_HEIGHT = 1944
SINK_ELEMENT = ('appsink name=appsink sync=false emit-signals=true '
'max-buffers=1 drop=true')
SCREEN_SINK = 'glimagesink sync=false'
FAKE_SINK = 'fakesink sync=false'
SRC_CAPS = 'video/x-raw,format=YUY2,width={width},height={height},framerate={rate}'
SINK_CAPS = 'video/x-raw,format=RGB,width={width},height={height}'
LEAKY_Q = 'queue max-size-buffers=1 leaky=downstream'
PIPELINE = '''
{src_element} ! {src_caps} ! {leaky_q} ! tee name=t
t. ! {leaky_q} ! {screen_sink}
t. ! {leaky_q} ! videoconvert ! {sink_caps} ! {sink_element}
'''
def on_bus_message(bus, message, loop):
t = message.type
if t == Gst.MessageType.EOS:
loop.quit()
elif t == Gst.MessageType.WARNING:
err, debug = message.parse_warning()
sys.stderr.write('Warning: %s: %s\n' % (err, debug))
elif t == Gst.MessageType.ERROR:
err, debug = message.parse_error()
sys.stderr.write('Error: %s: %s\n' % (err, debug))
loop.quit()
return True
def on_new_sample(sink, snapinfo):
if not snapinfo.save_frame():
# Throw away the frame
return Gst.FlowReturn.OK
sample = sink.emit('pull-sample')
buf = sample.get_buffer()
result, mapinfo = buf.map(Gst.MapFlags.READ)
if result:
imgfile = snapinfo.get_filename()
caps = sample.get_caps()
width = WIDTH
height = HEIGHT
img = Image.frombytes('RGB', (width, height), mapinfo.data, 'raw')
img.save(imgfile)
img.close()
buf.unmap(mapinfo)
return Gst.FlowReturn.OK
def run_pipeline(snapinfo):
src_caps = SRC_CAPS.format(width=SRC_WIDTH, height=SRC_HEIGHT, rate=SRC_RATE)
sink_caps = SINK_CAPS.format(width=SINK_WIDTH, height=SINK_HEIGHT)
screen_sink = FAKE_SINK
pipeline = PIPELINE.format(
leaky_q=LEAKY_Q,
src_element=SRC_ELEMENT,
src_caps=src_caps,
sink_caps=sink_caps,
sink_element=SINK_ELEMENT,
screen_sink=screen_sink)
pipeline = Gst.parse_launch(pipeline)
appsink = pipeline.get_by_name('appsink')
appsink.connect('new-sample', partial(on_new_sample, snapinfo=snapinfo))
loop = GObject.MainLoop()
# Set up a pipeline bus watch to catch errors.
bus = pipeline.get_bus()
bus.add_signal_watch()
bus.connect('message', on_bus_message, loop)
# Connect the loop to the snaphelper
snapinfo.connect_loop(loop)
# Run pipeline.
pipeline.set_state(Gst.State.PLAYING)
try:
loop.run()
except:
pass
# Clean up.
pipeline.set_state(Gst.State.NULL)
while GLib.MainContext.default().iteration(False):
pass
class SnapHelper:
def __init__(self, sysfs, prefix='img', oneshot=True, suffix='jpg'):
self.prefix = prefix
self.oneshot = oneshot
self.suffix = suffix
self.snap_it = oneshot
self.num = 0
self.scrapframes = SCRAP_FRAMES
self.sysfs = sysfs
def get_filename(self):
while True:
filename = self.prefix + str(self.num).zfill(4) + '.' + self.suffix
self.num = self.num + 1
if not os.path.exists(filename):
break
return filename
#def check_af(self):
#try:
# self.sysfs.seek(0)
# v = self.sysfs.read()
# if int(v) != 0x10:
# print('NO Focus')
#except:
# pass
# def refocus(self):
# try:#
# self.sysfs.write('1')
# self.sysfs.flush()
# except:
# pass
def save_frame(self):
# We always want to throw away the initial frames to let the
# camera stabilize. This seemed empirically to be the right number
# when running on desktop.
if self.scrapframes > 0:
self.scrapframes = self.scrapframes - 1
return False
if self.snap_it:
self.snap_it = False
retval = True
else:
retval = False
if self.oneshot:
self.loop.quit()
return retval
def connect_loop(self, loop):
self.loop = loop
def take_picture(snap):
start_time = int(round(time.time()))
run_pipeline(snap)
print(time.time()- start_time)
def main():
button = GPIO(138, "in")
last_state = False
with open(AF_SYSFS_NODE, 'w+') as sysfs:
snap = SnapHelper(sysfs, 'test', 'oneshot', 'jpg')
sysfs.write('2')
while 1:
button_state = button.read()
if(button_state==True and last_state == False):
snap = SnapHelper(sysfs, 'test', 'oneshot', 'jpg')
take_picture(snap)
last_state = button_state
if __name__== "__main__":
main()
sys.exit()
Output is what i expect, but it is slow.
I switched to a USB-webcam and used the pygame library instead.