Example #1
0
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)
Example #2
0
 def resize(self, event):
     FigureCanvasTkAgg.resize(self, event)
     self._fig.tight_layout()
Example #3
0
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()