class MplInteraction(object): def __init__(self, figure): """Initializer :param Figure figure: The matplotlib figure to attach the behavior to. :param Canvas FigureCanvas: The matplotlib PyQT canvas to allow focus after declaration """ self._fig_ref = weakref.ref(figure) self.canvas = FigureCanvas(figure) self._cids_zoom = [] self._cids_pan = [] self._cids_rectangle = [] self._cids_callback_zoom = {} self._cids_callback_pan = {} self._cids_callback_rectangle = {} self.rectangleStats = rectangle_selection(0,0,0,0) self._rectangle_selector = None self._callback_rectangle = None self._cids = [] def __del__(self): self.disconnect() def _add_connection(self, event_name, callback): """Called to add a connection to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ cid = self.canvas.mpl_connect(event_name, callback) self._cids.append(cid) def _add_rectangle_callback(self, callback): """ Because the callback method can only be created when the Zoom event is created and the axis can only be known after the creation if it, this method allow to assign the callback before the creation fo the rectangle selector object """ self._callback_rectangle = callback def _add_connection_rectangle(self, event_name, callback): """Called to add a connection of type rectangle release to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ self._cids_callback_rectangle[event_name] = callback def _add_connection_zoom(self, event_name, callback): """Called to add a connection of type zoom to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ #cid = self.canvas.mpl_connect(event_name, callback) #self._cids_zoom.append(cid) self._cids_callback_zoom[event_name] = callback def _add_connection_pan(self, event_name, callback): """Called to add a connection of type pan to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ #cid = self.canvas.mpl_connect(event_name, callback) #self._cids_pan.append(cid) self._cids_callback_pan[event_name] = callback def disconnect_rectangle(self): if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_rectangle: figure.canvas.mpl_disconnect(cid) self._disable_rectangle() self._cids_rectangle.clear() def disconnect_zoom(self): """ Disconnect all zoom events and disable the rectangle selector """ if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_zoom: figure.canvas.mpl_disconnect(cid) self._cids_zoom.clear() def disconnect_pan(self): """ Disconnect all pan events """ if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_pan: figure.canvas.mpl_disconnect(cid) self._cids_pan.clear() def _disable_rectangle(self): self._rectangle_selector.set_visible(False) self._rectangle_selector.set_active(False) def disconnect(self): """Disconnect interaction from Figure.""" if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids: figure.canvas.mpl_disconnect(cid) self._fig_ref = None def _enable_rectangle(self): if not self._rectangle_selector.active: self._rectangle_selector.set_active(True) if not self._rectangle_selector.visible: self._rectangle_selector.set_visible(True) def connect_rectangle(self): for event_name, callback in self._cids_callback_rectangle.items(): cid = self.canvas.mpl_connect(event_name, callback) self._cids_rectangle.append(cid) self._enable_rectangle() def connect_zoom(self): """ Assign all callback zoom events to the mpl """ for event_name, callback in self._cids_callback_zoom.items(): cid = self.canvas.mpl_connect(event_name, callback) self._cids_zoom.append(cid) self._enable_rectangle() def connect_pan(self): """ Assign all callback pan events to the mpl """ for event_name, callback in self._cids_callback_pan.items(): cid = self.canvas.mpl_connect(event_name, callback) self._cids_pan.append(cid) @property def figure(self): """The Figure this interaction is connected to or None if not connected.""" return self._fig_ref() if self._fig_ref is not None else None def _axes_to_update(self, event): """Returns two sets of Axes to update according to event. :param MouseEvent event: Matplotlib event to consider :return: Axes for which to update xlimits and ylimits :rtype: 2-tuple of set (xaxes, yaxes) """ x_axes, y_axes = set(), set() for ax in self.figure.axes: if ax.contains(event)[0]: x_axes.add(ax) y_axes.add(ax) return x_axes, y_axes def create_rectangle_ax(self, ax): rectprops = dict(facecolor = 'red', edgecolor = 'black', alpha = 0.1, fill = True, linewidth = 1, linestyle= '-') self._rectangle_selector = RectangleSelector(ax, self._callback_rectangle, drawtype= 'box', useblit= True, rectprops= rectprops, button= [1,3], minspanx = 5, minspany=5, spancoords='pixels', interactive=True) self._disable_rectangle() def update_rectangle(self, coord1_1, coord1_2, coord2_1, coord2_2): """ Set coordinates of the rectangle to be drawed, and send message to draw the new spectrum :param int coord1_1: initial x value of the coordinate :param int coord1_2: initial y value of the coordinate :param int coord2_1: end x value of the coordinate :param int coord2_2: end y value of the coordinate """ #self.disconnect_zoom() #self.disconnect_pan() self.connect_rectangle() self.figure.canvas.clearFocus() self._rectangle_selector.extents = (coord1_1, coord2_1, coord1_2, coord2_2) self.rectangleStats.ix = coord1_1 self.rectangleStats.iy = coord1_2 self.rectangleStats.ex = coord2_1 self.rectangleStats.ey = coord2_2 pub.sendMessage('rectangleSelected', ix = coord1_1, iy =coord1_2 , ex = coord2_1, ey= coord2_2) #self.disconnect_rectangle() def redraw_rectangle_without_interaction_tool(self): """ Redraw the rectangle on the axe that does not require to activate tools(pan or zoom) after it and to emit the signal named rectangleSelected """ if self.rectangleStats.ix != 0: self.connect_rectangle() self._rectangle_selector.extents = (self.rectangleStats.ix, self.rectangleStats.ex, self.rectangleStats.iy, self.rectangleStats.ey) pub.sendMessage('rectangleSelected', ix = self.rectangleStats.ix, iy =self.rectangleStats.iy , ex = self.rectangleStats.ex, ey= self.rectangleStats.ey) def redraw_rectangle_from_slider(self): """ Redraw the rectangle on the axe and maintain the state of the active tool when a slice had been changed """ if self.rectangleStats.ix != 0: self._enable_rectangle() self._rectangle_selector.extents = (self.rectangleStats.ix, self.rectangleStats.ex, self.rectangleStats.iy, self.rectangleStats.ey) pub.sendMessage('rectangleSelected', ix = self.rectangleStats.ix, iy =self.rectangleStats.iy , ex = self.rectangleStats.ex, ey= self.rectangleStats.ey) def redraw_rectangle_with_interaction_tool(self): """ Redraw the rectangle on the axe that requiere to activate tools (pan or zoom) after it and to prevent the signal emission """ if self.rectangleStats.ix != 0: self._rectangle_selector.set_visible(True) self._rectangle_selector.extents = (self.rectangleStats.ix, self.rectangleStats.ex, self.rectangleStats.iy, self.rectangleStats.ey) def _draw(self): """Conveninent method to redraw the figure""" self.canvas.draw()
class ROIDataAquisition(object): ''' This class provides a GUI for selecting box shaped Regions Of Interest (ROIs). Each ROI is represented as a tuple: ((min_x,max_x),(min_y,max_y),(min_z,max_z)). When using the zoom/pan tool from the toolbar ROI selection is disabled. Once you click again on the zoom/pan button zooming/panning will be disabled and ROI selection is enabled. Note that when you are marking the ROI on a slice that is outside the Z-range selected by the range slider, once you are done selecting the ROI, you will see no change on the current slice. This is the correct behavior, though initially you may be surprised by it. ''' def __init__(self, image, window_level=None, figure_size=(10, 8)): self.image = image self.npa, self.min_intensity, self.max_intensity = self.get_window_level_numpy_array( self.image, window_level) self.rois = [] # ROI display settings self.roi_display_properties = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) # Create a figure. self.fig, self.axes = plt.subplots(1, 1, figsize=figure_size) # Connect the mouse button press to the canvas (__call__ method is the invoked callback). self.fig.canvas.mpl_connect('button_press_event', self) self.roi_selector = RectangleSelector( self.axes, lambda eclick, erelease: None, drawtype='box', useblit=True, button=[1, 3], # Left, right buttons only. minspanx=5, minspany=5, # Ignore motion smaller than 5 pixels. spancoords='pixels', interactive=True, rectprops=self.roi_display_properties) self.roi_selector.set_visible(False) ui = self.create_ui() # Display the data and the controls, first time we display the image is outside the "update_display" method # as that method relies on the existance of a previous image which is removed from the figure. self.axes.imshow(self.npa[self.slice_slider.value, :, :], cmap=plt.cm.Greys_r, vmin=self.min_intensity, vmax=self.max_intensity) self.update_display() display(ui) def create_ui(self): # Create the active UI components. Height and width are specified in 'em' units. This is # a html size specification, size relative to current font size. self.addroi_button = widgets.Button(description='Add ROI', width='7em', height='3em') self.addroi_button.on_click(self.add_roi) self.clearlast_button = widgets.Button(description='Clear Last', width='7em', height='3em') self.clearlast_button.on_click(self.clear_last) self.clearall_button = widgets.Button(description='Clear All', width='7em', height='3em') self.clearall_button.on_click(self.clear_all) self.roi_range_slider = widgets.IntRangeSlider( description='ROI z range:', min=0, max=self.npa.shape[0] - 1, step=1, value=[0, self.npa.shape[0] - 1], width='20em') self.slice_slider = widgets.IntSlider(description='image z slice:', min=0, max=self.npa.shape[0] - 1, step=1, value=int( (self.npa.shape[0] - 1) / 2), width='20em') self.slice_slider.observe(self.on_slice_slider_value_change, names='value') # Layout of UI components. This is pure ugliness because we are not using a UI toolkit. Layout is done # using the box widget and padding so that the visible UI components are spaced nicely. bx0 = widgets.Box(padding=7, children=[self.slice_slider]) bx1 = widgets.Box(padding=7, children=[self.addroi_button]) bx2 = widgets.Box(padding=15, children=[self.clearlast_button]) bx3 = widgets.Box(padding=15, children=[self.clearall_button]) bx4 = widgets.Box(padding=15, children=[self.roi_range_slider]) return widgets.HBox(children=[ widgets.HBox(children=[bx1, bx2, bx3]), widgets.VBox(children=[bx0, bx4]) ]) def on_slice_slider_value_change(self, change): self.update_display() def get_window_level_numpy_array(self, image, window_level): npa = sitk.GetArrayViewFromImage(image) # We don't take the minimum/maximum values, just in case there are outliers (top/bottom 2%) if not window_level: min_max = np.percentile(npa.flatten(), [2, 98]) return npa, min_max[0], min_max[1] else: return npa, window_level[1] - window_level[0] / 2.0, window_level[ 1] + window_level[0] / 2.0 def update_display(self): # Draw the image and ROIs. # imshow adds an image to the axes, so we also remove the previous one. self.axes.imshow(self.npa[self.slice_slider.value, :, :], cmap=plt.cm.Greys_r, vmin=self.min_intensity, vmax=self.max_intensity) self.axes.images[0].remove() # Iterate over all of the ROIs and only display/undisplay those that are relevant. for roi_data in self.rois: if self.slice_slider.value >= roi_data[3][ 0] and self.slice_slider.value <= roi_data[3][1]: roi_data[0].set_visible(True) else: roi_data[0].set_visible(False) self.axes.set_title('selected {0} ROIs'.format(len(self.rois))) self.axes.set_axis_off() self.fig.canvas.draw_idle() def add_roi_data(self, roi_data): ''' Add regions of interest to this GUI. Input is an iterable containing tuples where each tuple contains three tuples (min_x,max_x),(min_y,max_y), (min_z,max_z). The ROI is the box defined by these integer values and includes both min/max values. ''' self.validate_rois(roi_data) for roi in roi_data: self.rois.append((patches.Rectangle( (roi[0][0], roi[1][0]), roi[0][1] - roi[0][0], roi[1][1] - roi[1][0], **self.roi_display_properties), roi[0], roi[1], roi[2])) self.axes.add_patch(self.rois[-1][0]) self.update_display() def set_rois(self, roi_data): ''' Clear any existing ROIs and set the display to the given ones. Input is an iterable containing tuples where each tuple contains three tuples (min_x,max_x),(min_y,max_y), (min_z,max_z). The ROI is the box defined by these integer values and includes both min/max values. ''' self.clear_all_data() self.add_roi_data(roi_data) def validate_rois(self, roi_data): for roi in roi_data: # First element in each tuple is expected to be smaller or equal to the second element. if roi[0][0] > roi[0][1] or roi[1][0] > roi[1][1] or roi[2][ 0] > roi[2][1]: raise ValueError( 'First element in each tuple is expected to be smaller than second element, error in ROI (' + ', '.join(map(str, roi)) + ').') # Note that SimpleITK uses x-y-z specification vs. numpy's z-y-x if roi[0][0] >= self.npa.shape[2] or roi[0][1] < 0 or roi[1][ 0] >= self.npa.shape[1] or roi[1][1] < 0 or roi[2][ 0] >= self.npa.shape[0] or roi[2][0] < 0: raise ValueError('Given ROI (' + ', '.join(map(str, roi)) + ') is outside the image bounds.') def add_roi(self, button): if self.roi_selector.visible: self.roi_selector.set_visible(False) # Extent is in sub-pixel coordinates, we need it in pixels/voxels. roi_extent = [ int(round(coord)) for coord in self.roi_selector.extents ] # We keep the patch for display and the x,y,z ranges of the ROI. self.rois.append((patches.Rectangle( (roi_extent[0], roi_extent[2]), roi_extent[1] - roi_extent[0], roi_extent[3] - roi_extent[2], **self.roi_display_properties), (roi_extent[0], roi_extent[1]), (roi_extent[2], roi_extent[3]), self.roi_range_slider.value)) self.axes.add_patch(self.rois[-1][0]) self.update_display() def clear_all_data(self): for roi_data in self.rois: roi_data[0].remove() del self.rois[:] def clear_all(self, button): self.clear_all_data() self.update_display() def clear_last(self, button): if self.rois: self.rois[-1][0].remove() self.rois.pop() self.update_display() def get_rois(self): ''' Return a list of tuples representing the ROIs. Each tuple contains three tuples (min_x,max_x), (min_y,max_y), (min_z,max_z). The ROI is the box defined by these integer values and includes both min/max values. ''' return [(roi_data[1], roi_data[2], roi_data[3]) for roi_data in self.rois] def __call__(self, event): # This is dangerous as we are accessing a "private" variable to find the state # of the figure's toolbar ('ZOOM',PAN' or None). When Zoom or pan are active we will # ignore the button press, once the user deactivates the zoom/pan we can allow them # to select the ROI. # Discussion on stack overflow with matplotlib developer (circa 2013), no change to date: # http://stackoverflow.com/questions/20711148/ignore-matplotlib-cursor-widget-when-toolbar-widget-selected if self.fig.canvas.toolbar._active is None: self.roi_selector.set_visible(True) self.addroi_button.disabled = False self.update_display()
def gui(): """""" """""" """""" """ G L O B A L S """ """""" """""" """""" # global dispInterf_state # global stop_event global root, livefeed_canvas, imageplots_frame global slider_exposure, measurementfolder_name, calibrationfolder_name global button_initcamera, button_release, button_saveconfig global text_config, tbox, entry_measfolder, entry_calibfolder, piezosteps_var, CheckVar, output_text global a, colors, canvas_plot, line, ax global updateconfig_event global POI, ROI global displaymidline_state global piezo_dispaxis, calib_linear_region global toggle_selector_RS POI, ROI = [], [] displaymidline_state = False measurementfolder_name = 'stack' calibrationfolder_name = 'stack' calib_linear_region = [19, None] # dispInterf_state = 0 q = Queue() stdout_queue = Queue() beginlive_event = Event() stoplive_event = Event() release_event = Event() # stop_event = Event() piezostep_event = Event() updateconfig_event = Event() plotmidline_event = Event() """ MAIN WINDOW """ root = tk.Tk() root.iconbitmap('winicon.ico') #root.wm_attributes('-topmost', 1) # w, h = root.winfo_screenwidth(), root.winfo_screenheight() #root.geometry("%dx%d+0+0" % (w, h)) root.title('White light interferometry: Topography') root.configure(background='grey') """ MENU """ menubar = tk.Menu(root) filemenu = tk.Menu(menubar, tearoff=0) #filemenu.add_command(label = 'Load image', command=openimage) filemenu.add_command(label='Save displayed image') menubar.add_cascade(label='File', menu=filemenu) optionsmenu = tk.Menu(menubar, tearoff=0) optionsmenu.add_command(label='Configure camera') menubar.add_cascade(label='Options', menu=optionsmenu) menubar.add_command(label='Help') """""" """""" """""" """""" """""" """""" """""" """""" """""" """""" """""" """ L A Y O U T """ """""" """""" """""" """""" """""" """""" """""" """""" """""" """""" """""" """ 2 MAIN TABS """ tabControl = ttk.Notebook(root) tab_ops = ttk.Frame(tabControl) tab_config = ttk.Frame(tabControl) tabControl.add(tab_ops, text='Operations') tabControl.add(tab_config, text='Camera configuration') tabControl.grid(row=0, sticky='we') """ CAMERA CONFIGURATIONS FROM .INI FILE """ text_config = tk.Text(tab_config, bg='gray18', fg='thistle1') text_config.grid(row=0, sticky='we') scrollb = tk.Scrollbar(tab_config, command=text_config.yview) scrollb.grid(row=0, column=1, sticky='nswe') text_config['yscrollcommand'] = scrollb.set root.config_ini = 'config.ini' file_contents = open(root.config_ini).read() text_config.insert('end', file_contents) tbox_contents = text_config.get('1.0', 'end') # c.char is a fixed array, so in case future editing needs more array space, # an expanded array is passed as an argument tbox = Array(ctypes.c_char, bytes(tbox_contents + '\n' * 10, 'utf8')) text_config.tag_config('section', foreground='khaki1') update_tboxEmbellishments() """ SAVE CONFIGURATION CHANGES """ button_saveconfig = tk.Button(tab_config, text="Save changes", bg="white", fg="black", command=update_config) button_saveconfig.grid(row=0, column=2, padx=10, pady=10) """ CAMERA CONNECTION/DISCONNECTION FRAME """ cameraonoff_frame = tk.Frame(tab_ops) cameraonoff_frame.grid(row=0, sticky='we') cameraonoff_frame.grid_rowconfigure(0, weight=1) cameraonoff_frame.grid_columnconfigure(0, weight=1) cameraonoff_frame.grid_columnconfigure(1, weight=1) """ INITIALIZE CAMERA """ button_initcamera = tk.Button(cameraonoff_frame, text="INITIALIZE CAMERA", bg="white", fg="black", command=lambda: create_cameraprocess(q, release_event, \ beginlive_event, stoplive_event, piezostep_event, \ updateconfig_event, plotmidline_event, stdout_queue)) button_initcamera.grid(row=0, column=0, padx=10, pady=10) """ RELEASE CAMERA """ button_release = tk.Button( cameraonoff_frame, text="RELEASE CAMERA", bg="white", fg="black", command=lambda: notify_releasecamera(release_event)) button_release.grid(row=0, column=1, padx=10, pady=10) # """ # EXPOSURE TIME CONFIGURATION # """ # label_exposure = tk.Label(cameraonoff_frame, text='Exposure time: ') # label_exposure.grid(row=0,column=2,padx=10,pady=10,sticky='we') # # slider_exposure = tk.Scale(cameraonoff_frame, from_=6, to=2000, orient='horizontal') # slider_exposure.grid(row=0,column=3,padx=10,pady=10,sticky='we') # # entry_exposure = tk.Entry(cameraonoff_frame) # entry_exposure.grid(row=0,column=4,padx=10,pady=10,sticky='we') # entry_exposure.delete(0, 'end') # entry_exposure.insert(0, '') # entry_exposure.bind("<Return>", get_exposure) # # button_exposure = tk.Button(cameraonoff_frame, # text="Set", # bg="white", # fg="black", # command=set_exposure) # button_exposure.grid(row=0,column=5,padx=10,pady=10,sticky='we') root.rowconfigure(0, weight=1) root.columnconfigure(0, weight=1) tab_ops.rowconfigure(0, weight=1) tab_ops.columnconfigure(0, weight=1) tab_config.rowconfigure(0, weight=1) tab_config.columnconfigure(0, weight=1) main_frame = tk.Frame(tab_ops) main_frame.grid(row=1, column=0, sticky='nswe') main_frame.grid_rowconfigure(0, weight=1) main_frame.grid_rowconfigure(1, weight=1) main_frame.grid_rowconfigure(2, weight=1) main_frame.grid_rowconfigure(3, weight=1) main_frame.grid_columnconfigure(0, weight=1) main_frame.grid_columnconfigure(1, weight=1) main_frame.grid_columnconfigure(2, weight=1) imageplots_frame = tk.Frame(main_frame) imageplots_frame.grid(row=0, column=0, sticky='nswe') imageplots_frame.grid_rowconfigure(0, weight=1) imageplots_frame.grid_columnconfigure(0, weight=1) oscillation_frame = tk.Frame(main_frame) oscillation_frame.grid(row=0, column=1, sticky='nswe') oscillation_frame.grid_rowconfigure(0, weight=1) oscillation_frame.grid_columnconfigure(0, weight=1) buttons_frame = tk.Frame(main_frame) buttons_frame.grid(row=0, column=2, sticky='nswe') buttons_frame.grid_rowconfigure(0, weight=1) buttons_frame.grid_columnconfigure(0, weight=1) piezosteps_frame = tk.Frame(buttons_frame) piezosteps_frame.grid(row=0, column=0, sticky='nswe') piezosteps_frame.grid_rowconfigure(0, weight=1) piezosteps_frame.grid_columnconfigure(0, weight=1) preperation_frame = tk.Frame(buttons_frame, borderwidth=1, relief='solid') preperation_frame.grid(row=1, column=0, sticky='nswe') preperation_frame.grid_rowconfigure(0, weight=1) preperation_frame.grid_columnconfigure(0, weight=1) measurement_frame = tk.Frame(buttons_frame, borderwidth=1, relief='solid') measurement_frame.grid(row=2, column=0, sticky='nswe', pady=10) measurement_frame.grid_rowconfigure(0, weight=1) measurement_frame.grid_columnconfigure(0, weight=1) selections_frame = tk.Frame(main_frame) selections_frame.grid(row=2, column=0, sticky='nswe') selections_frame.grid_rowconfigure(0, weight=1) selections_frame.grid_columnconfigure(0, weight=1) """ CREATE CANVAS FOR IMAGE DISPLAY """ # sections = cfg.load_config_fromtkText('ALL', text_config.get('1.0', 'end')) # h = eval(sections[2]['height']) # w = eval(sections[2]['width']) dpi = 96.0 # f = Figure(figsize=(w/dpi,h/dpi)) f = Figure(figsize=(500 / dpi, 500 / dpi), dpi=96) f.subplots_adjust(left=0.0, bottom=0.0, right=1.0, top=1.0) ax = f.add_subplot(111) ax.set_axis_off() img = Image.frombytes('L', (500, 500), b'\x00' * 250000) ax.imshow(img, cmap='gray', vmin=0, vmax=65535) livefeed_canvas = FigureCanvasTkAgg(f, master=imageplots_frame) livefeed_canvas.get_tk_widget().grid(row=0, column=0, sticky='nswe') livefeed_canvas.draw() """ TOOLBAR - IMAGE SHOW """ toolbarimshowFrame = tk.Frame(master=imageplots_frame) toolbarimshowFrame.grid(row=1, column=0) toolbarimshow = NavigationToolbar2Tk(livefeed_canvas, toolbarimshowFrame) toolbarimshow.update() # create global list of different plotting colors colors = lspec() """ CREATE CANVAS FOR POI AND MIDLINE PLOTTING """ # dpi = 96 # fig = Figure(figsize=(imageplots_frame.winfo_height()/dpi, imageplots_frame.winfo_width()/3/dpi)) fig = Figure(tight_layout=True) a = fig.add_subplot(111) canvas_plot = FigureCanvasTkAgg(fig, master=imageplots_frame) canvas_plot.get_tk_widget().grid(row=0, column=1, sticky='nswe') """ TOOLBAR - LINE PLOT """ toolbarplotFrame = tk.Frame(master=imageplots_frame) toolbarplotFrame.grid(row=1, column=1) toolbarplot = NavigationToolbar2Tk(canvas_plot, toolbarplotFrame) toolbarplot.update() label_preparation = tk.Label(preperation_frame, text='P R E P A R A T I O N') label_preparation.grid(row=0, columnspan=2) """ POINTS OF INTEREST SELECTION BUTTONS """ button_POIenable = tk.Button(preperation_frame, text="Select POI", fg="gold2", bg='grey18', command=enable_POIsel) button_POIenable.grid(row=1, column=0, padx=0, pady=10, sticky='ew') button_POIdisable = tk.Button(preperation_frame, text="OK!", fg="black", command=disable_POIsel) button_POIdisable.grid(row=1, column=1, padx=0, pady=0, sticky='ew') """ REGION OF INTEREST SELECT BUTTONS """ button_ROIenable = tk.Button(preperation_frame, text="Select ROI", fg="chocolate2", bg='grey18', command=enable_ROIsel) button_ROIenable.grid(row=2, column=0, padx=0, pady=0, sticky='ew') # button_ROIdisable = tk.Button(preperation_frame, # text="OK!", # fg="black", # command=disable_ROIsel) # button_ROIdisable.grid(row=2,column=1,padx=0,pady=0,sticky='ew') simplebuttons_frame = tk.Frame(selections_frame) simplebuttons_frame.grid(row=0, column=0, sticky='nswe') selections_frame.grid_rowconfigure(0, weight=1) selections_frame.grid_columnconfigure(0, weight=1) """ OSCILLATE PIEZO BUTTON """ button_oscillate = tk.Button(preperation_frame, text="Oscillate Piezo", fg="black", command=lambda: create_oscillationprocess( piezostep_event, stdout_queue)) button_oscillate.grid(row=3, column=0, padx=0, pady=0, sticky='ew') """ PLOT MIDLINE BUTTON """ button_oscillate = tk.Button( preperation_frame, text="Display intensity across\nhor/ntal line", fg="black", command=lambda: toggle_displayMidline(plotmidline_event)) button_oscillate.grid(row=3, column=1, padx=0, pady=0, sticky='ew') """ OPTION LIST FOR NUMBER OF PIEZO STEPS - LABEL """ label_piezosteps = tk.Label(piezosteps_frame, text='Piezo steps:') label_piezosteps.grid(row=0, column=0, padx=0, pady=0, sticky='ew') """ OPTION LIST FOR NUMBER OF PIEZO STEPS - VALUES """ piezosteps_options = ['100', '200', '300', '400', '500', '600'] piezosteps_var = tk.StringVar() piezosteps_var.set(piezosteps_options[-1]) # default value optionmenu_piezosteps = tk.OptionMenu(piezosteps_frame, piezosteps_var, *piezosteps_options) optionmenu_piezosteps.grid(row=0, column=1, padx=0, pady=0, sticky='ew') label_measurement = tk.Label(measurement_frame, text='M E A S U R E M E N T') label_measurement.grid(row=0, columnspan=2) """ EXECUTE PIEZO SCAN FOR INTERFEROGRAM STACK CAPTURING """ button_oscillate = tk.Button(measurement_frame, text="Piezo scan\nCapture interferograms", fg="khaki1", bg='grey18', command=prepare_piezoscan) button_oscillate.grid(row=1, column=0, columnspan=2, padx=0, pady=10, sticky='ew') CheckVar = tk.StringVar() CheckVar.set('m') C1 = tk.Radiobutton(measurement_frame, text='Measurement', variable=CheckVar, value='m') C2 = tk.Radiobutton(measurement_frame, text='Calibration', variable=CheckVar, value='c') C1.grid(row=2, column=0, padx=0, pady=0, sticky='we') C2.grid(row=2, column=1, padx=0, pady=0, sticky='we') """ EXECUTE PIEZO SCAN FOR CALIBRATION PROCESS """ button_oscillate = tk.Button(measurement_frame, text="Calibrate", fg="khaki1", bg='grey18', command=prepare_calibrate) button_oscillate.grid(row=5, column=0, padx=0, pady=0, sticky='ew') """ INSERT MEASUREMENT IMAGE STACK FOLDER NAME """ label_folder = tk.Label(measurement_frame, text='Measurement\nfolder name:') label_folder.grid(row=3, column=0, padx=0, pady=0, sticky='ew') entry_measfolder = tk.Entry(measurement_frame) entry_measfolder.grid(row=3, column=1, padx=0, pady=0, sticky='we') entry_measfolder.delete(0, 'end') entry_measfolder.insert(0, 'stack') entry_measfolder.bind("<Return>", setMeasurementFolderName) """ INSERT CALIBRATION IMAGE STACK FOLDER NAME """ label_folder = tk.Label(measurement_frame, text='Calibration\nfolder name:') label_folder.grid(row=4, column=0, padx=0, pady=0, sticky='ew') entry_calibfolder = tk.Entry(measurement_frame) entry_calibfolder.grid(row=4, column=1, padx=0, pady=0, sticky='we') entry_calibfolder.delete(0, 'end') entry_calibfolder.insert(0, 'stack') entry_calibfolder.bind("<Return>", setCalibrationFolderName) """ EXECUTE INTERFEROGRAM STACK ANALYSIS FOR SURFACE ELEVATION MAP EXTRACTION """ button_oscillate = tk.Button(measurement_frame, text="Analyze", fg="khaki1", bg='grey18', command=prepare_analysis) button_oscillate.grid(row=5, column=1, padx=0, pady=0, sticky='ew') # """ # DISPLAY POI INTERFEROGRAMS # """ # button_toggleDispInterf = tk.Button(oscillation_frame, # text="Display/Hide\nPOI Interferograms", # fg="black", # command=toggleDispInterf_threaded) # button_toggleDispInterf.grid(row=2,column=1,padx=0,pady=0,sticky='ew') """ LIVE FEED BUTTON """ button_live = tk.Button(simplebuttons_frame, text="Live!", fg="red", bg='grey18', command=lambda: notify_beginlive(beginlive_event)) button_live.grid(row=0, column=0, padx=10, pady=10, sticky='ew') """ STOP LIVE BUTTON """ button_reset = tk.Button(simplebuttons_frame, text="Stop Live Feed", fg="red", bg='grey18', command=lambda: notify_stoplive(stoplive_event)) button_reset.grid(row=0, column=1, padx=10, pady=10, sticky='we') """""" """ || || || || || || VV VV VV """ """""" piezo_dispaxis = np.loadtxt('Mapping_Steps_Displacement_2.txt') """ OUTPUT TEXT """ redirectstdout_frame = tk.Frame(main_frame) redirectstdout_frame.grid(row=3, column=0, sticky='nswe') output_text = ScrolledText(redirectstdout_frame, bg='gray18', fg='thistle1', width=75, height=10) output_text.see('end') output_text.grid(row=0, padx=10, pady=10, sticky='nswe') """ CLEAR OUTPUT TEXT BOX """ button_cleartbox = tk.Button(redirectstdout_frame, text="Clear", fg="lawn green", bg='grey18', command=clear_outputtext) button_cleartbox.grid(row=0, column=1, padx=10, pady=10, sticky='we') """ RECTANGLE SELECTOR OBJECT - for ROI selection """ toggle_selector_RS = RectangleSelector( ax, line_select_callback, drawtype='box', useblit=True, button=[1, 3], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', interactive=True) toggle_selector_RS.set_active(False) toggle_selector_RS.set_visible(False) """ STDOUT REDIRECTION """ sys.stdout = StdoutQueue(stdout_queue) sys.stderr = StdoutQueue(stdout_queue) # Instantiate and start the text monitor monitor = Thread(target=text_catcher, args=(output_text, stdout_queue)) monitor.daemon = True monitor.start() # sys.stdout = StdoutRedirector(output_text) # sys.stderr = StderrRedirector(output_text) root.protocol("WM_DELETE_WINDOW", on_close) root.config(menu=menubar) root.mainloop()
class MplInteraction(object): def __init__(self, figure): """Initializer :param Figure figure: The matplotlib figure to attach the behavior to. """ self._fig_ref = weakref.ref(figure) self.canvas = FigureCanvas(figure) self._cids_zoom = [] self._cids_pan = [] self._cids = [] self._cids_callback_zoom = {} self._cids_callback_pan = {} self._callback_rectangle = None self._rectangle_selector = None self._xLimits = None self._yLimits = None #Create invokers self._invokerZoom = UndoHistoryZoomInvoker(figure.canvas) self._invokerPan = UndoHistoryPanInvoker(figure.canvas) def _add_connection(self, event_name, callback): cid = self.canvas.mpl_connect(event_name, callback) self._cids.append(cid) def create_rectangle_ax(self, ax): rectprops = dict(facecolor = None, edgecolor = 'black', alpha = 1, fill=False, linewidth = 1, linestyle = '-') self._rectangle_selector = RectangleSelector(ax, self._callback_rectangle, drawtype='box', useblit=True, rectprops = rectprops, button=[1, 3], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', interactive=False) self._rectangle_selector.set_visible(False) def set_axes_limits(self, xLimits, yLimits): """ Get initial limits to allow to adjust the last zoom command to be the inital limits """ self._xLimits = xLimits self._yLimits = yLimits def __del__(self): self.disconnect() def _add_rectangle_callback(self, callback): """ Beacuse the callback method can only be created when the Zoom event is created and the axe can only be know after the creation of it, this method allow to assign the callback before the creation of the rectangle selector object """ self._callback_rectangle = callback def _add_connection_zoom(self, event_name, callback): """Called to add a connection of type zoom to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ #cid = self.canvas.mpl_connect(event_name, callback) #self._cids_zoom.append(cid) self._cids_callback_zoom[event_name] = callback def _add_connection_pan(self, event_name, callback): """Called to add a connection of type pan to an event of the figure :param str event_name: The matplotlib event name to connect to. :param callback: The callback to register to this event. """ #cid = self.canvas.mpl_connect(event_name, callback) #self._cids_pan.append(cid) self._cids_callback_pan[event_name] = callback def disconnect_zoom(self): """ Disconnect all zoom events and disable the rectangle selector """ if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_zoom: figure.canvas.mpl_disconnect(cid) self._disable_rectangle() self._cids_zoom.clear() def disconnect_pan(self): """ Disconnect all pan events """ if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_pan: figure.canvas.mpl_disconnect(cid) self._cids_pan.clear() def _disable_rectangle(self): self._rectangle_selector.set_visible(False) self._rectangle_selector.set_active(False) def _enable_rectangle(self): self._rectangle_selector.set_visible(True) self._rectangle_selector.set_active(True) def connect_zoom(self): """ Assign all callback zoom events to the mpl """ for event_name, callback in self._cids_callback_zoom.items(): cid = self.canvas.mpl_connect(event_name, callback) self._cids_zoom.append(cid) self._enable_rectangle() def connect_pan(self): """ Assign all callback pan events to the mpl """ for event_name, callback in self._cids_callback_pan.items(): cid = self.canvas.mpl_connect(event_name, callback) self._cids_pan.append(cid) def disconnect(self): """Disconnect interaction from Figure.""" if self._fig_ref is not None: figure = self._fig_ref() if figure is not None: for cid in self._cids_zoom: figure.canvas.mpl_disconnect(cid) for cid in self._cids_pan: figure.canvas.mpl_disconnect(cid) for cid in self._cids: figure.canvas.mpl_disconnect(cid) self._fig_ref = None @property def figure(self): """The Figure this interaction is connected to or None if not connected.""" return self._fig_ref() if self._fig_ref is not None else None def undo_last_action(self): """ First, it undo the last action made by the zoom event Second, because the command list contains each command, the first one is related to adjust the zoom, which ocurred before, so the command list execute twice the same event, and because of that, the undo button need to be disabled and the command list clear """ self._invokerZoom.undo() if self._invokerZoom.command_list_length() <= 1: self._invokerZoom.clear_command_list() pub.sendMessage('setStateUndo', state = False) def add_zoom_reset(self): if self._invokerZoom.command_list_length() == 0: #Send the signal to change undo button state pub.sendMessage('setStateUndo', state = True) zoomResetCommand = ZoomResetCommand(self.figure, self._xLimits, self._yLimits) self._invokerZoom.command(zoomResetCommand) self._draw_idle() def _add_initial_zoom_reset(self): if self._invokerZoom.command_list_length() == 0: #Send the signal to change undo button state pub.sendMessage('setStateUndo', state = True) zoomResetCommand = ZoomResetCommand(self.figure, self._xLimits, self._yLimits) self._invokerZoom.command(zoomResetCommand) self._draw_idle() def clear_commands(self): """ Delete all commands """ self._invokerZoom.clear_command_list() def _draw_idle(self): """Update altered figure, but not automatically re-drawn""" self.canvas.draw_idle() def _draw(self): """Conveninent method to redraw the figure""" self.canvas.draw()
class IsoCenter_Child(QMain, IsoCenter.Ui_IsoCenter): "Class that contains subroutines to define isocenter from Lynx image" def __init__(self, parent, owner): super(IsoCenter_Child, self).__init__() self.Owner = owner self.setupUi(self) self.setStyleSheet(parent.styleSheet()) self.parent = parent self.canvas = self.Display_IsoCenter.canvas self.toolbar = self.canvas.toolbar # Connect buttons self.Button_LoadSpot.clicked.connect(self.load) self.Button_detectIsoCenter.clicked.connect(self.drawRect) self.Button_SetIsoCenter.clicked.connect(self.LockIsoCenter) self.Button_Done.clicked.connect(self.Done) # Works only after first rectangle was drawn try: self.Button_detectIsoCenter.clicked.connect(self.initclick) except AttributeError: pass # Flags and Containers self.Image = None self.press = None self.rects = [] self.target_markers = [] # Flags self.IsoCenter_flag = False # Lists for isocenter markers in canvas self.target_markers = [] def drawRect(self): # Remove previous spotdetections for item in self.target_markers: if type(item) == matplotlib.contour.QuadContourSet: [artist.set_visible(False) for artist in item.collections] else: item.set_visible(False) # change cursor style QApplication.setOverrideCursor(Qt.CrossCursor) # Rectangle selector for 2d fit rectprops = dict(facecolor='orange', edgecolor=None, alpha=0.2, fill=True) # drawtype is 'box' or 'line' or 'none' self.RS = RectangleSelector( self.canvas.axes, self.line_select_callback, drawtype='box', rectprops=rectprops, button=[1], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', useblit=True, interactive=True) self.canvas.draw() self.bg = self.canvas.copy_from_bbox(self.RS.ax.bbox) self.RS.set_visible(True) ext = (0, 4, 0, 1) self.RS.draw_shape(ext) # Update displayed handles self.RS._corner_handles.set_data(*self.RS.corners) self.RS._edge_handles.set_data(*self.RS.edge_centers) self.RS._center_handle.set_data(*self.RS.center) for artist in self.RS.artists: self.RS.ax.draw_artist(artist) artist.set_animated(False) self.canvas.draw() self.cid = self.canvas.mpl_connect("button_press_event", self.initclick) def line_select_callback(self, eclick, erelease): x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata p1 = (x1, y1) p2 = (x2, y2) self.spotDetect(p1, p2) def initclick(self, evt): self.RS.background = self.bg self.RS.update() for artist in self.RS.artists: artist.set_animated(True) self.canvas.mpl_disconnect(self.cid) def load(self): "load radiography image of beam IsoCenter" # get filename from full path and display fname = Qfile.getOpenFileName(self, 'Open file', "", "Dicom files (*.dcm *tiff *tif)")[0] try: # import imagedata with regard to filetype if fname.endswith("dcm"): meta = dcm.read_file(fname) self.Image = RadiographyImage(fname, meta.pixel_array, meta.PixelSpacing) elif fname.endswith("tif") or fname.endswith("tiff"): pw, okx = QInputDialog.getDouble(self, 'Pixel Spacing', 'pixel width (mm):', 0.05, decimals=2) self.Image = RadiographyImage(fname, tifffile.imread(fname), pw) self.Text_Filename.setText(fname) # display filename self.canvas.axes.imshow(self.Image.array, cmap='gray', zorder=1, origin='lower') self.canvas.draw() logging.info('{:s} imported as Isocenter'.format(fname)) except Exception: logging.ERROR("{:s} could not be opened".format(fname)) self.IsoCenter_flag = False return 0 def LockIsoCenter(self): """ Read current values from sliders/ spot location text fields and set as final isocenter coordinates to be used for the actual positioning""" self.SpotTxt_x.setStyleSheet("color: rgb(255, 0, 0);") self.SpotTxt_y.setStyleSheet("color: rgb(255, 0, 0);") # Raise flag for checksum check later self.IsoCenter_flag = True # Function to pass IsoCenter values to parent window self.Owner.return_isocenter( self.Image, [self.SpotTxt_x.value(), self.SpotTxt_y.value()]) logging.info('Isocenter coordinates confirmed') def update_crosshair(self): """Get value from Spinboxes and update all markers/plots if that value is changed""" x = self.SpotTxt_x.value() y = self.SpotTxt_y.value() # Update Plot Markers self.hline.set_ydata(y) self.vline.set_xdata(x) # Update Plot self.Display_IsoCenter.canvas.draw() self.SpotTxt_x.setStyleSheet("color: rgb(0, 0, 0);") self.SpotTxt_y.setStyleSheet("color: rgb(0, 0, 0);") self.IsoCenter_flag = False def spotDetect(self, p1, p2): " Function that is invoked by ROI selection, autodetects earpin" # Restore old cursor QApplication.restoreOverrideCursor() # Get ROI limits from drawn rectangle corners x = int(min(p1[0], p2[0]) + 0.5) y = int(min(p1[1], p2[1]) + 0.5) width = int(np.abs(p1[0] - p2[0]) + 0.5) height = int(np.abs(p1[1] - p2[1]) + 0.5) subset = self.Image.array[y:y + height, x:x + width] # Calculate fit function values try: popt, pcov = find_center(subset, x, y, sigma=5.0) logging.info('Detected coordinates for isocenter:' 'x = {:2.1f}, y = {:2.1f}'.format(popt[1], popt[2])) except Exception: logging.error('Autodetection of Landmark in ROI failed.') # self.TxtEarpinX.setValue(0) # self.TxtEarpinY.setValue(0) return 0 xx, yy, xrange, yrange = array2mesh(self.Image.array) data_fitted = twoD_Gaussian((xx, yy), *popt) # Print markers into image ax = self.canvas.axes self.target_markers.append( ax.contour(xx, yy, data_fitted.reshape(yrange, xrange), 5)) self.target_markers.append(ax.axvline(popt[1], 0, ax.get_ylim()[1])) self.target_markers.append(ax.axhline(popt[2], 0, ax.get_xlim()[1])) self.canvas.draw() self.SpotTxt_x.setValue(popt[1]) self.SpotTxt_y.setValue(popt[2]) logging.info('Coordinates of IsoCenter set to ' 'x = {:.1f}, y = {:.1f}'.format(popt[1], popt[2])) def Done(self): "Ends IsoCenter Definition and closes Child" # Also check whether all values were locked to main window if not self.IsoCenter_flag: Hint = QMessage() Hint.setStandardButtons(QMessage.No | QMessage.Yes) Hint.setIcon(QMessage.Information) Hint.setText("Some values have not been locked or were modified!" "\nProceed?") answer = Hint.exec_() if answer == QMessage.Yes: self.close() else: self.close()
class Axes2D(object): def __init__(self, ax, parent): self.ax = ax self.parent = parent self.params_2d = {} self.y, self.x1, self.x2, self.data = [], [], [], [] self.rectangle_coordinates = None self.RectangleSelector = None self.cut_window = None self.colormap_window = None self.apply_log_status = False self.plot_obj = None self.Ranges = Plot2DRangesHandler(self) def set_rectangle(self): self.RectangleSelector = RectangleSelector( self.ax, self.line_select_callback, drawtype='box', button=[1], # don't use middle button minspanx=5, minspany=5, spancoords='pixels', interactive=True) def update_rs(event): if self.RectangleSelector is not None: if self.RectangleSelector.active and self.parent.status == 2: self.RectangleSelector.update() self.parent.mpl_connect('draw_event', update_rs) if self.rectangle_coordinates is not None: x1, y1, x2, y2 = self.rectangle_coordinates self.RectangleSelector.extents = (x1, x2, y1, y2) self.RectangleSelector.update() def line_select_callback(self, eclick, erelease): """eclick and erelease are the press and release events""" assert self.cut_window is not None self.rectangle_coordinates = eclick.xdata, eclick.ydata, erelease.xdata, erelease.ydata self.cut_window.canvas.update_cut_plot() def context_menu(self, event): menu = QMenu() y = self.parent.parent().height() position = self.parent.mapFromParent(QPoint(event.x, y - event.y)) parameter_menu = menu.addMenu('Plot parameters') change_colormap_action = parameter_menu.addAction("Change colormap") change_colormap_action.triggered.connect(self.open_colormap_window) log_action_name = "Disable log" if self.apply_log_status else "Apply log" change_log_action = parameter_menu.addAction(log_action_name) change_log_action.triggered.connect(self.change_log_status) reset_action = parameter_menu.addAction("Reset parameters") reset_action.triggered.connect(self.reset_parameters) redraw_action = menu.addAction("Redraw graph") redraw_action.triggered.connect(self.redraw_2d_plot) menu.addSeparator() open_cut_window_action = menu.addAction("Open cut window") open_cut_window_action.triggered.connect(self.open_cut_window) open_cut_window_action.setEnabled(self.cut_window is None) menu.exec_(self.parent.parent().mapToGlobal(position)) def open_colormap_window(self, event): range_init, range_whole = self.Ranges.get_ranges_for_colormap(self.y) self.colormap_window = ColormapWindow(range_init, range_whole, title='Colormap') self.colormap_window.set_callback(self.colormap_callback) self.colormap_window.show() def redraw_2d_plot(self): self.ax.cla() self.plot_obj = self.ax.imshow(self.y, **self.params_2d) if self.cut_window: self.set_rectangle() def colormap_callback(self, range_): self.Ranges.update_params(range_) self.redraw_2d_plot() self.parent.draw() def change_log_status(self, event): self.apply_log_status = not self.apply_log_status if self.apply_log_status: self.y = self.apply_log(self.data) else: self.y = self.data self.Ranges.change_regime() self.redraw_2d_plot() self.parent.draw() def reset_parameters(self, event): self.apply_log_status = False self.params_2d.pop('vmin', None) self.params_2d.pop('vmax', None) self.y = self.data self.redraw_2d_plot() self.parent.draw() @staticmethod def apply_log(data): min_value = max([0.1, np.amin(data)]) max_value = np.amax(data) return np.log(np.clip(data, min_value, max_value)) def update_plot(self, obj, file): self.data = obj[()] if self.apply_log_status: self.y = self.apply_log(self.data) else: self.y = self.data try: x_ax = file[obj.attrs['x_axis']][()] y_ax = file[obj.attrs['y_axis']][()] assert (len(x_ax), len(y_ax)) == self.y.shape \ or (len(y_ax), len(x_ax)) == self.y.shape, 'shapes are wrong' if (len(y_ax), len(x_ax)) == self.y.shape: y_ax, x_ax = x_ax, y_ax self.x1 = y_ax self.x2 = x_ax except (AssertionError, TypeError, KeyError): self.params_2d.pop('extent', None) self.x1 = list(range(0, self.y.shape[1])) self.x2 = list(range(0, self.y.shape[0])) except Exception as er: print(er) return self.params_2d.update( dict(extent=[self.x1[0], self.x1[-1], self.x2[0], self.x2[-1]])) if self.plot_obj is not None: self.plot_obj.set_data(self.y) self.plot_obj.set_extent(self.params_2d['extent']) self.ax.relim() # Recalculate limits self.ax.autoscale_view(True, True, True) else: self.plot_obj = self.ax.imshow(self.y, **self.params_2d) self.parent.draw() if self.cut_window: self.cut_window.canvas.update_cut_plot() def open_cut_window(self): self.cut_window = CutWindow(self) self.set_rectangle() if self.rectangle_coordinates: self.cut_window.canvas.update_cut_plot() def on_closing_cut_window(self): self.RectangleSelector.set_visible(False) self.RectangleSelector.update() self.RectangleSelector = None self.cut_window = None
class MapView(FigureCanvasQTAgg): """ Map view using cartopy/matplotlib to view multibeam tracklines and surfaces with a map context. """ box_select = Signal(float, float, float, float) def __init__(self, parent=None, width: int = 5, height: int = 4, dpi: int = 100, map_proj=ccrs.PlateCarree(), settings=None): self.fig = Figure(figsize=(width, height), dpi=dpi) self.map_proj = map_proj self.axes = self.fig.add_subplot(projection=map_proj) # self.axes.coastlines(resolution='10m') self.fig.add_axes(self.axes) #self.fig.subplots_adjust(left=0, right=1, bottom=0, top=1) self.axes.gridlines(draw_labels=True, crs=self.map_proj) self.axes.add_feature(cfeature.LAND) self.axes.add_feature(cfeature.COASTLINE) self.line_objects = {} # dict of {line name: [lats, lons, lineplot]} self.surface_objects = { } # nested dict {surfname: {layername: [lats, lons, surfplot]}} self.active_layers = {} # dict of {surfname: [layername1, layername2]} self.data_extents = { 'min_lat': 999, 'max_lat': -999, 'min_lon': 999, 'max_lon': -999 } self.selected_line_objects = [] super(MapView, self).__init__(self.fig) self.navi_toolbar = NavigationToolbar2QT(self.fig.canvas, self) self.rs = RectangleSelector(self.axes, self._line_select_callback, drawtype='box', useblit=False, button=[1], minspanx=5, minspany=5, spancoords='pixels', interactive=True) self.set_extent(90, -90, 100, -100) def set_background(self, layername: str, transparency: float, surf_transparency: float): """ A function for rendering different background layers in QGIS. Disabled for cartopy """ pass def set_extent(self, max_lat: float, min_lat: float, max_lon: float, min_lon: float, buffer: bool = True): """ Set the extent of the 2d window Parameters ---------- max_lat set the maximum latitude of the displayed map min_lat set the minimum latitude of the displayed map max_lon set the maximum longitude of the displayed map min_lon set the minimum longitude of the displayed map buffer if True, will extend the extents by half the current width/height """ self.data_extents['min_lat'] = np.min( [min_lat, self.data_extents['min_lat']]) self.data_extents['max_lat'] = np.max( [max_lat, self.data_extents['max_lat']]) self.data_extents['min_lon'] = np.min( [min_lon, self.data_extents['min_lon']]) self.data_extents['max_lon'] = np.max( [max_lon, self.data_extents['max_lon']]) if self.data_extents['min_lat'] != 999 and self.data_extents[ 'max_lat'] != -999 and self.data_extents[ 'min_lon'] != 999 and self.data_extents['max_lon'] != -999: if buffer: lat_buffer = np.max([(max_lat - min_lat) * 0.5, 0.5]) lon_buffer = np.max([(max_lon - min_lon) * 0.5, 0.5]) else: lat_buffer = 0 lon_buffer = 0 self.axes.set_extent([ np.clip(min_lon - lon_buffer, -179.999999999, 179.999999999), np.clip(max_lon + lon_buffer, -179.999999999, 179.999999999), np.clip(min_lat - lat_buffer, -90, 90), np.clip(max_lat + lat_buffer, -90, 90) ], crs=ccrs.Geodetic()) def add_line(self, line_name: str, lats: np.ndarray, lons: np.ndarray, refresh: bool = False): """ Draw a new multibeam trackline on the cartopy display, unless it is already there Parameters ---------- line_name name of the multibeam line lats numpy array of latitude values to plot lons numpy array of longitude values to plot refresh set to True if you want to show the line after adding here, kluster will redraw the screen after adding lines itself """ if line_name in self.line_objects: return # this is about 3x slower, use transform_points instead # lne = self.axes.plot(lons, lats, color='blue', linewidth=2, transform=ccrs.Geodetic()) ret = self.axes.projection.transform_points(ccrs.Geodetic(), lons, lats) x = ret[..., 0] y = ret[..., 1] lne = self.axes.plot(x, y, color='blue', linewidth=2) self.line_objects[line_name] = [lats, lons, lne[0]] if refresh: self.refresh_screen() def remove_line(self, line_name, refresh=False): """ Remove a multibeam line from the cartopy display Parameters ---------- line_name name of the multibeam line refresh optional screen refresh, True most of the time, unless you want to remove multiple lines and then refresh at the end """ if line_name in self.line_objects: lne = self.line_objects[line_name][2] lne.remove() self.line_objects.pop(line_name) if refresh: self.refresh_screen() def add_surface(self, surfname: str, lyrname: str, surfx: np.ndarray, surfy: np.ndarray, surfz: np.ndarray, surf_crs: int): """ Add a new surface/layer with the provided data Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data surfx 1 dim numpy array for the grid x values surfy 1 dim numpy array for the grid y values surfz 2 dim numpy array for the grid values (depth, uncertainty, etc.) surf_crs integer epsg code """ try: addlyr = True if lyrname in self.active_layers[surfname]: addlyr = False except KeyError: addlyr = True if addlyr: self._add_surface_layer(surfname, lyrname, surfx, surfy, surfz, surf_crs) self.refresh_screen() def hide_surface(self, surfname: str, lyrname: str): """ Hide the surface layer that corresponds to the given names. Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ try: hidelyr = True if lyrname not in self.active_layers[surfname]: hidelyr = False except KeyError: hidelyr = False if hidelyr: self._hide_surface_layer(surfname, lyrname) return True else: return False def show_surface(self, surfname: str, lyrname: str): """ Cartopy backend currently just deletes/adds surface data, doesn't really hide or show. Return False here to signal we did not hide """ return False def remove_surface(self, surfname: str): """ Remove the surface from memory by removing the name from the surface_objects dict Parameters ---------- surfname path to the surface that is used as a name """ if surfname in self.surface_objects: for lyr in self.surface_objects[surfname]: self.hide_surface(surfname, lyr) surf = self.surface_objects[surfname][lyr][2] surf.remove() self.surface_objects.pop(surfname) self.refresh_screen() def _add_surface_layer(self, surfname: str, lyrname: str, surfx: np.ndarray, surfy: np.ndarray, surfz: np.ndarray, surf_crs: int): """ Add a new surface/layer with the provided data Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data surfx 1 dim numpy array for the grid x values surfy 1 dim numpy array for the grid y values surfz 2 dim numpy array for the grid values (depth, uncertainty, etc.) surf_crs integer epsg code """ try: makelyr = True if lyrname in self.surface_objects[surfname]: makelyr = False except KeyError: makelyr = True if makelyr: desired_crs = self.map_proj lon2d, lat2d = np.meshgrid(surfx, surfy) xyz = desired_crs.transform_points(ccrs.epsg(int(surf_crs)), lon2d, lat2d) lons = xyz[..., 0] lats = xyz[..., 1] if lyrname != 'depth': vmin, vmax = np.nanmin(surfz), np.nanmax(surfz) else: # need an outlier resistant min max depth range value twostd = np.nanstd(surfz) med = np.nanmedian(surfz) vmin, vmax = med - twostd, med + twostd # print(vmin, vmax) surfplt = self.axes.pcolormesh(lons, lats, surfz.T, vmin=vmin, vmax=vmax, zorder=10) setextents = False if not self.line_objects and not self.surface_objects: # if this is the first thing you are loading, jump to it's extents setextents = True self._add_to_active_layers(surfname, lyrname) self._add_to_surface_objects(surfname, lyrname, [lats, lons, surfplt]) if setextents: self.set_extents_from_surfaces() else: surfplt = self.surface_objects[surfname][lyrname][2] newsurfplt = self.axes.add_artist(surfplt) # update the object with the newly added artist self.surface_objects[surfname][lyrname][2] = newsurfplt self._add_to_active_layers(surfname, lyrname) def _hide_surface_layer(self, surfname: str, lyrname: str): """ Hide the surface layer that corresponds to the given names. Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ surfplt = self.surface_objects[surfname][lyrname][2] surfplt.remove() self._remove_from_active_layers(surfname, lyrname) self.refresh_screen() def _add_to_active_layers(self, surfname: str, lyrname: str): """ Add the surface layer to the active layers dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.active_layers: self.active_layers[surfname].append(lyrname) else: self.active_layers[surfname] = [lyrname] def _add_to_surface_objects(self, surfname: str, lyrname: str, data: list): """ Add the surface layer data to the surface objects dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data data list of [2dim y values for the grid, 2dim x values for the grid, matplotlib.collections.QuadMesh] """ if surfname in self.surface_objects: self.surface_objects[surfname][lyrname] = data else: self.surface_objects[surfname] = {lyrname: data} def _remove_from_active_layers(self, surfname: str, lyrname: str): """ Remove the surface layer from the active layers dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.active_layers: if lyrname in self.active_layers[surfname]: self.active_layers[surfname].remove(lyrname) def _remove_from_surface_objects(self, surfname, lyrname): """ Remove the surface layer from the surface objects dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.surface_objects: if lyrname in self.surface_objects[surfname]: self.surface_objects[surfname].pop(lyrname) def change_line_colors(self, line_names: list, color: str): """ Change the provided line names to the provided color Parameters ---------- line_names list of line names to use as keys in the line objects dict color string color identifier, ex: 'r' or 'red' """ for line in line_names: lne = self.line_objects[line][2] lne.set_color(color) self.selected_line_objects.append(lne) self.refresh_screen() def reset_line_colors(self): """ Reset all lines back to the default color """ for lne in self.selected_line_objects: lne.set_color('b') self.selected_line_objects = [] self.refresh_screen() def _line_select_callback(self, eclick: MouseEvent, erelease: MouseEvent): """ Handle the return of the Matplotlib RectangleSelector, provides an event with the location of the click and an event with the location of the release Parameters ---------- eclick MouseEvent with the position of the initial click erelease MouseEvent with the position of the final release of the mouse button """ x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata self.rs.set_visible(False) # set the visible property back to True so that the next move event shows the box self.rs.visible = True # signal with min lat, max lat, min lon, max lon self.box_select.emit(y1, y2, x1, x2) # print("(%3.2f, %3.2f) --> (%3.2f, %3.2f)" % (x1, y1, x2, y2)) def set_extents_from_lines(self): """ Set the maximum extent based on the line_object coordinates """ lats = [] lons = [] for ln in self.line_objects: lats.append(self.line_objects[ln][0]) lons.append(self.line_objects[ln][1]) if not lats or not lons: self.set_extent(90, -90, 100, -100) else: lats = np.concatenate(lats) lons = np.concatenate(lons) self.set_extent(np.max(lats), np.min(lats), np.max(lons), np.min(lons)) self.refresh_screen() def set_extents_from_surfaces(self): """ Set the maximum extent based on the surface_objects coordinates """ lats = [] lons = [] for surf in self.surface_objects: for lyrs in self.surface_objects[surf]: lats.append(self.surface_objects[surf][lyrs][0]) lons.append(self.surface_objects[surf][lyrs][1]) if not lats or not lons: self.set_extent(90, -90, 100, -100) else: lats = np.concatenate(lats) lons = np.concatenate(lons) self.set_extent(np.max(lats), np.min(lats), np.max(lons), np.min(lons)) self.refresh_screen() def clear(self): """ Clear all loaded data including surfaces and lines and refresh the screen """ self.line_objects = {} self.surface_objects = {} self.active_layers = {} self.data_extents = { 'min_lat': 999, 'max_lat': -999, 'min_lon': 999, 'max_lon': -999 } self.selected_line_objects = [] self.set_extent(90, -90, 100, -100) self.refresh_screen() def refresh_screen(self): """ Reset to the original zoom/extents """ self.axes.relim() self.axes.autoscale_view() self.fig.canvas.draw_idle() self.fig.canvas.flush_events()
class MvPoints(): def __init__(self, xdata, ydata,fig,axis,color=0,area=40,alpha=1): self.x=xdata self.y=ydata self.points=[] try: if color>=0 or color<10000: self.colors=np.ones(len(xdata))*color self.oldcolor=self.colors else: self.colors=color*2 self.oldcolor=color*2 except: self.colors=color*2 self.oldcolor=color*2 self.fig=fig self.ax=axis self.evnt=None self.alpha=alpha self.area=area self.scat=self.ax[0].scatter(self.x,self.y,s=self.area, c=self.colors,alpha=self.alpha) self.labels=[] self.labels2=[] for i in range(len(xdata)): self.labels.append(self.ax[0].annotate(i, (self.x[i], self.y[i]))) self.labels2.append(self.ax[1].annotate(i, (self.x[i], self.y[i]))) self.ax[0].set_xlim((-1.3,1.3)) self.ax[0].set_ylim((-1.3,1.3)) self.rs = RectangleSelector(self.ax[0], self.line_select_callback, drawtype='box', useblit=False, button=[1], minspanx=5, minspany=5, spancoords='pixels', interactive=True) def line_select_callback(self,eclick, erelease): x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata newx=[] newy=[] self.points=[] xacc=0 yacc=0 self.rs.set_visible(False) for i in range(len(self.x)): newx.append(self.x[i]) newy.append(self.y[i]) if x1 <self.x[i] and x2>self.x[i] and y2>self.y[i] and y1<self.y[i]: self.colors[i]=1 self.points.append(i) xacc+=self.x[i] yacc+=self.y[i] n=len(self.points) if n!=0: #CENTER THE MOUSE MOVING ACCORDING THE POINTS self.x0=xacc/n self.y0=yacc/n ## REMPLACE THE CHANGES POINTS self.x=newx self.y=newy self.scat.remove() self.scat=self.ax[0].scatter(self.x,self.y,s=self.area, c=self.colors,alpha=self.alpha) self.ax[0].set_xlim((-1.3,1.3)) self.ax[0].set_ylim((-1.3,1.3)) self.onclck=self.fig.canvas.mpl_connect('button_press_event', self.onclick) self.evtrelease=self.fig.canvas.mpl_connect('button_release_event', self.onrelease) self.evnt=self.fig.canvas.mpl_connect('motion_notify_event', self.on_drag) self.rs=None else: self.rs = RectangleSelector(self.ax[0], self.line_select_callback, drawtype='box', useblit=False, button=[1], minspanx=5, minspany=5, spancoords='pixels', interactive=True) def on_drag(self,event): xpress, ypress = event.xdata, event.ydata try: for i in self.points: try: self.labels[i].remove() self.labels[i]=self.ax[0].annotate(i, (self.x[i], self.y[i])) except: print('x') #self.labels[i]=self.ax[0].annotate(i, (self.x[i], self.y[i])) self.x[i]=self.x[i]-self.x0+xpress self.y[i]=self.y[i]-self.y0+ypress self.x0=xpress self.y0=ypress self.scat.remove() self.scat=self.ax[0].scatter(self.x,self.y,s=self.area, c=self.colors,alpha=self.alpha) self.ax[0].set_xlim((-1.3,1.3)) self.ax[0].set_ylim((-1.3,1.3)) self.fig.canvas.draw() except: print(xpress,ypress) def onclick(self,event): self.fig.canvas.mpl_disconnect(self.evnt) self.points=[] def onrelease(self,event): self.rs = RectangleSelector(self.ax[0], self.line_select_callback, drawtype='box', useblit=False, button=[1], minspanx=5, minspany=5, spancoords='pixels', interactive=True) self.fig.canvas.mpl_disconnect(self.onclck) self.fig.canvas.mpl_disconnect(self.evtrelease) self.colors=self.oldcolor.copy() def change(self,x,y): try: self.scat2.remove() except: print("First") self.ax[0].set_title("Result of DR with Mixture Kernel") self.ax[1].set_title("Expected DR") self.scat2=self.ax[1].scatter(self.x,self.y,s=self.area, c=self.colors,alpha=self.alpha) self.ax[1].set_xlim((-1.3,1.3)) self.ax[1].set_ylim((-1.3,1.3)) self.scat.remove() self.scat=self.ax[0].scatter(x,y,s=self.area, c=self.colors,alpha=self.alpha) self.ax[0].set_xlim((-1.3,1.3)) self.ax[0].set_ylim((-1.3,1.3)) for i in range(len(self.x)): self.labels[i].remove() self.labels[i]=self.ax[0].annotate(i, (x[i], y[i])) self.labels2[i].remove() self.labels2[i]=self.ax[1].annotate(i, (self.x[i], self.y[i])) self.x=x self.y=y