diff --git a/src/navigate/controller/configurator.py b/src/navigate/controller/configurator.py index 8231510e1..10385c96c 100644 --- a/src/navigate/controller/configurator.py +++ b/src/navigate/controller/configurator.py @@ -33,6 +33,7 @@ import tkinter as tk from time import sleep from tkinter import filedialog, messagebox +from typing import Optional # Third Party Imports @@ -87,22 +88,17 @@ def __init__(self, root: tk.Tk, splash_screen): self.microscope_id = 0 self.create_config_window(0) - print( - "WARNING: The Configuration Assistant is not fully implemented. " - "Users are still required to manually configure their system." - ) - - def on_cancel(self): + def on_cancel(self) -> None: """Closes the window and exits the program""" self.root.destroy() exit() - def add_microscope(self): + def add_microscope(self) -> None: """Add a new microscope tab""" self.microscope_id += 1 self.create_config_window(self.microscope_id) - def delete_microscopes(self): + def delete_microscopes(self) -> None: """Delete all microscopes""" # delete microscopes for tab_id in self.view.microscope_window.tabs(): @@ -110,12 +106,12 @@ def delete_microscopes(self): self.view.microscope_window.tab_list = [] self.microscope_id = 0 - def new_configuration(self): + def new_configuration(self) -> None: """Create new configurations""" self.delete_microscopes() self.create_config_window(self.microscope_id) - def save(self): + def save(self) -> None: """Save configuration file""" def set_value(temp_dict, key_list, value): @@ -225,7 +221,7 @@ def set_value(temp_dict, key_list, value): f". Please double check!", ) - def write_to_yaml(self, config, filename): + def write_to_yaml(self, config: dict, filename: str) -> None: """write yaml file Parameters @@ -256,8 +252,14 @@ def write_func(prefix, config_dict, f): f.write("microscopes:\n") write_func(" ", config, f) - def create_config_window(self, id): - """Creates the configuration window tabs.""" + def create_config_window(self, id: int) -> None: + """Creates the configuration window tabs. + + Parameters + ---------- + id : int + The id of the microscope + """ tab_name = "Microscope-" + str(id) microscope_tab = MicroscopeTab( @@ -285,11 +287,26 @@ def create_config_window(self, id): sticky=tk.NSEW, ) - def load_configuration(self): + def load_configuration(self) -> None: """Load configuration""" - def get_widget_value(name, value_dict): - """Get the value from a dict""" + def get_widget_value(name, value_dict) -> Optional[str]: + """Get the value from a dict + + Parameters + ---------- + name: str + key name + value_dict: dict + value dictionary + + Returns + ------- + value : Optional[str] + + - The value of the key if it exists + - None if the key does not exist + """ value = value_dict for key in name.split("/"): if key.strip() == "": @@ -300,7 +317,7 @@ def get_widget_value(name, value_dict): return value def get_widgets_value(widgets, value_dict): - """Get all key-value from valude_dict, keys are from widgets""" + """Get all key-value from value_dict, keys are from widgets""" temp = {} for key in widgets: if key == "frame_config": @@ -327,7 +344,7 @@ def get_widgets_value(widgets, value_dict): return temp def build_widgets_value(widgets, value_dict): - """According to valude_dict build values for widgets""" + """According to value_dict build values for widgets""" if widgets is None or value_dict is None: return [None] result = [] diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index b90a200ea..49fa80efb 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -604,8 +604,8 @@ def set_mode_of_sub(self, mode): Parameters __________ - mode : string - string = 'live', 'stop' + mode : str + The string = 'live', 'stop' """ self.channels_tab_controller.set_mode(mode) self.camera_view_controller.set_mode(mode) @@ -634,8 +634,9 @@ def execute(self, command, *args): Parameters __________ command : string - string = 'stage', 'stop_stage', 'move_stage_and_update_info', - args* : function-specific passes. + The string includes 'stage', 'stop_stage', 'move_stage_and_update_info', + args* : Iterable. + Function-specific passes """ if command == "joystick_toggle": @@ -1163,10 +1164,10 @@ def display_images( camera_view_controller : CameraViewController Camera View Controller object. show_img_pipe : multiprocessing.Pipe - Pipe for showing images. + The pipe for showing images. data_buffer : SharedNDArray Pre-allocated shared memory array. - Size dictated by x_pixels, y_pixels, an number_of_frames in + Size dictated by x_pixels, y_pixels, and number_of_frames in configuration file. """ camera_view_controller.initialize_non_live_display( @@ -1379,8 +1380,8 @@ def register_event_listener(self, event_name, event_handler): ---------- event_name : string Name of the event. - event_handler : function - Function to handle the event. + event_handler : callable + The function to handle the event. """ self.event_listeners[event_name] = event_handler diff --git a/src/navigate/controller/sub_controllers/camera_view.py b/src/navigate/controller/sub_controllers/camera_view.py index 8f48d40e0..425f2a269 100644 --- a/src/navigate/controller/sub_controllers/camera_view.py +++ b/src/navigate/controller/sub_controllers/camera_view.py @@ -33,20 +33,21 @@ # Standard Library Imports import platform import tkinter as tk +from tkinter import messagebox import logging import threading -from typing import Dict +from typing import Dict, Optional import tempfile import os import time +import abc +import copy # Third Party Imports import cv2 from PIL import Image, ImageTk import matplotlib.pyplot as plt import numpy as np -import copy -import abc # Local Imports from navigate.controller.sub_controllers.gui import GUIController @@ -117,6 +118,9 @@ def __init__(self, view, parent_controller=None): #: numpy.ndarray: The variance map. self._variance = None + #: str: The imaging mode. + self.image_mode = None + #: bool: The flag for the display of the cross-hair. self.apply_cross_hair = True @@ -520,17 +524,10 @@ def initialize_non_live_display(self, microscope_state, camera_parameters): self.is_displaying_image.value = False self.image_count = 0 # was image_counter self.slice_index = 0 - self.image_mode = microscope_state["image_mode"] self.stack_cycling_mode = microscope_state["stack_cycling_mode"] self.number_of_channels = int(microscope_state["selected_channels"]) - - self.selected_channels = [] - for channel_name, channel_data in microscope_state["channels"].items(): - if channel_data["is_selected"]: - channel_idx = channel_name.split("_")[-1] - self.selected_channels.append(f"CH{channel_idx}") - + self.get_selected_channels(microscope_state) self.number_of_slices = int(microscope_state["number_z_steps"]) self.total_images_per_volume = self.number_of_channels * self.number_of_slices self.original_image_width = int(camera_parameters["img_x_pixels"]) @@ -552,6 +549,25 @@ def initialize_non_live_display(self, microscope_state, camera_parameters): self.update_canvas_size() self.reset_display(False, False) + def get_selected_channels(self, microscope_state: Optional[dict] = None) -> None: + """Get the selected microscope channels from the MicroscopeState. + + Parameters + ---------- + microscope_state : Optional[dict] + The microscope state dictionary object. + """ + if microscope_state is None: + microscope_state = self.parent_controller.configuration["experiment"][ + "MicroscopeState" + ] + + self.selected_channels = [] + for channel_name, channel_data in microscope_state["channels"].items(): + if channel_data["is_selected"]: + channel_idx = channel_name.split("_")[-1] + self.selected_channels.append(f"CH{channel_idx}") + def reset_display(self, display_flag=True, reset_crosshair=True): """Set the display back to the original digital zoom. @@ -685,7 +701,7 @@ def move_stage(self): command = "move_stage_and_update_info" self.parent_controller.execute(command, stage_position) else: - tk.messagebox.showerror( + messagebox.showerror( title="Warning", message="Can't move to there! Invalid stage position!" ) @@ -716,7 +732,7 @@ def digital_zoom(self): Returns ------- - image : np.array + image : np.ndarray Image after digital zoom applied """ self.zoom_rect = self.zoom_rect - self.zoom_offset @@ -916,14 +932,8 @@ def process_image(self): image = self.apply_lut(image) self.populate_image(image) - def left_click(self, *args): - """Toggles cross-hair on image upon left click event. - - Parameters - ---------- - args : tuple - Arguments. - """ + def left_click(self, *_): + """Toggles cross-hair on image upon left click event.""" if self.image is not None: self.apply_cross_hair = not self.apply_cross_hair self.process_image() @@ -1135,6 +1145,7 @@ def initialize_non_live_display(self, microscope_state, camera_parameters): super().initialize_non_live_display(microscope_state, camera_parameters) self.update_display_state() self.view.live_frame.channel["values"] = self.selected_channels + self.view.live_frame.channel.set(self.selected_channels[0]) self.spooled_images = SpooledImageLoader( channels=self.number_of_channels, size_y=self.original_image_height, @@ -1150,14 +1161,8 @@ def update_snr(self): self._offset, self._variance = copy.deepcopy(off), copy.deepcopy(var) self.image_palette["SNR"].grid(row=3, column=0, sticky=tk.NSEW, pady=3) - def slider_update(self, *args): - """Updates the image when the slider is moved. - - Parameters - ---------- - args : tuple - Arguments. - """ + def slider_update(self, *_): + """Updates the image when the slider is moved.""" slider_index = self.view.slider.get() channel_index = self.view.live_frame.channel.get() @@ -1177,16 +1182,11 @@ def slider_update(self, *args): with self.is_displaying_image as is_displaying_image: is_displaying_image.value = False - def update_display_state(self, *args): + def update_display_state(self, *_): """Image Display Combobox Called. Sets self.display_state to desired display format. Toggles state of slider widget. Sets number of positions. - - Parameters - ---------- - args : tuple - Arguments. """ if self.number_of_slices == 0: return @@ -1205,11 +1205,12 @@ def update_display_state(self, *args): ) self.view.slider.configure(state="normal") self.view.slider.grid() - self.view.live_frame.channel.configure(state="normal") + self.view.live_frame.channel.state(["!disabled", "readonly"]) + # was normal if self.view.live_frame.channel.get() not in self.selected_channels: self.view.live_frame.channel.set(self.selected_channels[0]) - def initialize(self, name, data): + def initialize(self, name: str, data: list): """Sets widgets based on data given from main controller/config. Parameters @@ -1345,13 +1346,13 @@ def display_image(self, image): is_displaying_image.value = False logger.info(f"Displaying image took {time.time() - start_time:.4f} seconds") - def set_mask_color_table(self, colors): + def set_mask_color_table(self, colors: list): """Set up segmentation mask color table Parameters ---------- colors : list - List of colors to use for the segmentation mask + The list of colors to use for the segmentation mask """ self.mask_color_table = np.zeros((256, 1, 3), dtype=np.uint8) self.mask_color_table[0] = [0, 0, 0] @@ -1370,8 +1371,8 @@ def display_mask(self, mask): Parameters ---------- - mask : np.array - Segmentation mask to display) + mask : np.ndarray + Segmentation mask to display """ self.ilastik_seg_mask = cv2.applyColorMap(mask, self.mask_color_table) self.ilastik_mask_ready_lock.release() @@ -1400,6 +1401,15 @@ def __init__(self, view, parent_controller=None): #: tkinter.Canvas: The tkinter canvas that displays the image. self.view = view + #: int: The image height. + self.XY_image_height = None + + #: int: The image width. + self.XY_image_width = None + + #: int: Scaling factor for ratio of lateral and axial dimensions. + self.Z_image_value = None + #: np.ndarray: The image data. self.image = None @@ -1427,7 +1437,7 @@ def __init__(self, view, parent_controller=None): self.menu.entryconfig("Move Here", state="disabled") self.menu.entryconfig("Mark Position", state="disabled") - def initialize(self, name, data): + def initialize(self, name: str, data: list): """Initialize the MIP view. Sets the min and max intensity values for the image.Disables the min and max @@ -1452,9 +1462,12 @@ def initialize(self, name, data): self.image_palette["Gray"].widget.invoke() self.image_palette["Autoscale"].widget.invoke() self.image_palette["SNR"].grid_remove() + self.render_widgets["perspective"].widget["values"] = ("XY", "ZY", "ZX") self.render_widgets["perspective"].set("XY") - self.render_widgets["channel"].set("CH1") + + self.get_selected_channels() + self.render_widgets["channel"].set(self.selected_channels[0]) # event binding self.render_widgets["perspective"].get_variable().trace_add( @@ -1514,17 +1527,23 @@ def get_mip_image(self): image : numpy.ndarray Image data """ - if self.xy_mip is None: + views = [self.xy_mip, self.zy_mip, self.zx_mip] + if any(view is None for view in views): return None display_mode = self.render_widgets["perspective"].get() - channel_idx = int(self.render_widgets["channel"].get()[2:]) - 1 + channel = self.render_widgets["channel"].get() + if channel in self.selected_channels: + channel_idx = self.selected_channels.index(channel) + else: + return + if display_mode == "XY": image = self.xy_mip[channel_idx] elif display_mode == "ZY": - image = self.zy_mip[channel_idx].T - elif display_mode == "ZX": - image = self.zx_mip[channel_idx] + image = self.zy_mip[channel_idx, :].T + else: + image = self.zx_mip[channel_idx, :] image = self.flip_image(image) # map the image to canvas size() @@ -1542,11 +1561,12 @@ def initialize_non_live_display(self, microscope_state, camera_parameters): Camera parameters. """ super().initialize_non_live_display(microscope_state, camera_parameters) + self.render_widgets["channel"].set(self.selected_channels[0]) self.perspective = self.render_widgets["perspective"].get() self.XY_image_width = self.original_image_width self.XY_image_height = self.original_image_height - # in microns z_range = microscope_state["abs_z_end"] - microscope_state["abs_z_start"] + # TODO: may stretch by the value of binning. self.Z_image_value = int( self.XY_image_width * camera_parameters["fov_x"] / z_range @@ -1594,14 +1614,9 @@ def display_image(self, image): with self.is_displaying_image as is_displaying_image: is_displaying_image.value = False - def display_mip_image(self, *args): - """Display MIP image in non-live view. + def display_mip_image(self, *_): + """Display MIP image in non-live view.""" - Parameters - ---------- - args : tuple - Arguments. - """ if self.perspective != self.render_widgets["perspective"].get(): self.update_perspective() if self.mode != "stop": @@ -1610,16 +1625,19 @@ def display_mip_image(self, *args): if self.image is not None: self.process_image() - def update_perspective(self, *args, display=False): - """Update the perspective of the image. + def update_perspective(self): + """Update the perspective of the image.""" + attribute_list = [ + "XY_image_width", + "XY_image_height", + "Z_image_value", + ] + if any( + not hasattr(self, attr) or getattr(self, attr) is None + for attr in attribute_list + ): + return - Parameters - ---------- - args : tuple - Arguments. - display : bool - Flag to display the image. - """ display_mode = self.render_widgets["perspective"].get() self.perspective = display_mode if display_mode == "XY": diff --git a/src/navigate/view/configurator_application_window.py b/src/navigate/view/configurator_application_window.py index ca95800de..b70dea90d 100644 --- a/src/navigate/view/configurator_application_window.py +++ b/src/navigate/view/configurator_application_window.py @@ -33,6 +33,7 @@ from tkinter import ttk, simpledialog import logging from pathlib import Path +from typing import Optional, Callable # Third Party Imports @@ -396,6 +397,8 @@ def create_hardware_widgets(self, hardware_widgets, frame, direction="vertical") self.values_dict[k] = v[3] temp = list(v[3].keys()) widget.config(values=temp) + widget.state(["!disabled", "readonly"]) + if v[2] == "bool": widget.set(str(temp[-1])) else: @@ -443,7 +446,7 @@ def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwarg parent : frame parent frame to put widgets widgets_value : dict - valude dict of widgets + value_dict of widgets *args Variable length argument list. **kwargs @@ -465,9 +468,9 @@ def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwarg temp_ref = widgets["frame_config"].get("ref", None) direction = widgets["frame_config"].get("direction", "vertical") if collapsible: - self.foldAllFrames() + self.fold_all_frames() frame = CollapsibleFrame(parent=parent, title=title) - # only display one callapsible frame at a time + # only display one collapsible frame at a time frame.label.bind("", self.create_toggle_function(frame)) else: frame = ttk.Frame(parent) @@ -493,12 +496,12 @@ def build_widgets(self, widgets, *args, parent=None, widgets_value=None, **kwarg except tk._tkinter.TclError: pass - def foldAllFrames(self, except_frame=None): + def fold_all_frames(self, except_frame: Optional[tk.Frame] = None) -> None: """Fold all collapsible frames except one frame Parameters ---------- - except_frame : tk.Frame + except_frame : Optional[tk.Frame] the unfold frame """ for child in self.hardware_frame.winfo_children(): @@ -508,7 +511,7 @@ def foldAllFrames(self, except_frame=None): if isinstance(child, CollapsibleFrame) and child is not except_frame: child.fold() - def create_toggle_function(self, frame): + def create_toggle_function(self, frame: tk.Frame) -> Callable[..., None]: """Toggle collapsible frame Parameters @@ -518,12 +521,14 @@ def create_toggle_function(self, frame): """ def func(event): - self.foldAllFrames(frame) + self.fold_all_frames(frame) frame.toggle_visibility() return func - def build_event_handler(self, hardware_widgets, key, frame, frame_id): + def build_event_handler( + self, hardware_widgets: dict, key: str, frame: tk.Frame, frame_id: int + ) -> Callable[..., None]: """Build button event handler Parameters @@ -536,6 +541,11 @@ def build_event_handler(self, hardware_widgets, key, frame, frame_id): the frame to put/delete widgets frame_id : int index of the frame + + Returns + ------- + func : Callable + event handler function """ def func(*args, **kwargs): @@ -559,7 +569,7 @@ def func(*args, **kwargs): ref=v[2].get("ref", None), direction=v[2].get("direction", "vertical"), ) - # collaps other frame + # collapse other frame elif v[2].get("delete", False): frame.grid_remove() self.variables_list[frame_id - self.row_offset] = None diff --git a/src/navigate/view/main_window_content/display_notebook.py b/src/navigate/view/main_window_content/display_notebook.py index c62a3d177..ead381506 100644 --- a/src/navigate/view/main_window_content/display_notebook.py +++ b/src/navigate/view/main_window_content/display_notebook.py @@ -313,22 +313,18 @@ def __init__( self.live_var = tk.StringVar() #: ttk.Combobox: The combobox that holds the live display functionality. - self.live = ttk.Combobox( - self, textvariable=self.live_var, state="readonly", width=6 - ) + self.live = ttk.Combobox(self, textvariable=self.live_var, width=6) self.live["values"] = ("Live", "Slice") self.live.set("Live") self.live.grid(row=0, column=0) - self.live.state = "readonly" + self.live.state(["!disabled", "readonly"]) self.channel_var = tk.StringVar() - self.channel = ttk.Combobox( - self, textvariable=self.channel_var, state="readonly", width=6 - ) + self.channel = ttk.Combobox(self, textvariable=self.channel_var, width=6) self.channel["values"] = "CH1" self.channel.set("CH1") self.channel.grid(row=1, column=0) - self.channel.state = "readonly" + self.channel.state(["disabled", "readonly"]) class MipRenderFrame(ttk.Labelframe, CommonMethods): @@ -377,7 +373,8 @@ def __init__( input_args={"width": 5}, ), } - + self.inputs["perspective"].widget.state(["!disabled", "readonly"]) + self.inputs["channel"].widget.state(["!disabled", "readonly"]) self.inputs["perspective"].grid(row=0, column=0, sticky=tk.EW, padx=3, pady=3) self.inputs["channel"].grid(row=1, column=0, sticky=tk.EW, padx=3, pady=3) self.columnconfigure(0, weight=1) diff --git a/src/navigate/view/popups/acquire_popup.py b/src/navigate/view/popups/acquire_popup.py index e5f7dbd12..294fbe1a2 100644 --- a/src/navigate/view/popups/acquire_popup.py +++ b/src/navigate/view/popups/acquire_popup.py @@ -142,6 +142,7 @@ def __init__(self, root, *args, **kwargs): input_var=tk.StringVar(), label_args={"padding": [0, 0, 30, 0]}, ) + self.inputs[entry_names[i]].widget.state(["!disabled", "readonly"]) self.inputs[entry_names[i]].set_values(tuple(FILE_TYPES)) self.inputs[entry_names[i]].set("TIFF") @@ -153,6 +154,7 @@ def __init__(self, root, *args, **kwargs): input_var=tk.StringVar(), label_args={"padding": [0, 0, 36, 0]}, ) + self.inputs[entry_names[i]].widget.state(["!disabled", "readonly"]) self.inputs[entry_names[i]].set_values( ("BABB", "Water", "CUBIC", "CLARITY", "uDISCO", "eFLASH") )