Source code for magmap.gui.verifier_editor

# Viewer for verifying blobs
"""Blob verifier viewer GUI."""

import dataclasses
from typing import Optional, Dict, Sequence, Any, Callable

import numpy as np

from matplotlib import pyplot as plt
from matplotlib import figure
from matplotlib import gridspec
from matplotlib.widgets import Button, Slider, TextBox

from magmap.cv import detector
from magmap.gui import plot_editor
from magmap.io import libmag
from magmap.plot import plot_3d, plot_support
from magmap.settings import config


[docs] class VerifierEditor(plot_support.ImageSyncMixin): """Editor to verify blobs."""
[docs] @dataclasses.dataclass class BlobView: """Storage class for each view.""" #: Plot Editor. plot_ed: "plot_editor.PlotEditor" #: Displayed blob. blob: np.ndarray
def __init__(self, img5d, blobs, title=None, fig=None, fn_update_blob=None): """Initialize the viewer.""" super().__init__(img5d) self.blobs: "detector.Blobs" = blobs self.title: Optional[str] = title self.fig: Optional[figure.Figure] = fig #: Handler for updating a blob; defaults to None. self.fn_update_blob: Optional[Callable[ [np.ndarray, Optional[np.ndarray]], np.ndarray]] = fn_update_blob # GUI elements self._grid_spec = None self._row_slider = None self._col_slider = None self._back_btn = None self._next_btn = None self._page_txt = None # blob views grid layout self._nrows: int = 3 self._ncols: int = 3 #: Available blob flags, sorted in ascending order. self._blob_flags: Sequence[Any] = [] #: Dictionary of sub-plot index to data object for the plot. self._blob_views: Dict[int, "VerifierEditor.BlobView"] = {} #: Mask of blobs to show. self._blobs_show: Sequence = [] #: Starting index of blob to show in current page. self._blob_offset: int = 0
[docs] def show_fig(self): """Set up the figure.""" # set up the figure and main layout if self.fig is None: self.fig = figure.Figure(self.title) self.fig.clear() self._grid_spec = gridspec.GridSpec( 2, 1, wspace=0.1, hspace=0.1, height_ratios=(20, 1), figure=self.fig, left=0.06, right=0.94, bottom=0.02, top=0.96) # add controls with sub-grid-spec that includes gaps to accommodate # labels, which otherwise overlap other controls gs_controls = gridspec.GridSpecFromSubplotSpec( 1, 8, subplot_spec=self._grid_spec[1, 0], wspace=0.1, width_ratios=(30, 5, 30, 3, 11, 11, 3, 7)) self._row_slider = Slider( self.fig.add_subplot(gs_controls[0, 0]), "Rows", 0, 10, valinit=self._nrows, valstep=1, valfmt="%d") self._col_slider = Slider( self.fig.add_subplot(gs_controls[0, 2]), "Cols", 0, 10, valinit=self._ncols, valstep=1, valfmt="%d") self._back_btn = Button( self.fig.add_subplot(gs_controls[0, 4]), "Back") self._next_btn = Button( self.fig.add_subplot(gs_controls[0, 5]), "Next") self._page_txt = TextBox( self.fig.add_subplot(gs_controls[0, 7]), "Page", "1", label_pad=0.05) for btn in (self._back_btn, self._next_btn, self._page_txt): # enable button and color theme self.enable_btn(btn) # set up and display blobs self._setup_blobs() self.show_views() # attach listeners self._listeners.append(self.fig.canvas.mpl_connect( "button_press_event", self._on_mouse_press)) self._listeners.append(self.fig.canvas.mpl_connect( "close_event", self.on_close)) # attach handlers self._row_slider.on_changed(self._change_row) self._col_slider.on_changed(self._change_col) self._back_btn.on_clicked(self._back_page) self._next_btn.on_clicked(self._next_page) self._page_txt.on_submit(self._select_page) plt.ion() # avoid the need for draw calls self.fig.canvas.draw_idle()
def _setup_blobs(self): """Set up blobs.""" blobs = self.blobs.blobs if blobs is None: # reset blobs setup self._blobs_show = [] self._blob_flags = [] else: # get blobs with confirmation flags set by user (ie non-neg) self._blobs_show = self.blobs.get_blob_confirmed(blobs) >= 0 self._blob_flags = sorted(np.unique( self.blobs.get_blob_confirmed( blobs[self._blobs_show]).astype(int)))
[docs] def show_views(self): """Show blob views.""" # clear all prior axes for view in self._blob_views.values(): view.plot_ed.axes.clear() # set up grid spect in main view area gs_viewers = gridspec.GridSpecFromSubplotSpec( self._nrows, self._ncols, subplot_spec=self._grid_spec[0, 0]) # get indices of these blobs to access by view rather than copy blobs_inds = np.argwhere(self._blobs_show) nblobs = len(blobs_inds) subimg_shape = (50, 50) for row in range(self._nrows): for col in range(self._ncols): n = row * self._ncols + col + self._blob_offset if n >= nblobs: break # add axes ax = self.fig.add_subplot(gs_viewers[row, col]) plot_support.hide_axes(ax) aspect, origin = plot_support.get_aspect_ratio(config.PLANE[0]) # display plot editor centered on blob overlayer = plot_support.ImageOverlayer( ax, aspect, origin, rgb=config.rgb, additive_blend=self.additive_blend) plot_ed = plot_editor.PlotEditor( overlayer, self.img5d.img[0], None, None) # get blob as view and use absolute coordinates as ROI offset blob = self.blobs.blobs[blobs_inds[n][0]] offset = self.blobs.get_blob_abs_coords(blob).astype(int) plot_ed.coord = offset plot_ed.show_overview() # center plot on offset offset_ctr = plot_3d.roi_center_to_offset( offset[1:], subimg_shape) plot_ed.view_subimg(offset_ctr, subimg_shape) self.plot_eds[n] = plot_ed # store plot and blob blob_view = self.BlobView(plot_ed, blob) self._set_ax_title(blob_view) self._blob_views[n] = blob_view
def _on_mouse_press(self, evt): """Respond to mouse button press events.""" for key, view in self._blob_views.items(): # ignore presses outside the given plot if evt.inaxes != view.plot_ed.axes: continue # get the index of the current blob confirmed flag and increment i = np.argwhere(self._blob_flags == self.blobs.get_blob_confirmed( view.blob))[0][0] + 1 if i >= len(self._blob_flags): # reset if the index exceeds the flag list i = 0 # update the blob's flag and show in axes title, which will update # the underlying blobs array since this blob is a view, not a copy self.blobs.set_blob_confirmed(view.blob, self._blob_flags[i]) self._set_ax_title(view) if self.fn_update_blob: # handle any blob changes with the same updated blob as both # new and old blobs since blobs array has already been updated self.fn_update_blob(view.blob, view.blob) def _set_ax_title(self, view: "BlobView"): """Set the axes title for a blob view. Args: view: Blob view. """ # show the blob's confirmed flag in the title view.plot_ed.axes.set_title( f"Class: {self.blobs.get_blob_confirmed(view.blob).astype(int)}") def _change_row(self, evt): """Handle change to the number of rows.""" self._nrows = int(evt) self.show_views() def _change_col(self, evt): """Handle change to the number of cols.""" self._ncols = int(evt) self.show_views() def _back_page(self, evt): """Scroll back one page of views.""" if self._blob_offset == 0: return self._blob_offset -= self._nrows * self._ncols if self._blob_offset < 0: self._blob_offset = 0 self.show_views() def _next_page(self, evt): """Scroll forward one page of views.""" nblobs = np.sum(self._blobs_show) offset = self._blob_offset + self._nrows * self._ncols if offset >= nblobs: return self._blob_offset = offset print("offset:", self._blob_offset) self.show_views() def _select_page(self, text): """Handle selecting a page of blob views.""" # only accept numbers and convert floats to ints if not libmag.is_number(text): return page = int(float(text)) # limit to max page nblobs = np.sum(self._blobs_show) nblobs_per_page = self._nrows * self._ncols npages = np.ceil(nblobs / nblobs_per_page).astype(int) if page > npages: page = npages # convert page to offset offset = (page - 1) * nblobs_per_page if offset < 0: offset = 0 page = 0 # update displayed page self._page_txt.set_val(page) self._blob_offset = offset self.show_views()