Update matplotlib plot with Gtk+ button event

2019-09-14 18:22发布

问题:

I've encapsulated a matplotlib plot in a Gtk+ window and I'm trying to update that plot when a button is clicked (it's Gauss' circle problem). Trouble is, I'm not exactly sure how to get the plot to update with an event. So far I have the following.

#! /usr/bin/env python3.4
# -*- coding: utf-8 -*-

""" Main application--embed Matplotlib figure in window with UI """


import gi
gi.require_version('Gtk', '3.0')

import numpy as np
from gi.repository import Gtk, GObject
from matplotlib.figure import Figure

# make sure cairocffi is installed, pycairo doesn't support FigureCanvasGTK3Agg
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg \
    as FigureCanvas

from matplotlib.patches import Ellipse
from typing import List, Tuple
from math import sqrt


class Main(Gtk.Window):
    """ Main window UI """
    SIGMA = 10

    def __init__(self):
        Gtk.Window.__init__(self, title='Gauss\' Circle Problem')
        self.connect('destroy', lambda _: Gtk.main_quit())
        self.set_border_width(10)
        self.set_default_size(600, 450)

        # Set up the l/r box layout
        self.box = Gtk.Box(spacing=10)
        self.add(self.box)

        # Set up the right column
        self.rcolumn = Gtk.Grid()
        self.box.pack_end(self.rcolumn, False, False, 1)

        # Set up spin button
        adjustment = Gtk.Adjustment(10, 3, 100, 1, 0, 0)
        self.spinbutton = Gtk.SpinButton()
        self.spinbutton.set_adjustment(adjustment)
        self.rcolumn.attach(self.spinbutton, 0, 0, 1, 1)

        # Set up update button
        self.update_plot_button = Gtk.Button(label='Update')
        self.update_plot_button.connect('clicked', self.update_sigma_event)
        self.rcolumn.attach_next_to(self.update_plot_button, 
            self.spinbutton, Gtk.PackDirection.BTT, 1, 1)

        self._add_plot()

    def update_sigma_event(self, button) -> None:
        """ Update sigma and replot """
        self.SIGMA = self.spinbutton.get_value()
        self._add_plot()

    def _add_plot(self) -> None:
        """ Add the plot to the window """
        fig = Figure(figsize=(5, 4))
        ax = fig.add_subplot(111, aspect='equal')

        arr = np.zeros([self.SIGMA * 2 + 1] * 2)

        points = self.collect(int(self.SIGMA), int(self.SIGMA), self.SIGMA)

        # flip pixel value if it lies inside (or on) the circle
        for p in points:
            arr[p] = 1

        # plot ellipse on top of boxes to show their centroids lie inside
        circ = Ellipse(\
            xy=(int(self.SIGMA), int(self.SIGMA)), 
            width=2 * self.SIGMA,
            height=2 * self.SIGMA,
            angle=0.0
        )

        ax.add_artist(circ)
        circ.set_clip_box(ax.bbox)
        circ.set_alpha(0.2)
        circ.set_facecolor((1, 1, 1))
        ax.set_xlim(-0.5, 2 * self.SIGMA + 0.5)
        ax.set_ylim(-0.5, 2 * self.SIGMA + 0.5)

        # Plot the pixel centers
        ax.scatter(*zip(*points), marker='.', color='white')

        # now plot the array that's been created
        ax.imshow(-arr, interpolation='none', cmap='gray')

        # add it to the window
        canvas = FigureCanvas(fig)
        self.box.pack_start(canvas, True, True, 0)


    @staticmethod
    def collect(x: int, y: int, sigma: float =3.0) -> List[Tuple[int, int]]:
        """ create a small collection of points in a neighborhood of some 
        point 
        """
        neighborhood = []

        X = int(sigma)
        for i in range(-X, X + 1):
            Y = int(pow(sigma * sigma - i * i, 1/2))
            for j in range(-Y, Y + 1):
                neighborhood.append((x + i, y + j))

        return neighborhood


if __name__ == '__main__':
    window = Main()
    window.show_all()
    Gtk.main()

I'm not exactly sure where to go from here, I just know that updating the SpinButton indeed adjusts self.SIGMA, but I don't know how to tell matplotlib to update the plot in the window.

Also, this is what it looks like currently if you aren't able to run it (I'm also trying to vertically center the two button widgets in the right column :P):

回答1:

This is a solution I've found to my problem:

#! /usr/bin/env python3.4
# -*- coding: utf-8 -*-

""" Main application--embed Matplotlib figure in window with UI """

import gi
gi.require_version('Gtk', '3.0')

import numpy as np
from gi.repository import Gtk, GObject
from matplotlib.figure import Figure

# make sure cairocffi is installed, pycairo doesn't support FigureCanvasGTK3Agg
from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg \
    as FigureCanvas

from matplotlib.patches import Ellipse
from typing import List, Tuple, Union
from math import sqrt


class Main(Gtk.Window):
    """ Main window UI """
    SIGMA = 10
    INVERT = -1

    def __init__(self) -> None:
        Gtk.Window.__init__(self, title='Gauss\' Circle Problem')
        self.connect('destroy', lambda _: Gtk.main_quit())
        self.set_border_width(10)
        self.set_default_size(650, 500)

        # Set up the l/r box layout
        self.box = Gtk.Box(spacing=10)
        self.add(self.box)

        # Set up the right column
        self.rcolumn = Gtk.VBox(spacing=0)
        self.rcolumn.set_spacing(10)
        self.box.pack_end(self.rcolumn, False, False, 20)

        # Set up spin button
        adjustment = Gtk.Adjustment(self.SIGMA, 1, 30, 1, 0, 0)
        self.spinbutton = Gtk.SpinButton()
        self.spinbutton.set_adjustment(adjustment)
        self.rcolumn.pack_start(self.spinbutton, False, False, 0)

        # Set up invert checkbox
        self.invertbutton = Gtk.CheckButton('Invert')
        self.invertbutton.set_active(True)
        self.invertbutton.connect('toggled', self.switch_toggle_parity, 'invert')
        self.rcolumn.add(self.invertbutton)

        # Set up update button
        self.update_plot_button = Gtk.Button(label='Update')
        self.update_plot_button.connect('clicked', self.update_sigma_event)
        self.rcolumn.add(self.update_plot_button)

        self.initial_plot()

    def calculate(self) -> None:
        """ Re-calculate using the formula """
        arr = np.zeros([self.SIGMA * 2 + 1] * 2)

        points = self.collect(int(self.SIGMA), int(self.SIGMA), self.SIGMA)

        # flip pixel value if it lies inside (or on) the circle
        for p in points:
            arr[p] = 1

        # plot ellipse on top of boxes to show their centroids lie inside
        circ = Ellipse(
            xy=(int(self.SIGMA), int(self.SIGMA)),
            width=2 * self.SIGMA,
            height=2 * self.SIGMA,
            angle=0.0
        )

        self.ax.clear()
        self.ax.add_artist(circ)
        circ.set_clip_box(self.ax.bbox)
        circ.set_alpha(0.2)
        circ.set_facecolor((1, 1, 1))
        self.ax.set_xlim(-0.5, 2 * self.SIGMA + 0.5)
        self.ax.set_ylim(-0.5, 2 * self.SIGMA + 0.5)

        # Plot the pixel centers
        self.ax.scatter(*zip(*points), marker='.',
            color='white' if self.INVERT == -1 else 'black')

        # now plot the array that's been created
        self.ax.imshow(self.INVERT * arr, interpolation='none', cmap='gray')

    def initial_plot(self) -> None:
        """ Set up the initial plot; only called once """
        self.fig = Figure(figsize=(5, 4))
        self.canvas = FigureCanvas(self.fig)
        self.box.pack_start(self.canvas, True, True, 0)
        self.ax = self.fig.add_subplot(111, aspect='equal')
        self.calculate()
        self.draw_plot()

    def update_sigma_event(self, button: Union[Gtk.Button, None] =None) -> None:
        """ Update sigma and trigger a replot """
        self.SIGMA = int(self.spinbutton.get_value())
        self.calculate()
        self.draw_plot()

    def switch_toggle_parity(self, button: Union[Gtk.CheckButton, None] =None,
            name: str ='') -> None:
        """ Switch the parity of the plot before update """
        self.INVERT *= -1

    def draw_plot(self) -> None:
        """ Draw or update the current plot """
        self.fig.canvas.draw()

    @staticmethod
    def collect(x: int, y: int, sigma: float =3.0) -> List[Tuple[int, int]]:
        """ create a small collection of points in a neighborhood of some 
        point 
        """
        neighborhood = []

        X = int(sigma)
        for i in range(-X, X + 1):
            Y = int(pow(sigma * sigma - i * i, 1/2))
            for j in range(-Y, Y + 1):
                neighborhood.append((x + i, y + j))

        return neighborhood


if __name__ == '__main__':
    window = Main()
    window.show_all()
    Gtk.main()

I've also added a button that swaps the parity of the binary image plot and re-structured the method calls.

It's a slow/simple start, but I suppose we all have to start somewhere! Comments and suggestions welcome.



回答2:

Might be not entirely adequate to what you're doing, but there's a similarly simple yet faster algorithm for Gauss circle problem (with some Java source code and an ugly but handy illustration): https://stackoverflow.com/a/42373448/5298879

It's around 3.4x faster than counting points in one of the quarters, plus the center, plus the ones on the axis, that you're doing now, while taking just one more line of code.

You simply imagine an inscribed square and count only one-eighth of what's outside that square inside that circle.