Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b196a89
add the ability to start a new saving session
annie-xd-wang Oct 11, 2024
f1224c3
add tiff data reader
annie-xd-wang Oct 11, 2024
134b201
update multipositions as ListProxy
annie-xd-wang Oct 12, 2024
f57ac0a
update tiff data source and image writer
annie-xd-wang Oct 12, 2024
f669f7a
add multiposition option to ZStack
annie-xd-wang Oct 12, 2024
80404dc
save data before running data node
annie-xd-wang Oct 12, 2024
a1db04e
3D volume search feature
annie-xd-wang Oct 12, 2024
95b1a68
reorganize features
annie-xd-wang Oct 16, 2024
9986885
add new feature: update experiment values
annie-xd-wang Oct 16, 2024
61bb66d
map positions
annie-xd-wang Oct 18, 2024
effa43f
typo
annie-xd-wang Oct 18, 2024
c7500cf
implement stage offset
annie-xd-wang Oct 24, 2024
1bddd44
typo
annie-xd-wang Oct 24, 2024
2b5e469
add x and y direction alignment
annie-xd-wang Oct 29, 2024
ca2d836
update dictionary parameters of features
annie-xd-wang Nov 6, 2024
035e17b
update volume search z position
annie-xd-wang Nov 6, 2024
e861929
update volume search function
annie-xd-wang Nov 7, 2024
0e7475e
Revert "update multipositions as ListProxy"
annie-xd-wang Nov 7, 2024
b3a9f7f
update experiment values
annie-xd-wang Nov 7, 2024
15b9003
update image writer if experiment values are changed
annie-xd-wang Nov 7, 2024
4bf0165
Merge branch 'develop' into 3D-volume-search
annie-xd-wang Nov 7, 2024
9ef9e14
revert data_source
annie-xd-wang Nov 8, 2024
15dabc0
minor updates
annie-xd-wang Nov 12, 2024
32c0ba1
Merge remote-tracking branch 'upstream/develop' into 3D-volume-search
annie-xd-wang Nov 12, 2024
81c5064
use updated multi_position table
annie-xd-wang Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/navigate/controller/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,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"]:
Expand Down Expand Up @@ -534,7 +544,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
Expand Down
7 changes: 7 additions & 0 deletions src/navigate/controller/sub_controllers/channels_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 6 additions & 0 deletions src/navigate/controller/sub_controllers/channels_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,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

Expand Down
19 changes: 16 additions & 3 deletions src/navigate/controller/sub_controllers/features_popup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import tkinter as tk
from tkinter import messagebox
import inspect
import json
import ast
import os
import platform

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 0 additions & 2 deletions src/navigate/controller/sub_controllers/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
170 changes: 170 additions & 0 deletions src/navigate/model/analysis/boundary_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@
# 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 import measure
from scipy.ndimage import median_filter, binary_fill_holes

import numpy as np
import numpy.typing as npt

Expand Down Expand Up @@ -446,3 +450,169 @@ 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):
"""A default label volume image function

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,
x_direction,
y_direction,
current_pixel_size,
current_image_width,
current_image_height,
target_pixel_size,
target_image_width,
target_image_height,
overlap=0.05,
filter_pixel_number=10,
):
"""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
filter_pixel_number : int
The pixel number to filter a label

Returns
-------
z_range : int
The maximum number of z steps
positions : array
Array of positions
"""
if target_pixel_size >= current_pixel_size:
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

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

# 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

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))

min_x -= shift_x
min_y -= shift_y

z_pos = z + z_start + min_z * z_step

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_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_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
49 changes: 48 additions & 1 deletion src/navigate/model/data_sources/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
# Standard Library Imports
import logging
from typing import Any, Dict
import abc

# Third Party Imports
import numpy.typing as npt
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -293,6 +297,7 @@ 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

# 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)

Expand Down Expand Up @@ -388,6 +393,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."""
Expand All @@ -397,3 +433,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
Loading
Loading