From fbbd837d1c44c45c45ae077ac1b280320349668e Mon Sep 17 00:00:00 2001 From: Patrick Cleeve Date: Mon, 10 Feb 2025 11:00:17 +1100 Subject: [PATCH] [feat] minor gui/model changes for fibsem tab --- src/odemis/acq/stitching/_tiledacq.py | 3 +- src/odemis/gui/comp/overlay/rectangle.py | 9 +- src/odemis/gui/conf/data.py | 87 +++++++++++++++- src/odemis/gui/cont/stream_bar.py | 99 ++++++++++++++++++- src/odemis/gui/cont/tabs/localization_tab.py | 2 +- src/odemis/gui/main_xrc.py | 3 +- src/odemis/gui/model/__init__.py | 2 +- src/odemis/gui/model/_constants.py | 7 ++ src/odemis/gui/model/stream_view.py | 6 +- src/odemis/gui/model/tab_gui_data.py | 49 +++++++++ src/odemis/gui/win/acquisition.py | 51 ++++++++-- .../xmlh/resources/panel_tab_localization.xrc | 2 +- src/odemis/model/_metadata.py | 4 +- 13 files changed, 300 insertions(+), 24 deletions(-) diff --git a/src/odemis/acq/stitching/_tiledacq.py b/src/odemis/acq/stitching/_tiledacq.py index a7289b007f..670e6f3a5c 100644 --- a/src/odemis/acq/stitching/_tiledacq.py +++ b/src/odemis/acq/stitching/_tiledacq.py @@ -43,8 +43,7 @@ SpectrumStream, FluoStream, MultipleDetectorStream, util, executeAsyncTask, \ CLStream from odemis.model import DataArray -from odemis.util import dataio as udataio, img, linalg -from odemis.util import rect_intersect +from odemis.util import dataio as udataio, img, linalg, rect_intersect from odemis.util.img import assembleZCube from odemis.util.linalg import generate_triangulation_points from odemis.util.raster import point_in_polygon diff --git a/src/odemis/gui/comp/overlay/rectangle.py b/src/odemis/gui/comp/overlay/rectangle.py index e4e3f277be..ed69cebaab 100644 --- a/src/odemis/gui/comp/overlay/rectangle.py +++ b/src/odemis/gui/comp/overlay/rectangle.py @@ -86,7 +86,7 @@ class RectangleOverlay(EditableShape, RectangleEditingMixin, WorldOverlay): The selected rectangle can be manipulated by dragging its edges or rotating it. """ - def __init__(self, cnvs, colour=gui.SELECTION_COLOUR, center=(0, 0)): + def __init__(self, cnvs, colour=gui.SELECTION_COLOUR, center=(0, 0), show_selection_points: bool = True): EditableShape.__init__(self, cnvs) RectangleEditingMixin.__init__(self, colour, center) # RectangleOverlay has attributes and methods of the "WorldOverlay" interface. @@ -151,6 +151,9 @@ def __init__(self, cnvs, colour=gui.SELECTION_COLOUR, center=(0, 0)): background=None ) + # draw selection points on shape + self._draw_selection_points = show_selection_points + def to_dict(self) -> dict: """ Convert the necessary class attributes and its values to a dict. @@ -533,7 +536,9 @@ def draw(self, ctx, shift=(0, 0), scale=1.0, line_width=4, dash=True): ctx.stroke() self._calc_edges() - self.draw_edges(ctx, b_point1, b_point2, b_point3, b_point4) + + if self._draw_selection_points: + self.draw_edges(ctx, b_point1, b_point2, b_point3, b_point4) # Side labels if self.selected.value: diff --git a/src/odemis/gui/conf/data.py b/src/odemis/gui/conf/data.py index 8513b08588..b47fc4854c 100644 --- a/src/odemis/gui/conf/data.py +++ b/src/odemis/gui/conf/data.py @@ -167,7 +167,11 @@ "event": wx.EVT_SCROLL_CHANGED # only affects when it's a slider }), ("probeCurrent", { - "event": wx.EVT_SCROLL_CHANGED # only affects when it's a slider + "label": "Beam Current", + "control_type": odemis.gui.CONTROL_SLIDER, + "type": "float", + "scale": "linear", + "event": wx.EVT_SCROLL_CHANGED }), ("spotSize", { "tooltip": "Electron-beam Spot size", @@ -235,6 +239,52 @@ "control_type": odemis.gui.CONTROL_NONE, }), )), + "ion-beam": + OrderedDict(( + ("accelVoltage", { + "label": "Accel. Voltage", + "tooltip": "Accelerating voltage", + "event": wx.EVT_SCROLL_CHANGED # only affects when it's a slider + }), + ("probeCurrent", { + "label": "Beam Current", + "control_type": odemis.gui.CONTROL_SLIDER, + "type": "float", + "scale": "linear", + "event": wx.EVT_SCROLL_CHANGED + }), + ("resolution", { + "label": "Resolution", + "control_type": odemis.gui.CONTROL_COMBO, + "tooltip": "Number of pixels in the image", + "choices": None, + "accuracy": None, # never simplify the numbers + }), + ("dwellTime", { + "control_type": odemis.gui.CONTROL_SLIDER, + "tooltip": "Pixel integration time", + # "range": (1e-9, 1), + # "scale": "log", + "type": "float", + "accuracy": 3, + "event": wx.EVT_SCROLL_CHANGED + }), + ("horizontalFoV", { + "label": "HFW", + "tooltip": "Horizontal Field Width", + "control_type": odemis.gui.CONTROL_COMBO, + "choices": util.hfw_choices, + # "accuracy": 3, + }), + ("scale", { + # same as binning (but accepts floats) + "control_type": odemis.gui.CONTROL_NONE, + # "tooltip": "Pixel resolution preset", + # means will make sure both dimensions are treated as one + # "choices": util.binning_1d_from_2d, + }), + + )), "ebeam-blanker": OrderedDict(( ("period", { @@ -594,6 +644,27 @@ ("brightness", { "control_type": odemis.gui.CONTROL_SLIDER, }), + ("mode", { + "label": "Detector Mode", + }), + ("type", { + "label": "Detector Type", + }), + )), + "se-detector-ion": + OrderedDict(( + ("brightness", { + "label": "Brightness", + }), + ("contrast", { + "label": "Contrast", + }), + ("mode", { + "label": "Detector Mode", + }), + ("type", { + "label": "Detector Type", + }), )), } @@ -771,6 +842,20 @@ }, }, }, + "meteor" : { + "e-beam": { + "scale": { + "control_type": odemis.gui.CONTROL_NONE, + }, + "resolution": { + "label": "Resolution", + "control_type": odemis.gui.CONTROL_COMBO, + "tooltip": "Number of pixels in the image", + "choices": None, + "accuracy": None, # never simplify the numbers + } + }, + } } # The sparc-simplex is identical to the sparc diff --git a/src/odemis/gui/cont/stream_bar.py b/src/odemis/gui/cont/stream_bar.py index 9e4cd1e6e9..98c32a7f78 100644 --- a/src/odemis/gui/cont/stream_bar.py +++ b/src/odemis/gui/cont/stream_bar.py @@ -25,9 +25,9 @@ import gc import logging import os -from collections import OrderedDict import threading import time +from collections import OrderedDict import wx @@ -35,12 +35,13 @@ import odemis.gui.conf.file import odemis.gui.model as guimodel from odemis import model +from odemis.acq.stream import FastEMOverviewStream, StaticSEMStream, StaticStream from odemis.acq.stream_settings import StreamSettingsConfig -from odemis.acq.stream import StaticStream, FastEMOverviewStream from odemis.gui.conf.data import get_local_vas from odemis.gui.cont.stream import StreamController from odemis.gui.model import TOOL_NONE, TOOL_SPOT from odemis.gui.util import call_in_wx_main +from odemis.util.dataio import data_to_static_streams # There are two kinds of controllers: # * Stream controller: links 1 stream <-> stream panel (cont/stream/StreamPanel) @@ -2185,3 +2186,97 @@ def clear(self, clear_model=True): # Force a check of what can be garbage collected, as some of the streams # could be quite big, that will help to reduce memory pressure. gc.collect() + + +class CryoFIBAcquiredStreamsController(CryoStreamsController): + """ + StreamBarController to display the acquired reference image for automated milling workflow. + The only role of the controller is to display the streams in the + acquired view when the feature is selected. + """ + + def __init__(self, tab_data, feature_view, *args, **kwargs): + """ + feature_view (StreamView): the view to show the feature streams + """ + super().__init__(tab_data, *args, **kwargs) + self._feature_view = feature_view + + tab_data.main.currentFeature.subscribe(self._on_current_feature_changes) + + def showFeatureStream(self, stream) -> StreamController: + """ + Shows an Feature stream (in the Acquired view) + Must be run in the main GUI thread. + """ + self._feature_view.addStream(stream) + sc = self._add_stream_cont(stream, show_panel=True, static=self.static_mode, + view=self._feature_view) + return sc + + @call_in_wx_main + def _on_current_feature_changes(self, feature): + """ + Handle switching the acquired streams appropriate to the current feature + :param feature: (CryoFeature or None) the newly selected current feature + """ + self.clear_feature_streams() + # show the feature streams on the acquired view + + acquired_streams = [] + if feature: + if feature.reference_image is not None: + acquired_streams = data_to_static_streams([feature.reference_image]) + for stream in acquired_streams: + self.showFeatureStream(stream) + self.stream = stream # should only ever be 1 stream + # refit the selected feature in the acquired view + self._view_controller.viewports[3].canvas.fit_view_to_content() + + def clear_feature_streams(self): + """ + Remove from display all feature streams (but leave the overview and live streams) + But DO NOT REMOVE the streams from the model + """ + # Remove the panels, and indirectly it will clear the view + v = self._feature_view + for sc in self.stream_controllers.copy(): + if not isinstance(sc.stream, StaticSEMStream): + logging.warning("Unexpected non static stream: %s", sc.stream) + continue + + self._stream_bar.remove_stream_panel(sc.stream_panel) + if hasattr(v, "removeStream"): + v.removeStream(sc.stream) + if sc in self.stream_controllers: + self.stream_controllers.remove(sc) + + self.stream = None + self._stream_bar.fit_streams() + + # Force a check of what can be garbage collected, as some of the streams + # could be quite big, that will help to reduce memory pressure. + gc.collect() + + def clear(self, clear_model=True): + """ + Remove all the streams, from the GUI (view, stream panels) + Must be called in the main GUI thread. + :param clear_model: unused, but required because of external api + """ + # clear the graphical part + self._stream_bar.clear() + + # Clean up the views + for stream in self._tab_data_model.streams.value: + if isinstance(stream, StaticStream): + for v in (self._feature_view, self._ov_view): + if hasattr(v, "removeStream"): + v.removeStream(stream) + + # Clear the stream controller + self.stream_controllers = [] + + # Force a check of what can be garbage collected, as some of the streams + # could be quite big, that will help to reduce memory pressure. + gc.collect() diff --git a/src/odemis/gui/cont/tabs/localization_tab.py b/src/odemis/gui/cont/tabs/localization_tab.py index ee4eb3e950..1fad7e6e68 100644 --- a/src/odemis/gui/cont/tabs/localization_tab.py +++ b/src/odemis/gui/cont/tabs/localization_tab.py @@ -191,7 +191,7 @@ def __init__(self, name, button, panel, main_frame, main_data): elif self.main_data.role == "meteor": # The stage is in the FM referential, but we care about the stage-bare # in the SEM referential to move between positions - self._allowed_targets = [FM_IMAGING, SEM_IMAGING] + self._allowed_targets = [FM_IMAGING] self._stage = self.tab_data_model.main.stage_bare elif self.main_data.role == "mimas": # Only useful near the active positions: milling (FIB) or FLM diff --git a/src/odemis/gui/main_xrc.py b/src/odemis/gui/main_xrc.py index 4128fb8f12..41a80df371 100644 --- a/src/odemis/gui/main_xrc.py +++ b/src/odemis/gui/main_xrc.py @@ -779,6 +779,7 @@ def __init__(self, parent): self.btn_create_move_feature = xrc.XRCCTRL(self, "btn_create_move_feature") self.cmb_feature_status = xrc.XRCCTRL(self, "cmb_feature_status") self.btn_go_to_feature = xrc.XRCCTRL(self, "btn_go_to_feature") + self.label_feature_z = xrc.XRCCTRL(self, "label_feature_z") self.ctrl_feature_z = xrc.XRCCTRL(self, "ctrl_feature_z") self.btn_use_current_z = xrc.XRCCTRL(self, "btn_use_current_z") self.menu_localization_streams = xrc.XRCCTRL(self, "menu_localization_streams") @@ -9111,7 +9112,7 @@ def __init_resources(): - + #DDDDDD diff --git a/src/odemis/gui/model/__init__.py b/src/odemis/gui/model/__init__.py index a98ea36109..716995c7d0 100644 --- a/src/odemis/gui/model/__init__.py +++ b/src/odemis/gui/model/__init__.py @@ -28,7 +28,7 @@ FixedOverviewView, MicroscopeView, StreamView, View) from .tab_gui_data import (AcquisitionWindowData, ActuatorGUIData, AnalysisGUIData, ChamberGUIData, CryoChamberGUIData, - CryoCorrelationGUIData, CryoGUIData, + CryoCorrelationGUIData, CryoGUIData, CryoFIBSEMGUIData, CryoLocalizationGUIData, EnzelAlignGUIData, FastEMAcquisitionGUIData, FastEMMainTabGUIData, FastEMSetupGUIData, LiveViewGUIData, MicroscopyGUIData, diff --git a/src/odemis/gui/model/_constants.py b/src/odemis/gui/model/_constants.py index e762f30eaf..29288afba9 100644 --- a/src/odemis/gui/model/_constants.py +++ b/src/odemis/gui/model/_constants.py @@ -20,6 +20,9 @@ Odemis. If not, see http://www.gnu.org/licenses/. """ + +from enum import Enum + # The different states of a microscope STATE_OFF = 0 STATE_ON = 1 @@ -96,3 +99,7 @@ CALIBRATION_1 = "Calibration 1" CALIBRATION_2 = "Calibration 2" CALIBRATION_3 = "Calibration 3" + +class AcquisitionMode(Enum): + FLM = 1 + FIBSEM = 2 diff --git a/src/odemis/gui/model/stream_view.py b/src/odemis/gui/model/stream_view.py index d3cdc7580f..d8c7026fe0 100644 --- a/src/odemis/gui/model/stream_view.py +++ b/src/odemis/gui/model/stream_view.py @@ -25,6 +25,8 @@ import queue import threading import time +from concurrent.futures import Future +from typing import Dict, Optional, Tuple from odemis import model from odemis.acq.stream import DataProjection, RGBSpatialProjection, Stream, StreamTree @@ -431,7 +433,7 @@ def moveStageToView(self): shift = (view_pos[0] - prev_pos["x"], view_pos[1] - prev_pos["y"]) return self.moveStageBy(shift) - def moveStageTo(self, pos): + def moveStageTo(self, pos: Tuple[float, float]) -> Optional[Future]: """ Request an absolute move of the stage to a given position @@ -450,7 +452,7 @@ def moveStageTo(self, pos): f.add_done_callback(self._on_stage_move_done) return f - def clipToStageLimits(self, pos): + def clipToStageLimits(self, pos: Dict[str, float]) -> Dict[str, float]: """ Clip current position in x/y direction to the maximum allowed stage limits. diff --git a/src/odemis/gui/model/tab_gui_data.py b/src/odemis/gui/model/tab_gui_data.py index 160650e924..68762d499b 100644 --- a/src/odemis/gui/model/tab_gui_data.py +++ b/src/odemis/gui/model/tab_gui_data.py @@ -28,6 +28,7 @@ import odemis.acq.stream as acqstream from odemis import model from odemis.acq.feature import CryoFeature +from odemis.acq.move import FM_IMAGING, SEM_IMAGING from odemis.gui import conf from odemis.gui.conf import get_general_conf from odemis.gui.cont.fastem_project_tree import FastEMTreeNode, NodeType @@ -339,6 +340,8 @@ def __init__(self, main): self.zPos.clip_on_range = True self.streams.subscribe(self._on_stream_change, init=True) + self.view_posture = model.VigilantAttribute(FM_IMAGING) + if main.stigmator: # stigmator should have a "MD_CALIB" containing a dict[float, dict], # where the key is the stigmator angle (rad), and the value contains @@ -386,6 +389,47 @@ def _on_project_path_change(self, _): config.fn_count) +class CryoFIBSEMGUIData(CryoGUIData): + """ Represent an interface used to control the FIBSEM. + It it used for METEOR systems. + """ + + def __init__(self, main): + super().__init__(main) + + # Current tool selected (from the toolbar) + tools = {TOOL_NONE, TOOL_RULER, TOOL_FEATURE} + # Update the tool selection with the new tool list + self.tool.choices = tools + # VA for autofocus procedure mode + self.autofocus_active = BooleanVA(False) + # the streams to acquire among all streams in .streams + self.acquisitionStreams = model.ListVA() + # the static overview map streams, among all streams in .streams + self.overviewStreams = model.ListVA() + # for the filename + config = conf.get_acqui_conf() + self.filename = model.StringVA(create_filename( + config.pj_last_path, config.fn_ptn, + config.last_extension, + config.fn_count)) + self.main.project_path.subscribe(self._on_project_path_change) + + # milling patterns + self.patterns = model.ListVA() + + self.view_posture = model.VigilantAttribute(SEM_IMAGING) + self.is_sem_active_view: bool = False + self.is_fib_active_view: bool = False + + def _on_project_path_change(self, _): + config = conf.get_acqui_conf() + self.filename.value = create_filename( + config.pj_last_path, config.fn_ptn, + config.last_extension, + config.fn_count) + + class CryoCorrelationGUIData(CryoGUIData): """ Represent an interface used to correlate multiple streams together. @@ -488,7 +532,12 @@ def __init__(self, main): self.stage_align_slider_va = model.FloatVA(1e-6) self.show_advaned = model.BooleanVA(False) + self.view_posture = VigilantAttribute(FM_IMAGING) + # self.main.posture_manager.current_posture.subscribe(self._on_posture_change, init=True) # TODO: enable once new pm is merged + def _on_posture_change(self, posture): + # sync the view posture with the current posture + self.view_posture.value = posture class AnalysisGUIData(MicroscopyGUIData): """ diff --git a/src/odemis/gui/win/acquisition.py b/src/odemis/gui/win/acquisition.py index 9669035399..3a2bae2b55 100644 --- a/src/odemis/gui/win/acquisition.py +++ b/src/odemis/gui/win/acquisition.py @@ -27,7 +27,7 @@ import os.path from builtins import str from concurrent.futures._base import CancelledError -from typing import List, Tuple +from typing import List, Tuple, Optional import odemis.gui.model as guimodel import wx @@ -1405,18 +1405,49 @@ def ShowChamberFileDialog(parent, projectname): fn = dialog.GetFilename() return os.path.join(path, fn) -def LoadProjectFileDialog(parent, projectname): +def LoadProjectFileDialog( + parent: wx.Frame, + projectname: str, + message: str = "Choose a project directory to load", +) -> Optional[str]: """ - parent (wxframe): parent window - projectname (string): project name to propose by default - return (string or none): the project directory name to load (or the none if the user cancelled) + :param parent (wx.Frame): parent window + :param projectname (string): project name to propose by default + :param message (string): message to display in the dialog + :return (string or none): the project directory name to load (or the none if the user cancelled) """ # current project name - dialog = wx.DirDialog(parent, - message="Choose a project directory to load", - defaultPath=projectname, - style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST, - ) + dialog = wx.DirDialog( + parent, + message=message, + defaultPath=projectname, + style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST, + ) + + # Show the dialog and check whether is was accepted or cancelled + if dialog.ShowModal() != wx.ID_OK: + return None + + # project path have been selected... + return dialog.GetPath() + +def SelectFileDialog( + parent: wx.Frame, + message: str, + default_path: str, +) -> Optional[str]: + """ + :param parent (wx.Frame): parent window + :param message (string): message to display in the dialog + :param default_path (string): default path to open the dialog + :return (string or none): the selected file name (or the none if the user cancelled) + """ + dialog = wx.FileDialog( + parent, + message=message, + defaultDir=default_path, + style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST, + ) # Show the dialog and check whether is was accepted or cancelled if dialog.ShowModal() != wx.ID_OK: diff --git a/src/odemis/gui/xmlh/resources/panel_tab_localization.xrc b/src/odemis/gui/xmlh/resources/panel_tab_localization.xrc index 52ae78e38e..fcde21ea0d 100644 --- a/src/odemis/gui/xmlh/resources/panel_tab_localization.xrc +++ b/src/odemis/gui/xmlh/resources/panel_tab_localization.xrc @@ -314,7 +314,7 @@ - + #DDDDDD diff --git a/src/odemis/model/_metadata.py b/src/odemis/model/_metadata.py index 8d32513db5..20566f8b11 100644 --- a/src/odemis/model/_metadata.py +++ b/src/odemis/model/_metadata.py @@ -88,7 +88,7 @@ # FIB-SEM metadata # position of the stage (in m or rad) for each axis in the chamber (raw hardware values) MD_STAGE_POSITION_RAW = "Stage position raw" # dict of str -> float, -MD_SAMPLE_PRE_TILT = "pre-tilt" # (rad) pre-tilt of the sample stage / shuttle (tilt) +MD_SAMPLE_PRE_TILT = "Sample pre-tilt" # (rad) pre-tilt of the sample stage / shuttle (tilt) MD_STREAK_TIMERANGE = "Streak Time Range" # (s) Time range for one streak/sweep MD_STREAK_MCPGAIN = "Streak MCP Gain" # (int) Multiplying gain for microchannel plate @@ -228,6 +228,8 @@ MD_FM_IMAGING_RANGE = "FM imaging range" # dict str → [float, float] defining the volume of the FM imaging area, along x, y and z axes. MD_FAV_FM_POS_ACTIVE = "Favourite FM position active" # dict str->float representing the position required for FM imaging MD_FAV_SEM_POS_ACTIVE = "Favourite SEM position active" # dict -> float representing the position required for SEM imaging +MD_FAV_FIB_POS_ACTIVE = "Favourite FIB position active" # dict -> float representing the position required for FIB imaging +MD_FAV_MILL_POS_ACTIVE = "Favourite Milling position active" # dict -> float representing the position required for milling # The following metadata is used to store the destination components of the # specific known positions for the actuators.