class FigureCanvas(object): """ FigureCanvas class; wrapper around MPL's FigureCanvasTkAgg """ def __init__(self, figure, master=None): # Create the MPL canvas self.mpl_canvas = FigureCanvasTkAgg(figure, master=master) # Immediately remove highlightthickness and borders so the canvas' winfo_reqwidth() and # winfo_reqheight() are equivalent to get_dimensions(). Note that removing this line will subtly # break this class, because you can no longer rely on set_dimensions() to set the figure and # canvas sizes to be equivalent self.tk_widget.config(takefocus=False, bd=0, highlightthickness=0) # Immediately unbind <Configure>. On resize, MPL automatically redraws figure, which is # undesired because we want to manually control size, because it draws figure even if it had # never been drawn and was not yet intended to be drawn, and because it introduces extreme lag to # scrolling of large plots and possibly images self.tk_widget.unbind('<Configure>') # Freeze FigureCanvas by default until it is packed self.frozen = True # Contains FigureCanvas size in pixels if it has been resized while frozen; otherwise is set to None self._thawed_fig_size = None @property def mpl_figure(self): return self.mpl_canvas.figure @property def tk_widget(self): return self.mpl_canvas.get_tk_widget() @property def dpi(self): return self.mpl_figure.dpi # Update FigureCanvas to reflect any visual changes in the figure (e.g. updated image, plot, etc) def draw(self): if not self.frozen: # If FigureCanvas was resized while frozen, then we apply the resizing and redraw if self._thawed_fig_size is not None: self._resize_and_draw() # If FigureCanvas has not been resized while frozen then we just draw else: self.mpl_canvas.draw() # Pack and unfreeze the FigureCanvas def pack(self): self.tk_widget.pack() self.frozen = False # Clear the FigureCanvas def clear(self): # For some reason running clear() does not clear all memory used to draw the figure. # This can be seen by using only clear() and then opening/closing a large image; # the memory usage will be larger after closure than it was prior to opening. # Setting figure size to be 1x1 pixels, prior to running clear(), seems to negate # this issue (it is likely the leak still occurs but is much much smaller). # Since clear() is not intended to change the figure size, we restore it back after clearing. # Unfortunately this process does take a bit of time for large images. original_dimensions = self.get_dimensions(type='pixels') # Suppress MPL 2+ user warning on too small margins for plots, since this is intended with warnings.catch_warnings(): warnings.simplefilter('ignore', category=UserWarning) self.set_dimensions((1, 1), type='pixels', force=True) self.mpl_figure.clear() self.set_dimensions(original_dimensions, type='pixels', force=True) # Destroys the FigureCanvas. Must be called once this object is no longer being used to prevent a # memory leak def destroy(self): # For some reason running clear() does not clear all memory. This can be seen by # using only clear() and then opening/closing a large image; the memory usage # will be larger after closure than it was prior to opening. Setting figure size # to be 1x1 pixels, prior to running clear(), seems to negate this issue (it is # likely the leak still occurs but is much much smaller). # Suppress MPL 2+ user warning on too small margins for plots, since this is intended with warnings.catch_warnings(): warnings.simplefilter('ignore', category=UserWarning) self.set_dimensions((1, 1), type='pixels', force=True) self.mpl_figure.clear() self.tk_widget.destroy() # While frozen, the displayed figure image is not updated for changes even if requested def freeze(self): self.frozen = True # Thaw after being frozen, and by default also redraw to update figure image for changes made while frozen def thaw(self, redraw=True): self.frozen = False if redraw: self.draw() # Returns the FigureCanvas dimensions (MPL figure and its containing TK canvas have the same dimensions) # in units of type (pixels or inches) def get_dimensions(self, type='pixels'): # While frozen, we report the size the FigureCanvas will take on once redrawn if self._thawed_fig_size is not None: fig_size = self._thawed_fig_size # Get fig_size from pixels fig_size = self._fig_size_convert(fig_size, input_type='pixels', output_type=type) # Report the true FigureCanvas size else: fig_size = self.mpl_figure.get_size_inches() # Get fig_size from inches fig_size = self._fig_size_convert(fig_size, input_type='inches', output_type=type) return fig_size # Sets the FigureCanvas dimensions (MPL figure and its containing TK canvas have the same dimensions) # in units of type (pixels or inches). By default the FigureCanvas is resized immediately only when the # FigureCanvas is not frozen because on resize it will also be redrawn; `force` allows for immediate # resizing even when frozen. def set_dimensions(self, fig_size, type='pixels', force=False): # Get fig_size in pixels fig_size = self._fig_size_convert(fig_size, input_type=type, output_type='pixels') # Skip adjusting dimensions if they are not changing because this can be an expensive # operation for large images if fig_size == self.get_dimensions(type='pixels'): return # Set figure canvas size and resize, if the window is not frozen or immediate resize is being forced # (see `_resize_and_draw` for more info as to why a redraw command is implicit in the resize call) self._thawed_fig_size = fig_size if force or (not self.frozen): self._resize_and_draw() # Resize and redraw the FigureCanvas. The contents of this method could actually be part of # `set_dimensions`, however that would redraw the image immediately on resize every time, regardless of # whether the FigureCanvas is frozen (due to MPL's FigureCanvasTkAgg.resize calling draw). Therefore this # method is used to delay resizing when the image is frozen until it is thawed. While overloading MPL's # resize call is possible to remove its draw call, MPL resizing works by deleting the previous image and # totally redrawing it (especially for plots), therefore doing this would make the image vanish until # its drawn again; i.e. redrawing is an implicit part of resizing. def _resize_and_draw(self): class ResizeEvent(object): def __init__(self, width, height): self.width = width self.height = height fig_size = self._thawed_fig_size # Set containing TK canvas size self.tk_widget.config(width=fig_size[0], height=fig_size[1]) # Set MPL Figure size, which also redraws the figure self.mpl_canvas.resize(ResizeEvent(fig_size[0], fig_size[1])) # Reset thawed size to none (otherwise this will keep firing even when no resize needed) self._thawed_fig_size = None # Convert figure size from input type to output type. Valid options for both *input_type* and # *output_type* are pixels|inches. def _fig_size_convert(self, fig_size, input_type, output_type): # Ensure types are is valid types = ('pixels', 'inches') if (input_type not in types) or (output_type not in types): error_type = input_type if (input_type not in types) else output_type raise ValueError("Unknown type, '{0}'. Expected: {1}.".format( error_type, ' or '.join(types))) # Pixels -> Pixels if input_type == 'pixels' and output_type == 'pixels': fig_size = np.round(fig_size).astype('int') # Pixels -> Inches elif input_type == 'pixels' and output_type == 'inches': fig_size = np.asanyarray(fig_size) / self.dpi # Inches -> Inches elif input_type == 'inches' and output_type == 'inches': fig_size = fig_size # Inches -> Pixels elif input_type == 'inches' and output_type == 'pixels': fig_size = np.round(np.asanyarray(fig_size) * self.dpi).astype('int') return tuple(fig_size)
def resize(self, event): FigureCanvasTkAgg.resize(self, event) self._fig.tight_layout()
class DiceviewApp: class States(Enum): SAMPLE = 0 SAMPLE_WAIT = 1 SAMPLE_READY = 2 PADDING = 5 ROWMIN = 400 COLMIN = 800 FACES = 20 def __init__(self): self.root = tkinter.Tk() self.root.title("diceview") try: self.root.state("zoomed") except (tkinter.TclError): pass m = self.root.maxsize() self.root.geometry('{}x{}+0+0'.format(*m)) self.root.focus_set() self.root.configure(bg="#ddd", padx=self.PADDING, pady=self.PADDING) self.root.rowconfigure(0, minsize=self.ROWMIN, weight=1) self.root.rowconfigure(1, minsize=self.ROWMIN, weight=1) self.root.columnconfigure(0, minsize=self.COLMIN, weight=1) self.root.columnconfigure(1, minsize=self.COLMIN, weight=1) self.root.minsize(self.PADDING * 6 + self.COLMIN * 2, self.PADDING * 6 + self.ROWMIN * 2) self.graph_bar = tkinter.Frame(self.root) self.graph_chi = tkinter.Frame(self.root, bg='#000') self.graph_bar.grid(row=0, column=0, sticky=W + E + N + S, padx=self.PADDING, pady=self.PADDING) self.graph_chi.grid(row=0, column=1, sticky=W + E + N + S, padx=self.PADDING, pady=self.PADDING) self.graph_bar.pack_propagate(False) self.graph_chi.pack_propagate(False) self._nullfig = Figure() self.canvas_bar = FigureCanvasTkAgg(self._nullfig, master=self.graph_bar) self.canvas_chi = FigureCanvasTkAgg(self._nullfig, master=self.graph_chi) self.canvas_bar.get_tk_widget().pack(fill="both", expand=True) self.canvas_chi.get_tk_widget().pack(fill="both", expand=True) self.imframe = tkinter.Frame(self.root, bg="#eee") self.imframe.grid(row=1, column=1, sticky=W + E + N + S, padx=self.PADDING, pady=self.PADDING) self.imframe.pack_propagate(False) self.tkimage = tkinter.Label(self.imframe, bd=0, bg='#eee') self.tkimage.pack(fill="both", expand=True) self.statframe = tkinter.Frame(self.root, bg="#eee") self.statframe.grid(row=1, column=0, sticky=W + E + N + S, padx=self.PADDING, pady=self.PADDING) self.statframe.rowconfigure(0, weight=1) self.statframe.rowconfigure(1, weight=1) self.statframe.columnconfigure(0, weight=1) self.statframe.columnconfigure(1, weight=1) self.lilfont = font.Font(family='Trebuchet MS', size=25, weight='normal') self.bigfont = font.Font(family='Trebuchet MS', size=75, weight='bold') positions = { "Actuations": (0, 0), "Dice Rolls": (0, 1), "Average Roll": (1, 0), "Chi-Squared": (1, 1) } self.sf_subs = {} self.sf_labels = {} self.sf_vars = {} for pos in positions: self.sf_subs[pos] = tkinter.Frame(self.statframe, bg='#eee') self.sf_subs[pos].grid(row=positions[pos][0], column=positions[pos][1], sticky=W + E + N + S, padx=30, pady=30) self.sf_subs[pos].rowconfigure(0, weight=1) self.sf_subs[pos].rowconfigure(1, weight=5) self.sf_subs[pos].columnconfigure(0, weight=1) self.sf_subs[pos].grid_propagate(False) self.sf_labels[pos] = tkinter.Label(self.sf_subs[pos], text=pos, font=self.lilfont, bg='#eee') self.sf_labels[pos].grid(row=0, column=0, sticky=W + E) self.sf_labels[pos].grid_propagate(False) self.sf_vars[pos] = tkinter.Label(self.sf_subs[pos], text="0", font=self.bigfont, bg='#eee') self.sf_vars[pos].grid(row=1, column=0, sticky=W + E) self.sf_vars[pos].grid_propagate(False) self.actuations = 0 self.chi_history = [] self.die = Die("D20", 20) self.vision = VisionThread() self.vision.start() self.state = self.States.SAMPLE self.root.after(1, self.tick) def tick(self): if self.state == self.States.SAMPLE: self.vision.sample() self.state = self.States.SAMPLE_WAIT if self.state == self.States.SAMPLE_WAIT: with self.vision.reslock: if self.vision.fresh: self.state = self.States.SAMPLE_READY if self.state == self.States.SAMPLE_READY: dice, frame = self.vision.wait_results() self.show_frame(frame) self.update_stats(dice) self.state = self.States.SAMPLE self.root.after(10, self.tick) def show_frame(self, frame): frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) size = clamp_aspect(16.0 / 9.0, self.tkimage.winfo_width(), self.tkimage.winfo_height()) frame = cv2.resize(frame, size) frame = Image.fromarray(frame) frame = ImageTk.PhotoImage(frame) self.tkimage.configure(image=frame) self.tkimage.image = frame def update_stats(self, dice): self.actuations += 1 for roll in dice: self.die.add_roll(roll) self.sf_vars["Actuations"].configure( text="{:d}".format(self.actuations)) if self.die.rolls() < 1: return self.chi_history.append(self.die.chi_squared()) self.sf_vars["Dice Rolls"].configure( text="{:d}".format(self.die.rolls())) self.sf_vars["Average Roll"].configure( text="{:2.2f}".format(self.die.average())) self.sf_vars["Chi-Squared"].configure( text="{:2.2f}".format(self.chi_history[-1])) class FakeEvent: def __init__(self, width, height): self.width = width self.height = height self.canvas_chi.figure = graphs.chi_graph(self.chi_history) self.canvas_chi.resize( FakeEvent(self.graph_chi.winfo_width(), self.graph_chi.winfo_height())) self.canvas_bar.figure = graphs.count_graph(self.die) self.canvas_bar.resize( FakeEvent(self.graph_bar.winfo_width(), self.graph_bar.winfo_height())) def shutdown(self): self.root.destroy() self.vision.stop() self.vision.join(timeout=10) exit(0) def run(self): self.root.protocol("WM_DELETE_WINDOW", self.shutdown) self.root.mainloop()