From b196a89f6d03509f2877acf6c548fdbf1b103e12 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 11:31:02 -0700 Subject: [PATCH 01/23] add the ability to start a new saving session --- src/navigate/model/features/image_writer.py | 177 +++++++++++--------- 1 file changed, 102 insertions(+), 75 deletions(-) diff --git a/src/navigate/model/features/image_writer.py b/src/navigate/model/features/image_writer.py index b58653cc7..0ff039876 100644 --- a/src/navigate/model/features/image_writer.py +++ b/src/navigate/model/features/image_writer.py @@ -36,6 +36,7 @@ import logging import shutil import time +from datetime import datetime # Third Party Imports import numpy as np @@ -113,91 +114,24 @@ def __init__( #: bool: Is 32 vs 64-bit file format. self.big_tiff = False - # create the save directory if it doesn't already exist - self.save_directory = os.path.join( - self.model.configuration["experiment"]["Saving"]["save_directory"], - self.sub_dir, - ) - logger.info(f"Save Directory: {self.save_directory}") - try: - if not os.path.exists(self.save_directory): - try: - os.makedirs(self.save_directory) - logger.debug(f"Save Directory Created - {self.save_directory}") - except OSError: - logger.debug( - f"Unable to Create Save Directory - {self.save_directory}" - ) - self.model.stop_acquisition = True - self.model.event_queue.put( - "warning", - "Unable to Create Save Directory. Acquisition Terminated", - ) - return - except FileNotFoundError as e: - logger.error(f"Unable to Create Save Directory - {self.save_directory}") - - # create the MIP directory if it doesn't already exist - #: np.ndarray : Maximum intensity projection image. - self.mip = None - - #: str : Directory for saving maximum intensity projection images. - self.mip_directory = os.path.join(self.save_directory, "MIP") - try: - if not os.path.exists(self.mip_directory): - try: - os.makedirs(self.mip_directory) - logger.debug(f"MIP Directory Created - {self.mip_directory}") - except OSError: - logger.debug( - f"Unable to Create MIP Directory - {self.mip_directory}" - ) - self.model.stop_acquisition = True - self.model.event_queue.put( - "warning", - "Unable to create MIP Directory. Acquisition Terminated.", - ) - return - except FileNotFoundError as e: - logger.error("Image Writer: Unable to create MIP directory.") - - # Set up the file name and path in the save directory - #: str : File type for saving data. - self.file_type = self.model.configuration["experiment"]["Saving"]["file_type"] - logger.info(f"Saving Data as File Type: {self.file_type}") - - current_channel = self.model.active_microscope.current_channel - ext = "." + self.file_type.lower().replace(" ", ".").replace("-", ".") - if image_name is None: - image_name = self.generate_image_name(current_channel, ext=ext) - file_name = os.path.join(self.save_directory, image_name) - - # Initialize data source, pointing to the new file name - #: navigate.model.data_sources.DataSource : Data source for saving data to disk. - self.data_source = data_sources.get_data_source(self.file_type)( - file_name=file_name - ) + #: dict: Saving config + self.saving_config = saving_config - # Pass experiment and configuration to metadata - self.data_source.set_metadata_from_configuration_experiment( - self.model.configuration, microscope_name - ) - - self.data_source.set_metadata(saving_config) - - # Make sure that there is enough disk space to save the data. - self.calculate_and_check_disk_space() + #: DataSource: Data source + self.data_source = None # camera flip flags - microscope_name = self.model.active_microscope_name camera_config = self.model.configuration["configuration"]["microscopes"][ - microscope_name + self.microscope_name ]["camera"] self.flip_flags = { "x": camera_config.get("flip_x", False), "y": camera_config.get("flip_y", False), } + # initialize saving + self.initialize_saving(sub_dir) + def save_image(self, frame_ids): """Save the data to disk. @@ -357,3 +291,96 @@ def calculate_and_check_disk_space(self): logger.info("Big-TIFF Format Selected.") else: self.data_source.set_bigtiff(False) + + def get_saving_file_name(self, sub_dir): + self.sub_dir = sub_dir + # create the save directory if it doesn't already exist + self.save_directory = os.path.join( + self.model.configuration["experiment"]["Saving"]["save_directory"], + self.sub_dir, + ) + logger.info(f"Save Directory: {self.save_directory}") + try: + if not os.path.exists(self.save_directory): + try: + os.makedirs(self.save_directory) + logger.debug(f"Save Directory Created - {self.save_directory}") + except OSError: + logger.debug( + f"Unable to Create Save Directory - {self.save_directory}" + ) + self.model.stop_acquisition = True + self.model.event_queue.put( + "warning", + "Unable to Create Save Directory. Acquisition Terminated", + ) + return + except FileNotFoundError as e: + logger.error(f"Unable to Create Save Directory - {self.save_directory}") + + # Set up the file name and path in the save directory + #: str : File type for saving data. + self.file_type = self.model.configuration["experiment"]["Saving"]["file_type"] + logger.info(f"Saving Data as File Type: {self.file_type}") + + current_channel = self.model.active_microscope.current_channel + ext = "." + self.file_type.lower().replace(" ", ".").replace("-", ".") + if image_name is None: + image_name = self.generate_image_name(current_channel, ext=ext) + file_name = os.path.join(self.save_directory, image_name) + + if os.path.exists(file_name): + current_time = datetime.now().strftime("%H-%M") + return self.create_saving_directory(f"{sub_dir}-{current_time}") + + return file_name + + def initialize_saving(self, sub_dir=""): + + if self.data_source is not None: + self.data_source.close() + self.data_source = None + + file_name = self.get_saving_file_name(sub_dir) + + # create the MIP directory if it doesn't already exist + #: np.ndarray : Maximum intensity projection image. + self.mip = None + + #: str : Directory for saving maximum intensity projection images. + self.mip_directory = os.path.join(self.save_directory, "MIP") + try: + if not os.path.exists(self.mip_directory): + try: + os.makedirs(self.mip_directory) + logger.debug(f"MIP Directory Created - {self.mip_directory}") + except OSError: + logger.debug( + f"Unable to Create MIP Directory - {self.mip_directory}" + ) + self.model.stop_acquisition = True + self.model.event_queue.put( + "warning", + "Unable to create MIP Directory. Acquisition Terminated.", + ) + return + except FileNotFoundError as e: + logger.error("Image Writer: Unable to create MIP directory.") + + + + # Initialize data source, pointing to the new file name + #: navigate.model.data_sources.DataSource : Data source for saving data to disk. + self.data_source = data_sources.get_data_source(self.file_type)( + file_name=file_name + ) + + # Pass experiment and configuration to metadata + self.data_source.set_metadata_from_configuration_experiment( + self.model.configuration, self.microscope_name + ) + + self.data_source.set_metadata(self.saving_config) + + # Make sure that there is enough disk space to save the data. + self.calculate_and_check_disk_space() From f1224c398b6994a168e123603626f405c0224a21 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 11:31:28 -0700 Subject: [PATCH 02/23] add tiff data reader --- .../model/data_sources/data_source.py | 58 +++++++++- .../model/data_sources/tiff_data_source.py | 102 +++++++++++++++++- 2 files changed, 151 insertions(+), 9 deletions(-) diff --git a/src/navigate/model/data_sources/data_source.py b/src/navigate/model/data_sources/data_source.py index eb7a30777..cfaa67e03 100644 --- a/src/navigate/model/data_sources/data_source.py +++ b/src/navigate/model/data_sources/data_source.py @@ -33,6 +33,7 @@ # Standard Library Imports import logging from typing import Any, Dict +import abc # Third Party Imports import numpy.typing as npt @@ -78,7 +79,10 @@ def __init__(self, file_name: str = "", mode: str = "w") -> None: # str: Mode to open the file in. Can be 'r' or 'w'. self._mode = None - # bool: Has the data source been closed? + #: bool: Is write mode + self._write_mode = False + + #: bool: Has the data source been closed? self._closed = True #: int: Number of bits per pixel. @@ -293,12 +297,14 @@ def _cztp_indices(self, frame_id: int, per_stack: bool = True) -> tuple: c = frame_id % self.shape_c z = (frame_id // self.shape_c) % self.shape_z - t = (frame_id // (self.shape_c * self.shape_z)) % self.shape_t - p = frame_id // (self.shape_c * self.shape_z * self.shape_t) + # NOTE: Uncomment this if we want time to vary faster than positions + # t = (frame_id // (self.shape_c * self.shape_z)) % self.shape_t + # p = frame_id // (self.shape_c * self.shape_z * self.shape_t) + # TODO: current ZStack positions vary faster than time # NOTE: Uncomment this if we want positions to vary faster than time - # t = frame_id // (self.shape_c * self.shape_z * self.positions) - # p = (frame_id // (self.shape_c * self.shape_z)) % self.positions + t = frame_id // (self.shape_c * self.shape_z * self.positions) + p = (frame_id // (self.shape_c * self.shape_z)) % self.positions else: # Timepoint acquisition, only c varies faster than t @@ -388,6 +394,37 @@ def read(self) -> None: """ logger.error("DataSource.read implemented in a derived class.") raise NotImplementedError("Implemented in a derived class.") + + def get_data(self, timepoint: int=0, position: int=0, channel: int=0, z: int=-1, resolution: int=1) -> npt.ArrayLike: + """Get data according to timepoint, position, channel and z-axis id + + Parameters + ---------- + timepoint : int + The timepoint value + position : int + The position id in multi-position table + channel : int + The channel id + z : int + The index of Z in a Z-stack. + Return all z if -1. + resolution : int + values from 1, 2, 4, 8 + + Returns + ------- + data : npt.ArrayLike + Image data + + Raises + ------ + NotImplementedError + If not implemented in a derived class. + """ + logger.error("DataSource.get_data is not implemented in a derived class.") + raise NotImplementedError(f"get_data is not implemented in a derived class {self.__class__}.") + def close(self) -> None: """Clean up any leftover file pointers, etc.""" @@ -397,3 +434,14 @@ def __del__(self): """Destructor""" if not self._closed: self.close() + +class DataReader(metaclass=abc.ABCMeta): + + @abc.abstractmethod + def __init__(self, *args, **kwargs): + pass + + @property + @abc.abstractmethod + def shape(self): + pass diff --git a/src/navigate/model/data_sources/tiff_data_source.py b/src/navigate/model/data_sources/tiff_data_source.py index bf5c01d01..1a3c072bf 100644 --- a/src/navigate/model/data_sources/tiff_data_source.py +++ b/src/navigate/model/data_sources/tiff_data_source.py @@ -34,17 +34,23 @@ import os import uuid from pathlib import Path +import logging # Third Party Imports import tifffile import numpy.typing as npt +from numpy import stack # Local imports -from .data_source import DataSource +from .data_source import DataSource, DataReader from ..metadata_sources.metadata import Metadata from ..metadata_sources.ome_tiff_metadata import OMETIFFMetadata +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + class TiffDataSource(DataSource): """Data source for TIFF files.""" @@ -64,7 +70,6 @@ def __init__( """ #: np.ndarray: Image data self.image = None - self._write_mode = None self._views = [] super().__init__(file_name, mode) @@ -144,6 +149,7 @@ def is_ome(self) -> bool: def read(self) -> None: """Read a tiff file.""" + self.mode = "r" self.image = tifffile.TiffFile(self.file_name) # TODO: Parse metadata @@ -153,6 +159,48 @@ def read(self) -> None: ax = "Z" setattr(self, f"shape_{ax.lower()}", self.data.shape[i]) + def get_data(self, timepoint: int=0, position: int=0, channel: int=0, z: int=-1, resolution: int=1) -> npt.ArrayLike: + """Get data according to timepoint, position, channel and z-axis id + + Parameters + ---------- + timepoint : int + The timepoint value + position : int + The position id in multi-position table + channel : int + The channel id + z : int + The index of Z in a Z-stack. + Return all z if -1. + resolution : int + values from 1, 2, 4, 8 + Not supported for now. + + Returns + ------- + data : npt.ArrayLike + Image data + """ + # TODO: may need to support .tif + file_suffix = ".ome.tiff" if self.is_ome else ".tiff" + filename = os.path.join(self.save_directory, f"Position{position}", f"CH{channel:02d}-{timepoint:06d}{file_suffix}") + + if not os.path.exists(filename): + return None + + self.mode = "r" + + image = tifffile.TiffFile(filename) + if z < 0: + return TiffReader(image) + + z_num = len(image.pages) + if z < z_num: + return image.pages[z].asarray() + + return None + def write(self, data: npt.ArrayLike, **kw) -> None: """Writes 2D image to the data source. @@ -190,7 +238,7 @@ def write(self, data: npt.ArrayLike, **kw) -> None: self.image[c].write(data, description=ome_xml, contiguous=True) else: dx, dy, dz = self.metadata.voxel_size - md = {"spacing": dz, "unit": "um", "axes": "ZYX"} + md = {"spacing": dz, "unit": "um", "axes": "ZYX", "channel": c, "timepoint": self._current_time} self.image[c].write( data, resolution=(1e4 / dx, 1e4 / dy, "CENTIMETER"), @@ -281,7 +329,7 @@ def close(self, internal=False) -> None: if self.image is None: return # internal flag needed to avoid _check_shape call until last file is written - if self._write_mode: + if self.mode == "w": if not internal: self._check_shape(self._current_frame - 1, self.metadata.per_stack) for ch in range(len(self.image)): @@ -302,3 +350,49 @@ def close(self, internal=False) -> None: self.image.close() if not internal: self._closed = True + + +class TiffReader(DataReader): + def __init__(self, tiff_file: tifffile.TiffFile): + self.tiff = tiff_file + + @property + def shape(self): + page_number = len(self.tiff.pages) + + x, y = self.tiff.pages[0].shape + + return (page_number, x, y) + + def __getitem__(self, index): + + if isinstance(index, int): + # Return the entire page as a NumPy array + return self.tiff.pages[index].asarray() + + elif isinstance(index, slice): + # Handle case for slicing all pages + pages = [self.tiff.pages[i].asarray() for i in range(index.start, index.stop)] + return stack(pages, axis=0) + + elif isinstance(index, tuple): + # Check if the first index is an integer (page index) + if isinstance(index[0], int): + page_index = index[0] + if len(index) == 2: + return self.tiff.pages[page_index].asarray()[index[1]] + elif len(index) == 3: + return self.tiff.pages[page_index].asarray()[index[1], index[2]] + elif isinstance(index[0], slice): + if len(index) == 2: + pages = [self.tiff.pages[i].asarray()[index[1]] for i in range(index.start, index.stop)] + + elif len(index) == 3: + pages = [self.tiff.pages[i].asarray()[index[1], index[2]] for i in range(index.start, index.stop)] + return stack(pages, axis=0) + + logger.debug(f"TiffReader: Invalid indexing format. {index}") + return None + + def __array__(self): + return self.tiff.asarray() From 134b201509553bb486fd7d0d3d902753ca6446b5 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 17:54:47 -0700 Subject: [PATCH 03/23] update multipositions as ListProxy --- src/navigate/controller/controller.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index 3245e8914..ad453e439 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -503,7 +503,12 @@ def update_experiment_setting(self): # update multi-positions positions = self.multiposition_tab_controller.get_positions() - self.configuration["experiment"]["MultiPositions"] = positions + update_config_dict( + self.manager, + self.configuration["experiment"], + "MultiPositions", + positions + ) self.configuration["experiment"]["MicroscopeState"][ "multiposition_count" ] = len(positions) From f57ac0a7510a7ae2cf35c0b952279ec8f75f1905 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 17:55:15 -0700 Subject: [PATCH 04/23] update tiff data source and image writer --- src/navigate/model/data_sources/tiff_data_source.py | 5 ++--- src/navigate/model/features/image_writer.py | 13 +++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/navigate/model/data_sources/tiff_data_source.py b/src/navigate/model/data_sources/tiff_data_source.py index 1a3c072bf..76dc924b1 100644 --- a/src/navigate/model/data_sources/tiff_data_source.py +++ b/src/navigate/model/data_sources/tiff_data_source.py @@ -184,12 +184,10 @@ def get_data(self, timepoint: int=0, position: int=0, channel: int=0, z: int=-1, """ # TODO: may need to support .tif file_suffix = ".ome.tiff" if self.is_ome else ".tiff" - filename = os.path.join(self.save_directory, f"Position{position}", f"CH{channel:02d}-{timepoint:06d}{file_suffix}") + filename = os.path.join(self.save_directory, f"Position{position}", f"CH{channel:02d}_{timepoint:06d}{file_suffix}") if not os.path.exists(filename): return None - - self.mode = "r" image = tifffile.TiffFile(filename) if z < 0: @@ -332,6 +330,7 @@ def close(self, internal=False) -> None: if self.mode == "w": if not internal: self._check_shape(self._current_frame - 1, self.metadata.per_stack) + if type(self.image) == list: for ch in range(len(self.image)): self.image[ch].close() if self.is_ome and len(self._views) > 0: diff --git a/src/navigate/model/features/image_writer.py b/src/navigate/model/features/image_writer.py index 0ff039876..50ae6faf7 100644 --- a/src/navigate/model/features/image_writer.py +++ b/src/navigate/model/features/image_writer.py @@ -121,6 +121,8 @@ def __init__( self.data_source = None # camera flip flags + if self.microscope_name is None: + self.microscope_name = self.model.active_microscope_name camera_config = self.model.configuration["configuration"]["microscopes"][ self.microscope_name ]["camera"] @@ -130,7 +132,7 @@ def __init__( } # initialize saving - self.initialize_saving(sub_dir) + self.initialize_saving(sub_dir, image_name) def save_image(self, frame_ids): """Save the data to disk. @@ -292,7 +294,7 @@ def calculate_and_check_disk_space(self): else: self.data_source.set_bigtiff(False) - def get_saving_file_name(self, sub_dir): + def get_saving_file_name(self, sub_dir="", image_name=None): self.sub_dir = sub_dir # create the save directory if it doesn't already exist self.save_directory = os.path.join( @@ -335,13 +337,16 @@ def get_saving_file_name(self, sub_dir): return file_name - def initialize_saving(self, sub_dir=""): + def initialize_saving(self, sub_dir="", image_name=None): if self.data_source is not None: self.data_source.close() self.data_source = None - file_name = self.get_saving_file_name(sub_dir) + self.current_time_point = 0 + + file_name = self.get_saving_file_name(sub_dir, image_name) + print("saving to new file:", file_name) # create the MIP directory if it doesn't already exist #: np.ndarray : Maximum intensity projection image. From f669f7a0a224f8a509118520a205cc8fe954ce39 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 17:55:51 -0700 Subject: [PATCH 05/23] add multiposition option to ZStack --- src/navigate/model/features/common_features.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index d8aac6169..cf117f50c 100644 --- a/src/navigate/model/features/common_features.py +++ b/src/navigate/model/features/common_features.py @@ -912,7 +912,7 @@ class ZStackAcquisition: """ def __init__( - self, model, get_origin=False, saving_flag=False, saving_dir="z-stack" + self, model, get_origin=False, saving_flag=False, saving_dir="z-stack", force_multiposition=False ): """Initialize the ZStackAcquisition class. @@ -980,6 +980,9 @@ def __init__( #: int: The number of channels in the z-stack. self.channels = 1 + #: bool: Force multiposition + self.force_multiposition = force_multiposition + #: ImageWriter: An image writer object for saving z-stack images. self.image_writer = None if saving_flag: @@ -1043,7 +1046,7 @@ def pre_signal_func(self): self.restore_f = pos_dict["f_pos"] # position: x, y, z, theta, f - if bool(microscope_state["is_multiposition"]): + if bool(microscope_state["is_multiposition"]) or self.force_multiposition: self.positions = self.model.configuration["experiment"]["MultiPositions"] else: self.positions = [ @@ -1207,6 +1210,8 @@ def signal_func(self): self.model.resume_data_thread() self.should_pause_data_thread = False + self.model.mark_saving_flags([self.model.frame_id]) + return True def signal_end(self): @@ -1315,7 +1320,6 @@ def in_data_func(self, frame_ids): A list of frame IDs received during data acquisition. """ - self.model.mark_saving_flags(frame_ids) self.received_frames += len(frame_ids) if self.image_writer is not None: self.image_writer.save_image(frame_ids) From 80404dc26d80630227e9b8eed3553ccae8205a43 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 17:57:12 -0700 Subject: [PATCH 06/23] save data before running data node --- src/navigate/model/model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/navigate/model/model.py b/src/navigate/model/model.py index 809f58080..458acf377 100644 --- a/src/navigate/model/model.py +++ b/src/navigate/model/model.py @@ -928,6 +928,10 @@ def run_data_process(self, num_of_frames=0, data_func=None): wait_num = self.camera_wait_iterations + # ImageWriter to save images + if data_func: + data_func(frame_ids) + if hasattr(self, "data_container") and not self.data_container.end_flag: if self.data_container.is_closed: self.logger.info("Data container is closed.") @@ -936,10 +940,6 @@ def run_data_process(self, num_of_frames=0, data_func=None): self.data_container.run(frame_ids) - # ImageWriter to save images - if data_func: - data_func(frame_ids) - # show image self.logger.info(f"Image delivered to controller: {frame_ids[0]}") self.show_img_pipe.send(frame_ids[-1]) From a1db04edec8265d272c2575d78afd253b4968f13 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 11 Oct 2024 18:00:26 -0700 Subject: [PATCH 07/23] 3D volume search feature The feature has been added to a pipeline for tests. The structure works. Need to map the boundaries to positions. --- .../model/analysis/boundary_detect.py | 21 +++++ .../features/feature_related_functions.py | 5 +- src/navigate/model/features/volume_search.py | 84 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/src/navigate/model/analysis/boundary_detect.py b/src/navigate/model/analysis/boundary_detect.py index 7be21d3d9..32d3c2045 100644 --- a/src/navigate/model/analysis/boundary_detect.py +++ b/src/navigate/model/analysis/boundary_detect.py @@ -36,6 +36,10 @@ # Third party imports from skimage import filters from skimage.transform import downscale_local_mean +from skimage.measure import label +from skimage.segmentation import find_boundaries +from scipy.ndimage import binary_fill_holes + import numpy as np import numpy.typing as npt @@ -446,3 +450,20 @@ def dp_shortest_path(start, end, step, offset=-1): result = dp_shortest_path(start, end, step, offset) return result + + +def find_cell_boundary_3d(z_stack_image, threshold_value): + + z, x, y = z_stack_image.shape + binary_images = [] + + for i in range(z): + thresh_img = z_stack_image[i] > threshold_value + binary_images.append(thresh_img.astype(np.uint8)) + + filled_images = binary_fill_holes(binary_images) + cell_labels = label(filled_images) + boundaries = find_boundaries(cell_labels > 0, connectivity=1) + + return boundaries + \ No newline at end of file diff --git a/src/navigate/model/features/feature_related_functions.py b/src/navigate/model/features/feature_related_functions.py index 7dfd0cb6b..8c27148af 100644 --- a/src/navigate/model/features/feature_related_functions.py +++ b/src/navigate/model/features/feature_related_functions.py @@ -56,7 +56,10 @@ ) from navigate.model.features.image_writer import ImageWriter # noqa from navigate.model.features.restful_features import IlastikSegmentation # noqa -from navigate.model.features.volume_search import VolumeSearch # noqa +from navigate.model.features.volume_search import ( + VolumeSearch, # noqa + VolumeSearch3D, # noqa +) from navigate.model.features.remove_empty_tiles import ( DetectTissueInStack, # noqa DetectTissueInStackAndReturn, # noqa diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index 7e6aace23..2a98e3f71 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -35,6 +35,7 @@ find_tissue_boundary_2d, binary_detect, map_boundary, + find_cell_boundary_3d, ) import numpy as np @@ -461,3 +462,86 @@ def end_data_func(self): def cleanup(self): """Cleanup function""" self.has_tissue_queue.put(False) + + +class VolumeSearch3D: + + def __init__( + self, + model, + target_resolution="Nanoscale", + target_zoom="N/A", + threshold_value=462, + analysis_function=None + ): + """Initialize VolumeSearch + + Parameters + ---------- + model : navigate.model.model.Model + Navigate Model + target_resolution : str + Name of microscope to use for tiled imaging of tissue + target_zoom : str + Resolution of microscope (target_resolution) to use for tiled imaging + of tissue + flipx : bool + Flip the direction in which new tiles are added. + flipy : bool + Flip the direction in which new tiles are added. + overlap : float + Value between 0 and 1 indicating percent overlap of tiles. + debug : bool + If True, save debug images to disk. + """ + + #: navigate.model.model.Model: Navigate Model + self.model = model + + #: str: Name of microscope to use for tiled imaging of tissue + self.target_resolution = target_resolution + + #: str: Resolution of microscope (target_resolution) to use for tiled imaging + self.target_zoom = target_zoom + + #: int: threshold value + self.threshold_value = threshold_value + + #: function: analysis function + self.analysis_function = analysis_function if analysis_function else find_cell_boundary_3d + + #: dict: Feature configuration + self.config_table = { + "data": { + "main": self.data_func, + "cleanup": self.cleanup, + } + } + + def data_func(self, frame_ids): + + self.model.logger.debug("Starting 3D Volume Search") + + z_stack_data = self.model.image_writer.data_source.get_data() + # boundaries = self.analysis_function(z_stack_data, self.threshold_value) + + # TODO: map boundaries to positions + # TODO: remove this, it's used for feature pipeline tests. + positions = [[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7]] + + self.model.event_queue.put(("multiposition", positions)) + self.model.configuration["experiment"]["MultiPositions"] = positions + self.model.configuration["experiment"]["MicroscopeState"][ + "multiposition_count" + ] = len(positions) + if len(positions) > 0: + self.model.configuration["experiment"]["MicroscopeState"][ + "is_multiposition" + ] = True + + self.model.image_writer.initialize_saving(sub_dir=str(self.target_resolution)) + + self.model.logger.debug(f"Volume Search 3D completed!") + + def cleanup(self): + pass From 95b1a6820b733e3bca5f2ae37f25a532a3c85c67 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 16 Oct 2024 11:37:40 -0700 Subject: [PATCH 08/23] reorganize features --- .../controller/sub_controllers/menus.py | 2 - .../model/features/common_features.py | 232 --------------- .../features/feature_related_functions.py | 7 +- src/navigate/model/features/update_setting.py | 273 ++++++++++++++++++ src/navigate/model/model.py | 11 - 5 files changed, 278 insertions(+), 247 deletions(-) create mode 100644 src/navigate/model/features/update_setting.py diff --git a/src/navigate/controller/sub_controllers/menus.py b/src/navigate/controller/sub_controllers/menus.py index c389b2918..685b9f629 100644 --- a/src/navigate/controller/sub_controllers/menus.py +++ b/src/navigate/controller/sub_controllers/menus.py @@ -497,8 +497,6 @@ def initialize_menus(self): # add-on features self.feature_list_names = [ "None", - "Switch Resolution", - "Z Stack Acquisition", "Threshold", "Ilastik Segmentation", "Volume Search", diff --git a/src/navigate/model/features/common_features.py b/src/navigate/model/features/common_features.py index cf117f50c..e6bbe37f4 100644 --- a/src/navigate/model/features/common_features.py +++ b/src/navigate/model/features/common_features.py @@ -46,106 +46,6 @@ # Logger Setup p = __name__.split(".")[1] logger = logging.getLogger(p) - -class ChangeResolution: - """ - ChangeResolution class for modifying the resolution mode of a microscope. - - This class provides functionality to change the resolution mode of a microscope by - reconfiguring the microscope settings and updating the active microscope. - - Notes: - ------ - - This class is used to change the resolution mode of a microscope by updating the - microscope settings and configuring the active microscope accordingly. - - - The `resolution_mode` parameter specifies the desired resolution mode, and the - `zoom_value` parameter specifies the zoom value to be set. These parameters can - be adjusted to modify the microscope's configuration. - - - The `ChangeResolution` class is typically used to adapt the microscope's settings - for different imaging requirements during microscopy experiments. - - - The resolution change process involves reconfiguring the microscope, updating the - active microscope instance, and resuming data acquisition. - - - The `config_table` attribute is used to define the configuration for the - resolution change process, including signal acquisition and cleanup steps. - """ - - def __init__(self, model, resolution_mode="high", zoom_value="N/A"): - """Initialize the ChangeResolution class. - - - Parameters: - ---------- - model : MicroscopeModel - The microscope model object used for resolution mode changes. - resolution_mode : str, optional - The desired resolution mode to set for the microscope. Default is "high". - zoom_value : str, optional - The zoom value to set for the microscope. Default is "N/A". - """ - #: MicroscopeModel: The microscope model associated with the resolution change. - self.model = model - - #: dict: A dictionary defining the configuration for the resolution change - self.config_table = { - "signal": {"main": self.signal_func, "cleanup": self.cleanup}, - "node": {"device_related": True}, - } - - #: str: The desired resolution mode to set for the microscope. - self.resolution_mode = resolution_mode - - #: str: The zoom value to set for the microscope. - self.zoom_value = zoom_value - - def signal_func(self): - """Perform actions to change the resolution mode and update the active - microscope. - - This method carries out actions to change the resolution mode of the microscope - by reconfiguring the microscope settings, updating the active microscope, and - resuming data acquisition. - - Returns: - ------- - bool - A boolean value indicating the success of the resolution change process. - """ - # pause data thread - self.model.pause_data_thread() - # end active microscope - self.model.active_microscope.end_acquisition() - # prepare new microscope - self.model.configuration["experiment"]["MicroscopeState"][ - "microscope_name" - ] = self.resolution_mode - self.model.configuration["experiment"]["MicroscopeState"][ - "zoom" - ] = self.zoom_value - self.model.change_resolution(self.resolution_mode) - logger.debug(f"current resolution is {self.resolution_mode}") - logger.debug( - f"current active microscope is {self.model.active_microscope_name}" - ) - # prepare active microscope - waveform_dict = self.model.active_microscope.prepare_acquisition() - self.model.event_queue.put(("waveform", waveform_dict)) - # resume data thread - self.model.resume_data_thread() - return True - - def cleanup(self): - """Perform cleanup actions if needed. - - This method is responsible for performing cleanup actions if required after the - resolution change process. - """ - self.model.resume_data_thread() - - class Snap: """Snap class for capturing data frames using a microscope. @@ -1579,135 +1479,3 @@ def data_func(self, frame_ids): self.model.event_queue.put(("multiposition", table_values)) - -class SetCameraParameters: - """ - SetCameraParameters class for modifying the parameters of a camera. - - This class provides functionality to update the parameters of a camera. - - Notes: - ------ - - This class can set sensor_mode, readout_direction and rolling_shutter_with. - - - If the value of a parameter is None it doesn't update the parameter value. - """ - - def __init__( - self, - model, - microscope_name=None, - sensor_mode="Normal", - readout_direction=None, - rolling_shutter_width=None, - ): - """Initialize the ChangeResolution class. - - - Parameters: - ---------- - model : MicroscopeModel - The microscope model object used for resolution mode changes. - sensor_mode : str, optional - The desired sensor mode to set for the camera. "Normal" or "Light-Sheet" - readout_direction : str, optional - The readout direction to set for the camera. - "Top-to-Bottom", "Bottom-to-Top", "Bidirectional" or "Rev. Bidirectional" - rolling_shutter_width : int, optional - The number of pixels for the rolling shutter. - """ - #: MicroscopeModel: The microscope model associated with the resolution change. - self.model = model - - #: dict: A dictionary defining the configuration for the resolution change - self.config_table = { - "signal": {"main": self.signal_func, "cleanup": self.cleanup}, - "node": {"device_related": True}, - } - #: str: Microscope name - self.microscope_name = microscope_name - - #: str: The desired sensor mode to set for the camera. - self.sensor_mode = sensor_mode - - #: str: The reading direction to set for the microscope. - self.readout_direction = readout_direction - - #: int: The number of pixels for the rolling shutter. - try: - self.rolling_shutter_width = int(rolling_shutter_width) - except (ValueError, TypeError): - self.rolling_shutter_width = None - - def signal_func(self): - """Perform actions to change the resolution mode and update the active - microscope. - - This method carries out actions to change the resolution mode of the microscope - by reconfiguring the microscope settings, updating the active microscope, and - resuming data acquisition. - - Returns: - ------- - bool - A boolean value indicating the success of the resolution change process. - """ - if ( - self.microscope_name is None - or self.microscope_name - not in self.model.configuration["configuration"]["microscopes"].keys() - ): - self.microscope_name = self.model.active_microscope_name - update_flag = False - update_sensor_mode = False - camera_parameters = self.model.configuration["experiment"]["CameraParameters"][ - self.microscope_name - ] - camera_config = self.model.configuration["configuration"]["microscopes"][ - self.microscope_name - ]["camera"] - updated_value = [None] * 3 - if ( - self.sensor_mode in ["Normal", "Light-Sheet"] - and self.sensor_mode != camera_parameters["sensor_mode"] - ): - update_flag = True - update_sensor_mode = True - camera_parameters["sensor_mode"] = self.sensor_mode - updated_value[0] = self.sensor_mode - if camera_parameters["sensor_mode"] == "Light-Sheet": - if self.readout_direction in camera_config[ - "supported_readout_directions" - ] and ( - update_sensor_mode - or camera_parameters["readout_direction"] != self.readout_direction - ): - update_flag = True - camera_parameters["readout_direction"] = self.readout_direction - updated_value[1] = self.readout_direction - if self.rolling_shutter_width and ( - update_sensor_mode - or self.rolling_shutter_width != camera_parameters["number_of_pixels"] - ): - update_flag = True - camera_parameters["number_of_pixels"] = self.rolling_shutter_width - updated_value[2] = self.rolling_shutter_width - - if not update_flag: - return True - # pause data thread - self.model.pause_data_thread() - # end active microscope - self.model.active_microscope.end_acquisition() - # set parameters and prepare active microscope - waveform_dict = self.model.active_microscope.prepare_acquisition() - self.model.event_queue.put(("waveform", waveform_dict)) - self.model.event_queue.put(("display_camera_parameters", updated_value)) - # prepare channel - self.model.active_microscope.prepare_next_channel() - # resume data thread - self.model.resume_data_thread() - return True - - def cleanup(self): - self.model.resume_data_thread() diff --git a/src/navigate/model/features/feature_related_functions.py b/src/navigate/model/features/feature_related_functions.py index 8c27148af..ed6c214c6 100644 --- a/src/navigate/model/features/feature_related_functions.py +++ b/src/navigate/model/features/feature_related_functions.py @@ -42,7 +42,6 @@ from navigate.model.features.autofocus import Autofocus # noqa from navigate.model.features.adaptive_optics import TonyWilson # noqa from navigate.model.features.common_features import ( - ChangeResolution, # noqa Snap, # noqa WaitToContinue, # noqa WaitForExternalTrigger, # noqa @@ -52,7 +51,6 @@ StackPause, # noqa ZStackAcquisition, # noqa FindTissueSimple2D, # noqa - SetCameraParameters, # noqa ) from navigate.model.features.image_writer import ImageWriter # noqa from navigate.model.features.restful_features import IlastikSegmentation # noqa @@ -66,6 +64,11 @@ DetectTissueInStackAndRecord, # noqa RemoveEmptyPositions, # noqa ) +from navigate.model.features.update_setting import ( + ChangeResolution, # noqa + SetCameraParameters, # noqa +) + from navigate.tools.file_functions import load_yaml_file from navigate.tools.common_functions import load_module_from_file diff --git a/src/navigate/model/features/update_setting.py b/src/navigate/model/features/update_setting.py new file mode 100644 index 000000000..dab6b6eaf --- /dev/null +++ b/src/navigate/model/features/update_setting.py @@ -0,0 +1,273 @@ +# Copyright (c) 2021-2024 The University of Texas Southwestern Medical Center. +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted for academic and research use only (subject to the +# limitations in the disclaimer below) provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. + +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. + +# * Neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. + +# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY +# THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# Standard library imports +import logging + +# Third party imports + +# Local application imports + +# Logger Setup +p = __name__.split(".")[1] +logger = logging.getLogger(p) + +class ChangeResolution: + """ + ChangeResolution class for modifying the resolution mode of a microscope. + + This class provides functionality to change the resolution mode of a microscope by + reconfiguring the microscope settings and updating the active microscope. + + Notes: + ------ + - This class is used to change the resolution mode of a microscope by updating the + microscope settings and configuring the active microscope accordingly. + + - The `resolution_mode` parameter specifies the desired resolution mode, and the + `zoom_value` parameter specifies the zoom value to be set. These parameters can + be adjusted to modify the microscope's configuration. + + - The `ChangeResolution` class is typically used to adapt the microscope's settings + for different imaging requirements during microscopy experiments. + + - The resolution change process involves reconfiguring the microscope, updating the + active microscope instance, and resuming data acquisition. + + - The `config_table` attribute is used to define the configuration for the + resolution change process, including signal acquisition and cleanup steps. + """ + + def __init__(self, model, resolution_mode="high", zoom_value="N/A"): + """Initialize the ChangeResolution class. + + + Parameters: + ---------- + model : MicroscopeModel + The microscope model object used for resolution mode changes. + resolution_mode : str, optional + The desired resolution mode to set for the microscope. Default is "high". + zoom_value : str, optional + The zoom value to set for the microscope. Default is "N/A". + """ + #: MicroscopeModel: The microscope model associated with the resolution change. + self.model = model + + #: dict: A dictionary defining the configuration for the resolution change + self.config_table = { + "signal": {"main": self.signal_func, "cleanup": self.cleanup}, + "node": {"device_related": True}, + } + + #: str: The desired resolution mode to set for the microscope. + self.resolution_mode = resolution_mode + + #: str: The zoom value to set for the microscope. + self.zoom_value = zoom_value + + def signal_func(self): + """Perform actions to change the resolution mode and update the active + microscope. + + This method carries out actions to change the resolution mode of the microscope + by reconfiguring the microscope settings, updating the active microscope, and + resuming data acquisition. + + Returns: + ------- + bool + A boolean value indicating the success of the resolution change process. + """ + # pause data thread + self.model.pause_data_thread() + # end active microscope + self.model.active_microscope.end_acquisition() + # prepare new microscope + self.model.configuration["experiment"]["MicroscopeState"][ + "microscope_name" + ] = self.resolution_mode + self.model.configuration["experiment"]["MicroscopeState"][ + "zoom" + ] = self.zoom_value + self.model.change_resolution(self.resolution_mode) + logger.debug(f"current resolution is {self.resolution_mode}") + logger.debug( + f"current active microscope is {self.model.active_microscope_name}" + ) + # prepare active microscope + waveform_dict = self.model.active_microscope.prepare_acquisition() + self.model.event_queue.put(("waveform", waveform_dict)) + # resume data thread + self.model.resume_data_thread() + return True + + def cleanup(self): + """Perform cleanup actions if needed. + + This method is responsible for performing cleanup actions if required after the + resolution change process. + """ + self.model.resume_data_thread() + + +class SetCameraParameters: + """ + SetCameraParameters class for modifying the parameters of a camera. + + This class provides functionality to update the parameters of a camera. + + Notes: + ------ + - This class can set sensor_mode, readout_direction and rolling_shutter_with. + + - If the value of a parameter is None it doesn't update the parameter value. + """ + + def __init__( + self, + model, + microscope_name=None, + sensor_mode="Normal", + readout_direction=None, + rolling_shutter_width=None, + ): + """Initialize the ChangeResolution class. + + + Parameters: + ---------- + model : MicroscopeModel + The microscope model object used for resolution mode changes. + sensor_mode : str, optional + The desired sensor mode to set for the camera. "Normal" or "Light-Sheet" + readout_direction : str, optional + The readout direction to set for the camera. + "Top-to-Bottom", "Bottom-to-Top", "Bidirectional" or "Rev. Bidirectional" + rolling_shutter_width : int, optional + The number of pixels for the rolling shutter. + """ + #: MicroscopeModel: The microscope model associated with the resolution change. + self.model = model + + #: dict: A dictionary defining the configuration for the resolution change + self.config_table = { + "signal": {"main": self.signal_func, "cleanup": self.cleanup}, + "node": {"device_related": True}, + } + #: str: Microscope name + self.microscope_name = microscope_name + + #: str: The desired sensor mode to set for the camera. + self.sensor_mode = sensor_mode + + #: str: The reading direction to set for the microscope. + self.readout_direction = readout_direction + + #: int: The number of pixels for the rolling shutter. + try: + self.rolling_shutter_width = int(rolling_shutter_width) + except (ValueError, TypeError): + self.rolling_shutter_width = None + + def signal_func(self): + """Perform actions to change the resolution mode and update the active + microscope. + + This method carries out actions to change the resolution mode of the microscope + by reconfiguring the microscope settings, updating the active microscope, and + resuming data acquisition. + + Returns: + ------- + bool + A boolean value indicating the success of the resolution change process. + """ + if ( + self.microscope_name is None + or self.microscope_name + not in self.model.configuration["configuration"]["microscopes"].keys() + ): + self.microscope_name = self.model.active_microscope_name + update_flag = False + update_sensor_mode = False + camera_parameters = self.model.configuration["experiment"]["CameraParameters"][ + self.microscope_name + ] + camera_config = self.model.configuration["configuration"]["microscopes"][ + self.microscope_name + ]["camera"] + updated_value = [None] * 3 + if ( + self.sensor_mode in ["Normal", "Light-Sheet"] + and self.sensor_mode != camera_parameters["sensor_mode"] + ): + update_flag = True + update_sensor_mode = True + camera_parameters["sensor_mode"] = self.sensor_mode + updated_value[0] = self.sensor_mode + if camera_parameters["sensor_mode"] == "Light-Sheet": + if self.readout_direction in camera_config[ + "supported_readout_directions" + ] and ( + update_sensor_mode + or camera_parameters["readout_direction"] != self.readout_direction + ): + update_flag = True + camera_parameters["readout_direction"] = self.readout_direction + updated_value[1] = self.readout_direction + if self.rolling_shutter_width and ( + update_sensor_mode + or self.rolling_shutter_width != camera_parameters["number_of_pixels"] + ): + update_flag = True + camera_parameters["number_of_pixels"] = self.rolling_shutter_width + updated_value[2] = self.rolling_shutter_width + + if not update_flag: + return True + # pause data thread + self.model.pause_data_thread() + # end active microscope + self.model.active_microscope.end_acquisition() + # set parameters and prepare active microscope + waveform_dict = self.model.active_microscope.prepare_acquisition() + self.model.event_queue.put(("waveform", waveform_dict)) + self.model.event_queue.put(("display_camera_parameters", updated_value)) + # prepare channel + self.model.active_microscope.prepare_next_channel() + # resume data thread + self.model.resume_data_thread() + return True + + def cleanup(self): + self.model.resume_data_thread() diff --git a/src/navigate/model/model.py b/src/navigate/model/model.py index 458acf377..67a2e6c5c 100644 --- a/src/navigate/model/model.py +++ b/src/navigate/model/model.py @@ -47,7 +47,6 @@ from navigate.model.features.image_writer import ImageWriter from navigate.model.features.auto_tile_scan import CalculateFocusRange # noqa from navigate.model.features.common_features import ( - ChangeResolution, Snap, ZStackAcquisition, FindTissueSimple2D, @@ -271,16 +270,6 @@ def __init__(self, args: argparse.Namespace, configuration=None, event_queue=Non #: list: List of features. self.feature_list = [] - # automatically switch resolution - self.feature_list.append( - [ - {"name": ChangeResolution, "args": ("Mesoscale", "1x")}, - {"name": Snap}, - ] - ) - # z stack acquisition - self.feature_list.append([{"name": ZStackAcquisition}]) - # threshold and tile self.feature_list.append([{"name": FindTissueSimple2D}]) From 99868853ce4798ce5f8b22a6b1023166e4788cf9 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 16 Oct 2024 15:12:42 -0700 Subject: [PATCH 09/23] add new feature: update experiment values --- src/navigate/controller/controller.py | 2 +- .../sub_controllers/channels_tab.py | 6 ++ .../features/feature_related_functions.py | 1 + src/navigate/model/features/update_setting.py | 58 +++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index ad453e439..3eaba6127 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -524,7 +524,7 @@ def update_experiment_setting(self): self.channels_tab_controller.is_multiposition_val.set(False) # TODO: validate experiment dict - + self.channels_tab_controller.update_experiment_values() warning_message += self.channels_tab_controller.verify_experiment_values() # additional microscopes diff --git a/src/navigate/controller/sub_controllers/channels_tab.py b/src/navigate/controller/sub_controllers/channels_tab.py index 30b7ac962..ca361388b 100644 --- a/src/navigate/controller/sub_controllers/channels_tab.py +++ b/src/navigate/controller/sub_controllers/channels_tab.py @@ -799,6 +799,12 @@ def execute(self, command, *args): self.show_verbose_info("Received command from child", command, args) + def update_experiment_values(self): + """Update experiment values""" + self.channel_setting_controller.update_experiment_values() + self.update_z_steps() + + def verify_experiment_values(self): """Verify channel tab settings and return warning info diff --git a/src/navigate/model/features/feature_related_functions.py b/src/navigate/model/features/feature_related_functions.py index ed6c214c6..df6127617 100644 --- a/src/navigate/model/features/feature_related_functions.py +++ b/src/navigate/model/features/feature_related_functions.py @@ -67,6 +67,7 @@ from navigate.model.features.update_setting import ( ChangeResolution, # noqa SetCameraParameters, # noqa + UpdateExperimentSetting, # noqa ) from navigate.tools.file_functions import load_yaml_file diff --git a/src/navigate/model/features/update_setting.py b/src/navigate/model/features/update_setting.py index dab6b6eaf..402e65fb6 100644 --- a/src/navigate/model/features/update_setting.py +++ b/src/navigate/model/features/update_setting.py @@ -32,6 +32,7 @@ # Standard library imports import logging +from functools import reduce # Third party imports @@ -127,6 +128,8 @@ def signal_func(self): # prepare active microscope waveform_dict = self.model.active_microscope.prepare_acquisition() self.model.event_queue.put(("waveform", waveform_dict)) + # prepare channel + self.model.active_microscope.prepare_next_channel() # resume data thread self.model.resume_data_thread() return True @@ -271,3 +274,58 @@ def signal_func(self): def cleanup(self): self.model.resume_data_thread() + + +class UpdateExperimentSetting: + + def __init__(self, model, experiment_parameters={}): + self.model = model + + #: dict: A dictionary defining the configuration for the resolution change + self.config_table = { + "signal": {"main": self.signal_func, "cleanup": self.cleanup}, + "node": {"device_related": True}, + } + + self.experiment_parameters = experiment_parameters + + def signal_func(self): + """Perform actions to change the resolution mode and update the active + microscope. + + This method carries out actions to change the resolution mode of the microscope + by reconfiguring the microscope settings, updating the active microscope, and + resuming data acquisition. + + Returns: + ------- + bool + A boolean value indicating the success of the resolution change process. + """ + if type(self.experiment_parameters) != dict: + return False + # pause data thread + self.model.pause_data_thread() + # end active microscope + self.model.active_microscope.end_acquisition() + + # update experiment values + for k, v in self.experiment_parameters.items(): + try: + parameters = k.split(".") + config_ref = reduce(lambda pre, n: f"{pre}['{n}']", parameters, "") + exec(f"self.model.configuration['experiment']{config_ref} = {v}") + except Exception as e: + logger.error(f"*** parameter {k} failed to update to value {v}") + logger.error(e) + # set parameters and prepare active microscope + waveform_dict = self.model.active_microscope.prepare_acquisition() + self.model.event_queue.put(("waveform", waveform_dict)) + # prepare channel + self.model.active_microscope.prepare_next_channel() + # resume data thread + self.model.resume_data_thread() + return True + + def cleanup(self): + self.model.resume_data_thread() From 61bb66dab3759403e7c253bad31f221cfe68cd81 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 18 Oct 2024 14:44:28 -0700 Subject: [PATCH 10/23] map positions --- .../model/analysis/boundary_detect.py | 132 ++++++++++++++++-- src/navigate/model/features/update_setting.py | 3 + src/navigate/model/features/volume_search.py | 115 +++++++++++---- 3 files changed, 208 insertions(+), 42 deletions(-) diff --git a/src/navigate/model/analysis/boundary_detect.py b/src/navigate/model/analysis/boundary_detect.py index 32d3c2045..4d6e3188f 100644 --- a/src/navigate/model/analysis/boundary_detect.py +++ b/src/navigate/model/analysis/boundary_detect.py @@ -32,13 +32,13 @@ # Standard library imports import math from typing import Optional +from itertools import product # Third party imports from skimage import filters from skimage.transform import downscale_local_mean -from skimage.measure import label -from skimage.segmentation import find_boundaries -from scipy.ndimage import binary_fill_holes +from skimage import measure +from scipy.ndimage import median_filter, binary_fill_holes import numpy as np import numpy.typing as npt @@ -452,18 +452,120 @@ def dp_shortest_path(start, end, step, offset=-1): return result -def find_cell_boundary_3d(z_stack_image, threshold_value): +def find_cell_boundary_3d(z_stack_image): + """A default label volume image function - z, x, y = z_stack_image.shape - binary_images = [] - - for i in range(z): - thresh_img = z_stack_image[i] > threshold_value - binary_images.append(thresh_img.astype(np.uint8)) + Parameters + ---------- + z_stack_image : ndarray + A 3d array image data + + Returns + ------- + labels : ndarray + Labeled array + """ + denoised_image = median_filter(z_stack_image, size=3) + thresholded_image = denoised_image > filters.threshold_otsu(denoised_image) + filled_images = binary_fill_holes(thresholded_image) + cell_labels = measure.label(filled_images) + + return cell_labels + + +def map_labels( + labeled_image, + position, + z_start, + z_step, + current_pixel_size, + current_image_width, + current_image_height, + target_pixel_size, + target_image_width, + target_image_height, + overlap=0.05, +): + """Map labels to positions + + Parameters + ---------- + labeled_image : ndarray + Labeled image data + position : array[int] + position of x, y, z, theta, f + z_start : float + Z start position + z_step : float + step of Z + current_pixel_size : float + Current camera pixel size + current_image_width : int + Current image width + current_image_height : int + Current image height + target_pixel_size : float + Target camera pixel size + target_image_width : int + Target image width + target_image_height : int + Target image height + overlap : float + Overlap ratio + + Returns + ------- + z_range : int + The maximum number of z steps + positions : array + Array of positions + """ + if target_pixel_size >= current_pixel_size: + return 0, [position] + if overlap < 0: + overlap = 0 + + target_num = np.max(labeled_image) + position_table = [] + x, y, z, theta, f = position + center_x = current_image_width // 2 + center_y = current_image_height // 2 + + x_pixel = int(target_image_width * target_pixel_size / current_pixel_size) + y_pixel = int(target_image_height * target_pixel_size / current_pixel_size) + + regionprops = measure.regionprops(labeled_image) + z_range = 1 + + for i in range(target_num): + min_z, min_y, min_x, max_z, max_y, max_x = regionprops[i].bbox + + num_x = math.ceil((max_x - min_x) / (x_pixel * (1 - overlap))) + 1 + shift_x = (num_x * x_pixel - (max_x - min_x)) // 2 + + num_y = math.ceil((max_y - min_y) / (y_pixel * (1 - overlap))) + 1 + shift_y = (num_y * y_pixel - (max_y - min_y)) // 2 + + z_range = max(z_range, (max_z - min_z)) + + min_x -= shift_x + min_y -= shift_y + + z_pos = z + z_start + min_z * z_step + + x_start = x + (min_x + x_pixel / 2 - center_x) * current_pixel_size + x_positions = [x_start] + for _ in range(1, num_x): + x_positions.append(x_start + (x_pixel * (1 - overlap) * current_pixel_size)) + + y_start = y + (min_y + y_pixel / 2 - center_y) * current_pixel_size + y_positions = [y_start] + for _ in range(1, num_y): + y_positions.append(y_start + (y_pixel * (1 - overlap) * current_pixel_size)) - filled_images = binary_fill_holes(binary_images) - cell_labels = label(filled_images) - boundaries = find_boundaries(cell_labels > 0, connectivity=1) + position_table += [ + [x_pos, y_pos] + [z_pos, theta, f] + for x_pos, y_pos in product(x_positions, y_positions) + ] - return boundaries - \ No newline at end of file + return z_range, position_table diff --git a/src/navigate/model/features/update_setting.py b/src/navigate/model/features/update_setting.py index 402e65fb6..3d106206f 100644 --- a/src/navigate/model/features/update_setting.py +++ b/src/navigate/model/features/update_setting.py @@ -128,6 +128,7 @@ def signal_func(self): # prepare active microscope waveform_dict = self.model.active_microscope.prepare_acquisition() self.model.event_queue.put(("waveform", waveform_dict)) + self.model.frame_id = 0 # prepare channel self.model.active_microscope.prepare_next_channel() # resume data thread @@ -266,6 +267,7 @@ def signal_func(self): waveform_dict = self.model.active_microscope.prepare_acquisition() self.model.event_queue.put(("waveform", waveform_dict)) self.model.event_queue.put(("display_camera_parameters", updated_value)) + self.model.frame_id = 0 # prepare channel self.model.active_microscope.prepare_next_channel() # resume data thread @@ -321,6 +323,7 @@ def signal_func(self): # set parameters and prepare active microscope waveform_dict = self.model.active_microscope.prepare_acquisition() self.model.event_queue.put(("waveform", waveform_dict)) + self.model.frame_id = 0 # prepare channel self.model.active_microscope.prepare_next_channel() # resume data thread diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index 2a98e3f71..a22ffd673 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -36,6 +36,7 @@ binary_detect, map_boundary, find_cell_boundary_3d, + map_labels, ) import numpy as np @@ -465,14 +466,15 @@ def cleanup(self): class VolumeSearch3D: - def __init__( self, model, target_resolution="Nanoscale", target_zoom="N/A", - threshold_value=462, - analysis_function=None + position_id=0, + z_step_size=0.1, + overlap=0.5, + analysis_function=None, ): """Initialize VolumeSearch @@ -485,14 +487,14 @@ def __init__( target_zoom : str Resolution of microscope (target_resolution) to use for tiled imaging of tissue - flipx : bool - Flip the direction in which new tiles are added. - flipy : bool - Flip the direction in which new tiles are added. + position_id : int + The index of position in multiposition table + z_step_size : float + Target z step size overlap : float - Value between 0 and 1 indicating percent overlap of tiles. - debug : bool - If True, save debug images to disk. + The overlap ratio + analysis_function : callable + An analysis function return a labeled object """ #: navigate.model.model.Model: Navigate Model @@ -504,11 +506,19 @@ def __init__( #: str: Resolution of microscope (target_resolution) to use for tiled imaging self.target_zoom = target_zoom - #: int: threshold value - self.threshold_value = threshold_value + #: int: The index of position + self.position_id = position_id + + #: float: The Z step size + self.z_step = z_step_size + + #: float: The overlap ratio + self.overlap = overlap #: function: analysis function - self.analysis_function = analysis_function if analysis_function else find_cell_boundary_3d + self.analysis_function = ( + analysis_function if analysis_function else find_cell_boundary_3d + ) #: dict: Feature configuration self.config_table = { @@ -520,28 +530,79 @@ def __init__( def data_func(self, frame_ids): - self.model.logger.debug("Starting 3D Volume Search") + self.model.logger.info("Starting 3D Volume Search") + + microscope_state_config = self.model.configuration["experiment"][ + "MicroscopeState" + ] - z_stack_data = self.model.image_writer.data_source.get_data() - # boundaries = self.analysis_function(z_stack_data, self.threshold_value) + if self.position_id > microscope_state_config["multiposition_count"]: + self.position_id = 0 + + z_stack_data = self.model.image_writer.data_source.get_data( + position=self.position_id + ) + labeled_image = self.analysis_function(z_stack_data) - # TODO: map boundaries to positions - # TODO: remove this, it's used for feature pipeline tests. - positions = [[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [3, 4, 5, 6, 7]] + # map labeled cells + z_start = microscope_state_config["start_position"] + z_step = microscope_state_config["step_size"] + position = self.model.configuration["experiment"]["MultiPositions"][ + self.position_id + ] + current_microscope_name = self.model.active_microscope_name + current_zoom_value = microscope_state_config["zoom"] + current_pixel_size = self.model.configuration["configuration"]["microscopes"][ + current_microscope_name + ]["zoom"]["pixel_size"][current_zoom_value] + current_image_width = self.model.configuration["experiment"][ + "CameraParameters" + ][current_microscope_name]["img_x_pixels"] + current_image_height = self.model.configuration["experiment"][ + "CameraParameters" + ][current_microscope_name]["img_y_pixels"] + + target_pixel_size = self.model.configuration["configuration"]["microscopes"][ + self.target_resolution + ]["zoom"]["pixel_size"][self.target_zoom] + target_image_width = self.model.configuration["experiment"]["CameraParameters"][ + self.target_resolution + ]["img_x_pixels"] + target_image_height = self.model.configuration["experiment"][ + "CameraParameters" + ][self.target_resolution]["img_y_pixels"] + + z_range, positions = map_labels( + labeled_image, + position, + z_start, + z_step, + current_pixel_size, + current_image_width, + current_image_height, + target_pixel_size, + target_image_width, + target_image_height, + overlap=self.overlap, + ) self.model.event_queue.put(("multiposition", positions)) self.model.configuration["experiment"]["MultiPositions"] = positions - self.model.configuration["experiment"]["MicroscopeState"][ - "multiposition_count" - ] = len(positions) + microscope_state_config["multiposition_count"] = len(positions) if len(positions) > 0: - self.model.configuration["experiment"]["MicroscopeState"][ - "is_multiposition" - ] = True + microscope_state_config["is_multiposition"] = True + + microscope_state_config["start_position"] = 0 + microscope_state_config["end_position"] = z_range * z_step + microscope_state_config["step_size"] = self.z_step + microscope_state_config["number_z_steps"] = z_range * z_step // self.z_step + + self.model.logger.info(f"New Z range would be {microscope_state_config["end_position"]}" + f" with step_size {self.z_step}") self.model.image_writer.initialize_saving(sub_dir=str(self.target_resolution)) - - self.model.logger.debug(f"Volume Search 3D completed!") + + self.model.logger.info(f"Volume Search 3D completed!") def cleanup(self): pass From effa43fbc40edb22f4abf3eee76f85ccf59e6ad4 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 18 Oct 2024 14:45:32 -0700 Subject: [PATCH 11/23] typo --- src/navigate/model/features/volume_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index a22ffd673..c4a680782 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -597,7 +597,7 @@ def data_func(self, frame_ids): microscope_state_config["step_size"] = self.z_step microscope_state_config["number_z_steps"] = z_range * z_step // self.z_step - self.model.logger.info(f"New Z range would be {microscope_state_config["end_position"]}" + self.model.logger.info(f"New Z range would be {microscope_state_config['end_position']}" f" with step_size {self.z_step}") self.model.image_writer.initialize_saving(sub_dir=str(self.target_resolution)) From c7500cfd9a5923a0aab921cd74616f0d25fc1f97 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Thu, 24 Oct 2024 09:37:50 -0700 Subject: [PATCH 12/23] implement stage offset --- .../model/analysis/boundary_detect.py | 2 +- src/navigate/model/features/volume_search.py | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/navigate/model/analysis/boundary_detect.py b/src/navigate/model/analysis/boundary_detect.py index 4d6e3188f..9858e1222 100644 --- a/src/navigate/model/analysis/boundary_detect.py +++ b/src/navigate/model/analysis/boundary_detect.py @@ -521,7 +521,7 @@ def map_labels( Array of positions """ if target_pixel_size >= current_pixel_size: - return 0, [position] + return 1, [position] if overlap < 0: overlap = 0 diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index c4a680782..2ecc55571 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -547,11 +547,48 @@ def data_func(self, frame_ids): # map labeled cells z_start = microscope_state_config["start_position"] z_step = microscope_state_config["step_size"] - position = self.model.configuration["experiment"]["MultiPositions"][ - self.position_id - ] + + if microscope_state_config["multiposition_count"] == 0: + pos_dict = self.model.get_stage_position + position = [ + pos_dict[f"{axis}_pos"] for axis in ["x", 'y', "z", "theta", "f"] + ] + else: + position = self.model.configuration["experiment"]["MultiPositions"][ + self.position_id + ] + current_microscope_name = self.model.active_microscope_name - current_zoom_value = microscope_state_config["zoom"] + current_zoom_value = self.model.active_microscope.zoom.zoomvalue + # offset + if self.target_resolution != current_microscope_name: + current_stage_offset = self.model.configuration["configuration"]["microscopes"][ + current_microscope_name + ]["stage"] + target_stage_offset = self.model.configuration["configuration"]["microscopes"][ + self.target_resolution + ]["stage"] + for i, axis in enumerate(["x", "y", "z", "theta", "f"]): + position[i] += target_stage_offset[f"{axis}_offset"] - current_stage_offset[f"{axis}_offset"] + else: + solvent = self.model.configuration["experiment"]["Saving"]["solvent"] + stage_solvent_offsets = self.model.active_microscope.zoom.stage_offsets + if solvent in stage_solvent_offsets.keys(): + stage_offset = stage_solvent_offsets[solvent] + for i, axis in enumerate(["x", "y", "z", "theta", "f"]): + if axis not in stage_offset.keys(): + continue + try: + position[i] += float( + stage_offset[axis][self.target_zoom][current_zoom_value] + ) + except (ValueError, KeyError): + self.model.logger.info( + f"*** Offsets from {self.target_zoom} to {current_zoom_value} are " + f"not implemented! There is not enough information in the " + f"configuration.yaml file!" + ) + current_pixel_size = self.model.configuration["configuration"]["microscopes"][ current_microscope_name ]["zoom"]["pixel_size"][current_zoom_value] From 1bddd4410c8f828fb48b8e6604307cebcd9f9ff9 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Thu, 24 Oct 2024 11:21:34 -0700 Subject: [PATCH 13/23] typo --- src/navigate/model/features/volume_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index 2ecc55571..9a3aa643a 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -549,7 +549,7 @@ def data_func(self, frame_ids): z_step = microscope_state_config["step_size"] if microscope_state_config["multiposition_count"] == 0: - pos_dict = self.model.get_stage_position + pos_dict = self.model.get_stage_position() position = [ pos_dict[f"{axis}_pos"] for axis in ["x", 'y', "z", "theta", "f"] ] From 2b5e469470bdbcd7af757a5e3d56db82a5ffac35 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Mon, 28 Oct 2024 20:29:49 -0700 Subject: [PATCH 14/23] add x and y direction alignment --- .../model/analysis/boundary_detect.py | 61 +++++++++++++++---- src/navigate/model/features/volume_search.py | 40 ++++++++---- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/src/navigate/model/analysis/boundary_detect.py b/src/navigate/model/analysis/boundary_detect.py index 9858e1222..69216e5fa 100644 --- a/src/navigate/model/analysis/boundary_detect.py +++ b/src/navigate/model/analysis/boundary_detect.py @@ -478,6 +478,8 @@ def map_labels( position, z_start, z_step, + x_direction, + y_direction, current_pixel_size, current_image_width, current_image_height, @@ -524,10 +526,23 @@ def map_labels( return 1, [position] if overlap < 0: overlap = 0 + if x_direction not in ["x", "-x", "y", "-y"]: + x_direction = "x" + if y_direction not in ["x", "-x", "y", "-y"]: + y_direction = "y" + + assert x_direction[-1] != y_direction[-1] + + x_direction_alignment = -1 if x_direction[0] == "-" else 1 + y_direction_alignment = -1 if y_direction[0] == "-" else 1 + + x_direction_index = 0 if x_direction[-1] == "x" else 1 + y_direction_index = 1 - x_direction_index target_num = np.max(labeled_image) position_table = [] x, y, z, theta, f = position + center_x = current_image_width // 2 center_y = current_image_height // 2 @@ -540,10 +555,10 @@ def map_labels( for i in range(target_num): min_z, min_y, min_x, max_z, max_y, max_x = regionprops[i].bbox - num_x = math.ceil((max_x - min_x) / (x_pixel * (1 - overlap))) + 1 + num_x = math.ceil((max_x - min_x) / (x_pixel * (1 - overlap))) shift_x = (num_x * x_pixel - (max_x - min_x)) // 2 - num_y = math.ceil((max_y - min_y) / (y_pixel * (1 - overlap))) + 1 + num_y = math.ceil((max_y - min_y) / (y_pixel * (1 - overlap))) shift_y = (num_y * y_pixel - (max_y - min_y)) // 2 z_range = max(z_range, (max_z - min_z)) @@ -553,19 +568,41 @@ def map_labels( z_pos = z + z_start + min_z * z_step - x_start = x + (min_x + x_pixel / 2 - center_x) * current_pixel_size + x_start = ( + position[x_direction_index] + + x_direction_alignment + * (min_x + x_pixel / 2 - center_x) + * current_pixel_size + ) x_positions = [x_start] for _ in range(1, num_x): - x_positions.append(x_start + (x_pixel * (1 - overlap) * current_pixel_size)) - - y_start = y + (min_y + y_pixel / 2 - center_y) * current_pixel_size + x_positions.append( + x_start + + x_direction_alignment * (x_pixel * (1 - overlap) * current_pixel_size) + ) + + y_start = ( + position[y_direction_index] + + y_direction_alignment + * (min_y + y_pixel / 2 - center_y) + * current_pixel_size + ) y_positions = [y_start] for _ in range(1, num_y): - y_positions.append(y_start + (y_pixel * (1 - overlap) * current_pixel_size)) - - position_table += [ - [x_pos, y_pos] + [z_pos, theta, f] - for x_pos, y_pos in product(x_positions, y_positions) - ] + y_positions.append( + y_start + + y_direction_alignment * (y_pixel * (1 - overlap) * current_pixel_size) + ) + + if x_direction_index == 0: + position_table += [ + [x_pos, y_pos] + [z_pos, theta, f] + for x_pos, y_pos in product(x_positions, y_positions) + ] + else: + position_table += [ + [y_pos, x_pos] + [z_pos, theta, f] + for x_pos, y_pos in product(x_positions, y_positions) + ] return z_range, position_table diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index 9a3aa643a..1c6047161 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -473,7 +473,9 @@ def __init__( target_zoom="N/A", position_id=0, z_step_size=0.1, - overlap=0.5, + x_direction="x", + y_direction="y", + overlap=0.05, analysis_function=None, ): """Initialize VolumeSearch @@ -511,6 +513,11 @@ def __init__( #: float: The Z step size self.z_step = z_step_size + #: string + self.x_direction = x_direction + + #: string + self.y_direction = y_direction #: float: The overlap ratio self.overlap = overlap @@ -547,11 +554,11 @@ def data_func(self, frame_ids): # map labeled cells z_start = microscope_state_config["start_position"] z_step = microscope_state_config["step_size"] - + if microscope_state_config["multiposition_count"] == 0: pos_dict = self.model.get_stage_position() position = [ - pos_dict[f"{axis}_pos"] for axis in ["x", 'y', "z", "theta", "f"] + pos_dict[f"{axis}_pos"] for axis in ["x", "y", "z", "theta", "f"] ] else: position = self.model.configuration["experiment"]["MultiPositions"][ @@ -562,14 +569,17 @@ def data_func(self, frame_ids): current_zoom_value = self.model.active_microscope.zoom.zoomvalue # offset if self.target_resolution != current_microscope_name: - current_stage_offset = self.model.configuration["configuration"]["microscopes"][ - current_microscope_name - ]["stage"] - target_stage_offset = self.model.configuration["configuration"]["microscopes"][ - self.target_resolution - ]["stage"] + current_stage_offset = self.model.configuration["configuration"][ + "microscopes" + ][current_microscope_name]["stage"] + target_stage_offset = self.model.configuration["configuration"][ + "microscopes" + ][self.target_resolution]["stage"] for i, axis in enumerate(["x", "y", "z", "theta", "f"]): - position[i] += target_stage_offset[f"{axis}_offset"] - current_stage_offset[f"{axis}_offset"] + position[i] += ( + target_stage_offset[f"{axis}_offset"] + - current_stage_offset[f"{axis}_offset"] + ) else: solvent = self.model.configuration["experiment"]["Saving"]["solvent"] stage_solvent_offsets = self.model.active_microscope.zoom.stage_offsets @@ -588,7 +598,7 @@ def data_func(self, frame_ids): f"not implemented! There is not enough information in the " f"configuration.yaml file!" ) - + current_pixel_size = self.model.configuration["configuration"]["microscopes"][ current_microscope_name ]["zoom"]["pixel_size"][current_zoom_value] @@ -614,6 +624,8 @@ def data_func(self, frame_ids): position, z_start, z_step, + self.x_direction, + self.y_direction, current_pixel_size, current_image_width, current_image_height, @@ -634,8 +646,10 @@ def data_func(self, frame_ids): microscope_state_config["step_size"] = self.z_step microscope_state_config["number_z_steps"] = z_range * z_step // self.z_step - self.model.logger.info(f"New Z range would be {microscope_state_config['end_position']}" - f" with step_size {self.z_step}") + self.model.logger.info( + f"New Z range would be {microscope_state_config['end_position']}" + f" with step_size {self.z_step}" + ) self.model.image_writer.initialize_saving(sub_dir=str(self.target_resolution)) From ca2d83641685cae0914d04529d381186d2b8b9f7 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 15:09:32 -0800 Subject: [PATCH 15/23] update dictionary parameters of features --- .../sub_controllers/features_popup.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/navigate/controller/sub_controllers/features_popup.py b/src/navigate/controller/sub_controllers/features_popup.py index 40c801021..21544b205 100644 --- a/src/navigate/controller/sub_controllers/features_popup.py +++ b/src/navigate/controller/sub_controllers/features_popup.py @@ -33,7 +33,7 @@ import tkinter as tk from tkinter import messagebox import inspect -import json +import ast import os import platform @@ -557,9 +557,22 @@ def update_feature_parameters(popup): elif a == "False": feature["args"][i] = False elif popup.inputs_type[i] is float: - feature["args"][i] = float(a) + try: + feature["args"][i] = float(a) + except ValueError: + feature["args"][i] = a elif popup.inputs_type[i] is dict: - feature["args"][i] = json.loads(a.replace("'", '"')) + try: + feature["args"][i] = ast.literal_eval(a.replace("'", '"')) + except (SyntaxError, ValueError): + spec = inspect.getfullargspec(feature["name"]) + arg_name = spec.args[i+2] + messagebox.showerror( + title="Upate Feature Parameter Error", + message=f"The argument {arg_name} has something wrong!\n" + "Please make sure you input a correct value!" + ) + return elif a == "None": feature["args"][i] = None if "true" in feature: From 035e17b28b1222f5f5b0151151ecfeb7e32e414e Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 15:14:17 -0800 Subject: [PATCH 16/23] update volume search z position --- src/navigate/model/features/volume_search.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index 1c6047161..d412962c3 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -553,6 +553,7 @@ def data_func(self, frame_ids): # map labeled cells z_start = microscope_state_config["start_position"] + z_end = microscope_state_config["end_position"] z_step = microscope_state_config["step_size"] if microscope_state_config["multiposition_count"] == 0: @@ -560,6 +561,8 @@ def data_func(self, frame_ids): position = [ pos_dict[f"{axis}_pos"] for axis in ["x", "y", "z", "theta", "f"] ] + # current stage position is the end of z + position[2] -= z_end else: position = self.model.configuration["experiment"]["MultiPositions"][ self.position_id @@ -647,7 +650,7 @@ def data_func(self, frame_ids): microscope_state_config["number_z_steps"] = z_range * z_step // self.z_step self.model.logger.info( - f"New Z range would be {microscope_state_config['end_position']}" + f"New Z range would be 0 to {microscope_state_config['end_position']}" f" with step_size {self.z_step}" ) From e861929d36bc81b8c432c81646ac95541802cf8b Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 16:12:09 -0800 Subject: [PATCH 17/23] update volume search function --- .../model/analysis/boundary_detect.py | 10 ++++++ src/navigate/model/features/volume_search.py | 31 +++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/navigate/model/analysis/boundary_detect.py b/src/navigate/model/analysis/boundary_detect.py index 69216e5fa..8b7095d94 100644 --- a/src/navigate/model/analysis/boundary_detect.py +++ b/src/navigate/model/analysis/boundary_detect.py @@ -487,6 +487,7 @@ def map_labels( target_image_width, target_image_height, overlap=0.05, + filter_pixel_number=10, ): """Map labels to positions @@ -514,6 +515,8 @@ def map_labels( Target image height overlap : float Overlap ratio + filter_pixel_number : int + The pixel number to filter a label Returns ------- @@ -555,6 +558,13 @@ def map_labels( for i in range(target_num): min_z, min_y, min_x, max_z, max_y, max_x = regionprops[i].bbox + # do not need to calculate the position + # if the label area is smaller than filter_pixel_number in x or y. + if (max_x - min_x) < filter_pixel_number or ( + max_y - min_y + ) < filter_pixel_number: + continue + num_x = math.ceil((max_x - min_x) / (x_pixel * (1 - overlap))) shift_x = (num_x * x_pixel - (max_x - min_x)) // 2 diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index d412962c3..f96ca053d 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -39,6 +39,8 @@ map_labels, ) import numpy as np +from tifffile import imwrite +from os import path def draw_box(img, xl, yl, xu, yu, fill=65535): @@ -477,6 +479,8 @@ def __init__( y_direction="y", overlap=0.05, analysis_function=None, + current_pixel_size=1.0, + filter_pixel_number=10, ): """Initialize VolumeSearch @@ -497,6 +501,10 @@ def __init__( The overlap ratio analysis_function : callable An analysis function return a labeled object + current_pixel_size : float + The current pixel size + filter_pixel_number : int + The pixel number to filter a label """ #: navigate.model.model.Model: Navigate Model @@ -527,6 +535,12 @@ def __init__( analysis_function if analysis_function else find_cell_boundary_3d ) + #: float: The current pixel size + self.current_pixel_size = current_pixel_size + + #: int: The filter pixel number + self.filter_pixel_number = 10 + #: dict: Feature configuration self.config_table = { "data": { @@ -551,6 +565,15 @@ def data_func(self, frame_ids): ) labeled_image = self.analysis_function(z_stack_data) + # save labels + imwrite( + file=path.join( + self.model.configuration["experiment"]["Saving"]["save_directory"], + "labels.tiff", + ), + data=labeled_image.astype(np.uint16), + ) + # map labeled cells z_start = microscope_state_config["start_position"] z_end = microscope_state_config["end_position"] @@ -586,7 +609,10 @@ def data_func(self, frame_ids): else: solvent = self.model.configuration["experiment"]["Saving"]["solvent"] stage_solvent_offsets = self.model.active_microscope.zoom.stage_offsets - if solvent in stage_solvent_offsets.keys(): + if ( + stage_solvent_offsets is not None + and solvent in stage_solvent_offsets.keys() + ): stage_offset = stage_solvent_offsets[solvent] for i, axis in enumerate(["x", "y", "z", "theta", "f"]): if axis not in stage_offset.keys(): @@ -629,13 +655,14 @@ def data_func(self, frame_ids): z_step, self.x_direction, self.y_direction, - current_pixel_size, + self.current_pixel_size, current_image_width, current_image_height, target_pixel_size, target_image_width, target_image_height, overlap=self.overlap, + filter_pixel_number=self.filter_pixel_number, ) self.model.event_queue.put(("multiposition", positions)) From 0e7475e7420db7912f3332aba2e7bd491ed9c6b5 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 16:19:35 -0800 Subject: [PATCH 18/23] Revert "update multipositions as ListProxy" This reverts commit 134b201509553bb486fd7d0d3d902753ca6446b5. --- src/navigate/controller/controller.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index 3eaba6127..2f5f0264f 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -503,12 +503,7 @@ def update_experiment_setting(self): # update multi-positions positions = self.multiposition_tab_controller.get_positions() - update_config_dict( - self.manager, - self.configuration["experiment"], - "MultiPositions", - positions - ) + self.configuration["experiment"]["MultiPositions"] = positions self.configuration["experiment"]["MicroscopeState"][ "multiposition_count" ] = len(positions) From b3a9f7f13644812ba1d4fb801da89a65e659994f Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 16:48:28 -0800 Subject: [PATCH 19/23] update experiment values --- src/navigate/controller/controller.py | 10 ++++++++++ .../controller/sub_controllers/channels_settings.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/navigate/controller/controller.py b/src/navigate/controller/controller.py index 2f5f0264f..4d426805d 100644 --- a/src/navigate/controller/controller.py +++ b/src/navigate/controller/controller.py @@ -488,6 +488,16 @@ def update_experiment_setting(self): microscope_name = self.configuration["experiment"]["MicroscopeState"][ "microscope_name" ] + zoom_value = self.configuration["experiment"]["MicroscopeState"]["zoom"] + resolution_value = self.menu_controller.resolution_value.get() + + # set microscope and zoom value according to GUI + if f"{microscope_name} {zoom_value}" != resolution_value: + microscope_name, zoom_value = resolution_value.split() + self.configuration["experiment"]["MicroscopeState"][ + "microscope_name" + ] = microscope_name + self.configuration["experiment"]["MicroscopeState"]["zoom"] = zoom_value # set waveform template if self.acquire_bar_controller.mode in ["live", "single", "z-stack"]: diff --git a/src/navigate/controller/sub_controllers/channels_settings.py b/src/navigate/controller/sub_controllers/channels_settings.py index c1c7ba481..88b666b46 100644 --- a/src/navigate/controller/sub_controllers/channels_settings.py +++ b/src/navigate/controller/sub_controllers/channels_settings.py @@ -200,6 +200,13 @@ def populate_empty_values(self): if self.view.interval_spins[i].get() == "": self.view.interval_spins[i].set(1.0) + def update_experiment_values(self): + """Update experiment values according to GUI""" + for i in range(self.num): + channel_vals = self.get_vals_by_channel(i) + for name in channel_vals: + self.channel_callback(i, name)() + def set_spinbox_range_limits(self, settings): """Set the range limits for the spinboxes in the View. From 15b900389ef7b2903e99fbf9244e3b370dac8c92 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Wed, 6 Nov 2024 16:49:33 -0800 Subject: [PATCH 20/23] update image writer if experiment values are changed --- src/navigate/model/features/update_setting.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/navigate/model/features/update_setting.py b/src/navigate/model/features/update_setting.py index 3d106206f..80a38ef20 100644 --- a/src/navigate/model/features/update_setting.py +++ b/src/navigate/model/features/update_setting.py @@ -326,6 +326,14 @@ def signal_func(self): self.model.frame_id = 0 # prepare channel self.model.active_microscope.prepare_next_channel() + # update image writer + if self.model.image_writer: + try: + self.model.image_writer.data_source.set_metadata_from_configuration_experiment( + self.model.configuration + ) + except Exception as e: + logger.exception(f"Update image writer metadata failed: {e}") # resume data thread self.model.resume_data_thread() return True From 9ef9e14e6ffb25888850c89954a8d603190474ed Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Fri, 8 Nov 2024 15:07:16 -0800 Subject: [PATCH 21/23] revert data_source --- src/navigate/model/data_sources/data_source.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/navigate/model/data_sources/data_source.py b/src/navigate/model/data_sources/data_source.py index cfaa67e03..4401bfccb 100644 --- a/src/navigate/model/data_sources/data_source.py +++ b/src/navigate/model/data_sources/data_source.py @@ -298,13 +298,12 @@ def _cztp_indices(self, frame_id: int, per_stack: bool = True) -> tuple: z = (frame_id // self.shape_c) % self.shape_z # NOTE: Uncomment this if we want time to vary faster than positions - # t = (frame_id // (self.shape_c * self.shape_z)) % self.shape_t - # p = frame_id // (self.shape_c * self.shape_z * self.shape_t) + t = (frame_id // (self.shape_c * self.shape_z)) % self.shape_t + p = frame_id // (self.shape_c * self.shape_z * self.shape_t) - # TODO: current ZStack positions vary faster than time # NOTE: Uncomment this if we want positions to vary faster than time - t = frame_id // (self.shape_c * self.shape_z * self.positions) - p = (frame_id // (self.shape_c * self.shape_z)) % self.positions + # t = frame_id // (self.shape_c * self.shape_z * self.positions) + # p = (frame_id // (self.shape_c * self.shape_z)) % self.positions else: # Timepoint acquisition, only c varies faster than t From 15dabc082f7153b21acc8dbb61f7a796588bb955 Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 12 Nov 2024 14:05:05 -0800 Subject: [PATCH 22/23] minor updates --- src/navigate/model/data_sources/tiff_data_source.py | 2 +- src/navigate/model/features/volume_search.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/navigate/model/data_sources/tiff_data_source.py b/src/navigate/model/data_sources/tiff_data_source.py index 76dc924b1..1e7c3e8b7 100644 --- a/src/navigate/model/data_sources/tiff_data_source.py +++ b/src/navigate/model/data_sources/tiff_data_source.py @@ -330,7 +330,7 @@ def close(self, internal=False) -> None: if self.mode == "w": if not internal: self._check_shape(self._current_frame - 1, self.metadata.per_stack) - if type(self.image) == list: + if type(self.image) is list: for ch in range(len(self.image)): self.image[ch].close() if self.is_ome and len(self._views) > 0: diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index f96ca053d..fa6cee609 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -539,7 +539,7 @@ def __init__( self.current_pixel_size = current_pixel_size #: int: The filter pixel number - self.filter_pixel_number = 10 + self.filter_pixel_number = filter_pixel_number #: dict: Feature configuration self.config_table = { @@ -584,12 +584,12 @@ def data_func(self, frame_ids): position = [ pos_dict[f"{axis}_pos"] for axis in ["x", "y", "z", "theta", "f"] ] - # current stage position is the end of z - position[2] -= z_end else: position = self.model.configuration["experiment"]["MultiPositions"][ self.position_id ] + # current stage position is the end of z + position[2] -= z_end current_microscope_name = self.model.active_microscope_name current_zoom_value = self.model.active_microscope.zoom.zoomvalue From 81c5064e8287d055c0b3e37648faf1714889dbad Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Tue, 12 Nov 2024 15:36:45 -0800 Subject: [PATCH 23/23] use updated multi_position table --- src/navigate/model/features/volume_search.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/navigate/model/features/volume_search.py b/src/navigate/model/features/volume_search.py index fa6cee609..a337d3c32 100644 --- a/src/navigate/model/features/volume_search.py +++ b/src/navigate/model/features/volume_search.py @@ -557,7 +557,7 @@ def data_func(self, frame_ids): "MicroscopeState" ] - if self.position_id > microscope_state_config["multiposition_count"]: + if self.position_id > len(self.model.configuration["multi_positions"]): self.position_id = 0 z_stack_data = self.model.image_writer.data_source.get_data( @@ -585,7 +585,7 @@ def data_func(self, frame_ids): pos_dict[f"{axis}_pos"] for axis in ["x", "y", "z", "theta", "f"] ] else: - position = self.model.configuration["experiment"]["MultiPositions"][ + position = self.model.configuration["multi_positions"][ self.position_id ] # current stage position is the end of z @@ -666,8 +666,7 @@ def data_func(self, frame_ids): ) self.model.event_queue.put(("multiposition", positions)) - self.model.configuration["experiment"]["MultiPositions"] = positions - microscope_state_config["multiposition_count"] = len(positions) + self.model.configuration["multi_positions"] = positions if len(positions) > 0: microscope_state_config["is_multiposition"] = True