diff --git a/.DS_Store b/.DS_Store index e99174d..0bd9a61 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index b97131e..2fa2ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -68,9 +68,6 @@ instance/ # Scrapy stuff: .scrapy -# Sphinx documentation -docs/_build/ - # PyBuilder target/ diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 0000000..4b9dc2b Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle new file mode 100644 index 0000000..05a771b Binary files /dev/null and b/docs/_build/doctrees/environment.pickle differ diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree new file mode 100644 index 0000000..1163b9f Binary files /dev/null and b/docs/_build/doctrees/index.doctree differ diff --git a/docs/_build/doctrees/main.doctree b/docs/_build/doctrees/main.doctree new file mode 100644 index 0000000..d7f6082 Binary files /dev/null and b/docs/_build/doctrees/main.doctree differ diff --git a/docs/_build/doctrees/modules.doctree b/docs/_build/doctrees/modules.doctree new file mode 100644 index 0000000..3d29f04 Binary files /dev/null and b/docs/_build/doctrees/modules.doctree differ diff --git a/docs/_build/doctrees/nellie.doctree b/docs/_build/doctrees/nellie.doctree new file mode 100644 index 0000000..4e2334f Binary files /dev/null and b/docs/_build/doctrees/nellie.doctree differ diff --git a/docs/_build/doctrees/nellie.feature_extraction.doctree b/docs/_build/doctrees/nellie.feature_extraction.doctree new file mode 100644 index 0000000..767eec9 Binary files /dev/null and b/docs/_build/doctrees/nellie.feature_extraction.doctree differ diff --git a/docs/_build/doctrees/nellie.im_info.doctree b/docs/_build/doctrees/nellie.im_info.doctree new file mode 100644 index 0000000..fd8de85 Binary files /dev/null and b/docs/_build/doctrees/nellie.im_info.doctree differ diff --git a/docs/_build/doctrees/nellie.segmentation.doctree b/docs/_build/doctrees/nellie.segmentation.doctree new file mode 100644 index 0000000..5d606cc Binary files /dev/null and b/docs/_build/doctrees/nellie.segmentation.doctree differ diff --git a/docs/_build/doctrees/nellie.tracking.doctree b/docs/_build/doctrees/nellie.tracking.doctree new file mode 100644 index 0000000..70350a0 Binary files /dev/null and b/docs/_build/doctrees/nellie.tracking.doctree differ diff --git a/docs/_build/doctrees/nellie.utils.doctree b/docs/_build/doctrees/nellie.utils.doctree new file mode 100644 index 0000000..158f731 Binary files /dev/null and b/docs/_build/doctrees/nellie.utils.doctree differ diff --git a/docs/_build/doctrees/nellie_napari.doctree b/docs/_build/doctrees/nellie_napari.doctree new file mode 100644 index 0000000..8615146 Binary files /dev/null and b/docs/_build/doctrees/nellie_napari.doctree differ diff --git a/docs/_build/doctrees/tests.doctree b/docs/_build/doctrees/tests.doctree new file mode 100644 index 0000000..0ed1ace Binary files /dev/null and b/docs/_build/doctrees/tests.doctree differ diff --git a/docs/_build/doctrees/tests.unit.doctree b/docs/_build/doctrees/tests.unit.doctree new file mode 100644 index 0000000..550051c Binary files /dev/null and b/docs/_build/doctrees/tests.unit.doctree differ diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo new file mode 100644 index 0000000..a01bcbb --- /dev/null +++ b/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 44e986329f1902031559e4ed323a7345 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/_modules/index.html b/docs/_build/html/_modules/index.html new file mode 100644 index 0000000..1fcb9ad --- /dev/null +++ b/docs/_build/html/_modules/index.html @@ -0,0 +1,120 @@ + + + + + + + Overview: module code — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/main.html b/docs/_build/html/_modules/main.html new file mode 100644 index 0000000..6eea22b --- /dev/null +++ b/docs/_build/html/_modules/main.html @@ -0,0 +1,111 @@ + + + + + + + main — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for main

+import napari
+from nellie_napari import NellieLoader
+
+
+
+[docs] +def main(): + viewer = napari.Viewer() + napari.run()
+ + + +if __name__ == "__main__": + main() + +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/cli.html b/docs/_build/html/_modules/nellie/cli.html new file mode 100644 index 0000000..b5b96ee --- /dev/null +++ b/docs/_build/html/_modules/nellie/cli.html @@ -0,0 +1,132 @@ + + + + + + + nellie.cli — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.cli

+import argparse
+import os
+from nellie.run import run
+
+
+
+[docs] +def process_files(files, ch, num_t, output_dir): + for file_num, tif_file in enumerate(files): + print(f'Processing file {file_num + 1} of {len(files)}, channel {ch + 1} of 1') + try: + _ = run(tif_file, remove_edges=False, ch=ch, num_t=num_t, output_dirpath=output_dir) + except: + print(f'Failed to run {tif_file}') + continue
+ + + +
+[docs] +def process_directory(directory, substring, output_dir, ch, num_t): + all_files = sorted([os.path.join(directory, f) for f in os.listdir(directory) if substring in f and f.endswith('.tiff')]) + process_files(all_files, ch, num_t, output_dir)
+ + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Process TIF images within subdirectories containing a specific substring.') + parser.add_argument('--directory', required=True, help='Subdirectory with TIF files') + parser.add_argument('--substring', required=True, help='Substring to look for in filenames') + parser.add_argument('--output_directory', default=None, help='Output directory. Default is parent + nellie_output.') + parser.add_argument('--ch', type=int, default=0, help='Channel number to process') + parser.add_argument('--num_t', type=int, default=1, help='Number of time points') + args = parser.parse_args() + + process_directory(args.directory, args.substring, args.output_directory, args.ch, args.num_t) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/feature_extraction/hierarchical.html b/docs/_build/html/_modules/nellie/feature_extraction/hierarchical.html new file mode 100644 index 0000000..abf7329 --- /dev/null +++ b/docs/_build/html/_modules/nellie/feature_extraction/hierarchical.html @@ -0,0 +1,2066 @@ + + + + + + + nellie.feature_extraction.hierarchical — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.feature_extraction.hierarchical

+import os.path
+import pickle
+
+import numpy as np
+from scipy import spatial
+from skimage.measure import regionprops
+
+from nellie import logger
+from nellie.im_info.verifier import ImInfo
+from nellie.tracking.flow_interpolation import FlowInterpolator
+import pandas as pd
+import time
+
+
+
+[docs] +class Hierarchy: + """ + A class to handle the hierarchical structure of image data, including voxel, node, branch, and component features. + + Parameters + ---------- + im_info : ImInfo + Object containing metadata and pathways related to the image. + skip_nodes : bool, optional + Whether to skip node processing (default is True). + viewer : optional + Viewer for updating status messages (default is None). + + Attributes + ---------- + im_info : ImInfo + The ImInfo object containing the image metadata. + num_t : int + Number of time frames in the image. + spacing : tuple + Spacing between dimensions (Z, Y, X) or (Y, X) depending on the presence of Z. + im_raw : memmap + Raw image data loaded from disk. + im_struct : memmap + Preprocessed structural image data. + im_distance : memmap + Distance-transformed image data. + im_skel : memmap + Skeletonized image data. + label_components : memmap + Instance-labeled image data of components. + label_branches : memmap + Re-labeled skeleton data of branches. + im_border_mask : memmap + Image data with border mask. + im_pixel_class : memmap + Image data classified by pixel types. + im_obj_reassigned : memmap + Object reassigned labels across time. + im_branch_reassigned : memmap + Branch reassigned labels across time. + flow_interpolator_fw : FlowInterpolator + Forward flow interpolator. + flow_interpolator_bw : FlowInterpolator + Backward flow interpolator. + viewer : optional + Viewer to display status updates. + """ + def __init__(self, im_info: ImInfo, skip_nodes=True, + viewer=None): + self.im_info = im_info + self.num_t = self.im_info.shape[0] + if self.im_info.no_z: + self.spacing = (self.im_info.dim_res['Y'], self.im_info.dim_res['X']) + else: + self.spacing = (self.im_info.dim_res['Z'], self.im_info.dim_res['Y'], self.im_info.dim_res['X']) + + self.skip_nodes = skip_nodes + + self.im_raw = None + self.im_struct = None + self.im_distance = None + self.im_skel = None + self.im_pixel_class = None + self.label_components = None + self.label_branches = None + self.im_border_mask = None + self.im_pixel_class = None + self.im_obj_reassigned = None + self.im_branch_reassigned = None + + self.flow_interpolator_fw = FlowInterpolator(im_info) + self.flow_interpolator_bw = FlowInterpolator(im_info, forward=False) + + # self.shape = None + + self.voxels = None + self.nodes = None + self.branches = None + self.components = None + self.image = None + + self.viewer = viewer + + def _get_t(self): + """ + Retrieves the number of time frames from image metadata, raising an error if the information is insufficient. + + Returns + ------- + int + Number of time frames. + """ + if self.num_t is None and not self.im_info.no_t: + # if self.im_info.no_t: + # raise ValueError("No time dimension in image.") + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + # if self.num_t < 3: + # raise ValueError("num_t must be at least 3") + return self.num_t + + def _allocate_memory(self): + """ + Loads the required image data into memory using memory-mapped arrays. This includes raw image data, structural + data, skeletons, component labels, and other features related to the image pipeline. + """ + # getting reshaped image will load the image into memory.. should probably do this case by case + self.im_raw = self.im_info.get_memmap(self.im_info.im_path) + self.im_struct = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) + self.im_distance = self.im_info.get_memmap(self.im_info.pipeline_paths['im_distance']) + self.im_skel = self.im_info.get_memmap(self.im_info.pipeline_paths['im_skel']) + self.label_components = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) + self.label_branches = self.im_info.get_memmap(self.im_info.pipeline_paths['im_skel_relabelled']) + self.im_border_mask = self.im_info.get_memmap(self.im_info.pipeline_paths['im_border']) + self.im_pixel_class = self.im_info.get_memmap(self.im_info.pipeline_paths['im_pixel_class']) + if not self.im_info.no_t: + if os.path.exists(self.im_info.pipeline_paths['im_obj_label_reassigned']) and os.path.exists( + self.im_info.pipeline_paths['im_branch_label_reassigned']): + self.im_obj_reassigned = self.im_info.get_memmap(self.im_info.pipeline_paths['im_obj_label_reassigned']) + self.im_branch_reassigned = self.im_info.get_memmap(self.im_info.pipeline_paths['im_branch_label_reassigned']) + + # self.im_obj_reassigned = get_reshaped_image(im_obj_reassigned, self.num_t, self.im_info) + # self.im_branch_reassigned = get_reshaped_image(im_branch_reassigned, self.num_t, self.im_info) + # self.im_raw = get_reshaped_image(im_raw, self.num_t, self.im_info) + # self.im_struct = get_reshaped_image(im_struct, self.num_t, self.im_info) + # self.label_components = get_reshaped_image(label_components, self.num_t, self.im_info) + # self.label_branches = get_reshaped_image(label_branches, self.num_t, self.im_info) + # self.im_skel = get_reshaped_image(im_skel, self.num_t, self.im_info) + # self.im_pixel_class = get_reshaped_image(im_pixel_class, self.num_t, self.im_info) + # self.im_distance = get_reshaped_image(im_distance, self.num_t, self.im_info) + # self.im_border_mask = get_reshaped_image(im_border_mask, self.num_t, self.im_info) + + # self.shape = self.im_raw.shape + # self.im_info.shape = self.shape + + def _get_hierarchies(self): + """ + Executes the hierarchical feature extraction process, which includes running voxel, node, branch, component, + and image analyses. + """ + self.voxels = Voxels(self) + logger.info("Running voxel analysis") + start = time.time() + self.voxels.run() + end = time.time() + v_time = end - start + + self.nodes = Nodes(self) + logger.info("Running node analysis") + start = time.time() + self.nodes.run() + end = time.time() + n_time = end - start + + self.branches = Branches(self) + logger.info("Running branch analysis") + start = time.time() + self.branches.run() + end = time.time() + b_time = end - start + + self.components = Components(self) + logger.info("Running component analysis") + start = time.time() + self.components.run() + end = time.time() + c_time = end - start + + self.image = Image(self) + logger.info("Running image analysis") + start = time.time() + self.image.run() + end = time.time() + i_time = end - start + + logger.debug(f"Voxel analysis took {v_time} seconds") + logger.debug(f"Node analysis took {n_time} seconds") + logger.debug(f"Branch analysis took {b_time} seconds") + logger.debug(f"Component analysis took {c_time} seconds") + logger.debug(f"Image analysis took {i_time} seconds") + + def _save_dfs(self): + """ + Saves the extracted features to CSV files, including voxel, node, branch, component, and image features. + """ + + if self.viewer is not None: + self.viewer.status = f'Saving features to csv files.' + voxel_features, voxel_headers = create_feature_array(self.voxels) + voxel_df = pd.DataFrame(voxel_features, columns=voxel_headers) + voxel_df.to_csv(self.im_info.pipeline_paths['features_voxels'], index=True) + + if not self.skip_nodes: + node_features, node_headers = create_feature_array(self.nodes) + node_df = pd.DataFrame(node_features, columns=node_headers) + node_df.to_csv(self.im_info.pipeline_paths['features_nodes'], index=True) + + branch_features, branch_headers = create_feature_array(self.branches, self.branches.branch_label) + branch_df = pd.DataFrame(branch_features, columns=branch_headers) + branch_df.to_csv(self.im_info.pipeline_paths['features_branches'], index=True) + + component_features, component_headers = create_feature_array(self.components, self.components.component_label) + component_df = pd.DataFrame(component_features, columns=component_headers) + component_df.to_csv(self.im_info.pipeline_paths['features_organelles'], index=True) + + image_features, image_headers = create_feature_array(self.image) + image_df = pd.DataFrame(image_features, columns=image_headers) + image_df.to_csv(self.im_info.pipeline_paths['features_image'], index=True) + + def _save_adjacency_maps(self): + """ + Constructs adjacency maps for voxels, nodes, branches, and components and saves them as a pickle file. + """ + # edge list: + v_n = [] + v_b = [] + v_o = [] + # v_i = [] + for t in range(len(self.voxels.time)): + num_voxels = len(self.voxels.coords[t]) + if not self.skip_nodes: + # num_nodes = len(self.nodes.nodes[t]) + # max_frame_nodes = np.max(self.nodes.nodes[t]) + max_frame_nodes = len(self.nodes.nodes[t]) + v_n_temp = np.zeros((num_voxels, max_frame_nodes), dtype=bool) + for voxel, nodes in enumerate(self.voxels.node_labels[t]): + if len(nodes) == 0: + continue + v_n_temp[voxel, nodes-1] = True + v_n.append(np.argwhere(v_n_temp)) + + # num_branches = len(self.branches.branch_label[t]) + max_frame_branches = np.max(self.voxels.branch_labels[t]) + v_b_temp = np.zeros((num_voxels, max_frame_branches), dtype=bool) + for voxel, branches in enumerate(self.voxels.branch_labels[t]): + if branches == 0: + continue + v_b_temp[voxel, branches-1] = True + v_b.append(np.argwhere(v_b_temp)) + + # v_b_matrix = self.voxels.branch_labels[t][:, None] == self.branches.branch_label[t] + # v_b.append(np.argwhere(v_b_matrix)) + + # num_organelles = len(self.components.component_label[t]) + max_frame_organelles = np.max(self.voxels.component_labels[t]) + v_o_temp = np.zeros((num_voxels, max_frame_organelles+1), dtype=bool) + for voxel, components in enumerate(self.voxels.component_labels[t]): + if components == 0: + continue + v_o_temp[voxel, components] = True + v_o.append(np.argwhere(v_o_temp)) + # v_o_matrix = self.voxels.component_labels[t][:, None] == self.components.component_label[t] + # v_o.append(np.argwhere(v_o_matrix)) + + # v_i_matrix = np.ones((len(self.voxels.coords[t]), 1), dtype=bool) + # v_i.append(np.argwhere(v_i_matrix)) + + n_b = [] + n_o = [] + # n_i = [] + if not self.skip_nodes: + for t in range(len(self.nodes.time)): + n_b_matrix = self.nodes.branch_label[t][:, None] == self.branches.branch_label[t] + n_b.append(np.argwhere(n_b_matrix)) + + n_o_matrix = self.nodes.component_label[t][:, None] == self.components.component_label[t] + n_o.append(np.argwhere(n_o_matrix)) + + # n_i_matrix = np.ones((len(self.nodes.nodes[t]), 1), dtype=bool) + # n_i.append(np.argwhere(n_i_matrix)) + + b_o = [] + # b_i = [] + for t in range(len(self.branches.time)): + b_o_matrix = self.branches.component_label[t][:, None] == self.components.component_label[t] + b_o.append(np.argwhere(b_o_matrix)) + + # b_i_matrix = np.ones((len(self.branches.branch_label[t]), 1), dtype=bool) + # b_i.append(np.argwhere(b_i_matrix)) + + # o_i = [] + # for t in range(len(self.components.time)): + # o_i_matrix = np.ones((len(self.components.component_label[t]), 1), dtype=bool) + # o_i.append(np.argwhere(o_i_matrix)) + + # create a dict with all the edges + # could also link voxels between t frames + edges = { + "v_b": v_b, "v_n": v_n, "v_o": v_o, # "v_i": v_i, + # "n_v": n_v, + "n_b": n_b, "n_o": n_o, # "n_i": n_i, + # "b_v": b_v, "b_n": b_n, + "b_o": b_o, # "b_i": b_i, + # "o_v": o_v, "o_n": o_n, "o_b": o_b, # "o_i": o_i, + # "i_v": i_v, "i_n": i_n, "i_b": i_b, # "i_o": i_o, + } + # pickle and save + with open(self.im_info.pipeline_paths['adjacency_maps'], "wb") as f: + pickle.dump(edges, f) + +
+[docs] + def run(self): + """ + Main function to run the entire hierarchical feature extraction process, which includes memory allocation, + hierarchical analysis, and saving the results. + """ + self._get_t() + self._allocate_memory() + self._get_hierarchies() + self._save_dfs() + if self.viewer is not None: + self.viewer.status = f'Finalizing run.' + self._save_adjacency_maps() + if self.viewer is not None: + self.viewer.status = f'Done!'
+
+ + + +
+[docs] +def append_to_array(to_append): + """ + Converts feature dictionaries into lists of arrays and headers for saving to a CSV. + + Parameters + ---------- + to_append : dict + Dictionary containing feature names and values to append. + + Returns + ------- + list + List of feature arrays. + list + List of corresponding feature headers. + """ + new_array = [] + new_headers = [] + for feature, stats in to_append.items(): + if type(stats) is not dict: + stats = {'raw': stats} + stats['raw'] = [np.array(stats['raw'])] + for stat, vals in stats.items(): + vals = np.array(vals)[0] + # if len(vals.shape) > 1: + # for i in range(len(vals[0])): + # new_array.append(vals[:, i]) + # new_headers.append(f'{feature}_{stat}_{i}') + # else: + new_array.append(vals) + new_headers.append(f'{feature}_{stat}') + return new_array, new_headers
+ + + +
+[docs] +def create_feature_array(level, labels=None): + """ + Creates a 2D feature array and corresponding headers for saving features to CSV. + + Parameters + ---------- + level : object + The level (e.g., voxel, node, branch, etc.) from which features are extracted. + labels : array-like, optional + Array of labels to use for the first column of the output (default is None). + + Returns + ------- + numpy.ndarray + 2D array of features. + list + List of corresponding feature headers. + """ + full_array = None + headers = None + all_attr = [] + attr_dict = [] + if node_attr := getattr(level, 'aggregate_node_metrics', None): + all_attr.append(node_attr) + if voxel_attr := getattr(level, 'aggregate_voxel_metrics', None): + all_attr.append(voxel_attr) + if branch_attr := getattr(level, 'aggregate_branch_metrics', None): + all_attr.append(branch_attr) + if component_attr := getattr(level, 'aggregate_component_metrics', None): + all_attr.append(component_attr) + inherent_features = level.features_to_save + for feature in inherent_features: + if feature_vals := getattr(level, feature, None): + all_attr.append([{feature: feature_vals[t]} for t in range(len(feature_vals))]) + + for t in range(len(all_attr[0])): + time_dict = {} + for attr in all_attr: + time_dict.update(attr[t]) + attr_dict.append(time_dict) + + for t in range(len(attr_dict)): + to_append = attr_dict[t] + time_array, new_headers = append_to_array(to_append) + if labels is None: + labels_t = np.array(range(len(time_array[0]))) + else: + labels_t = labels[t] + time_array.insert(0, labels_t) + # append a list of t values to the start of time_array + time_array.insert(0, np.array([t] * len(time_array[0]))) + if headers is None: + headers = new_headers + if full_array is None: + full_array = np.array(time_array).T + else: + time_array = np.array(time_array).T + full_array = np.vstack([full_array, time_array]) + + headers.insert(0, 'label') + headers.insert(0, 't') + return full_array, headers
+ + + +
+[docs] +class Voxels: + """ + A class to extract and store voxel-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted voxel features. + coords : list + List of voxel coordinates. + intensity : list + List of voxel intensity values. + structure : list + List of voxel structural values. + vec01 : list + List of vectors from frame t-1 to t. + vec12 : list + List of vectors from frame t to t+1. + lin_vel : list + List of linear velocity vectors. + ang_vel : list + List of angular velocity vectors. + directionality_rel : list + List of directionality features. + node_labels : list + List of node labels assigned to voxels. + branch_labels : list + List of branch labels assigned to voxels. + component_labels : list + List of component labels assigned to voxels. + node_dim0_lims : list + List of node bounding box limits in dimension 0 (Z or Y). + node_dim1_lims : list + List of node bounding box limits in dimension 1 (Y or X). + node_dim2_lims : list + List of node bounding box limits in dimension 2 (X). + node_voxel_idxs : list + List of voxel indices associated with each node. + stats_to_aggregate : list + List of statistics to aggregate for features. + features_to_save : list + List of voxel features to save. + """ + def __init__(self, hierarchy: Hierarchy): + self.hierarchy = hierarchy + + self.time = [] + self.coords = [] + + # add voxel metrics + self.x = [] + self.y = [] + self.z = [] + self.intensity = [] + self.structure = [] + + self.vec01 = [] + self.vec12 = [] + + # self.ang_acc = [] + self.ang_acc_mag = [] + self.ang_vel_mag = [] + # self.ang_vel_orient = [] + self.ang_vel = [] + # self.lin_acc = [] + self.lin_acc_mag = [] + self.lin_vel_mag = [] + # self.lin_vel_orient = [] + self.lin_vel = [] + + # self.ang_acc_rel = [] + self.ang_acc_mag_rel = [] + self.ang_vel_mag_rel = [] + # self.ang_vel_orient_rel = [] + # self.ang_vel_rel = [] + # self.lin_acc_rel = [] + self.lin_acc_mag_rel = [] + self.lin_vel_mag_rel = [] + # self.lin_vel_orient_rel = [] + # self.lin_vel_rel = [] + self.directionality_rel = [] + # self.directionality_acc_rel = [] + + # self.ang_acc_com = [] + # self.ang_acc_com_mag = [] + # self.ang_vel_mag_com = [] + # self.ang_vel_orient_com = [] + # self.ang_vel_com = [] + # self.lin_acc_com = [] + # self.lin_acc_com_mag = [] + # self.lin_vel_mag_com = [] + # self.lin_vel_orient_com = [] + # self.lin_vel_com = [] + # self.directionality_com = [] + # self.directionality_acc_com = [] + + self.node_labels = [] + self.branch_labels = [] + self.component_labels = [] + self.image_name = [] + + self.node_dim0_lims = [] + self.node_dim1_lims = [] + self.node_dim2_lims = [] + self.node_voxel_idxs = [] + + self.stats_to_aggregate = [ + "lin_vel_mag", "lin_vel_mag_rel", "ang_vel_mag", "ang_vel_mag_rel", + "lin_acc_mag", "lin_acc_mag_rel", "ang_acc_mag", "ang_acc_mag_rel", + "directionality_rel", "structure", "intensity" + ] + + # self.stats_to_aggregate = [ + # "ang_acc", "ang_acc_com", "ang_acc_com_mag", "ang_acc_mag", "ang_acc_rel", "ang_acc_rel_mag", + # "ang_vel", "ang_vel_com", "ang_vel_mag", "ang_vel_mag_com", "ang_vel_mag_rel", + # "ang_vel_orient", "ang_vel_orient_com", "ang_vel_orient_rel", "ang_vel_rel", + # "directionality_acc_com", "directionality_acc_rel", "directionality_com", "directionality_rel", + # "lin_acc", "lin_acc_com", "lin_acc_com_mag", "lin_acc_mag", "lin_acc_rel", "lin_acc_rel_mag", + # "lin_vel", "lin_vel_com", "lin_vel_mag", "lin_vel_mag_rel", "lin_vel_orient", "lin_vel_orient_com", + # "lin_vel_orient_rel", "lin_vel_mag_com", + # "lin_vel_rel", "intensity", "structure" + # ] + + self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] + + def _get_node_info(self, t, frame_coords): + """ + Gathers node-related information for each frame, including pixel classes, skeleton radii, and bounding boxes. + + Parameters + ---------- + t : int + The time frame index. + frame_coords : array-like + The coordinates of the voxels in the current frame. + """ + # get all network pixels + skeleton_pixels = np.argwhere(self.hierarchy.im_pixel_class[t] > 0) + skeleton_radius = self.hierarchy.im_distance[t][tuple(skeleton_pixels.T)] + + # create bounding boxes of size largest_thickness around each skeleton pixel + lims_dim0 = (skeleton_radius[:, np.newaxis] * np.array([-1, 1]) + skeleton_pixels[:, 0, np.newaxis]).astype(int) + lims_dim0[:, 1] += 1 + lims_dim1 = (skeleton_radius[:, np.newaxis] * np.array([-1, 1]) + skeleton_pixels[:, 1, np.newaxis]).astype(int) + lims_dim1[:, 1] += 1 + + lims_dim0[lims_dim0 < 0] = 0 + lims_dim1[lims_dim1 < 0] = 0 + + if not self.hierarchy.im_info.no_z: + lims_dim2 = (skeleton_radius[:, np.newaxis] * np.array([-1, 1]) + skeleton_pixels[:, 2, np.newaxis]).astype( + int) + lims_dim2[:, 1] += 1 + lims_dim2[lims_dim2 < 0] = 0 + max_dim0 = self.hierarchy.im_info.shape[self.hierarchy.im_info.axes.index('Z')] + max_dim1 = self.hierarchy.im_info.shape[self.hierarchy.im_info.axes.index('Y')] + max_dim2 = self.hierarchy.im_info.shape[self.hierarchy.im_info.axes.index('X')] + lims_dim2[lims_dim2 > max_dim2] = max_dim2 + else: + max_dim0 = self.hierarchy.im_info.shape[self.hierarchy.im_info.axes.index('Y')] + max_dim1 = self.hierarchy.im_info.shape[self.hierarchy.im_info.axes.index('X')] + + lims_dim0[lims_dim0 > max_dim0] = max_dim0 + lims_dim1[lims_dim1 > max_dim1] = max_dim1 + + self.node_dim0_lims.append(lims_dim0) + self.node_dim1_lims.append(lims_dim1) + + self.node_dim2_lims.append(lims_dim2) if not self.hierarchy.im_info.no_z else None + + frame_coords = np.array(frame_coords) + # process frame coords in chunks of 1000 max + chunk_size = 10000 + num_chunks = int(np.ceil(len(frame_coords) / chunk_size)) + chunk_node_voxel_idxs = {idx: [] for idx in range(len(skeleton_pixels))} + chunk_nodes_idxs = [] + for chunk_num in range(num_chunks): + logger.debug(f"Processing chunk {chunk_num + 1} of {num_chunks}") + start = chunk_num * chunk_size + end = min((chunk_num + 1) * chunk_size, len(frame_coords)) + chunk_frame_coords = frame_coords[start:end] + + if not self.hierarchy.im_info.no_z: + dim0_coords, dim1_coords, dim2_coords = chunk_frame_coords[:, 0], chunk_frame_coords[:, 1], chunk_frame_coords[:, 2] + dim0_mask = (lims_dim0[:, 0][:, None] <= dim0_coords) & (lims_dim0[:, 1][:, None] >= dim0_coords) + dim1_mask = (lims_dim1[:, 0][:, None] <= dim1_coords) & (lims_dim1[:, 1][:, None] >= dim1_coords) + dim2_mask = (lims_dim2[:, 0][:, None] <= dim2_coords) & (lims_dim2[:, 1][:, None] >= dim2_coords) + mask = dim0_mask & dim1_mask & dim2_mask + else: + dim0_coords, dim1_coords = chunk_frame_coords[:, 0], chunk_frame_coords[:, 1] + dim0_mask = (lims_dim0[:, 0][:, None] <= dim0_coords) & (lims_dim0[:, 1][:, None] >= dim0_coords) + dim1_mask = (lims_dim1[:, 0][:, None] <= dim1_coords) & (lims_dim1[:, 1][:, None] >= dim1_coords) + mask = dim0_mask & dim1_mask + + # improve efficiency + frame_coord_nodes_idxs = [[] for _ in range(mask.shape[1])] + rows, cols = np.nonzero(mask) + for row, col in zip(rows, cols): + frame_coord_nodes_idxs[col].append(row) + frame_coord_nodes_idxs = [np.array(indices) for indices in frame_coord_nodes_idxs] + + chunk_nodes_idxs.extend(frame_coord_nodes_idxs) + + for i in range(skeleton_pixels.shape[0]): + chunk_node_voxel_idxs[i].extend(np.nonzero(mask[i])[0] + start) + + # Append the result + self.node_labels.append(chunk_nodes_idxs) + # convert chunk_node_voxel_idxs to a list of arrays + chunk_node_voxel_idxs = [np.array(chunk_node_voxel_idxs[i]) for i in range(len(skeleton_pixels))] + self.node_voxel_idxs.append(chunk_node_voxel_idxs) + + def _get_min_euc_dist(self, t, vec): + """ + Calculates the minimum Euclidean distance for voxels to their nearest branch. + + Parameters + ---------- + t : int + The time frame index. + vec : array-like + The vector field for the current time frame. + + Returns + ------- + pandas.Series + The indices of the minimum distance for each branch. + """ + euc_dist = np.linalg.norm(vec, axis=1) + branch_labels = self.branch_labels[t] + + df = pd.DataFrame({'euc_dist': euc_dist, 'branch_label': branch_labels}) + df = df[~np.isnan(df['euc_dist'])] + idxmin = df.groupby('branch_label')['euc_dist'].idxmin() + # if there are no non-nan values in a branch, give idxmin at that branch index a value of nan + missing_branches = np.setdiff1d(np.unique(branch_labels), df['branch_label']) + for missing_branch in missing_branches: + idxmin[missing_branch] = np.nan + + return idxmin + + def _get_ref_coords(self, coords_a, coords_b, idxmin, t): + """ + Retrieves the reference coordinates for calculating relative velocity and acceleration. + + Parameters + ---------- + coords_a : array-like + The coordinates in frame A. + coords_b : array-like + The coordinates in frame B. + idxmin : pandas.Series + Indices of minimum Euclidean distances. + t : int + The time frame index. + + Returns + ------- + tuple + The reference coordinates for frames A and B. + """ + vals_a = idxmin[self.branch_labels[t]].values + vals_a_no_nan = vals_a.copy() + vals_a_no_nan[np.isnan(vals_a_no_nan)] = 0 + vals_a_no_nan = vals_a_no_nan.astype(int) + + vals_b = idxmin[self.branch_labels[t]].values + vals_b_no_nan = vals_b.copy() + vals_b_no_nan[np.isnan(vals_b_no_nan)] = 0 + vals_b_no_nan = vals_b_no_nan.astype(int) + ref_a = coords_a[vals_a_no_nan] + ref_b = coords_b[vals_b_no_nan] + + ref_a[np.isnan(vals_a)] = np.nan + ref_b[np.isnan(vals_b)] = np.nan + + return ref_a, ref_b + + def _get_motility_stats(self, t, coords_1_px): + """ + Computes motility-related features for each voxel, including linear and angular velocities, accelerations, + and directionality. + + Parameters + ---------- + t : int + The time frame index. + coords_1_px : array-like + Coordinates of the voxels in pixel space for frame t. + """ + coords_1_px = coords_1_px.astype('float32') + if self.hierarchy.im_info.no_z: + dims = 2 + else: + dims = 3 + + vec01 = [] + vec12 = [] + if t > 0: + vec01_px = self.hierarchy.flow_interpolator_bw.interpolate_coord(coords_1_px, t) + vec01 = vec01_px * self.hierarchy.spacing + self.vec01.append(vec01) + else: + self.vec01.append(np.full((len(coords_1_px), dims), np.nan)) + + if t < self.hierarchy.num_t - 1: + vec12_px = self.hierarchy.flow_interpolator_fw.interpolate_coord(coords_1_px, t) + vec12 = vec12_px * self.hierarchy.spacing + self.vec12.append(vec12) + else: + self.vec12.append(np.full((len(coords_1_px), dims), np.nan)) + + coords_1 = coords_1_px * self.hierarchy.spacing + # coords_com_1 = np.nanmean(coords_1, axis=0) + # r1_rel_com = coords_1 - coords_com_1 + # r1_com_mag = np.linalg.norm(r1_rel_com, axis=1) + + if len(vec01) and len(vec12): + coords_0_px = coords_1_px - vec01_px + coords_0 = coords_0_px * self.hierarchy.spacing + + lin_vel_01, lin_vel_mag_01, lin_vel_orient_01 = self._get_linear_velocity(coords_0, coords_1) + ang_vel_01, ang_vel_mag_01, ang_vel_orient_01 = self._get_angular_velocity(coords_0, coords_1) + + idxmin01 = self._get_min_euc_dist(t, vec01) + ref_coords01 = self._get_ref_coords(coords_0, coords_1, idxmin01, t) + ref_coords01[0][np.isnan(vec01)] = np.nan + ref_coords01[1][np.isnan(vec01)] = np.nan + r0_rel_01 = coords_0 - ref_coords01[0] + r1_rel_01 = coords_1 - ref_coords01[1] + + lin_vel_rel_01, lin_vel_mag_rel_01, lin_vel_orient_rel_01 = self._get_linear_velocity(r0_rel_01, r1_rel_01) + ang_vel_rel_01, ang_vel_mag_rel_01, ang_vel_orient_rel_01 = self._get_angular_velocity(r0_rel_01, r1_rel_01) + + # coords_com_0 = np.nanmean(coords_0, axis=0) + # r0_rel_com = coords_0 - coords_com_0 + # lin_vel_com_01, lin_vel_mag_com_01, lin_vel_orient_com_01 = self._get_linear_velocity(r0_rel_com, + # r1_rel_com) + # ang_vel_com_01, ang_vel_mag_com_01, ang_vel_orient_com_01 = self._get_angular_velocity(r0_rel_com, + # r1_rel_com) + + # r0_com_mag = np.linalg.norm(r0_rel_com, axis=1) +# directionality_com_01 = np.abs(r0_com_mag - r1_com_mag) / (r0_com_mag + r1_com_mag) + +# r0_rel_mag_01 = np.linalg.norm(r0_rel_01, axis=1) +# r1_rel_mag_01 = np.linalg.norm(r1_rel_01, axis=1) +# directionality_rel_01 = np.abs(r0_rel_mag_01 - r1_rel_mag_01) / (r0_rel_mag_01 + r1_rel_mag_01) + + if len(vec12): + coords_2_px = coords_1_px + vec12_px + coords_2 = coords_2_px * self.hierarchy.spacing + + lin_vel, lin_vel_mag, lin_vel_orient = self._get_linear_velocity(coords_1, coords_2) + ang_vel, ang_vel_mag, ang_vel_orient = self._get_angular_velocity(coords_1, coords_2) + + idxmin12 = self._get_min_euc_dist(t, vec12) + ref_coords12 = self._get_ref_coords(coords_1, coords_2, idxmin12, t) + ref_coords12[0][np.isnan(vec12)] = np.nan + ref_coords12[1][np.isnan(vec12)] = np.nan + r1_rel_12 = coords_1 - ref_coords12[0] + r2_rel_12 = coords_2 - ref_coords12[1] + + lin_vel_rel, lin_vel_mag_rel, lin_vel_orient_rel = self._get_linear_velocity(r1_rel_12, r2_rel_12) + ang_vel_rel, ang_vel_mag_rel, ang_vel_orient_rel = self._get_angular_velocity(r1_rel_12, r2_rel_12) + + # coords_com_2 = np.nanmean(coords_2, axis=0) + # r2_rel_com = coords_2 - coords_com_2 + # lin_vel_com, lin_vel_mag_com, lin_vel_orient_com = self._get_linear_velocity(r1_rel_com, r2_rel_com) + # ang_vel_com, ang_vel_mag_com, ang_vel_orient_com = self._get_angular_velocity(r1_rel_com, r2_rel_com) + + # r2_com_mag = np.linalg.norm(r2_rel_com, axis=1) +# directionality_com = np.abs(r2_com_mag - r1_com_mag) / (r2_com_mag + r1_com_mag) + + r2_rel_mag_12 = np.linalg.norm(r2_rel_12, axis=1) + r1_rel_mag_12 = np.linalg.norm(r1_rel_12, axis=1) + directionality_rel = np.abs(r2_rel_mag_12 - r1_rel_mag_12) / (r2_rel_mag_12 + r1_rel_mag_12) + else: + # vectors of nans + lin_vel = np.full((len(coords_1), dims), np.nan) + lin_vel_mag = np.full(len(coords_1), np.nan) + # lin_vel_orient = np.full((len(coords_1), dims), np.nan) + ang_vel_mag = np.full(len(coords_1), np.nan) + lin_vel_rel = np.full((len(coords_1), dims), np.nan) + lin_vel_mag_rel = np.full(len(coords_1), np.nan) +# lin_vel_orient_rel = np.full((len(coords_1), dims), np.nan) + ang_vel_mag_rel = np.full(len(coords_1), np.nan) +# lin_vel_com = np.full((len(coords_1), dims), np.nan) +# lin_vel_mag_com = np.full(len(coords_1), np.nan) +# lin_vel_orient_com = np.full((len(coords_1), dims), np.nan) +# ang_vel_mag_com = np.full(len(coords_1), np.nan) +# directionality_com = np.full(len(coords_1), np.nan) + directionality_rel = np.full(len(coords_1), np.nan) + if dims == 3: + ang_vel = np.full((len(coords_1), dims), np.nan) +# ang_vel_orient = np.full((len(coords_1), dims), np.nan) + ang_vel_rel = np.full((len(coords_1), dims), np.nan) +# ang_vel_orient_rel = np.full((len(coords_1), dims), np.nan) +# ang_vel_com = np.full((len(coords_1), dims), np.nan) +# ang_vel_orient_com = np.full((len(coords_1), dims), np.nan) + else: + ang_vel = np.full(len(coords_1), np.nan) +# ang_vel_orient = np.full(len(coords_1), np.nan) + ang_vel_rel = np.full(len(coords_1), np.nan) +# ang_vel_orient_rel = np.full(len(coords_1), np.nan) +# ang_vel_com = np.full(len(coords_1), np.nan) +# ang_vel_orient_com = np.full(len(coords_1), np.nan) + self.lin_vel.append(lin_vel) + self.lin_vel_mag.append(lin_vel_mag) + # self.lin_vel_orient.append(lin_vel_orient) + self.ang_vel.append(ang_vel) + self.ang_vel_mag.append(ang_vel_mag) + # self.ang_vel_orient.append(ang_vel_orient) + # self.lin_vel_rel.append(lin_vel_rel) + self.lin_vel_mag_rel.append(lin_vel_mag_rel) + # self.lin_vel_orient_rel.append(lin_vel_orient_rel) + # self.ang_vel_rel.append(ang_vel_rel) + self.ang_vel_mag_rel.append(ang_vel_mag_rel) + # self.ang_vel_orient_rel.append(ang_vel_orient_rel) + # self.lin_vel_com.append(lin_vel_com) + # self.lin_vel_mag_com.append(lin_vel_mag_com) + # self.lin_vel_orient_com.append(lin_vel_orient_com) + # self.ang_vel_com.append(ang_vel_com) + # self.ang_vel_mag_com.append(ang_vel_mag_com) + # self.ang_vel_orient_com.append(ang_vel_orient_com) + # self.directionality_com.append(directionality_com) + self.directionality_rel.append(directionality_rel) + + if len(vec01) and len(vec12): + lin_acc = (lin_vel - lin_vel_01) / self.hierarchy.im_info.dim_res['T'] + lin_acc_mag = np.linalg.norm(lin_acc, axis=1) + ang_acc = (ang_vel - ang_vel_01) / self.hierarchy.im_info.dim_res['T'] + lin_acc_rel = (lin_vel_rel - lin_vel_rel_01) / self.hierarchy.im_info.dim_res['T'] + lin_acc_rel_mag = np.linalg.norm(lin_acc_rel, axis=1) + ang_acc_rel = (ang_vel_rel - ang_vel_rel_01) / self.hierarchy.im_info.dim_res['T'] + # lin_acc_com = (lin_vel_com - lin_vel_com_01) / self.hierarchy.im_info.dim_res['T'] + # lin_acc_com_mag = np.linalg.norm(lin_acc_com, axis=1) +# ang_acc_com = (ang_vel_com - ang_vel_com_01) / self.hierarchy.im_info.dim_res['T'] + if self.hierarchy.im_info.no_z: + ang_acc_mag = np.abs(ang_acc) + ang_acc_rel_mag = np.abs(ang_acc_rel) +# ang_acc_com_mag = np.abs(ang_acc_com) + else: + ang_acc_mag = np.linalg.norm(ang_acc, axis=1) + ang_acc_rel_mag = np.linalg.norm(ang_acc_rel, axis=1) +# ang_acc_com_mag = np.linalg.norm(ang_acc_com, axis=1) + # directionality acceleration is the change of directionality based on + # directionality_com_01 and directionality_com + # directionality_acc_com = np.abs(directionality_com - directionality_com_01) + # directionality_acc_rel = np.abs(directionality_rel - directionality_rel_01) + else: + # vectors of nans + # lin_acc = np.full((len(coords_1), dims), np.nan) + lin_acc_mag = np.full(len(coords_1), np.nan) + ang_acc_mag = np.full(len(coords_1), np.nan) +# lin_acc_rel = np.full((len(coords_1), dims), np.nan) + lin_acc_rel_mag = np.full(len(coords_1), np.nan) + ang_acc_rel_mag = np.full(len(coords_1), np.nan) +# lin_acc_com = np.full((len(coords_1), dims), np.nan) +# lin_acc_com_mag = np.full(len(coords_1), np.nan) +# ang_acc_com_mag = np.full(len(coords_1), np.nan) +# directionality_acc_com = np.full(len(coords_1), np.nan) +# directionality_acc_rel = np.full(len(coords_1), np.nan) +# if dims == 3: +# ang_acc = np.full((len(coords_1), dims), np.nan) +# ang_acc_rel = np.full((len(coords_1), dims), np.nan) +# ang_acc_com = np.full((len(coords_1), dims), np.nan) +# else: +# ang_acc = np.full(len(coords_1), np.nan) +# ang_acc_rel = np.full(len(coords_1), np.nan) +# ang_acc_com = np.full(len(coords_1), np.nan) + # self.lin_acc.append(lin_acc) + self.lin_acc_mag.append(lin_acc_mag) +# self.ang_acc.append(ang_acc) + self.ang_acc_mag.append(ang_acc_mag) +# self.lin_acc_rel.append(lin_acc_rel) + self.lin_acc_mag_rel.append(lin_acc_rel_mag) +# self.ang_acc_rel.append(ang_acc_rel) + self.ang_acc_mag_rel.append(ang_acc_rel_mag) +# self.lin_acc_com.append(lin_acc_com) +# self.lin_acc_com_mag.append(lin_acc_com_mag) +# self.ang_acc_com.append(ang_acc_com) +# self.ang_acc_com_mag.append(ang_acc_com_mag) +# self.directionality_acc_com.append(directionality_acc_com) +# self.directionality_acc_rel.append(directionality_acc_rel) + + def _get_linear_velocity(self, ra, rb): + """ + Computes the linear velocity, its magnitude, and orientation between two sets of coordinates. + + Parameters + ---------- + ra : array-like + Coordinates in the earlier frame (frame t-1 or t). + rb : array-like + Coordinates in the later frame (frame t or t+1). + + Returns + ------- + tuple + Tuple containing linear velocity vectors, magnitudes, and orientations. + """ + lin_disp = rb - ra + lin_vel = lin_disp / self.hierarchy.im_info.dim_res['T'] + lin_vel_mag = np.linalg.norm(lin_vel, axis=1) + lin_vel_orient = (lin_vel.T / lin_vel_mag).T + if self.hierarchy.im_info.no_z: + lin_vel_orient = np.where(np.isinf(lin_vel_orient), [np.nan, np.nan], lin_vel_orient) + else: + lin_vel_orient = np.where(np.isinf(lin_vel_orient), [np.nan, np.nan, np.nan], lin_vel_orient) + + return lin_vel, lin_vel_mag, lin_vel_orient + + def _get_angular_velocity_2d(self, ra, rb): + """ + Computes the angular velocity, its magnitude, and orientation between two sets of coordinates. + Uses either 2D or 3D calculations depending on the image dimensionality. + + Parameters + ---------- + ra : array-like + Coordinates in the earlier frame (frame t-1 or t). + rb : array-like + Coordinates in the later frame (frame t or t+1). + + Returns + ------- + tuple + Tuple containing angular velocity vectors, magnitudes, and orientations. + """ + # calculate angles of ra and rb relative to x-axis + theta_a = np.arctan2(ra[:, 1], ra[:, 0]) + theta_b = np.arctan2(rb[:, 1], rb[:, 0]) + + # calculate the change in angle + delta_theta = theta_b - theta_a + + # normalize the change in angle to be between -pi and pi + delta_theta = (delta_theta + np.pi) % (2 * np.pi) - np.pi + + # get the angular velocity + ang_vel = delta_theta / self.hierarchy.im_info.dim_res['T'] + ang_vel_mag = np.abs(ang_vel) + ang_vel_orient = np.sign(ang_vel) + + return ang_vel, ang_vel_mag, ang_vel_orient + + def _get_angular_velocity_3d(self, ra, rb): + cross_product = np.cross(ra, rb, axis=1) + norm = np.linalg.norm(ra, axis=1) * np.linalg.norm(rb, axis=1) + ang_disp = np.divide(cross_product.T, norm.T).T + ang_disp[norm == 0] = [np.nan, np.nan, np.nan] + + ang_vel = ang_disp / self.hierarchy.im_info.dim_res['T'] + ang_vel_mag = np.linalg.norm(ang_vel, axis=1) + ang_vel_orient = (ang_vel.T / ang_vel_mag).T + ang_vel_orient = np.where(np.isinf(ang_vel_orient), [np.nan, np.nan, np.nan], ang_vel_orient) + + return ang_vel, ang_vel_mag, ang_vel_orient + + def _get_angular_velocity(self, ra, rb): + if self.hierarchy.im_info.no_z: + return self._get_angular_velocity_2d(ra, rb) + + return self._get_angular_velocity_3d(ra, rb) + + def _run_frame(self, t=None): + frame_coords = np.argwhere(self.hierarchy.label_components[t] > 0) + self.coords.append(frame_coords) + + frame_component_labels = self.hierarchy.label_components[t][tuple(frame_coords.T)] + self.component_labels.append(frame_component_labels) + + frame_branch_labels = self.hierarchy.label_branches[t][tuple(frame_coords.T)] + self.branch_labels.append(frame_branch_labels) + + frame_intensity_vals = self.hierarchy.im_raw[t][tuple(frame_coords.T)] + self.intensity.append(frame_intensity_vals) + + if not self.hierarchy.im_info.no_z: + frame_z_vals = frame_coords[:, 0] + self.z.append(frame_z_vals) + frame_y_vals = frame_coords[:, 1] + self.y.append(frame_y_vals) + frame_x_vals = frame_coords[:, 2] + self.x.append(frame_x_vals) + else: + self.z.append(np.full(len(frame_coords), np.nan)) + frame_y_vals = frame_coords[:, 0] + self.y.append(frame_y_vals) + frame_x_vals = frame_coords[:, 1] + self.x.append(frame_x_vals) + + frame_structure_vals = self.hierarchy.im_struct[t][tuple(frame_coords.T)] + self.structure.append(frame_structure_vals) + + frame_t = np.ones(frame_coords.shape[0], dtype=int) * t + self.time.append(frame_t) + + im_name = np.ones(frame_coords.shape[0], dtype=object) * self.hierarchy.im_info.file_info.filename_no_ext + self.image_name.append(im_name) + + if not self.hierarchy.skip_nodes: + self._get_node_info(t, frame_coords) + self._get_motility_stats(t, frame_coords) + +
+[docs] + def run(self): + """ + Main function to run the extraction of voxel features over all time frames. + Iterates over each frame to extract coordinates, intensity, structural features, and motility statistics. + """ + if self.hierarchy.num_t is None: + self.hierarchy.num_t = 1 + for t in range(self.hierarchy.num_t): + if self.hierarchy.viewer is not None: + self.hierarchy.viewer.status = f'Extracting voxel features. Frame: {t + 1} of {self.hierarchy.num_t}.' + self._run_frame(t)
+
+ + + +
+[docs] +def aggregate_stats_for_class(child_class, t, list_of_idxs): + # initialize a dictionary to hold lists of aggregated stats for each stat name + # aggregate_stats = { + # stat_name: {"mean": [], "std_dev": [], "25%": [], "50%": [], "75%": [], "min": [], "max": [], "range": [], + # "sum": []} for + # stat_name in child_class.stats_to_aggregate if stat_name != 'reassigned_label'} + aggregate_stats = { + stat_name: { + "mean": [], "std_dev": [], "min": [], "max": [], "sum": []} for + stat_name in child_class.stats_to_aggregate if stat_name != 'reassigned_label' + } + + largest_idx = max([len(idxs) for idxs in list_of_idxs]) + for stat_name in child_class.stats_to_aggregate: + if stat_name == 'reassigned_label': + continue + # access the relevant attribute for the current time frame + stat_array = np.array(getattr(child_class, stat_name)[t]) + + # add a column of nans to the end of the stat array + if len(stat_array.shape) > 1: + continue # just skip these... probably no one will use them + # nan_vector = np.full((1, stat_array.shape[1]), np.nan) + # stat_array = np.vstack([stat_array, nan_vector]) + else: + stat_array = np.append(stat_array, np.nan) + + # create a big np array of all the idxs in list of idxs + idxs_array = np.full((len(list_of_idxs), largest_idx), len(stat_array) - 1, dtype=int) + for i, idxs in enumerate(list_of_idxs): + idxs_array[i, :len(idxs)] = idxs + + # populate the idxs_array with the values from the stat_array at the indices in idxs_array + stat_values = stat_array[idxs_array.astype(int)] + + # calculate various statistics for the subset + mean = np.nanmean(stat_values, axis=1) + std_dev = np.nanstd(stat_values, axis=1) + # quartiles = np.nanquantile(stat_values, [0.25, 0.5, 0.75], axis=1) + min_val = np.nanmin(stat_values, axis=1) + max_val = np.nanmax(stat_values, axis=1) + # range_val = max_val - min_val + sum_val = np.nansum(stat_values, axis=1) + + # append the calculated statistics to their respective lists in the aggregate_stats dictionary + aggregate_stats[stat_name]["mean"].append(mean) + aggregate_stats[stat_name]["std_dev"].append(std_dev) + # aggregate_stats[stat_name]["25%"].append(quartiles[0]) + # aggregate_stats[stat_name]["50%"].append(quartiles[1]) # median + # aggregate_stats[stat_name]["75%"].append(quartiles[2]) + aggregate_stats[stat_name]["min"].append(min_val) + aggregate_stats[stat_name]["max"].append(max_val) + # aggregate_stats[stat_name]["range"].append(range_val) + aggregate_stats[stat_name]["sum"].append(sum_val) + + return aggregate_stats
+ + + +
+[docs] +class Nodes: + """ + A class to extract and store node-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted node features. + nodes : list + List of node coordinates for each frame. + z, x, y : list + List of node coordinates in 3D or 2D space. + node_thickness : list + List of node thickness values. + divergence : list + List of divergence values for nodes. + convergence : list + List of convergence values for nodes. + vergere : list + List of vergere values (convergence + divergence). + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each node. + voxel_idxs : list + List of voxel indices associated with each node. + branch_label : list + List of branch labels assigned to nodes. + component_label : list + List of component labels assigned to nodes. + image_name : list + List of image file names. + stats_to_aggregate : list + List of statistics to aggregate for nodes. + features_to_save : list + List of node features to save. + """ + def __init__(self, hierarchy): + self.hierarchy = hierarchy + + self.time = [] + self.nodes = [] + + + # add voxel aggregate metrics + self.aggregate_voxel_metrics = [] + # add node metrics + self.z = [] + self.x = [] + self.y = [] + self.node_thickness = [] + self.divergence = [] + self.convergence = [] + self.vergere = [] + # self.lin_magnitude_variability = [] + # self.ang_magnitude_variability = [] + # self.lin_direction_uniformity = [] + # self.ang_direction_uniformity = [] + + self.stats_to_aggregate = [ + "divergence", "convergence", "vergere", "node_thickness", + # "lin_magnitude_variability", "ang_magnitude_variability", + # "lin_direction_uniformity", "ang_direction_uniformity", + ] + + self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] + + self.voxel_idxs = self.hierarchy.voxels.node_voxel_idxs + self.branch_label = [] + self.component_label = [] + self.image_name = [] + + self.node_z_lims = self.hierarchy.voxels.node_dim0_lims + self.node_y_lims = self.hierarchy.voxels.node_dim1_lims + self.node_x_lims = self.hierarchy.voxels.node_dim2_lims + + def _get_aggregate_voxel_stats(self, t): + """ + Aggregates voxel-level statistics for each node in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ + frame_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, self.hierarchy.voxels.node_voxel_idxs[t]) + self.aggregate_voxel_metrics.append(frame_agg) + + def _get_node_stats(self, t): + """ + Computes node-level statistics, including thickness, divergence, convergence, and vergere. + + Parameters + ---------- + t : int + The time frame index. + """ + radius = distance_check(self.hierarchy.im_border_mask[t], self.nodes[t], self.hierarchy.spacing) + self.node_thickness.append(radius * 2) + + divergence = [] + convergence = [] + vergere = [] + # lin_mag_variability = [] + # ang_mag_variability = [] + # lin_dir_uniformity = [] + # ang_dir_uniformity = [] + z = [] + y = [] + x = [] + for i, node in enumerate(self.nodes[t]): + vox_idxs = self.voxel_idxs[t][i] + if len(vox_idxs) == 0: + divergence.append(np.nan) + convergence.append(np.nan) + vergere.append(np.nan) + # lin_mag_variability.append(np.nan) + # ang_mag_variability.append(np.nan) + # lin_dir_uniformity.append(np.nan) + # ang_dir_uniformity.append(np.nan) + z.append(np.nan) + y.append(np.nan) + x.append(np.nan) + continue + + if not self.hierarchy.im_info.no_z: + z.append(np.nanmean(self.hierarchy.voxels.coords[t][vox_idxs][:, 0]) * self.hierarchy.spacing[0]) + y.append(np.nanmean(self.hierarchy.voxels.coords[t][vox_idxs][:, 1]) * self.hierarchy.spacing[1]) + x.append(np.nanmean(self.hierarchy.voxels.coords[t][vox_idxs][:, 2]) * self.hierarchy.spacing[2]) + else: + z.append(np.nan) + y.append(np.nanmean(self.hierarchy.voxels.coords[t][vox_idxs][:, 0]) * self.hierarchy.spacing[0]) + x.append(np.nanmean(self.hierarchy.voxels.coords[t][vox_idxs][:, 1]) * self.hierarchy.spacing[1]) + + dist_vox_node = self.hierarchy.voxels.coords[t][vox_idxs] - self.nodes[t][i] + dist_vox_node_mag = np.linalg.norm(dist_vox_node, axis=1, keepdims=True) + dir_vox_node = dist_vox_node / dist_vox_node_mag + + dot_prod_01 = -np.nanmean(np.sum(-self.hierarchy.voxels.vec01[t][vox_idxs] * dir_vox_node, axis=1)) + convergence.append(dot_prod_01) + + dot_prod_12 = np.nanmean(np.sum(self.hierarchy.voxels.vec12[t][vox_idxs] * dir_vox_node, axis=1)) + divergence.append(dot_prod_12) + + vergere.append(dot_prod_01 + dot_prod_12) + # high vergere is a funnel point (converges then diverges) + # low vergere is a dispersal point (diverges then converges) + + # lin_vel_mag = self.hierarchy.voxels.lin_vel_mag[t][vox_idxs] + # lin_mag_variability.append(np.nanstd(lin_vel_mag)) + # ang_vel_mag = self.hierarchy.voxels.ang_vel_mag[t][vox_idxs] + # ang_mag_variability.append(np.nanstd(ang_vel_mag)) + # + # lin_vel = self.hierarchy.voxels.lin_vel[t][vox_idxs] + # lin_unit_vec = lin_vel / lin_vel_mag[:, np.newaxis] + # lin_similarity_matrix = np.dot(lin_unit_vec, lin_unit_vec.T) + # np.fill_diagonal(lin_similarity_matrix, np.nan) + # lin_dir_uniformity.append(np.nanmean(lin_similarity_matrix)) + # + # ang_vel = self.hierarchy.voxels.ang_vel[t][vox_idxs] + # ang_unit_vec = ang_vel / ang_vel_mag[:, np.newaxis] + # ang_similarity_matrix = np.dot(ang_unit_vec, ang_unit_vec.T) + # np.fill_diagonal(ang_similarity_matrix, np.nan) + # ang_dir_uniformity.append(np.nanmean(ang_similarity_matrix)) + self.divergence.append(divergence) + self.convergence.append(convergence) + self.vergere.append(vergere) + # self.lin_magnitude_variability.append(lin_mag_variability) + # self.ang_magnitude_variability.append(ang_mag_variability) + # self.lin_direction_uniformity.append(lin_dir_uniformity) + # self.ang_direction_uniformity.append(ang_dir_uniformity) + self.z.append(z) + self.y.append(y) + self.x.append(x) + + def _run_frame(self, t): + """ + Extracts node features for a single time frame, including voxel metrics, node coordinates, and node statistics. + + Parameters + ---------- + t : int + The time frame index. + """ + frame_skel_coords = np.argwhere(self.hierarchy.im_pixel_class[t] > 0) + self.nodes.append(frame_skel_coords) + + frame_t = np.ones(frame_skel_coords.shape[0], dtype=int) * t + self.time.append(frame_t) + + frame_component_label = self.hierarchy.label_components[t][tuple(frame_skel_coords.T)] + self.component_label.append(frame_component_label) + + frame_branch_label = self.hierarchy.label_branches[t][tuple(frame_skel_coords.T)] + self.branch_label.append(frame_branch_label) + + im_name = np.ones(frame_skel_coords.shape[0], dtype=object) * self.hierarchy.im_info.file_info.filename_no_ext + self.image_name.append(im_name) + + self._get_aggregate_voxel_stats(t) + self._get_node_stats(t) + +
+[docs] + def run(self): + """ + Main function to run the extraction of node features over all time frames. + Iterates over each frame to extract node features and calculate metrics. + """ + if self.hierarchy.skip_nodes: + return + for t in range(self.hierarchy.num_t): + if self.hierarchy.viewer is not None: + self.hierarchy.viewer.status = f'Extracting node features. Frame: {t + 1} of {self.hierarchy.num_t}.' + self._run_frame(t)
+
+ + + +
+[docs] +def distance_check(border_mask, check_coords, spacing): + border_coords = np.argwhere(border_mask) * spacing + border_tree = spatial.cKDTree(border_coords) + dist, _ = border_tree.query(check_coords * spacing, k=1) + return dist
+ + + +
+[docs] +class Branches: + """ + A class to extract and store branch-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted branch features. + branch_label : list + List of branch labels for each frame. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each branch. + aggregate_node_metrics : list + List of aggregated node metrics for each branch. + z, x, y : list + List of branch centroid coordinates in 3D or 2D space. + branch_length : list + List of branch length values. + branch_thickness : list + List of branch thickness values. + branch_aspect_ratio : list + List of aspect ratios for branches. + branch_tortuosity : list + List of tortuosity values for branches. + branch_area : list + List of branch area values. + branch_axis_length_maj, branch_axis_length_min : list + List of major and minor axis lengths for branches. + branch_extent : list + List of extent values for branches. + branch_solidity : list + List of solidity values for branches. + reassigned_label : list + List of reassigned branch labels across time. + stats_to_aggregate : list + List of statistics to aggregate for branches. + features_to_save : list + List of branch features to save. + """ + def __init__(self, hierarchy): + self.hierarchy = hierarchy + + self.time = [] + self.branch_label = [] + + self.aggregate_voxel_metrics = [] + self.aggregate_node_metrics = [] + # add branch metrics + self.z = [] + self.y = [] + self.x = [] + self.branch_length = [] + self.branch_thickness = [] + self.branch_aspect_ratio = [] + self.branch_tortuosity = [] + self.branch_area = [] + self.branch_axis_length_maj = [] + self.branch_axis_length_min = [] + self.branch_extent = [] + self.branch_solidity = [] + self.reassigned_label = [] + + self.branch_idxs = [] + self.component_label = [] + self.image_name = [] + + self.stats_to_aggregate = [ + "branch_length", "branch_thickness", "branch_aspect_ratio", "branch_tortuosity", "branch_area", "branch_axis_length_maj", "branch_axis_length_min", + "branch_extent", "branch_solidity", "reassigned_label" + ] + + self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] + + def _get_aggregate_stats(self, t): + """ + Aggregates voxel and node-level statistics for each branch in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ + voxel_labels = self.hierarchy.voxels.branch_labels[t] + grouped_vox_idxs = [np.argwhere(voxel_labels == label).flatten() + for label in np.unique(voxel_labels) if label != 0] + vox_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, grouped_vox_idxs) + self.aggregate_voxel_metrics.append(vox_agg) + + if not self.hierarchy.skip_nodes: + node_labels = self.hierarchy.nodes.branch_label[t] + grouped_node_idxs = [np.argwhere(node_labels == label).flatten() + for label in np.unique(node_labels) if label != 0] + node_agg = aggregate_stats_for_class(self.hierarchy.nodes, t, grouped_node_idxs) + self.aggregate_node_metrics.append(node_agg) + + def _get_branch_stats(self, t): + """ + Computes branch-level statistics, including length, thickness, aspect ratio, tortuosity, and solidity. + + Parameters + ---------- + t : int + The time frame index. + """ + branch_idx_array_1 = np.array(self.branch_idxs[t]) + branch_idx_array_2 = np.array(self.branch_idxs[t])[:, None, :] + dist = np.linalg.norm(branch_idx_array_1 - branch_idx_array_2, axis=-1) + dist[dist >= 2] = 0 # remove any distances greater than adjacent pixel + neighbors = np.sum(dist > 0, axis=1) + tips = np.where(neighbors == 1)[0] + lone_tips = np.where(neighbors == 0)[0] + dist = np.triu(dist) + + neighbor_idxs = np.argwhere(dist > 0) + + # all coords idxs should be within 1 pixel of each other + neighbor_coords_0 = self.branch_idxs[t][neighbor_idxs[:, 0]] + neighbor_coords_1 = self.branch_idxs[t][neighbor_idxs[:, 1]] + # assert np.all(np.abs(neighbor_coords_0 - neighbor_coords_1) <= 1) + + # labels should be the exact same + neighbor_labels_0 = self.hierarchy.im_skel[t][tuple(neighbor_coords_0.T)] + neighbor_labels_1 = self.hierarchy.im_skel[t][tuple(neighbor_coords_1.T)] + # assert np.all(neighbor_labels_0 == neighbor_labels_1) + + scaled_coords_0 = neighbor_coords_0 * self.hierarchy.spacing + scaled_coords_1 = neighbor_coords_1 * self.hierarchy.spacing + distances = np.linalg.norm(scaled_coords_0 - scaled_coords_1, axis=1) + unique_labels = np.unique(self.hierarchy.im_skel[t][self.hierarchy.im_skel[t] > 0]) + + label_lengths = np.zeros(len(unique_labels)) + for i, label in enumerate(unique_labels): + label_lengths[i] = np.sum(distances[neighbor_labels_0 == label]) + + lone_tip_coords = self.branch_idxs[t][lone_tips] + tip_coords = self.branch_idxs[t][tips] + + lone_tip_labels = self.hierarchy.im_skel[t][tuple(lone_tip_coords.T)] + tip_labels = self.hierarchy.im_skel[t][tuple(tip_coords.T)] + + # find the distance between the two tips with the same label in tip_labels + tip_distances = np.zeros(len(tip_labels)) + for i, label in enumerate(tip_labels): + matched_idxs = tip_coords[tip_labels == label] * self.hierarchy.spacing + tip_distances[i] = np.linalg.norm(matched_idxs[0] - matched_idxs[1]) + + tortuosity = np.zeros(len(unique_labels)) + for i, label in enumerate(unique_labels): + tip_dist = tip_distances[tip_labels == label] + if len(tip_dist): + tortuosity[i] = label_lengths[i] / tip_dist[0] + else: + tortuosity[i] = 1 + + self.branch_tortuosity.append(tortuosity) + + radii = distance_check(self.hierarchy.im_border_mask[t], self.branch_idxs[t], self.hierarchy.spacing) + lone_tip_radii = radii[lone_tips] + tip_radii = radii[tips] + + for label, radius in zip(lone_tip_labels, lone_tip_radii): + label_lengths[unique_labels == label] += radius * 2 + for label, radius in zip(tip_labels, tip_radii): + label_lengths[unique_labels == label] += radius + + # mean radii for each branch: + median_thickenss = [] + thicknesses = radii * 2 + for label, thickness in zip(unique_labels, thicknesses): + median_thickenss.append(np.median(thickness)) + + # if thickness at an index is larger than the length, set it to the length, and length to thickness + for i, thickness in enumerate(median_thickenss): + if thickness > label_lengths[i]: + median_thickenss[i] = label_lengths[i] + label_lengths[i] = thickness + + aspect_ratios = label_lengths / median_thickenss + + self.branch_aspect_ratio.append(aspect_ratios) + self.branch_thickness.append(median_thickenss) + self.branch_length.append(label_lengths) + + regions = regionprops(self.hierarchy.label_branches[t], spacing=self.hierarchy.spacing) + areas = [] + axis_length_maj = [] + axis_length_min = [] + extent = [] + solidity = [] + reassigned_label = [] + z = [] + y = [] + x = [] + for region in regions: + reassigned_label_region = np.nan + if not self.hierarchy.im_info.no_t: + if self.hierarchy.im_branch_reassigned is not None: + region_reassigned_labels = self.hierarchy.im_branch_reassigned[t][tuple(region.coords.T)] + # find which label is most common in the region via bin-counting + reassigned_label_region = np.argmax(np.bincount(region_reassigned_labels)) + reassigned_label.append(reassigned_label_region) + areas.append(region.area) + # due to bug in skimage (at the time of writing: https://github.com/scikit-image/scikit-image/issues/6630) + try: + maj_axis = region.major_axis_length + min_axis = region.minor_axis_length + except ValueError: + maj_axis = np.nan + min_axis = np.nan + axis_length_maj.append(maj_axis) + axis_length_min.append(min_axis) + extent.append(region.extent) + solidity.append(region.solidity) + if not self.hierarchy.im_info.no_z: + z.append(region.centroid[0]) + y.append(region.centroid[1]) + x.append(region.centroid[2]) + else: + z.append(np.nan) + y.append(region.centroid[0]) + x.append(region.centroid[1]) + self.branch_area.append(areas) + self.branch_axis_length_maj.append(axis_length_maj) + self.branch_axis_length_min.append(axis_length_min) + self.branch_extent.append(extent) + self.branch_solidity.append(solidity) + self.reassigned_label.append(reassigned_label) + self.z.append(z) + self.y.append(y) + self.x.append(x) + + def _run_frame(self, t): + """ + Extracts branch features for a single time frame, including voxel and node metrics, branch coordinates, and + branch statistics. + + Parameters + ---------- + t : int + The time frame index. + """ + frame_branch_idxs = np.argwhere(self.hierarchy.im_skel[t] > 0) + self.branch_idxs.append(frame_branch_idxs) + + frame_skel_branch_labels = self.hierarchy.im_skel[t][tuple(frame_branch_idxs.T)] + + smallest_label = int(np.min(self.hierarchy.im_skel[t][self.hierarchy.im_skel[t] > 0])) + largest_label = int(np.max(self.hierarchy.im_skel[t])) + frame_branch_labels = np.arange(smallest_label, largest_label + 1) + num_branches = len(frame_branch_labels) + + frame_t = np.ones(num_branches, dtype=int) * t + self.time.append(frame_t) + + # get the first voxel idx for each branch + if self.hierarchy.im_info.no_z: + frame_branch_coords = np.zeros((num_branches, 2), dtype=int) + else: + frame_branch_coords = np.zeros((num_branches, 3), dtype=int) + for i in frame_branch_labels: + branch_voxels = frame_branch_idxs[frame_skel_branch_labels == i] + frame_branch_coords[i - 1] = branch_voxels[0] + frame_component_label = self.hierarchy.label_components[t][tuple(frame_branch_coords.T)] + self.component_label.append(frame_component_label) + + frame_branch_label = self.hierarchy.im_skel[t][tuple(frame_branch_coords.T)] + self.branch_label.append(frame_branch_label) + + im_name = np.ones(num_branches, dtype=object) * self.hierarchy.im_info.file_info.filename_no_ext + self.image_name.append(im_name) + + self._get_aggregate_stats(t) + self._get_branch_stats(t) + +
+[docs] + def run(self): + """ + Main function to run the extraction of branch features over all time frames. + Iterates over each frame to extract branch features and calculate metrics. + """ + for t in range(self.hierarchy.num_t): + if self.hierarchy.viewer is not None: + self.hierarchy.viewer.status = f'Extracting branch features. Frame: {t + 1} of {self.hierarchy.num_t}.' + self._run_frame(t)
+
+ + + +
+[docs] +class Components: + """ + A class to extract and store component-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted component features. + component_label : list + List of component labels for each frame. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each component. + aggregate_node_metrics : list + List of aggregated node metrics for each component. + aggregate_branch_metrics : list + List of aggregated branch metrics for each component. + z, x, y : list + List of component centroid coordinates in 3D or 2D space. + organelle_area : list + List of component area values. + organelle_axis_length_maj, organelle_axis_length_min : list + List of major and minor axis lengths for components. + organelle_extent : list + List of extent values for components. + organelle_solidity : list + List of solidity values for components. + reassigned_label : list + List of reassigned component labels across time. + stats_to_aggregate : list + List of statistics to aggregate for components. + features_to_save : list + List of component features to save. + """ + def __init__(self, hierarchy): + self.hierarchy = hierarchy + + self.time = [] + self.component_label = [] + self.aggregate_voxel_metrics = [] + self.aggregate_node_metrics = [] + self.aggregate_branch_metrics = [] + # add component metrics + self.z = [] + self.y = [] + self.x = [] + self.organelle_area = [] + self.organelle_axis_length_maj = [] + self.organelle_axis_length_min = [] + self.organelle_extent = [] + self.organelle_solidity = [] + self.reassigned_label = [] + + self.image_name = [] + + self.stats_to_aggregate = [ + "organelle_area", "organelle_axis_length_maj", "organelle_axis_length_min", "organelle_extent", "organelle_solidity", "reassigned_label", + ] + + self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] + + def _get_aggregate_stats(self, t): + """ + Aggregates voxel, node, and branch-level statistics for each component in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ + voxel_labels = self.hierarchy.voxels.component_labels[t] + grouped_vox_idxs = [np.argwhere(voxel_labels == label).flatten() for label in np.unique(voxel_labels) if + label != 0] + vox_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, grouped_vox_idxs) + self.aggregate_voxel_metrics.append(vox_agg) + + if not self.hierarchy.skip_nodes: + node_labels = self.hierarchy.nodes.component_label[t] + grouped_node_idxs = [np.argwhere(node_labels == label).flatten() for label in np.unique(voxel_labels) if + label != 0] + node_agg = aggregate_stats_for_class(self.hierarchy.nodes, t, grouped_node_idxs) + self.aggregate_node_metrics.append(node_agg) + + branch_labels = self.hierarchy.branches.component_label[t] + grouped_branch_idxs = [np.argwhere(branch_labels == label).flatten() for label in np.unique(voxel_labels) if + label != 0] + branch_agg = aggregate_stats_for_class(self.hierarchy.branches, t, grouped_branch_idxs) + self.aggregate_branch_metrics.append(branch_agg) + + def _get_component_stats(self, t): + """ + Computes component-level statistics, including area, axis lengths, extent, and solidity. + + Parameters + ---------- + t : int + The time frame index. + """ + regions = regionprops(self.hierarchy.label_components[t], spacing=self.hierarchy.spacing) + areas = [] + axis_length_maj = [] + axis_length_min = [] + extent = [] + solidity = [] + reassigned_label = [] + z = [] + y = [] + x = [] + for region in regions: + reassigned_label_region = np.nan + if not self.hierarchy.im_info.no_t: + if self.hierarchy.im_obj_reassigned is not None: + region_reassigned_labels = self.hierarchy.im_obj_reassigned[t][tuple(region.coords.T)] + reassigned_label_region = np.argmax(np.bincount(region_reassigned_labels)) + reassigned_label.append(reassigned_label_region) + areas.append(region.area) + try: + maj_axis = region.major_axis_length + min_axis = region.minor_axis_length + except ValueError: + maj_axis = np.nan + min_axis = np.nan + axis_length_maj.append(maj_axis) + axis_length_min.append(min_axis) + extent.append(region.extent) + solidity.append(region.solidity) + if not self.hierarchy.im_info.no_z: + z.append(region.centroid[0]) + y.append(region.centroid[1]) + x.append(region.centroid[2]) + else: + z.append(np.nan) + y.append(region.centroid[0]) + x.append(region.centroid[1]) + self.organelle_area.append(areas) + self.organelle_axis_length_maj.append(axis_length_maj) + self.organelle_axis_length_min.append(axis_length_min) + self.organelle_extent.append(extent) + self.organelle_solidity.append(solidity) + self.reassigned_label.append(reassigned_label) + self.z.append(z) + self.y.append(y) + self.x.append(x) + + def _run_frame(self, t): + """ + Extracts component features for a single time frame, including voxel, node, and branch metrics, and component + statistics. + + Parameters + ---------- + t : int + The time frame index. + """ + smallest_label = int(np.min(self.hierarchy.label_components[t][self.hierarchy.label_components[t] > 0])) + largest_label = int(np.max(self.hierarchy.label_components[t])) + frame_component_labels = np.arange(smallest_label, largest_label + 1) + self.component_label.append(frame_component_labels) + num_components = len(frame_component_labels) + + frame_t = np.ones(num_components, dtype=int) * t + self.time.append(frame_t) + + im_name = np.ones(num_components, dtype=object) * self.hierarchy.im_info.file_info.filename_no_ext + self.image_name.append(im_name) + + self._get_aggregate_stats(t) + self._get_component_stats(t) + +
+[docs] + def run(self): + """ + Main function to run the extraction of component features over all time frames. + Iterates over each frame to extract component features and calculate metrics. + """ + for t in range(self.hierarchy.num_t): + if self.hierarchy.viewer is not None: + self.hierarchy.viewer.status = f'Extracting organelle features. Frame: {t + 1} of {self.hierarchy.num_t}.' + self._run_frame(t)
+
+ + + +
+[docs] +class Image: + """ + A class to extract and store global image-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted image-level features. + image_name : list + List of image file names. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for the entire image. + aggregate_node_metrics : list + List of aggregated node metrics for the entire image. + aggregate_branch_metrics : list + List of aggregated branch metrics for the entire image. + aggregate_component_metrics : list + List of aggregated component metrics for the entire image. + stats_to_aggregate : list + List of statistics to aggregate for the entire image. + features_to_save : list + List of image-level features to save. + """ + def __init__(self, hierarchy): + self.hierarchy = hierarchy + + self.time = [] + self.image_name = [] + self.aggregate_voxel_metrics = [] + self.aggregate_node_metrics = [] + self.aggregate_branch_metrics = [] + self.aggregate_component_metrics = [] + self.stats_to_aggregate = [] + self.features_to_save = [] + + def _get_aggregate_stats(self, t): + """ + Aggregates voxel, node, branch, and component-level statistics for the entire image in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ + voxel_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, + [np.arange(len(self.hierarchy.voxels.coords[t]))]) + self.aggregate_voxel_metrics.append(voxel_agg) + + if not self.hierarchy.skip_nodes: + node_agg = aggregate_stats_for_class(self.hierarchy.nodes, t, [np.arange(len(self.hierarchy.nodes.nodes[t]))]) + self.aggregate_node_metrics.append(node_agg) + + branch_agg = aggregate_stats_for_class(self.hierarchy.branches, t, + [self.hierarchy.branches.branch_label[t].flatten() - 1]) + self.aggregate_branch_metrics.append(branch_agg) + + component_agg = aggregate_stats_for_class(self.hierarchy.components, t, + [np.arange(len(self.hierarchy.components.component_label[t]))]) + self.aggregate_component_metrics.append(component_agg) + + def _run_frame(self, t): + """ + Extracts image-level features for a single time frame, including aggregated voxel, node, branch, and + component metrics. + + Parameters + ---------- + t : int + The time frame index. + """ + self.time.append(t) + self.image_name.append(self.hierarchy.im_info.file_info.filename_no_ext) + + self._get_aggregate_stats(t) + +
+[docs] + def run(self): + """ + Main function to run the extraction of image-level features over all time frames. + Iterates over each frame to extract and aggregate voxel, node, branch, and component-level features. + """ + for t in range(self.hierarchy.num_t): + if self.hierarchy.viewer is not None: + self.hierarchy.viewer.status = f'Extracting image features. Frame: {t + 1} of {self.hierarchy.num_t}.' + self._run_frame(t)
+
+ + + +if __name__ == "__main__": + + im_path = r"F:\60x_568mito_488phal_dapi_siDRP12_w1iSIM-561_s1 - Stage1 _1_-1.tif" + # im_info = run(im_path, remove_edges=False, ch=0) + im_info = ImInfo(im_path) + num_t = 1 + + hierarchy = Hierarchy(im_info, num_t) + hierarchy.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/im_info/verifier.html b/docs/_build/html/_modules/nellie/im_info/verifier.html new file mode 100644 index 0000000..0376fd5 --- /dev/null +++ b/docs/_build/html/_modules/nellie/im_info/verifier.html @@ -0,0 +1,592 @@ + + + + + + + nellie.im_info.verifier — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.im_info.verifier

+import os
+
+import nd2
+import numpy as np
+import ome_types
+from tifffile import tifffile
+
+from nellie import logger
+
+
+
+[docs] +class FileInfo: + def __init__(self, filepath, output_dir=None): + self.filepath = filepath + self.metadata = None + self.metadata_type = None + self.axes = None + self.shape = None + self.dim_res = None + + self.input_dir = os.path.dirname(filepath) + self.basename = os.path.basename(filepath) + self.filename_no_ext = os.path.splitext(self.basename)[0] + self.extension = os.path.splitext(filepath)[1] + self.output_dir = output_dir or os.path.join(self.input_dir, 'nellie_output') + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + self.nellie_necessities_dir = os.path.join(self.output_dir, 'nellie_necessities') + if not os.path.exists(self.nellie_necessities_dir): + os.makedirs(self.nellie_necessities_dir) + + self.ome_output_path = None + self.good_dims = False + self.good_axes = False + + self.ch = 0 + self.t_start = 0 + self.t_end = None + self.dtype = None + + def _find_tif_metadata(self): + with tifffile.TiffFile(self.filepath) as tif: + if tif.is_ome or tif.ome_metadata is not None: + ome_xml = tifffile.tiffcomment(self.filepath) + metadata = ome_types.from_xml(ome_xml) + metadata_type = 'ome' + elif tif.is_imagej: + metadata = tif.imagej_metadata + metadata_type = 'imagej' + if 'physicalsizex' not in metadata: + metadata_type = 'imagej_tif_tags' + metadata = [metadata, tif.pages[0].tags._dict] + else: + metadata = tif.pages[0].tags._dict + metadata_type = None + + self.metadata = metadata + self.metadata_type = metadata_type + self.axes = tif.series[0].axes + self.shape = tif.series[0].shape + + return metadata, metadata_type + + def _find_nd2_metadata(self): + with nd2.ND2File(self.filepath) as nd2_file: + metadata = nd2_file.metadata.channels[0] + metadata.recorded_data = nd2_file.events(orient='list') + self.metadata = metadata + self.metadata_type = 'nd2' + self.axes = ''.join(nd2_file.sizes.keys()) + self.shape = tuple(nd2_file.sizes.values()) + +
+[docs] + def find_metadata(self): + if self.filepath.endswith('.tiff') or self.filepath.endswith('.tif'): + self._find_tif_metadata() + elif self.filepath.endswith('.nd2'): + self._find_nd2_metadata() + else: + raise ValueError('File type not supported')
+ + + def _get_imagej_metadata(self, metadata): + self.dim_res['X'] = metadata['physicalsizex'] if 'physicalsizex' in metadata else None + self.dim_res['Y'] = metadata['physicalsizey'] if 'physicalsizey' in metadata else None + self.dim_res['Z'] = metadata['spacing'] if 'spacing' in metadata else None + self.dim_res['T'] = metadata['finterval'] if 'finterval' in metadata else None + + def _get_ome_metadata(self, metadata): + self.dim_res['X'] = metadata.images[0].pixels.physical_size_x + self.dim_res['Y'] = metadata.images[0].pixels.physical_size_y + self.dim_res['Z'] = metadata.images[0].pixels.physical_size_z + self.dim_res['T'] = metadata.images[0].pixels.time_increment + + def _get_tif_tags_metadata(self, metadata): + tag_names = {tag_value.name: tag_code for tag_code, tag_value in metadata.items()} + + if 'XResolution' in tag_names: + self.dim_res['X'] = metadata[tag_names['XResolution']].value[1] \ + / metadata[tag_names['XResolution']].value[0] + if 'YResolution' in tag_names: + self.dim_res['Y'] = metadata[tag_names['YResolution']].value[1] \ + / metadata[tag_names['YResolution']].value[0] + if 'ResolutionUnit' in tag_names: + if metadata[tag_names['ResolutionUnit']].value == tifffile.RESUNIT.CENTIMETER: + self.dim_res['X'] *= 1E-4 * 1E6 + self.dim_res['Y'] *= 1E-4 * 1E6 + if 'Z' in self.axes: + if 'ZResolution' in tag_names: + self.dim_res['Z'] = 1 / metadata[tag_names['ZResolution']].value[0] + if 'T' in self.axes: + if 'FrameRate' in tag_names: + self.dim_res['T'] = 1 / metadata[tag_names['FrameRate']].value[0] + + def _get_nd2_metadata(self, metadata): + if 'Time [s]' in metadata.recorded_data: + timestamps = metadata.recorded_data['Time [s]'] + self.dim_res['T'] = timestamps[-1] / len(timestamps) + self.dim_res['X'] = metadata.volume.axesCalibration[0] + self.dim_res['Y'] = metadata.volume.axesCalibration[1] + self.dim_res['Z'] = metadata.volume.axesCalibration[2] + +
+[docs] + def load_metadata(self): + self.dim_res = {'X': None, 'Y': None, 'Z': None, 'T': None} + if self.metadata_type == 'ome': + self._get_ome_metadata(self.metadata) + elif self.metadata_type == 'imagej': + self._get_imagej_metadata(self.metadata) + elif self.metadata_type == 'imagej_tif_tags': + self._get_imagej_metadata(self.metadata[0]) + self._get_tif_tags_metadata(self.metadata[1]) + elif self.metadata_type == 'nd2': + self._get_nd2_metadata(self.metadata) + elif self.metadata_type is None: + self._get_tif_tags_metadata(self.metadata) + self._validate()
+ + + def _check_axes(self): + if len(self.shape) != len(self.axes): + self.change_axes('Q' * len(self.shape)) + for axis in self.axes: + if axis not in ['T', 'Z', 'Y', 'X', 'C']: + self.good_axes = False + return + # if any duplicates, not good + if len(set(self.axes)) != len(self.axes): + self.good_axes = False + return + # if X or Y are not there, not good + if 'X' not in self.axes or 'Y' not in self.axes: + self.good_axes = False + return + self.good_axes = True + + def _check_dim_res(self): + check_dims = ['X', 'Y', 'Z', 'T'] + for dim in check_dims: + if dim in self.axes and self.dim_res[dim] is None: + self.good_dims = False + return + self.good_dims = True + +
+[docs] + def change_axes(self, new_axes): + # if len(new_axes) != len(self.shape): + self.good_axes = False + # return + # raise ValueError('New axes must have the same length as the shape of the data') + self.axes = new_axes + self._validate()
+ + +
+[docs] + def change_dim_res(self, dim, new_size): + if dim not in self.dim_res: + return + # raise ValueError('Invalid dimension') + self.dim_res[dim] = new_size + self._validate()
+ + +
+[docs] + def change_selected_channel(self, ch): + if not self.good_dims or not self.good_axes: + raise ValueError('Must have both valid axes and dimensions to change channel') + if 'C' not in self.axes: + raise KeyError('No channel dimension to change') + if ch < 0 or ch >= self.shape[self.axes.index('C')]: + raise IndexError('Invalid channel index') + self.ch = ch + self._get_output_path()
+ + +
+[docs] + def select_temporal_range(self, start=0, end=None): + if not self.good_dims or not self.good_axes: + return + # raise ValueError('Must have both valid axes and dimensions to select temporal range') + if 'T' not in self.axes: + return + # raise KeyError('No time dimension to select') + self.t_start = start + self.t_end = end + if self.t_end is None: + self.t_end = self.shape[self.axes.index('T')] - 1 + self._get_output_path()
+ + + def _validate(self): + self._check_axes() + self._check_dim_res() + self.select_temporal_range() + self._get_output_path() + +
+[docs] + def read_file(self): + if self.extension == '.nd2': + data = nd2.imread(self.filepath) + elif self.extension == '.tif' or self.extension == '.tiff': + try: + data = tifffile.memmap(self.filepath) + except: + data = tifffile.imread(self.filepath) + else: + logger.error(f'Filetype {self.extension} not supported. Please convert to .nd2 or .tif.') + raise ValueError + self.dtype = data.dtype + return data
+ + + def _get_output_path(self): + t_text = f'-t{self.t_start}_to_{self.t_end}' if 'T' in self.axes else '' + dim_texts = [] + for axis in self.axes: + if axis not in self.dim_res: + continue + dim_res = self.dim_res[axis] + # round to 4 decimal places + if dim_res is None: + dim_res = 'None' + else: + dim_res = str(round(dim_res, 4)) + # convert '.' to 'p' + dim_res = dim_res.replace('.', 'p') + dim_texts.append(f'{axis}{dim_res}') + dim_text = f"-{'_'.join(dim_texts)}" + output_name = f'{self.filename_no_ext}-{self.axes}{dim_text}-ch{self.ch}{t_text}' + self.user_output_path_no_ext = os.path.join(self.output_dir, output_name) + self.nellie_necessities_output_path_no_ext = os.path.join(self.nellie_necessities_dir, output_name) + self.ome_output_path = self.nellie_necessities_output_path_no_ext + '.ome.tif' + +
+[docs] + def save_ome_tiff(self): + if not self.good_axes or not self.good_dims: + raise ValueError('Cannot save file with invalid axes or dimensions') + + axes = self.axes + data = self.read_file() + if 'T' not in self.axes: + data = data[np.newaxis, ...] + axes = 'T' + self.axes + else: + t_index = self.axes.index('T') + selected_range = range(self.t_start, self.t_end + 1) + data = np.take(data, selected_range, axis=t_index) + # if len(selected_range) == 1: + # data = np.expand_dims(data, axis=t_index) + if 'C' in axes: + data = np.take(data, self.ch, axis=axes.index('C')) + axes = axes.replace('C', '') + + # ensure 'T' is the 0th dimension + if 'T' in axes: + t_index = axes.index('T') + data = np.moveaxis(data, t_index, 0) + axes = 'T' + axes.replace('T', '') + + tifffile.imwrite( + self.ome_output_path, data, bigtiff=True, metadata={"axes": axes} + ) + + ome_xml = tifffile.tiffcomment(self.ome_output_path) + ome = ome_types.from_xml(ome_xml) + ome.images[0].pixels.physical_size_x = self.dim_res['X'] + ome.images[0].pixels.physical_size_y = self.dim_res['Y'] + ome.images[0].pixels.physical_size_z = self.dim_res['Z'] + ome.images[0].pixels.time_increment = self.dim_res['T'] + dtype_name = data.dtype.name + if data.dtype.name == 'float64': + dtype_name = 'double' + if data.dtype.name == 'float32': + dtype_name = 'float' + ome.images[0].pixels.type = dtype_name + ome_xml = ome.to_xml() + tifffile.tiffcomment(self.ome_output_path, ome_xml)
+
+ + + +
+[docs] +class ImInfo: + def __init__(self, file_info: FileInfo): + self.file_info = file_info + self.im_path = file_info.ome_output_path + if not os.path.exists(self.im_path): + file_info.save_ome_tiff() + self.im = tifffile.memmap(self.im_path) + + self.screenshot_dir = os.path.join(self.file_info.output_dir, 'screenshots') + self.graph_dir = os.path.join(self.file_info.output_dir, 'graphs') + + self.dim_res = {'X': None, 'Y': None, 'Z': None, 'T': None} + self.axes = None + self.new_axes = None + self.shape = None + self.ome_metadata = None + self._get_ome_metadata() + + self.no_z = True + self.no_t = True + self._check_axes_exist() + + self.pipeline_paths = {} + self._create_output_paths() + + def _check_axes_exist(self): + if 'Z' in self.axes and self.shape[self.new_axes.index('Z')] > 1: + self.no_z = False + if 'T' in self.axes and self.shape[self.new_axes.index('T')] > 1: + self.no_t = False + +
+[docs] + def create_output_path(self, pipeline_path: str, ext: str = '.ome.tif', for_nellie=True): + if for_nellie: + output_path = f'{self.file_info.nellie_necessities_output_path_no_ext}-{pipeline_path}{ext}' + else: + output_path = f'{self.file_info.user_output_path_no_ext}-{pipeline_path}{ext}' + self.pipeline_paths[pipeline_path] = output_path + return self.pipeline_paths[pipeline_path]
+ + + def _create_output_paths(self): + self.create_output_path('im_preprocessed') + self.create_output_path('im_instance_label') + self.create_output_path('im_skel') + self.create_output_path('im_skel_relabelled') + self.create_output_path('im_pixel_class') + self.create_output_path('im_marker') + self.create_output_path('im_distance') + self.create_output_path('im_border') + self.create_output_path('flow_vector_array', ext='.npy') + self.create_output_path('voxel_matches', ext='.npy') + self.create_output_path('im_branch_label_reassigned') + self.create_output_path('im_obj_label_reassigned') + self.create_output_path('features_voxels', ext='.csv', for_nellie=False) + self.create_output_path('features_nodes', ext='.csv', for_nellie=False) + self.create_output_path('features_branches', ext='.csv', for_nellie=False) + self.create_output_path('features_organelles', ext='.csv', for_nellie=False) + self.create_output_path('features_image', ext='.csv', for_nellie=False) + self.create_output_path('adjacency_maps', ext='.pkl') + +
+[docs] + def remove_intermediates(self): + all_pipeline_paths = [self.pipeline_paths[pipeline_path] for pipeline_path in self.pipeline_paths] + for pipeline_path in all_pipeline_paths + [self.im_path]: + if 'csv' in pipeline_path: + continue + elif os.path.exists(pipeline_path): + os.remove(pipeline_path)
+ + + def _get_ome_metadata(self, ): + with tifffile.TiffFile(self.im_path) as tif: + self.axes = tif.series[0].axes + self.new_axes = self.axes + if 'T' not in self.axes: + self.im = self.im[np.newaxis, ...] + self.new_axes = 'T' + self.axes + self.shape = self.im.shape + self.ome_metadata = ome_types.from_xml(tifffile.tiffcomment(self.im_path)) + self.dim_res['X'] = self.ome_metadata.images[0].pixels.physical_size_x + self.dim_res['Y'] = self.ome_metadata.images[0].pixels.physical_size_y + self.dim_res['Z'] = self.ome_metadata.images[0].pixels.physical_size_z + self.dim_res['T'] = self.ome_metadata.images[0].pixels.time_increment + +
+[docs] + def get_memmap(self, file_path, read_mode='r+'): + memmap = tifffile.memmap(file_path, mode=read_mode) + if 'T' not in self.axes: + memmap = memmap[np.newaxis, ...] + return memmap
+ + +
+[docs] + def allocate_memory(self, output_path, dtype='float', data=None, description='No description.', + return_memmap=False, read_mode='r+'): + axes = self.axes + if 'T' not in self.axes: + axes = 'T' + axes + if data is not None: + data = data[np.newaxis, ...] + if data is None: + tifffile.imwrite( + output_path, shape=self.shape, dtype=dtype, bigtiff=True, metadata={"axes": axes} + ) + else: + dtype = dtype or data.dtype.name + tifffile.imwrite( + output_path, data, bigtiff=True, metadata={"axes": axes} + ) + ome = self.ome_metadata + ome.images[0].description = description + ome.images[0].pixels.type = dtype + ome_xml = ome.to_xml() + tifffile.tiffcomment(output_path, ome_xml) + if return_memmap: + return self.get_memmap(output_path, read_mode=read_mode)
+
+ + + +if __name__ == "__main__": + test_dir = '/Users/austin/test_files/nellie_all_tests' + all_paths = os.listdir(test_dir) + all_paths = [os.path.join(test_dir, path) for path in all_paths if path.endswith('.tiff') or path.endswith('.tif') or path.endswith('.nd2')] + # for filepath in all_paths: + # file_info = FileInfo(filepath) + # file_info.find_metadata() + # file_info.load_metadata() + # print(file_info.metadata_type) + # print(file_info.axes) + # print(file_info.shape) + # print(file_info.dim_res) + # print('\n\n') + + test_file = all_paths[1] + file_info = FileInfo(test_file) + file_info.find_metadata() + file_info.load_metadata() + print(f'{file_info.metadata_type=}') + print(f'{file_info.axes=}') + print(f'{file_info.shape=}') + print(f'{file_info.dim_res=}') + print(f'{file_info.good_axes=}') + print(f'{file_info.good_dims=}') + print('\n') + + file_info.change_axes('TZYX') + print('Axes changed') + print(f'{file_info.axes=}') + print(f'{file_info.dim_res=}') + print(f'{file_info.good_axes=}') + print(f'{file_info.good_dims=}') + print('\n') + + file_info.change_dim_res('T', 0.5) + file_info.change_dim_res('Z', 0.2) + + print('Dimension resolutions changed') + print(f'{file_info.axes=}') + print(f'{file_info.dim_res=}') + print(f'{file_info.good_axes=}') + print(f'{file_info.good_dims=}') + print('\n') + + # print(f'{file_info.ch=}') + # file_info.change_selected_channel(3) + # print('Channel changed') + # print(f'{file_info.ch=}') + + print(f'{file_info.t_start=}') + print(f'{file_info.t_end=}') + file_info.select_temporal_range(1, 3) + print('Temporal range selected') + print(f'{file_info.t_start=}') + print(f'{file_info.t_end=}') + + # file_info.save_ome_tiff() + im_info = ImInfo(file_info) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/run.html b/docs/_build/html/_modules/nellie/run.html new file mode 100644 index 0000000..383d5ef --- /dev/null +++ b/docs/_build/html/_modules/nellie/run.html @@ -0,0 +1,212 @@ + + + + + + + nellie.run — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.run

+from nellie.feature_extraction.hierarchical import Hierarchy
+from nellie.im_info.verifier import FileInfo, ImInfo
+from nellie.segmentation.filtering import Filter
+from nellie.segmentation.labelling import Label
+from nellie.segmentation.mocap_marking import Markers
+from nellie.segmentation.networking import Network
+from nellie.tracking.hu_tracking import HuMomentTracking
+from nellie.tracking.voxel_reassignment import VoxelReassigner
+
+
+
+[docs] +def run(file_info, remove_edges=False, otsu_thresh_intensity=False, threshold=None): + im_info = ImInfo(file_info) + preprocessing = Filter(im_info, remove_edges=remove_edges) + preprocessing.run() + + segmenting = Label(im_info, otsu_thresh_intensity=otsu_thresh_intensity, threshold=threshold) + segmenting.run() + + networking = Network(im_info) + networking.run() + + mocap_marking = Markers(im_info) + mocap_marking.run() + + hu_tracking = HuMomentTracking(im_info) + hu_tracking.run() + + vox_reassign = VoxelReassigner(im_info) + vox_reassign.run() + + hierarchy = Hierarchy(im_info) + hierarchy.run() + + return im_info
+ + + +if __name__ == "__main__": + # # Single file run + # im_path = r"/Users/austin/test_files/nellie_all_tests/ND Stimulation Parallel 12.nd2" + # im_info = run(im_path, remove_edges=False, num_t=5) + # im_info = run(im_path, remove_edges=False, ch=1, dim_sizes={'T': 1, 'Z': 0.1, 'Y': 0.1, 'X': 0.1}, otsu_thresh_intensity=True) + + # Directory batch run + # import os + # top_dirs = [ + # r"C:\Users\austin\GitHub\nellie-supplemental\comparisons\simulations\multi_grid\outputs", + # r"C:\Users\austin\GitHub\nellie-supplemental\comparisons\simulations\separation\outputs", + # r"C:\Users\austin\GitHub\nellie-supplemental\comparisons\simulations\px_sizes\outputs", + # ] + # ch = 0 + # num_t = 1 + # # get all non-folder files + # for top_dir in top_dirs: + # all_files = os.listdir(top_dir) + # all_files = [os.path.join(top_dir, file) for file in all_files if not os.path.isdir(os.path.join(top_dir, file))] + # all_files = [file for file in all_files if file.endswith('.tif')] + # for file_num, tif_file in enumerate(all_files): + # # for ch in range(1): + # print(f'Processing file {file_num + 1} of {len(all_files)}, channel {ch + 1} of 1') + # im_info = ImInfo(tif_file, ch=ch) + # if os.path.exists(im_info.pipeline_paths['im_skel_relabelled']): + # print(f'Already exists, skipping.') + # continue + # im_info = run(tif_file, remove_edges=False, ch=ch, num_t=num_t) + + test_file = '/Users/austin/test_files/nellie_all_tests/yeast_3d_mitochondria.ome.tif' + # test_file = all_paths[1] + file_info = FileInfo(test_file) + file_info.find_metadata() + file_info.load_metadata() + # print(f'{file_info.metadata_type=}') + # print(f'{file_info.axes=}') + # print(f'{file_info.shape=}') + # print(f'{file_info.dim_res=}') + # print(f'{file_info.good_axes=}') + # print(f'{file_info.good_dims=}') + # print('\n') + + # file_info.change_axes('TZYX') + # print('Axes changed') + # print(f'{file_info.axes=}') + # print(f'{file_info.dim_res=}') + # print(f'{file_info.good_axes=}') + # print(f'{file_info.good_dims=}') + # print('\n') + # + # file_info.change_dim_res('T', 1) + # file_info.change_dim_res('Z', 0.5) + # file_info.change_dim_res('Y', 0.2) + # file_info.change_dim_res('X', 0.2) + # + # print('Dimension resolutions changed') + # print(f'{file_info.axes=}') + # print(f'{file_info.dim_res=}') + # print(f'{file_info.good_axes=}') + # print(f'{file_info.good_dims=}') + # print('\n') + # + # # print(f'{file_info.ch=}') + # # file_info.change_selected_channel(3) + # # print('Channel changed') + # # print(f'{file_info.ch=}') + # + # print(f'{file_info.t_start=}') + # print(f'{file_info.t_end=}') + # file_info.select_temporal_range(1, 3) + # print('Temporal range selected') + # print(f'{file_info.t_start=}') + # print(f'{file_info.t_end=}') + # + # # file_info.save_ome_tiff() + # # im_info = ImInfo(file_info) + run(file_info) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/segmentation/filtering.html b/docs/_build/html/_modules/nellie/segmentation/filtering.html new file mode 100644 index 0000000..dcd2676 --- /dev/null +++ b/docs/_build/html/_modules/nellie/segmentation/filtering.html @@ -0,0 +1,384 @@ + + + + + + + nellie.segmentation.filtering — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.segmentation.filtering

+from itertools import combinations_with_replacement
+
+from nellie import logger
+from nellie.im_info.verifier import ImInfo
+from nellie.utils.general import bbox
+import numpy as np
+from nellie import ndi, xp, device_type
+
+from nellie.utils.gpu_functions import triangle_threshold, otsu_threshold
+
+
+
+[docs] +class Filter: + def __init__(self, im_info: ImInfo, + num_t=None, remove_edges=False, + min_radius_um=0.20, max_radius_um=1, alpha_sq=0.5, beta_sq=0.5, frob_thresh=None, viewer=None): + self.im_info = im_info + if not self.im_info.no_z: + self.z_ratio = self.im_info.dim_res['Z'] / self.im_info.dim_res['X'] + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + self.remove_edges = remove_edges + # either (roughly) diffraction limit, or pixel size, whichever is larger + self.min_radius_um = max(min_radius_um, self.im_info.dim_res['X']) + self.max_radius_um = max_radius_um + + self.min_radius_px = self.min_radius_um / self.im_info.dim_res['X'] + self.max_radius_px = self.max_radius_um / self.im_info.dim_res['X'] + + self.im_memmap = None + self.frangi_memmap = None + + self.sigma_vec = None + self.sigmas = None + + self.alpha_sq = alpha_sq + self.beta_sq = beta_sq + + self.frob_thresh = frob_thresh + + self.viewer = viewer + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for frangi filter.') + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.shape = self.im_memmap.shape + im_frangi_path = self.im_info.pipeline_paths['im_preprocessed'] + self.frangi_memmap = self.im_info.allocate_memory(im_frangi_path, dtype='double', + description='frangi filtered im', + return_memmap=True) + + def _get_sigma_vec(self, sigma): + if self.im_info.no_z: + self.sigma_vec = (sigma, sigma) + else: + self.sigma_vec = (sigma / self.z_ratio, sigma, sigma) + return self.sigma_vec + + def _set_default_sigmas(self): + logger.debug('Setting to sigma values.') + min_sigma_step_size = 0.2 + num_sigma = 5 + + sigma_1 = self.min_radius_px / 2 + sigma_2 = self.max_radius_px / 3 + self.sigma_min = min(sigma_1, sigma_2) + self.sigma_max = max(sigma_1, sigma_2) + + + sigma_step_size_calculated = (self.sigma_max - self.sigma_min) / num_sigma + sigma_step_size = max(min_sigma_step_size, sigma_step_size_calculated) # Avoid taking too small of steps. + + self.sigmas = list(np.arange(self.sigma_min, self.sigma_max, sigma_step_size)) + + logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') + + def _gauss_filter(self, sigma, t=None): + self._get_sigma_vec(sigma) + gauss_volume = xp.asarray(self.im_memmap[t, ...], dtype='double') + logger.debug(f'Gaussian filtering {t=} with {self.sigma_vec=}.') + + gauss_volume = ndi.gaussian_filter(gauss_volume, sigma=self.sigma_vec, + mode='reflect', cval=0.0, truncate=3).astype('double') + return gauss_volume + + def _calculate_gamma(self, gauss_volume): + gamma_tri = triangle_threshold(gauss_volume[gauss_volume > 0]) + gamma_otsu, _ = otsu_threshold(gauss_volume[gauss_volume > 0]) + gamma = min(gamma_tri, gamma_otsu) + return gamma + + def _compute_hessian(self, image, mask=True): + gradients = xp.gradient(image) + axes = range(image.ndim) + h_elems = xp.array([xp.gradient(gradients[ax0], axis=ax1).astype('float16') + for ax0, ax1 in combinations_with_replacement(axes, 2)]) + if mask: + h_mask = self._get_frob_mask(h_elems) + else: + h_mask = xp.ones_like(image, dtype='bool') + if self.remove_edges: + h_mask = self._remove_edges(h_mask) + + if self.im_info.no_z: + if device_type == 'cuda': + hxx, hxy, hyy = [elem[..., np.newaxis, np.newaxis] for elem in h_elems[:, h_mask].get()] + else: + hxx, hxy, hyy = [elem[..., np.newaxis, np.newaxis] for elem in h_elems[:, h_mask]] + hessian_matrices = np.concatenate([ + np.concatenate([hxx, hxy], axis=-1), + np.concatenate([hxy, hyy], axis=-1) + ], axis=-2) + else: + if device_type == 'cuda': + hxx, hxy, hxz, hyy, hyz, hzz = [elem[..., np.newaxis, np.newaxis] for elem in h_elems[:, h_mask].get()] + else: + hxx, hxy, hxz, hyy, hyz, hzz = [elem[..., np.newaxis, np.newaxis] for elem in h_elems[:, h_mask]] + hessian_matrices = np.concatenate([ + np.concatenate([hxx, hxy, hxz], axis=-1), + np.concatenate([hxy, hyy, hyz], axis=-1), + np.concatenate([hxz, hyz, hzz], axis=-1) + ], axis=-2) + + return h_mask, hessian_matrices + + def _get_frob_mask(self, hessian_matrices): + rescaled_hessian = hessian_matrices / xp.max(xp.abs(hessian_matrices)) + frobenius_norm = xp.linalg.norm(rescaled_hessian, axis=0) + frobenius_norm[xp.isinf(frobenius_norm)] = xp.max(frobenius_norm[~xp.isinf(frobenius_norm)]) + if self.frob_thresh is None: + non_zero_frobenius = frobenius_norm[frobenius_norm > 0] + if len(non_zero_frobenius) == 0: + frobenius_threshold = 0 + else: + frob_triangle_thresh = triangle_threshold(non_zero_frobenius) + frob_otsu_thresh, _ = otsu_threshold(non_zero_frobenius) + frobenius_threshold = min(frob_triangle_thresh, frob_otsu_thresh) + else: + frobenius_threshold = self.frob_thresh + mask = frobenius_norm > frobenius_threshold + return mask + + def _compute_chunkwise_eigenvalues(self, hessian_matrices, chunk_size=1E6): + chunk_size = int(chunk_size) + total_voxels = len(hessian_matrices) + + eigenvalues_list = [] + + if chunk_size is None: # chunk size is entire vector + chunk_size = total_voxels + + # iterate over chunks + # todo make chunk size dynamic based on available memory + for start_idx in range(0, total_voxels, int(chunk_size)): + end_idx = min(start_idx + chunk_size, total_voxels) + gpu_chunk = xp.array(hessian_matrices[start_idx:end_idx]) + chunk_eigenvalues = xp.linalg.eigvalsh(gpu_chunk) + eigenvalues_list.append(chunk_eigenvalues) + + # concatenate all the eigval chunks and reshape to the original spatial structure + eigenvalues_flat = xp.concatenate(eigenvalues_list, axis=0) + sort_order = xp.argsort(xp.abs(eigenvalues_flat), axis=1) + eigenvalues_flat = xp.take_along_axis(eigenvalues_flat, sort_order, axis=1) + + return eigenvalues_flat + + def _filter_hessian(self, eigenvalues, gamma_sq): + if self.im_info.no_z: + rb_sq = (xp.abs(eigenvalues[:, 0]) / xp.abs(eigenvalues[:, 1])) ** 2 + s_sq = (eigenvalues[:, 0] ** 2) + (eigenvalues[:, 1] ** 2) + filtered_im = (xp.exp(-(rb_sq / self.beta_sq))) * (1 - xp.exp(-(s_sq / gamma_sq))) + else: + ra_sq = (xp.abs(eigenvalues[:, 1]) / xp.abs(eigenvalues[:, 2])) ** 2 + rb_sq = (xp.abs(eigenvalues[:, 1]) / xp.sqrt(xp.abs(eigenvalues[:, 1] * eigenvalues[:, 2]))) ** 2 + s_sq = (xp.sqrt((eigenvalues[:, 0] ** 2) + (eigenvalues[:, 1] ** 2) + (eigenvalues[:, 2] ** 2))) ** 2 + filtered_im = (1 - xp.exp(-(ra_sq / self.alpha_sq))) * (xp.exp(-(rb_sq / self.beta_sq))) * \ + (1 - xp.exp(-(s_sq / gamma_sq))) + if not self.im_info.no_z: + filtered_im[eigenvalues[:, 2] > 0] = 0 + filtered_im[eigenvalues[:, 1] > 0] = 0 + filtered_im = xp.nan_to_num(filtered_im, False, 1) + return filtered_im + + def _filter_log(self, frame, mask): + lapofg = xp.zeros_like(frame, dtype='double') + for i, s in enumerate(self.sigmas): + sigma_vec = self._get_sigma_vec(s) + current_lapofg = -ndi.gaussian_laplace(frame, sigma_vec) * xp.mean(s) ** 2 + current_lapofg = current_lapofg * mask + min_indices = current_lapofg < lapofg + lapofg[min_indices] = current_lapofg[min_indices] + if i == 0: + lapofg = current_lapofg + lapofg_min_proj = lapofg + return lapofg_min_proj + + def _run_frame(self, t, mask=True): + logger.info(f'Running frangi filter on {t=}.') + vesselness = xp.zeros_like(self.im_memmap[t, ...], dtype='float64') + temp = xp.zeros_like(self.im_memmap[t, ...], dtype='float64') + masks = xp.ones_like(self.im_memmap[t, ...], dtype='bool') + for sigma_num, sigma in enumerate(self.sigmas): + gauss_volume = self._gauss_filter(sigma, t) # * xp.mean(sigma) ** 2 + + gamma = self._calculate_gamma(gauss_volume) + gamma_sq = 2 * gamma ** 2 + + h_mask, hessian_matrices = self._compute_hessian(gauss_volume, mask=mask) + if len(hessian_matrices) == 0: + continue + eigenvalues = self._compute_chunkwise_eigenvalues(hessian_matrices.astype('float')) + + temp[h_mask] = self._filter_hessian(eigenvalues, gamma_sq=gamma_sq) + + max_indices = temp > vesselness + vesselness[max_indices] = temp[max_indices] + masks = xp.where(~h_mask, 0, masks) + + vesselness = vesselness * masks + return vesselness + + def _mask_volume(self, frangi_frame): + frangi_threshold = xp.percentile(frangi_frame[frangi_frame > 0], 1) + frangi_mask = frangi_frame > frangi_threshold + frangi_mask = ndi.binary_opening(frangi_mask) + frangi_frame = frangi_frame * frangi_mask + return frangi_frame + + def _remove_edges(self, frangi_frame): + if self.im_info.no_z: + num_z = 1 + else: + num_z = self.im_info.shape[self.im_info.axes.index('Z')] + for z_idx in range(num_z): + if self.im_info.no_z: + rmin, rmax, cmin, cmax = bbox(frangi_frame) + else: + rmin, rmax, cmin, cmax = bbox(frangi_frame[z_idx, ...]) + frangi_frame[z_idx, rmin:rmin + 15, ...] = 0 + frangi_frame[z_idx, rmax - 15:rmax + 1, ...] = 0 + return frangi_frame + + def _run_filter(self, mask=True): + for t in range(self.num_t): + if self.viewer is not None: + self.viewer.status = f'Preprocessing. Frame: {t + 1} of {self.num_t}.' + frangi_frame = self._run_frame(t, mask=mask) + if not xp.sum(frangi_frame): + frangi_frame = self._mask_volume(frangi_frame) + filtered_im = frangi_frame + + if device_type == 'cuda': + filtered_im = filtered_im.get() + + if self.im_info.no_t or self.num_t == 1: + self.frangi_memmap[:] = filtered_im[:] + else: + self.frangi_memmap[t, ...] = filtered_im + self.frangi_memmap.flush() + +
+[docs] + def run(self, mask=True): + logger.info('Running frangi filter.') + self._get_t() + self._allocate_memory() + self._set_default_sigmas() + self._run_filter(mask=mask)
+
+ + + +if __name__ == "__main__": + im_path = r"F:\2024_06_26_SD_ExM_nhs_u2OS_488+578_cropped.tif" + im_info = ImInfo(im_path, dim_res={'T': 1, 'Z': 0.2, 'Y': 0.1, 'X': 0.1}, dimension_order='ZYX') + filter_im = Filter(im_info) + filter_im.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/segmentation/labelling.html b/docs/_build/html/_modules/nellie/segmentation/labelling.html new file mode 100644 index 0000000..c1f6c07 --- /dev/null +++ b/docs/_build/html/_modules/nellie/segmentation/labelling.html @@ -0,0 +1,271 @@ + + + + + + + nellie.segmentation.labelling — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.segmentation.labelling

+
+from nellie import xp, ndi, logger, device_type
+from nellie.im_info.verifier import ImInfo
+from nellie.utils.gpu_functions import otsu_threshold, triangle_threshold
+
+
+
+[docs] +class Label: + def __init__(self, im_info: ImInfo, + num_t=None, + threshold=None, + snr_cleaning=False, otsu_thresh_intensity=False, + viewer=None): + self.im_info = im_info + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + self.threshold = threshold + self.snr_cleaning = snr_cleaning + self.otsu_thresh_intensity = otsu_thresh_intensity + + self.im_memmap = None + self.frangi_memmap = None + + self.max_label_num = 0 + + if not self.im_info.no_z: + self.min_z_radius_um = min(self.im_info.dim_res['Z'], 0.2) + + self.semantic_mask_memmap = None + self.instance_label_memmap = None + self.shape = () + + self.debug = {} + + self.viewer = viewer + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for semantic segmentation.') + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.frangi_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) + self.shape = self.frangi_memmap.shape + + im_instance_label_path = self.im_info.pipeline_paths['im_instance_label'] + self.instance_label_memmap = self.im_info.allocate_memory(im_instance_label_path, + dtype='int32', + description='instance segmentation', + return_memmap=True) + + def _get_labels(self, frame): + ndim = 2 if self.im_info.no_z else 3 + footprint = ndi.generate_binary_structure(ndim, 1) + + triangle = 10 ** triangle_threshold(xp.log10(frame[frame > 0])) + otsu, _ = otsu_threshold(xp.log10(frame[frame > 0])) + otsu = 10 ** otsu + min_thresh = min([triangle, otsu]) + + mask = frame > min_thresh + + if not self.im_info.no_z: + mask = ndi.binary_fill_holes(mask) + + if not self.im_info.no_z and self.im_info.dim_res['Z'] >= self.min_z_radius_um: + mask = ndi.binary_opening(mask, structure=xp.ones((2, 2, 2))) + elif self.im_info.no_z: + mask = ndi.binary_opening(mask, structure=xp.ones((2, 2))) + + labels, _ = ndi.label(mask, structure=footprint) + # remove anything 4 pixels or under using bincounts + areas = xp.bincount(labels.ravel())[1:] + mask = xp.where(xp.isin(labels, xp.where(areas >= 4)[0]+1), labels, 0) > 0 + labels, _ = ndi.label(mask, structure=footprint) + return mask, labels + + def _get_subtraction_mask(self, original_frame, labels_frame): + subtraction_mask = original_frame.copy() + subtraction_mask[labels_frame > 0] = 0 + return subtraction_mask + + def _get_object_snrs(self, original_frame, labels_frame): + logger.debug('Calculating object SNRs.') + subtraction_mask = self._get_subtraction_mask(original_frame, labels_frame) + unique_labels = xp.unique(labels_frame) + extend_bbox_by = 1 + keep_labels = [] + for label in unique_labels: + if label == 0: + continue + coords = xp.nonzero(labels_frame == label) + z_coords, r_coords, c_coords = coords + + zmin, zmax = xp.min(z_coords), xp.max(z_coords) + rmin, rmax = xp.min(r_coords), xp.max(r_coords) + cmin, cmax = xp.min(c_coords), xp.max(c_coords) + + zmin, zmax = xp.clip(zmin - extend_bbox_by, 0, labels_frame.shape[0]), xp.clip(zmax + extend_bbox_by, 0, + labels_frame.shape[0]) + rmin, rmax = xp.clip(rmin - extend_bbox_by, 0, labels_frame.shape[1]), xp.clip(rmax + extend_bbox_by, 0, + labels_frame.shape[1]) + cmin, cmax = xp.clip(cmin - extend_bbox_by, 0, labels_frame.shape[2]), xp.clip(cmax + extend_bbox_by, 0, + labels_frame.shape[2]) + + # only keep objects over 1 std from its surroundings + local_intensity = subtraction_mask[zmin:zmax, rmin:rmax, cmin:cmax] + local_intensity_mean = local_intensity[local_intensity > 0].mean() + local_intensity_std = local_intensity[local_intensity > 0].std() + label_intensity_mean = original_frame[coords].mean() + intensity_cutoff = label_intensity_mean / (local_intensity_mean + local_intensity_std) + if intensity_cutoff > 1: + keep_labels.append(label) + + keep_labels = xp.asarray(keep_labels) + labels_frame = xp.where(xp.isin(labels_frame, keep_labels), labels_frame, 0) + return labels_frame + + def _run_frame(self, t): + logger.info(f'Running semantic segmentation, volume {t}/{self.num_t - 1}') + original_in_mem = xp.asarray(self.im_memmap[t, ...]) + frangi_in_mem = xp.asarray(self.frangi_memmap[t, ...]) + if self.otsu_thresh_intensity or self.threshold is not None: + if self.otsu_thresh_intensity: + thresh, _ = otsu_threshold(original_in_mem[original_in_mem > 0]) + else: + thresh = self.threshold + mask = original_in_mem > thresh + original_in_mem *= mask + frangi_in_mem *= mask + _, labels = self._get_labels(frangi_in_mem) + if self.snr_cleaning: + labels = self._get_object_snrs(original_in_mem, labels) + labels[labels > 0] += self.max_label_num + self.max_label_num = xp.max(labels) + return labels + + def _run_segmentation(self): + for t in range(self.num_t): + if self.viewer is not None: + self.viewer.status = f'Extracting organelles. Frame: {t + 1} of {self.num_t}.' + labels = self._run_frame(t) + if device_type == 'cuda': + labels = labels.get() + if self.im_info.no_t or self.num_t == 1: + self.instance_label_memmap[:] = labels[:] + else: + self.instance_label_memmap[t, ...] = labels + + self.instance_label_memmap.flush() + +
+[docs] + def run(self): + logger.info('Running semantic segmentation.') + self._get_t() + self._allocate_memory() + self._run_segmentation()
+
+ + + +if __name__ == "__main__": + im_path = r"F:\2024_06_26_SD_ExM_nhs_u2OS_488+578_cropped.tif" + im_info = ImInfo(im_path, dim_res={'T': 1, 'Z': 0.2, 'Y': 0.1, 'X': 0.1}, dimension_order='ZYX') + segment_unique = Label(im_info) + segment_unique.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/segmentation/mocap_marking.html b/docs/_build/html/_modules/nellie/segmentation/mocap_marking.html new file mode 100644 index 0000000..ed911bb --- /dev/null +++ b/docs/_build/html/_modules/nellie/segmentation/mocap_marking.html @@ -0,0 +1,331 @@ + + + + + + + nellie.segmentation.mocap_marking — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.segmentation.mocap_marking

+import numpy as np
+from scipy.spatial import cKDTree, distance
+
+from nellie import xp, ndi, logger, device_type
+from nellie.im_info.verifier import ImInfo
+
+
+
+[docs] +class Markers: + def __init__(self, im_info: ImInfo, num_t=None, + min_radius_um=0.20, max_radius_um=1, use_im='distance', num_sigma=5, + viewer=None): + self.im_info = im_info + + # if self.im_info.no_t: + # return + + self.num_t = num_t + if self.im_info.no_t: + self.num_t = 1 + elif num_t is None: # and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + if not self.im_info.no_z: + self.z_ratio = self.im_info.dim_res['Z'] / self.im_info.dim_res['X'] + self.min_radius_um = max(min_radius_um, self.im_info.dim_res['X']) + self.max_radius_um = max_radius_um + + self.min_radius_px = self.min_radius_um / self.im_info.dim_res['X'] + self.max_radius_px = self.max_radius_um / self.im_info.dim_res['X'] + self.use_im = use_im + self.num_sigma = num_sigma + + self.shape = () + + self.im_memmap = None + self.im_frangi_memmap = None + self.label_memmap = None + self.im_marker_memmap = None + self.im_distance_memmap = None + self.im_border_memmap = None + + self.debug = None + + self.viewer = viewer + + def _get_sigma_vec(self, sigma): + if self.im_info.no_z: + sigma_vec = (sigma, sigma) + else: + sigma_vec = (sigma / self.z_ratio, sigma, sigma) + return sigma_vec + + def _set_default_sigmas(self): + logger.debug('Setting sigma values.') + min_sigma_step_size = 0.2 + + self.sigma_min = self.min_radius_px / 2 + self.sigma_max = self.max_radius_px / 3 + + sigma_step_size_calculated = (self.sigma_max - self.sigma_min) / self.num_sigma + sigma_step_size = max(min_sigma_step_size, sigma_step_size_calculated) # Avoid taking too small of steps. + + self.sigmas = list(xp.arange(self.sigma_min, self.sigma_max, sigma_step_size)) + logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for mocap marking.') + + self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.im_frangi_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) + self.shape = self.label_memmap.shape + + im_marker_path = self.im_info.pipeline_paths['im_marker'] + self.im_marker_memmap = self.im_info.allocate_memory(im_marker_path, + dtype='uint8', + description='mocap marker image', + return_memmap=True) + + im_distance_path = self.im_info.pipeline_paths['im_distance'] + self.im_distance_memmap = self.im_info.allocate_memory(im_distance_path, + dtype='float', + description='distance transform image', + return_memmap=True) + + im_border_path = self.im_info.pipeline_paths['im_border'] + self.im_border_memmap = self.im_info.allocate_memory(im_border_path, + dtype='uint8', + description='border image', + return_memmap=True) + + def _distance_im(self, mask): + border_mask = ndi.binary_dilation(mask, iterations=1) ^ mask + + if device_type == 'cuda': + mask_coords = xp.argwhere(mask).get() + border_mask_coords = xp.argwhere(border_mask).get() + else: + mask_coords = xp.argwhere(mask) + border_mask_coords = xp.argwhere(border_mask) + + border_tree = cKDTree(border_mask_coords) + dist, _ = border_tree.query(mask_coords, k=1, distance_upper_bound=self.max_radius_px * 2) + distances_im_frame = xp.zeros_like(mask, dtype='float32') + if self.im_info.no_z: + distances_im_frame[mask_coords[:, 0], mask_coords[:, 1]] = dist + else: + distances_im_frame[mask_coords[:, 0], mask_coords[:, 1], mask_coords[:, 2]] = dist + # any inf pixels get set to upper bound + distances_im_frame[distances_im_frame == xp.inf] = self.max_radius_px * 2 + return distances_im_frame, border_mask + + def _remove_close_peaks(self, coord, check_im): + check_im_max = ndi.maximum_filter(check_im, size=3, mode='nearest') + if not self.im_info.no_z: + intensities = check_im_max[coord[:, 0], coord[:, 1], coord[:, 2]] + else: + intensities = check_im_max[coord[:, 0], coord[:, 1]] + + # sort to remove peaks that are too close by keeping the brightest peak + idx_maxsort = np.argsort(-intensities) + + if device_type == 'cuda': + coord_sorted = coord[idx_maxsort].get() + else: + coord_sorted = coord[idx_maxsort] + + tree = cKDTree(coord_sorted) + min_dist = 2 + indices = tree.query_ball_point(coord_sorted, r=min_dist, p=2, workers=-1) + rejected_peaks_indices = set() + naccepted = 0 + for idx, candidates in enumerate(indices): + if idx not in rejected_peaks_indices: + # keep current point and the points at exactly spacing from it + candidates.remove(idx) + dist = distance.cdist([coord_sorted[idx]], + coord_sorted[candidates], + distance.minkowski, + p=2).reshape(-1) + candidates = [c for c, d in zip(candidates, dist) + if d < min_dist] + + rejected_peaks_indices.update(candidates) + naccepted += 1 + + cleaned_coords = np.delete(coord_sorted, tuple(rejected_peaks_indices), axis=0) + + return cleaned_coords + + def _local_max_peak(self, use_im, mask, distance_im): + lapofg = xp.empty(((len(self.sigmas),) + use_im.shape), dtype=float) + for i, s in enumerate(self.sigmas): + sigma_vec = self._get_sigma_vec(s) + current_lapofg = -ndi.gaussian_laplace(use_im, sigma_vec) * xp.mean(s) ** 2 + current_lapofg[current_lapofg < 0] = 0 + lapofg[i] = current_lapofg + + filt_footprint = xp.ones((3,) * (use_im.ndim + 1)) + max_filt = ndi.maximum_filter(lapofg, footprint=filt_footprint, mode='nearest') + # if peaks are empty, return empty array + if max_filt.size == 0: + if self.im_info.no_z: + return xp.empty((0, 2), dtype=int) + else: + return xp.empty((0, 3), dtype=int) + peaks = xp.empty(lapofg.shape, dtype=bool) + for filt_slice, max_filt_slice in enumerate(max_filt): + peaks[filt_slice] = (xp.asarray(lapofg[filt_slice]) == xp.asarray(max_filt_slice)) # * max_filt_mask + distance_mask = distance_im > 0 + peaks = peaks * mask * distance_mask + # get the coordinates of all true pixels in peaks + coords = xp.max(peaks, axis=0) + coords_idx = xp.argwhere(coords) + return coords_idx + + def _run_frame(self, t): + logger.info(f'Running motion capture marking, volume {t}/{self.num_t - 1}') + intensity_frame = xp.asarray(self.im_memmap[t]) + mask_frame = xp.asarray(self.label_memmap[t] > 0) + distance_im, border_mask = self._distance_im(mask_frame) + if self.use_im == 'distance': + peak_coords = self._local_max_peak(distance_im, mask_frame, distance_im) + elif self.use_im == 'frangi': + peak_coords = self._local_max_peak(xp.asarray(self.im_frangi_memmap[t]), mask_frame, distance_im) + peak_coords = self._remove_close_peaks(peak_coords, intensity_frame) + peak_im = xp.zeros_like(mask_frame) + peak_im[tuple(peak_coords.T)] = 1 + if device_type == "cuda": + return peak_im.get(), distance_im.get(), border_mask.get() + else: + return peak_im, distance_im, border_mask + + def _run_mocap_marking(self): + for t in range(self.num_t): + if self.viewer is not None: + self.viewer.status = f'Mocap marking. Frame: {t + 1} of {self.num_t}.' + marker_frame = self._run_frame(t) + if self.im_marker_memmap.shape != self.shape and self.im_info.no_t: + self.im_marker_memmap[:], self.im_distance_memmap[:], self.im_border_memmap[:] = marker_frame + else: + self.im_marker_memmap[t], self.im_distance_memmap[t], self.im_border_memmap[t] = marker_frame + self.im_marker_memmap.flush() + self.im_distance_memmap.flush() + self.im_border_memmap.flush() + +
+[docs] + def run(self): + # if self.im_info.no_t: + # return + self._get_t() + self._allocate_memory() + self._set_default_sigmas() + self._run_mocap_marking()
+
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nelly_smorgasbord\deskewed-iono_pre.ome.tif" + im_info = ImInfo(im_path) + num_t = 3 + markers = Markers(im_info, num_t=num_t) + markers.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/segmentation/networking.html b/docs/_build/html/_modules/nellie/segmentation/networking.html new file mode 100644 index 0000000..d720c19 --- /dev/null +++ b/docs/_build/html/_modules/nellie/segmentation/networking.html @@ -0,0 +1,489 @@ + + + + + + + nellie.segmentation.networking — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.segmentation.networking

+import numpy as np
+import skimage.measure
+import skimage.morphology as morph
+from scipy.spatial import cKDTree
+
+from nellie import xp, ndi, logger, device_type
+from nellie.im_info.verifier import ImInfo
+from nellie.utils.gpu_functions import triangle_threshold, otsu_threshold
+
+
+
+[docs] +class Network: + def __init__(self, im_info: ImInfo, num_t=None, + min_radius_um=0.20, max_radius_um=1, clean_skel=None, + viewer=None): + self.im_info = im_info + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + if not self.im_info.no_z: + if clean_skel is None: + clean_skel = False + self.z_ratio = self.im_info.dim_res['Z'] / self.im_info.dim_res['X'] + # either (roughly) diffraction limit, or pixel size, whichever is larger + self.min_radius_um = max(min_radius_um, self.im_info.dim_res['X']) + self.max_radius_um = max_radius_um + + self.min_radius_px = self.min_radius_um / self.im_info.dim_res['X'] + self.max_radius_px = self.max_radius_um / self.im_info.dim_res['X'] + + if self.im_info.no_z: + self.scaling = (im_info.dim_res['Y'], im_info.dim_res['X']) + else: + self.scaling = (im_info.dim_res['Z'], im_info.dim_res['Y'], im_info.dim_res['X']) + + self.shape = () + + self.im_memmap = None + self.im_frangi_memmap = None + self.label_memmap = None + self.network_memmap = None + self.pixel_class_memmap = None + self.skel_memmap = None + self.skel_relabelled_memmap = None + + self.clean_skel = True if clean_skel is None else clean_skel + + self.sigmas = None + + self.debug = None + + self.viewer = viewer + + def _remove_connected_label_pixels(self, skel_labels): + if device_type == 'cuda': + skel_labels = skel_labels.get() + + if self.im_info.no_z: + height, width = skel_labels.shape + else: + depth, height, width = skel_labels.shape + + true_coords = np.argwhere(skel_labels > 0) + + pixels_to_delete = [] + for coord in true_coords: + if self.im_info.no_z: + y, x = coord + else: + z, y, x = coord + + if not self.im_info.no_z: + if z == 0 or z == depth - 1: + continue + if y == 0 or y == height - 1 or x == 0 or x == width - 1: + continue # skip boundary voxels + + # extract 3x3x3 neighborhood + if self.im_info.no_z: + label_neighborhood = skel_labels[y - 1:y + 2, x - 1:x + 2] + else: + label_neighborhood = skel_labels[z - 1:z + 2, y - 1:y + 2, x - 1:x + 2] + + # get labels of set voxels in the neighborhood + labels_in_neighborhood = label_neighborhood[label_neighborhood > 0] + + if len(set(labels_in_neighborhood.tolist())) > 1: + if self.im_info.no_z: + pixels_to_delete.append((y, x)) + else: + pixels_to_delete.append((z, y, x)) + + if self.im_info.no_z: + for y, x in pixels_to_delete: + skel_labels[y, x] = 0 + else: + for z, y, x in pixels_to_delete: + skel_labels[z, y, x] = 0 + + return xp.array(skel_labels) + + def _add_missing_skeleton_labels(self, skel_frame, label_frame, frangi_frame, thresh): + logger.debug('Adding missing skeleton labels.') + gpu_frame = xp.array(label_frame) + # identify unique labels and find missing ones + unique_labels = xp.unique(gpu_frame) + unique_skel_labels = xp.unique(skel_frame) + + missing_labels = set(unique_labels.tolist()) - set(unique_skel_labels.tolist()) + + # for each missing label, find the centroid and mark it in the skeleton + for label in missing_labels: + if label == 0: # ignore bg label + continue + + label_coords = xp.argwhere(gpu_frame == label) + label_intensities = frangi_frame[tuple(label_coords.T)] + # centroid is where label_intensities is maximal + centroid = label_coords[xp.argmax(label_intensities)] + + skel_frame[tuple(centroid)] = label + + return skel_frame + + def _skeletonize(self, label_frame, frangi_frame): + cpu_frame = np.array(label_frame) + gpu_frame = xp.array(label_frame) + + skel = xp.array(morph.skeletonize(cpu_frame > 0).astype('bool')) + + if self.clean_skel: + masked_frangi = ndi.median_filter(frangi_frame, size=3) * (gpu_frame > 0) # * skel + thresh_otsu, _ = otsu_threshold(xp.log10(masked_frangi[masked_frangi > 0])) + thresh_otsu = 10 ** thresh_otsu + thresh_tri = triangle_threshold(xp.log10(masked_frangi[masked_frangi > 0])) + thresh_tri = 10 ** thresh_tri + thresh = min(thresh_otsu, thresh_tri) + cleaned_skel = (masked_frangi > thresh) * skel + + skel_labels = gpu_frame * cleaned_skel + label_sizes = xp.bincount(skel_labels.ravel()) + + above_threshold = label_sizes > 1 + + mask = xp.zeros_like(skel_labels, dtype=bool) + mask[above_threshold[skel_labels]] = True + mask[skel_labels == 0] = False + + skel_labels = gpu_frame * mask + else: + skel_labels = gpu_frame * skel + thresh = 0 + + return skel_labels, thresh + + def _get_sigma_vec(self, sigma): + if self.im_info.no_z: + sigma_vec = (sigma, sigma) + else: + sigma_vec = (sigma / self.z_ratio, sigma, sigma) + return sigma_vec + + def _set_default_sigmas(self): + logger.debug('Setting to sigma values.') + min_sigma_step_size = 0.2 + num_sigma = 5 + + self.sigma_min = self.min_radius_px / 2 + self.sigma_max = self.max_radius_px / 3 + + sigma_step_size_calculated = (self.sigma_max - self.sigma_min) / num_sigma + sigma_step_size = max(min_sigma_step_size, sigma_step_size_calculated) # Avoid taking too small of steps. + + self.sigmas = list(xp.arange(self.sigma_min, self.sigma_max, sigma_step_size)) + logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') + + def _relabel_objects(self, branch_skel_labels, label_frame): + if self.im_info.no_z: + structure = xp.ones((3, 3)) + else: + structure = xp.ones((3, 3, 3)) + # here, skel frame should be the branch labeled frame + relabelled_labels = branch_skel_labels.copy() + skel_mask = xp.array(branch_skel_labels > 0).astype('uint8') + label_mask = xp.array(label_frame > 0).astype('uint8') + skel_border = (ndi.binary_dilation(skel_mask, iterations=1, structure=structure) ^ skel_mask) * label_mask + skel_label_mask = (branch_skel_labels > 0) + + if device_type == 'cuda': + skel_label_mask = skel_label_mask.get() + + vox_matched = np.argwhere(skel_label_mask) + + if device_type == 'cuda': + vox_next_unmatched = np.argwhere(skel_border.get()) + else: + vox_next_unmatched = np.argwhere(skel_border) + + unmatched_diff = np.inf + while True: + num_unmatched = len(vox_next_unmatched) + if num_unmatched == 0: + break + tree = cKDTree(vox_matched * self.scaling) + dists, idxs = tree.query(vox_next_unmatched * self.scaling, k=1, workers=-1) + # remove any matches that are too far away + max_dist = 2 * np.min(self.scaling) # sqrt 3 * max scaling + unmatched_matches = np.array( + [[vox_matched[idx], vox_next_unmatched[i]] for i, idx in enumerate(idxs) if dists[i] < max_dist] + ) + if len(unmatched_matches) == 0: + break + matched_labels = branch_skel_labels[tuple(np.transpose(unmatched_matches[:, 0]))] + relabelled_labels[tuple(np.transpose(unmatched_matches[:, 1]))] = matched_labels + branch_skel_labels = relabelled_labels.copy() + relabelled_labels_mask = relabelled_labels > 0 + + if device_type == 'cuda': + relabelled_labels_mask_cpu = relabelled_labels_mask.get() + else: + relabelled_labels_mask_cpu = relabelled_labels_mask + + vox_matched = np.argwhere(relabelled_labels_mask_cpu) + relabelled_mask = relabelled_labels_mask.astype('uint8') + # add unmatched matches to coords_matched + skel_border = (ndi.binary_dilation(relabelled_mask, iterations=1, + structure=structure) - relabelled_mask) * label_mask + + if device_type == 'cuda': + vox_next_unmatched = np.argwhere(skel_border.get()) + else: + vox_next_unmatched = np.argwhere(skel_border) + + new_num_unmatched = len(vox_next_unmatched) + unmatched_diff_temp = abs(num_unmatched - new_num_unmatched) + if unmatched_diff_temp == unmatched_diff: + break + unmatched_diff = unmatched_diff_temp + logger.debug(f'Reassigned {unmatched_diff}/{num_unmatched} unassigned voxels. ' + f'{new_num_unmatched} remain.') + + return relabelled_labels + + def _local_max_peak(self, frame, mask): + lapofg = xp.empty(((len(self.sigmas),) + frame.shape), dtype=float) + for i, s in enumerate(self.sigmas): + sigma_vec = self._get_sigma_vec(s) + current_lapofg = -ndi.gaussian_laplace(frame, sigma_vec) * xp.mean(s) ** 2 + current_lapofg = current_lapofg * mask + current_lapofg[current_lapofg < 0] = 0 + lapofg[i] = current_lapofg + + filt_footprint = xp.ones((3,) * (frame.ndim + 1)) + max_filt = ndi.maximum_filter(lapofg, footprint=filt_footprint, mode='nearest') + peaks = xp.empty(lapofg.shape, dtype=bool) + max_filt_mask = mask + for filt_slice, max_filt_slice in enumerate(max_filt): + peaks[filt_slice] = (xp.asarray(lapofg[filt_slice]) == xp.asarray(max_filt_slice)) * max_filt_mask + # get the coordinates of all true pixels in peaks + coords = xp.max(peaks, axis=0) + coords_3d = xp.argwhere(coords) + peak_im = xp.zeros_like(frame) + peak_im[tuple(coords_3d.T)] = 1 + return coords_3d + + def _get_pixel_class(self, skel): + skel_mask = xp.array(skel > 0).astype('uint8') + if self.im_info.no_z: + weights = xp.ones((3, 3)) + else: + weights = xp.ones((3, 3, 3)) + skel_mask_sum = ndi.convolve(skel_mask, weights=weights, mode='constant', cval=0) * skel_mask + skel_mask_sum[skel_mask_sum > 4] = 4 + return skel_mask_sum + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for skeletonization.') + self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) # , read_type='r+') + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.im_frangi_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) + self.shape = self.label_memmap.shape + + im_skel_path = self.im_info.pipeline_paths['im_skel'] + self.skel_memmap = self.im_info.allocate_memory(im_skel_path, + dtype='uint16', + description='skeleton image', + return_memmap=True) + + im_pixel_class = self.im_info.pipeline_paths['im_pixel_class'] + self.pixel_class_memmap = self.im_info.allocate_memory(im_pixel_class, + dtype='uint8', + description='pixel class image', + return_memmap=True) + + im_skel_relabelled = self.im_info.pipeline_paths['im_skel_relabelled'] + self.skel_relabelled_memmap = self.im_info.allocate_memory(im_skel_relabelled, + dtype='uint32', + description='skeleton relabelled image', + return_memmap=True) + + def _get_branch_skel_labels(self, pixel_class): + # get the labels of the skeleton pixels that are not junctions or background + non_junctions = pixel_class > 0 + non_junctions = non_junctions * (pixel_class != 4) + if self.im_info.no_z: + structure = xp.ones((3, 3)) + else: + structure = xp.ones((3, 3, 3)) + non_junction_labels, _ = ndi.label(non_junctions, structure=structure) + return non_junction_labels + + def _run_frame(self, t): + logger.info(f'Running network analysis, volume {t}/{self.num_t - 1}') + label_frame = self.label_memmap[t] + frangi_frame = xp.array(self.im_frangi_memmap[t]) + skel_frame, thresh = self._skeletonize(label_frame, frangi_frame) + skel = self._remove_connected_label_pixels(skel_frame) + skel = self._add_missing_skeleton_labels(skel, label_frame, frangi_frame, thresh) + if device_type == 'cuda': + skel = skel.get() + + skel_pre = (skel > 0) * label_frame + pixel_class = self._get_pixel_class(skel_pre) + branch_skel_labels = self._get_branch_skel_labels(pixel_class) + branch_labels = self._relabel_objects(branch_skel_labels, label_frame) + return branch_skel_labels, pixel_class, branch_labels + + def _clean_junctions(self, pixel_class): + junctions = pixel_class == 4 + junction_labels = skimage.measure.label(junctions) + junction_objects = skimage.measure.regionprops(junction_labels) + junction_centroids = [obj.centroid for obj in junction_objects] + for junction_num, junction in enumerate(junction_objects): + # use ckd tree to find closest junction coord to junction centroid + if len(junction.coords) < 2: + continue + junction_tree = cKDTree(junction.coords) + _, nearest_junction_indices = junction_tree.query(junction_centroids[junction_num], k=1, workers=-1) + # remove the nearest junction coord from the junction + junction_coords = junction.coords.tolist() + junction_coords.pop(nearest_junction_indices) + pixel_class[tuple(np.array(junction_coords).T)] = 3 + return pixel_class + + def _run_networking(self): + for t in range(self.num_t): + if self.viewer is not None: + self.viewer.status = f'Extracting branches. Frame: {t + 1} of {self.num_t}.' + skel, pixel_class, skel_relabelled_memmap = self._run_frame(t) + if self.im_info.no_t or self.num_t == 1: + if device_type == 'cuda': + self.skel_memmap[:] = skel[:].get() + self.pixel_class_memmap[:] = pixel_class[:].get() + self.skel_relabelled_memmap[:] = skel_relabelled_memmap[:].get() + else: + self.skel_memmap[:] = skel[:] + self.pixel_class_memmap[:] = pixel_class[:] + self.skel_relabelled_memmap[:] = skel_relabelled_memmap[:] + else: + if device_type == 'cuda': + self.skel_memmap[t] = skel.get() + self.pixel_class_memmap[t] = pixel_class.get() + self.skel_relabelled_memmap[t] = skel_relabelled_memmap.get() + else: + self.skel_memmap[t] = skel + self.pixel_class_memmap[t] = pixel_class + self.skel_relabelled_memmap[t] = skel_relabelled_memmap + +
+[docs] + def run(self): + self._get_t() + self._allocate_memory() + self._run_networking()
+
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nelly_tests\deskewed-2023-07-13_14-58-28_000_wt_0_acquire.ome.tif" + im_info = ImInfo(im_path) + skel = Network(im_info, num_t=3) + skel.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/tracking/all_tracks_for_label.html b/docs/_build/html/_modules/nellie/tracking/all_tracks_for_label.html new file mode 100644 index 0000000..054d253 --- /dev/null +++ b/docs/_build/html/_modules/nellie/tracking/all_tracks_for_label.html @@ -0,0 +1,209 @@ + + + + + + + nellie.tracking.all_tracks_for_label — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.tracking.all_tracks_for_label

+import numpy as np
+
+from nellie.im_info.verifier import ImInfo
+from nellie.tracking.flow_interpolation import interpolate_all_forward, interpolate_all_backward
+
+
+
+[docs] +class LabelTracks: + def __init__(self, im_info: ImInfo, num_t: int = None, label_im_path: str = None): + self.im_info = im_info + self.num_t = num_t + if label_im_path is None: + label_im_path = self.im_info.pipeline_paths['im_instance_label'] + self.label_im_path = label_im_path + + if num_t is None: + self.num_t = im_info.shape[im_info.axes.index('T')] + + self.im_memmap = None + self.label_memmap = None + +
+[docs] + def initialize(self): + self.label_memmap = self.im_info.get_memmap(self.label_im_path) + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path)
+ + +
+[docs] + def run(self, label_num=None, start_frame=0, end_frame=None, min_track_num=0, skip_coords=1, max_distance_um=0.5): + if end_frame is None: + end_frame = self.num_t + num_frames = self.label_memmap.shape[0] - 1 + if start_frame > num_frames: + return [], {} + if label_num is None: + coords = np.argwhere(self.label_memmap[start_frame] > 0).astype(float) + else: + coords = np.argwhere(self.label_memmap[start_frame] == label_num).astype(float) + if coords.shape[0] == 0: + return [], {} + coords = np.array(coords[::skip_coords]) + coords_copy = coords.copy() + tracks = [] + track_properties = {} + if start_frame < end_frame: + tracks, track_properties = interpolate_all_forward(coords, start_frame, end_frame, self.im_info, + min_track_num, max_distance_um=max_distance_um) + new_end_frame = 0 # max(0, end_frame - start_frame) + if start_frame > 0: + tracks_bw, track_properties_bw = interpolate_all_backward(coords_copy, start_frame, new_end_frame, + self.im_info, min_track_num, + max_distance_um=max_distance_um) + tracks_bw = tracks_bw[::-1] + for property in track_properties_bw.keys(): + track_properties_bw[property] = track_properties_bw[property][::-1] + sort_idx = np.argsort([track[0] for track in tracks_bw]) + tracks_bw = [tracks_bw[i] for i in sort_idx] + tracks = tracks_bw + tracks + for property in track_properties_bw.keys(): + track_properties_bw[property] = [track_properties_bw[property][i] for i in sort_idx] + if not track_properties: + track_properties = track_properties_bw + else: + for property in track_properties_bw.keys(): + track_properties[property] = track_properties_bw[property] + track_properties[property] + return tracks, track_properties
+
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nellie_longer_smorgasbord\deskewed-peroxisome.ome.tif" + im_info = ImInfo(im_path, ch=0) + num_t = 20 + label_tracks = LabelTracks(im_info, num_t=num_t) + label_tracks.initialize() + # tracks, track_properties = label_tracks.run(label_num=None, skip_coords=1) + + all_tracks = [] + all_props = {} + max_track_num = 0 + for frame in range(num_t): + tracks, track_properties = label_tracks.run(label_num=None, start_frame=frame, end_frame=None, + min_track_num=max_track_num, + skip_coords=100) + all_tracks += tracks + for property in track_properties.keys(): + if property not in all_props.keys(): + all_props[property] = [] + all_props[property] += track_properties[property] + if len(tracks) == 0: + break + max_track_num = max([track[0] for track in tracks]) + 1 + + # pickle tracks and track properties + import pickle + import datetime + dt = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + with open(f'{dt}-mt_tracks.pkl', 'wb') as f: + pickle.dump(all_tracks, f) + with open(f'{dt}-mt_props.pkl', 'wb') as f: + pickle.dump(all_props, f) + + # import napari + # viewer = napari.Viewer() + # + # raw_im = im_info.get_im_memmap(im_info.im_path)[:num_t] + # viewer.add_image(raw_im, name='raw_im') + # viewer.add_tracks(all_tracks, properties=all_props, name='tracks') + # napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/tracking/flow_interpolation.html b/docs/_build/html/_modules/nellie/tracking/flow_interpolation.html new file mode 100644 index 0000000..572007e --- /dev/null +++ b/docs/_build/html/_modules/nellie/tracking/flow_interpolation.html @@ -0,0 +1,370 @@ + + + + + + + nellie.tracking.flow_interpolation — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.tracking.flow_interpolation

+import numpy as np
+from scipy.spatial import cKDTree
+
+from nellie import logger
+from nellie.im_info.verifier import ImInfo
+
+
+
+[docs] +class FlowInterpolator: + def __init__(self, im_info: ImInfo, num_t=None, max_distance_um=0.5, forward=True): + self.im_info = im_info + + if self.im_info.no_t: + return + + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + + if self.im_info.no_z: + self.scaling = (im_info.dim_res['Y'], im_info.dim_res['X']) + else: + self.scaling = (im_info.dim_res['Z'], im_info.dim_res['Y'], im_info.dim_res['X']) + + self.max_distance_um = max_distance_um * im_info.dim_res['T'] + self.max_distance_um = np.max(np.array([self.max_distance_um, 0.5])) + + self.forward = forward + + self.shape = () + + self.im_memmap = None + self.flow_vector_array = None + + # caching + self.current_t = None + self.check_rows = None + self.check_coords = None + self.current_tree = None + + self.debug = None + self._initialize() + + def _allocate_memory(self): + logger.debug('Allocating memory for mocap marking.') + + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.shape = self.im_memmap.shape + + flow_vector_array_path = self.im_info.pipeline_paths['flow_vector_array'] + self.flow_vector_array = np.load(flow_vector_array_path) + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _get_nearby_coords(self, t, coords): + # using a ckdtree, check for any nearby coords from coord + if self.current_t != t: + self.current_tree = cKDTree(self.check_coords * self.scaling) + scaled_coords = np.array(coords) * self.scaling + # get all coords and distances within the radius of the coord + # good coords are non-nan + good_coords = np.where(~np.isnan(scaled_coords[:, 0]))[0] + nearby_idxs = self.current_tree.query_ball_point(scaled_coords[good_coords], self.max_distance_um, p=2) + if len(nearby_idxs) == 0: + return [], [] + k_all = [len(nearby_idxs[i]) for i in range(len(nearby_idxs))] + max_k = np.max(k_all) + if max_k == 0: + return [], [] + distances, nearby_idxs = self.current_tree.query(scaled_coords[good_coords], k=max_k, p=2, workers=-1) + # if the first index is scalar, wrap the whole list in another list + if len(distances.shape) == 1: + distances = [distances] + nearby_idxs = [nearby_idxs] + distance_return = [[] for _ in range(len(coords))] + nearby_idxs_return = [[] for _ in range(len(coords))] + pos = 0 + for i in range(len(distances)): + if i not in good_coords: + continue + distance_return[i] = distances[pos][:k_all[pos]] + nearby_idxs_return[i] = nearby_idxs[pos][:k_all[pos]] + pos += 1 + return nearby_idxs_return, distance_return + + def _get_vector_weights(self, nearby_idxs, distances_all): + weights_all = [] + for i in range(len(nearby_idxs)): + # lowest cost should be most highly weighted + cost_weights = -self.check_rows[nearby_idxs[i], -1] + + if len(distances_all[i]) == 0: + weights_all.append(None) + continue + + if np.min(distances_all[i]) == 0: + distance_weights = (distances_all[i] == 0) * 1.0 + else: + distance_weights = 1 / distances_all[i] + + weights = cost_weights * distance_weights + weights -= np.min(weights) - 1 + weights /= np.sum(weights) + weights_all.append(weights) + return weights_all + + def _get_final_vector(self, nearby_idxs, weights_all): + if self.im_info.no_z: + final_vectors = np.zeros((len(nearby_idxs), 2)) + else: + final_vectors = np.zeros((len(nearby_idxs), 3)) + for i in range(len(nearby_idxs)): + if weights_all[i] is None: + final_vectors[i] = np.nan + continue + if self.im_info.no_z: + vectors = self.check_rows[nearby_idxs[i], 3:5] + else: + vectors = self.check_rows[nearby_idxs[i], 4:7] + if len(weights_all[i].shape) == 0: + final_vectors[i] = vectors[0] + else: + weighted_vectors = vectors * weights_all[i][:, None] + final_vectors[i] = np.sum(weighted_vectors, axis=0) # already normalized by weights + return final_vectors + +
+[docs] + def interpolate_coord(self, coords, t): + # interpolate the flow vector at the coordinate at time t, either forward in time or backward in time. + # For forward, simply find nearby LMPs, interpolate based on distance-weighted vectors + # For backward, get coords from t-1 + vector, then find nearby coords from that, and interpolate based on distance-weighted vectors + if self.current_t != t: + if self.forward: + # check_rows will be all rows where the self.flow_vector_array's 0th column is equal to t + self.check_rows = self.flow_vector_array[np.where(self.flow_vector_array[:, 0] == t)[0], :] + if self.im_info.no_z: + self.check_coords = self.check_rows[:, 1:3] + else: + self.check_coords = self.check_rows[:, 1:4] + else: + # check_rows will be all rows where the self.flow_vector_array's 0th columns is equal to t-1 + self.check_rows = self.flow_vector_array[np.where(self.flow_vector_array[:, 0] == t - 1)[0], :] + # check coords will be the coords + vector + if self.im_info.no_z: + self.check_coords = self.check_rows[:, 1:3] + self.check_rows[:, 3:5] + else: + self.check_coords = self.check_rows[:, 1:4] + self.check_rows[:, 4:7] + + nearby_idxs, distances_all = self._get_nearby_coords(t, coords) + self.current_t = t + + if nearby_idxs is None: + return None + + weights_all = self._get_vector_weights(nearby_idxs, distances_all) + final_vectors = self._get_final_vector(nearby_idxs, weights_all) + + return final_vectors
+ + + def _initialize(self): + if self.im_info.no_t: + return + self._get_t() + self._allocate_memory()
+ + + +
+[docs] +def interpolate_all_forward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5): + flow_interpx = FlowInterpolator(im_info, forward=True, max_distance_um=max_distance_um) + tracks = [] + track_properties = {'frame_num': []} + frame_range = np.arange(start_t, end_t) + for t in frame_range: + final_vector = flow_interpx.interpolate_coord(coords, t) + if final_vector is None or len(final_vector) == 0: + continue + for coord_num, coord in enumerate(coords): + if np.all(np.isnan(final_vector[coord_num])): + coords[coord_num] = np.nan + continue + if t == frame_range[0]: + if im_info.no_z: + tracks.append([coord_num + min_track_num, frame_range[0], coord[0], coord[1]]) + else: + tracks.append([coord_num + min_track_num, frame_range[0], coord[0], coord[1], coord[2]]) + track_properties['frame_num'].append(frame_range[0]) + + track_properties['frame_num'].append(t + 1) + if im_info.no_z: + coords[coord_num] = np.array([coord[0] + final_vector[coord_num][0], + coord[1] + final_vector[coord_num][1]]) + tracks.append([coord_num + min_track_num, t + 1, coord[0], coord[1]]) + else: + coords[coord_num] = np.array([coord[0] + final_vector[coord_num][0], + coord[1] + final_vector[coord_num][1], + coord[2] + final_vector[coord_num][2]]) + tracks.append([coord_num + min_track_num, t + 1, coord[0], coord[1], coord[2]]) + return tracks, track_properties
+ + + +
+[docs] +def interpolate_all_backward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5): + flow_interpx = FlowInterpolator(im_info, forward=False, max_distance_um=max_distance_um) + tracks = [] + track_properties = {'frame_num': []} + frame_range = list(np.arange(end_t, start_t + 1))[::-1] + for t in frame_range: + final_vector = flow_interpx.interpolate_coord(coords, t) + if final_vector is None or len(final_vector) == 0: + continue + for coord_num, coord in enumerate(coords): + # if final_vector[coord_num] is all nan, skip + if np.all(np.isnan(final_vector[coord_num])): + coords[coord_num] = np.nan + continue + if t == frame_range[0]: + if im_info.no_z: + tracks.append([coord_num + min_track_num, frame_range[0], coord[0], coord[1]]) + else: + tracks.append([coord_num + min_track_num, frame_range[0], coord[0], coord[1], coord[2]]) + track_properties['frame_num'].append(frame_range[0]) + if im_info.no_z: + coords[coord_num] = np.array([coord[0] - final_vector[coord_num][0], + coord[1] - final_vector[coord_num][1]]) + tracks.append([coord_num + min_track_num, t - 1, coord[0], coord[1]]) + else: + coords[coord_num] = np.array([coord[0] - final_vector[coord_num][0], + coord[1] - final_vector[coord_num][1], + coord[2] - final_vector[coord_num][2]]) + tracks.append([coord_num + min_track_num, t - 1, coord[0], coord[1], coord[2]]) + track_properties['frame_num'].append(t - 1) + return tracks, track_properties
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nelly_smorgasbord\deskewed-iono_pre.ome.tif" + im_info = ImInfo(im_path) + label_memmap = self.im_info.get_memmap(im_info.pipeline_paths['im_instance_label']) + im_memmap = self.im_info.get_memmap(im_info.im_path) + + import napari + viewer = napari.Viewer() + start_frame = 0 + # going backwards + coords = np.argwhere(label_memmap[0] > 0).astype(float) + # get 100 random coords + # np.random.seed(0) + # coords = coords[np.random.choice(coords.shape[0], 10000, replace=False), :].astype(float) + # x in range 450-650 + # y in range 600-750 + new_coords = [] + for coord in coords: + if 450 < coord[-1] < 650 and 600 < coord[-2] < 750: + new_coords.append(coord) + coords = np.array(new_coords[::1]) + tracks, track_properties = interpolate_all_forward(coords, start_frame, 3, im_info) + + viewer.add_image(im_memmap) + viewer.add_tracks(tracks, properties=track_properties, name='tracks') +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/tracking/hu_tracking.html b/docs/_build/html/_modules/nellie/tracking/hu_tracking.html new file mode 100644 index 0000000..5ba2207 --- /dev/null +++ b/docs/_build/html/_modules/nellie/tracking/hu_tracking.html @@ -0,0 +1,508 @@ + + + + + + + nellie.tracking.hu_tracking — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.tracking.hu_tracking

+import numpy as np
+from scipy.spatial.distance import cdist
+
+from nellie import xp, ndi, logger
+from nellie.im_info.verifier import ImInfo
+
+
+
+[docs] +class HuMomentTracking: + def __init__(self, im_info: ImInfo, num_t=None, + max_distance_um=1, + viewer=None): + self.im_info = im_info + + if self.im_info.no_t: + return + + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + + if self.im_info.no_z: + self.scaling = (im_info.dim_res['Y'], im_info.dim_res['X']) + else: + self.scaling = (im_info.dim_res['Z'], im_info.dim_res['Y'], im_info.dim_res['X']) + + self.max_distance_um = max_distance_um * self.im_info.dim_res['T'] + self.max_distance_um = xp.max(xp.array([self.max_distance_um, 0.5])) + + self.vector_start_coords = [] + self.vectors = [] + self.vector_magnitudes = [] + + self.shape = () + + self.im_memmap = None + self.im_frangi_memmap = None + self.im_distance_memmap = None + self.im_marker_memmap = None + self.flow_vector_array_path = None + + self.debug = None + + self.viewer = viewer + + def _calculate_normalized_moments(self, images): + # I know the broadcasting is super confusing, but it makes it so much faster (400x)... + + num_images, height, width = images.shape + extended_images = images[:, :, :, None, None] # shape (num_images, height, width, 1, 1) + + # pre-compute meshgrid + x, y = xp.meshgrid(xp.arange(width), xp.arange(height)) + + # reshape for broadcasting + x = x[None, :, :, None, None] + y = y[None, :, :, None, None] + + # raw moments + M = xp.sum(extended_images * (x ** xp.arange(4)[None, None, None, :, None]) * + (y ** xp.arange(4)[None, None, None, None, :]), axis=(1, 2)) + + # central Moments; compute x_bar and y_bar + x_bar = M[:, 1, 0] / M[:, 0, 0] + y_bar = M[:, 0, 1] / M[:, 0, 0] + + x_bar = x_bar[:, None, None, None, None] + y_bar = y_bar[:, None, None, None, None] + + # calculate mu using broadcasting + mu = xp.sum(extended_images * (x - x_bar) ** xp.arange(4)[None, None, None, :, None] * + (y - y_bar) ** xp.arange(4)[None, None, None, None, :], axis=(1, 2)) + + # normalized moments + i_plus_j = xp.arange(4)[:, None] + xp.arange(4)[None, :] + eta = mu / (M[:, 0, 0][:, None, None] ** ((i_plus_j[None, :, :] + 2) / 2)) + + return eta + + def _calculate_hu_moments(self, eta): + num_images = eta.shape[0] + hu = xp.zeros((num_images, 6)) # initialize Hu moments for each image + + hu[:, 0] = eta[:, 2, 0] + eta[:, 0, 2] + hu[:, 1] = (eta[:, 2, 0] - eta[:, 0, 2]) ** 2 + 4 * eta[:, 1, 1] ** 2 + hu[:, 2] = (eta[:, 3, 0] - 3 * eta[:, 1, 2]) ** 2 + (3 * eta[:, 2, 1] - eta[:, 0, 3]) ** 2 + hu[:, 3] = (eta[:, 3, 0] + eta[:, 1, 2]) ** 2 + (eta[:, 2, 1] + eta[:, 0, 3]) ** 2 + hu[:, 4] = (eta[:, 3, 0] - 3 * eta[:, 1, 2]) * (eta[:, 3, 0] + eta[:, 1, 2]) * \ + ((eta[:, 3, 0] + eta[:, 1, 2]) ** 2 - 3 * (eta[:, 2, 1] + eta[:, 0, 3]) ** 2) + \ + (3 * eta[:, 2, 1] - eta[:, 0, 3]) * (eta[:, 2, 1] + eta[:, 0, 3]) * \ + (3 * (eta[:, 3, 0] + eta[:, 1, 2]) ** 2 - (eta[:, 2, 1] + eta[:, 0, 3]) ** 2) + hu[:, 5] = (eta[:, 2, 0] - eta[:, 0, 2]) * \ + ((eta[:, 3, 0] + eta[:, 1, 2]) ** 2 - (eta[:, 2, 1] + eta[:, 0, 3]) ** 2) + \ + 4 * eta[:, 1, 1] * (eta[:, 3, 0] + eta[:, 1, 2]) * (eta[:, 2, 1] + eta[:, 0, 3]) + # don't want mirror symmetry invariance.. doesn't make sense for our application + # hu[:, 6] = (3 * eta[:, 2, 1] - eta[:, 0, 3]) * (eta[:, 3, 0] + eta[:, 1, 2]) * \ + # ((eta[:, 3, 0] + eta[:, 1, 2]) ** 2 - 3 * (eta[:, 2, 1] + eta[:, 0, 3]) ** 2) - \ + # (eta[:, 3, 0] - 3 * eta[:, 1, 2]) * (eta[:, 2, 1] + eta[:, 0, 3]) * \ + # (3 * (eta[:, 3, 0] + eta[:, 1, 2]) ** 2 - (eta[:, 2, 1] + eta[:, 0, 3]) ** 2) + + return hu # return the first 5 Hu moments for each image + + def _calculate_mean_and_variance(self, images): + num_images = images.shape[0] + features = xp.zeros((num_images, 2)) + mask = images != 0 + + if self.im_info.no_z: + axis = (1, 2) + else: + axis = (1, 2, 3) + count_nonzero = xp.sum(mask, axis=axis) + sum_nonzero = xp.sum(images * mask, axis=axis) + sumsq_nonzero = xp.sum((images * mask) ** 2, axis=axis) + + mean = sum_nonzero / count_nonzero + variance = (sumsq_nonzero - (sum_nonzero ** 2) / count_nonzero) / count_nonzero + + features[:, 0] = mean + features[:, 1] = variance + return features + + def _get_im_bounds(self, markers, distance_frame): + if not self.im_info.no_z: + radii = distance_frame[markers[:, 0], markers[:, 1], markers[:, 2]] + else: + radii = distance_frame[markers[:, 0], markers[:, 1]] + marker_radii = xp.ceil(radii) + low_0 = xp.clip(markers[:, 0] - marker_radii, 0, self.shape[1]) + high_0 = xp.clip(markers[:, 0] + (marker_radii + 1), 0, self.shape[1]) + low_1 = xp.clip(markers[:, 1] - marker_radii, 0, self.shape[2]) + high_1 = xp.clip(markers[:, 1] + (marker_radii + 1), 0, self.shape[2]) + if not self.im_info.no_z: + low_2 = xp.clip(markers[:, 2] - marker_radii, 0, self.shape[3]) + high_2 = xp.clip(markers[:, 2] + (marker_radii + 1), 0, self.shape[3]) + return low_0, high_0, low_1, high_1, low_2, high_2 + return low_0, high_0, low_1, high_1 + + def _get_sub_volumes(self, im_frame, im_bounds, max_radius): + if self.im_info.no_z: + y_low, y_high, x_low, x_high = im_bounds + else: + z_low, z_high, y_low, y_high, x_low, x_high = im_bounds + + # preallocate arrays + if self.im_info.no_z: + sub_volumes = xp.zeros((len(y_low), max_radius, max_radius)) + else: + sub_volumes = xp.zeros((len(y_low), max_radius, max_radius, max_radius)) + + # extract sub-volumes + for i in range(len(y_low)): + if self.im_info.no_z: + yl, yh, xl, xh = int(y_low[i]), int(y_high[i]), int(x_low[i]), int(x_high[i]) + sub_volumes[i, :yh - yl, :xh - xl] = im_frame[yl:yh, xl:xh] + else: + zl, zh, yl, yh, xl, xh = int(z_low[i]), int(z_high[i]), int(y_low[i]), int(y_high[i]), int( + x_low[i]), int(x_high[i]) + sub_volumes[i, :zh - zl, :yh - yl, :xh - xl] = im_frame[zl:zh, yl:yh, xl:xh] + + return sub_volumes + + def _get_orthogonal_projections(self, sub_volumes): + # max projections along each axis + z_projections = xp.max(sub_volumes, axis=1) + y_projections = xp.max(sub_volumes, axis=2) + x_projections = xp.max(sub_volumes, axis=3) + + return z_projections, y_projections, x_projections + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for hu-based tracking.') + self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) + self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) + self.im_frangi_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) + self.im_marker_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_marker']) + self.im_distance_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_distance']) + self.shape = self.label_memmap.shape + + self.flow_vector_array_path = self.im_info.pipeline_paths['flow_vector_array'] + + def _get_hu_moments(self, sub_volumes): + if self.im_info.no_z: + etas = self._calculate_normalized_moments(sub_volumes) + hu_moments = self._calculate_hu_moments(etas) + return hu_moments + intensity_projections = self._get_orthogonal_projections(sub_volumes) + etas_z = self._calculate_normalized_moments(intensity_projections[0]) + etas_y = self._calculate_normalized_moments(intensity_projections[1]) + etas_x = self._calculate_normalized_moments(intensity_projections[2]) + hu_moments_z = self._calculate_hu_moments(etas_z) + hu_moments_y = self._calculate_hu_moments(etas_y) + hu_moments_x = self._calculate_hu_moments(etas_x) + hu_moments = xp.concatenate((hu_moments_z, hu_moments_y, hu_moments_x), axis=1) + return hu_moments + + def _concatenate_hu_matrices(self, hu_matrices): + return xp.concatenate(hu_matrices, axis=1) + + def _get_feature_matrix(self, t): + intensity_frame = xp.array(self.im_memmap[t]) + frangi_frame = xp.array(self.im_frangi_memmap[t]) + frangi_frame[frangi_frame > 0] = xp.log10(frangi_frame[frangi_frame > 0]) + frangi_frame[frangi_frame < 0] -= xp.min(frangi_frame[frangi_frame < 0]) + + distance_frame = xp.array(self.im_distance_memmap[t]) + distance_max_frame = ndi.maximum_filter(distance_frame, size=3) * 2 + + marker_frame = xp.array(self.im_marker_memmap[t]) > 0 + marker_indices = xp.argwhere(marker_frame) + + region_bounds = self._get_im_bounds(marker_indices, distance_max_frame) + if len(region_bounds[0]) == 0: + return xp.array([]), xp.array([]) + + max_radius = int(xp.ceil(xp.max(distance_max_frame[marker_frame]))) * 2 + 1 + + intensity_sub_volumes = self._get_sub_volumes(intensity_frame, region_bounds, max_radius) + frangi_sub_volumes = self._get_sub_volumes(frangi_frame, region_bounds, max_radius) + + intensity_stats = self._calculate_mean_and_variance(intensity_sub_volumes) + frangi_stats = self._calculate_mean_and_variance(frangi_sub_volumes) + stats_feature_matrix = self._concatenate_hu_matrices([intensity_stats, frangi_stats]) + + intensity_hus = self._get_hu_moments(intensity_sub_volumes) + log_hu_feature_matrix = -1 * xp.copysign(1.0, intensity_hus) * xp.log10(xp.abs(intensity_hus)) + log_hu_feature_matrix[xp.isinf(log_hu_feature_matrix)] = xp.nan + + return stats_feature_matrix, log_hu_feature_matrix + + def _get_distance_mask(self, t): + marker_frame_pre = np.array(self.im_marker_memmap[t - 1]) > 0 + marker_indices_pre = np.argwhere(marker_frame_pre) + marker_indices_pre_scaled = marker_indices_pre * self.scaling + marker_frame_post = np.array(self.im_marker_memmap[t]) > 0 + marker_indices_post = np.argwhere(marker_frame_post) + marker_indices_post_scaled = marker_indices_post * self.scaling + + distance_matrix = xp.array(cdist(marker_indices_post_scaled, marker_indices_pre_scaled)) + distance_mask = distance_matrix < self.max_distance_um + distance_matrix = distance_matrix / self.max_distance_um # normalize to furthest possible distance + return distance_matrix, distance_mask + + def _get_difference_matrix(self, m1, m2): + if len(m1) == 0 or len(m2) == 0: + return xp.array([]) + m1_reshaped = m1[:, xp.newaxis, :].astype(xp.float16) + m2_reshaped = m2[xp.newaxis, :, :].astype(xp.float16) + difference_matrix = xp.abs(m1_reshaped - m2_reshaped) + return difference_matrix + + def _zscore_normalize(self, m, mask): + if len(m) == 0: + return xp.array([]) + depth = m.shape[2] + + sum_mask = xp.sum(mask) + mean_vals = xp.zeros(depth) + std_vals = xp.zeros(depth) + + # calculate mean values slice by slice + for d in range(depth): + slice_m = m[:, :, d] + mean_vals[d] = xp.sum(slice_m * mask) / sum_mask + + # calculate std values slice by slice + for d in range(depth): + slice_m = m[:, :, d] + std_vals[d] = xp.sqrt(xp.sum((slice_m - mean_vals[d]) ** 2 * mask) / sum_mask) + + # normalize and set to infinity where mask is 0 + for d in range(depth): + slice_m = m[:, :, d] + slice_m -= mean_vals[d] + slice_m /= std_vals[d] + slice_m[mask == 0] = xp.inf + + return m + + def _get_cost_matrix(self, t, stats_vecs, pre_stats_vecs, hu_vecs, pre_hu_vecs): + if len(stats_vecs) == 0 or len(pre_stats_vecs) == 0 or len(hu_vecs) == 0 or len(pre_hu_vecs) == 0: + return xp.array([]) + distance_matrix, distance_mask = self._get_distance_mask(t) + z_score_distance_matrix = self._zscore_normalize(xp.array(distance_matrix)[..., xp.newaxis], + distance_mask).astype(xp.float16) + del distance_matrix + stats_matrix = self._get_difference_matrix(stats_vecs, pre_stats_vecs) + z_score_stats_matrix = (self._zscore_normalize(stats_matrix, distance_mask) / stats_matrix.shape[2]).astype( + xp.float16) + del stats_matrix + hu_matrix = self._get_difference_matrix(hu_vecs, pre_hu_vecs) + z_score_hu_matrix = (self._zscore_normalize(hu_matrix, distance_mask) / hu_matrix.shape[2]).astype(xp.float16) + del hu_matrix, distance_mask + z_score_matrix = xp.concatenate((z_score_distance_matrix, z_score_stats_matrix, z_score_hu_matrix), + axis=2).astype(xp.float16) + cost_matrix = xp.nansum(z_score_matrix, axis=2).astype(xp.float16) + + return cost_matrix + + def _find_best_matches(self, cost_matrix): + if len(cost_matrix) == 0: + return [], [], [] + candidates = [] + cost_cutoff = 1 + + # find row-wise minimums + row_min_idx = xp.argmin(cost_matrix, axis=1) + row_min_val = xp.min(cost_matrix, axis=1) + + # find column-wise minimums + col_min_idx = xp.argmin(cost_matrix, axis=0) + col_min_val = xp.min(cost_matrix, axis=0) + + row_matches = [] + col_matches = [] + costs = [] + + # store each row's and column's minimums as candidates for matching + for i, (r_idx, r_val) in enumerate(zip(row_min_idx, row_min_val)): + if r_val > cost_cutoff: + continue + candidates.append((int(i), int(r_idx), float(r_val))) + row_matches.append(int(i)) + col_matches.append(int(r_idx)) + costs.append(float(r_val)) + + for j, (c_idx, c_val) in enumerate(zip(col_min_idx, col_min_val)): + if c_val > cost_cutoff: + continue + candidates.append((int(c_idx), int(j), float(c_val))) + row_matches.append(int(c_idx)) + col_matches.append(int(j)) + costs.append(float(c_val)) + + return row_matches, col_matches, costs + + def _run_hu_tracking(self): + pre_stats_vecs = None + pre_hu_vecs = None + flow_vector_array = None + for t in range(self.num_t): + if self.viewer is not None: + self.viewer.status = f'Tracking mocap markers. Frame: {t + 1} of {self.num_t}.' + logger.debug(f'Running hu-moment tracking for frame {t + 1} of {self.num_t}') + stats_vecs, hu_vecs = self._get_feature_matrix(t) + # todo make distance weighting be dependent on number of seconds between frames (more uncertain with more time) + # could also vary with size (radius) based on diffusion coefficient. bigger = probably closer + if pre_stats_vecs is None or pre_hu_vecs is None: + pre_stats_vecs = stats_vecs + pre_hu_vecs = hu_vecs + continue + cost_matrix = self._get_cost_matrix(t, stats_vecs, pre_stats_vecs, hu_vecs, pre_hu_vecs) + row_indices, col_indices, costs = self._find_best_matches(cost_matrix) + pre_marker_indices = np.argwhere(self.im_marker_memmap[t - 1])[col_indices] + marker_indices = np.argwhere(self.im_marker_memmap[t])[row_indices] + vecs = np.array(marker_indices) - np.array(pre_marker_indices) + + pre_stats_vecs = stats_vecs + pre_hu_vecs = hu_vecs + + costs = np.array(costs) + if self.im_info.no_z: + idx0_y, idx0_x = pre_marker_indices.T + vec_y, vec_x = vecs.T + frame_vector_array = np.concatenate((np.array([t - 1] * len(vec_y))[:, np.newaxis], + idx0_y[:, np.newaxis], idx0_x[:, np.newaxis], + vec_y[:, np.newaxis], vec_x[:, np.newaxis], + costs[:, np.newaxis]), axis=1) + else: + idx0_z, idx0_y, idx0_x = pre_marker_indices.T + vec_z, vec_y, vec_x = vecs.T + frame_vector_array = np.concatenate((np.array([t - 1] * len(vec_z))[:, np.newaxis], + idx0_z[:, np.newaxis], idx0_y[:, np.newaxis], + idx0_x[:, np.newaxis], + vec_z[:, np.newaxis], vec_y[:, np.newaxis], vec_x[:, np.newaxis], + costs[:, np.newaxis]), axis=1) + if flow_vector_array is None: + flow_vector_array = frame_vector_array + else: + flow_vector_array = np.concatenate((flow_vector_array, frame_vector_array), axis=0) + del frame_vector_array + + # save the array + np.save(self.flow_vector_array_path, flow_vector_array) + +
+[docs] + def run(self): + if self.im_info.no_t: + return + self._get_t() + self._allocate_memory() + self._run_hu_tracking()
+
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nelly_smorgasbord\deskewed-iono_pre.ome.tif" + im_info = ImInfo(im_path) + hu = HuMomentTracking(im_info, num_t=2) + hu.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/tracking/voxel_reassignment.html b/docs/_build/html/_modules/nellie/tracking/voxel_reassignment.html new file mode 100644 index 0000000..d8c9f6e --- /dev/null +++ b/docs/_build/html/_modules/nellie/tracking/voxel_reassignment.html @@ -0,0 +1,504 @@ + + + + + + + nellie.tracking.voxel_reassignment — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.tracking.voxel_reassignment

+import heapq
+
+import numpy as np
+from scipy.spatial import cKDTree
+
+from nellie import logger
+from nellie.im_info.verifier import ImInfo
+from nellie.tracking.flow_interpolation import FlowInterpolator
+
+
+
+[docs] +class VoxelReassigner: + def __init__(self, im_info: ImInfo, num_t=None, + viewer=None): + self.im_info = im_info + + if self.im_info.no_t: + return + + self.num_t = num_t + if num_t is None and not self.im_info.no_t: + self.num_t = im_info.shape[im_info.axes.index('T')] + self.flow_interpolator_fw = FlowInterpolator(im_info) + self.flow_interpolator_bw = FlowInterpolator(im_info, forward=False) + + self.running_matches = [] + + self.voxel_matches_path = None + self.branch_label_memmap = None + self.obj_label_memmap = None + self.reassigned_branch_memmap = None + self.reassigned_obj_memmap = None + + self.debug = None + + self.viewer = viewer + + def _match_forward(self, flow_interpolator, vox_prev, vox_next, t): + vectors_interpx_prev = flow_interpolator.interpolate_coord(vox_prev, t) + if vectors_interpx_prev is None: + return [], [], [] + # only keep voxels that are not nan + kept_prev_vox_idxs = ~np.isnan(vectors_interpx_prev).any(axis=1) + # only keep vectors where the voxel is not nan + vectors_interpx_prev = vectors_interpx_prev[kept_prev_vox_idxs] + # get centroids in t1 from voxels in t0 + interpolated flow at that voxel + vox_prev_kept = vox_prev[kept_prev_vox_idxs] + centroids_next_interpx = vox_prev_kept + vectors_interpx_prev + if len(centroids_next_interpx) == 0: + return [], [], [] + # now we have estimated centroids in t1 (centroids_next_interpx) and linked voxels in t0 (vox_prev[kept_prev_vox_idxs]). + # we then have to match t1 voxels (vox_next) to estimated t1 centroids (centroids_next_interpx) + match_dist, matched_idx = self._match_voxels_to_centroids(vox_next, centroids_next_interpx) + vox_matched_to_centroids = vox_next[matched_idx.tolist()] + # then link those t1 voxels back to the t0 voxels + # now we have linked t0 voxels (vox_prev_kept) to t1 voxels (vox_matched_to_centroids) + # but we have to make sure the link is within a distance constraint. + vox_prev_matched_valid, vox_next_matched_valid, distances_valid = self._distance_threshold( + vox_prev_kept, vox_matched_to_centroids + ) + return vox_prev_matched_valid, vox_next_matched_valid, distances_valid + + def _match_backward(self, flow_interpolator, vox_next, vox_prev, t): + # interpolate flow vectors to all voxels in t1 from centroids derived from t0 centroids + t0 flow vectors + vectors_interpx_prev = flow_interpolator.interpolate_coord(vox_next, t) + if vectors_interpx_prev is None: + return [], [], [] + # only keep voxels that are not nan + kept_next_vox_idxs = ~np.isnan(vectors_interpx_prev).any(axis=1) + # only keep vectors where the voxel is not nan + vectors_interpx_prev = vectors_interpx_prev[kept_next_vox_idxs] + # get centroids in t0 from voxels in t1 - interpolated flow (from t0 to t1) at that voxel + vox_next_kept = vox_next[kept_next_vox_idxs] + centroids_prev_interpx = vox_next_kept - vectors_interpx_prev + if len(centroids_prev_interpx) == 0: + return [], [], [] + # now we have estimated centroids in t0 (centroids_prev_interpx) and linked voxels in t1 (vox_next[kept_next_vox_idxs]). + # we then have to match t0 voxels (vox_prev) to estimated t0 centroids (centroids_prev_interpx) + match_dist, matched_idx = self._match_voxels_to_centroids(vox_prev, centroids_prev_interpx) + vox_matched_to_centroids = vox_prev[matched_idx.tolist()] + # then link those t1 voxels (vox_next_kept) back to the t0 voxels (vox_matched_to_centroids). + # but we have to make sure the link is within a distance constraint. + vox_prev_matched_valid, vox_next_matched_valid, distances_valid = self._distance_threshold( + vox_matched_to_centroids, vox_next_kept + ) + return vox_prev_matched_valid, vox_next_matched_valid, distances_valid + + def _match_voxels_to_centroids(self, coords_real, coords_interpx): + coords_interpx = np.array(coords_interpx) * self.flow_interpolator_fw.scaling + coords_real = np.array(coords_real) * self.flow_interpolator_fw.scaling + tree = cKDTree(coords_real) + dist, idx = tree.query(coords_interpx, k=1, workers=-1) + return dist, idx + + def _assign_unique_matches(self, vox_prev_matches, vox_next_matches, distances): + # create a dict where the key is a voxel in t1, and the value is a list of distances and t0 voxels matched to it + vox_next_dict = {} + for match_idx, match_next in enumerate(vox_next_matches): + match_next_tuple = tuple(match_next) + if match_next_tuple not in vox_next_dict.keys(): + vox_next_dict[match_next_tuple] = [[], []] + vox_next_dict[match_next_tuple][0].append(distances[match_idx]) + vox_next_dict[match_next_tuple][1].append(vox_prev_matches[match_idx]) + + # now assign matches based on the t1 voxel's closest (in distance) matched t0 voxel + vox_prev_matches_final = [] + vox_next_matches_final = [] + for match_next_tuple, (distance_match_list, vox_prev_match_list) in vox_next_dict.items(): + if len(distance_match_list) == 1: + vox_prev_matches_final.append(vox_prev_match_list[0]) + vox_next_matches_final.append(match_next_tuple) + continue + min_idx = np.argmin(distance_match_list) + vox_prev_matches_final.append(vox_prev_match_list[min_idx]) + vox_next_matches_final.append(match_next_tuple) + + # create a priority queue with (distance, prev_voxel, next_voxel) tuples + priority_queue = [(distances[i], tuple(vox_prev_matches[i]), tuple(vox_next_matches[i])) + for i in range(len(distances))] + heapq.heapify(priority_queue) # Convert list to a heap in-place + + assigned_prev = set() + assigned_next = set() + vox_prev_matches_final = [] + vox_next_matches_final = [] + + while priority_queue: + # pop the smallest distance tuple from the heap + distance, prev_voxel, next_voxel = heapq.heappop(priority_queue) + + if prev_voxel not in assigned_prev or next_voxel not in assigned_next: + # if neither of the voxels has been assigned, then assign them + vox_prev_matches_final.append(prev_voxel) + vox_next_matches_final.append(next_voxel) + assigned_prev.add(prev_voxel) + assigned_next.add(next_voxel) + + return vox_prev_matches_final, vox_next_matches_final + + def _distance_threshold(self, vox_prev_matched, vox_next_matched): + distances = np.linalg.norm((vox_prev_matched - vox_next_matched) * self.flow_interpolator_fw.scaling, axis=1) + distance_mask = distances < self.flow_interpolator_fw.max_distance_um + vox_prev_matched_valid = vox_prev_matched[distance_mask] + vox_next_matched_valid = vox_next_matched[distance_mask] + distances_valid = distances[distance_mask] + return vox_prev_matched_valid, vox_next_matched_valid, distances_valid + +
+[docs] + def match_voxels(self, vox_prev, vox_next, t): + # forward interpolation: + # from t0 voxels and interpolated flow, get t1 centroids. + # match nearby t1 voxels to t1 centroids, which are linked to t0 voxels. + logger.debug(f'Forward voxel matching for t: {t}') + vox_prev_matches_fw, vox_next_matches_fw, distances_fw = self._match_forward( + self.flow_interpolator_fw, vox_prev, vox_next, t + ) + + # backward interpolation: + # from t0 centroids and real flow, get t1 centroids. + # interpolate flow at nearby t1 voxels. subtract flow from voxels to get t0 centroids. + # match nearby t0 voxels to t0 centroids, which are linked to t1 voxels. + logger.debug(f'Backward voxel matching for t: {t}') + vox_prev_matches_bw, vox_next_matches_bw, distances_bw = self._match_backward( + self.flow_interpolator_bw, vox_next, vox_prev, t + 1 + ) + + logger.debug(f'Assigning unique matches for t: {t}') + vox_prev_matches = np.concatenate([vox_prev_matches_fw, vox_prev_matches_bw]) + vox_next_matches = np.concatenate([vox_next_matches_fw, vox_next_matches_bw]) + distances = np.concatenate([distances_fw, distances_bw]) + + vox_prev_matches_unique, vox_next_matches_unique = self._assign_unique_matches(vox_prev_matches, + vox_next_matches, distances) + + vox_next_matches_unique = np.array(vox_next_matches_unique) + if len(vox_next_matches_unique) == 0: + return [], [] + vox_next_matched_tuples = set([tuple(coord) for coord in vox_next_matches_unique]) + vox_next_unmatched = np.array([coord for coord in vox_next if tuple(coord) not in vox_next_matched_tuples]) + + unmatched_diff = np.inf + while unmatched_diff: + num_unmatched = len(vox_next_unmatched) + if num_unmatched == 0: + break + tree = cKDTree(vox_next_matches_unique * self.flow_interpolator_fw.scaling) + dists, idxs = tree.query(vox_next_unmatched * self.flow_interpolator_fw.scaling, k=1, workers=-1) + unmatched_matches = np.array([ + [vox_prev_matches_unique[idx], vox_next_unmatched[i]] + for i, idx in enumerate(idxs) if dists[i] < self.flow_interpolator_fw.max_distance_um + ]) + if len(unmatched_matches) == 0: + break + # add unmatched matches to coords_matched + vox_prev_matches_unique = np.concatenate([vox_prev_matches_unique, unmatched_matches[:, 0]]) + vox_next_matches_unique = np.concatenate([vox_next_matches_unique, unmatched_matches[:, 1]]) + vox_next_matched_tuples = set([tuple(coord) for coord in vox_next_matches_unique]) + vox_next_unmatched = np.array([coord for coord in vox_next if tuple(coord) not in vox_next_matched_tuples]) + new_num_unmatched = len(vox_next_unmatched) + unmatched_diff = num_unmatched - new_num_unmatched + logger.debug(f'Reassigned {unmatched_diff}/{num_unmatched} unassigned voxels. ' + f'{new_num_unmatched} remain.') + return np.array(vox_prev_matches_unique), np.array(vox_next_matches_unique)
+ + + def _get_t(self): + if self.num_t is None: + if self.im_info.no_t: + self.num_t = 1 + else: + self.num_t = self.im_info.shape[self.im_info.axes.index('T')] + else: + return + + def _allocate_memory(self): + logger.debug('Allocating memory for voxel reassignment.') + self.voxel_matches_path = self.im_info.pipeline_paths['voxel_matches'] + + self.branch_label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_skel_relabelled']) + self.obj_label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) + self.shape = self.branch_label_memmap.shape + + reassigned_branch_label_path = self.im_info.pipeline_paths['im_branch_label_reassigned'] + self.reassigned_branch_memmap = self.im_info.allocate_memory(reassigned_branch_label_path, + dtype='int32', + description='branch label reassigned', + return_memmap=True) + + reassigned_obj_label_path = self.im_info.pipeline_paths['im_obj_label_reassigned'] + self.reassigned_obj_memmap = self.im_info.allocate_memory(reassigned_obj_label_path, + dtype='int32', + description='object label reassigned', + return_memmap=True) + + def _run_frame(self, t, all_mask_coords, reassigned_memmap): + logger.info(f'Reassigning pixels in frame {t + 1} of {self.num_t - 1}') + + vox_prev = all_mask_coords[t] + vox_next = all_mask_coords[t + 1] + if len(vox_prev) == 0 or len(vox_next) == 0: + return True + + matched_prev, matched_next = self.match_voxels(vox_prev, vox_next, t) + if len(matched_prev) == 0: + return True + matched_prev = matched_prev.astype('uint16') + matched_next = matched_next.astype('uint16') + + self.running_matches.append([matched_prev, matched_next]) + + reassigned_memmap[t + 1][tuple(matched_next.T)] = reassigned_memmap[t][tuple(matched_prev.T)] + + return False + + def _run_reassignment(self, label_type): + # todo, be able to specify which frame to start at. + if label_type == 'branch': + label_memmap = self.branch_label_memmap + reassigned_memmap = self.reassigned_branch_memmap + elif label_type == 'obj': + label_memmap = self.obj_label_memmap + reassigned_memmap = self.reassigned_obj_memmap + else: + raise ValueError('label_type must be "branch" or "obj".') + vox_prev = np.argwhere(label_memmap[0] > 0) + reassigned_memmap[0][tuple(vox_prev.T)] = label_memmap[0][tuple(vox_prev.T)] + all_mask_coords = [np.argwhere(label_memmap[t] > 0) for t in range(self.num_t)] + + for t in range(self.num_t - 1): + if self.viewer is not None: + self.viewer.status = f'Reassigning voxels. Frame: {t + 1} of {self.num_t}.' + no_matches = self._run_frame(t, all_mask_coords, reassigned_memmap) + + if no_matches: + break + +
+[docs] + def run(self): + if self.im_info.no_t: + return + self._get_t() + self._allocate_memory() + self._run_reassignment('branch') + self._run_reassignment('obj') + # save running matches to npy + np.save(self.voxel_matches_path, np.array(self.running_matches, dtype=object))
+
+ + + +if __name__ == "__main__": + im_path = r"D:\test_files\nelly_smorgasbord\deskewed-iono_pre.ome.tif" + im_info = ImInfo(im_path) + num_t = 3 + run_obj = VoxelReassigner(im_info, num_t=num_t) + run_obj.run() + + import pickle + + # This section seems random, but it allows for finding links between any level of the hierarchy to any + # other level in the hierarchy at any time point via lots of dot products. + edges_loaded = pickle.load(open(im_info.pipeline_paths['adjacency_maps'], "rb")) + + mask_01 = run_obj.obj_label_memmap[:2] > 0 + mask_voxels_0 = np.argwhere(mask_01[0]) + mask_voxels_1 = np.argwhere(mask_01[1]) + + t0_coords_in_mask_0 = {tuple(coord): idx for idx, coord in enumerate(mask_voxels_0)} + t1_coords_in_mask_1 = {tuple(coord): idx for idx, coord in enumerate(mask_voxels_1)} + + idx_matches_0 = [t0_coords_in_mask_0[tuple(coord)] for coord in run_obj.running_matches[0][0]] + idx_matches_1 = [t1_coords_in_mask_1[tuple(coord)] for coord in run_obj.running_matches[0][1]] + # sort based on idx_matches_0 + sorted_idx_matches_0_order = np.argsort(idx_matches_0) + sorted_idx_matches_0 = np.array(idx_matches_0)[sorted_idx_matches_0_order] + sorted_idx_matches_1 = np.array(idx_matches_1)[sorted_idx_matches_0_order] + + v_t = np.zeros((len(mask_voxels_0), len(mask_voxels_1)), dtype=np.uint16) + v_t[sorted_idx_matches_0, sorted_idx_matches_1] = True + + b_v = edges_loaded['b_v'][0].astype(np.uint16) + # dot product b_v and v_t + import cupy as cp + + + def dot_product_in_chunks(a, b, chunk_size=100): + result = cp.zeros((a.shape[0], b.shape[1]), dtype=cp.uint8) + for start_row in range(0, a.shape[1], chunk_size): + print(start_row, start_row + chunk_size) + end_row = start_row + chunk_size + v_t_chunk = b[start_row:end_row, :] + b_v_chunk = a[:, start_row:end_row] + result += cp.dot(b_v_chunk, v_t_chunk) # Adjust this line as per your logic + return result + + + # Convert your numpy arrays to cupy arrays + v_t_cp = cp.array(v_t, dtype=cp.uint8) + b_v_cp = cp.array(b_v, dtype=cp.uint8) + + # Perform dot product in chunks + b0_v1_cp = dot_product_in_chunks(b_v_cp, v_t_cp) + + # Convert the result back to a numpy array if needed + b1_v1 = edges_loaded['b_v'][1].astype(np.uint16) + b1_v1_cp = cp.array(b1_v1, dtype=cp.uint8) + b0_b1_cp = dot_product_in_chunks(b0_v1_cp, b1_v1_cp.T) + b0_b1 = cp.asnumpy(b0_b1_cp) + # b0_b1 are the new edges between branches in time 0 and time 1, values are the number of voxels in common between (aka weighting to give the edges) + + # find the indices of the maximum value in each col + max_idx = np.argmax(b0_b1, axis=0) + 1 + + mask_branches = np.zeros(mask_01.shape, dtype=np.uint16) + branch_labels_0 = np.argmax(b_v.T, axis=1) + branch_labels_1 = np.argmax(b1_v1.T, axis=1) + + mask_branches[0][tuple(mask_voxels_0.T)] = branch_labels_0 + 1 + + # replace any non-zero values in b1_v1 with the max_idx + new_branch_labels_1 = max_idx[branch_labels_1] + mask_branches[1][tuple(mask_voxels_1.T)] = new_branch_labels_1 + 1 + # these branches are relabelled by t0 branch labels. + + # lets do this with nodes, too + n_v = edges_loaded['n_v'][0].astype(np.uint16) + n_v_cp = cp.array(n_v, dtype=cp.uint8) + n0_v1_cp = dot_product_in_chunks(n_v_cp, v_t_cp) + n1_v1 = edges_loaded['n_v'][1].astype(np.uint16) + n1_v1_cp = cp.array(n1_v1, dtype=cp.uint8) + n0_n1_cp = dot_product_in_chunks(n0_v1_cp, n1_v1_cp.T) + + n0_n1 = cp.asnumpy(n0_n1_cp) + max_idx_n = np.argmax(n0_n1, axis=0) + 1 + + mask_nodes = np.zeros(mask_01.shape, dtype=np.uint16) + node_labels_0 = np.argmax(n_v.T, axis=1) + node_labels_1 = np.argmax(n1_v1.T, axis=1) + mask_nodes[0][tuple(mask_voxels_0.T)] = node_labels_0 + 1 + new_node_labels_1 = max_idx_n[node_labels_1] + mask_nodes[1][tuple(mask_voxels_1.T)] = new_node_labels_1 + 1 + + # # todo useful for getting continuous tracks for voxels + # matches_t0_t1 = run_obj.running_matches[0][1] + # matches_t1_t2 = run_obj.running_matches[1][0] + # + # t1_coords_in_t0_t1 = {tuple(coord): idx for idx, coord in enumerate(matches_t0_t1)} + # t1_coords_in_t1_t2 = {tuple(coord): idx for idx, coord in enumerate(matches_t1_t2)} + # + # t0_coords_in_t0_t1 = {tuple(coord): idx for idx, coord in enumerate(run_obj.running_matches[0][0])} + # + # # Create the continuous track list + # continuous_tracks = [] + # for t1_coord, t0_idx in t1_coords_in_t0_t1.items(): + # if t1_coord in t1_coords_in_t1_t2: + # t0_coord = run_obj.running_matches[0][0][t0_idx] + # t2_idx = t1_coords_in_t1_t2[t1_coord] + # t2_coord = run_obj.running_matches[1][1][t2_idx] + # continuous_tracks.append([t0_coord, t1_coord, t2_coord]) + # + # napari_tracks = [] + # for i, track in enumerate(continuous_tracks): + # napari_tracks.append([i, 0, track[0][0], track[0][1], track[0][2]]) + # napari_tracks.append([i, 1, track[1][0], track[1][1], track[1][2]]) + # napari_tracks.append([i, 2, track[2][0], track[2][1], track[2][2]]) +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/utils/general.html b/docs/_build/html/_modules/nellie/utils/general.html new file mode 100644 index 0000000..0663978 --- /dev/null +++ b/docs/_build/html/_modules/nellie/utils/general.html @@ -0,0 +1,151 @@ + + + + + + + nellie.utils.general — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.utils.general

+from nellie import logger, xp
+
+
+
+[docs] +def get_reshaped_image(im, num_t=None, im_info=None, t_slice=None): + logger.debug('Reshaping image.') + im_to_return = im + if im_info.no_z: + ndim = 2 + else: + ndim = 3 + # if 'C' in im_info.axes: + # im_to_return = xp.moveaxis(im_to_return, 0, -1 + if 'T' not in im_info.axes or (len(im_info.axes) > ndim and len(im_to_return.shape) == ndim): + im_to_return = im_to_return[None, ...] + logger.debug(f'Adding time dimension to image, shape is now {im_to_return.shape}.') + elif num_t is not None: + num_t = min(num_t, im_to_return.shape[0]) + im_to_return = im_to_return[:num_t, ...] + logger.debug(f'{num_t} timepoints found, shape is now {im_to_return.shape}.') + elif t_slice is not None: + im_to_return = im_to_return[t_slice:t_slice + 1, ...] # t: t + 1 to keep the time dimension + logger.debug(f'Using time slice {t_slice}, shape is now {im_to_return.shape}.') + return im_to_return
+ + + +
+[docs] +def bbox(im): + if len(im.shape) == 2: + rows = xp.any(im, axis=1) + cols = xp.any(im, axis=0) + if (not rows.any()) or (not cols.any()): + return 0, 0, 0, 0 + rmin, rmax = xp.where(rows)[0][[0, -1]] + cmin, cmax = xp.where(cols)[0][[0, -1]] + return int(rmin), int(rmax), int(cmin), int(cmax) + + elif len(im.shape) == 3: + r = xp.any(im, axis=(1, 2)) + c = xp.any(im, axis=(0, 2)) + z = xp.any(im, axis=(0, 1)) + if (not r.any()) or (not c.any()) or (not z.any()): + return 0, 0, 0, 0, 0, 0 + rmin, rmax = xp.where(r)[0][[0, -1]] + cmin, cmax = xp.where(c)[0][[0, -1]] + zmin, zmax = xp.where(z)[0][[0, -1]] + return int(rmin), int(rmax), int(cmin), int(cmax), int(zmin), int(zmax) + + else: + print("Image not 2D or 3D... Cannot get bounding box.") + return None
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie/utils/gpu_functions.html b/docs/_build/html/_modules/nellie/utils/gpu_functions.html new file mode 100644 index 0000000..668b840 --- /dev/null +++ b/docs/_build/html/_modules/nellie/utils/gpu_functions.html @@ -0,0 +1,179 @@ + + + + + + + nellie.utils.gpu_functions — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie.utils.gpu_functions

+from nellie import xp, device_type
+
+
+
+[docs] +def otsu_effectiveness(image, inter_variance): + # flatten image and create histogram + flattened_image = image.flatten() + sigma_total_squared = xp.var(flattened_image) + normalized_sigma_B_squared = inter_variance / sigma_total_squared + return normalized_sigma_B_squared
+ + + +
+[docs] +def otsu_threshold(matrix, nbins=256): + # gpu version of skimage.filters.threshold_otsu + counts, bin_edges = xp.histogram(matrix.reshape(-1), bins=nbins, range=(matrix.min(), matrix.max())) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2. + counts = counts / xp.sum(counts) + + weight1 = xp.cumsum(counts) + mean1 = xp.cumsum(counts * bin_centers) / weight1 + if device_type == 'mps': + weight2 = xp.cumsum(xp.flip(counts, dims=[0])) + weight2 = xp.flip(weight2, dims=[0]) + flipped_counts_bin_centers = xp.flip(counts * bin_centers, dims=[0]) + cumsum_flipped = xp.cumsum(flipped_counts_bin_centers) + mean2 = xp.flip(cumsum_flipped / xp.flip(weight2, dims=[0]), dims=[0]) + else: + weight2 = xp.cumsum(counts[::-1])[::-1] + mean2 = (xp.cumsum((counts * bin_centers)[::-1]) / weight2[::-1])[::-1] + + variance12 = weight1[:-1] * weight2[1:] * (mean1[:-1] - mean2[1:]) ** 2 + + idx = xp.argmax(variance12) + threshold = bin_centers[idx] + + return threshold, variance12[idx]
+ + + +
+[docs] +def triangle_threshold(matrix, nbins=256): + # gpu version of skimage.filters.threshold_triangle + hist, bin_edges = xp.histogram(matrix.reshape(-1), bins=nbins, range=(xp.min(matrix), xp.max(matrix))) + bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2. + hist = hist / xp.sum(hist) + + arg_peak_height = xp.argmax(hist) + peak_height = hist[arg_peak_height] + arg_low_level, arg_high_level = xp.flatnonzero(hist)[[0, -1]] + + flip = arg_peak_height - arg_low_level < arg_high_level - arg_peak_height + if flip: + if device_type == 'mps': + hist = xp.flip(hist, dims=[0]) + else: + hist = xp.flip(hist, axis=0) + + # todo check this + arg_low_level = nbins - arg_high_level - 1 + arg_peak_height = nbins - arg_peak_height - 1 + del (arg_high_level) + + width = arg_peak_height - arg_low_level + x1 = xp.arange(width) + y1 = hist[x1 + arg_low_level] + + norm = xp.sqrt(peak_height ** 2 + width ** 2) + peak_height = peak_height / norm + width = width / norm + + length = peak_height * x1 - width * y1 + arg_level = xp.argmax(length) + arg_low_level + + if flip: + arg_level = nbins - arg_level - 1 + + return bin_centers[arg_level]
+ +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_analysis.html b/docs/_build/html/_modules/nellie_napari/nellie_analysis.html new file mode 100644 index 0000000..597e61f --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_analysis.html @@ -0,0 +1,811 @@ + + + + + + + nellie_napari.nellie_analysis — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_analysis

+import os
+import pickle
+
+import numpy as np
+from qtpy.QtWidgets import QComboBox, QCheckBox, QPushButton, QLabel, QTableWidget, QTableWidgetItem, \
+    QSpinBox, QDoubleSpinBox, QWidget, QVBoxLayout, QGroupBox, QHBoxLayout
+from qtpy.QtCore import Qt
+
+from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
+from napari.utils.notifications import show_info
+import pandas as pd
+import datetime
+
+
+
+[docs] +class NellieAnalysis(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.viewer = napari_viewer + + self.canvas = FigureCanvasQTAgg() + self.canvas.figure.set_layout_engine("constrained") + + self.scale = (1, 1, 1) + + self.log_scale = False + self.is_median = False + self.mean_median_toggle = None + self.overlay_button = None + self.match_t_toggle = None + self.match_t = False + self.hist_min = None + self.hist_max = None + self.num_bins = None + self.hist_reset = True + self.export_data_button = None + self.save_graph_button = None + + self.click_match_table = None + + self.voxel_df = None + self.node_df = None + self.branch_df = None + self.organelle_df = None + self.image_df = None + + self.all_attr_data = None + self.attr_data = None + self.time_col = None + self.adjacency_maps = None + self.data_to_plot = None + self.mean = np.nan + self.std = np.nan + self.median = np.nan + self.iqr = np.nan + self.perc75 = np.nan + self.perc25 = np.nan + + # self.layout = QGridLayout() + # self.setLayout(self.layout) + + self.dropdown = None + self.dropdown_attr = None + self.log_scale_checkbox = None + self.selected_level = None + + self.label_mask = None + self.label_mask_layer = None + self.label_coords = [] + + self.click_match_texts = [] + + self.layout_anchors = { + 'dropdown': (0, 0), + 'canvas': (1, 0), + 'table': (50, 0) + } + + self.df = None + self.initialized = False + +
+[docs] + def reset(self): + self.initialized = False + self.voxel_df = None + self.node_df = None + self.branch_df = None + self.organelle_df = None + self.image_df = None
+ + +
+[docs] + def post_init(self): + self.log_scale_checkbox = QCheckBox("Log scale") + self.log_scale_checkbox.stateChanged.connect(self.on_log_scale) + + self.mean_median_toggle = QCheckBox("Median view") + self.mean_median_toggle.stateChanged.connect(self.toggle_mean_med) + + self.overlay_button = QPushButton("Overlay mask") + self.overlay_button.clicked.connect(self.overlay) + + self.match_t_toggle = QCheckBox("Timepoint data") + self.match_t_toggle.stateChanged.connect(self.toggle_match_t) + self.match_t_toggle.setEnabled(False) + self.viewer.dims.events.current_step.connect(self.on_t_change) + + self.hist_min = QDoubleSpinBox() + self.hist_min.setEnabled(False) + self.hist_min.valueChanged.connect(self.on_hist_change) + self.hist_min.setDecimals(4) + + self.hist_max = QDoubleSpinBox() + self.hist_max.setEnabled(False) + self.hist_max.valueChanged.connect(self.on_hist_change) + self.hist_max.setDecimals(4) + + self.num_bins = QSpinBox() + self.num_bins.setRange(1, 100) + self.num_bins.setValue(10) + self.num_bins.setEnabled(False) + self.num_bins.valueChanged.connect(self.on_hist_change) + + self.export_data_button = QPushButton("Export graph data") + self.export_data_button.clicked.connect(self.export_data) + + self.save_graph_button = QPushButton("Save graph") + self.save_graph_button.clicked.connect(self.save_graph) + + if self.nellie.im_info.no_z: + self.scale = (self.nellie.im_info.dim_res['Y'], self.nellie.im_info.dim_res['X']) + else: + self.scale = (self.nellie.im_info.dim_res['Z'], self.nellie.im_info.dim_res['Y'], self.nellie.im_info.dim_res['X']) + self.viewer.scale_bar.visible = True + self.viewer.scale_bar.unit = 'um' + + self._create_dropdown_selection() + self.check_for_adjacency_map() + + # # self.dropdown_attr = QComboBox() + # self._create_dropdown_selection() + # self.dropdown.setCurrentIndex(3) # Organelle + # self.dropdown_attr.currentIndexChanged.connect(self.on_attr_selected) + # self.dropdown_attr.setCurrentIndex(6) # Area + + self.set_ui() + self.initialized = True
+ + +
+[docs] + def set_ui(self): + main_layout = QVBoxLayout() + + # Attribute dropdown group + attr_group = QGroupBox("Select hierarchy level and attribute") + attr_layout = QHBoxLayout() + attr_layout.addWidget(self.dropdown) + attr_layout.addWidget(self.dropdown_attr) + attr_group.setLayout(attr_layout) + + # Histogram group + hist_group = QGroupBox("Histogram options") + hist_layout = QVBoxLayout() + + sub_layout = QHBoxLayout() + sub_layout.addWidget(QLabel("Min"), alignment=Qt.AlignRight) + sub_layout.addWidget(self.hist_min) + sub_layout.addWidget(self.hist_max) + sub_layout.addWidget(QLabel("Max"), alignment=Qt.AlignLeft) + hist_layout.addLayout(sub_layout) + hist_layout.addWidget(self.canvas) + + sub_layout = QHBoxLayout() + sub_layout.addWidget(QLabel("Bins"), alignment=Qt.AlignRight) + sub_layout.addWidget(self.num_bins) + hist_layout.addLayout(sub_layout) + hist_group.setLayout(hist_layout) + + hist_layout.addWidget(self.canvas) + + sub_layout = QHBoxLayout() + sub_layout.addWidget(self.log_scale_checkbox) + sub_layout.addWidget(self.mean_median_toggle) + sub_layout.addWidget(self.match_t_toggle) + sub_layout.addWidget(self.overlay_button) + hist_layout.addLayout(sub_layout) + + # Save options group + save_group = QGroupBox("Export options") + save_layout = QVBoxLayout() + sub_layout = QHBoxLayout() + sub_layout.addWidget(self.export_data_button) + sub_layout.addWidget(self.save_graph_button) + save_layout.addLayout(sub_layout) + save_group.setLayout(save_layout) + + main_layout.addWidget(attr_group) + main_layout.addWidget(hist_group) + main_layout.addWidget(save_group) + self.setLayout(main_layout)
+ + + def _create_dropdown_selection(self): + # Create the dropdown menu + self.dropdown = QComboBox() + self.dropdown.currentIndexChanged.connect(self.on_level_selected) + + self.rewrite_dropdown() + + self.dropdown_attr = QComboBox() + self.dropdown_attr.currentIndexChanged.connect(self.on_attr_selected) + + self.set_default_dropdowns() + +
+[docs] + def set_default_dropdowns(self): + organelle_idx = self.dropdown.findText('organelle') + self.dropdown.setCurrentIndex(organelle_idx) + area_raw_idx = self.dropdown_attr.findText('organelle_area_raw') + self.dropdown_attr.setCurrentIndex(area_raw_idx)
+ + +
+[docs] + def check_for_adjacency_map(self): + self.overlay_button.setEnabled(False) + if os.path.exists(self.nellie.im_info.pipeline_paths['adjacency_maps']): + self.overlay_button.setEnabled(True)
+ + +
+[docs] + def rewrite_dropdown(self): + self.check_for_adjacency_map() + + self.dropdown.clear() + if os.path.exists(self.nellie.im_info.pipeline_paths['features_nodes']): + options = ['none', 'voxel', 'node', 'branch', 'organelle', 'image'] + else: + options = ['none', 'voxel', 'branch', 'organelle', 'image'] + for option in options: + self.dropdown.addItem(option) + + if self.dropdown_attr is not None: + self.set_default_dropdowns() + + self.adjacency_maps = None
+ + +
+[docs] + def export_data(self): + dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + export_dir = self.nellie.im_info.graph_dir + if not os.path.exists(export_dir): + os.makedirs(export_dir) + text = f"{dt}-{self.selected_level}-{self.dropdown_attr.currentText()}" + if self.match_t: + current_t = self.viewer.dims.current_step[0] + text += f"_T{current_t}" + timepoints = self.time_col[self.df['t'] == current_t] + else: + timepoints = self.time_col + text += self.nellie.im_info.file_info.filename_no_ext + export_path = os.path.join(export_dir, f"{text}.csv") + df_to_save = pd.DataFrame({'t': timepoints, self.dropdown_attr.currentText(): self.attr_data}) + df_to_save.to_csv(export_path) + + # append time column + show_info(f"Data exported to {export_path}")
+ + +
+[docs] + def save_graph(self): + dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + export_dir = self.nellie.im_info.graph_dir + if not os.path.exists(export_dir): + os.makedirs(export_dir) + text = f"{dt}-{self.selected_level}_{self.dropdown_attr.currentText()}" + if self.match_t: + text += f"_T{self.viewer.dims.current_step[0]}" + text += self.nellie.im_info.file_info.filename_no_ext + export_path = os.path.join(export_dir, f"{text}.png") + self.canvas.figure.savefig(export_path, dpi=300) + show_info(f"Graph saved to {export_path}")
+ + +
+[docs] + def on_hist_change(self, event): + self.plot_data(self.dropdown_attr.currentText())
+ + +
+[docs] + def get_index(self, layer, event): + # get the coordinates of where the mouse is hovering + pos = self.viewer.cursor.position + matched_row = None + if self.nellie.im_info.no_z: + t, y, x = int(np.round(pos[0])), int(np.round(pos[1])), int(np.round(pos[2])) + else: + # todo, gonna be more complicated than this I think + t, z, y, x = int(np.round(pos[0])), int(np.round(pos[1])), int(np.round(pos[2])), int(np.round(pos[3])) + # find corresponding coord in self.label_coords + if self.nellie.im_info.no_z: + t_coords = self.label_coords[t] + match = np.where((t_coords[:, 0] == y) & (t_coords[:, 1] == x)) + if len(match[0]) == 0: + return + else: + matched_row = match[0][0] + # show_info(f"Matched csv index: {matched_idx}, at T,Y,X: {t, y, x}") + else: + coord = self.label_coords[t][z] + if matched_row is None: + return + # get the value of self.voxel_df_idxs[self.voxel_time_col == t] at the matched_idx row + voxel_idx = self.voxel_df[self.voxel_df.columns[0]][self.voxel_df['t'] == t].iloc[matched_row] + node_row = np.where(self.adjacency_maps['v_n'][t][matched_row])[0] + node_idx = self.node_df[self.node_df.columns[0]][self.node_df['t'] == t].iloc[node_row].values + branch_row = np.where(self.adjacency_maps['v_b'][t][matched_row])[0][0] + branch_idx = self.branch_df[self.branch_df.columns[0]][self.branch_df['t'] == t].iloc[branch_row] + organelle_row = np.where(self.adjacency_maps['v_o'][t][matched_row])[0][0] + organelle_idx = self.organelle_df[self.organelle_df.columns[0]][self.organelle_df['t'] == t].iloc[organelle_row] + image_row = np.where(self.adjacency_maps['v_i'][t][matched_row])[0][0] + image_idx = self.image_df[self.image_df.columns[0]][self.image_df['t'] == t].iloc[image_row] + + self.click_match_table = QTableWidget() + self.click_match_table.setRowCount(1) + items = [f"{voxel_idx}", f"{node_idx}", f"{branch_idx}", f"{organelle_idx}", f"{image_idx}"] + self.click_match_table.setColumnCount(len(items)) + if os.path.exists(self.nellie.im_info.pipeline_paths['features_nodes']): + self.click_match_table.setHorizontalHeaderLabels(["Voxel", "Nodes", "Branch", "Organelle", "Image"]) + else: + self.click_match_table.setHorizontalHeaderLabels(["Voxel", "Branch", "Organelle", "Image"]) + for i, item in enumerate(items): + self.click_match_table.setItem(0, i, QTableWidgetItem(item)) + self.layout.addWidget(self.click_match_table, self.layout_anchors['table'][0], self.layout_anchors['table'][1], 1, 4) + self.click_match_table.setVerticalHeaderLabels([f"{t, y, x}\nCSV row"])
+ + +
+[docs] + def overlay(self): + if self.label_mask is None: + label_mask = self.nellie.im_info.get_memmap(self.nellie.im_info.pipeline_paths['im_instance_label']) + self.label_mask = (label_mask > 0).astype(float) + for t in range(self.nellie.im_info.shape[0]): + self.label_coords.append(np.argwhere(self.label_mask[t])) + self.label_mask[t] *= np.nan + + if self.adjacency_maps is None: + pkl_path = self.nellie.im_info.pipeline_paths['adjacency_maps'] + # load pkl file + with open(pkl_path, 'rb') as f: + adjacency_slices = pickle.load(f) + self.adjacency_maps = {'n_v': [], 'b_v': [], 'o_v': []} + for t in range(len(adjacency_slices['v_n'])): + adjacency_slice = adjacency_slices['v_n'][t] + # num_nodes = np.unique(adjacency_slice[:, 1]).shape[0] + max_node = np.max(adjacency_slice[:, 1]) + 1 + min_node = np.min(adjacency_slice[:, 1]) + if len(self.label_coords[t]) == 0: + continue + # adjacency_matrix = np.zeros((len(self.label_coords[t]), num_nodes), dtype=bool) + adjacency_matrix = np.zeros((len(self.label_coords[t]), max_node), dtype=bool) + adjacency_matrix[adjacency_slice[:, 0], adjacency_slice[:, 1]-min_node] = 1 + self.adjacency_maps['n_v'].append(adjacency_matrix.T) + for t in range(len(adjacency_slices['v_b'])): + adjacency_slice = adjacency_slices['v_b'][t] + max_branch = np.max(adjacency_slice[:, 1]) + 1 + min_branch = np.min(adjacency_slice[:, 1]) + if len(self.label_coords[t]) == 0: + continue + adjacency_matrix = np.zeros((len(self.label_coords[t]), max_branch), dtype=bool) + adjacency_matrix[adjacency_slice[:, 0], adjacency_slice[:, 1]-min_branch] = 1 + self.adjacency_maps['b_v'].append(adjacency_matrix.T) + for t in range(len(adjacency_slices['v_o'])): + # organelles are indexed consecutively over the whole timelapse rather than per timepoint, so different + # way to construct the matrices + adjacency_slice = adjacency_slices['v_o'][t] + num_organelles = np.unique(adjacency_slice[:, 1]).shape[0] + min_organelle = np.min(adjacency_slice[:, 1]) + if len(self.label_coords[t]) == 0: + continue + # adjacency_matrix = np.zeros((np.max(adjacency_slice[:, 0])+1, np.max(adjacency_slice[:, 1])+1), dtype=bool) + adjacency_matrix = np.zeros((len(self.label_coords[t]), num_organelles), dtype=bool) + adjacency_matrix[adjacency_slice[:, 0], adjacency_slice[:, 1]-min_organelle] = 1 + self.adjacency_maps['o_v'].append(adjacency_matrix.T) + # adjacency_slice = adjacency_slices['v_b'][t] + # adjacency_matrix = np.zeros((adjacency_slice.shape[0], np.max(adjacency_slice[:, 1])+1)) + # adjacency_matrix[adjacency_slice[:, 0], adjacency_slice[:, 1]] = 1 + + if self.attr_data is None: + return + + for t in range(self.nellie.im_info.shape[0]): + t_attr_data = self.all_attr_data[self.time_col == t].astype(float) + if len(t_attr_data) == 0: + continue + if self.selected_level == 'voxel': + self.label_mask[t][tuple(self.label_coords[t].T)] = t_attr_data + continue + elif self.selected_level == 'node' and len(self.adjacency_maps['n_v']) > 0: + adjacency_mask = np.array(self.adjacency_maps['n_v'][t]) + elif self.selected_level == 'branch': + adjacency_mask = np.array(self.adjacency_maps['b_v'][t]) + elif self.selected_level == 'organelle': + adjacency_mask = np.array(self.adjacency_maps['o_v'][t]) + # elif self.selected_level == 'image': + # adjacency_mask = np.array(self.adjacency_maps['i_v'][t]) + else: + return + reshaped_t_attr = t_attr_data.values.reshape(-1, 1) + # adjacency_mask = np.array(self.adjacency_maps['n_v'][t]) + if adjacency_mask.shape[0] == 0: + continue + attributed_voxels = adjacency_mask * reshaped_t_attr + attributed_voxels[~adjacency_mask] = np.nan + voxel_attributes = np.nanmean(attributed_voxels, axis=0) + # if t > len(self.label_coords): + # continue + # if self.selected_level == 'branch': + # self.label_mask[t][tuple(self.skel_label_coords[t].T)] = voxel_attributes + # else: + self.label_mask[t][tuple(self.label_coords[t].T)] = voxel_attributes + + layer_name = f'{self.selected_level} {self.dropdown_attr.currentText()}' + if 'reassigned' not in self.dropdown_attr.currentText(): + # label_mask_layer = self.viewer.add_image(self.label_mask, name=layer_name, opacity=1, + # colormap='turbo', scale=self.scale) + perc98 = np.nanpercentile(self.attr_data, 98) + # no nans, no infs + real_vals = self.attr_data[~np.isnan(self.attr_data)] + real_vals = real_vals[~np.isinf(real_vals)] + min_val = np.min(real_vals) - (np.abs(np.min(real_vals)) * 0.01) + if np.isnan(min_val): + if len(real_vals) == 0: + min_val = 0 + else: + min_val = np.nanmin(real_vals) + if min_val == perc98: + perc98 = min_val + (np.abs(min_val) * 0.01) + if np.isnan(perc98): + if len(real_vals) == 0: + perc98 = 1 + else: + perc98 = np.nanmax(real_vals) + contrast_limits = [min_val, perc98] + # label_mask_layer.contrast_limits = contrast_limits + # label_mask_layer.name = layer_name + if not self.nellie.im_info.no_z: + # if the layer isn't in 3D view, make it 3d view + self.viewer.dims.ndisplay = 3 + # label_mask_layer.interpolation3d = 'nearest' + # label_mask_layer.refresh() + # self.label_mask_layer.mouse_drag_callbacks.append(self.get_index) + if 'reassigned' in self.dropdown_attr.currentText(): + # make the label_mask_layer a label layer + self.viewer.add_labels(self.label_mask.copy().astype('uint64'), scale=self.scale, name=layer_name) + else: + if not self.nellie.im_info.no_z: + self.viewer.add_image(self.label_mask.copy(), name=layer_name, opacity=1, + colormap='turbo', scale=self.scale, contrast_limits=contrast_limits, + interpolation3d='nearest') + else: + self.viewer.add_image(self.label_mask.copy(), name=layer_name, opacity=1, + colormap='turbo', scale=self.scale, contrast_limits=contrast_limits, + interpolation2d='nearest') + + self.match_t_toggle.setEnabled(True) + self.viewer.reset_view()
+ + +
+[docs] + def on_t_change(self, event): + if self.match_t: + self.on_attr_selected(self.dropdown_attr.currentIndex())
+ + +
+[docs] + def toggle_match_t(self, state): + if state == 2: + self.match_t = True + else: + self.match_t = False + self.on_attr_selected(self.dropdown_attr.currentIndex())
+ + +
+[docs] + def toggle_mean_med(self, state): + if state == 2: + self.is_median = True + else: + self.is_median = False + self.on_attr_selected(self.dropdown_attr.currentIndex())
+ + +
+[docs] + def get_csvs(self): + self.voxel_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_voxels']) + if os.path.exists(self.nellie.im_info.pipeline_paths['features_nodes']): + self.node_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_nodes']) + self.branch_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_branches']) + self.organelle_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_organelles']) + self.image_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_image'])
+ + + # self.voxel_time_col = voxel_df['t'] + # self.voxel_df_idxs = voxel_df[voxel_df.columns[0]] + +
+[docs] + def on_level_selected(self, index): + # This method is called whenever a radio button is selected + # 'button' parameter is the clicked radio button + self.selected_level = self.dropdown.itemText(index) + self.overlay_button.setEnabled(True) + if self.selected_level == 'voxel': + if self.voxel_df is None: + self.voxel_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_voxels']) + self.df = self.voxel_df + elif self.selected_level == 'node': + if self.node_df is None: + if os.path.exists(self.nellie.im_info.pipeline_paths['features_nodes']): + self.node_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_nodes']) + self.df = self.node_df + elif self.selected_level == 'branch': + if self.branch_df is None: + self.branch_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_branches']) + self.df = self.branch_df + elif self.selected_level == 'organelle': + if self.organelle_df is None: + self.organelle_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_organelles']) + self.df = self.organelle_df + elif self.selected_level == 'image': + # turn off overlay button + self.overlay_button.setEnabled(False) + if self.image_df is None: + self.image_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_image']) + self.df = self.image_df + else: + return + + self.dropdown_attr.clear() + # add a None option + self.dropdown_attr.addItem("None") + for col in self.df.columns[::-1]: + # if "raw" not in col: + # continue + # remove "_raw" from the column name + # col = col[:-4] + self.dropdown_attr.addItem(col)
+ + +
+[docs] + def on_attr_selected(self, index): + self.hist_reset = True + # if there are no items in dropdown_attr, return + if self.dropdown_attr.count() == 0: + return + + selected_attr = self.dropdown_attr.itemText(index) + if selected_attr == '': + return + if selected_attr == "None": + # clear the canvas + self.canvas.figure.clear() + self.canvas.draw() + return + + self.all_attr_data = self.df[selected_attr] + self.time_col = self.df['t'] + if self.match_t: + t = self.viewer.dims.current_step[0] + self.attr_data = self.df[self.df['t'] == t][selected_attr] + else: + self.attr_data = self.df[selected_attr] + self.get_stats() + self.plot_data(selected_attr)
+ + +
+[docs] + def get_stats(self): + if self.attr_data is None: + return + if not self.log_scale: + data = self.attr_data + else: + data = np.log10(self.attr_data) + # convert non real numbers to nan + data = data.replace([np.inf, -np.inf], np.nan) + # only real data + data = data.dropna() + self.data_to_plot = data + + # todo only enable when mean is selected + if not self.is_median: + self.mean = np.nanmean(data) + self.std = np.nanstd(data) + + # todo only enable when median is selected + if self.is_median: + self.median = np.nanmedian(data) + self.perc75 = np.nanpercentile(data, 75) + self.perc25 = np.nanpercentile(data, 25) + self.iqr = self.perc75 - self.perc25
+ + +
+[docs] + def draw_stats(self): + if self.attr_data is None: + return + # draw lines for mean, median, std, percentiles on the canvas + ax = self.canvas.figure.get_axes()[0] + if self.is_median: + ax.axvline(self.perc25, color='r', linestyle='--', label='25th percentile') + ax.axvline(self.median, color='m', linestyle='-', label='Median') + ax.axvline(self.perc75, color='r', linestyle='--', label='75th percentile') + else: + ax.axvline(self.mean - self.std, color='b', linestyle='--', label='Mean - Std') + ax.axvline(self.mean, color='c', linestyle='-', label='Mean') + ax.axvline(self.mean + self.std, color='b', linestyle='--', label='Mean + Std') + ax.legend() + self.canvas.draw()
+ + +
+[docs] + def plot_data(self, title): + self.canvas.figure.clear() + ax = self.canvas.figure.add_subplot(111) + self.data_to_plot = self.data_to_plot.replace([np.inf, -np.inf], np.nan) + try: + if self.hist_reset: + nbins = int(len(self.attr_data) ** 0.5) # pretty nbins + ax.hist(self.data_to_plot, bins=nbins) + hist_min = ax.get_xlim()[0] + hist_max = ax.get_xlim()[1] + else: + nbins = self.num_bins.value() + hist_min = self.hist_min.value() + hist_max = self.hist_max.value() + ax.hist(self.data_to_plot, bins=nbins, range=(hist_min, hist_max)) + except ValueError: + nbins = 10 + hist_min = 0 + hist_max = 1 + if self.is_median: + full_title = f"{title}\n\nQuartiles: {self.perc25:.4f}, {self.median:.4f}, {self.perc75:.4f}" + else: + full_title = f"{title}\n\nMean: {self.mean:.4f}, Std: {self.std:.4f}" + if self.match_t: + full_title += f"\nTimepoint: {self.viewer.dims.current_step[0]}" + else: + full_title += f"\nTimepoint: all (pooled)" + ax.set_title(full_title) + if not self.log_scale: + ax.set_xlabel("Value") + else: + ax.set_xlabel("Value (log10)") + ax.set_ylabel("Frequency") + self.canvas.draw() + self.draw_stats() + + # if self.hist_min is not enabled + if self.hist_reset: + self.hist_min.setEnabled(True) + self.hist_min.setValue(hist_min) + self.hist_min.setRange(hist_min, hist_max) + self.hist_min.setSingleStep((hist_max - hist_min) / 100) + + self.hist_max.setEnabled(True) + self.hist_max.setValue(hist_max) + self.hist_max.setRange(hist_min, hist_max) + self.hist_max.setSingleStep((hist_max - hist_min) / 100) + + self.num_bins.setEnabled(True) + self.num_bins.setValue(nbins) + self.num_bins.setRange(1, len(self.attr_data)) + self.hist_reset = False
+ + +
+[docs] + def on_log_scale(self, state): + self.hist_reset = True + if state == 2: + self.log_scale = True + else: + self.log_scale = False + self.on_attr_selected(self.dropdown_attr.currentIndex())
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_fileselect.html b/docs/_build/html/_modules/nellie_napari/nellie_fileselect.html new file mode 100644 index 0000000..f9f8629 --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_fileselect.html @@ -0,0 +1,592 @@ + + + + + + + nellie_napari.nellie_fileselect — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_fileselect

+import os
+import napari
+from PyQt5.QtWidgets import QSpinBox
+from qtpy.QtWidgets import QWidget, QLabel, QPushButton, QLineEdit, QFileDialog, QVBoxLayout, QHBoxLayout, QGroupBox
+from napari.utils.notifications import show_info
+from tifffile import tifffile
+
+from nellie.im_info.verifier import FileInfo, ImInfo
+
+
+
+[docs] +class NellieFileSelect(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.filepath = None + self.file_info: FileInfo = None + self.im_info = None + + self.batch_fileinfo_list = None + + self.viewer = napari_viewer + self.viewer.title = 'Nellie Napari' + # Add text showing what filepath is selected + self.filepath_text = QLabel(text="No file selected.") + self.filepath_text.setWordWrap(True) + + # Filepath selector button + self.filepath_button = QPushButton(text="Select File") + self.filepath_button.clicked.connect(self.select_filepath) + self.filepath_button.setEnabled(True) + + # Folder selector button + self.folder_button = QPushButton(text="Select Folder") + self.folder_button.clicked.connect(self.select_folder) + self.folder_button.setEnabled(True) + + self.selection_text = QLabel("Selected file:") + self.selection_text.setWordWrap(True) + + self.reset_button = QPushButton(text="Reset") + self.reset_button.clicked.connect(self.nellie.reset) + self.reset_button.setEnabled(True) + + self.file_shape_text = QLabel(text="None") + self.file_shape_text.setWordWrap(True) + + self.current_order_text = QLabel(text="None") + self.current_order_text.setWordWrap(True) + + self.dim_order_button = QLineEdit(self) + self.dim_order_button.setText("None") + self.dim_order_button.setToolTip("Accepted axes: None") + self.dim_order_button.setEnabled(False) + self.dim_order_button.textChanged.connect(self.handle_dim_order_changed) + + self.dim_t_text = 'None' + self.dim_z_text = 'None' + self.dim_xy_text = 'None' + + self.label_t = QLabel("T resolution (s):") + self.dim_t_button = QLineEdit(self) + self.dim_t_button.setText("None") + self.dim_t_button.setEnabled(False) + self.dim_t_button.textChanged.connect(self.handle_t_changed) + + self.label_z = QLabel("Z resolution (um):") + self.dim_z_button = QLineEdit(self) + self.dim_z_button.setText("None") + self.dim_z_button.setEnabled(False) + self.dim_z_button.textChanged.connect(self.handle_z_changed) + + self.label_xy = QLabel("X & Y resolution (um):") + self.dim_xy_button = QLineEdit(self) + self.dim_xy_button.setText("None") + self.dim_xy_button.setEnabled(False) + self.dim_xy_button.textChanged.connect(self.handle_xy_changed) + + self.label_channel = QLabel("Channel:") + self.channel_button = QSpinBox(self) + self.channel_button.setRange(0, 0) + self.channel_button.setValue(0) + self.channel_button.setEnabled(False) + self.channel_button.valueChanged.connect(self.change_channel) + + self.label_time = QLabel("Start frame:") + self.label_time_2 = QLabel("End frame:") + self.start_frame_button = QSpinBox(self) + self.start_frame_button.setRange(0, 0) + self.start_frame_button.setValue(0) + self.start_frame_button.setEnabled(False) + self.start_frame_button.valueChanged.connect(self.change_time) + self.end_frame_button = QSpinBox(self) + self.end_frame_button.setRange(0, 0) + self.end_frame_button.setValue(0) + self.end_frame_button.setEnabled(False) + self.end_frame_button.valueChanged.connect(self.change_time) + self.end_frame_init = False + + self.confirm_button = QPushButton(text="Confirm") + self.confirm_button.clicked.connect(self.on_confirm) + self.confirm_button.setEnabled(False) + + self.preview_button = QPushButton(text="Preview image") + self.preview_button.clicked.connect(self.on_preview) + self.preview_button.setEnabled(False) + + # self.delete_button = QPushButton(text="Delete image") + + self.process_button = QPushButton(text="Process image") + self.process_button.clicked.connect(self.on_process) + self.process_button.setEnabled(False) + + self.init_ui() + +
+[docs] + def init_ui(self): + main_layout = QVBoxLayout() + + # File Selection Group + file_group = QGroupBox("File Selection") + file_layout = QVBoxLayout() + file_button_sublayout = QHBoxLayout() + file_button_sublayout.addWidget(self.filepath_button) + file_button_sublayout.addWidget(self.folder_button) + file_layout.addLayout(file_button_sublayout) + file_sub_layout = QHBoxLayout() + file_sub_layout.addWidget(self.selection_text) + file_sub_layout.addWidget(self.filepath_text) + file_layout.addLayout(file_sub_layout) + file_layout.addWidget(self.reset_button) + file_group.setLayout(file_layout) + + # Axes Info Group + axes_group = QGroupBox("Axes Information") + axes_layout = QVBoxLayout() + sub_layout = QHBoxLayout() + sub_layout.addWidget(QLabel("Dimension order:")) + sub_layout.addWidget(self.dim_order_button) + sub_layout.addWidget(QLabel("Only T, C, Z, Y, and X allowed.")) + axes_layout.addLayout(sub_layout) + for label, button in [ + (QLabel("File shape:"), self.file_shape_text), + (QLabel("Current order:"), self.current_order_text) + ]: + sub_layout = QHBoxLayout() + sub_layout.addWidget(label) + sub_layout.addWidget(button) + axes_layout.addLayout(sub_layout) + axes_group.setLayout(axes_layout) + + # Dimensions Group + dim_group = QGroupBox("Dimension Resolutions") + dim_layout = QVBoxLayout() + for label, button in [ + (self.label_t, self.dim_t_button), + (self.label_z, self.dim_z_button), + (self.label_xy, self.dim_xy_button) + ]: + sub_layout = QHBoxLayout() + sub_layout.addWidget(label) + sub_layout.addWidget(button) + dim_layout.addLayout(sub_layout) + dim_group.setLayout(dim_layout) + + # Slice Settings Group + slice_group = QGroupBox("Slice Settings") + slice_layout = QVBoxLayout() + for label, button in [ + (self.label_time, self.start_frame_button), + (self.label_time_2, self.end_frame_button), + (self.label_channel, self.channel_button) + ]: + sub_layout = QHBoxLayout() + sub_layout.addWidget(label) + sub_layout.addWidget(button) + slice_layout.addLayout(sub_layout) + slice_group.setLayout(slice_layout) + + # Action Buttons Group + action_group = QGroupBox("Actions") + action_layout = QHBoxLayout() + action_layout.addWidget(self.confirm_button) + action_layout.addWidget(self.preview_button) + action_layout.addWidget(self.process_button) + action_group.setLayout(action_layout) + + # Add all groups to main layout + main_layout.addWidget(file_group) + main_layout.addWidget(axes_group) + main_layout.addWidget(dim_group) + main_layout.addWidget(slice_group) + main_layout.addWidget(action_group) + + self.setLayout(main_layout)
+ + +
+[docs] + def select_filepath(self): + self.batch_fileinfo_list = None + filepath, _ = QFileDialog.getOpenFileName(self, "Select file") + self.validate_path(filepath) + if self.filepath is None: + return + self.selection_text.setText("Selected file:") + + self.file_info = FileInfo(self.filepath) + self.initialize_single_file() + filename = os.path.basename(self.filepath) + self.filepath_text.setText(f"{filename}")
+ + +
+[docs] + def select_folder(self): + folderpath = QFileDialog.getExistingDirectory(self, "Select folder") + self.validate_path(folderpath) + if self.filepath is None: + return + self.selection_text.setText("Selected folder:") + + self.initialize_folder() + self.filepath_text.setText(f"{folderpath}")
+ + + +
+[docs] + def validate_path(self, filepath): + if not filepath: + show_info("Invalid selection.") + return None + self.filepath = filepath
+ + +
+[docs] + def initialize_single_file(self): + self.file_info.find_metadata() + self.file_info.load_metadata() + self.file_shape_text.setText(f"{self.file_info.shape}") + + self.dim_order_button.setText(f"{self.file_info.axes}") + self.dim_order_button.setEnabled(True) + self.on_change()
+ + +
+[docs] + def initialize_folder(self): + # get all .tif, .tiff, and .nd2 files in the folder + files = [f for f in os.listdir(self.filepath) if f.endswith('.tif') or f.endswith('.tiff') or f.endswith('.nd2')] + # for each file, create a FileInfo object + self.batch_fileinfo_list = [FileInfo(os.path.join(self.filepath, f)) for f in files] + for file_info in self.batch_fileinfo_list: + file_info.find_metadata() + file_info.load_metadata() + self.file_info = self.batch_fileinfo_list[0] + self.initialize_single_file()
+ + # This assumes all files in the folder have the same metadata (dim order, resolutions, temporal range, channels) + +
+[docs] + def on_change(self): + self.confirm_button.setEnabled(False) + self.check_available_dims() + if len(self.file_info.shape) == 2: + self.dim_order_button.setToolTip("Accepted axes: 'Y', 'X' (e.g. 'YX')") + elif len(self.file_info.shape) == 3: + self.dim_order_button.setToolTip("Accepted axes: ['T' or 'C' or 'Z'], 'Y', 'X' (e.g. 'ZYX')") + elif len(self.file_info.shape) == 4: + self.dim_order_button.setToolTip("Accepted axes: ['T' or 'C' or 'Z']x2, 'Y', 'X' (e.g. 'TZYX')") + elif len(self.file_info.shape) == 5: + self.dim_order_button.setToolTip("Accepted axes: ['T' or 'C' or 'Z']x3, 'Y', 'X' (e.g. 'TZCYX')") + elif len(self.file_info.shape) > 5: + self.dim_order_button.setStyleSheet("background-color: red") + show_info(f"Error: Too many dimensions found ({self.file_info.shape}).") + + if self.file_info.good_axes: + current_order_text = f"({', '.join(self.file_info.axes)})" + self.current_order_text.setText(current_order_text) + self.dim_order_button.setStyleSheet("background-color: green") + else: + self.current_order_text.setText("Invalid") + self.dim_order_button.setStyleSheet("background-color: red") + + if self.file_info.good_dims and self.file_info.good_axes: + self.confirm_button.setEnabled(True) + + # self.delete_button.setEnabled(False) + self.preview_button.setEnabled(False) + self.process_button.setEnabled(False) + # check file_info output path. if it exists, enable the delete and preview button + if os.path.exists(self.file_info.ome_output_path) and self.file_info.good_dims and self.file_info.good_axes: + # self.delete_button.setEnabled(True) + self.preview_button.setEnabled(True) + self.process_button.setEnabled(True)
+ + +
+[docs] + def check_available_dims(self): + def check_dim(dim, dim_button, dim_text): + dim_button.setStyleSheet("background-color: green") + if dim in self.file_info.axes: + dim_button.setEnabled(True) + if dim_text is None or dim_text == 'None': + dim_button.setText(str(self.file_info.dim_res[dim])) + else: + dim_button.setText(dim_text) + if self.file_info.dim_res[dim] is None: + dim_button.setStyleSheet("background-color: red") + if dim_button.text() == 'None' or dim_button.text() is None: + dim_button.setStyleSheet("background-color: red") + else: + dim_button.setEnabled(False) + if dim in self.file_info.dim_res: + dim_button.setText(str(self.file_info.dim_res[dim])) + else: + dim_button.setText("None") + + check_dim('T', self.dim_t_button, self.dim_t_text) + check_dim('Z', self.dim_z_button, self.dim_z_text) + check_dim('X', self.dim_xy_button, self.dim_xy_text) + check_dim('Y', self.dim_xy_button, self.dim_xy_text) + + self.channel_button.setEnabled(False) + if 'C' in self.file_info.axes: + self.channel_button.setEnabled(True) + self.channel_button.setRange(0, self.file_info.shape[self.file_info.axes.index('C')]-1) + + self.start_frame_button.setEnabled(False) + self.end_frame_button.setEnabled(False) + if 'T' in self.file_info.axes: + self.start_frame_button.setEnabled(True) + self.end_frame_button.setEnabled(True) + current_start_frame = self.start_frame_button.value() + current_end_frame = self.end_frame_button.value() + max_t = self.file_info.shape[self.file_info.axes.index('T')] - 1 + self.start_frame_button.setRange(0, current_end_frame) + self.end_frame_button.setRange(current_start_frame, max_t) + if not self.end_frame_init: + self.start_frame_button.setValue(0) + self.end_frame_button.setValue(max_t) + self.end_frame_init = True
+ + +
+[docs] + def handle_dim_order_changed(self, text): + if self.batch_fileinfo_list is None: + self.file_info.change_axes(text) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_axes(text) + + self.end_frame_init = False + self.on_change()
+ + +
+[docs] + def handle_t_changed(self, text): + self.dim_t_text = text + try: + value = float(self.dim_t_text) + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('T', value) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('T', value) + except ValueError: + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('T', None) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('T', None) + self.on_change()
+ + +
+[docs] + def handle_z_changed(self, text): + self.dim_z_text = text + try: + value = float(self.dim_z_text) + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('Z', value) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('Z', value) + except ValueError: + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('Z', None) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('Z', None) + self.on_change()
+ + +
+[docs] + def handle_xy_changed(self, text): + self.dim_xy_text = text + try: + value = float(self.dim_xy_text) + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('X', value) + self.file_info.change_dim_res('Y', value) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('X', value) + file_info.change_dim_res('Y', value) + except ValueError: + if self.batch_fileinfo_list is None: + self.file_info.change_dim_res('X', None) + self.file_info.change_dim_res('Y', None) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_dim_res('X', None) + file_info.change_dim_res('Y', None) + self.on_change()
+ + +
+[docs] + def change_channel(self): + if self.batch_fileinfo_list is None: + self.file_info.change_selected_channel(self.channel_button.value()) + else: + for file_info in self.batch_fileinfo_list: + file_info.change_selected_channel(self.channel_button.value()) + self.on_change()
+ + +
+[docs] + def change_time(self): + if self.batch_fileinfo_list is None: + self.file_info.select_temporal_range(self.start_frame_button.value(), self.end_frame_button.value()) + else: + for file_info in self.batch_fileinfo_list: + file_info.select_temporal_range(self.start_frame_button.value(), self.end_frame_button.value()) + self.on_change()
+ + +
+[docs] + def on_confirm(self): + show_info("Saving OME TIFF file.") + if self.batch_fileinfo_list is None: + self.im_info = ImInfo(self.file_info) + else: + self.im_info = [ImInfo(file_info) for file_info in self.batch_fileinfo_list] + self.on_change()
+ + +
+[docs] + def on_process(self): + # switch to process tab + if self.batch_fileinfo_list is None: + self.im_info = ImInfo(self.file_info) + else: + self.im_info = [ImInfo(file_info) for file_info in self.batch_fileinfo_list] + self.on_change() + self.nellie.go_process()
+ + +
+[docs] + def on_preview(self): + im_memmap = tifffile.memmap(self.file_info.ome_output_path) + # num_t = min(2, self.im_info.shape[self.im_info.axes.index('T')]) + if 'Z' in self.file_info.axes: + scale = (self.file_info.dim_res['Z'], self.file_info.dim_res['Y'], self.file_info.dim_res['X']) + self.viewer.dims.ndisplay = 3 + else: + scale = (self.file_info.dim_res['Y'], self.file_info.dim_res['X']) + self.viewer.dims.ndisplay = 2 + self.viewer.add_image(im_memmap, name=self.file_info.filename_no_ext, scale=scale, blending='additive', + interpolation3d='nearest', interpolation2d='nearest') + self.viewer.scale_bar.visible = True + self.viewer.scale_bar.unit = 'um'
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_home.html b/docs/_build/html/_modules/nellie_napari/nellie_home.html new file mode 100644 index 0000000..def48af --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_home.html @@ -0,0 +1,209 @@ + + + + + + + nellie_napari.nellie_home — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_home

+from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget, QPushButton, QMessageBox
+from qtpy.QtGui import QPixmap, QFont
+from qtpy.QtCore import Qt
+import napari
+import os
+import datetime
+import numpy as np
+from napari.utils.notifications import show_info
+import matplotlib.image
+
+
+
+[docs] +class Home(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.viewer = napari_viewer + + self.layout = QVBoxLayout() + self.setLayout(self.layout) + + # Logo + logo_path = os.path.join(os.path.dirname(__file__), 'logo.png') + logo_label = QLabel(self) + pixmap = QPixmap(logo_path) + logo_label.setPixmap( + pixmap.scaled(300, 300, Qt.KeepAspectRatio))#, Qt.SmoothTransformation)) # Adjust size as needed + logo_label.setAlignment(Qt.AlignCenter) + self.layout.addWidget(logo_label) + + # Title + title = QLabel("Nellie") + title.setFont(QFont("Arial", 24, QFont.Bold)) + title.setAlignment(Qt.AlignCenter) + self.layout.addWidget(title) + + # Subtitle + subtitle = QLabel("Automated organelle segmentation, tracking, and hierarchical feature extraction in 2D/3D live-cell microscopy.") + subtitle.setFont(QFont("Arial", 16)) + subtitle.setAlignment(Qt.AlignCenter) + subtitle.setWordWrap(True) + self.layout.addWidget(subtitle) + + # Add a large "Start" button + self.start_button = QPushButton("Start") + self.start_button.setFont(QFont("Arial", 20)) + self.start_button.setFixedWidth(200) + self.start_button.setFixedHeight(100) + # rounded-edges + self.start_button.setStyleSheet("border-radius: 10px;") + # opens the file select tab + self.start_button.clicked.connect(lambda: self.nellie.setCurrentIndex(self.nellie.file_select_tab)) + self.layout.addWidget(self.start_button, alignment=Qt.AlignCenter) + + github_link = QLabel("<a href='https://arxiv.org/abs/2403.13214'>Cite our paper!</a>") + github_link.setOpenExternalLinks(True) + github_link.setAlignment(Qt.AlignCenter) + self.layout.addWidget(github_link) + + # Link to GitHub + github_link = QLabel("<a href='https://github.com/aelefebv/nellie'>Visit Nellie's GitHub Page!</a>") + github_link.setOpenExternalLinks(True) + github_link.setAlignment(Qt.AlignCenter) + self.layout.addWidget(github_link) + + # screenshot button + self.screenshot_button = QPushButton(text="Easy screenshot:\n[Ctrl-Shift-E]") + self.screenshot_button.setStyleSheet("border-radius: 5px;") + self.screenshot_button.clicked.connect(self.screenshot) + self.screenshot_button.setEnabled(True) + self.viewer.bind_key('Ctrl-Shift-E', self.screenshot, overwrite=True) + + self.layout.addWidget(self.screenshot_button, alignment=Qt.AlignCenter) + +
+[docs] + def screenshot(self, event=None): + if self.nellie.im_info is None: + show_info("No file selected, cannot take screenshot") + return + + # easy no prompt screenshot + dt = datetime.datetime.now() # year, month, day, hour, minute, second, millisecond up to 3 digits + dt = dt.strftime("%Y%m%d_%H%M%S%f")[:-3] + + screenshot_folder = self.nellie.im_info.screenshot_dir + if not os.path.exists(screenshot_folder): + os.makedirs(screenshot_folder) + + im_name = f'{dt}-{self.nellie.im_info.file_info.filename_no_ext}.png' + file_path = os.path.join(screenshot_folder, im_name) + + # Take screenshot + screenshot = self.viewer.screenshot(canvas_only=True) + + # Save the screenshot + try: + # save as png to file_path using imsave + screenshot = np.ascontiguousarray(screenshot) + matplotlib.image.imsave(file_path, screenshot, format="png") + show_info(f"Screenshot saved to {file_path}") + except Exception as e: + QMessageBox.warning(None, "Error", f"Failed to save screenshot: {str(e)}") + raise e
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_loader.html b/docs/_build/html/_modules/nellie_napari/nellie_loader.html new file mode 100644 index 0000000..1efca78 --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_loader.html @@ -0,0 +1,208 @@ + + + + + + + nellie_napari.nellie_loader — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_loader

+from napari.utils.notifications import show_info
+from qtpy.QtWidgets import QTabWidget
+
+from nellie_napari import NellieProcessor
+from nellie_napari.nellie_home import Home
+from nellie_napari.nellie_analysis import NellieAnalysis
+from nellie_napari.nellie_fileselect import NellieFileSelect
+from nellie_napari.nellie_settings import Settings
+from nellie_napari.nellie_visualizer import NellieVisualizer
+
+
+
+[docs] +class NellieLoader(QTabWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', parent=None): + super().__init__(parent) + self.home = Home(napari_viewer, self) + self.file_select = NellieFileSelect(napari_viewer, self) + self.processor = NellieProcessor(napari_viewer, self) + self.visualizer = NellieVisualizer(napari_viewer, self) + self.analyzer = NellieAnalysis(napari_viewer, self) + self.settings = Settings(napari_viewer, self) + + self.home_tab = None + self.file_select_tab = None + self.processor_tab = None + self.visualizer_tab = None + self.analysis_tab = None + self.settings_tab = None + + self.add_tabs() + self.currentChanged.connect(self.on_tab_change) # Connect the signal to the slot + + self.im_info = None + self.im_info_list = None + +
+[docs] + def add_tabs(self): + self.home_tab = self.addTab(self.home, "Home") + self.file_select_tab = self.addTab(self.file_select, "File validation") + self.processor_tab = self.addTab(self.processor, "Process") + self.visualizer_tab = self.addTab(self.visualizer, "Visualize") + self.analysis_tab = self.addTab(self.analyzer, "Analyze") + self.settings_tab = self.addTab(self.settings, "Settings") + + self.setTabEnabled(self.processor_tab, False) + self.setTabEnabled(self.visualizer_tab, False) + self.setTabEnabled(self.analysis_tab, False)
+ + +
+[docs] + def reset(self): + self.setCurrentIndex(self.home_tab) + + # needs to be in reverse order + self.removeTab(self.settings_tab) + self.removeTab(self.analysis_tab) + self.removeTab(self.visualizer_tab) + self.removeTab(self.processor_tab) + self.removeTab(self.file_select_tab) + + self.file_select = NellieFileSelect(self.file_select.viewer, self) + self.processor = NellieProcessor(self.processor.viewer, self) + self.visualizer = NellieVisualizer(self.visualizer.viewer, self) + self.analyzer = NellieAnalysis(self.analyzer.viewer, self) + self.settings = Settings(self.settings.viewer, self) + + self.add_tabs() + + self.im_info = None + self.im_info_list = None
+ + +
+[docs] + def on_tab_change(self, index): + if index == self.analysis_tab: # Check if the Analyze tab is selected + if not self.analyzer.initialized: + show_info("Initializing analysis tab") + self.analyzer.post_init() + elif index == self.visualizer_tab: + if not self.visualizer.initialized: + show_info("Initializing visualizer tab") + self.visualizer.post_init() + self.settings.post_init()
+ + +
+[docs] + def go_process(self): + if self.file_select.batch_fileinfo_list is None: + self.im_info = self.file_select.im_info + else: + self.im_info = self.file_select.im_info[0] + self.im_info_list = self.file_select.im_info + print(self.im_info_list) + self.setTabEnabled(self.processor_tab, True) + self.setTabEnabled(self.visualizer_tab, True) + self.processor.post_init() + self.visualizer.post_init() + self.on_tab_change(self.processor_tab) + self.setCurrentIndex(self.processor_tab)
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_processor.html b/docs/_build/html/_modules/nellie_napari/nellie_processor.html new file mode 100644 index 0000000..f184668 --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_processor.html @@ -0,0 +1,525 @@ + + + + + + + nellie_napari.nellie_processor — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_processor

+import os
+import subprocess
+import time
+
+from napari.utils.notifications import show_info
+from qtpy.QtWidgets import QWidget, QPushButton, QVBoxLayout, QGroupBox, QLabel, QHBoxLayout
+from qtpy.QtGui import QFont, QIcon
+from qtpy.QtCore import Qt, QTimer
+
+from nellie.feature_extraction.hierarchical import Hierarchy
+from nellie.segmentation.filtering import Filter
+from nellie.segmentation.labelling import Label
+from nellie.segmentation.mocap_marking import Markers
+from nellie.segmentation.networking import Network
+from nellie.tracking.hu_tracking import HuMomentTracking
+from nellie.tracking.voxel_reassignment import VoxelReassigner
+from napari.qt.threading import thread_worker
+
+
+
+[docs] +class NellieProcessor(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.viewer = napari_viewer + + self.im_info_list = None + self.current_im_info = None + + self.status_label = QLabel("Awaiting your input") + self.status = None + self.num_ellipses = 1 + + self.status_timer = QTimer() + self.status_timer.timeout.connect(self.update_status) + + self.open_dir_button = QPushButton(text="Open output directory") + self.open_dir_button.clicked.connect(self.open_directory) + self.open_dir_button.setEnabled(False) + + # Run im button + self.run_button = QPushButton(text="Run Nellie") + self.run_button.clicked.connect(self.run_nellie) + self.run_button.setEnabled(True) + self.run_button.setFixedWidth(200) + self.run_button.setFixedHeight(100) + self.run_button.setStyleSheet("border-radius: 10px;") + self.run_button.setFont(QFont("Arial", 20)) + + # Preprocess im button + self.preprocess_button = QPushButton(text="Run preprocessing") + self.preprocess_button.clicked.connect(self.run_preprocessing) + self.preprocess_button.setEnabled(False) + + # Segment im button + self.segment_button = QPushButton(text="Run segmentation") + self.segment_button.clicked.connect(self.run_segmentation) + self.segment_button.setEnabled(False) + + # Run mocap button + self.mocap_button = QPushButton(text="Run mocap marking") + self.mocap_button.clicked.connect(self.run_mocap) + self.mocap_button.setEnabled(False) + + # Run tracking button + self.track_button = QPushButton(text="Run tracking") + self.track_button.clicked.connect(self.run_tracking) + self.track_button.setEnabled(False) + + # Run reassign button + self.reassign_button = QPushButton(text="Run voxel reassignment") + self.reassign_button.clicked.connect(self.run_reassign) + self.reassign_button.setEnabled(False) + + # Run feature extraction button + self.feature_export_button = QPushButton(text="Run feature export") + self.feature_export_button.clicked.connect(self.run_feature_export) + self.feature_export_button.setEnabled(False) + + self.set_ui() + + self.initialized = False + self.pipeline = False + +
+[docs] + def set_ui(self): + main_layout = QVBoxLayout() + + # Status group + status_group = QGroupBox("Status") + status_layout = QHBoxLayout() # Changed to QHBoxLayout + status_layout.addWidget(self.status_label, alignment=Qt.AlignLeft) + status_layout.addWidget(self.open_dir_button, alignment=Qt.AlignCenter) + status_group.setMaximumHeight(100) + status_group.setLayout(status_layout) + + # Run full pipeline + full_pipeline_group = QGroupBox("Run full pipeline") + full_pipeline_layout = QVBoxLayout() + full_pipeline_layout.addWidget(self.run_button, alignment=Qt.AlignCenter) + full_pipeline_group.setLayout(full_pipeline_layout) + + # Run partial pipeline + partial_pipeline_group = QGroupBox("Run individual steps") + partial_pipeline_layout = QVBoxLayout() + partial_pipeline_layout.addWidget(self.preprocess_button) + partial_pipeline_layout.addWidget(self.segment_button) + partial_pipeline_layout.addWidget(self.mocap_button) + partial_pipeline_layout.addWidget(self.track_button) + partial_pipeline_layout.addWidget(self.reassign_button) + partial_pipeline_layout.addWidget(self.feature_export_button) + partial_pipeline_group.setLayout(partial_pipeline_layout) + + main_layout.addWidget(status_group) + main_layout.addWidget(full_pipeline_group) + main_layout.addWidget(partial_pipeline_group) + + self.setLayout(main_layout)
+ + +
+[docs] + def post_init(self): + if self.nellie.im_info_list is None: + self.im_info_list = [self.nellie.im_info] + self.current_im_info = self.nellie.im_info + else: + self.im_info_list = self.nellie.im_info_list + self.current_im_info = self.nellie.im_info_list[0] + self.check_file_existence() + self.initialized = True + self.open_dir_button.setEnabled(os.path.exists(self.current_im_info.file_info.output_dir))
+ + +
+[docs] + def check_file_existence(self): + self.nellie.visualizer.check_file_existence() + + # set all other buttons to disabled first + self.run_button.setEnabled(False) + self.preprocess_button.setEnabled(False) + self.segment_button.setEnabled(False) + self.mocap_button.setEnabled(False) + self.track_button.setEnabled(False) + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(False) + + analysis_path = self.current_im_info.pipeline_paths['features_organelles'] + if os.path.exists(analysis_path): + self.nellie.setTabEnabled(self.nellie.analysis_tab, True) + else: + self.nellie.setTabEnabled(self.nellie.analysis_tab, False) + + self.open_dir_button.setEnabled(os.path.exists(self.current_im_info.file_info.output_dir)) + + if os.path.exists(self.current_im_info.im_path): + self.run_button.setEnabled(True) + self.preprocess_button.setEnabled(True) + else: + return + + frangi_path = self.current_im_info.pipeline_paths['im_preprocessed'] + if os.path.exists(frangi_path): + self.segment_button.setEnabled(True) + else: + self.segment_button.setEnabled(False) + self.mocap_button.setEnabled(False) + self.track_button.setEnabled(False) + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(False) + return + + im_instance_label_path = self.current_im_info.pipeline_paths['im_instance_label'] + im_skel_relabelled_path = self.current_im_info.pipeline_paths['im_skel_relabelled'] + if os.path.exists(im_instance_label_path) and os.path.exists(im_skel_relabelled_path): + self.mocap_button.setEnabled(True) + else: + self.mocap_button.setEnabled(False) + self.track_button.setEnabled(False) + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(False) + return + + im_marker_path = self.current_im_info.pipeline_paths['im_marker'] + if os.path.exists(im_marker_path): + self.track_button.setEnabled(True) + else: + self.track_button.setEnabled(False) + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(False) + return + + track_path = self.current_im_info.pipeline_paths['flow_vector_array'] + if os.path.exists(track_path): + self.reassign_button.setEnabled(True) + self.feature_export_button.setEnabled(True) + else: + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(True) + # if im_info's 'T' axis has more than 1 timepoint, disable the feature export button + if self.current_im_info.shape[0] > 1: + self.feature_export_button.setEnabled(False) + return
+ + + + + @thread_worker + def _run_preprocessing(self): + self.status = "preprocessing" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Preprocessing file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + preprocessing = Filter(im_info=self.current_im_info, + remove_edges=self.nellie.settings.remove_edges_checkbox.isChecked(), + viewer=self.viewer) + preprocessing.run() + +
+[docs] + def run_preprocessing(self): + worker = self._run_preprocessing() + worker.started.connect(self.turn_off_buttons) + if self.pipeline: + worker.finished.connect(self.run_segmentation) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.finished.connect(self.check_file_existence) + worker.start()
+ + + @thread_worker + def _run_segmentation(self): + self.status = "segmentation" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Segmentation file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + segmenting = Label(im_info=self.current_im_info, viewer=self.viewer) + segmenting.run() + networking = Network(im_info=self.current_im_info, viewer=self.viewer) + networking.run() + +
+[docs] + def run_segmentation(self): + worker = self._run_segmentation() + worker.started.connect(self.turn_off_buttons) + if self.pipeline: + worker.finished.connect(self.run_mocap) + worker.finished.connect(self.check_file_existence) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.start()
+ + + @thread_worker + def _run_mocap(self): + self.status = "mocap marking" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Mocap Marking file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + mocap_marking = Markers(im_info=self.current_im_info, viewer=self.viewer) + mocap_marking.run() + +
+[docs] + def run_mocap(self): + worker = self._run_mocap() + worker.started.connect(self.turn_off_buttons) + if self.pipeline: + worker.finished.connect(self.run_tracking) + worker.finished.connect(self.check_file_existence) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.start()
+ + + + @thread_worker + def _run_tracking(self): + self.status = "tracking" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Tracking file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + hu_tracking = HuMomentTracking(im_info=self.current_im_info, viewer=self.viewer) + hu_tracking.run() + +
+[docs] + def run_tracking(self): + worker = self._run_tracking() + worker.started.connect(self.turn_off_buttons) + if self.pipeline: + if self.nellie.settings.voxel_reassign.isChecked(): + worker.finished.connect(self.run_reassign) + else: + worker.finished.connect(self.run_feature_export) + worker.finished.connect(self.check_file_existence) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.start()
+ + + @thread_worker + def _run_reassign(self): + self.status = "voxel reassignment" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Voxel Reassignment file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + vox_reassign = VoxelReassigner(im_info=self.current_im_info, viewer=self.viewer) + vox_reassign.run() + +
+[docs] + def run_reassign(self): + worker = self._run_reassign() + worker.started.connect(self.turn_off_buttons) + if self.pipeline: + worker.finished.connect(self.run_feature_export) + worker.finished.connect(self.check_file_existence) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.start()
+ + + + @thread_worker + def _run_feature_export(self): + self.status = "feature export" + for im_num, im_info in enumerate(self.im_info_list): + show_info(f"Nellie is running: Feature export file {im_num + 1}/{len(self.im_info_list)}") + self.current_im_info = im_info + hierarchy = Hierarchy(im_info=self.current_im_info, + skip_nodes=not bool(self.nellie.settings.analyze_node_level.isChecked()), + viewer=self.viewer) + hierarchy.run() + if self.nellie.settings.remove_intermediates_checkbox.isChecked(): + try: + self.current_im_info.remove_intermediates() + except Exception as e: + show_info(f"Error removing intermediates: {e}") + if self.nellie.analyzer.initialized: + self.nellie.analyzer.rewrite_dropdown() + +
+[docs] + def run_feature_export(self): + worker = self._run_feature_export() + worker.started.connect(self.turn_off_buttons) + worker.finished.connect(self.check_file_existence) + worker.finished.connect(self.turn_off_pipeline) + worker.started.connect(self.set_status) + worker.finished.connect(self.reset_status) + worker.start()
+ + +
+[docs] + def turn_off_pipeline(self): + self.pipeline = False
+ + +
+[docs] + def run_nellie(self): + self.pipeline = True + self.run_preprocessing()
+ + +
+[docs] + def set_status(self): + self.running = True + self.status_timer.start(500) # Update every 250 ms
+ + +
+[docs] + def update_status(self): + if self.running: + self.status_label.setText(f"Running {self.status}{'.' * (self.num_ellipses % 4)}") + self.num_ellipses += 1 + else: + self.status_timer.stop()
+ + +
+[docs] + def reset_status(self): + self.running = False + self.status_label.setText("Awaiting your input") + self.num_ellipses = 1 + self.status_timer.stop()
+ + +
+[docs] + def turn_off_buttons(self): + self.run_button.setEnabled(False) + self.preprocess_button.setEnabled(False) + self.segment_button.setEnabled(False) + self.mocap_button.setEnabled(False) + self.track_button.setEnabled(False) + self.reassign_button.setEnabled(False) + self.feature_export_button.setEnabled(False)
+ + +
+[docs] + def open_directory(self): + directory = self.current_im_info.file_info.output_dir + if os.path.exists(directory): + if os.name == 'nt': # For Windows + os.startfile(directory) + elif os.name == 'posix': # For macOS and Linux + subprocess.call(['open', directory]) + else: + show_info("Output directory does not exist.")
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_settings.html b/docs/_build/html/_modules/nellie_napari/nellie_settings.html new file mode 100644 index 0000000..8d08cc0 --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_settings.html @@ -0,0 +1,192 @@ + + + + + + + nellie_napari.nellie_settings — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_settings

+from qtpy.QtWidgets import QWidget, QCheckBox, QSpinBox, QLabel, QVBoxLayout, QGroupBox, QHBoxLayout
+import napari
+
+
+
+[docs] +class Settings(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.viewer = napari_viewer + + # Checkbox for 'Remove edges' + self.remove_edges_checkbox = QCheckBox("Remove image edges") + self.remove_edges_checkbox.setChecked(False) + self.remove_edges_checkbox.setEnabled(True) + self.remove_edges_checkbox.setToolTip( + "Originally for Snouty deskewed images. If you see weird image edge artifacts, enable this.") + + self.remove_intermediates_checkbox = QCheckBox("Remove intermediate files") + self.remove_intermediates_checkbox.setChecked(False) + self.remove_intermediates_checkbox.setEnabled(True) + self.remove_intermediates_checkbox.setToolTip( + "Remove intermediate files after processing. This means only csv files will be saved.") + + self.voxel_reassign = QCheckBox("Auto-run voxel reassignment") + self.voxel_reassign.setChecked(False) + self.voxel_reassign.setEnabled(True) + + # Analyze node level + self.analyze_node_level = QCheckBox("Analyze node level (slow)") + self.analyze_node_level.setChecked(False) + self.analyze_node_level.setEnabled(True) + + # Track all frames + self.track_all_frames = QCheckBox("Visualize all frames' voxel tracks") + self.track_all_frames.setChecked(True) + self.track_all_frames.setEnabled(True) + + # Label above the spinner box + self.skip_vox_label = QLabel("Visualize tracks for every N voxel. N=") + + self.skip_vox = QSpinBox() + self.skip_vox.setRange(1, 10000) + self.skip_vox.setValue(5) + self.skip_vox.setEnabled(False) + + self.set_ui() + + self.initialized = False + +
+[docs] + def post_init(self): + self.initialized = True
+ + +
+[docs] + def set_ui(self): + main_layout = QVBoxLayout() + + # Processor settings + processor_group = QGroupBox("Processor settings") + processor_layout = QVBoxLayout() + subprocessor_layout1 = QHBoxLayout() + subprocessor_layout1.addWidget(self.remove_intermediates_checkbox) + subprocessor_layout1.addWidget(self.remove_edges_checkbox) + subprocessor_layout2 = QHBoxLayout() + subprocessor_layout2.addWidget(self.analyze_node_level) + subprocessor_layout2.addWidget(self.voxel_reassign) + processor_layout.addLayout(subprocessor_layout1) + processor_layout.addLayout(subprocessor_layout2) + processor_group.setLayout(processor_layout) + + # Tracking settings + tracking_group = QGroupBox("Track visualization settings") + tracking_layout = QVBoxLayout() + tracking_layout.addWidget(self.track_all_frames) + skip_vox_layout = QHBoxLayout() + skip_vox_layout.addWidget(self.skip_vox_label) + skip_vox_layout.addWidget(self.skip_vox) + tracking_layout.addLayout(skip_vox_layout) + tracking_group.setLayout(tracking_layout) + + main_layout.addWidget(processor_group) + main_layout.addWidget(tracking_group) + self.setLayout(main_layout)
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_modules/nellie_napari/nellie_visualizer.html b/docs/_build/html/_modules/nellie_napari/nellie_visualizer.html new file mode 100644 index 0000000..16e984d --- /dev/null +++ b/docs/_build/html/_modules/nellie_napari/nellie_visualizer.html @@ -0,0 +1,462 @@ + + + + + + + nellie_napari.nellie_visualizer — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Source code for nellie_napari.nellie_visualizer

+import os
+
+from qtpy.QtWidgets import QGridLayout, QWidget, QPushButton, QGroupBox, QHBoxLayout, QVBoxLayout
+from tifffile import tifffile
+
+from nellie import logger
+from nellie.tracking.all_tracks_for_label import LabelTracks
+
+
+
+[docs] +class NellieVisualizer(QWidget): + def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + super().__init__(parent) + self.nellie = nellie + self.viewer = napari_viewer + + self.scale = (1, 1, 1) + + self.im_memmap = None + self.raw_layer = None + self.im_branch_label_reassigned_layer = None + self.im_branch_label_reassigned = None + self.im_obj_label_reassigned_layer = None + self.im_obj_label_reassigned = None + self.im_marker_layer = None + self.im_marker = None + self.im_skel_relabelled_layer = None + self.im_instance_label_layer = None + self.frangi_layer = None + self.im_skel_relabelled = None + self.im_instance_label = None + self.im_frangi = None + + # Raw im button + self.raw_button = QPushButton(text="Open raw image") + self.raw_button.clicked.connect(self.open_raw) + self.raw_button.setEnabled(True) + + # Preprocess open button + self.open_preprocess_button = QPushButton(text="Open preprocessed image") + self.open_preprocess_button.clicked.connect(self.open_preprocess_image) + self.open_preprocess_button.setEnabled(False) + + # Segment open button + self.open_segment_button = QPushButton(text="Open segmentation images") + self.open_segment_button.clicked.connect(self.open_segment_image) + self.open_segment_button.setEnabled(False) + + # open im button + self.open_mocap_button = QPushButton(text="Open mocap marker image") + self.open_mocap_button.clicked.connect(self.open_mocap_image) + self.open_mocap_button.setEnabled(False) + + # open reassign button + self.open_reassign_button = QPushButton(text="Open reassigned labels image") + self.open_reassign_button.clicked.connect(self.open_reassign_image) + self.open_reassign_button.setEnabled(False) + + self.track_button = QPushButton(text="Visualize selected label's tracks") + self.track_button.clicked.connect(self.on_track_selected) + self.track_button.setEnabled(False) + + self.track_all_button = QPushButton(text="Visualize all frame labels' tracks") + self.track_all_button.clicked.connect(self.track_all) + self.track_all_button.setEnabled(False) + + self.set_ui() + + # self.layout = QGridLayout() + # self.setLayout(self.layout) + # + # self.layout.addWidget(self.raw_button, 1, 0, 1, 1) + # self.layout.addWidget(self.open_preprocess_button, 2, 0, 1, 1) + # self.layout.addWidget(self.open_segment_button, 3, 0, 1, 1) + # self.layout.addWidget(self.open_mocap_button, 4, 0, 1, 1) + # self.layout.addWidget(self.open_reassign_button, 5, 0, 1, 1) + + self.initialized = False + +
+[docs] + def set_ui(self): + main_layout = QVBoxLayout() + + # visualization group + visualization_group = QGroupBox("Image visualization") + visualization_layout = QVBoxLayout() + visualization_layout.addWidget(self.raw_button) + visualization_layout.addWidget(self.open_preprocess_button) + visualization_layout.addWidget(self.open_segment_button) + visualization_layout.addWidget(self.open_mocap_button) + visualization_layout.addWidget(self.open_reassign_button) + visualization_group.setLayout(visualization_layout) + + # tracking group + tracking_group = QGroupBox("Track visualization") + tracking_layout = QVBoxLayout() + tracking_layout.addWidget(self.track_button) + tracking_layout.addWidget(self.track_all_button) + tracking_group.setLayout(tracking_layout) + + main_layout.addWidget(visualization_group) + main_layout.addWidget(tracking_group) + self.setLayout(main_layout)
+ + +
+[docs] + def post_init(self): + self.set_scale() + self.viewer.scale_bar.visible = True + self.viewer.scale_bar.unit = 'um' + self.initialized = True
+ + +
+[docs] + def check_3d(self): + if not self.nellie.im_info.no_z and self.viewer.dims.ndim != 3: + # ndimensions should be 3 for viewer + self.viewer.dims.ndim = 3 + self.viewer.dims.ndisplay = 3
+ + +
+[docs] + def set_scale(self): + dim_res = self.nellie.im_info.dim_res + if self.nellie.im_info.no_z: + self.scale = (dim_res['Y'], dim_res['X']) + else: + self.scale = (dim_res['Z'], dim_res['Y'], dim_res['X'])
+ + +
+[docs] + def open_preprocess_image(self): + self.im_frangi = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_preprocessed']) + self.check_3d() + self.frangi_layer = self.viewer.add_image(self.im_frangi, name='Pre-processed', colormap='turbo', scale=self.scale) + self.frangi_layer.interpolation = 'nearest' + self.viewer.layers.selection.active = self.frangi_layer
+ + +
+[docs] + def open_segment_image(self): + self.im_instance_label = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_instance_label']) + self.im_skel_relabelled = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_skel_relabelled']) + + self.check_3d() + + self.im_skel_relabelled_layer = self.viewer.add_labels(self.im_skel_relabelled, name='Labels: Branches', + opacity=1, scale=self.scale, visible=False) + self.im_instance_label_layer = self.viewer.add_labels(self.im_instance_label, name='Labels: Organelles', + opacity=1, scale=self.scale) + self.viewer.layers.selection.active = self.im_instance_label_layer
+ + +
+[docs] + def on_track_selected(self): + # if flow_vector_array path does not point to an existing file, return + if not os.path.exists(self.nellie.im_info.pipeline_paths['flow_vector_array']): + return + + # pos = None + # label = 0 + layer = self.viewer.layers.selection.active + t = int(self.viewer.dims.current_step[0]) + # if event.button == 1 and event.is_dragging is False and 'Alt' in event.modifiers: + # scaled_pos = self.viewer.cursor.position + # pos = [scaled_pos[i+1] / self.scale[i] for i in range(len(scaled_pos)-1)] + # pos = (scaled_pos[0], *pos) + # pos = tuple(int(pos_dim) for pos_dim in pos) + # label = layer.selected_label + # if pos is None: + # return + + + if layer == self.im_instance_label_layer: + label_path = self.nellie.im_info.pipeline_paths['im_instance_label'] + elif layer == self.im_skel_relabelled_layer: + label_path = self.nellie.im_info.pipeline_paths['im_skel_relabelled'] + elif layer == self.im_branch_label_reassigned_layer: + label_path = self.nellie.im_info.pipeline_paths['im_branch_label_reassigned'] + elif layer == self.im_obj_label_reassigned_layer: + label_path = self.nellie.im_info.pipeline_paths['im_obj_label_reassigned'] + else: + return + + label = layer.selected_label + if label == 0: + return + + label_tracks = LabelTracks(im_info=self.nellie.im_info, label_im_path=label_path) + label_tracks.initialize() + all_tracks = [] + all_props = {} + max_track_num = 0 + if self.nellie.settings.track_all_frames.isChecked() and ( + layer == self.im_branch_label_reassigned_layer or layer == self.im_obj_label_reassigned_layer): + for frame in range(self.nellie.im_info.shape[0]): + tracks, track_properties = label_tracks.run(label_num=label, start_frame=frame, end_frame=None, + min_track_num=max_track_num, + skip_coords=self.nellie.settings.skip_vox.value()) + all_tracks += tracks + for property in track_properties.keys(): + if property not in all_props.keys(): + all_props[property] = [] + all_props[property] += track_properties[property] + if len(tracks) == 0: + break + max_track_num = max([track[0] for track in tracks])+1 + else: + all_tracks, all_props = label_tracks.run(label_num=label, start_frame=t, end_frame=None, + skip_coords=self.nellie.settings.skip_vox.value()) + if len(all_tracks) == 0: + return + self.viewer.add_tracks(all_tracks, properties=all_props, name=f'Tracks: Label {label}', scale=self.scale) + self.viewer.layers.selection.active = layer + self.check_file_existence()
+ + +
+[docs] + def track_all(self): + layer = self.viewer.layers.selection.active + if layer == self.im_instance_label_layer: + label_path = self.nellie.im_info.pipeline_paths['im_instance_label'] + elif layer == self.im_skel_relabelled_layer: + label_path = self.nellie.im_info.pipeline_paths['im_skel_relabelled'] + elif layer == self.im_branch_label_reassigned_layer: + label_path = self.nellie.im_info.pipeline_paths['im_branch_label_reassigned'] + elif layer == self.im_obj_label_reassigned_layer: + label_path = self.nellie.im_info.pipeline_paths['im_obj_label_reassigned'] + else: + return + + t = int(self.viewer.dims.current_step[0]) + label_tracks = LabelTracks(im_info=self.nellie.im_info, label_im_path=label_path) + label_tracks.initialize() + all_tracks = [] + all_props = {} + max_track_num = 0 + if self.nellie.settings.track_all_frames.isChecked() and ( + layer == self.im_branch_label_reassigned_layer or layer == self.im_obj_label_reassigned_layer): + for frame in range(self.nellie.im_info.shape[0]): + tracks, track_properties = label_tracks.run(start_frame=frame, end_frame=None, + min_track_num=max_track_num, + skip_coords=self.nellie.settings.skip_vox.value()) + all_tracks += tracks + for property in track_properties.keys(): + if property not in all_props.keys(): + all_props[property] = [] + all_props[property] += track_properties[property] + if len(tracks) == 0: + break + max_track_num = max([track[0] for track in tracks]) + 1 + else: + all_tracks, all_props = label_tracks.run(start_frame=t, end_frame=None, + skip_coords=self.nellie.settings.skip_vox.value()) + if len(all_tracks) == 0: + return + self.viewer.add_tracks(all_tracks, properties=all_props, name=f'Tracks: All labels', scale=self.scale) + self.viewer.layers.selection.active = layer + self.check_file_existence()
+ + +
+[docs] + def open_mocap_image(self): + self.check_3d() + + self.im_marker = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_marker']) + self.im_marker_layer = self.viewer.add_image(self.im_marker, name='Mocap Markers', colormap='red', + blending='additive', contrast_limits=[0, 1], scale=self.scale) + self.im_marker_layer.interpolation = 'nearest' + self.viewer.layers.selection.active = self.im_marker_layer
+ + +
+[docs] + def open_reassign_image(self): + self.check_3d() + + self.im_branch_label_reassigned = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_branch_label_reassigned']) + self.im_branch_label_reassigned_layer = self.viewer.add_labels(self.im_branch_label_reassigned, name='Reassigned px: Branches', scale=self.scale, visible=False) + + self.im_obj_label_reassigned = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_obj_label_reassigned']) + self.im_obj_label_reassigned_layer = self.viewer.add_labels(self.im_obj_label_reassigned, name='Reassigned px: Organelles', scale=self.scale) + self.viewer.layers.selection.active = self.im_obj_label_reassigned_layer
+ + +
+[docs] + def open_raw(self): + self.check_3d() + self.im_memmap = tifffile.memmap(self.nellie.im_info.im_path) + self.raw_layer = self.viewer.add_image(self.im_memmap, name='raw', colormap='gray', + blending='additive', scale=self.scale) + # make 3d interpolation to nearest + self.raw_layer.interpolation = 'nearest' + self.viewer.layers.selection.active = self.raw_layer
+ + +
+[docs] + def check_file_existence(self): + # set all other buttons to disabled first + self.raw_button.setEnabled(False) + self.open_preprocess_button.setEnabled(False) + self.open_segment_button.setEnabled(False) + self.open_mocap_button.setEnabled(False) + self.open_reassign_button.setEnabled(False) + self.track_button.setEnabled(False) + self.track_all_button.setEnabled(False) + + if os.path.exists(self.nellie.im_info.im_path): + self.raw_button.setEnabled(True) + else: + self.raw_button.setEnabled(False) + + frangi_path = self.nellie.im_info.pipeline_paths['im_preprocessed'] + if os.path.exists(frangi_path): + self.open_preprocess_button.setEnabled(True) + else: + self.open_preprocess_button.setEnabled(False) + + im_instance_label_path = self.nellie.im_info.pipeline_paths['im_instance_label'] + im_skel_relabelled_path = self.nellie.im_info.pipeline_paths['im_skel_relabelled'] + if os.path.exists(im_instance_label_path) and os.path.exists(im_skel_relabelled_path): + self.open_segment_button.setEnabled(True) + self.nellie.settings.skip_vox.setEnabled(True) + self.nellie.settings.track_all_frames.setEnabled(True) + self.track_button.setEnabled(True) + self.track_all_button.setEnabled(True) + # self.viewer.help = 'Alt + click a label to see its tracks' + else: + self.open_segment_button.setEnabled(False) + self.nellie.settings.skip_vox.setEnabled(False) + self.nellie.settings.track_all_frames.setEnabled(False) + + im_marker_path = self.nellie.im_info.pipeline_paths['im_marker'] + if os.path.exists(im_marker_path): + self.open_mocap_button.setEnabled(True) + else: + self.open_mocap_button.setEnabled(False) + + im_branch_label_path = self.nellie.im_info.pipeline_paths['im_branch_label_reassigned'] + if os.path.exists(im_branch_label_path): + self.open_reassign_button.setEnabled(True) + self.track_button.setEnabled(True) + self.track_all_button.setEnabled(True) + # self.viewer.help = 'Alt + click a label to see its tracks' + else: + self.open_reassign_button.setEnabled(False)
+
+ + + +if __name__ == "__main__": + import napari + viewer = napari.Viewer() + napari.run() +
+ +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 0000000..f97d23f --- /dev/null +++ b/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,18 @@ +.. Nellie documentation master file, created by + sphinx-quickstart on Wed Oct 2 19:26:20 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Nellie documentation +==================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules \ No newline at end of file diff --git a/docs/_build/html/_sources/main.rst.txt b/docs/_build/html/_sources/main.rst.txt new file mode 100644 index 0000000..eace87b --- /dev/null +++ b/docs/_build/html/_sources/main.rst.txt @@ -0,0 +1,7 @@ +main module +=========== + +.. automodule:: main + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/modules.rst.txt b/docs/_build/html/_sources/modules.rst.txt new file mode 100644 index 0000000..d954396 --- /dev/null +++ b/docs/_build/html/_sources/modules.rst.txt @@ -0,0 +1,10 @@ +nellie +====== + +.. toctree:: + :maxdepth: 4 + + main + nellie + nellie_napari + tests diff --git a/docs/_build/html/_sources/nellie.feature_extraction.rst.txt b/docs/_build/html/_sources/nellie.feature_extraction.rst.txt new file mode 100644 index 0000000..617e89e --- /dev/null +++ b/docs/_build/html/_sources/nellie.feature_extraction.rst.txt @@ -0,0 +1,21 @@ +nellie.feature\_extraction package +================================== + +Submodules +---------- + +nellie.feature\_extraction.hierarchical module +---------------------------------------------- + +.. automodule:: nellie.feature_extraction.hierarchical + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.feature_extraction + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie.im_info.rst.txt b/docs/_build/html/_sources/nellie.im_info.rst.txt new file mode 100644 index 0000000..0c61970 --- /dev/null +++ b/docs/_build/html/_sources/nellie.im_info.rst.txt @@ -0,0 +1,21 @@ +nellie.im\_info package +======================= + +Submodules +---------- + +nellie.im\_info.verifier module +------------------------------- + +.. automodule:: nellie.im_info.verifier + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.im_info + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie.rst.txt b/docs/_build/html/_sources/nellie.rst.txt new file mode 100644 index 0000000..ca4d672 --- /dev/null +++ b/docs/_build/html/_sources/nellie.rst.txt @@ -0,0 +1,41 @@ +nellie package +============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + nellie.feature_extraction + nellie.im_info + nellie.segmentation + nellie.tracking + nellie.utils + +Submodules +---------- + +nellie.cli module +----------------- + +.. automodule:: nellie.cli + :members: + :undoc-members: + :show-inheritance: + +nellie.run module +----------------- + +.. automodule:: nellie.run + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie.segmentation.rst.txt b/docs/_build/html/_sources/nellie.segmentation.rst.txt new file mode 100644 index 0000000..bf8451b --- /dev/null +++ b/docs/_build/html/_sources/nellie.segmentation.rst.txt @@ -0,0 +1,45 @@ +nellie.segmentation package +=========================== + +Submodules +---------- + +nellie.segmentation.filtering module +------------------------------------ + +.. automodule:: nellie.segmentation.filtering + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.labelling module +------------------------------------ + +.. automodule:: nellie.segmentation.labelling + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.mocap\_marking module +----------------------------------------- + +.. automodule:: nellie.segmentation.mocap_marking + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.networking module +------------------------------------- + +.. automodule:: nellie.segmentation.networking + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.segmentation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie.tracking.rst.txt b/docs/_build/html/_sources/nellie.tracking.rst.txt new file mode 100644 index 0000000..0fe9b3c --- /dev/null +++ b/docs/_build/html/_sources/nellie.tracking.rst.txt @@ -0,0 +1,45 @@ +nellie.tracking package +======================= + +Submodules +---------- + +nellie.tracking.all\_tracks\_for\_label module +---------------------------------------------- + +.. automodule:: nellie.tracking.all_tracks_for_label + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.flow\_interpolation module +------------------------------------------ + +.. automodule:: nellie.tracking.flow_interpolation + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.hu\_tracking module +----------------------------------- + +.. automodule:: nellie.tracking.hu_tracking + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.voxel\_reassignment module +------------------------------------------ + +.. automodule:: nellie.tracking.voxel_reassignment + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.tracking + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie.utils.rst.txt b/docs/_build/html/_sources/nellie.utils.rst.txt new file mode 100644 index 0000000..1d4c733 --- /dev/null +++ b/docs/_build/html/_sources/nellie.utils.rst.txt @@ -0,0 +1,45 @@ +nellie.utils package +==================== + +Submodules +---------- + +nellie.utils.base\_logger module +-------------------------------- + +.. automodule:: nellie.utils.base_logger + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.general module +--------------------------- + +.. automodule:: nellie.utils.general + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.gpu\_functions module +---------------------------------- + +.. automodule:: nellie.utils.gpu_functions + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.torch\_xp module +----------------------------- + +.. automodule:: nellie.utils.torch_xp + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/nellie_napari.rst.txt b/docs/_build/html/_sources/nellie_napari.rst.txt new file mode 100644 index 0000000..8604bab --- /dev/null +++ b/docs/_build/html/_sources/nellie_napari.rst.txt @@ -0,0 +1,69 @@ +nellie\_napari package +====================== + +Submodules +---------- + +nellie\_napari.nellie\_analysis module +-------------------------------------- + +.. automodule:: nellie_napari.nellie_analysis + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_fileselect module +---------------------------------------- + +.. automodule:: nellie_napari.nellie_fileselect + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_home module +---------------------------------- + +.. automodule:: nellie_napari.nellie_home + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_loader module +------------------------------------ + +.. automodule:: nellie_napari.nellie_loader + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_processor module +--------------------------------------- + +.. automodule:: nellie_napari.nellie_processor + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_settings module +-------------------------------------- + +.. automodule:: nellie_napari.nellie_settings + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_visualizer module +---------------------------------------- + +.. automodule:: nellie_napari.nellie_visualizer + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie_napari + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/tests.rst.txt b/docs/_build/html/_sources/tests.rst.txt new file mode 100644 index 0000000..49fc745 --- /dev/null +++ b/docs/_build/html/_sources/tests.rst.txt @@ -0,0 +1,18 @@ +tests package +============= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + tests.unit + +Module contents +--------------- + +.. automodule:: tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_sources/tests.unit.rst.txt b/docs/_build/html/_sources/tests.unit.rst.txt new file mode 100644 index 0000000..673b4ae --- /dev/null +++ b/docs/_build/html/_sources/tests.unit.rst.txt @@ -0,0 +1,29 @@ +tests.unit package +================== + +Submodules +---------- + +tests.unit.test\_frangi\_filter module +-------------------------------------- + +.. automodule:: tests.unit.test_frangi_filter + :members: + :undoc-members: + :show-inheritance: + +tests.unit.test\_im\_info module +-------------------------------- + +.. automodule:: tests.unit.test_im_info + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: tests.unit + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/_build/html/_static/alabaster.css b/docs/_build/html/_static/alabaster.css new file mode 100644 index 0000000..e3174bf --- /dev/null +++ b/docs/_build/html/_static/alabaster.css @@ -0,0 +1,708 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar { + max-height: 100%; + overflow-y: auto; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 160px; +} + +div.sphinxsidebar .search > div { + display: table-cell; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin-left: 0; + margin-right: 0; + margin-top: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Hide ugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 0000000..e5179b7 --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: inherit; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/custom.css b/docs/_build/html/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/docs/_build/html/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/docs/_build/html/_static/doctools.js b/docs/_build/html/_static/doctools.js new file mode 100644 index 0000000..4d67807 --- /dev/null +++ b/docs/_build/html/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/_build/html/_static/documentation_options.js b/docs/_build/html/_static/documentation_options.js new file mode 100644 index 0000000..a18beba --- /dev/null +++ b/docs/_build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.3.2', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/_build/html/_static/file.png differ diff --git a/docs/_build/html/_static/language_data.js b/docs/_build/html/_static/language_data.js new file mode 100644 index 0000000..367b8ed --- /dev/null +++ b/docs/_build/html/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/_build/html/_static/minus.png b/docs/_build/html/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/docs/_build/html/_static/minus.png differ diff --git a/docs/_build/html/_static/plus.png b/docs/_build/html/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/docs/_build/html/_static/plus.png differ diff --git a/docs/_build/html/_static/pygments.css b/docs/_build/html/_static/pygments.css new file mode 100644 index 0000000..04a4174 --- /dev/null +++ b/docs/_build/html/_static/pygments.css @@ -0,0 +1,84 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #004461; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #582800 } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000000 } /* Generic.EmphStrong */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #745334 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #990000 } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #004461 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #888888 } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8 } /* Text.Whitespace */ +.highlight .mb { color: #990000 } /* Literal.Number.Bin */ +.highlight .mf { color: #990000 } /* Literal.Number.Float */ +.highlight .mh { color: #990000 } /* Literal.Number.Hex */ +.highlight .mi { color: #990000 } /* Literal.Number.Integer */ +.highlight .mo { color: #990000 } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_build/html/_static/searchtools.js b/docs/_build/html/_static/searchtools.js new file mode 100644 index 0000000..b08d58c --- /dev/null +++ b/docs/_build/html/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/_build/html/_static/sphinx_highlight.js b/docs/_build/html/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/docs/_build/html/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html new file mode 100644 index 0000000..de41f42 --- /dev/null +++ b/docs/_build/html/genindex.html @@ -0,0 +1,858 @@ + + + + + + + Index — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | L + | M + | N + | O + | P + | R + | S + | T + | U + | V + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + + +
    +
  • + nellie + +
  • +
  • + nellie.cli + +
  • +
  • + nellie.feature_extraction + +
  • +
  • + nellie.feature_extraction.hierarchical + +
  • +
  • + nellie.im_info + +
  • +
  • + nellie.im_info.verifier + +
  • +
  • + nellie.run + +
  • +
  • + nellie.segmentation + +
  • +
  • + nellie.segmentation.filtering + +
  • +
  • + nellie.segmentation.labelling + +
  • +
  • + nellie.segmentation.mocap_marking + +
  • +
  • + nellie.segmentation.networking + +
  • +
  • + nellie.tracking + +
  • +
  • + nellie.tracking.all_tracks_for_label + +
  • +
  • + nellie.tracking.flow_interpolation + +
  • +
  • + nellie.tracking.hu_tracking + +
  • +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
    +
  • + tests + +
  • +
  • + tests.unit + +
  • +
  • + tests.unit.test_frangi_filter + +
  • +
+ +

U

+ + +
+ +

V

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html new file mode 100644 index 0000000..477f443 --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,121 @@ + + + + + + + + Nellie documentation — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Nellie documentation¶

+

Add your content using reStructuredText syntax. See the +reStructuredText +documentation for details.

+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/main.html b/docs/_build/html/main.html new file mode 100644 index 0000000..fbc2b68 --- /dev/null +++ b/docs/_build/html/main.html @@ -0,0 +1,105 @@ + + + + + + + + main module — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

main module¶

+
+
+main.main()[source]¶
+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/modules.html b/docs/_build/html/modules.html new file mode 100644 index 0000000..bacd88f --- /dev/null +++ b/docs/_build/html/modules.html @@ -0,0 +1,312 @@ + + + + + + + + nellie — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie¶

+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.feature_extraction.html b/docs/_build/html/nellie.feature_extraction.html new file mode 100644 index 0000000..56168ca --- /dev/null +++ b/docs/_build/html/nellie.feature_extraction.html @@ -0,0 +1,523 @@ + + + + + + + + nellie.feature_extraction package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie.feature_extraction package¶

+
+

Submodules¶

+
+
+

nellie.feature_extraction.hierarchical module¶

+
+
+class nellie.feature_extraction.hierarchical.Branches(hierarchy)[source]¶
+

Bases: object

+

A class to extract and store branch-level features from hierarchical image data.

+
+

Parameters¶

+
+
hierarchyHierarchy

The Hierarchy object containing the image data and metadata.

+
+
+
+
+

Attributes¶

+
+
hierarchyHierarchy

The Hierarchy object.

+
+
timelist

List of time frames associated with the extracted branch features.

+
+
branch_labellist

List of branch labels for each frame.

+
+
aggregate_voxel_metricslist

List of aggregated voxel metrics for each branch.

+
+
aggregate_node_metricslist

List of aggregated node metrics for each branch.

+
+
z, x, ylist

List of branch centroid coordinates in 3D or 2D space.

+
+
branch_lengthlist

List of branch length values.

+
+
branch_thicknesslist

List of branch thickness values.

+
+
branch_aspect_ratiolist

List of aspect ratios for branches.

+
+
branch_tortuositylist

List of tortuosity values for branches.

+
+
branch_arealist

List of branch area values.

+
+
branch_axis_length_maj, branch_axis_length_minlist

List of major and minor axis lengths for branches.

+
+
branch_extentlist

List of extent values for branches.

+
+
branch_soliditylist

List of solidity values for branches.

+
+
reassigned_labellist

List of reassigned branch labels across time.

+
+
stats_to_aggregatelist

List of statistics to aggregate for branches.

+
+
features_to_savelist

List of branch features to save.

+
+
+
+
+run()[source]¶
+

Main function to run the extraction of branch features over all time frames. +Iterates over each frame to extract branch features and calculate metrics.

+
+ +
+
+ +
+
+class nellie.feature_extraction.hierarchical.Components(hierarchy)[source]¶
+

Bases: object

+

A class to extract and store component-level features from hierarchical image data.

+
+

Parameters¶

+
+
hierarchyHierarchy

The Hierarchy object containing the image data and metadata.

+
+
+
+
+

Attributes¶

+
+
hierarchyHierarchy

The Hierarchy object.

+
+
timelist

List of time frames associated with the extracted component features.

+
+
component_labellist

List of component labels for each frame.

+
+
aggregate_voxel_metricslist

List of aggregated voxel metrics for each component.

+
+
aggregate_node_metricslist

List of aggregated node metrics for each component.

+
+
aggregate_branch_metricslist

List of aggregated branch metrics for each component.

+
+
z, x, ylist

List of component centroid coordinates in 3D or 2D space.

+
+
organelle_arealist

List of component area values.

+
+
organelle_axis_length_maj, organelle_axis_length_minlist

List of major and minor axis lengths for components.

+
+
organelle_extentlist

List of extent values for components.

+
+
organelle_soliditylist

List of solidity values for components.

+
+
reassigned_labellist

List of reassigned component labels across time.

+
+
stats_to_aggregatelist

List of statistics to aggregate for components.

+
+
features_to_savelist

List of component features to save.

+
+
+
+
+run()[source]¶
+

Main function to run the extraction of component features over all time frames. +Iterates over each frame to extract component features and calculate metrics.

+
+ +
+
+ +
+
+class nellie.feature_extraction.hierarchical.Hierarchy(im_info: ImInfo, skip_nodes=True, viewer=None)[source]¶
+

Bases: object

+

A class to handle the hierarchical structure of image data, including voxel, node, branch, and component features.

+
+

Parameters¶

+
+
im_infoImInfo

Object containing metadata and pathways related to the image.

+
+
skip_nodesbool, optional

Whether to skip node processing (default is True).

+
+
vieweroptional

Viewer for updating status messages (default is None).

+
+
+
+
+

Attributes¶

+
+
im_infoImInfo

The ImInfo object containing the image metadata.

+
+
num_tint

Number of time frames in the image.

+
+
spacingtuple

Spacing between dimensions (Z, Y, X) or (Y, X) depending on the presence of Z.

+
+
im_rawmemmap

Raw image data loaded from disk.

+
+
im_structmemmap

Preprocessed structural image data.

+
+
im_distancememmap

Distance-transformed image data.

+
+
im_skelmemmap

Skeletonized image data.

+
+
label_componentsmemmap

Instance-labeled image data of components.

+
+
label_branchesmemmap

Re-labeled skeleton data of branches.

+
+
im_border_maskmemmap

Image data with border mask.

+
+
im_pixel_classmemmap

Image data classified by pixel types.

+
+
im_obj_reassignedmemmap

Object reassigned labels across time.

+
+
im_branch_reassignedmemmap

Branch reassigned labels across time.

+
+
flow_interpolator_fwFlowInterpolator

Forward flow interpolator.

+
+
flow_interpolator_bwFlowInterpolator

Backward flow interpolator.

+
+
vieweroptional

Viewer to display status updates.

+
+
+
+
+run()[source]¶
+

Main function to run the entire hierarchical feature extraction process, which includes memory allocation, +hierarchical analysis, and saving the results.

+
+ +
+
+ +
+
+class nellie.feature_extraction.hierarchical.Image(hierarchy)[source]¶
+

Bases: object

+

A class to extract and store global image-level features from hierarchical image data.

+
+

Parameters¶

+
+
hierarchyHierarchy

The Hierarchy object containing the image data and metadata.

+
+
+
+
+

Attributes¶

+
+
hierarchyHierarchy

The Hierarchy object.

+
+
timelist

List of time frames associated with the extracted image-level features.

+
+
image_namelist

List of image file names.

+
+
aggregate_voxel_metricslist

List of aggregated voxel metrics for the entire image.

+
+
aggregate_node_metricslist

List of aggregated node metrics for the entire image.

+
+
aggregate_branch_metricslist

List of aggregated branch metrics for the entire image.

+
+
aggregate_component_metricslist

List of aggregated component metrics for the entire image.

+
+
stats_to_aggregatelist

List of statistics to aggregate for the entire image.

+
+
features_to_savelist

List of image-level features to save.

+
+
+
+
+run()[source]¶
+

Main function to run the extraction of image-level features over all time frames. +Iterates over each frame to extract and aggregate voxel, node, branch, and component-level features.

+
+ +
+
+ +
+
+class nellie.feature_extraction.hierarchical.Nodes(hierarchy)[source]¶
+

Bases: object

+

A class to extract and store node-level features from hierarchical image data.

+
+

Parameters¶

+
+
hierarchyHierarchy

The Hierarchy object containing the image data and metadata.

+
+
+
+
+

Attributes¶

+
+
hierarchyHierarchy

The Hierarchy object.

+
+
timelist

List of time frames associated with the extracted node features.

+
+
nodeslist

List of node coordinates for each frame.

+
+
z, x, ylist

List of node coordinates in 3D or 2D space.

+
+
node_thicknesslist

List of node thickness values.

+
+
divergencelist

List of divergence values for nodes.

+
+
convergencelist

List of convergence values for nodes.

+
+
vergerelist

List of vergere values (convergence + divergence).

+
+
aggregate_voxel_metricslist

List of aggregated voxel metrics for each node.

+
+
voxel_idxslist

List of voxel indices associated with each node.

+
+
branch_labellist

List of branch labels assigned to nodes.

+
+
component_labellist

List of component labels assigned to nodes.

+
+
image_namelist

List of image file names.

+
+
stats_to_aggregatelist

List of statistics to aggregate for nodes.

+
+
features_to_savelist

List of node features to save.

+
+
+
+
+run()[source]¶
+

Main function to run the extraction of node features over all time frames. +Iterates over each frame to extract node features and calculate metrics.

+
+ +
+
+ +
+
+class nellie.feature_extraction.hierarchical.Voxels(hierarchy: Hierarchy)[source]¶
+

Bases: object

+

A class to extract and store voxel-level features from hierarchical image data.

+
+

Parameters¶

+
+
hierarchyHierarchy

The Hierarchy object containing the image data and metadata.

+
+
+
+
+

Attributes¶

+
+
hierarchyHierarchy

The Hierarchy object.

+
+
timelist

List of time frames associated with the extracted voxel features.

+
+
coordslist

List of voxel coordinates.

+
+
intensitylist

List of voxel intensity values.

+
+
structurelist

List of voxel structural values.

+
+
vec01list

List of vectors from frame t-1 to t.

+
+
vec12list

List of vectors from frame t to t+1.

+
+
lin_vellist

List of linear velocity vectors.

+
+
ang_vellist

List of angular velocity vectors.

+
+
directionality_rellist

List of directionality features.

+
+
node_labelslist

List of node labels assigned to voxels.

+
+
branch_labelslist

List of branch labels assigned to voxels.

+
+
component_labelslist

List of component labels assigned to voxels.

+
+
node_dim0_limslist

List of node bounding box limits in dimension 0 (Z or Y).

+
+
node_dim1_limslist

List of node bounding box limits in dimension 1 (Y or X).

+
+
node_dim2_limslist

List of node bounding box limits in dimension 2 (X).

+
+
node_voxel_idxslist

List of voxel indices associated with each node.

+
+
stats_to_aggregatelist

List of statistics to aggregate for features.

+
+
features_to_savelist

List of voxel features to save.

+
+
+
+
+run()[source]¶
+

Main function to run the extraction of voxel features over all time frames. +Iterates over each frame to extract coordinates, intensity, structural features, and motility statistics.

+
+ +
+
+ +
+
+nellie.feature_extraction.hierarchical.aggregate_stats_for_class(child_class, t, list_of_idxs)[source]¶
+
+ +
+
+nellie.feature_extraction.hierarchical.append_to_array(to_append)[source]¶
+

Converts feature dictionaries into lists of arrays and headers for saving to a CSV.

+
+

Parameters¶

+
+
to_appenddict

Dictionary containing feature names and values to append.

+
+
+
+
+

Returns¶

+
+
list

List of feature arrays.

+
+
list

List of corresponding feature headers.

+
+
+
+
+ +
+
+nellie.feature_extraction.hierarchical.create_feature_array(level, labels=None)[source]¶
+

Creates a 2D feature array and corresponding headers for saving features to CSV.

+
+

Parameters¶

+
+
levelobject

The level (e.g., voxel, node, branch, etc.) from which features are extracted.

+
+
labelsarray-like, optional

Array of labels to use for the first column of the output (default is None).

+
+
+
+
+

Returns¶

+
+
numpy.ndarray

2D array of features.

+
+
list

List of corresponding feature headers.

+
+
+
+
+ +
+
+nellie.feature_extraction.hierarchical.distance_check(border_mask, check_coords, spacing)[source]¶
+
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.html b/docs/_build/html/nellie.html new file mode 100644 index 0000000..26181f0 --- /dev/null +++ b/docs/_build/html/nellie.html @@ -0,0 +1,300 @@ + + + + + + + + nellie package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie package¶

+
+

Subpackages¶

+
+ +
+
+
+

Submodules¶

+
+
+

nellie.cli module¶

+
+
+nellie.cli.process_directory(directory, substring, output_dir, ch, num_t)[source]¶
+
+ +
+
+nellie.cli.process_files(files, ch, num_t, output_dir)[source]¶
+
+ +
+
+

nellie.run module¶

+
+
+nellie.run.run(file_info, remove_edges=False, otsu_thresh_intensity=False, threshold=None)[source]¶
+
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.im_info.html b/docs/_build/html/nellie.im_info.html new file mode 100644 index 0000000..09a12f7 --- /dev/null +++ b/docs/_build/html/nellie.im_info.html @@ -0,0 +1,181 @@ + + + + + + + + nellie.im_info package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie.im_info package¶

+
+

Submodules¶

+
+
+

nellie.im_info.verifier module¶

+
+
+class nellie.im_info.verifier.FileInfo(filepath, output_dir=None)[source]¶
+

Bases: object

+
+
+change_axes(new_axes)[source]¶
+
+ +
+
+change_dim_res(dim, new_size)[source]¶
+
+ +
+
+change_selected_channel(ch)[source]¶
+
+ +
+
+find_metadata()[source]¶
+
+ +
+
+load_metadata()[source]¶
+
+ +
+
+read_file()[source]¶
+
+ +
+
+save_ome_tiff()[source]¶
+
+ +
+
+select_temporal_range(start=0, end=None)[source]¶
+
+ +
+ +
+
+class nellie.im_info.verifier.ImInfo(file_info: FileInfo)[source]¶
+

Bases: object

+
+
+allocate_memory(output_path, dtype='float', data=None, description='No description.', return_memmap=False, read_mode='r+')[source]¶
+
+ +
+
+create_output_path(pipeline_path: str, ext: str = '.ome.tif', for_nellie=True)[source]¶
+
+ +
+
+get_memmap(file_path, read_mode='r+')[source]¶
+
+ +
+
+remove_intermediates()[source]¶
+
+ +
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.segmentation.html b/docs/_build/html/nellie.segmentation.html new file mode 100644 index 0000000..391b10c --- /dev/null +++ b/docs/_build/html/nellie.segmentation.html @@ -0,0 +1,162 @@ + + + + + + + + nellie.segmentation package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie.segmentation package¶

+
+

Submodules¶

+
+
+

nellie.segmentation.filtering module¶

+
+
+class nellie.segmentation.filtering.Filter(im_info: ImInfo, num_t=None, remove_edges=False, min_radius_um=0.2, max_radius_um=1, alpha_sq=0.5, beta_sq=0.5, frob_thresh=None, viewer=None)[source]¶
+

Bases: object

+
+
+run(mask=True)[source]¶
+
+ +
+ +
+
+

nellie.segmentation.labelling module¶

+
+
+class nellie.segmentation.labelling.Label(im_info: ImInfo, num_t=None, threshold=None, snr_cleaning=False, otsu_thresh_intensity=False, viewer=None)[source]¶
+

Bases: object

+
+
+run()[source]¶
+
+ +
+ +
+
+

nellie.segmentation.mocap_marking module¶

+
+
+class nellie.segmentation.mocap_marking.Markers(im_info: ImInfo, num_t=None, min_radius_um=0.2, max_radius_um=1, use_im='distance', num_sigma=5, viewer=None)[source]¶
+

Bases: object

+
+
+run()[source]¶
+
+ +
+ +
+
+

nellie.segmentation.networking module¶

+
+
+class nellie.segmentation.networking.Network(im_info: ImInfo, num_t=None, min_radius_um=0.2, max_radius_um=1, clean_skel=None, viewer=None)[source]¶
+

Bases: object

+
+
+run()[source]¶
+
+ +
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.tracking.html b/docs/_build/html/nellie.tracking.html new file mode 100644 index 0000000..ded5d4d --- /dev/null +++ b/docs/_build/html/nellie.tracking.html @@ -0,0 +1,182 @@ + + + + + + + + nellie.tracking package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie.tracking package¶

+
+

Submodules¶

+
+
+

nellie.tracking.all_tracks_for_label module¶

+
+
+class nellie.tracking.all_tracks_for_label.LabelTracks(im_info: ImInfo, num_t: int | None = None, label_im_path: str | None = None)[source]¶
+

Bases: object

+
+
+initialize()[source]¶
+
+ +
+
+run(label_num=None, start_frame=0, end_frame=None, min_track_num=0, skip_coords=1, max_distance_um=0.5)[source]¶
+
+ +
+ +
+
+

nellie.tracking.flow_interpolation module¶

+
+
+class nellie.tracking.flow_interpolation.FlowInterpolator(im_info: ImInfo, num_t=None, max_distance_um=0.5, forward=True)[source]¶
+

Bases: object

+
+
+interpolate_coord(coords, t)[source]¶
+
+ +
+ +
+
+nellie.tracking.flow_interpolation.interpolate_all_backward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5)[source]¶
+
+ +
+
+nellie.tracking.flow_interpolation.interpolate_all_forward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5)[source]¶
+
+ +
+
+

nellie.tracking.hu_tracking module¶

+
+
+class nellie.tracking.hu_tracking.HuMomentTracking(im_info: ImInfo, num_t=None, max_distance_um=1, viewer=None)[source]¶
+

Bases: object

+
+
+run()[source]¶
+
+ +
+ +
+
+

nellie.tracking.voxel_reassignment module¶

+
+
+class nellie.tracking.voxel_reassignment.VoxelReassigner(im_info: ImInfo, num_t=None, viewer=None)[source]¶
+

Bases: object

+
+
+match_voxels(vox_prev, vox_next, t)[source]¶
+
+ +
+
+run()[source]¶
+
+ +
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie.utils.html b/docs/_build/html/nellie.utils.html new file mode 100644 index 0000000..2cfbb59 --- /dev/null +++ b/docs/_build/html/nellie.utils.html @@ -0,0 +1,161 @@ + + + + + + + + nellie.utils package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie.utils package¶

+
+

Submodules¶

+
+
+

nellie.utils.base_logger module¶

+
+
+

nellie.utils.general module¶

+
+
+nellie.utils.general.bbox(im)[source]¶
+
+ +
+
+nellie.utils.general.get_reshaped_image(im, num_t=None, im_info=None, t_slice=None)[source]¶
+
+ +
+
+

nellie.utils.gpu_functions module¶

+
+
+nellie.utils.gpu_functions.otsu_effectiveness(image, inter_variance)[source]¶
+
+ +
+
+nellie.utils.gpu_functions.otsu_threshold(matrix, nbins=256)[source]¶
+
+ +
+
+nellie.utils.gpu_functions.triangle_threshold(matrix, nbins=256)[source]¶
+
+ +
+
+

nellie.utils.torch_xp module¶

+
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/nellie_napari.html b/docs/_build/html/nellie_napari.html new file mode 100644 index 0000000..59e11eb --- /dev/null +++ b/docs/_build/html/nellie_napari.html @@ -0,0 +1,550 @@ + + + + + + + + nellie_napari package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

nellie_napari package¶

+
+

Submodules¶

+
+
+

nellie_napari.nellie_analysis module¶

+
+
+class nellie_napari.nellie_analysis.NellieAnalysis(napari_viewer: napari.viewer.Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+check_for_adjacency_map()[source]¶
+
+ +
+
+draw_stats()[source]¶
+
+ +
+
+export_data()[source]¶
+
+ +
+
+get_csvs()[source]¶
+
+ +
+
+get_index(layer, event)[source]¶
+
+ +
+
+get_stats()[source]¶
+
+ +
+
+on_attr_selected(index)[source]¶
+
+ +
+
+on_hist_change(event)[source]¶
+
+ +
+
+on_level_selected(index)[source]¶
+
+ +
+
+on_log_scale(state)[source]¶
+
+ +
+
+on_t_change(event)[source]¶
+
+ +
+
+overlay()[source]¶
+
+ +
+
+plot_data(title)[source]¶
+
+ +
+
+post_init()[source]¶
+
+ +
+
+reset()[source]¶
+
+ +
+
+rewrite_dropdown()[source]¶
+
+ +
+
+save_graph()[source]¶
+
+ +
+
+set_default_dropdowns()[source]¶
+
+ +
+
+set_ui()[source]¶
+
+ +
+
+toggle_match_t(state)[source]¶
+
+ +
+
+toggle_mean_med(state)[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_fileselect module¶

+
+
+class nellie_napari.nellie_fileselect.NellieFileSelect(napari_viewer: Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+change_channel()[source]¶
+
+ +
+
+change_time()[source]¶
+
+ +
+
+check_available_dims()[source]¶
+
+ +
+
+handle_dim_order_changed(text)[source]¶
+
+ +
+
+handle_t_changed(text)[source]¶
+
+ +
+
+handle_xy_changed(text)[source]¶
+
+ +
+
+handle_z_changed(text)[source]¶
+
+ +
+
+init_ui()[source]¶
+
+ +
+
+initialize_folder()[source]¶
+
+ +
+
+initialize_single_file()[source]¶
+
+ +
+
+on_change()[source]¶
+
+ +
+
+on_confirm()[source]¶
+
+ +
+
+on_preview()[source]¶
+
+ +
+
+on_process()[source]¶
+
+ +
+
+select_filepath()[source]¶
+
+ +
+
+select_folder()[source]¶
+
+ +
+
+validate_path(filepath)[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_home module¶

+
+
+class nellie_napari.nellie_home.Home(napari_viewer: Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+screenshot(event=None)[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_loader module¶

+
+
+class nellie_napari.nellie_loader.NellieLoader(napari_viewer: napari.viewer.Viewer, parent=None)[source]¶
+

Bases: QTabWidget

+
+
+add_tabs()[source]¶
+
+ +
+
+go_process()[source]¶
+
+ +
+
+on_tab_change(index)[source]¶
+
+ +
+
+reset()[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_processor module¶

+
+
+class nellie_napari.nellie_processor.NellieProcessor(napari_viewer: napari.viewer.Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+check_file_existence()[source]¶
+
+ +
+
+open_directory()[source]¶
+
+ +
+
+post_init()[source]¶
+
+ +
+
+reset_status()[source]¶
+
+ +
+
+run_feature_export()[source]¶
+
+ +
+
+run_mocap()[source]¶
+
+ +
+
+run_nellie()[source]¶
+
+ +
+
+run_preprocessing()[source]¶
+
+ +
+
+run_reassign()[source]¶
+
+ +
+
+run_segmentation()[source]¶
+
+ +
+
+run_tracking()[source]¶
+
+ +
+
+set_status()[source]¶
+
+ +
+
+set_ui()[source]¶
+
+ +
+
+turn_off_buttons()[source]¶
+
+ +
+
+turn_off_pipeline()[source]¶
+
+ +
+
+update_status()[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_settings module¶

+
+
+class nellie_napari.nellie_settings.Settings(napari_viewer: Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+post_init()[source]¶
+
+ +
+
+set_ui()[source]¶
+
+ +
+ +
+
+

nellie_napari.nellie_visualizer module¶

+
+
+class nellie_napari.nellie_visualizer.NellieVisualizer(napari_viewer: napari.viewer.Viewer, nellie, parent=None)[source]¶
+

Bases: QWidget

+
+
+check_3d()[source]¶
+
+ +
+
+check_file_existence()[source]¶
+
+ +
+
+on_track_selected()[source]¶
+
+ +
+
+open_mocap_image()[source]¶
+
+ +
+
+open_preprocess_image()[source]¶
+
+ +
+
+open_raw()[source]¶
+
+ +
+
+open_reassign_image()[source]¶
+
+ +
+
+open_segment_image()[source]¶
+
+ +
+
+post_init()[source]¶
+
+ +
+
+set_scale()[source]¶
+
+ +
+
+set_ui()[source]¶
+
+ +
+
+track_all()[source]¶
+
+ +
+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv new file mode 100644 index 0000000..88e8e28 Binary files /dev/null and b/docs/_build/html/objects.inv differ diff --git a/docs/_build/html/py-modindex.html b/docs/_build/html/py-modindex.html new file mode 100644 index 0000000..28dbad6 --- /dev/null +++ b/docs/_build/html/py-modindex.html @@ -0,0 +1,288 @@ + + + + + + + Python Module Index — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ m | + n | + t +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ m
+ main +
 
+ n
+ nellie +
    + nellie.cli +
    + nellie.feature_extraction +
    + nellie.feature_extraction.hierarchical +
    + nellie.im_info +
    + nellie.im_info.verifier +
    + nellie.run +
    + nellie.segmentation +
    + nellie.segmentation.filtering +
    + nellie.segmentation.labelling +
    + nellie.segmentation.mocap_marking +
    + nellie.segmentation.networking +
    + nellie.tracking +
    + nellie.tracking.all_tracks_for_label +
    + nellie.tracking.flow_interpolation +
    + nellie.tracking.hu_tracking +
    + nellie.tracking.voxel_reassignment +
    + nellie.utils +
    + nellie.utils.base_logger +
    + nellie.utils.general +
    + nellie.utils.gpu_functions +
+ nellie_napari +
    + nellie_napari.nellie_analysis +
    + nellie_napari.nellie_fileselect +
    + nellie_napari.nellie_home +
    + nellie_napari.nellie_loader +
    + nellie_napari.nellie_processor +
    + nellie_napari.nellie_settings +
    + nellie_napari.nellie_visualizer +
 
+ t
+ tests +
    + tests.unit +
    + tests.unit.test_frangi_filter +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html new file mode 100644 index 0000000..b572347 --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,120 @@ + + + + + + + Search — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js new file mode 100644 index 0000000..c2d3538 --- /dev/null +++ b/docs/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Attributes": [[4, "attributes"], [4, "id2"], [4, "id4"], [4, "id6"], [4, "id8"], [4, "id10"]], "Contents:": [[0, null]], "Module contents": [[3, "module-nellie"], [4, "module-nellie.feature_extraction"], [5, "module-nellie.im_info"], [6, "module-nellie.segmentation"], [7, "module-nellie.tracking"], [8, "module-nellie.utils"], [9, "module-nellie_napari"], [10, "module-tests"], [11, "module-tests.unit"]], "Nellie documentation": [[0, null]], "Parameters": [[4, "parameters"], [4, "id1"], [4, "id3"], [4, "id5"], [4, "id7"], [4, "id9"], [4, "id11"], [4, "id12"]], "Returns": [[4, "returns"], [4, "id13"]], "Submodules": [[3, "submodules"], [4, "submodules"], [5, "submodules"], [6, "submodules"], [7, "submodules"], [8, "submodules"], [9, "submodules"], [11, "submodules"]], "Subpackages": [[3, "subpackages"], [10, "subpackages"]], "main module": [[1, null]], "nellie": [[2, null]], "nellie package": [[3, null]], "nellie.cli module": [[3, "module-nellie.cli"]], "nellie.feature_extraction package": [[4, null]], "nellie.feature_extraction.hierarchical module": [[4, "module-nellie.feature_extraction.hierarchical"]], "nellie.im_info package": [[5, null]], "nellie.im_info.verifier module": [[5, "module-nellie.im_info.verifier"]], "nellie.run module": [[3, "module-nellie.run"]], "nellie.segmentation package": [[6, null]], "nellie.segmentation.filtering module": [[6, "module-nellie.segmentation.filtering"]], "nellie.segmentation.labelling module": [[6, "module-nellie.segmentation.labelling"]], "nellie.segmentation.mocap_marking module": [[6, "module-nellie.segmentation.mocap_marking"]], "nellie.segmentation.networking module": [[6, "module-nellie.segmentation.networking"]], "nellie.tracking package": [[7, null]], "nellie.tracking.all_tracks_for_label module": [[7, "module-nellie.tracking.all_tracks_for_label"]], "nellie.tracking.flow_interpolation module": [[7, "module-nellie.tracking.flow_interpolation"]], "nellie.tracking.hu_tracking module": [[7, "module-nellie.tracking.hu_tracking"]], "nellie.tracking.voxel_reassignment module": [[7, "module-nellie.tracking.voxel_reassignment"]], "nellie.utils package": [[8, null]], "nellie.utils.base_logger module": [[8, "module-nellie.utils.base_logger"]], "nellie.utils.general module": [[8, "module-nellie.utils.general"]], "nellie.utils.gpu_functions module": [[8, "module-nellie.utils.gpu_functions"]], "nellie.utils.torch_xp module": [[8, "nellie-utils-torch-xp-module"]], "nellie_napari package": [[9, null]], "nellie_napari.nellie_analysis module": [[9, "module-nellie_napari.nellie_analysis"]], "nellie_napari.nellie_fileselect module": [[9, "module-nellie_napari.nellie_fileselect"]], "nellie_napari.nellie_home module": [[9, "module-nellie_napari.nellie_home"]], "nellie_napari.nellie_loader module": [[9, "module-nellie_napari.nellie_loader"]], "nellie_napari.nellie_processor module": [[9, "module-nellie_napari.nellie_processor"]], "nellie_napari.nellie_settings module": [[9, "module-nellie_napari.nellie_settings"]], "nellie_napari.nellie_visualizer module": [[9, "module-nellie_napari.nellie_visualizer"]], "tests package": [[10, null]], "tests.unit package": [[11, null]], "tests.unit.test_frangi_filter module": [[11, "module-tests.unit.test_frangi_filter"]], "tests.unit.test_im_info module": [[11, "tests-unit-test-im-info-module"]]}, "docnames": ["index", "main", "modules", "nellie", "nellie.feature_extraction", "nellie.im_info", "nellie.segmentation", "nellie.tracking", "nellie.utils", "nellie_napari", "tests", "tests.unit"], "envversion": {"sphinx": 63, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["index.rst", "main.rst", "modules.rst", "nellie.rst", "nellie.feature_extraction.rst", "nellie.im_info.rst", "nellie.segmentation.rst", "nellie.tracking.rst", "nellie.utils.rst", "nellie_napari.rst", "tests.rst", "tests.unit.rst"], "indexentries": {"add_tabs() (nellie_napari.nellie_loader.nellieloader method)": [[9, "nellie_napari.nellie_loader.NellieLoader.add_tabs", false]], "aggregate_stats_for_class() (in module nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.aggregate_stats_for_class", false]], "append_to_array() (in module nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.append_to_array", false]], "bbox() (in module nellie.utils.general)": [[8, "nellie.utils.general.bbox", false]], "branches (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Branches", false]], "change_channel() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.change_channel", false]], "change_time() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.change_time", false]], "check_3d() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.check_3d", false]], "check_available_dims() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.check_available_dims", false]], "check_file_existence() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.check_file_existence", false]], "check_file_existence() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.check_file_existence", false]], "check_for_adjacency_map() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.check_for_adjacency_map", false]], "components (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Components", false]], "create_feature_array() (in module nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.create_feature_array", false]], "distance_check() (in module nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.distance_check", false]], "draw_stats() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.draw_stats", false]], "export_data() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.export_data", false]], "get_csvs() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.get_csvs", false]], "get_index() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.get_index", false]], "get_reshaped_image() (in module nellie.utils.general)": [[8, "nellie.utils.general.get_reshaped_image", false]], "get_stats() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.get_stats", false]], "go_process() (nellie_napari.nellie_loader.nellieloader method)": [[9, "nellie_napari.nellie_loader.NellieLoader.go_process", false]], "handle_dim_order_changed() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.handle_dim_order_changed", false]], "handle_t_changed() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.handle_t_changed", false]], "handle_xy_changed() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.handle_xy_changed", false]], "handle_z_changed() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.handle_z_changed", false]], "hierarchy (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Hierarchy", false]], "home (class in nellie_napari.nellie_home)": [[9, "nellie_napari.nellie_home.Home", false]], "image (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Image", false]], "init_ui() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.init_ui", false]], "initialize_folder() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.initialize_folder", false]], "initialize_single_file() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.initialize_single_file", false]], "module": [[3, "module-nellie", false], [3, "module-nellie.cli", false], [3, "module-nellie.run", false], [4, "module-nellie.feature_extraction", false], [4, "module-nellie.feature_extraction.hierarchical", false], [8, "module-nellie.utils", false], [8, "module-nellie.utils.base_logger", false], [8, "module-nellie.utils.general", false], [8, "module-nellie.utils.gpu_functions", false], [9, "module-nellie_napari", false], [9, "module-nellie_napari.nellie_analysis", false], [9, "module-nellie_napari.nellie_fileselect", false], [9, "module-nellie_napari.nellie_home", false], [9, "module-nellie_napari.nellie_loader", false], [9, "module-nellie_napari.nellie_processor", false], [9, "module-nellie_napari.nellie_settings", false], [9, "module-nellie_napari.nellie_visualizer", false], [10, "module-tests", false], [11, "module-tests.unit", false], [11, "module-tests.unit.test_frangi_filter", false]], "nellie": [[3, "module-nellie", false]], "nellie.cli": [[3, "module-nellie.cli", false]], "nellie.feature_extraction": [[4, "module-nellie.feature_extraction", false]], "nellie.feature_extraction.hierarchical": [[4, "module-nellie.feature_extraction.hierarchical", false]], "nellie.run": [[3, "module-nellie.run", false]], "nellie.utils": [[8, "module-nellie.utils", false]], "nellie.utils.base_logger": [[8, "module-nellie.utils.base_logger", false]], "nellie.utils.general": [[8, "module-nellie.utils.general", false]], "nellie.utils.gpu_functions": [[8, "module-nellie.utils.gpu_functions", false]], "nellie_napari": [[9, "module-nellie_napari", false]], "nellie_napari.nellie_analysis": [[9, "module-nellie_napari.nellie_analysis", false]], "nellie_napari.nellie_fileselect": [[9, "module-nellie_napari.nellie_fileselect", false]], "nellie_napari.nellie_home": [[9, "module-nellie_napari.nellie_home", false]], "nellie_napari.nellie_loader": [[9, "module-nellie_napari.nellie_loader", false]], "nellie_napari.nellie_processor": [[9, "module-nellie_napari.nellie_processor", false]], "nellie_napari.nellie_settings": [[9, "module-nellie_napari.nellie_settings", false]], "nellie_napari.nellie_visualizer": [[9, "module-nellie_napari.nellie_visualizer", false]], "nellieanalysis (class in nellie_napari.nellie_analysis)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis", false]], "nelliefileselect (class in nellie_napari.nellie_fileselect)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect", false]], "nellieloader (class in nellie_napari.nellie_loader)": [[9, "nellie_napari.nellie_loader.NellieLoader", false]], "nellieprocessor (class in nellie_napari.nellie_processor)": [[9, "nellie_napari.nellie_processor.NellieProcessor", false]], "nellievisualizer (class in nellie_napari.nellie_visualizer)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer", false]], "nodes (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Nodes", false]], "on_attr_selected() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.on_attr_selected", false]], "on_change() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.on_change", false]], "on_confirm() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.on_confirm", false]], "on_hist_change() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.on_hist_change", false]], "on_level_selected() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.on_level_selected", false]], "on_log_scale() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.on_log_scale", false]], "on_preview() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.on_preview", false]], "on_process() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.on_process", false]], "on_t_change() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.on_t_change", false]], "on_tab_change() (nellie_napari.nellie_loader.nellieloader method)": [[9, "nellie_napari.nellie_loader.NellieLoader.on_tab_change", false]], "on_track_selected() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.on_track_selected", false]], "open_directory() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.open_directory", false]], "open_mocap_image() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.open_mocap_image", false]], "open_preprocess_image() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.open_preprocess_image", false]], "open_raw() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.open_raw", false]], "open_reassign_image() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.open_reassign_image", false]], "open_segment_image() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.open_segment_image", false]], "otsu_effectiveness() (in module nellie.utils.gpu_functions)": [[8, "nellie.utils.gpu_functions.otsu_effectiveness", false]], "otsu_threshold() (in module nellie.utils.gpu_functions)": [[8, "nellie.utils.gpu_functions.otsu_threshold", false]], "overlay() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.overlay", false]], "plot_data() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.plot_data", false]], "post_init() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.post_init", false]], "post_init() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.post_init", false]], "post_init() (nellie_napari.nellie_settings.settings method)": [[9, "nellie_napari.nellie_settings.Settings.post_init", false]], "post_init() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.post_init", false]], "process_directory() (in module nellie.cli)": [[3, "nellie.cli.process_directory", false]], "process_files() (in module nellie.cli)": [[3, "nellie.cli.process_files", false]], "reset() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.reset", false]], "reset() (nellie_napari.nellie_loader.nellieloader method)": [[9, "nellie_napari.nellie_loader.NellieLoader.reset", false]], "reset_status() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.reset_status", false]], "rewrite_dropdown() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.rewrite_dropdown", false]], "run() (in module nellie.run)": [[3, "nellie.run.run", false]], "run() (nellie.feature_extraction.hierarchical.branches method)": [[4, "nellie.feature_extraction.hierarchical.Branches.run", false]], "run() (nellie.feature_extraction.hierarchical.components method)": [[4, "nellie.feature_extraction.hierarchical.Components.run", false]], "run() (nellie.feature_extraction.hierarchical.hierarchy method)": [[4, "nellie.feature_extraction.hierarchical.Hierarchy.run", false]], "run() (nellie.feature_extraction.hierarchical.image method)": [[4, "nellie.feature_extraction.hierarchical.Image.run", false]], "run() (nellie.feature_extraction.hierarchical.nodes method)": [[4, "nellie.feature_extraction.hierarchical.Nodes.run", false]], "run() (nellie.feature_extraction.hierarchical.voxels method)": [[4, "nellie.feature_extraction.hierarchical.Voxels.run", false]], "run_feature_export() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_feature_export", false]], "run_mocap() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_mocap", false]], "run_nellie() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_nellie", false]], "run_preprocessing() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_preprocessing", false]], "run_reassign() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_reassign", false]], "run_segmentation() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_segmentation", false]], "run_tracking() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.run_tracking", false]], "save_graph() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.save_graph", false]], "screenshot() (nellie_napari.nellie_home.home method)": [[9, "nellie_napari.nellie_home.Home.screenshot", false]], "select_filepath() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.select_filepath", false]], "select_folder() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.select_folder", false]], "set_default_dropdowns() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.set_default_dropdowns", false]], "set_scale() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.set_scale", false]], "set_status() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.set_status", false]], "set_ui() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.set_ui", false]], "set_ui() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.set_ui", false]], "set_ui() (nellie_napari.nellie_settings.settings method)": [[9, "nellie_napari.nellie_settings.Settings.set_ui", false]], "set_ui() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.set_ui", false]], "settings (class in nellie_napari.nellie_settings)": [[9, "nellie_napari.nellie_settings.Settings", false]], "tests": [[10, "module-tests", false]], "tests.unit": [[11, "module-tests.unit", false]], "tests.unit.test_frangi_filter": [[11, "module-tests.unit.test_frangi_filter", false]], "toggle_match_t() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.toggle_match_t", false]], "toggle_mean_med() (nellie_napari.nellie_analysis.nellieanalysis method)": [[9, "nellie_napari.nellie_analysis.NellieAnalysis.toggle_mean_med", false]], "track_all() (nellie_napari.nellie_visualizer.nellievisualizer method)": [[9, "nellie_napari.nellie_visualizer.NellieVisualizer.track_all", false]], "triangle_threshold() (in module nellie.utils.gpu_functions)": [[8, "nellie.utils.gpu_functions.triangle_threshold", false]], "turn_off_buttons() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.turn_off_buttons", false]], "turn_off_pipeline() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.turn_off_pipeline", false]], "update_status() (nellie_napari.nellie_processor.nellieprocessor method)": [[9, "nellie_napari.nellie_processor.NellieProcessor.update_status", false]], "validate_path() (nellie_napari.nellie_fileselect.nelliefileselect method)": [[9, "nellie_napari.nellie_fileselect.NellieFileSelect.validate_path", false]], "voxels (class in nellie.feature_extraction.hierarchical)": [[4, "nellie.feature_extraction.hierarchical.Voxels", false]]}, "objects": {"": [[1, 0, 0, "-", "main"], [3, 0, 0, "-", "nellie"], [9, 0, 0, "-", "nellie_napari"], [10, 0, 0, "-", "tests"]], "main": [[1, 1, 1, "", "main"]], "nellie": [[3, 0, 0, "-", "cli"], [4, 0, 0, "-", "feature_extraction"], [5, 0, 0, "-", "im_info"], [3, 0, 0, "-", "run"], [6, 0, 0, "-", "segmentation"], [7, 0, 0, "-", "tracking"], [8, 0, 0, "-", "utils"]], "nellie.cli": [[3, 1, 1, "", "process_directory"], [3, 1, 1, "", "process_files"]], "nellie.feature_extraction": [[4, 0, 0, "-", "hierarchical"]], "nellie.feature_extraction.hierarchical": [[4, 2, 1, "", "Branches"], [4, 2, 1, "", "Components"], [4, 2, 1, "", "Hierarchy"], [4, 2, 1, "", "Image"], [4, 2, 1, "", "Nodes"], [4, 2, 1, "", "Voxels"], [4, 1, 1, "", "aggregate_stats_for_class"], [4, 1, 1, "", "append_to_array"], [4, 1, 1, "", "create_feature_array"], [4, 1, 1, "", "distance_check"]], "nellie.feature_extraction.hierarchical.Branches": [[4, 3, 1, "", "run"]], "nellie.feature_extraction.hierarchical.Components": [[4, 3, 1, "", "run"]], "nellie.feature_extraction.hierarchical.Hierarchy": [[4, 3, 1, "", "run"]], "nellie.feature_extraction.hierarchical.Image": [[4, 3, 1, "", "run"]], "nellie.feature_extraction.hierarchical.Nodes": [[4, 3, 1, "", "run"]], "nellie.feature_extraction.hierarchical.Voxels": [[4, 3, 1, "", "run"]], "nellie.im_info": [[5, 0, 0, "-", "verifier"]], "nellie.im_info.verifier": [[5, 2, 1, "", "FileInfo"], [5, 2, 1, "", "ImInfo"]], "nellie.im_info.verifier.FileInfo": [[5, 3, 1, "", "change_axes"], [5, 3, 1, "", "change_dim_res"], [5, 3, 1, "", "change_selected_channel"], [5, 3, 1, "", "find_metadata"], [5, 3, 1, "", "load_metadata"], [5, 3, 1, "", "read_file"], [5, 3, 1, "", "save_ome_tiff"], [5, 3, 1, "", "select_temporal_range"]], "nellie.im_info.verifier.ImInfo": [[5, 3, 1, "", "allocate_memory"], [5, 3, 1, "", "create_output_path"], [5, 3, 1, "", "get_memmap"], [5, 3, 1, "", "remove_intermediates"]], "nellie.run": [[3, 1, 1, "", "run"]], "nellie.segmentation": [[6, 0, 0, "-", "filtering"], [6, 0, 0, "-", "labelling"], [6, 0, 0, "-", "mocap_marking"], [6, 0, 0, "-", "networking"]], "nellie.segmentation.filtering": [[6, 2, 1, "", "Filter"]], "nellie.segmentation.filtering.Filter": [[6, 3, 1, "", "run"]], "nellie.segmentation.labelling": [[6, 2, 1, "", "Label"]], "nellie.segmentation.labelling.Label": [[6, 3, 1, "", "run"]], "nellie.segmentation.mocap_marking": [[6, 2, 1, "", "Markers"]], "nellie.segmentation.mocap_marking.Markers": [[6, 3, 1, "", "run"]], "nellie.segmentation.networking": [[6, 2, 1, "", "Network"]], "nellie.segmentation.networking.Network": [[6, 3, 1, "", "run"]], "nellie.tracking": [[7, 0, 0, "-", "all_tracks_for_label"], [7, 0, 0, "-", "flow_interpolation"], [7, 0, 0, "-", "hu_tracking"], [7, 0, 0, "-", "voxel_reassignment"]], "nellie.tracking.all_tracks_for_label": [[7, 2, 1, "", "LabelTracks"]], "nellie.tracking.all_tracks_for_label.LabelTracks": [[7, 3, 1, "", "initialize"], [7, 3, 1, "", "run"]], "nellie.tracking.flow_interpolation": [[7, 2, 1, "", "FlowInterpolator"], [7, 1, 1, "", "interpolate_all_backward"], [7, 1, 1, "", "interpolate_all_forward"]], "nellie.tracking.flow_interpolation.FlowInterpolator": [[7, 3, 1, "", "interpolate_coord"]], "nellie.tracking.hu_tracking": [[7, 2, 1, "", "HuMomentTracking"]], "nellie.tracking.hu_tracking.HuMomentTracking": [[7, 3, 1, "", "run"]], "nellie.tracking.voxel_reassignment": [[7, 2, 1, "", "VoxelReassigner"]], "nellie.tracking.voxel_reassignment.VoxelReassigner": [[7, 3, 1, "", "match_voxels"], [7, 3, 1, "", "run"]], "nellie.utils": [[8, 0, 0, "-", "base_logger"], [8, 0, 0, "-", "general"], [8, 0, 0, "-", "gpu_functions"]], "nellie.utils.general": [[8, 1, 1, "", "bbox"], [8, 1, 1, "", "get_reshaped_image"]], "nellie.utils.gpu_functions": [[8, 1, 1, "", "otsu_effectiveness"], [8, 1, 1, "", "otsu_threshold"], [8, 1, 1, "", "triangle_threshold"]], "nellie_napari": [[9, 0, 0, "-", "nellie_analysis"], [9, 0, 0, "-", "nellie_fileselect"], [9, 0, 0, "-", "nellie_home"], [9, 0, 0, "-", "nellie_loader"], [9, 0, 0, "-", "nellie_processor"], [9, 0, 0, "-", "nellie_settings"], [9, 0, 0, "-", "nellie_visualizer"]], "nellie_napari.nellie_analysis": [[9, 2, 1, "", "NellieAnalysis"]], "nellie_napari.nellie_analysis.NellieAnalysis": [[9, 3, 1, "", "check_for_adjacency_map"], [9, 3, 1, "", "draw_stats"], [9, 3, 1, "", "export_data"], [9, 3, 1, "", "get_csvs"], [9, 3, 1, "", "get_index"], [9, 3, 1, "", "get_stats"], [9, 3, 1, "", "on_attr_selected"], [9, 3, 1, "", "on_hist_change"], [9, 3, 1, "", "on_level_selected"], [9, 3, 1, "", "on_log_scale"], [9, 3, 1, "", "on_t_change"], [9, 3, 1, "", "overlay"], [9, 3, 1, "", "plot_data"], [9, 3, 1, "", "post_init"], [9, 3, 1, "", "reset"], [9, 3, 1, "", "rewrite_dropdown"], [9, 3, 1, "", "save_graph"], [9, 3, 1, "", "set_default_dropdowns"], [9, 3, 1, "", "set_ui"], [9, 3, 1, "", "toggle_match_t"], [9, 3, 1, "", "toggle_mean_med"]], "nellie_napari.nellie_fileselect": [[9, 2, 1, "", "NellieFileSelect"]], "nellie_napari.nellie_fileselect.NellieFileSelect": [[9, 3, 1, "", "change_channel"], [9, 3, 1, "", "change_time"], [9, 3, 1, "", "check_available_dims"], [9, 3, 1, "", "handle_dim_order_changed"], [9, 3, 1, "", "handle_t_changed"], [9, 3, 1, "", "handle_xy_changed"], [9, 3, 1, "", "handle_z_changed"], [9, 3, 1, "", "init_ui"], [9, 3, 1, "", "initialize_folder"], [9, 3, 1, "", "initialize_single_file"], [9, 3, 1, "", "on_change"], [9, 3, 1, "", "on_confirm"], [9, 3, 1, "", "on_preview"], [9, 3, 1, "", "on_process"], [9, 3, 1, "", "select_filepath"], [9, 3, 1, "", "select_folder"], [9, 3, 1, "", "validate_path"]], "nellie_napari.nellie_home": [[9, 2, 1, "", "Home"]], "nellie_napari.nellie_home.Home": [[9, 3, 1, "", "screenshot"]], "nellie_napari.nellie_loader": [[9, 2, 1, "", "NellieLoader"]], "nellie_napari.nellie_loader.NellieLoader": [[9, 3, 1, "", "add_tabs"], [9, 3, 1, "", "go_process"], [9, 3, 1, "", "on_tab_change"], [9, 3, 1, "", "reset"]], "nellie_napari.nellie_processor": [[9, 2, 1, "", "NellieProcessor"]], "nellie_napari.nellie_processor.NellieProcessor": [[9, 3, 1, "", "check_file_existence"], [9, 3, 1, "", "open_directory"], [9, 3, 1, "", "post_init"], [9, 3, 1, "", "reset_status"], [9, 3, 1, "", "run_feature_export"], [9, 3, 1, "", "run_mocap"], [9, 3, 1, "", "run_nellie"], [9, 3, 1, "", "run_preprocessing"], [9, 3, 1, "", "run_reassign"], [9, 3, 1, "", "run_segmentation"], [9, 3, 1, "", "run_tracking"], [9, 3, 1, "", "set_status"], [9, 3, 1, "", "set_ui"], [9, 3, 1, "", "turn_off_buttons"], [9, 3, 1, "", "turn_off_pipeline"], [9, 3, 1, "", "update_status"]], "nellie_napari.nellie_settings": [[9, 2, 1, "", "Settings"]], "nellie_napari.nellie_settings.Settings": [[9, 3, 1, "", "post_init"], [9, 3, 1, "", "set_ui"]], "nellie_napari.nellie_visualizer": [[9, 2, 1, "", "NellieVisualizer"]], "nellie_napari.nellie_visualizer.NellieVisualizer": [[9, 3, 1, "", "check_3d"], [9, 3, 1, "", "check_file_existence"], [9, 3, 1, "", "on_track_selected"], [9, 3, 1, "", "open_mocap_image"], [9, 3, 1, "", "open_preprocess_image"], [9, 3, 1, "", "open_raw"], [9, 3, 1, "", "open_reassign_image"], [9, 3, 1, "", "open_segment_image"], [9, 3, 1, "", "post_init"], [9, 3, 1, "", "set_scale"], [9, 3, 1, "", "set_ui"], [9, 3, 1, "", "track_all"]], "tests": [[11, 0, 0, "-", "unit"]], "tests.unit": [[11, 0, 0, "-", "test_frangi_filter"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:class", "3": "py:method"}, "terms": {"0": [4, 5, 6, 7], "1": [4, 6, 7], "2": [4, 6], "256": 8, "2d": 4, "3d": 4, "5": [6, 7], "A": 4, "No": 5, "The": 4, "across": 4, "add": 0, "add_tab": [2, 9], "aggreg": 4, "aggregate_branch_metr": 4, "aggregate_component_metr": 4, "aggregate_node_metr": 4, "aggregate_stats_for_class": [3, 4], "aggregate_voxel_metr": 4, "all": 4, "all_tracks_for_label": [2, 3], "alloc": 4, "allocate_memori": [3, 5], "alpha_sq": 6, "analysi": 4, "ang_vel": 4, "angular": 4, "append": 4, "append_to_arrai": [3, 4], "ar": 4, "area": 4, "arrai": 4, "aspect": 4, "assign": 4, "associ": 4, "axi": 4, "backward": 4, "base": [4, 5, 6, 7, 9], "base_logg": [2, 3], "bbox": [3, 8], "beta_sq": 6, "between": 4, "bool": 4, "border": 4, "border_mask": 4, "bound": 4, "box": 4, "branch": [3, 4], "branch_area": 4, "branch_aspect_ratio": 4, "branch_axis_length_maj": 4, "branch_axis_length_min": 4, "branch_ext": 4, "branch_label": 4, "branch_length": 4, "branch_solid": 4, "branch_thick": 4, "branch_tortuos": 4, "calcul": 4, "centroid": 4, "ch": [3, 5], "change_ax": [3, 5], "change_channel": [2, 9], "change_dim_r": [3, 5], "change_selected_channel": [3, 5], "change_tim": [2, 9], "check_3d": [2, 9], "check_available_dim": [2, 9], "check_coord": 4, "check_file_exist": [2, 9], "check_for_adjacency_map": [2, 9], "child_class": 4, "class": [4, 5, 6, 7, 9], "classifi": 4, "clean_skel": 6, "cli": 2, "column": 4, "compon": [3, 4], "component_label": 4, "contain": 4, "content": 2, "converg": 4, "convert": 4, "coord": [4, 7], "coordin": 4, "correspond": 4, "creat": 4, "create_feature_arrai": [3, 4], "create_output_path": [3, 5], "csv": 4, "data": [4, 5], "default": 4, "depend": 4, "descript": 5, "detail": 0, "dict": 4, "dictionari": 4, "dim": 5, "dimens": 4, "direction": 4, "directionality_rel": 4, "directori": 3, "disk": 4, "displai": 4, "distanc": [4, 6], "distance_check": [3, 4], "diverg": 4, "draw_stat": [2, 9], "dtype": 5, "e": 4, "each": 4, "end": 5, "end_fram": 7, "end_t": 7, "entir": 4, "etc": 4, "event": 9, "export_data": [2, 9], "ext": 5, "extent": 4, "extract": 4, "fals": [3, 5, 6], "featur": 4, "feature_extract": [2, 3], "features_to_sav": 4, "file": [3, 4], "file_info": [3, 5], "file_path": 5, "fileinfo": [3, 5], "filepath": [5, 9], "filter": [2, 3], "find_metadata": [3, 5], "first": 4, "float": 5, "flow": 4, "flow_interpol": [2, 3], "flow_interpolator_bw": 4, "flow_interpolator_fw": 4, "flowinterpol": [3, 4, 7], "for_nelli": 5, "forward": [4, 7], "frame": 4, "frob_thresh": 6, "from": 4, "function": 4, "g": 4, "gener": [2, 3], "get_csv": [2, 9], "get_index": [2, 9], "get_memmap": [3, 5], "get_reshaped_imag": [3, 8], "get_stat": [2, 9], "global": 4, "go_process": [2, 9], "gpu_funct": [2, 3], "handl": 4, "handle_dim_order_chang": [2, 9], "handle_t_chang": [2, 9], "handle_xy_chang": [2, 9], "handle_z_chang": [2, 9], "header": 4, "hierarch": [2, 3], "hierarchi": [3, 4], "home": [2, 9], "hu_track": [2, 3], "humomenttrack": [3, 7], "i": 4, "im": 8, "im_border_mask": 4, "im_branch_reassign": 4, "im_dist": 4, "im_info": [2, 3, 4, 6, 7, 8], "im_obj_reassign": 4, "im_pixel_class": 4, "im_raw": 4, "im_skel": 4, "im_struct": 4, "imag": [3, 4, 8], "image_nam": 4, "iminfo": [3, 4, 5, 6, 7], "includ": 4, "index": 9, "indic": 4, "init_ui": [2, 9], "initi": [3, 7], "initialize_fold": [2, 9], "initialize_single_fil": [2, 9], "instanc": 4, "int": [4, 7], "intens": 4, "inter_vari": 8, "interpol": 4, "interpolate_all_backward": [3, 7], "interpolate_all_forward": [3, 7], "interpolate_coord": [3, 7], "iter": 4, "label": [2, 3, 4], "label_branch": 4, "label_compon": 4, "label_im_path": 7, "label_num": 7, "labeltrack": [3, 7], "layer": 9, "length": 4, "level": 4, "like": 4, "limit": 4, "lin_vel": 4, "linear": 4, "list": 4, "list_of_idx": 4, "load": 4, "load_metadata": [3, 5], "main": [0, 2, 4], "major": 4, "marker": [3, 6], "mask": [4, 6], "match_voxel": [3, 7], "matrix": 8, "max_distance_um": 7, "max_radius_um": 6, "memmap": 4, "memori": 4, "messag": 4, "metadata": 4, "metric": 4, "min_radius_um": 6, "min_track_num": 7, "minor": 4, "mocap_mark": [2, 3], "modul": [0, 2], "motil": 4, "name": 4, "napari": 9, "napari_view": 9, "nbin": 8, "ndarrai": 4, "nelli": 9, "nellie_analysi": 2, "nellie_fileselect": 2, "nellie_hom": 2, "nellie_load": 2, "nellie_napari": [0, 2], "nellie_processor": 2, "nellie_set": 2, "nellie_visu": 2, "nellieanalysi": [2, 9], "nelliefileselect": [2, 9], "nellieload": [2, 9], "nellieprocessor": [2, 9], "nellievisu": [2, 9], "network": [2, 3], "new_ax": 5, "new_siz": 5, "node": [3, 4], "node_dim0_lim": 4, "node_dim1_lim": 4, "node_dim2_lim": 4, "node_label": 4, "node_thick": 4, "node_voxel_idx": 4, "none": [3, 4, 5, 6, 7, 8, 9], "num_sigma": 6, "num_t": [3, 4, 6, 7, 8], "number": 4, "numpi": 4, "object": [4, 5, 6, 7], "om": 5, "on_attr_select": [2, 9], "on_chang": [2, 9], "on_confirm": [2, 9], "on_hist_chang": [2, 9], "on_level_select": [2, 9], "on_log_scal": [2, 9], "on_preview": [2, 9], "on_process": [2, 9], "on_t_chang": [2, 9], "on_tab_chang": [2, 9], "on_track_select": [2, 9], "open_directori": [2, 9], "open_mocap_imag": [2, 9], "open_preprocess_imag": [2, 9], "open_raw": [2, 9], "open_reassign_imag": [2, 9], "open_segment_imag": [2, 9], "option": 4, "organelle_area": 4, "organelle_axis_length_maj": 4, "organelle_axis_length_min": 4, "organelle_ext": 4, "organelle_solid": 4, "otsu_effect": [3, 8], "otsu_thresh_intens": [3, 6], "otsu_threshold": [3, 8], "output": 4, "output_dir": [3, 5], "output_path": 5, "over": 4, "overlai": [2, 9], "packag": [0, 2], "parent": 9, "pathwai": 4, "pipeline_path": 5, "pixel": 4, "plot_data": [2, 9], "post_init": [2, 9], "preprocess": 4, "presenc": 4, "process": 4, "process_directori": [2, 3], "process_fil": [2, 3], "qtabwidget": 9, "qwidget": 9, "r": 5, "ratio": 4, "raw": 4, "re": 4, "read_fil": [3, 5], "read_mod": 5, "reassign": 4, "reassigned_label": 4, "relat": 4, "remove_edg": [3, 6], "remove_intermedi": [3, 5], "reset": [2, 9], "reset_statu": [2, 9], "restructuredtext": 0, "result": 4, "return_memmap": 5, "rewrite_dropdown": [2, 9], "run": [2, 4, 6, 7], "run_feature_export": [2, 9], "run_mocap": [2, 9], "run_nelli": [2, 9], "run_preprocess": [2, 9], "run_reassign": [2, 9], "run_segment": [2, 9], "run_track": [2, 9], "save": 4, "save_graph": [2, 9], "save_ome_tiff": [3, 5], "screenshot": [2, 9], "see": 0, "segment": [2, 3], "select_filepath": [2, 9], "select_fold": [2, 9], "select_temporal_rang": [3, 5], "set": [2, 9], "set_default_dropdown": [2, 9], "set_scal": [2, 9], "set_statu": [2, 9], "set_ui": [2, 9], "skeleton": 4, "skip": 4, "skip_coord": 7, "skip_nod": 4, "snr_clean": 6, "solid": 4, "sourc": [1, 3, 4, 5, 6, 7, 8, 9], "space": 4, "start": 5, "start_fram": 7, "start_t": 7, "state": 9, "statist": 4, "stats_to_aggreg": 4, "statu": 4, "store": 4, "str": [5, 7], "structur": 4, "submodul": [2, 10], "subpackag": 2, "substr": 3, "syntax": 0, "t": [4, 7], "t_slice": 8, "test": [0, 2], "test_frangi_filt": [2, 10], "test_im_info": [2, 10], "text": 9, "thick": 4, "threshold": [3, 6], "tif": 5, "time": 4, "titl": 9, "to_append": 4, "toggle_match_t": [2, 9], "toggle_mean_m": [2, 9], "torch_xp": [2, 3], "tortuos": 4, "track": [2, 3], "track_al": [2, 9], "transform": 4, "triangle_threshold": [3, 8], "true": [4, 5, 6, 7], "tupl": 4, "turn_off_button": [2, 9], "turn_off_pipelin": [2, 9], "type": 4, "unit": [2, 10], "updat": 4, "update_statu": [2, 9], "us": [0, 4], "use_im": 6, "util": [2, 3], "validate_path": [2, 9], "valu": 4, "vec01": 4, "vec12": 4, "vector": 4, "veloc": 4, "verger": 4, "verifi": [2, 3], "viewer": [4, 6, 7, 9], "vox_next": 7, "vox_prev": 7, "voxel": [3, 4], "voxel_idx": 4, "voxel_reassign": [2, 3], "voxelreassign": [3, 7], "whether": 4, "which": 4, "x": 4, "y": 4, "your": 0, "z": 4}, "titles": ["Nellie documentation", "main module", "nellie", "nellie package", "nellie.feature_extraction package", "nellie.im_info package", "nellie.segmentation package", "nellie.tracking package", "nellie.utils package", "nellie_napari package", "tests package", "tests.unit package"], "titleterms": {"all_tracks_for_label": 7, "attribut": 4, "base_logg": 8, "cli": 3, "content": [0, 3, 4, 5, 6, 7, 8, 9, 10, 11], "document": 0, "feature_extract": 4, "filter": 6, "flow_interpol": 7, "gener": 8, "gpu_funct": 8, "hierarch": 4, "hu_track": 7, "im_info": 5, "label": 6, "main": 1, "mocap_mark": 6, "modul": [1, 3, 4, 5, 6, 7, 8, 9, 10, 11], "nelli": [0, 2, 3, 4, 5, 6, 7, 8], "nellie_analysi": 9, "nellie_fileselect": 9, "nellie_hom": 9, "nellie_load": 9, "nellie_napari": 9, "nellie_processor": 9, "nellie_set": 9, "nellie_visu": 9, "network": 6, "packag": [3, 4, 5, 6, 7, 8, 9, 10, 11], "paramet": 4, "return": 4, "run": 3, "segment": 6, "submodul": [3, 4, 5, 6, 7, 8, 9, 11], "subpackag": [3, 10], "test": [10, 11], "test_frangi_filt": 11, "test_im_info": 11, "torch_xp": 8, "track": 7, "unit": 11, "util": 8, "verifi": 5, "voxel_reassign": 7}}) \ No newline at end of file diff --git a/docs/_build/html/tests.html b/docs/_build/html/tests.html new file mode 100644 index 0000000..6ce7cc7 --- /dev/null +++ b/docs/_build/html/tests.html @@ -0,0 +1,133 @@ + + + + + + + + tests package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

tests package¶

+
+

Subpackages¶

+ +
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/_build/html/tests.unit.html b/docs/_build/html/tests.unit.html new file mode 100644 index 0000000..c9a403b --- /dev/null +++ b/docs/_build/html/tests.unit.html @@ -0,0 +1,128 @@ + + + + + + + + tests.unit package — Nellie 0.3.2 documentation + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

tests.unit package¶

+
+

Submodules¶

+
+
+

tests.unit.test_frangi_filter module¶

+
+
+

tests.unit.test_im_info module¶

+
+
+

Module contents¶

+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..b441d68 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,32 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +project = 'Nellie' +copyright = '2024, Austin E. Y. T. Lefebvre' +author = 'Austin E. Y. T. Lefebvre' +release = '0.3.2' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.viewcode"] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..f97d23f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +.. Nellie documentation master file, created by + sphinx-quickstart on Wed Oct 2 19:26:20 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Nellie documentation +==================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules \ No newline at end of file diff --git a/docs/main.rst b/docs/main.rst new file mode 100644 index 0000000..eace87b --- /dev/null +++ b/docs/main.rst @@ -0,0 +1,7 @@ +main module +=========== + +.. automodule:: main + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..d954396 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,10 @@ +nellie +====== + +.. toctree:: + :maxdepth: 4 + + main + nellie + nellie_napari + tests diff --git a/docs/nellie.feature_extraction.rst b/docs/nellie.feature_extraction.rst new file mode 100644 index 0000000..617e89e --- /dev/null +++ b/docs/nellie.feature_extraction.rst @@ -0,0 +1,21 @@ +nellie.feature\_extraction package +================================== + +Submodules +---------- + +nellie.feature\_extraction.hierarchical module +---------------------------------------------- + +.. automodule:: nellie.feature_extraction.hierarchical + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.feature_extraction + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie.im_info.rst b/docs/nellie.im_info.rst new file mode 100644 index 0000000..0c61970 --- /dev/null +++ b/docs/nellie.im_info.rst @@ -0,0 +1,21 @@ +nellie.im\_info package +======================= + +Submodules +---------- + +nellie.im\_info.verifier module +------------------------------- + +.. automodule:: nellie.im_info.verifier + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.im_info + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie.rst b/docs/nellie.rst new file mode 100644 index 0000000..ca4d672 --- /dev/null +++ b/docs/nellie.rst @@ -0,0 +1,41 @@ +nellie package +============== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + nellie.feature_extraction + nellie.im_info + nellie.segmentation + nellie.tracking + nellie.utils + +Submodules +---------- + +nellie.cli module +----------------- + +.. automodule:: nellie.cli + :members: + :undoc-members: + :show-inheritance: + +nellie.run module +----------------- + +.. automodule:: nellie.run + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie.segmentation.rst b/docs/nellie.segmentation.rst new file mode 100644 index 0000000..bf8451b --- /dev/null +++ b/docs/nellie.segmentation.rst @@ -0,0 +1,45 @@ +nellie.segmentation package +=========================== + +Submodules +---------- + +nellie.segmentation.filtering module +------------------------------------ + +.. automodule:: nellie.segmentation.filtering + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.labelling module +------------------------------------ + +.. automodule:: nellie.segmentation.labelling + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.mocap\_marking module +----------------------------------------- + +.. automodule:: nellie.segmentation.mocap_marking + :members: + :undoc-members: + :show-inheritance: + +nellie.segmentation.networking module +------------------------------------- + +.. automodule:: nellie.segmentation.networking + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.segmentation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie.tracking.rst b/docs/nellie.tracking.rst new file mode 100644 index 0000000..0fe9b3c --- /dev/null +++ b/docs/nellie.tracking.rst @@ -0,0 +1,45 @@ +nellie.tracking package +======================= + +Submodules +---------- + +nellie.tracking.all\_tracks\_for\_label module +---------------------------------------------- + +.. automodule:: nellie.tracking.all_tracks_for_label + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.flow\_interpolation module +------------------------------------------ + +.. automodule:: nellie.tracking.flow_interpolation + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.hu\_tracking module +----------------------------------- + +.. automodule:: nellie.tracking.hu_tracking + :members: + :undoc-members: + :show-inheritance: + +nellie.tracking.voxel\_reassignment module +------------------------------------------ + +.. automodule:: nellie.tracking.voxel_reassignment + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.tracking + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie.utils.rst b/docs/nellie.utils.rst new file mode 100644 index 0000000..1d4c733 --- /dev/null +++ b/docs/nellie.utils.rst @@ -0,0 +1,45 @@ +nellie.utils package +==================== + +Submodules +---------- + +nellie.utils.base\_logger module +-------------------------------- + +.. automodule:: nellie.utils.base_logger + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.general module +--------------------------- + +.. automodule:: nellie.utils.general + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.gpu\_functions module +---------------------------------- + +.. automodule:: nellie.utils.gpu_functions + :members: + :undoc-members: + :show-inheritance: + +nellie.utils.torch\_xp module +----------------------------- + +.. automodule:: nellie.utils.torch_xp + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/nellie_napari.rst b/docs/nellie_napari.rst new file mode 100644 index 0000000..8604bab --- /dev/null +++ b/docs/nellie_napari.rst @@ -0,0 +1,69 @@ +nellie\_napari package +====================== + +Submodules +---------- + +nellie\_napari.nellie\_analysis module +-------------------------------------- + +.. automodule:: nellie_napari.nellie_analysis + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_fileselect module +---------------------------------------- + +.. automodule:: nellie_napari.nellie_fileselect + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_home module +---------------------------------- + +.. automodule:: nellie_napari.nellie_home + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_loader module +------------------------------------ + +.. automodule:: nellie_napari.nellie_loader + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_processor module +--------------------------------------- + +.. automodule:: nellie_napari.nellie_processor + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_settings module +-------------------------------------- + +.. automodule:: nellie_napari.nellie_settings + :members: + :undoc-members: + :show-inheritance: + +nellie\_napari.nellie\_visualizer module +---------------------------------------- + +.. automodule:: nellie_napari.nellie_visualizer + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: nellie_napari + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/tests.rst b/docs/tests.rst new file mode 100644 index 0000000..49fc745 --- /dev/null +++ b/docs/tests.rst @@ -0,0 +1,18 @@ +tests package +============= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + tests.unit + +Module contents +--------------- + +.. automodule:: tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/tests.unit.rst b/docs/tests.unit.rst new file mode 100644 index 0000000..673b4ae --- /dev/null +++ b/docs/tests.unit.rst @@ -0,0 +1,29 @@ +tests.unit package +================== + +Submodules +---------- + +tests.unit.test\_frangi\_filter module +-------------------------------------- + +.. automodule:: tests.unit.test_frangi_filter + :members: + :undoc-members: + :show-inheritance: + +tests.unit.test\_im\_info module +-------------------------------- + +.. automodule:: tests.unit.test_im_info + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: tests.unit + :members: + :undoc-members: + :show-inheritance: diff --git a/nellie/.DS_Store b/nellie/.DS_Store index 5008ddf..1be10f7 100644 Binary files a/nellie/.DS_Store and b/nellie/.DS_Store differ diff --git a/nellie/feature_extraction/hierarchical.py b/nellie/feature_extraction/hierarchical.py index 1864f6f..d538d94 100644 --- a/nellie/feature_extraction/hierarchical.py +++ b/nellie/feature_extraction/hierarchical.py @@ -13,6 +13,53 @@ class Hierarchy: + """ + A class to handle the hierarchical structure of image data, including voxel, node, branch, and component features. + + Parameters + ---------- + im_info : ImInfo + Object containing metadata and pathways related to the image. + skip_nodes : bool, optional + Whether to skip node processing (default is True). + viewer : optional + Viewer for updating status messages (default is None). + + Attributes + ---------- + im_info : ImInfo + The ImInfo object containing the image metadata. + num_t : int + Number of time frames in the image. + spacing : tuple + Spacing between dimensions (Z, Y, X) or (Y, X) depending on the presence of Z. + im_raw : memmap + Raw image data loaded from disk. + im_struct : memmap + Preprocessed structural image data. + im_distance : memmap + Distance-transformed image data. + im_skel : memmap + Skeletonized image data. + label_components : memmap + Instance-labeled image data of components. + label_branches : memmap + Re-labeled skeleton data of branches. + im_border_mask : memmap + Image data with border mask. + im_pixel_class : memmap + Image data classified by pixel types. + im_obj_reassigned : memmap + Object reassigned labels across time. + im_branch_reassigned : memmap + Branch reassigned labels across time. + flow_interpolator_fw : FlowInterpolator + Forward flow interpolator. + flow_interpolator_bw : FlowInterpolator + Backward flow interpolator. + viewer : optional + Viewer to display status updates. + """ def __init__(self, im_info: ImInfo, skip_nodes=True, viewer=None): self.im_info = im_info @@ -50,6 +97,14 @@ def __init__(self, im_info: ImInfo, skip_nodes=True, self.viewer = viewer def _get_t(self): + """ + Retrieves the number of time frames from image metadata, raising an error if the information is insufficient. + + Returns + ------- + int + Number of time frames. + """ if self.num_t is None and not self.im_info.no_t: # if self.im_info.no_t: # raise ValueError("No time dimension in image.") @@ -59,6 +114,10 @@ def _get_t(self): return self.num_t def _allocate_memory(self): + """ + Loads the required image data into memory using memory-mapped arrays. This includes raw image data, structural + data, skeletons, component labels, and other features related to the image pipeline. + """ # getting reshaped image will load the image into memory.. should probably do this case by case self.im_raw = self.im_info.get_memmap(self.im_info.im_path) self.im_struct = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) @@ -89,6 +148,10 @@ def _allocate_memory(self): # self.im_info.shape = self.shape def _get_hierarchies(self): + """ + Executes the hierarchical feature extraction process, which includes running voxel, node, branch, component, + and image analyses. + """ self.voxels = Voxels(self) logger.info("Running voxel analysis") start = time.time() @@ -131,6 +194,10 @@ def _get_hierarchies(self): logger.debug(f"Image analysis took {i_time} seconds") def _save_dfs(self): + """ + Saves the extracted features to CSV files, including voxel, node, branch, component, and image features. + """ + if self.viewer is not None: self.viewer.status = f'Saving features to csv files.' voxel_features, voxel_headers = create_feature_array(self.voxels) @@ -155,6 +222,9 @@ def _save_dfs(self): image_df.to_csv(self.im_info.pipeline_paths['features_image'], index=True) def _save_adjacency_maps(self): + """ + Constructs adjacency maps for voxels, nodes, branches, and components and saves them as a pickle file. + """ # edge list: v_n = [] v_b = [] @@ -243,6 +313,10 @@ def _save_adjacency_maps(self): pickle.dump(edges, f) def run(self): + """ + Main function to run the entire hierarchical feature extraction process, which includes memory allocation, + hierarchical analysis, and saving the results. + """ self._get_t() self._allocate_memory() self._get_hierarchies() @@ -255,6 +329,21 @@ def run(self): def append_to_array(to_append): + """ + Converts feature dictionaries into lists of arrays and headers for saving to a CSV. + + Parameters + ---------- + to_append : dict + Dictionary containing feature names and values to append. + + Returns + ------- + list + List of feature arrays. + list + List of corresponding feature headers. + """ new_array = [] new_headers = [] for feature, stats in to_append.items(): @@ -274,6 +363,23 @@ def append_to_array(to_append): def create_feature_array(level, labels=None): + """ + Creates a 2D feature array and corresponding headers for saving features to CSV. + + Parameters + ---------- + level : object + The level (e.g., voxel, node, branch, etc.) from which features are extracted. + labels : array-like, optional + Array of labels to use for the first column of the output (default is None). + + Returns + ------- + numpy.ndarray + 2D array of features. + list + List of corresponding feature headers. + """ full_array = None headers = None all_attr = [] @@ -321,6 +427,55 @@ def create_feature_array(level, labels=None): class Voxels: + """ + A class to extract and store voxel-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted voxel features. + coords : list + List of voxel coordinates. + intensity : list + List of voxel intensity values. + structure : list + List of voxel structural values. + vec01 : list + List of vectors from frame t-1 to t. + vec12 : list + List of vectors from frame t to t+1. + lin_vel : list + List of linear velocity vectors. + ang_vel : list + List of angular velocity vectors. + directionality_rel : list + List of directionality features. + node_labels : list + List of node labels assigned to voxels. + branch_labels : list + List of branch labels assigned to voxels. + component_labels : list + List of component labels assigned to voxels. + node_dim0_lims : list + List of node bounding box limits in dimension 0 (Z or Y). + node_dim1_lims : list + List of node bounding box limits in dimension 1 (Y or X). + node_dim2_lims : list + List of node bounding box limits in dimension 2 (X). + node_voxel_idxs : list + List of voxel indices associated with each node. + stats_to_aggregate : list + List of statistics to aggregate for features. + features_to_save : list + List of voxel features to save. + """ def __init__(self, hierarchy: Hierarchy): self.hierarchy = hierarchy @@ -404,6 +559,16 @@ def __init__(self, hierarchy: Hierarchy): self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] def _get_node_info(self, t, frame_coords): + """ + Gathers node-related information for each frame, including pixel classes, skeleton radii, and bounding boxes. + + Parameters + ---------- + t : int + The time frame index. + frame_coords : array-like + The coordinates of the voxels in the current frame. + """ # get all network pixels skeleton_pixels = np.argwhere(self.hierarchy.im_pixel_class[t] > 0) skeleton_radius = self.hierarchy.im_distance[t][tuple(skeleton_pixels.T)] @@ -481,6 +646,21 @@ def _get_node_info(self, t, frame_coords): self.node_voxel_idxs.append(chunk_node_voxel_idxs) def _get_min_euc_dist(self, t, vec): + """ + Calculates the minimum Euclidean distance for voxels to their nearest branch. + + Parameters + ---------- + t : int + The time frame index. + vec : array-like + The vector field for the current time frame. + + Returns + ------- + pandas.Series + The indices of the minimum distance for each branch. + """ euc_dist = np.linalg.norm(vec, axis=1) branch_labels = self.branch_labels[t] @@ -495,6 +675,25 @@ def _get_min_euc_dist(self, t, vec): return idxmin def _get_ref_coords(self, coords_a, coords_b, idxmin, t): + """ + Retrieves the reference coordinates for calculating relative velocity and acceleration. + + Parameters + ---------- + coords_a : array-like + The coordinates in frame A. + coords_b : array-like + The coordinates in frame B. + idxmin : pandas.Series + Indices of minimum Euclidean distances. + t : int + The time frame index. + + Returns + ------- + tuple + The reference coordinates for frames A and B. + """ vals_a = idxmin[self.branch_labels[t]].values vals_a_no_nan = vals_a.copy() vals_a_no_nan[np.isnan(vals_a_no_nan)] = 0 @@ -513,6 +712,17 @@ def _get_ref_coords(self, coords_a, coords_b, idxmin, t): return ref_a, ref_b def _get_motility_stats(self, t, coords_1_px): + """ + Computes motility-related features for each voxel, including linear and angular velocities, accelerations, + and directionality. + + Parameters + ---------- + t : int + The time frame index. + coords_1_px : array-like + Coordinates of the voxels in pixel space for frame t. + """ coords_1_px = coords_1_px.astype('float32') if self.hierarchy.im_info.no_z: dims = 2 @@ -709,6 +919,21 @@ def _get_motility_stats(self, t, coords_1_px): # self.directionality_acc_rel.append(directionality_acc_rel) def _get_linear_velocity(self, ra, rb): + """ + Computes the linear velocity, its magnitude, and orientation between two sets of coordinates. + + Parameters + ---------- + ra : array-like + Coordinates in the earlier frame (frame t-1 or t). + rb : array-like + Coordinates in the later frame (frame t or t+1). + + Returns + ------- + tuple + Tuple containing linear velocity vectors, magnitudes, and orientations. + """ lin_disp = rb - ra lin_vel = lin_disp / self.hierarchy.im_info.dim_res['T'] lin_vel_mag = np.linalg.norm(lin_vel, axis=1) @@ -721,6 +946,22 @@ def _get_linear_velocity(self, ra, rb): return lin_vel, lin_vel_mag, lin_vel_orient def _get_angular_velocity_2d(self, ra, rb): + """ + Computes the angular velocity, its magnitude, and orientation between two sets of coordinates. + Uses either 2D or 3D calculations depending on the image dimensionality. + + Parameters + ---------- + ra : array-like + Coordinates in the earlier frame (frame t-1 or t). + rb : array-like + Coordinates in the later frame (frame t or t+1). + + Returns + ------- + tuple + Tuple containing angular velocity vectors, magnitudes, and orientations. + """ # calculate angles of ra and rb relative to x-axis theta_a = np.arctan2(ra[:, 1], ra[:, 0]) theta_b = np.arctan2(rb[:, 1], rb[:, 0]) @@ -798,6 +1039,10 @@ def _run_frame(self, t=None): self._get_motility_stats(t, frame_coords) def run(self): + """ + Main function to run the extraction of voxel features over all time frames. + Iterates over each frame to extract coordinates, intensity, structural features, and motility statistics. + """ if self.hierarchy.num_t is None: self.hierarchy.num_t = 1 for t in range(self.hierarchy.num_t): @@ -807,6 +1052,26 @@ def run(self): def aggregate_stats_for_class(child_class, t, list_of_idxs): + """ + Aggregates statistical metrics (mean, standard deviation, min, max, sum) for features of a given class at + a specific time frame. + + Parameters + ---------- + child_class : object + The class from which the feature data is extracted. It should contain a list of statistics to aggregate + (i.e., `stats_to_aggregate`) and corresponding feature arrays. + t : int + The time frame index for which the aggregation is performed. + list_of_idxs : list of lists + A list where each sublist contains the indices of data points that should be grouped together for aggregation. + + Returns + ------- + dict + A dictionary where the keys are feature names and the values are dictionaries containing aggregated + statistics for each feature (mean, standard deviation, min, max, sum). + """ # initialize a dictionary to hold lists of aggregated stats for each stat name # aggregate_stats = { # stat_name: {"mean": [], "std_dev": [], "25%": [], "50%": [], "75%": [], "min": [], "max": [], "range": [], @@ -865,6 +1130,47 @@ def aggregate_stats_for_class(child_class, t, list_of_idxs): class Nodes: + """ + A class to extract and store node-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted node features. + nodes : list + List of node coordinates for each frame. + z, x, y : list + List of node coordinates in 3D or 2D space. + node_thickness : list + List of node thickness values. + divergence : list + List of divergence values for nodes. + convergence : list + List of convergence values for nodes. + vergere : list + List of vergere values (convergence + divergence). + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each node. + voxel_idxs : list + List of voxel indices associated with each node. + branch_label : list + List of branch labels assigned to nodes. + component_label : list + List of component labels assigned to nodes. + image_name : list + List of image file names. + stats_to_aggregate : list + List of statistics to aggregate for nodes. + features_to_save : list + List of node features to save. + """ def __init__(self, hierarchy): self.hierarchy = hierarchy @@ -905,10 +1211,26 @@ def __init__(self, hierarchy): self.node_x_lims = self.hierarchy.voxels.node_dim2_lims def _get_aggregate_voxel_stats(self, t): + """ + Aggregates voxel-level statistics for each node in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ frame_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, self.hierarchy.voxels.node_voxel_idxs[t]) self.aggregate_voxel_metrics.append(frame_agg) def _get_node_stats(self, t): + """ + Computes node-level statistics, including thickness, divergence, convergence, and vergere. + + Parameters + ---------- + t : int + The time frame index. + """ radius = distance_check(self.hierarchy.im_border_mask[t], self.nodes[t], self.hierarchy.spacing) self.node_thickness.append(radius * 2) @@ -988,6 +1310,14 @@ def _get_node_stats(self, t): self.x.append(x) def _run_frame(self, t): + """ + Extracts node features for a single time frame, including voxel metrics, node coordinates, and node statistics. + + Parameters + ---------- + t : int + The time frame index. + """ frame_skel_coords = np.argwhere(self.hierarchy.im_pixel_class[t] > 0) self.nodes.append(frame_skel_coords) @@ -1007,6 +1337,10 @@ def _run_frame(self, t): self._get_node_stats(t) def run(self): + """ + Main function to run the extraction of node features over all time frames. + Iterates over each frame to extract node features and calculate metrics. + """ if self.hierarchy.skip_nodes: return for t in range(self.hierarchy.num_t): @@ -1016,6 +1350,23 @@ def run(self): def distance_check(border_mask, check_coords, spacing): + """ + Calculates the minimum distance between given coordinates and a border mask using a KD-tree. + + Parameters + ---------- + border_mask : numpy.ndarray + A binary mask where the border is marked as `True`. + check_coords : numpy.ndarray + Coordinates of the points for which distances to the border will be calculated. + spacing : tuple or list + The spacing of the image dimensions (used to scale the coordinates). + + Returns + ------- + numpy.ndarray + An array of distances from each point in `check_coords` to the nearest border point. + """ border_coords = np.argwhere(border_mask) * spacing border_tree = spatial.cKDTree(border_coords) dist, _ = border_tree.query(check_coords * spacing, k=1) @@ -1023,6 +1374,51 @@ def distance_check(border_mask, check_coords, spacing): class Branches: + """ + A class to extract and store branch-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted branch features. + branch_label : list + List of branch labels for each frame. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each branch. + aggregate_node_metrics : list + List of aggregated node metrics for each branch. + z, x, y : list + List of branch centroid coordinates in 3D or 2D space. + branch_length : list + List of branch length values. + branch_thickness : list + List of branch thickness values. + branch_aspect_ratio : list + List of aspect ratios for branches. + branch_tortuosity : list + List of tortuosity values for branches. + branch_area : list + List of branch area values. + branch_axis_length_maj, branch_axis_length_min : list + List of major and minor axis lengths for branches. + branch_extent : list + List of extent values for branches. + branch_solidity : list + List of solidity values for branches. + reassigned_label : list + List of reassigned branch labels across time. + stats_to_aggregate : list + List of statistics to aggregate for branches. + features_to_save : list + List of branch features to save. + """ def __init__(self, hierarchy): self.hierarchy = hierarchy @@ -1058,6 +1454,14 @@ def __init__(self, hierarchy): self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] def _get_aggregate_stats(self, t): + """ + Aggregates voxel and node-level statistics for each branch in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ voxel_labels = self.hierarchy.voxels.branch_labels[t] grouped_vox_idxs = [np.argwhere(voxel_labels == label).flatten() for label in np.unique(voxel_labels) if label != 0] @@ -1072,6 +1476,14 @@ def _get_aggregate_stats(self, t): self.aggregate_node_metrics.append(node_agg) def _get_branch_stats(self, t): + """ + Computes branch-level statistics, including length, thickness, aspect ratio, tortuosity, and solidity. + + Parameters + ---------- + t : int + The time frame index. + """ branch_idx_array_1 = np.array(self.branch_idxs[t]) branch_idx_array_2 = np.array(self.branch_idxs[t])[:, None, :] dist = np.linalg.norm(branch_idx_array_1 - branch_idx_array_2, axis=-1) @@ -1200,6 +1612,15 @@ def _get_branch_stats(self, t): self.x.append(x) def _run_frame(self, t): + """ + Extracts branch features for a single time frame, including voxel and node metrics, branch coordinates, and + branch statistics. + + Parameters + ---------- + t : int + The time frame index. + """ frame_branch_idxs = np.argwhere(self.hierarchy.im_skel[t] > 0) self.branch_idxs.append(frame_branch_idxs) @@ -1234,6 +1655,10 @@ def _run_frame(self, t): self._get_branch_stats(t) def run(self): + """ + Main function to run the extraction of branch features over all time frames. + Iterates over each frame to extract branch features and calculate metrics. + """ for t in range(self.hierarchy.num_t): if self.hierarchy.viewer is not None: self.hierarchy.viewer.status = f'Extracting branch features. Frame: {t + 1} of {self.hierarchy.num_t}.' @@ -1241,6 +1666,45 @@ def run(self): class Components: + """ + A class to extract and store component-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted component features. + component_label : list + List of component labels for each frame. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for each component. + aggregate_node_metrics : list + List of aggregated node metrics for each component. + aggregate_branch_metrics : list + List of aggregated branch metrics for each component. + z, x, y : list + List of component centroid coordinates in 3D or 2D space. + organelle_area : list + List of component area values. + organelle_axis_length_maj, organelle_axis_length_min : list + List of major and minor axis lengths for components. + organelle_extent : list + List of extent values for components. + organelle_solidity : list + List of solidity values for components. + reassigned_label : list + List of reassigned component labels across time. + stats_to_aggregate : list + List of statistics to aggregate for components. + features_to_save : list + List of component features to save. + """ def __init__(self, hierarchy): self.hierarchy = hierarchy @@ -1269,6 +1733,14 @@ def __init__(self, hierarchy): self.features_to_save = self.stats_to_aggregate + ["x", "y", "z"] def _get_aggregate_stats(self, t): + """ + Aggregates voxel, node, and branch-level statistics for each component in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ voxel_labels = self.hierarchy.voxels.component_labels[t] grouped_vox_idxs = [np.argwhere(voxel_labels == label).flatten() for label in np.unique(voxel_labels) if label != 0] @@ -1289,6 +1761,14 @@ def _get_aggregate_stats(self, t): self.aggregate_branch_metrics.append(branch_agg) def _get_component_stats(self, t): + """ + Computes component-level statistics, including area, axis lengths, extent, and solidity. + + Parameters + ---------- + t : int + The time frame index. + """ regions = regionprops(self.hierarchy.label_components[t], spacing=self.hierarchy.spacing) areas = [] axis_length_maj = [] @@ -1336,6 +1816,15 @@ def _get_component_stats(self, t): self.x.append(x) def _run_frame(self, t): + """ + Extracts component features for a single time frame, including voxel, node, and branch metrics, and component + statistics. + + Parameters + ---------- + t : int + The time frame index. + """ smallest_label = int(np.min(self.hierarchy.label_components[t][self.hierarchy.label_components[t] > 0])) largest_label = int(np.max(self.hierarchy.label_components[t])) frame_component_labels = np.arange(smallest_label, largest_label + 1) @@ -1352,6 +1841,10 @@ def _run_frame(self, t): self._get_component_stats(t) def run(self): + """ + Main function to run the extraction of component features over all time frames. + Iterates over each frame to extract component features and calculate metrics. + """ for t in range(self.hierarchy.num_t): if self.hierarchy.viewer is not None: self.hierarchy.viewer.status = f'Extracting organelle features. Frame: {t + 1} of {self.hierarchy.num_t}.' @@ -1359,6 +1852,35 @@ def run(self): class Image: + """ + A class to extract and store global image-level features from hierarchical image data. + + Parameters + ---------- + hierarchy : Hierarchy + The Hierarchy object containing the image data and metadata. + + Attributes + ---------- + hierarchy : Hierarchy + The Hierarchy object. + time : list + List of time frames associated with the extracted image-level features. + image_name : list + List of image file names. + aggregate_voxel_metrics : list + List of aggregated voxel metrics for the entire image. + aggregate_node_metrics : list + List of aggregated node metrics for the entire image. + aggregate_branch_metrics : list + List of aggregated branch metrics for the entire image. + aggregate_component_metrics : list + List of aggregated component metrics for the entire image. + stats_to_aggregate : list + List of statistics to aggregate for the entire image. + features_to_save : list + List of image-level features to save. + """ def __init__(self, hierarchy): self.hierarchy = hierarchy @@ -1372,6 +1894,14 @@ def __init__(self, hierarchy): self.features_to_save = [] def _get_aggregate_stats(self, t): + """ + Aggregates voxel, node, branch, and component-level statistics for the entire image in the frame. + + Parameters + ---------- + t : int + The time frame index. + """ voxel_agg = aggregate_stats_for_class(self.hierarchy.voxels, t, [np.arange(len(self.hierarchy.voxels.coords[t]))]) self.aggregate_voxel_metrics.append(voxel_agg) @@ -1389,12 +1919,25 @@ def _get_aggregate_stats(self, t): self.aggregate_component_metrics.append(component_agg) def _run_frame(self, t): + """ + Extracts image-level features for a single time frame, including aggregated voxel, node, branch, and + component metrics. + + Parameters + ---------- + t : int + The time frame index. + """ self.time.append(t) self.image_name.append(self.hierarchy.im_info.file_info.filename_no_ext) self._get_aggregate_stats(t) def run(self): + """ + Main function to run the extraction of image-level features over all time frames. + Iterates over each frame to extract and aggregate voxel, node, branch, and component-level features. + """ for t in range(self.hierarchy.num_t): if self.hierarchy.viewer is not None: self.hierarchy.viewer.status = f'Extracting image features. Frame: {t + 1} of {self.hierarchy.num_t}.' diff --git a/nellie/im_info/verifier.py b/nellie/im_info/verifier.py index 1615c84..61efad1 100644 --- a/nellie/im_info/verifier.py +++ b/nellie/im_info/verifier.py @@ -9,7 +9,100 @@ class FileInfo: + """ + A class to handle file information, metadata extraction, and basic file operations for microscopy image files. + + Attributes + ---------- + filepath : str + Path to the input file. + metadata : dict or None + Stores the metadata extracted from the file. + metadata_type : str or None + Type of metadata detected (e.g., 'ome', 'imagej', 'nd2'). + axes : str or None + String representing the axes in the file (e.g., 'TZCYX'). + shape : tuple or None + Shape of the image file. + dim_res : dict or None + Dictionary of physical dimensions (X, Y, Z, T) resolution in microns or seconds. + input_dir : str + Directory of the input file. + basename : str + Filename with extension. + filename_no_ext : str + Filename without the extension. + extension : str + File extension (e.g., '.tiff', '.nd2'). + output_dir : str + Output directory for processed files. + nellie_necessities_dir : str + Directory for internal processing data. + ome_output_path : str or None + Path for OME TIFF output. + good_dims : bool + Whether the dimensional metadata is valid. + good_axes : bool + Whether the axes metadata is valid. + ch : int + Selected channel. + t_start : int + Start timepoint for processing. + t_end : int or None + End timepoint for processing. + dtype : type or None + Data type of the image. + + Methods + ------- + _find_tif_metadata() + Extract metadata from TIFF or OME-TIFF files. + _find_nd2_metadata() + Extract metadata from ND2 files. + find_metadata() + Detect file type and extract corresponding metadata. + _get_imagej_metadata(metadata) + Extract dimensional resolution from ImageJ metadata. + _get_ome_metadata(metadata) + Extract dimensional resolution from OME metadata. + _get_tif_tags_metadata(metadata) + Extract dimensional resolution from generic TIFF tags. + _get_nd2_metadata(metadata) + Extract dimensional resolution from ND2 metadata. + load_metadata() + Load and validate dimensional metadata based on the file type. + _check_axes() + Validate the axes metadata for correctness. + _check_dim_res() + Validate the dimensional resolution metadata for correctness. + change_axes(new_axes) + Change the axes string and revalidate the metadata. + change_dim_res(dim, new_size) + Modify the resolution of a specific dimension. + change_selected_channel(ch) + Select a different channel in the file for processing. + select_temporal_range(start=0, end=None) + Select a temporal range for processing. + _validate() + Validate the current state of axes and dimension metadata. + read_file() + Read the image file based on its type. + _get_output_path() + Generate the output file path based on the current axes, resolution, and channel. + save_ome_tiff() + Save the processed image file as an OME-TIFF file with updated metadata. + """ def __init__(self, filepath, output_dir=None): + """ + Initializes the FileInfo object and creates directories for outputs if they do not exist. + + Parameters + ---------- + filepath : str + Path to the input file. + output_dir : str, optional + Directory for saving output files. Defaults to a subdirectory within the input file's directory. + """ self.filepath = filepath self.metadata = None self.metadata_type = None @@ -39,6 +132,14 @@ def __init__(self, filepath, output_dir=None): self.dtype = None def _find_tif_metadata(self): + """ + Extracts metadata from TIFF or OME-TIFF files and updates relevant class attributes. + + Returns + ------- + tuple + Metadata and metadata type extracted from the TIFF file. + """ with tifffile.TiffFile(self.filepath) as tif: if tif.is_ome or tif.ome_metadata is not None: ome_xml = tifffile.tiffcomment(self.filepath) @@ -62,6 +163,9 @@ def _find_tif_metadata(self): return metadata, metadata_type def _find_nd2_metadata(self): + """ + Extracts metadata from ND2 files and updates relevant class attributes. + """ with nd2.ND2File(self.filepath) as nd2_file: metadata = nd2_file.metadata.channels[0] metadata.recorded_data = nd2_file.events(orient='list') @@ -71,6 +175,14 @@ def _find_nd2_metadata(self): self.shape = tuple(nd2_file.sizes.values()) def find_metadata(self): + """ + Detects file type (e.g., TIFF or ND2) and calls the appropriate metadata extraction method. + + Raises + ------ + ValueError + If the file type is not supported. + """ if self.filepath.endswith('.tiff') or self.filepath.endswith('.tif'): self._find_tif_metadata() elif self.filepath.endswith('.nd2'): @@ -79,18 +191,42 @@ def find_metadata(self): raise ValueError('File type not supported') def _get_imagej_metadata(self, metadata): + """ + Extracts dimensional resolution from ImageJ metadata and stores it in `dim_res`. + + Parameters + ---------- + metadata : dict + ImageJ metadata extracted from the file. + """ self.dim_res['X'] = metadata['physicalsizex'] if 'physicalsizex' in metadata else None self.dim_res['Y'] = metadata['physicalsizey'] if 'physicalsizey' in metadata else None self.dim_res['Z'] = metadata['spacing'] if 'spacing' in metadata else None self.dim_res['T'] = metadata['finterval'] if 'finterval' in metadata else None def _get_ome_metadata(self, metadata): + """ + Extracts dimensional resolution from OME metadata and stores it in `dim_res`. + + Parameters + ---------- + metadata : ome_types.OME + OME metadata object. + """ self.dim_res['X'] = metadata.images[0].pixels.physical_size_x self.dim_res['Y'] = metadata.images[0].pixels.physical_size_y self.dim_res['Z'] = metadata.images[0].pixels.physical_size_z self.dim_res['T'] = metadata.images[0].pixels.time_increment def _get_tif_tags_metadata(self, metadata): + """ + Extracts dimensional resolution from TIFF tag metadata and stores it in `dim_res`. + + Parameters + ---------- + metadata : dict + Dictionary of TIFF tags. + """ tag_names = {tag_value.name: tag_code for tag_code, tag_value in metadata.items()} if 'XResolution' in tag_names: @@ -111,6 +247,14 @@ def _get_tif_tags_metadata(self, metadata): self.dim_res['T'] = 1 / metadata[tag_names['FrameRate']].value[0] def _get_nd2_metadata(self, metadata): + """ + Extracts dimensional resolution from ND2 metadata and stores it in `dim_res`. + + Parameters + ---------- + metadata : dict + ND2 metadata object. + """ if 'Time [s]' in metadata.recorded_data: timestamps = metadata.recorded_data['Time [s]'] self.dim_res['T'] = timestamps[-1] / len(timestamps) @@ -119,6 +263,9 @@ def _get_nd2_metadata(self, metadata): self.dim_res['Z'] = metadata.volume.axesCalibration[2] def load_metadata(self): + """ + Loads and validates dimensional metadata based on the file type (OME, ImageJ, ND2, or generic TIFF). + """ self.dim_res = {'X': None, 'Y': None, 'Z': None, 'T': None} if self.metadata_type == 'ome': self._get_ome_metadata(self.metadata) @@ -134,6 +281,9 @@ def load_metadata(self): self._validate() def _check_axes(self): + """ + Validates the axes metadata and ensures the required axes (X, Y) are present. + """ if len(self.shape) != len(self.axes): self.change_axes('Q' * len(self.shape)) for axis in self.axes: @@ -151,6 +301,9 @@ def _check_axes(self): self.good_axes = True def _check_dim_res(self): + """ + Validates the dimensional resolution metadata, ensuring that X, Y, Z, and T dimensions have valid resolutions. + """ check_dims = ['X', 'Y', 'Z', 'T'] for dim in check_dims: if dim in self.axes and self.dim_res[dim] is None: @@ -159,6 +312,14 @@ def _check_dim_res(self): self.good_dims = True def change_axes(self, new_axes): + """ + Changes the axes string and revalidates the metadata. + + Parameters + ---------- + new_axes : str + New axes string to replace the existing one. + """ # if len(new_axes) != len(self.shape): self.good_axes = False # return @@ -167,6 +328,16 @@ def change_axes(self, new_axes): self._validate() def change_dim_res(self, dim, new_size): + """ + Modifies the resolution of a specific dimension. + + Parameters + ---------- + dim : str + Dimension to modify (e.g., 'X', 'Y', 'Z', 'T'). + new_size : float + New resolution for the specified dimension. + """ if dim not in self.dim_res: return # raise ValueError('Invalid dimension') @@ -174,6 +345,23 @@ def change_dim_res(self, dim, new_size): self._validate() def change_selected_channel(self, ch): + """ + Changes the selected channel for processing. + + Parameters + ---------- + ch : int + Index of the new channel to select. + + Raises + ------ + ValueError + If the axes or dimension metadata are invalid. + KeyError + If no channel dimension is available. + IndexError + If the selected channel index is out of range. + """ if not self.good_dims or not self.good_axes: raise ValueError('Must have both valid axes and dimensions to change channel') if 'C' not in self.axes: @@ -184,6 +372,16 @@ def change_selected_channel(self, ch): self._get_output_path() def select_temporal_range(self, start=0, end=None): + """ + Selects a temporal range for processing. + + Parameters + ---------- + start : int, optional + Start index of the temporal range. Defaults to 0. + end : int, optional + End index of the temporal range. Defaults to None, which includes all timepoints. + """ if not self.good_dims or not self.good_axes: return # raise ValueError('Must have both valid axes and dimensions to select temporal range') @@ -197,12 +395,42 @@ def select_temporal_range(self, start=0, end=None): self._get_output_path() def _validate(self): + """ + Validates the current state of the axes and dimensional metadata, then updates output paths. + + This method performs several validation steps: + 1. Calls `_check_axes()` to ensure the axes are valid. + 2. Calls `_check_dim_res()` to ensure that the dimensional resolutions are valid. + 3. Calls `select_temporal_range()` to set or update the time range if applicable. + 4. Calls `_get_output_path()` to update the output paths based on the current metadata. + + The method ensures that all aspects of the metadata (axes, dimensional resolutions, and temporal range) + are consistent and correctly applied before further processing. + + Raises + ------ + ValueError + If any aspect of the metadata is invalid or inconsistent. + """ self._check_axes() self._check_dim_res() self.select_temporal_range() self._get_output_path() def read_file(self): + """ + Reads the image file into memory, supporting TIFF and ND2 formats. + + Returns + ------- + np.ndarray + Numpy array representing the image data. + + Raises + ------ + ValueError + If the file type is unsupported. + """ if self.extension == '.nd2': data = nd2.imread(self.filepath) elif self.extension == '.tif' or self.extension == '.tiff': @@ -217,6 +445,25 @@ def read_file(self): return data def _get_output_path(self): + """ + Generates output paths for the processed image file, based on the current axes, resolutions, and selected channel. + + This method constructs a filename that incorporates the axes, the rounded dimensional resolutions (up to four + decimal places), the selected channel, and the temporal range. It also generates both user-facing and internal + processing paths. + + The generated paths include: + - `user_output_path_no_ext`: Path for the user output file (excluding file extension). + - `nellie_necessities_output_path_no_ext`: Path for internal processing output (excluding file extension). + - `ome_output_path`: Full path for the OME-TIFF output file. + + The method replaces periods in dimensional resolutions with 'p' to avoid issues with file systems. + + Notes + ----- + - Temporal range information is added if the 'T' (time) axis is present. + - If any dimensional resolution is `None`, the string 'None' is used in the filename. + """ t_text = f'-t{self.t_start}_to_{self.t_end}' if 'T' in self.axes else '' dim_texts = [] for axis in self.axes: @@ -238,6 +485,14 @@ def _get_output_path(self): self.ome_output_path = self.nellie_necessities_output_path_no_ext + '.ome.tif' def save_ome_tiff(self): + """ + Saves the processed image data as an OME-TIFF file, including updated metadata. + + Raises + ------ + ValueError + If the axes or dimensional resolution metadata is invalid. + """ if not self.good_axes or not self.good_dims: raise ValueError('Cannot save file with invalid axes or dimensions') @@ -283,7 +538,70 @@ def save_ome_tiff(self): class ImInfo: + """ + A class to manage image data and file outputs related to microscopy image processing. + + This class handles the initialization of memory-mapped image data, creation of output paths, + extraction of OME metadata, and memory allocation for various stages of an image processing pipeline. + + Attributes + ---------- + file_info : FileInfo + The FileInfo object containing metadata and file paths. + im_path : str + Path to the OME-TIFF image file. + im : np.ndarray + Memory-mapped image data loaded from the file. + screenshot_dir : str + Directory for saving screenshots of processed images. + graph_dir : str + Directory for saving graphs of processed data. + dim_res : dict + Dictionary storing the resolution of the image along the dimensions (X, Y, Z, T). + axes : str + Axes string representing the dimensions in the image (e.g., 'TZYX'). + new_axes : str + Modified axes string if additional dimensions are added. + shape : tuple + Shape of the image data. + ome_metadata : ome_types.OME + OME metadata object extracted from the image. + no_z : bool + Flag indicating if the Z dimension is absent or has a single slice. + no_t : bool + Flag indicating if the T dimension is absent or has a single timepoint. + pipeline_paths : dict + Dictionary storing output paths for different stages of the image processing pipeline. + + Methods + ------- + _check_axes_exist() + Checks if the Z and T dimensions exist and updates the flags `no_z` and `no_t` accordingly. + create_output_path(pipeline_path: str, ext: str = '.ome.tif', for_nellie=True) + Creates a file path for a specific stage of the image processing pipeline. + _create_output_paths() + Creates all necessary output paths for various stages in the image processing pipeline. + remove_intermediates() + Removes intermediate files created during the image processing pipeline, except for .csv files. + _get_ome_metadata() + Extracts OME metadata from the image and updates resolution, axes, and shape information. + get_memmap(file_path: str, read_mode: str = 'r+') + Returns a memory-mapped array for the image data from the specified file. + allocate_memory(output_path: str, dtype: str = 'float', data: np.ndarray = None, description: str = 'No description.', + return_memmap: bool = False, read_mode: str = 'r+') + Allocates memory for new image data, saves it to the specified file, and writes updated OME metadata. + """ def __init__(self, file_info: FileInfo): + """ + Initializes the ImInfo object, loading image data and setting up directories for screenshots and graphs. + + If the OME-TIFF file does not exist, it creates one by calling `save_ome_tiff()` from the FileInfo class. + + Parameters + ---------- + file_info : FileInfo + An instance of the FileInfo class, containing metadata and paths for the image file. + """ self.file_info = file_info self.im_path = file_info.ome_output_path if not os.path.exists(self.im_path): @@ -308,12 +626,34 @@ def __init__(self, file_info: FileInfo): self._create_output_paths() def _check_axes_exist(self): + """ + Checks the existence of the Z and T dimensions in the image data. + + Updates the `no_z` and `no_t` flags based on whether the Z and T axes are present and have more than one slice or timepoint. + """ if 'Z' in self.axes and self.shape[self.new_axes.index('Z')] > 1: self.no_z = False if 'T' in self.axes and self.shape[self.new_axes.index('T')] > 1: self.no_t = False def create_output_path(self, pipeline_path: str, ext: str = '.ome.tif', for_nellie=True): + """ + Creates a file path for a specific stage of the image processing pipeline. + + Parameters + ---------- + pipeline_path : str + A descriptive string representing the stage of the image processing pipeline (e.g., 'im_preprocessed'). + ext : str, optional + The file extension to use (default is '.ome.tif'). + for_nellie : bool, optional + Whether the output is for internal use by Nellie (default is True). + + Returns + ------- + str + The full file path for the given stage of the image processing pipeline. + """ if for_nellie: output_path = f'{self.file_info.nellie_necessities_output_path_no_ext}-{pipeline_path}{ext}' else: @@ -322,6 +662,12 @@ def create_output_path(self, pipeline_path: str, ext: str = '.ome.tif', for_nell return self.pipeline_paths[pipeline_path] def _create_output_paths(self): + """ + Creates all necessary output paths for different stages in the image processing pipeline. + + This method creates paths for various pipeline stages such as preprocessed images, instance labels, skeletons, + pixel classifications, flow vectors, adjacency maps, and various feature extraction results (voxels, nodes, branches, organelles, and images). + """ self.create_output_path('im_preprocessed') self.create_output_path('im_instance_label') self.create_output_path('im_skel') @@ -342,6 +688,12 @@ def _create_output_paths(self): self.create_output_path('adjacency_maps', ext='.pkl') def remove_intermediates(self): + """ + Removes intermediate files created during the image processing pipeline, except for CSV files. + + This method loops through all pipeline paths and deletes files (except .csv files) that were created during + processing. It also deletes the main image file if it exists. + """ all_pipeline_paths = [self.pipeline_paths[pipeline_path] for pipeline_path in self.pipeline_paths] for pipeline_path in all_pipeline_paths + [self.im_path]: if 'csv' in pipeline_path: @@ -350,6 +702,11 @@ def remove_intermediates(self): os.remove(pipeline_path) def _get_ome_metadata(self, ): + """ + Extracts OME metadata from the image and updates the `axes`, `new_axes`, `shape`, and `dim_res` attributes. + + If the 'T' axis is not present, it adds a new temporal axis to the image data and updates the axes accordingly. + """ with tifffile.TiffFile(self.im_path) as tif: self.axes = tif.series[0].axes self.new_axes = self.axes @@ -364,6 +721,21 @@ def _get_ome_metadata(self, ): self.dim_res['T'] = self.ome_metadata.images[0].pixels.time_increment def get_memmap(self, file_path, read_mode='r+'): + """ + Returns a memory-mapped array for the image data from the specified file. + + Parameters + ---------- + file_path : str + Path to the image file to be memory-mapped. + read_mode : str, optional + Mode for reading the memory-mapped file (default is 'r+'). + + Returns + ------- + np.ndarray + A memory-mapped numpy array representing the image data. + """ memmap = tifffile.memmap(file_path, mode=read_mode) if 'T' not in self.axes: memmap = memmap[np.newaxis, ...] @@ -371,6 +743,32 @@ def get_memmap(self, file_path, read_mode='r+'): def allocate_memory(self, output_path, dtype='float', data=None, description='No description.', return_memmap=False, read_mode='r+'): + """ + Allocates memory for new image data or writes new data to an output file. + + This method creates an empty OME-TIFF file with the specified `dtype` and shape, or writes the given `data` to the file. + It also updates the OME metadata with a description and the correct pixel type. + + Parameters + ---------- + output_path : str + Path to the output file. + dtype : str, optional + Data type for the new image (default is 'float'). + data : np.ndarray, optional + Numpy array containing image data to write (default is None, which allocates empty memory). + description : str, optional + Description for the OME metadata (default is 'No description.'). + return_memmap : bool, optional + Whether to return a memory-mapped array for the newly allocated file (default is False). + read_mode : str, optional + Mode for reading the memory-mapped file if `return_memmap` is True (default is 'r+'). + + Returns + ------- + np.ndarray, optional + A memory-mapped numpy array if `return_memmap` is set to True. + """ axes = self.axes if 'T' not in self.axes: axes = 'T' + axes diff --git a/nellie/segmentation/filtering.py b/nellie/segmentation/filtering.py index 03eae41..7726ded 100644 --- a/nellie/segmentation/filtering.py +++ b/nellie/segmentation/filtering.py @@ -10,9 +10,106 @@ class Filter: + """ + A class that applies the Frangi vesselness filter to 3D or 4D microscopy image data for vessel-like structure detection. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + z_ratio : float + Ratio of Z to X resolution for scaling Z-axis. + num_t : int + Number of timepoints in the image. + remove_edges : bool + Flag to remove edges from the processed image. + min_radius_um : float + Minimum radius of detected objects in micrometers. + max_radius_um : float + Maximum radius of detected objects in micrometers. + min_radius_px : float + Minimum radius of detected objects in pixels. + max_radius_px : float + Maximum radius of detected objects in pixels. + im_memmap : np.ndarray or None + Memory-mapped image data. + frangi_memmap : np.ndarray or None + Memory-mapped data for the Frangi-filtered image. + sigma_vec : tuple or None + Sigma vector used for Gaussian filtering. + sigmas : list or None + List of sigma values for multiscale Frangi filtering. + alpha_sq : float + Alpha squared parameter for Frangi filter's sensitivity to vesselness. + beta_sq : float + Beta squared parameter for Frangi filter's sensitivity to blobness. + frob_thresh : float or None + Threshold for Frobenius norm-based masking of the Hessian matrix. + viewer : object or None + Viewer object for displaying status during processing. + + Methods + ------- + _get_t() + Determines the number of timepoints in the image. + _allocate_memory() + Allocates memory for the Frangi-filtered image. + _get_sigma_vec(sigma) + Computes the sigma vector based on image dimensions (Z, Y, X). + _set_default_sigmas() + Sets the default sigma values for the Frangi filter. + _gauss_filter(sigma, t=None) + Applies a Gaussian filter to a single timepoint of the image. + _calculate_gamma(gauss_volume) + Calculates gamma values for vesselness thresholding using triangle and Otsu methods. + _compute_hessian(image, mask=True) + Computes the Hessian matrix of the input image and applies masking. + _get_frob_mask(hessian_matrices) + Creates a Frobenius norm mask for the Hessian matrix based on a threshold. + _compute_chunkwise_eigenvalues(hessian_matrices, chunk_size=1E6) + Computes eigenvalues of the Hessian matrix in chunks to avoid memory overflow. + _filter_hessian(eigenvalues, gamma_sq) + Applies the Frangi filter to the Hessian eigenvalues to detect vessel-like structures. + _filter_log(frame, mask) + Applies the Laplacian of Gaussian (LoG) filter to enhance vessel structures. + _run_frame(t, mask=True) + Runs the Frangi filter for a single timepoint in the image. + _mask_volume(frangi_frame) + Creates a binary mask of vessel-like structures in the image based on a threshold. + _remove_edges(frangi_frame) + Removes edges from the detected structures in the image. + _run_filter(mask=True) + Runs the Frangi filter over all timepoints in the image. + run(mask=True) + Main method to execute the Frangi filter process over the image data. + """ def __init__(self, im_info: ImInfo, num_t=None, remove_edges=False, min_radius_um=0.20, max_radius_um=1, alpha_sq=0.5, beta_sq=0.5, frob_thresh=None, viewer=None): + """ + Initializes the Filter object with image metadata and filter parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing image metadata and file paths. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + remove_edges : bool, optional + Whether to remove edges from the detected structures (default is False). + min_radius_um : float, optional + Minimum radius of detected objects in micrometers (default is 0.20). + max_radius_um : float, optional + Maximum radius of detected objects in micrometers (default is 1). + alpha_sq : float, optional + Alpha squared parameter for the Frangi filter (default is 0.5). + beta_sq : float, optional + Beta squared parameter for the Frangi filter (default is 0.5). + frob_thresh : float or None, optional + Threshold for the Frobenius norm-based mask (default is None). + viewer : object or None, optional + Viewer object for displaying status during processing (default is None). + """ self.im_info = im_info if not self.im_info.no_z: self.z_ratio = self.im_info.dim_res['Z'] / self.im_info.dim_res['X'] @@ -41,6 +138,11 @@ def __init__(self, im_info: ImInfo, self.viewer = viewer def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -50,6 +152,11 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for the Frangi-filtered image. + + This method creates memory-mapped arrays for both the original image and the Frangi-filtered image. + """ logger.debug('Allocating memory for frangi filter.') self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) self.shape = self.im_memmap.shape @@ -59,6 +166,19 @@ def _allocate_memory(self): return_memmap=True) def _get_sigma_vec(self, sigma): + """ + Generates the sigma vector for Gaussian filtering based on the image dimensions (Z, Y, X). + + Parameters + ---------- + sigma : float + The sigma value to use for Gaussian filtering. + + Returns + ------- + tuple + Sigma vector for Gaussian filtering. + """ if self.im_info.no_z: self.sigma_vec = (sigma, sigma) else: @@ -66,6 +186,9 @@ def _get_sigma_vec(self, sigma): return self.sigma_vec def _set_default_sigmas(self): + """ + Sets the default sigma values for the Frangi filter, based on the minimum and maximum radius in pixels. + """ logger.debug('Setting to sigma values.') min_sigma_step_size = 0.2 num_sigma = 5 @@ -84,6 +207,21 @@ def _set_default_sigmas(self): logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') def _gauss_filter(self, sigma, t=None): + """ + Applies a Gaussian filter to a single timepoint in the image. + + Parameters + ---------- + sigma : float + Sigma value for Gaussian filtering. + t : int or None, optional + Timepoint index to filter (default is None, for a single frame). + + Returns + ------- + np.ndarray + Gaussian-filtered volume. + """ self._get_sigma_vec(sigma) gauss_volume = xp.asarray(self.im_memmap[t, ...], dtype='double') logger.debug(f'Gaussian filtering {t=} with {self.sigma_vec=}.') @@ -93,12 +231,40 @@ def _gauss_filter(self, sigma, t=None): return gauss_volume def _calculate_gamma(self, gauss_volume): + """ + Calculates gamma values for vesselness thresholding using the triangle and Otsu methods. + + Parameters + ---------- + gauss_volume : np.ndarray + The Gaussian-filtered volume. + + Returns + ------- + float + The minimum gamma value for thresholding. + """ gamma_tri = triangle_threshold(gauss_volume[gauss_volume > 0]) gamma_otsu, _ = otsu_threshold(gauss_volume[gauss_volume > 0]) gamma = min(gamma_tri, gamma_otsu) return gamma def _compute_hessian(self, image, mask=True): + """ + Computes the Hessian matrix of the input image and applies optional masking. + + Parameters + ---------- + image : np.ndarray + The input image. + mask : bool, optional + Whether to apply Frobenius norm masking (default is True). + + Returns + ------- + tuple + Mask and Hessian matrix. + """ gradients = xp.gradient(image) axes = range(image.ndim) h_elems = xp.array([xp.gradient(gradients[ax0], axis=ax1).astype('float16') @@ -133,6 +299,19 @@ def _compute_hessian(self, image, mask=True): return h_mask, hessian_matrices def _get_frob_mask(self, hessian_matrices): + """ + Creates a Frobenius norm mask for the Hessian matrix based on a threshold. + + Parameters + ---------- + hessian_matrices : np.ndarray + The Hessian matrix for which to generate a mask. + + Returns + ------- + np.ndarray + The Frobenius norm mask. + """ rescaled_hessian = hessian_matrices / xp.max(xp.abs(hessian_matrices)) frobenius_norm = xp.linalg.norm(rescaled_hessian, axis=0) frobenius_norm[xp.isinf(frobenius_norm)] = xp.max(frobenius_norm[~xp.isinf(frobenius_norm)]) @@ -150,6 +329,21 @@ def _get_frob_mask(self, hessian_matrices): return mask def _compute_chunkwise_eigenvalues(self, hessian_matrices, chunk_size=1E6): + """ + Computes eigenvalues of the Hessian matrix in chunks to avoid memory overflow. + + Parameters + ---------- + hessian_matrices : np.ndarray + Hessian matrices to compute eigenvalues for. + chunk_size : float, optional + Size of the chunks to process at a time (default is 1E6). + + Returns + ------- + np.ndarray + Array of eigenvalues. + """ chunk_size = int(chunk_size) total_voxels = len(hessian_matrices) @@ -174,6 +368,21 @@ def _compute_chunkwise_eigenvalues(self, hessian_matrices, chunk_size=1E6): return eigenvalues_flat def _filter_hessian(self, eigenvalues, gamma_sq): + """ + Applies the Frangi filter to the Hessian eigenvalues to detect vessel-like structures. + + Parameters + ---------- + eigenvalues : np.ndarray + Eigenvalues of the Hessian matrix. + gamma_sq : float + Squared gamma value for vesselness calculation. + + Returns + ------- + np.ndarray + Filtered vesselness image. + """ if self.im_info.no_z: rb_sq = (xp.abs(eigenvalues[:, 0]) / xp.abs(eigenvalues[:, 1])) ** 2 s_sq = (eigenvalues[:, 0] ** 2) + (eigenvalues[:, 1] ** 2) @@ -191,6 +400,21 @@ def _filter_hessian(self, eigenvalues, gamma_sq): return filtered_im def _filter_log(self, frame, mask): + """ + Applies the Laplacian of Gaussian (LoG) filter to enhance vessel structures. + + Parameters + ---------- + frame : np.ndarray + Input image frame to filter. + mask : np.ndarray + Mask to apply during filtering. + + Returns + ------- + np.ndarray + Filtered image. + """ lapofg = xp.zeros_like(frame, dtype='double') for i, s in enumerate(self.sigmas): sigma_vec = self._get_sigma_vec(s) @@ -204,6 +428,21 @@ def _filter_log(self, frame, mask): return lapofg_min_proj def _run_frame(self, t, mask=True): + """ + Runs the Frangi filter for a single timepoint in the image. + + Parameters + ---------- + t : int + Timepoint index. + mask : bool, optional + Whether to apply masking during processing (default is True). + + Returns + ------- + np.ndarray + Vesselness-enhanced image for the given timepoint. + """ logger.info(f'Running frangi filter on {t=}.') vesselness = xp.zeros_like(self.im_memmap[t, ...], dtype='float64') temp = xp.zeros_like(self.im_memmap[t, ...], dtype='float64') @@ -229,6 +468,19 @@ def _run_frame(self, t, mask=True): return vesselness def _mask_volume(self, frangi_frame): + """ + Creates a binary mask of vessel-like structures in the image based on a threshold. + + Parameters + ---------- + frangi_frame : np.ndarray + Image processed by the Frangi filter. + + Returns + ------- + np.ndarray + Masked image. + """ frangi_threshold = xp.percentile(frangi_frame[frangi_frame > 0], 1) frangi_mask = frangi_frame > frangi_threshold frangi_mask = ndi.binary_opening(frangi_mask) @@ -236,6 +488,19 @@ def _mask_volume(self, frangi_frame): return frangi_frame def _remove_edges(self, frangi_frame): + """ + Removes edges from the detected structures in the image. + + Parameters + ---------- + frangi_frame : np.ndarray + The Frangi-filtered image. + + Returns + ------- + np.ndarray + Image with edges removed. + """ if self.im_info.no_z: num_z = 1 else: @@ -250,6 +515,14 @@ def _remove_edges(self, frangi_frame): return frangi_frame def _run_filter(self, mask=True): + """ + Runs the Frangi filter over all timepoints in the image. + + Parameters + ---------- + mask : bool, optional + Whether to apply masking during processing (default is True). + """ for t in range(self.num_t): if self.viewer is not None: self.viewer.status = f'Preprocessing. Frame: {t + 1} of {self.num_t}.' @@ -268,6 +541,14 @@ def _run_filter(self, mask=True): self.frangi_memmap.flush() def run(self, mask=True): + """ + Main method to execute the Frangi filter process over the image data. + + Parameters + ---------- + mask : bool, optional + Whether to apply masking during processing (default is True). + """ logger.info('Running frangi filter.') self._get_t() self._allocate_memory() diff --git a/nellie/segmentation/labelling.py b/nellie/segmentation/labelling.py index 888d228..7ee98c3 100644 --- a/nellie/segmentation/labelling.py +++ b/nellie/segmentation/labelling.py @@ -5,11 +5,82 @@ class Label: + """ + A class for semantic and instance segmentation of microscopy images using thresholding and signal-to-noise ratio (SNR) techniques. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + threshold : float or None + Intensity threshold for segmenting objects. + snr_cleaning : bool + Flag to enable or disable signal-to-noise ratio (SNR) based cleaning of segmented objects. + otsu_thresh_intensity : bool + Whether to apply Otsu's thresholding method to segment objects based on intensity. + im_memmap : np.ndarray or None + Memory-mapped original image data. + frangi_memmap : np.ndarray or None + Memory-mapped Frangi-filtered image data. + max_label_num : int + Maximum label number used for segmented objects. + min_z_radius_um : float + Minimum radius for Z-axis objects based on Z resolution, used for filtering objects in the Z dimension. + semantic_mask_memmap : np.ndarray or None + Memory-mapped mask for semantic segmentation. + instance_label_memmap : np.ndarray or None + Memory-mapped mask for instance segmentation. + shape : tuple + Shape of the segmented image. + debug : dict + Debugging information for tracking segmentation steps. + viewer : object or None + Viewer object for displaying status during processing. + + Methods + ------- + _get_t() + Determines the number of timepoints to process. + _allocate_memory() + Allocates memory for the original image, Frangi-filtered image, and instance segmentation masks. + _get_labels(frame) + Generates binary labels for segmented objects in a single frame based on thresholding. + _get_subtraction_mask(original_frame, labels_frame) + Creates a mask by subtracting labeled regions from the original frame. + _get_object_snrs(original_frame, labels_frame) + Calculates the signal-to-noise ratios (SNR) of segmented objects and removes objects with low SNR. + _run_frame(t) + Runs segmentation for a single timepoint in the image. + _run_segmentation() + Runs the full segmentation process for all timepoints in the image. + run() + Main method to execute the full segmentation process over the image data. + """ def __init__(self, im_info: ImInfo, num_t=None, threshold=None, snr_cleaning=False, otsu_thresh_intensity=False, viewer=None): + """ + Initializes the Label object with image metadata and segmentation parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing metadata and paths for the image file. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + threshold : float or None, optional + Intensity threshold for segmenting objects (default is None). + snr_cleaning : bool, optional + Whether to apply SNR-based cleaning to the segmented objects (default is False). + otsu_thresh_intensity : bool, optional + Whether to apply Otsu's method for intensity thresholding (default is False). + viewer : object or None, optional + Viewer object for displaying status during processing (default is None). + """ self.im_info = im_info self.num_t = num_t if num_t is None and not self.im_info.no_t: @@ -35,6 +106,11 @@ def __init__(self, im_info: ImInfo, self.viewer = viewer def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -44,6 +120,11 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for the original image, Frangi-filtered image, and instance segmentation masks. + + This method creates memory-mapped arrays for the original image data, Frangi-filtered data, and instance label data. + """ logger.debug('Allocating memory for semantic segmentation.') self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) self.frangi_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_preprocessed']) @@ -56,6 +137,21 @@ def _allocate_memory(self): return_memmap=True) def _get_labels(self, frame): + """ + Generates binary labels for segmented objects in a single frame based on thresholding. + + Uses triangle and Otsu thresholding to generate a mask, then labels the mask using connected component labeling. + + Parameters + ---------- + frame : np.ndarray + The input frame to be segmented. + + Returns + ------- + tuple + A tuple containing the mask and the labeled objects. + """ ndim = 2 if self.im_info.no_z else 3 footprint = ndi.generate_binary_structure(ndim, 1) @@ -82,11 +178,41 @@ def _get_labels(self, frame): return mask, labels def _get_subtraction_mask(self, original_frame, labels_frame): + """ + Creates a mask by subtracting labeled regions from the original frame. + + Parameters + ---------- + original_frame : np.ndarray + The original image data. + labels_frame : np.ndarray + The labeled objects in the frame. + + Returns + ------- + np.ndarray + The subtraction mask where labeled objects are removed. + """ subtraction_mask = original_frame.copy() subtraction_mask[labels_frame > 0] = 0 return subtraction_mask def _get_object_snrs(self, original_frame, labels_frame): + """ + Calculates the signal-to-noise ratios (SNR) of segmented objects and removes objects with low SNR. + + Parameters + ---------- + original_frame : np.ndarray + The original image data. + labels_frame : np.ndarray + Labeled objects in the frame. + + Returns + ------- + np.ndarray + The labels of objects that meet the SNR threshold. + """ logger.debug('Calculating object SNRs.') subtraction_mask = self._get_subtraction_mask(original_frame, labels_frame) unique_labels = xp.unique(labels_frame) @@ -123,6 +249,21 @@ def _get_object_snrs(self, original_frame, labels_frame): return labels_frame def _run_frame(self, t): + """ + Runs segmentation for a single timepoint in the image. + + Applies thresholding and optional SNR-based cleaning to segment objects in a single timepoint. + + Parameters + ---------- + t : int + Timepoint index. + + Returns + ------- + np.ndarray + The labeled objects for the given timepoint. + """ logger.info(f'Running semantic segmentation, volume {t}/{self.num_t - 1}') original_in_mem = xp.asarray(self.im_memmap[t, ...]) frangi_in_mem = xp.asarray(self.frangi_memmap[t, ...]) @@ -142,6 +283,11 @@ def _run_frame(self, t): return labels def _run_segmentation(self): + """ + Runs the full segmentation process for all timepoints in the image. + + Segments each timepoint sequentially, applying thresholding, labeling, and optional SNR cleaning. + """ for t in range(self.num_t): if self.viewer is not None: self.viewer.status = f'Extracting organelles. Frame: {t + 1} of {self.num_t}.' @@ -156,6 +302,11 @@ def _run_segmentation(self): self.instance_label_memmap.flush() def run(self): + """ + Main method to execute the full segmentation process over the image data. + + This method allocates necessary memory, segments each timepoint, and applies labeling. + """ logger.info('Running semantic segmentation.') self._get_t() self._allocate_memory() diff --git a/nellie/segmentation/mocap_marking.py b/nellie/segmentation/mocap_marking.py index 220a70d..55f80ec 100644 --- a/nellie/segmentation/mocap_marking.py +++ b/nellie/segmentation/mocap_marking.py @@ -6,9 +6,92 @@ class Markers: + """ + A class for generating motion capture markers in microscopy images using distance transforms and peak detection. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + min_radius_um : float + Minimum radius of detected objects in micrometers. + max_radius_um : float + Maximum radius of detected objects in micrometers. + min_radius_px : float + Minimum radius of detected objects in pixels. + max_radius_px : float + Maximum radius of detected objects in pixels. + use_im : str + Specifies which image to use for peak detection ('distance' or 'frangi'). + num_sigma : int + Number of sigma steps for multi-scale filtering. + shape : tuple + Shape of the input image. + im_memmap : np.ndarray or None + Memory-mapped original image data. + im_frangi_memmap : np.ndarray or None + Memory-mapped Frangi-filtered image data. + label_memmap : np.ndarray or None + Memory-mapped label data from instance segmentation. + im_marker_memmap : np.ndarray or None + Memory-mapped output for motion capture markers. + im_distance_memmap : np.ndarray or None + Memory-mapped output for distance transform. + im_border_memmap : np.ndarray or None + Memory-mapped output for image borders. + debug : dict or None + Debugging information for tracking the marking process. + viewer : object or None + Viewer object for displaying status during processing. + + Methods + ------- + _get_sigma_vec(sigma) + Computes the sigma vector for multi-scale filtering based on image dimensions. + _set_default_sigmas() + Sets the default sigma values for multi-scale filtering. + _get_t() + Determines the number of timepoints to process. + _allocate_memory() + Allocates memory for the markers, distance transform, and border images. + _distance_im(mask) + Computes the distance transform of the binary mask and identifies border pixels. + _remove_close_peaks(coord, check_im) + Removes peaks that are too close together, keeping the brightest peak in each cluster. + _local_max_peak(use_im, mask, distance_im) + Detects local maxima in the image based on multi-scale filtering. + _run_frame(t) + Runs marker detection for a single timepoint in the image. + _run_mocap_marking() + Runs the marker detection process for all timepoints in the image. + run() + Main method to execute the motion capture marking process over the image data. + """ def __init__(self, im_info: ImInfo, num_t=None, min_radius_um=0.20, max_radius_um=1, use_im='distance', num_sigma=5, viewer=None): + """ + Initializes the Markers object with image metadata and marking parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing metadata and paths for the image file. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + min_radius_um : float, optional + Minimum radius of detected objects in micrometers (default is 0.20). + max_radius_um : float, optional + Maximum radius of detected objects in micrometers (default is 1). + use_im : str, optional + Specifies which image to use for peak detection ('distance' or 'frangi', default is 'distance'). + num_sigma : int, optional + Number of sigma steps for multi-scale filtering (default is 5). + viewer : object or None, optional + Viewer object for displaying status during processing (default is None). + """ self.im_info = im_info # if self.im_info.no_t: @@ -43,6 +126,19 @@ def __init__(self, im_info: ImInfo, num_t=None, self.viewer = viewer def _get_sigma_vec(self, sigma): + """ + Computes the sigma vector for multi-scale filtering based on image dimensions. + + Parameters + ---------- + sigma : float + The sigma value to use for filtering. + + Returns + ------- + tuple + Sigma vector for Gaussian filtering in (Z, Y, X). + """ if self.im_info.no_z: sigma_vec = (sigma, sigma) else: @@ -50,6 +146,9 @@ def _get_sigma_vec(self, sigma): return sigma_vec def _set_default_sigmas(self): + """ + Sets the default sigma values for multi-scale filtering based on the minimum and maximum radius in pixels. + """ logger.debug('Setting sigma values.') min_sigma_step_size = 0.2 @@ -63,6 +162,11 @@ def _set_default_sigmas(self): logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -72,6 +176,11 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for motion capture markers, distance transform, and border images. + + This method creates memory-mapped arrays for the instance label data, original image data, Frangi-filtered data, markers, distance transforms, and borders. + """ logger.debug('Allocating memory for mocap marking.') self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) @@ -98,6 +207,19 @@ def _allocate_memory(self): return_memmap=True) def _distance_im(self, mask): + """ + Computes the distance transform of the binary mask and identifies border pixels. + + Parameters + ---------- + mask : np.ndarray + Binary mask of segmented objects. + + Returns + ------- + tuple + A tuple containing the distance transform, border mask, and updated distance frame. + """ border_mask = ndi.binary_dilation(mask, iterations=1) ^ mask if device_type == 'cuda': @@ -119,6 +241,21 @@ def _distance_im(self, mask): return distances_im_frame, border_mask def _remove_close_peaks(self, coord, check_im): + """ + Removes peaks that are too close together, keeping only the brightest peak within a given distance. + + Parameters + ---------- + coord : np.ndarray + Coordinates of detected peaks. + check_im : np.ndarray + Intensity image used for peak sorting. + + Returns + ------- + np.ndarray + Coordinates of the remaining peaks after filtering. + """ check_im_max = ndi.maximum_filter(check_im, size=3, mode='nearest') if not self.im_info.no_z: intensities = check_im_max[coord[:, 0], coord[:, 1], coord[:, 2]] @@ -157,6 +294,23 @@ def _remove_close_peaks(self, coord, check_im): return cleaned_coords def _local_max_peak(self, use_im, mask, distance_im): + """ + Detects local maxima in the image based on multi-scale filtering. + + Parameters + ---------- + use_im : np.ndarray + Image to use for detecting peaks ('distance' or 'frangi'). + mask : np.ndarray + Binary mask of segmented objects. + distance_im : np.ndarray + Distance transform of the binary mask. + + Returns + ------- + np.ndarray + Coordinates of the detected peaks. + """ lapofg = xp.empty(((len(self.sigmas),) + use_im.shape), dtype=float) for i, s in enumerate(self.sigmas): sigma_vec = self._get_sigma_vec(s) @@ -183,6 +337,19 @@ def _local_max_peak(self, use_im, mask, distance_im): return coords_idx def _run_frame(self, t): + """ + Runs marker detection for a single timepoint in the image. + + Parameters + ---------- + t : int + Timepoint index. + + Returns + ------- + tuple + A tuple containing the detected marker image, distance transform, and border mask for the given timepoint. + """ logger.info(f'Running motion capture marking, volume {t}/{self.num_t - 1}') intensity_frame = xp.asarray(self.im_memmap[t]) mask_frame = xp.asarray(self.label_memmap[t] > 0) @@ -200,6 +367,11 @@ def _run_frame(self, t): return peak_im, distance_im, border_mask def _run_mocap_marking(self): + """ + Runs the marker detection process for all timepoints in the image. + + This method processes each timepoint sequentially and applies motion capture marking. + """ for t in range(self.num_t): if self.viewer is not None: self.viewer.status = f'Mocap marking. Frame: {t + 1} of {self.num_t}.' @@ -213,6 +385,11 @@ def _run_mocap_marking(self): self.im_border_memmap.flush() def run(self): + """ + Main method to execute the motion capture marking process over the image data. + + This method allocates memory, sets sigma values, and runs the marking process for all timepoints. + """ # if self.im_info.no_t: # return self._get_t() diff --git a/nellie/segmentation/networking.py b/nellie/segmentation/networking.py index 84aca82..d23f2c1 100644 --- a/nellie/segmentation/networking.py +++ b/nellie/segmentation/networking.py @@ -9,9 +9,105 @@ class Network: + """ + A class for analyzing and skeletonizing network-like structures in 3D or 4D microscopy images, such as cellular branches. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + min_radius_um : float + Minimum radius of detected objects in micrometers. + max_radius_um : float + Maximum radius of detected objects in micrometers. + min_radius_px : float + Minimum radius of detected objects in pixels. + max_radius_px : float + Maximum radius of detected objects in pixels. + scaling : tuple + Scaling factors for Z, Y, and X dimensions. + shape : tuple + Shape of the input image. + im_memmap : np.ndarray or None + Memory-mapped original image data. + im_frangi_memmap : np.ndarray or None + Memory-mapped Frangi-filtered image data. + label_memmap : np.ndarray or None + Memory-mapped label data from instance segmentation. + network_memmap : np.ndarray or None + Memory-mapped output for network analysis. + pixel_class_memmap : np.ndarray or None + Memory-mapped output for pixel classification. + skel_memmap : np.ndarray or None + Memory-mapped output for skeleton images. + skel_relabelled_memmap : np.ndarray or None + Memory-mapped output for relabeled skeletons. + clean_skel : bool + Whether to clean the skeletons by removing noisy parts (default is True). + sigmas : list or None + List of sigma values for multi-scale filtering. + debug : dict or None + Debugging information for tracking network analysis steps. + viewer : object or None + Viewer object for displaying status during processing. + + Methods + ------- + _remove_connected_label_pixels(skel_labels) + Removes skeleton pixels that are connected to multiple labeled regions. + _add_missing_skeleton_labels(skel_frame, label_frame, frangi_frame, thresh) + Adds missing labels to the skeleton where the intensity is highest within a labeled region. + _skeletonize(label_frame, frangi_frame) + Skeletonizes the labeled regions and cleans up the skeleton based on intensity thresholds. + _get_sigma_vec(sigma) + Computes the sigma vector for multi-scale filtering based on image dimensions. + _set_default_sigmas() + Sets the default sigma values for multi-scale filtering. + _relabel_objects(branch_skel_labels, label_frame) + Relabels skeleton pixels by propagating labels to nearby unlabeled pixels. + _local_max_peak(frame, mask) + Detects local maxima in the image using multi-scale Laplacian of Gaussian filtering. + _get_pixel_class(skel) + Classifies skeleton pixels into junctions, branches, and endpoints based on connectivity. + _get_t() + Determines the number of timepoints to process. + _allocate_memory() + Allocates memory for skeleton images, pixel classification, and relabeled skeletons. + _get_branch_skel_labels(pixel_class) + Gets the branch skeleton labels, excluding junctions and background pixels. + _run_frame(t) + Runs skeletonization and network analysis for a single timepoint. + _clean_junctions(pixel_class) + Cleans up junctions by removing closely spaced junction pixels. + _run_networking() + Runs the network analysis process for all timepoints in the image. + run() + Main method to execute the network analysis process over the image data. + """ + def __init__(self, im_info: ImInfo, num_t=None, min_radius_um=0.20, max_radius_um=1, clean_skel=None, viewer=None): + """ + Initializes the Network object with image metadata and network analysis parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing metadata and paths for the image file. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + min_radius_um : float, optional + Minimum radius of detected objects in micrometers (default is 0.20). + max_radius_um : float, optional + Maximum radius of detected objects in micrometers (default is 1). + clean_skel : bool, optional + Whether to clean the skeleton by removing noisy parts (default is None, which means True for 3D images). + viewer : object or None, optional + Viewer object for displaying status during processing (default is None). + """ self.im_info = im_info self.num_t = num_t if num_t is None and not self.im_info.no_t: @@ -51,6 +147,21 @@ def __init__(self, im_info: ImInfo, num_t=None, self.viewer = viewer def _remove_connected_label_pixels(self, skel_labels): + """ + Removes skeleton pixels that are connected to multiple labeled regions. + + This method identifies pixels that are connected to more than one labeled region in the neighborhood and removes them. + + Parameters + ---------- + skel_labels : np.ndarray + Skeletonized label data. + + Returns + ------- + np.ndarray + Cleaned skeleton data with conflicting pixels removed. + """ if device_type == 'cuda': skel_labels = skel_labels.get() @@ -99,6 +210,25 @@ def _remove_connected_label_pixels(self, skel_labels): return xp.array(skel_labels) def _add_missing_skeleton_labels(self, skel_frame, label_frame, frangi_frame, thresh): + """ + Adds missing labels to the skeleton where the intensity is highest within a labeled region. + + Parameters + ---------- + skel_frame : np.ndarray + Skeleton data. + label_frame : np.ndarray + Labeled regions in the image. + frangi_frame : np.ndarray + Frangi-filtered image. + thresh : float + Threshold value used during skeleton cleaning. + + Returns + ------- + np.ndarray + Updated skeleton with missing labels added. + """ logger.debug('Adding missing skeleton labels.') gpu_frame = xp.array(label_frame) # identify unique labels and find missing ones @@ -122,6 +252,23 @@ def _add_missing_skeleton_labels(self, skel_frame, label_frame, frangi_frame, th return skel_frame def _skeletonize(self, label_frame, frangi_frame): + """ + Skeletonizes the labeled regions and cleans up the skeleton based on intensity thresholds. + + This method applies skeletonization and optionally cleans the skeleton using intensity information. + + Parameters + ---------- + label_frame : np.ndarray + Labeled regions in the image. + frangi_frame : np.ndarray + Frangi-filtered image used for intensity-based cleaning. + + Returns + ------- + tuple + Skeleton data and the intensity threshold used for cleaning. + """ cpu_frame = np.array(label_frame) gpu_frame = xp.array(label_frame) @@ -153,6 +300,19 @@ def _skeletonize(self, label_frame, frangi_frame): return skel_labels, thresh def _get_sigma_vec(self, sigma): + """ + Computes the sigma vector for multi-scale filtering based on image dimensions. + + Parameters + ---------- + sigma : float + The sigma value to use for filtering. + + Returns + ------- + tuple + Sigma vector for Gaussian filtering in (Z, Y, X). + """ if self.im_info.no_z: sigma_vec = (sigma, sigma) else: @@ -160,6 +320,9 @@ def _get_sigma_vec(self, sigma): return sigma_vec def _set_default_sigmas(self): + """ + Sets the default sigma values for multi-scale filtering based on the minimum and maximum radius in pixels. + """ logger.debug('Setting to sigma values.') min_sigma_step_size = 0.2 num_sigma = 5 @@ -174,6 +337,23 @@ def _set_default_sigmas(self): logger.debug(f'Calculated sigma step size = {sigma_step_size_calculated}. Sigmas = {self.sigmas}') def _relabel_objects(self, branch_skel_labels, label_frame): + """ + Relabels skeleton pixels by propagating labels to nearby unlabeled pixels. + + This method uses a nearest-neighbor approach to propagate labels to unlabeled pixels in the skeleton. + + Parameters + ---------- + branch_skel_labels : np.ndarray + Branch skeleton labels. + label_frame : np.ndarray + Labeled regions in the image. + + Returns + ------- + np.ndarray + Relabeled skeleton. + """ if self.im_info.no_z: structure = xp.ones((3, 3)) else: @@ -241,6 +421,21 @@ def _relabel_objects(self, branch_skel_labels, label_frame): return relabelled_labels def _local_max_peak(self, frame, mask): + """ + Detects local maxima in the image using multi-scale Laplacian of Gaussian filtering. + + Parameters + ---------- + frame : np.ndarray + The input image. + mask : np.ndarray + Binary mask of regions to process. + + Returns + ------- + np.ndarray + Coordinates of detected local maxima. + """ lapofg = xp.empty(((len(self.sigmas),) + frame.shape), dtype=float) for i, s in enumerate(self.sigmas): sigma_vec = self._get_sigma_vec(s) @@ -263,6 +458,19 @@ def _local_max_peak(self, frame, mask): return coords_3d def _get_pixel_class(self, skel): + """ + Classifies skeleton pixels into junctions, branches, and endpoints based on connectivity. + + Parameters + ---------- + skel : np.ndarray + Skeleton data. + + Returns + ------- + np.ndarray + Pixel classification of skeleton points (junctions, branches, or endpoints). + """ skel_mask = xp.array(skel > 0).astype('uint8') if self.im_info.no_z: weights = xp.ones((3, 3)) @@ -273,6 +481,11 @@ def _get_pixel_class(self, skel): return skel_mask_sum def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -282,6 +495,11 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for skeleton images, pixel classification, and relabeled skeletons. + + This method creates memory-mapped arrays for the instance label data, skeleton, pixel classification, and relabeled skeletons. + """ logger.debug('Allocating memory for skeletonization.') self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) # , read_type='r+') self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) @@ -307,6 +525,19 @@ def _allocate_memory(self): return_memmap=True) def _get_branch_skel_labels(self, pixel_class): + """ + Gets the branch skeleton labels, excluding junctions and background pixels. + + Parameters + ---------- + pixel_class : np.ndarray + Classified skeleton pixels. + + Returns + ------- + np.ndarray + Branch skeleton labels. + """ # get the labels of the skeleton pixels that are not junctions or background non_junctions = pixel_class > 0 non_junctions = non_junctions * (pixel_class != 4) @@ -318,6 +549,19 @@ def _get_branch_skel_labels(self, pixel_class): return non_junction_labels def _run_frame(self, t): + """ + Runs skeletonization and network analysis for a single timepoint. + + Parameters + ---------- + t : int + Timepoint index. + + Returns + ------- + tuple + Branch skeleton labels, pixel classification, and relabeled skeletons. + """ logger.info(f'Running network analysis, volume {t}/{self.num_t - 1}') label_frame = self.label_memmap[t] frangi_frame = xp.array(self.im_frangi_memmap[t]) @@ -334,6 +578,21 @@ def _run_frame(self, t): return branch_skel_labels, pixel_class, branch_labels def _clean_junctions(self, pixel_class): + """ + Cleans up junctions by removing closely spaced junction pixels. + + This method uses a KD-tree to remove redundant junction points, leaving only the centroid. + + Parameters + ---------- + pixel_class : np.ndarray + Pixel classification of skeleton points. + + Returns + ------- + np.ndarray + Cleaned pixel classification with redundant junctions removed. + """ junctions = pixel_class == 4 junction_labels = skimage.measure.label(junctions) junction_objects = skimage.measure.regionprops(junction_labels) @@ -351,6 +610,11 @@ def _clean_junctions(self, pixel_class): return pixel_class def _run_networking(self): + """ + Runs the network analysis process for all timepoints in the image. + + This method processes each timepoint sequentially and applies network analysis. + """ for t in range(self.num_t): if self.viewer is not None: self.viewer.status = f'Extracting branches. Frame: {t + 1} of {self.num_t}.' diff --git a/nellie/tracking/all_tracks_for_label.py b/nellie/tracking/all_tracks_for_label.py index 36ad18f..27419b9 100644 --- a/nellie/tracking/all_tracks_for_label.py +++ b/nellie/tracking/all_tracks_for_label.py @@ -5,7 +5,42 @@ class LabelTracks: + """ + A class to track labeled objects over multiple timepoints in a microscopy image using flow interpolation. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + label_im_path : str + Path to the labeled instance image. + im_memmap : np.ndarray or None + Memory-mapped original image data. + label_memmap : np.ndarray or None + Memory-mapped labeled instance image data. + + Methods + ------- + initialize() + Initializes memory-mapped data for both the raw image and the labeled instance image. + run(label_num=None, start_frame=0, end_frame=None, min_track_num=0, skip_coords=1, max_distance_um=0.5) + Runs the tracking process for labeled objects across timepoints, both forward and backward. + """ def __init__(self, im_info: ImInfo, num_t: int = None, label_im_path: str = None): + """ + Initializes the LabelTracks class with image metadata, timepoints, and label image path. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class containing image metadata and paths. + num_t : int, optional + Number of timepoints in the image (default is None, in which case it is inferred from the image metadata). + label_im_path : str, optional + Path to the labeled instance image. If not provided, defaults to the 'im_instance_label' path in `im_info`. + """ self.im_info = im_info self.num_t = num_t if label_im_path is None: @@ -19,10 +54,41 @@ def __init__(self, im_info: ImInfo, num_t: int = None, label_im_path: str = None self.label_memmap = None def initialize(self): + """ + Initializes memory-mapped data for both the raw image and the labeled instance image. + + This method prepares the image data and the labeled data for processing, mapping them into memory. + """ self.label_memmap = self.im_info.get_memmap(self.label_im_path) self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) def run(self, label_num=None, start_frame=0, end_frame=None, min_track_num=0, skip_coords=1, max_distance_um=0.5): + """ + Runs the tracking process for labeled objects across timepoints, using flow interpolation. + + This method uses forward and backward interpolation to track objects across multiple frames, starting from a given + frame. It can also track specific labels or all labels in the image. + + Parameters + ---------- + label_num : int, optional + Label number to track. If None, all labels are tracked (default is None). + start_frame : int, optional + The starting frame from which to begin tracking (default is 0). + end_frame : int, optional + The ending frame for the tracking. If None, tracks until the last frame (default is None). + min_track_num : int, optional + Minimum track number to assign to the coordinates (default is 0). + skip_coords : int, optional + The interval at which coordinates are sampled (default is 1). + max_distance_um : float, optional + Maximum distance allowed for interpolation (in micrometers, default is 0.5). + + Returns + ------- + tuple + A list of tracks and a dictionary of track properties. + """ if end_frame is None: end_frame = self.num_t num_frames = self.label_memmap.shape[0] - 1 diff --git a/nellie/tracking/flow_interpolation.py b/nellie/tracking/flow_interpolation.py index 6849898..58eb6a7 100644 --- a/nellie/tracking/flow_interpolation.py +++ b/nellie/tracking/flow_interpolation.py @@ -6,7 +6,71 @@ class FlowInterpolator: + """ + A class for interpolating flow vectors between timepoints in microscopy images using precomputed flow data. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + max_distance_um : float + Maximum distance allowed for interpolation (in micrometers). + forward : bool + Indicates if the interpolation is performed in the forward direction (True) or backward direction (False). + scaling : tuple + Scaling factors for Z, Y, and X dimensions. + shape : tuple + Shape of the input image. + im_memmap : np.ndarray or None + Memory-mapped original image data. + flow_vector_array : np.ndarray or None + Precomputed flow vector array loaded from disk. + current_t : int or None + Cached timepoint for the current flow vector calculation. + check_rows : np.ndarray or None + Flow vector data for the current timepoint. + check_coords : np.ndarray or None + Coordinates corresponding to the flow vector data for the current timepoint. + current_tree : cKDTree or None + KDTree for fast lookup of nearby coordinates in the current timepoint. + debug : dict or None + Debugging information for tracking processing steps. + + Methods + ------- + _allocate_memory() + Allocates memory and loads the precomputed flow vector array. + _get_t() + Determines the number of timepoints to process. + _get_nearby_coords(t, coords) + Finds nearby coordinates within a defined radius from the given coordinates using a KDTree. + _get_vector_weights(nearby_idxs, distances_all) + Computes the weights for nearby flow vectors based on their distances and costs. + _get_final_vector(nearby_idxs, weights_all) + Computes the final interpolated vector for each coordinate using distance-weighted vectors. + interpolate_coord(coords, t) + Interpolates the flow vector at the given coordinates and timepoint. + _initialize() + Initializes the FlowInterpolator by allocating memory and setting the timepoints. + + """ def __init__(self, im_info: ImInfo, num_t=None, max_distance_um=0.5, forward=True): + """ + Initializes the FlowInterpolator with image metadata and interpolation parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing metadata and paths for the image file. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + max_distance_um : float, optional + Maximum distance allowed for interpolation (in micrometers, default is 0.5). + forward : bool, optional + Indicates if the interpolation is performed in the forward direction (default is True). + """ self.im_info = im_info if self.im_info.no_t: @@ -41,6 +105,11 @@ def __init__(self, im_info: ImInfo, num_t=None, max_distance_um=0.5, forward=Tru self._initialize() def _allocate_memory(self): + """ + Allocates memory and loads the precomputed flow vector array. + + This method reads the flow vector data from disk and prepares it for use during interpolation. + """ logger.debug('Allocating memory for mocap marking.') self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) @@ -50,6 +119,11 @@ def _allocate_memory(self): self.flow_vector_array = np.load(flow_vector_array_path) def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -59,6 +133,21 @@ def _get_t(self): return def _get_nearby_coords(self, t, coords): + """ + Finds nearby coordinates within a defined radius from the given coordinates using a KDTree. + + Parameters + ---------- + t : int + Timepoint index. + coords : np.ndarray + Coordinates for which to find nearby points. + + Returns + ------- + tuple + Nearby indices and distances from the input coordinates. + """ # using a ckdtree, check for any nearby coords from coord if self.current_t != t: self.current_tree = cKDTree(self.check_coords * self.scaling) @@ -90,6 +179,21 @@ def _get_nearby_coords(self, t, coords): return nearby_idxs_return, distance_return def _get_vector_weights(self, nearby_idxs, distances_all): + """ + Computes the weights for nearby flow vectors based on their distances and costs. + + Parameters + ---------- + nearby_idxs : list + Indices of nearby coordinates. + distances_all : list + Distances from the input coordinates to the nearby points. + + Returns + ------- + list + Weights for each nearby flow vector. + """ weights_all = [] for i in range(len(nearby_idxs)): # lowest cost should be most highly weighted @@ -111,6 +215,21 @@ def _get_vector_weights(self, nearby_idxs, distances_all): return weights_all def _get_final_vector(self, nearby_idxs, weights_all): + """ + Computes the final interpolated vector for each coordinate using distance-weighted vectors. + + Parameters + ---------- + nearby_idxs : list + Indices of nearby coordinates. + weights_all : list + Weights for the flow vectors. + + Returns + ------- + np.ndarray + Final interpolated vectors for each input coordinate. + """ if self.im_info.no_z: final_vectors = np.zeros((len(nearby_idxs), 2)) else: @@ -131,6 +250,21 @@ def _get_final_vector(self, nearby_idxs, weights_all): return final_vectors def interpolate_coord(self, coords, t): + """ + Interpolates the flow vector at the given coordinates and timepoint. + + Parameters + ---------- + coords : np.ndarray + Input coordinates for interpolation. + t : int + Timepoint index. + + Returns + ------- + np.ndarray + Interpolated flow vectors at the given coordinates and timepoint. + """ # interpolate the flow vector at the coordinate at time t, either forward in time or backward in time. # For forward, simply find nearby LMPs, interpolate based on distance-weighted vectors # For backward, get coords from t-1 + vector, then find nearby coords from that, and interpolate based on distance-weighted vectors @@ -163,6 +297,11 @@ def interpolate_coord(self, coords, t): return final_vectors def _initialize(self): + """ + Initializes the FlowInterpolator by allocating memory and setting the timepoints. + + This method prepares the internal state of the object, including reading the flow vector array. + """ if self.im_info.no_t: return self._get_t() @@ -170,6 +309,29 @@ def _initialize(self): def interpolate_all_forward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5): + """ + Interpolates coordinates forward in time across multiple timepoints using flow vectors. + + Parameters + ---------- + coords : np.ndarray + Array of input coordinates to track. + start_t : int + Starting timepoint. + end_t : int + Ending timepoint. + im_info : ImInfo + An instance of the ImInfo class containing image metadata and paths. + min_track_num : int, optional + Minimum track number to assign to coordinates (default is 0). + max_distance_um : float, optional + Maximum distance allowed for interpolation (in micrometers, default is 0.5). + + Returns + ------- + tuple + List of tracks and associated track properties. + """ flow_interpx = FlowInterpolator(im_info, forward=True, max_distance_um=max_distance_um) tracks = [] track_properties = {'frame_num': []} @@ -203,6 +365,29 @@ def interpolate_all_forward(coords, start_t, end_t, im_info, min_track_num=0, ma def interpolate_all_backward(coords, start_t, end_t, im_info, min_track_num=0, max_distance_um=0.5): + """ + Interpolates coordinates backward in time across multiple timepoints using flow vectors. + + Parameters + ---------- + coords : np.ndarray + Array of input coordinates to track. + start_t : int + Starting timepoint. + end_t : int + Ending timepoint. + im_info : ImInfo + An instance of the ImInfo class containing image metadata and paths. + min_track_num : int, optional + Minimum track number to assign to coordinates (default is 0). + max_distance_um : float, optional + Maximum distance allowed for interpolation (in micrometers, default is 0.5). + + Returns + ------- + tuple + List of tracks and associated track properties. + """ flow_interpx = FlowInterpolator(im_info, forward=False, max_distance_um=max_distance_um) tracks = [] track_properties = {'frame_num': []} diff --git a/nellie/tracking/hu_tracking.py b/nellie/tracking/hu_tracking.py index 88b8cf9..d954efa 100644 --- a/nellie/tracking/hu_tracking.py +++ b/nellie/tracking/hu_tracking.py @@ -6,9 +6,98 @@ class HuMomentTracking: + """ + A class for tracking objects in microscopy images using Hu moments and distance-based matching. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + max_distance_um : float + Maximum allowed movement (in micrometers) for an object between frames. + scaling : tuple + Scaling factors for Z, Y, and X dimensions. + vector_start_coords : list + List of coordinates where vectors start. + vectors : list + List of tracking vectors between timepoints. + vector_magnitudes : list + List of magnitudes of tracking vectors. + shape : tuple + Shape of the input image. + im_memmap : np.ndarray or None + Memory-mapped original image data. + im_frangi_memmap : np.ndarray or None + Memory-mapped Frangi-filtered image data. + im_distance_memmap : np.ndarray or None + Memory-mapped distance transform data. + im_marker_memmap : np.ndarray or None + Memory-mapped marker data for object tracking. + flow_vector_array_path : str or None + Path to save the flow vector array. + debug : dict or None + Debugging information for tracking processing steps. + viewer : object or None + Viewer object for displaying status during processing. + + Methods + ------- + _calculate_normalized_moments(images) + Calculates the normalized moments for a set of images. + _calculate_hu_moments(eta) + Calculates the first six Hu moments for a set of images. + _calculate_mean_and_variance(images) + Calculates the mean and variance of intensity for a set of images. + _get_im_bounds(markers, distance_frame) + Calculates the bounds of the region around each marker in the image. + _get_sub_volumes(im_frame, im_bounds, max_radius) + Extracts sub-volumes from the image within the specified bounds. + _get_orthogonal_projections(sub_volumes) + Computes the orthogonal projections of the sub-volumes along each axis. + _get_t() + Determines the number of timepoints to process. + _allocate_memory() + Allocates memory for the necessary image data. + _get_hu_moments(sub_volumes) + Calculates Hu moments for the given sub-volumes of the image. + _concatenate_hu_matrices(hu_matrices) + Concatenates multiple Hu moment matrices into a single matrix. + _get_feature_matrix(t) + Extracts the feature matrix (mean, variance, Hu moments) for a specific timepoint. + _get_distance_mask(t) + Computes the distance matrix and mask between objects in consecutive frames. + _get_difference_matrix(m1, m2) + Computes the difference matrix between two feature matrices. + _zscore_normalize(m, mask) + Z-score normalizes the values in a matrix, using a binary mask to exclude certain regions. + _get_cost_matrix(t, stats_vecs, pre_stats_vecs, hu_vecs, pre_hu_vecs) + Calculates the cost matrix for matching objects between consecutive frames. + _find_best_matches(cost_matrix) + Finds the best object matches between two frames based on the cost matrix. + _run_hu_tracking() + Runs the full tracking algorithm over all timepoints, saving the results to disk. + run() + Main method to execute the Hu moment-based tracking process over the image data. + """ def __init__(self, im_info: ImInfo, num_t=None, max_distance_um=1, viewer=None): + """ + Initializes the HuMomentTracking object with image metadata and tracking parameters. + + Parameters + ---------- + im_info : ImInfo + An instance of the ImInfo class, containing metadata and paths for the image file. + num_t : int, optional + Number of timepoints to process. If None, defaults to the number of timepoints in the image. + max_distance_um : float, optional + Maximum allowed movement (in micrometers) for an object between frames (default is 1). + viewer : object or None, optional + Viewer object for displaying status during processing (default is None). + """ self.im_info = im_info if self.im_info.no_t: @@ -43,6 +132,19 @@ def __init__(self, im_info: ImInfo, num_t=None, self.viewer = viewer def _calculate_normalized_moments(self, images): + """ + Calculates the normalized moments for a set of images. + + Parameters + ---------- + images : np.ndarray + Input image data. + + Returns + ------- + np.ndarray + Normalized moments for each image. + """ # I know the broadcasting is super confusing, but it makes it so much faster (400x)... num_images, height, width = images.shape @@ -77,6 +179,19 @@ def _calculate_normalized_moments(self, images): return eta def _calculate_hu_moments(self, eta): + """ + Calculates the first six Hu moments for a set of images. + + Parameters + ---------- + eta : np.ndarray + The normalized moments for each image. + + Returns + ------- + np.ndarray + The first six Hu moments for each image. + """ num_images = eta.shape[0] hu = xp.zeros((num_images, 6)) # initialize Hu moments for each image @@ -100,6 +215,19 @@ def _calculate_hu_moments(self, eta): return hu # return the first 5 Hu moments for each image def _calculate_mean_and_variance(self, images): + """ + Calculates the mean and variance of intensity for a set of images. + + Parameters + ---------- + images : np.ndarray + Input image data. + + Returns + ------- + np.ndarray + Array containing the mean and variance of each image. + """ num_images = images.shape[0] features = xp.zeros((num_images, 2)) mask = images != 0 @@ -120,6 +248,21 @@ def _calculate_mean_and_variance(self, images): return features def _get_im_bounds(self, markers, distance_frame): + """ + Calculates the bounds of the region around each marker in the image. + + Parameters + ---------- + markers : np.ndarray + Coordinates of markers in the image. + distance_frame : np.ndarray + Distance transform of the image. + + Returns + ------- + tuple + Boundaries for sub-volumes around each marker. + """ if not self.im_info.no_z: radii = distance_frame[markers[:, 0], markers[:, 1], markers[:, 2]] else: @@ -136,6 +279,23 @@ def _get_im_bounds(self, markers, distance_frame): return low_0, high_0, low_1, high_1 def _get_sub_volumes(self, im_frame, im_bounds, max_radius): + """ + Extracts sub-volumes from the image within the specified bounds. + + Parameters + ---------- + im_frame : np.ndarray + Image data for a single frame. + im_bounds : tuple + Bounds for extracting sub-volumes. + max_radius : int + Maximum radius for the sub-volumes. + + Returns + ------- + np.ndarray + Extracted sub-volumes from the image. + """ if self.im_info.no_z: y_low, y_high, x_low, x_high = im_bounds else: @@ -160,6 +320,19 @@ def _get_sub_volumes(self, im_frame, im_bounds, max_radius): return sub_volumes def _get_orthogonal_projections(self, sub_volumes): + """ + Computes the orthogonal projections of the sub-volumes along each axis. + + Parameters + ---------- + sub_volumes : np.ndarray + Sub-volumes of the image. + + Returns + ------- + tuple + Z, Y, and X projections of the sub-volumes. + """ # max projections along each axis z_projections = xp.max(sub_volumes, axis=1) y_projections = xp.max(sub_volumes, axis=2) @@ -168,6 +341,11 @@ def _get_orthogonal_projections(self, sub_volumes): return z_projections, y_projections, x_projections def _get_t(self): + """ + Determines the number of timepoints to process. + + If `num_t` is not set and the image contains a temporal dimension, it sets `num_t` to the number of timepoints. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -177,6 +355,11 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for the necessary image data. + + This method creates memory-mapped arrays for the original image data, Frangi-filtered data, distance transform, markers, and more. + """ logger.debug('Allocating memory for hu-based tracking.') self.label_memmap = self.im_info.get_memmap(self.im_info.pipeline_paths['im_instance_label']) self.im_memmap = self.im_info.get_memmap(self.im_info.im_path) @@ -188,6 +371,19 @@ def _allocate_memory(self): self.flow_vector_array_path = self.im_info.pipeline_paths['flow_vector_array'] def _get_hu_moments(self, sub_volumes): + """ + Calculates Hu moments for the given sub-volumes of the image. + + Parameters + ---------- + sub_volumes : np.ndarray + Sub-volumes of the image. + + Returns + ------- + np.ndarray + The Hu moments for the sub-volumes. + """ if self.im_info.no_z: etas = self._calculate_normalized_moments(sub_volumes) hu_moments = self._calculate_hu_moments(etas) @@ -203,9 +399,35 @@ def _get_hu_moments(self, sub_volumes): return hu_moments def _concatenate_hu_matrices(self, hu_matrices): + """ + Concatenates multiple Hu moment matrices into a single matrix. + + Parameters + ---------- + hu_matrices : list + List of Hu moment matrices. + + Returns + ------- + np.ndarray + Concatenated matrix of Hu moments. + """ return xp.concatenate(hu_matrices, axis=1) def _get_feature_matrix(self, t): + """ + Extracts the feature matrix (mean, variance, Hu moments) for a specific timepoint. + + Parameters + ---------- + t : int + Timepoint index. + + Returns + ------- + tuple + Feature matrix containing statistics and Hu moments for the timepoint. + """ intensity_frame = xp.array(self.im_memmap[t]) frangi_frame = xp.array(self.im_frangi_memmap[t]) frangi_frame[frangi_frame > 0] = xp.log10(frangi_frame[frangi_frame > 0]) @@ -237,6 +459,19 @@ def _get_feature_matrix(self, t): return stats_feature_matrix, log_hu_feature_matrix def _get_distance_mask(self, t): + """ + Computes the distance matrix and mask between objects in consecutive frames. + + Parameters + ---------- + t : int + Timepoint index. + + Returns + ------- + tuple + Distance matrix and mask for matching objects between frames. + """ marker_frame_pre = np.array(self.im_marker_memmap[t - 1]) > 0 marker_indices_pre = np.argwhere(marker_frame_pre) marker_indices_pre_scaled = marker_indices_pre * self.scaling @@ -250,6 +485,21 @@ def _get_distance_mask(self, t): return distance_matrix, distance_mask def _get_difference_matrix(self, m1, m2): + """ + Computes the difference matrix between two feature matrices. + + Parameters + ---------- + m1 : np.ndarray + Feature matrix for the current frame. + m2 : np.ndarray + Feature matrix for the previous frame. + + Returns + ------- + np.ndarray + Difference matrix between the two feature matrices. + """ if len(m1) == 0 or len(m2) == 0: return xp.array([]) m1_reshaped = m1[:, xp.newaxis, :].astype(xp.float16) @@ -258,6 +508,21 @@ def _get_difference_matrix(self, m1, m2): return difference_matrix def _zscore_normalize(self, m, mask): + """ + Z-score normalizes the values in a matrix, using a binary mask to exclude certain regions. + + Parameters + ---------- + m : np.ndarray + Input matrix to be normalized. + mask : np.ndarray + Binary mask indicating which values to include. + + Returns + ------- + np.ndarray + Z-score normalized matrix. + """ if len(m) == 0: return xp.array([]) depth = m.shape[2] @@ -286,6 +551,27 @@ def _zscore_normalize(self, m, mask): return m def _get_cost_matrix(self, t, stats_vecs, pre_stats_vecs, hu_vecs, pre_hu_vecs): + """ + Calculates the cost matrix for matching objects between consecutive frames. + + Parameters + ---------- + t : int + Timepoint index. + stats_vecs : np.ndarray + Feature matrix for the current frame. + pre_stats_vecs : np.ndarray + Feature matrix for the previous frame. + hu_vecs : np.ndarray + Hu moments for the current frame. + pre_hu_vecs : np.ndarray + Hu moments for the previous frame. + + Returns + ------- + np.ndarray + Cost matrix for matching objects between frames. + """ if len(stats_vecs) == 0 or len(pre_stats_vecs) == 0 or len(hu_vecs) == 0 or len(pre_hu_vecs) == 0: return xp.array([]) distance_matrix, distance_mask = self._get_distance_mask(t) @@ -306,6 +592,19 @@ def _get_cost_matrix(self, t, stats_vecs, pre_stats_vecs, hu_vecs, pre_hu_vecs): return cost_matrix def _find_best_matches(self, cost_matrix): + """ + Finds the best object matches between two frames based on the cost matrix. + + Parameters + ---------- + cost_matrix : np.ndarray + Cost matrix for matching objects. + + Returns + ------- + tuple + Best matches for rows, columns, and their corresponding costs. + """ if len(cost_matrix) == 0: return [], [], [] candidates = [] @@ -343,6 +642,11 @@ def _find_best_matches(self, cost_matrix): return row_matches, col_matches, costs def _run_hu_tracking(self): + """ + Runs the full tracking algorithm over all timepoints, saving the results to disk. + + This method processes each timepoint sequentially and tracks objects using Hu moments and distance-based matching. + """ pre_stats_vecs = None pre_hu_vecs = None flow_vector_array = None @@ -392,6 +696,11 @@ def _run_hu_tracking(self): np.save(self.flow_vector_array_path, flow_vector_array) def run(self): + """ + Main method to execute the Hu moment-based tracking process over the image data. + + This method allocates memory, extracts features, and tracks objects across timepoints. + """ if self.im_info.no_t: return self._get_t() diff --git a/nellie/tracking/voxel_reassignment.py b/nellie/tracking/voxel_reassignment.py index 2438e25..18eac7c 100644 --- a/nellie/tracking/voxel_reassignment.py +++ b/nellie/tracking/voxel_reassignment.py @@ -9,8 +9,73 @@ class VoxelReassigner: + """ + A class for voxel reassignment across time points using forward and backward flow interpolation. + + Attributes + ---------- + im_info : ImInfo + An object containing image metadata and memory-mapped image data. + num_t : int + Number of timepoints in the image. + flow_interpolator_fw : FlowInterpolator + Flow interpolator for forward timepoint matching. + flow_interpolator_bw : FlowInterpolator + Flow interpolator for backward timepoint matching. + running_matches : list + List of running matches for voxel reassignment between timepoints. + voxel_matches_path : str or None + Path to save the voxel matches array. + branch_label_memmap : np.ndarray or None + Memory-mapped data for relabeled branches. + obj_label_memmap : np.ndarray or None + Memory-mapped data for object labels. + reassigned_branch_memmap : np.ndarray or None + Memory-mapped data for reassigned branches. + reassigned_obj_memmap : np.ndarray or None + Memory-mapped data for reassigned object labels. + viewer : Any + Optional viewer (e.g., for visualization purposes). + + Methods + ------- + _match_forward(flow_interpolator, vox_prev, vox_next, t) + Matches voxels forward using flow interpolation. + _match_backward(flow_interpolator, vox_next, vox_prev, t) + Matches voxels backward using flow interpolation. + _match_voxels_to_centroids(coords_real, coords_interpx) + Matches voxels to centroids using nearest neighbor search. + _assign_unique_matches(vox_prev_matches, vox_next_matches, distances) + Assigns unique matches between timepoint voxels based on minimum distances. + _distance_threshold(vox_prev_matched, vox_next_matched) + Filters voxel matches by applying a distance threshold. + match_voxels(vox_prev, vox_next, t) + Matches voxels between two consecutive timepoints using forward and backward interpolation. + _get_t() + Gets the number of timepoints in the dataset. + _allocate_memory() + Allocates memory for voxel reassignment data, including memory-mapped arrays. + _run_frame(t, all_mask_coords, reassigned_memmap) + Runs the voxel reassignment process for a single timepoint. + _run_reassignment(label_type) + Runs the voxel reassignment process for all frames, for either branch or object labels. + run() + Main method to execute voxel reassignment for both branch and object labels. + """ def __init__(self, im_info: ImInfo, num_t=None, viewer=None): + """ + Initializes the VoxelReassigner class with image metadata and timepoints. + + Parameters + ---------- + im_info : ImInfo + Image metadata and memory-mapped data. + num_t : int, optional + Number of timepoints in the dataset. If None, it is inferred from the image metadata (default is None). + viewer : Any, optional + Optional viewer for visualization purposes (default is None). + """ self.im_info = im_info if self.im_info.no_t: @@ -35,6 +100,25 @@ def __init__(self, im_info: ImInfo, num_t=None, self.viewer = viewer def _match_forward(self, flow_interpolator, vox_prev, vox_next, t): + """ + Matches voxels forward in time using flow interpolation. + + Parameters + ---------- + flow_interpolator : FlowInterpolator + Flow interpolator for forward voxel matching. + vox_prev : np.ndarray + Voxels from the previous timepoint. + vox_next : np.ndarray + Voxels from the next timepoint. + t : int + Current timepoint index. + + Returns + ------- + tuple + Arrays of matched voxels from the previous and next timepoints and valid distances between them. + """ vectors_interpx_prev = flow_interpolator.interpolate_coord(vox_prev, t) if vectors_interpx_prev is None: return [], [], [] @@ -60,6 +144,25 @@ def _match_forward(self, flow_interpolator, vox_prev, vox_next, t): return vox_prev_matched_valid, vox_next_matched_valid, distances_valid def _match_backward(self, flow_interpolator, vox_next, vox_prev, t): + """ + Matches voxels backward in time using flow interpolation. + + Parameters + ---------- + flow_interpolator : FlowInterpolator + Flow interpolator for backward voxel matching. + vox_next : np.ndarray + Voxels from the next timepoint. + vox_prev : np.ndarray + Voxels from the previous timepoint. + t : int + Current timepoint index. + + Returns + ------- + tuple + Arrays of matched voxels from the previous and next timepoints and valid distances between them. + """ # interpolate flow vectors to all voxels in t1 from centroids derived from t0 centroids + t0 flow vectors vectors_interpx_prev = flow_interpolator.interpolate_coord(vox_next, t) if vectors_interpx_prev is None: @@ -85,6 +188,21 @@ def _match_backward(self, flow_interpolator, vox_next, vox_prev, t): return vox_prev_matched_valid, vox_next_matched_valid, distances_valid def _match_voxels_to_centroids(self, coords_real, coords_interpx): + """ + Matches real voxel coordinates to interpolated centroids using nearest neighbor search. + + Parameters + ---------- + coords_real : np.ndarray + Real voxel coordinates. + coords_interpx : np.ndarray + Interpolated centroid coordinates. + + Returns + ------- + tuple + Arrays of distances and indices of matched centroids. + """ coords_interpx = np.array(coords_interpx) * self.flow_interpolator_fw.scaling coords_real = np.array(coords_real) * self.flow_interpolator_fw.scaling tree = cKDTree(coords_real) @@ -92,6 +210,23 @@ def _match_voxels_to_centroids(self, coords_real, coords_interpx): return dist, idx def _assign_unique_matches(self, vox_prev_matches, vox_next_matches, distances): + """ + Assigns unique voxel matches based on the minimum distance criteria. + + Parameters + ---------- + vox_prev_matches : np.ndarray + Array of matched voxels from the previous timepoint. + vox_next_matches : np.ndarray + Array of matched voxels from the next timepoint. + distances : np.ndarray + Array of distances between matched voxels. + + Returns + ------- + tuple + Arrays of uniquely matched voxels for the previous and next timepoints. + """ # create a dict where the key is a voxel in t1, and the value is a list of distances and t0 voxels matched to it vox_next_dict = {} for match_idx, match_next in enumerate(vox_next_matches): @@ -137,6 +272,21 @@ def _assign_unique_matches(self, vox_prev_matches, vox_next_matches, distances): return vox_prev_matches_final, vox_next_matches_final def _distance_threshold(self, vox_prev_matched, vox_next_matched): + """ + Filters voxel matches by applying a distance threshold. + + Parameters + ---------- + vox_prev_matched : np.ndarray + Array of matched voxels from the previous timepoint. + vox_next_matched : np.ndarray + Array of matched voxels from the next timepoint. + + Returns + ------- + tuple + Arrays of valid voxel matches and corresponding distances. + """ distances = np.linalg.norm((vox_prev_matched - vox_next_matched) * self.flow_interpolator_fw.scaling, axis=1) distance_mask = distances < self.flow_interpolator_fw.max_distance_um vox_prev_matched_valid = vox_prev_matched[distance_mask] @@ -145,6 +295,23 @@ def _distance_threshold(self, vox_prev_matched, vox_next_matched): return vox_prev_matched_valid, vox_next_matched_valid, distances_valid def match_voxels(self, vox_prev, vox_next, t): + """ + Matches voxels between two consecutive timepoints using both forward and backward interpolation. + + Parameters + ---------- + vox_prev : np.ndarray + Voxels from the previous timepoint. + vox_next : np.ndarray + Voxels from the next timepoint. + t : int + Current timepoint index. + + Returns + ------- + tuple + Arrays of matched voxels from the previous and next timepoints. + """ # forward interpolation: # from t0 voxels and interpolated flow, get t1 centroids. # match nearby t1 voxels to t1 centroids, which are linked to t0 voxels. @@ -201,6 +368,9 @@ def match_voxels(self, vox_prev, vox_next, t): return np.array(vox_prev_matches_unique), np.array(vox_next_matches_unique) def _get_t(self): + """ + Gets the number of timepoints from the image metadata or sets it if not provided. + """ if self.num_t is None: if self.im_info.no_t: self.num_t = 1 @@ -210,6 +380,9 @@ def _get_t(self): return def _allocate_memory(self): + """ + Allocates memory for voxel reassignment, including initializing memory-mapped arrays for branch and object labels. + """ logger.debug('Allocating memory for voxel reassignment.') self.voxel_matches_path = self.im_info.pipeline_paths['voxel_matches'] @@ -230,6 +403,23 @@ def _allocate_memory(self): return_memmap=True) def _run_frame(self, t, all_mask_coords, reassigned_memmap): + """ + Reassigns voxels in a single timepoint based on voxel matches with the previous timepoint. + + Parameters + ---------- + t : int + Current timepoint index. + all_mask_coords : list + List of voxel coordinates for each timepoint. + reassigned_memmap : np.ndarray + Memory-mapped array for the reassigned labels. + + Returns + ------- + bool + Returns True if no matches are found, otherwise False. + """ logger.info(f'Reassigning pixels in frame {t + 1} of {self.num_t - 1}') vox_prev = all_mask_coords[t] @@ -250,6 +440,14 @@ def _run_frame(self, t, all_mask_coords, reassigned_memmap): return False def _run_reassignment(self, label_type): + """ + Runs voxel reassignment for all frames based on the specified label type (either 'branch' or 'obj'). + + Parameters + ---------- + label_type : str + The label type, either 'branch' or 'obj'. + """ # todo, be able to specify which frame to start at. if label_type == 'branch': label_memmap = self.branch_label_memmap @@ -272,6 +470,9 @@ def _run_reassignment(self, label_type): break def run(self): + """ + Main method to execute voxel reassignment for both branch and object labels. + """ if self.im_info.no_t: return self._get_t() diff --git a/nellie_napari/nellie_analysis.py b/nellie_napari/nellie_analysis.py index eed3740..1f14b2a 100644 --- a/nellie_napari/nellie_analysis.py +++ b/nellie_napari/nellie_analysis.py @@ -13,7 +13,98 @@ class NellieAnalysis(QWidget): + """ + A class for analyzing and visualizing multi-dimensional microscopy data using histograms, graphs, and overlays in the napari viewer. + + Attributes + ---------- + viewer : napari.viewer.Viewer + An instance of the napari viewer. + nellie : object + Reference to the main Nellie object that contains the pipeline and analysis data. + canvas : FigureCanvasQTAgg + Canvas for rendering matplotlib plots. + scale : tuple + The scaling factors for X, Y, and Z dimensions, default is (1, 1, 1). + log_scale : bool + Boolean flag to toggle logarithmic scaling in histogram plots. + is_median : bool + Boolean flag to toggle between mean and median views. + match_t : bool + Boolean flag to enable/disable timepoint matching in data analysis. + hist_reset : bool + Boolean flag indicating whether the histogram settings are reset. + voxel_df, node_df, branch_df, organelle_df, image_df : pd.DataFrame + DataFrames containing features at different hierarchy levels (voxel, node, branch, organelle, image). + attr_data : pd.Series or None + Data for the selected attribute to be plotted. + time_col : pd.Series or None + Time column data from the currently selected level's DataFrame. + adjacency_maps : dict or None + Dictionary containing adjacency matrices for mapping hierarchy levels. + mean, std, median, iqr, perc75, perc25 : float + Statistical values for the selected attribute data (mean, std, median, interquartile range, 75th percentile, 25th percentile). + + Methods + ------- + reset() + Resets the internal state, including dataframes and initialization flags. + post_init() + Initializes UI elements such as checkboxes, buttons, and connects them to respective event handlers. + set_ui() + Sets up the user interface layout, including attribute selection, histogram, and export options. + _create_dropdown_selection() + Creates and configures the dropdowns for selecting hierarchy levels and attributes. + set_default_dropdowns() + Sets default values for the hierarchy level and attribute dropdowns. + check_for_adjacency_map() + Checks if an adjacency map is available and enables the overlay button accordingly. + rewrite_dropdown() + Rewrites the dropdown options based on the available data and features. + export_data() + Exports the current graph data to a CSV file. + save_graph() + Saves the current graph as a PNG file. + on_hist_change(event) + Event handler for histogram changes (e.g., adjusting bins or min/max values). + get_index(layer, event) + Gets the voxel index based on mouse hover coordinates in the napari viewer. + overlay() + Applies an overlay of selected attribute data onto the image, using adjacency maps to map voxel to higher-level features. + on_t_change(event) + Event handler that updates the graph when the timepoint changes in the napari viewer. + toggle_match_t(state) + Toggles timepoint matching and updates the graph accordingly. + toggle_mean_med(state) + Toggles between mean and median views and updates the graph. + get_csvs() + Loads the feature CSV files into DataFrames for voxels, nodes, branches, organelles, and images. + on_level_selected(index) + Event handler for when a hierarchy level is selected from the dropdown. + on_attr_selected(index) + Event handler for when an attribute is selected from the dropdown. + get_stats() + Computes basic statistics (mean, std, median, percentiles) for the selected attribute data. + draw_stats() + Draws the computed statistics on the histogram plot (e.g., mean, std, median, percentiles). + plot_data(title) + Plots the selected attribute data as a histogram, updates the UI, and displays statistical information. + on_log_scale(state) + Toggles logarithmic scaling for the histogram plot and refreshes the data. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the NellieAnalysis class. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the main Nellie object containing image and pipeline data. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.viewer = napari_viewer @@ -80,6 +171,9 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.initialized = False def reset(self): + """ + Resets the internal state, including the DataFrames and initialization flags. + """ self.initialized = False self.voxel_df = None self.node_df = None @@ -88,6 +182,9 @@ def reset(self): self.image_df = None def post_init(self): + """ + Initializes UI elements such as checkboxes, buttons, and connects them to their respective event handlers. + """ self.log_scale_checkbox = QCheckBox("Log scale") self.log_scale_checkbox.stateChanged.connect(self.on_log_scale) @@ -144,6 +241,9 @@ def post_init(self): self.initialized = True def set_ui(self): + """ + Sets up the user interface layout, including dropdowns for attribute selection, histogram controls, and export buttons. + """ main_layout = QVBoxLayout() # Attribute dropdown group @@ -195,6 +295,9 @@ def set_ui(self): self.setLayout(main_layout) def _create_dropdown_selection(self): + """ + Creates and configures the dropdown menus for selecting hierarchy levels and attributes. + """ # Create the dropdown menu self.dropdown = QComboBox() self.dropdown.currentIndexChanged.connect(self.on_level_selected) @@ -207,17 +310,26 @@ def _create_dropdown_selection(self): self.set_default_dropdowns() def set_default_dropdowns(self): + """ + Sets the default values for the hierarchy level and attribute dropdowns. + """ organelle_idx = self.dropdown.findText('organelle') self.dropdown.setCurrentIndex(organelle_idx) area_raw_idx = self.dropdown_attr.findText('organelle_area_raw') self.dropdown_attr.setCurrentIndex(area_raw_idx) def check_for_adjacency_map(self): + """ + Checks whether an adjacency map exists, and enables the overlay button if found. + """ self.overlay_button.setEnabled(False) if os.path.exists(self.nellie.im_info.pipeline_paths['adjacency_maps']): self.overlay_button.setEnabled(True) def rewrite_dropdown(self): + """ + Updates the hierarchy level dropdown based on the available data, and checks for adjacency maps. + """ self.check_for_adjacency_map() self.dropdown.clear() @@ -234,6 +346,9 @@ def rewrite_dropdown(self): self.adjacency_maps = None def export_data(self): + """ + Exports the current graph data as a CSV file to a specified directory. + """ dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") export_dir = self.nellie.im_info.graph_dir if not os.path.exists(export_dir): @@ -254,6 +369,9 @@ def export_data(self): show_info(f"Data exported to {export_path}") def save_graph(self): + """ + Saves the current graph as a PNG file to a specified directory. + """ dt = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") export_dir = self.nellie.im_info.graph_dir if not os.path.exists(export_dir): @@ -267,9 +385,22 @@ def save_graph(self): show_info(f"Graph saved to {export_path}") def on_hist_change(self, event): + """ + Event handler for updating the histogram when changes are made (e.g., adjusting the number of bins, min/max values). + """ self.plot_data(self.dropdown_attr.currentText()) def get_index(self, layer, event): + """ + Retrieves the index of the voxel or feature based on mouse hover coordinates in the napari viewer. + + Parameters + ---------- + layer : Layer + The layer on which the event is triggered. + event : Event + The event triggered by mouse hover. + """ # get the coordinates of where the mouse is hovering pos = self.viewer.cursor.position matched_row = None @@ -316,6 +447,9 @@ def get_index(self, layer, event): self.click_match_table.setVerticalHeaderLabels([f"{t, y, x}\nCSV row"]) def overlay(self): + """ + Applies an overlay of attribute data onto the image in the napari viewer, using adjacency maps for mapping between hierarchy levels. + """ if self.label_mask is None: label_mask = self.nellie.im_info.get_memmap(self.nellie.im_info.pipeline_paths['im_instance_label']) self.label_mask = (label_mask > 0).astype(float) @@ -446,10 +580,21 @@ def overlay(self): self.viewer.reset_view() def on_t_change(self, event): + """ + Event handler for timepoint changes in the napari viewer. Updates the attribute data and refreshes the plot accordingly. + """ if self.match_t: self.on_attr_selected(self.dropdown_attr.currentIndex()) def toggle_match_t(self, state): + """ + Toggles timepoint matching and updates the graph and data accordingly. + + Parameters + ---------- + state : int + The state of the checkbox (checked or unchecked). + """ if state == 2: self.match_t = True else: @@ -457,6 +602,14 @@ def toggle_match_t(self, state): self.on_attr_selected(self.dropdown_attr.currentIndex()) def toggle_mean_med(self, state): + """ + Toggles between mean and median views for the histogram plot. + + Parameters + ---------- + state : int + The state of the checkbox (checked or unchecked). + """ if state == 2: self.is_median = True else: @@ -464,6 +617,9 @@ def toggle_mean_med(self, state): self.on_attr_selected(self.dropdown_attr.currentIndex()) def get_csvs(self): + """ + Loads the CSV files containing voxel, node, branch, organelle, and image features into DataFrames. + """ self.voxel_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_voxels']) if os.path.exists(self.nellie.im_info.pipeline_paths['features_nodes']): self.node_df = pd.read_csv(self.nellie.im_info.pipeline_paths['features_nodes']) @@ -475,6 +631,14 @@ def get_csvs(self): # self.voxel_df_idxs = voxel_df[voxel_df.columns[0]] def on_level_selected(self, index): + """ + Event handler for when a hierarchy level is selected from the dropdown menu. + + Parameters + ---------- + index : int + The index of the selected item in the dropdown. + """ # This method is called whenever a radio button is selected # 'button' parameter is the clicked radio button self.selected_level = self.dropdown.itemText(index) @@ -516,6 +680,14 @@ def on_level_selected(self, index): self.dropdown_attr.addItem(col) def on_attr_selected(self, index): + """ + Event handler for when an attribute is selected from the dropdown menu. + + Parameters + ---------- + index : int + The index of the selected attribute in the dropdown. + """ self.hist_reset = True # if there are no items in dropdown_attr, return if self.dropdown_attr.count() == 0: @@ -541,6 +713,9 @@ def on_attr_selected(self, index): self.plot_data(selected_attr) def get_stats(self): + """ + Computes basic statistics (mean, std, median, percentiles) for the currently selected attribute data. + """ if self.attr_data is None: return if not self.log_scale: @@ -566,6 +741,9 @@ def get_stats(self): self.iqr = self.perc75 - self.perc25 def draw_stats(self): + """ + Draws statistics on the histogram plot, including lines for mean, median, std, and percentiles. + """ if self.attr_data is None: return # draw lines for mean, median, std, percentiles on the canvas @@ -582,6 +760,14 @@ def draw_stats(self): self.canvas.draw() def plot_data(self, title): + """ + Plots the selected attribute data as a histogram, updates the canvas, and displays the computed statistics. + + Parameters + ---------- + title : str + The title for the plot, usually the name of the selected attribute. + """ self.canvas.figure.clear() ax = self.canvas.figure.add_subplot(111) self.data_to_plot = self.data_to_plot.replace([np.inf, -np.inf], np.nan) @@ -635,6 +821,14 @@ def plot_data(self, title): self.hist_reset = False def on_log_scale(self, state): + """ + Toggles logarithmic scaling for the histogram plot and refreshes the data accordingly. + + Parameters + ---------- + state : int + The state of the checkbox (checked or unchecked). + """ self.hist_reset = True if state == 2: self.log_scale = True diff --git a/nellie_napari/nellie_fileselect.py b/nellie_napari/nellie_fileselect.py index 5e0348a..05b0cf4 100644 --- a/nellie_napari/nellie_fileselect.py +++ b/nellie_napari/nellie_fileselect.py @@ -9,7 +9,100 @@ class NellieFileSelect(QWidget): + """ + A class for selecting and configuring image files for processing in the Nellie pipeline within the napari viewer. + + Attributes + ---------- + viewer : napari.viewer.Viewer + The napari viewer instance. + nellie : object + Reference to the main Nellie object containing image processing pipelines and functions. + filepath : str or None + The selected file path. + file_info : FileInfo + Stores metadata and shape information about the selected image file. + im_info : ImInfo or None + Contains information about the image for processing after confirmation. + batch_fileinfo_list : list or None + List of FileInfo objects when a folder is selected for batch processing. + filepath_text : QLabel + Text widget displaying the selected file or folder path. + filepath_button : QPushButton + Button to open the file dialog for selecting an image file. + folder_button : QPushButton + Button to open the folder dialog for batch processing. + reset_button : QPushButton + Button to reset the file selection and clear all settings. + file_shape_text : QLabel + Displays the shape of the selected image file. + current_order_text : QLabel + Displays the current dimension order (axes) of the image file. + dim_order_button : QLineEdit + Input field for entering the dimension order of the image. + dim_t_button, dim_z_button, dim_xy_button : QLineEdit + Input fields for entering the resolution of the time (T), Z, and XY dimensions, respectively. + channel_button : QSpinBox + Spin box for selecting the channel in the multi-channel image. + start_frame_button, end_frame_button : QSpinBox + Spin boxes for selecting the start and end frame for temporal range selection. + confirm_button : QPushButton + Button to confirm the file selection and save the OME TIFF file. + preview_button : QPushButton + Button to preview the image in the napari viewer. + process_button : QPushButton + Button to process the selected image file(s) through the Nellie pipeline. + + Methods + ------- + init_ui() + Sets up the user interface layout with file selection, axes information, dimension resolutions, and actions. + select_filepath() + Opens a file dialog for selecting an image file and validates the selected file. + select_folder() + Opens a folder dialog for batch processing and validates the selected folder. + validate_path(filepath) + Validates the selected file or folder path and updates the file path attribute. + initialize_single_file() + Initializes the FileInfo for the selected image file, loads metadata, and updates UI elements. + initialize_folder() + Initializes the FileInfo objects for all image files in the selected folder. + on_change() + Updates UI elements based on the selected file's metadata, checking available dimensions and enabling buttons. + check_available_dims() + Checks the availability of specific dimensions (T, Z, XY) in the selected file and enables corresponding UI elements. + handle_dim_order_changed(text) + Handles changes in the dimension order (axes) input field and updates the metadata accordingly. + handle_t_changed(text) + Handles changes in the time (T) resolution input field and updates the metadata. + handle_z_changed(text) + Handles changes in the Z resolution input field and updates the metadata. + handle_xy_changed(text) + Handles changes in the XY resolution input field and updates the metadata. + change_channel() + Updates the selected channel for the image when the channel spin box is changed. + change_time() + Updates the temporal range when the start and end frame spin boxes are changed. + on_confirm() + Confirms the file selection, creates an ImInfo object, and prepares the file for processing. + on_process() + Prepares the selected file(s) for processing through the Nellie pipeline. + on_preview() + Opens a preview of the selected image in the napari viewer, adjusting the display settings based on the file's dimensionality. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the NellieFileSelect class. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the main Nellie object containing image processing pipelines and functions. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.filepath = None @@ -113,6 +206,9 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.init_ui() def init_ui(self): + """ + Sets up the user interface layout with sections for file selection, axes information, dimension resolutions, and action buttons. + """ main_layout = QVBoxLayout() # File Selection Group @@ -193,6 +289,9 @@ def init_ui(self): self.setLayout(main_layout) def select_filepath(self): + """ + Opens a file dialog for selecting an image file, validates the selected file, and updates the UI with metadata. + """ self.batch_fileinfo_list = None filepath, _ = QFileDialog.getOpenFileName(self, "Select file") self.validate_path(filepath) @@ -206,6 +305,9 @@ def select_filepath(self): self.filepath_text.setText(f"{filename}") def select_folder(self): + """ + Opens a folder dialog for selecting a folder for batch processing and initializes FileInfo objects for all files in the folder. + """ folderpath = QFileDialog.getExistingDirectory(self, "Select folder") self.validate_path(folderpath) if self.filepath is None: @@ -217,12 +319,23 @@ def select_folder(self): def validate_path(self, filepath): + """ + Validates the selected file or folder path and updates the file path attribute. + + Parameters + ---------- + filepath : str + The file or folder path selected by the user. + """ if not filepath: show_info("Invalid selection.") return None self.filepath = filepath def initialize_single_file(self): + """ + Initializes the FileInfo object for the selected image file, loads metadata, and updates the dimension resolution fields. + """ self.file_info.find_metadata() self.file_info.load_metadata() self.file_shape_text.setText(f"{self.file_info.shape}") @@ -232,6 +345,9 @@ def initialize_single_file(self): self.on_change() def initialize_folder(self): + """ + Initializes FileInfo objects for all .tif, .tiff, and .nd2 files in the selected folder and loads their metadata. + """ # get all .tif, .tiff, and .nd2 files in the folder files = [f for f in os.listdir(self.filepath) if f.endswith('.tif') or f.endswith('.tiff') or f.endswith('.nd2')] # for each file, create a FileInfo object @@ -244,6 +360,9 @@ def initialize_folder(self): # This assumes all files in the folder have the same metadata (dim order, resolutions, temporal range, channels) def on_change(self): + """ + Updates the user interface elements, including enabling or disabling buttons based on the file metadata and resolution settings. + """ self.confirm_button.setEnabled(False) self.check_available_dims() if len(self.file_info.shape) == 2: @@ -279,6 +398,9 @@ def on_change(self): self.process_button.setEnabled(True) def check_available_dims(self): + """ + Checks the availability of specific dimensions (T, Z, XY) in the selected file and enables the corresponding input fields for resolutions. + """ def check_dim(dim, dim_button, dim_text): dim_button.setStyleSheet("background-color: green") if dim in self.file_info.axes: @@ -324,6 +446,14 @@ def check_dim(dim, dim_button, dim_text): self.end_frame_init = True def handle_dim_order_changed(self, text): + """ + Handles changes in the dimension order input field and updates the FileInfo object accordingly. + + Parameters + ---------- + text : str + The new dimension order string entered by the user. + """ if self.batch_fileinfo_list is None: self.file_info.change_axes(text) else: @@ -334,6 +464,14 @@ def handle_dim_order_changed(self, text): self.on_change() def handle_t_changed(self, text): + """ + Handles changes in the time (T) resolution input field and updates the FileInfo object accordingly. + + Parameters + ---------- + text : str + The new T resolution entered by the user. + """ self.dim_t_text = text try: value = float(self.dim_t_text) @@ -351,6 +489,14 @@ def handle_t_changed(self, text): self.on_change() def handle_z_changed(self, text): + """ + Handles changes in the Z resolution input field and updates the FileInfo object accordingly. + + Parameters + ---------- + text : str + The new Z resolution entered by the user. + """ self.dim_z_text = text try: value = float(self.dim_z_text) @@ -368,6 +514,14 @@ def handle_z_changed(self, text): self.on_change() def handle_xy_changed(self, text): + """ + Handles changes in the XY resolution input field and updates the FileInfo object accordingly. + + Parameters + ---------- + text : str + The new XY resolution entered by the user. + """ self.dim_xy_text = text try: value = float(self.dim_xy_text) @@ -389,6 +543,9 @@ def handle_xy_changed(self, text): self.on_change() def change_channel(self): + """ + Updates the selected channel in the FileInfo object when the channel spin box value is changed. + """ if self.batch_fileinfo_list is None: self.file_info.change_selected_channel(self.channel_button.value()) else: @@ -397,6 +554,9 @@ def change_channel(self): self.on_change() def change_time(self): + """ + Updates the temporal range in the FileInfo object when the start or end frame spin box values are changed. + """ if self.batch_fileinfo_list is None: self.file_info.select_temporal_range(self.start_frame_button.value(), self.end_frame_button.value()) else: @@ -405,6 +565,9 @@ def change_time(self): self.on_change() def on_confirm(self): + """ + Confirms the file selection, creates an ImInfo object for the file, and prepares it for processing. + """ show_info("Saving OME TIFF file.") if self.batch_fileinfo_list is None: self.im_info = ImInfo(self.file_info) @@ -413,6 +576,9 @@ def on_confirm(self): self.on_change() def on_process(self): + """ + Prepares the selected file(s) for processing through the Nellie pipeline by creating ImInfo objects and switching to the processing tab. + """ # switch to process tab if self.batch_fileinfo_list is None: self.im_info = ImInfo(self.file_info) @@ -422,6 +588,9 @@ def on_process(self): self.nellie.go_process() def on_preview(self): + """ + Opens a preview of the selected image in the napari viewer, adjusting display settings (e.g., 2D or 3D view) based on the file's dimensionality. + """ im_memmap = tifffile.memmap(self.file_info.ome_output_path) # num_t = min(2, self.im_info.shape[self.im_info.axes.index('T')]) if 'Z' in self.file_info.axes: diff --git a/nellie_napari/nellie_home.py b/nellie_napari/nellie_home.py index 851b0ae..3f90880 100644 --- a/nellie_napari/nellie_home.py +++ b/nellie_napari/nellie_home.py @@ -10,7 +10,43 @@ class Home(QWidget): + """ + The Home screen for the Nellie application, displayed in the napari viewer. + It provides options to start using the application, navigate to the file selection tab, and take screenshots. + + Attributes + ---------- + viewer : napari.viewer.Viewer + The napari viewer instance. + nellie : object + Reference to the main Nellie object containing image processing pipelines and functions. + layout : QVBoxLayout + The vertical layout to organize the widgets on the home screen. + start_button : QPushButton + Button to start the application and navigate to the file selection tab. + screenshot_button : QPushButton + Button to take a screenshot of the current napari viewer canvas. + + Methods + ------- + __init__(napari_viewer, nellie, parent=None) + Initializes the home screen with a logo, title, description, and navigation buttons. + screenshot(event=None) + Takes a screenshot of the napari viewer and saves it to a specified folder. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the Home screen with a logo, title, description, and buttons for navigation and screenshot functionality. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the main Nellie object containing image processing pipelines and functions. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.viewer = napari_viewer @@ -72,6 +108,14 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.layout.addWidget(self.screenshot_button, alignment=Qt.AlignCenter) def screenshot(self, event=None): + """ + Takes a screenshot of the napari viewer and saves it as a PNG file in a specified folder. + + Parameters + ---------- + event : optional + An event object, if triggered by a key binding or button click (default is None). + """ if self.nellie.im_info is None: show_info("No file selected, cannot take screenshot") return diff --git a/nellie_napari/nellie_loader.py b/nellie_napari/nellie_loader.py index 3551ae6..4819353 100644 --- a/nellie_napari/nellie_loader.py +++ b/nellie_napari/nellie_loader.py @@ -10,7 +10,54 @@ class NellieLoader(QTabWidget): + """ + The main loader class for managing the different stages of the Nellie pipeline within the napari viewer. This class + provides a tabbed interface for file selection, processing, visualization, analysis, and settings management. + + Attributes + ---------- + home : Home + The home tab instance, providing an overview of the Nellie pipeline. + file_select : NellieFileSelect + The file selection tab instance, allowing users to select and validate image files. + processor : NellieProcessor + The image processing tab instance, where users can process images through the Nellie pipeline. + visualizer : NellieVisualizer + The visualization tab instance, where processed images can be visualized. + analyzer : NellieAnalysis + The analysis tab instance, enabling users to analyze processed image data. + settings : Settings + The settings tab instance, allowing users to configure various settings for the Nellie pipeline. + home_tab, file_select_tab, processor_tab, visualizer_tab, analysis_tab, settings_tab : int + Integer values representing the index of the respective tabs. + im_info : ImInfo or None + Contains metadata and information about the selected image file. + im_info_list : list of ImInfo or None + A list of ImInfo objects when batch processing is enabled (multiple files). + + Methods + ------- + add_tabs() + Adds the individual tabs to the widget. + reset() + Resets the state of the loader, removing and reinitializing all tabs. + on_tab_change(index) + Slot that is triggered when the user changes the tab. + go_process() + Initializes and enables the processing and visualization tabs for image processing. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', parent=None): + """ + Initializes the NellieLoader class, creating instances of the individual tabs for home, file selection, + processing, visualization, analysis, and settings. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.home = Home(napari_viewer, self) self.file_select = NellieFileSelect(napari_viewer, self) @@ -33,6 +80,11 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', parent=None): self.im_info_list = None def add_tabs(self): + """ + Adds the individual tabs for Home, File validation, Process, Visualize, Analyze, and Settings. + Initially disables the Process, Visualize, and Analyze tabs until they are needed. + """ + ... self.home_tab = self.addTab(self.home, "Home") self.file_select_tab = self.addTab(self.file_select, "File validation") self.processor_tab = self.addTab(self.processor, "Process") @@ -45,6 +97,10 @@ def add_tabs(self): self.setTabEnabled(self.analysis_tab, False) def reset(self): + """ + Resets the state of the loader, reinitializing all tabs. This method is typically called when the user + wants to start a new session with a fresh file selection and settings. + """ self.setCurrentIndex(self.home_tab) # needs to be in reverse order @@ -66,6 +122,15 @@ def reset(self): self.im_info_list = None def on_tab_change(self, index): + """ + Event handler that is triggered when the user changes the active tab. Initializes the Analyze or Visualize + tabs if they are selected for the first time, and always initializes the Settings tab. + + Parameters + ---------- + index : int + The index of the newly selected tab. + """ if index == self.analysis_tab: # Check if the Analyze tab is selected if not self.analyzer.initialized: show_info("Initializing analysis tab") @@ -77,6 +142,10 @@ def on_tab_change(self, index): self.settings.post_init() def go_process(self): + """ + Prepares the image(s) for processing and visualization. This method is called after a file has been selected + and validated. It enables the Process and Visualize tabs and initializes them. + """ if self.file_select.batch_fileinfo_list is None: self.im_info = self.file_select.im_info else: diff --git a/nellie_napari/nellie_processor.py b/nellie_napari/nellie_processor.py index d7ccf86..cdee030 100644 --- a/nellie_napari/nellie_processor.py +++ b/nellie_napari/nellie_processor.py @@ -18,7 +18,96 @@ class NellieProcessor(QWidget): + """ + The NellieProcessor class manages the different steps of the Nellie pipeline such as preprocessing, segmentation, + mocap marking, tracking, voxel reassignment, and feature extraction. It provides an interface to run each step + individually or as part of a full pipeline within a napari viewer. + + Attributes + ---------- + nellie : object + Reference to the Nellie instance managing the pipeline. + viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + im_info_list : list of ImInfo or None + List of ImInfo objects for the selected files. Contains metadata and file information. + current_im_info : ImInfo or None + The current image's information and metadata object. + status_label : QLabel + Label displaying the current status of the process. + status : str or None + The current status of the process (e.g., "preprocessing", "segmentation"). + num_ellipses : int + Counter to manage the ellipsis effect on the status label during execution. + status_timer : QTimer + Timer that periodically updates the status label during pipeline execution. + open_dir_button : QPushButton + Button to open the output directory of the current image file. + run_button : QPushButton + Button to run the full Nellie pipeline. + preprocess_button : QPushButton + Button to run only the preprocessing step of the pipeline. + segment_button : QPushButton + Button to run only the segmentation step of the pipeline. + mocap_button : QPushButton + Button to run only the mocap marking step of the pipeline. + track_button : QPushButton + Button to run only the tracking step of the pipeline. + reassign_button : QPushButton + Button to run only the voxel reassignment step of the pipeline. + feature_export_button : QPushButton + Button to run only the feature extraction step of the pipeline. + initialized : bool + Flag indicating whether the processor has been initialized. + pipeline : bool + Flag indicating whether the full pipeline is being run. + + Methods + ------- + set_ui() + Initializes and sets the layout and UI components for the NellieProcessor. + post_init() + Post-initialization method to load image information and check file existence. + check_file_existence() + Checks the existence of necessary files for each step of the pipeline and enables/disables buttons accordingly. + run_nellie() + Runs the entire Nellie pipeline starting from preprocessing to feature extraction. + run_preprocessing() + Runs the preprocessing step of the Nellie pipeline. + run_segmentation() + Runs the segmentation step of the Nellie pipeline. + run_mocap() + Runs the mocap marking step of the Nellie pipeline. + run_tracking() + Runs the tracking step of the Nellie pipeline. + run_reassign() + Runs the voxel reassignment step of the Nellie pipeline. + run_feature_export() + Runs the feature extraction step of the Nellie pipeline. + set_status() + Sets the status to indicate that a process has started, and starts the status update timer. + update_status() + Updates the status label with the current process and an ellipsis effect while the process is running. + reset_status() + Resets the status label to indicate that no process is running. + turn_off_buttons() + Disables all buttons to prevent multiple processes from running simultaneously. + open_directory() + Opens the output directory where the current image results are saved. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the NellieProcessor class, setting up the user interface and preparing for running various steps of the pipeline. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the Nellie instance that manages the pipeline. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.viewer = napari_viewer @@ -82,6 +171,10 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.pipeline = False def set_ui(self): + """ + Initializes and sets the layout and user interface components for the NellieProcessor. This includes the status label, + buttons for running individual steps, and the button for running the entire pipeline. + """ main_layout = QVBoxLayout() # Status group @@ -116,6 +209,10 @@ def set_ui(self): self.setLayout(main_layout) def post_init(self): + """ + Post-initialization method that checks the state of the selected images. It determines whether the pipeline + steps have already been completed and enables/disables the corresponding buttons accordingly. + """ if self.nellie.im_info_list is None: self.im_info_list = [self.nellie.im_info] self.current_im_info = self.nellie.im_info @@ -127,6 +224,10 @@ def post_init(self): self.open_dir_button.setEnabled(os.path.exists(self.current_im_info.file_info.output_dir)) def check_file_existence(self): + """ + Checks the existence of files required for each step of the pipeline (e.g., preprocessed images, segmented labels). + Enables or disables buttons based on the existence of these files. + """ self.nellie.visualizer.check_file_existence() # set all other buttons to disabled first @@ -199,6 +300,9 @@ def check_file_existence(self): @thread_worker def _run_preprocessing(self): + """ + Runs the preprocessing step in a separate thread. Filters the image to remove noise or unwanted edges before segmentation. + """ self.status = "preprocessing" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Preprocessing file {im_num + 1}/{len(self.im_info_list)}") @@ -209,6 +313,10 @@ def _run_preprocessing(self): preprocessing.run() def run_preprocessing(self): + """ + Starts the preprocessing step and updates the UI to reflect that preprocessing is running. + If the full pipeline is running, it automatically proceeds to segmentation after preprocessing is finished. + """ worker = self._run_preprocessing() worker.started.connect(self.turn_off_buttons) if self.pipeline: @@ -220,6 +328,9 @@ def run_preprocessing(self): @thread_worker def _run_segmentation(self): + """ + Runs the segmentation step in a separate thread. Labels and segments regions of interest in the preprocessed image. + """ self.status = "segmentation" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Segmentation file {im_num + 1}/{len(self.im_info_list)}") @@ -230,6 +341,10 @@ def _run_segmentation(self): networking.run() def run_segmentation(self): + """ + Starts the segmentation step and updates the UI to reflect that segmentation is running. + If the full pipeline is running, it automatically proceeds to mocap marking after segmentation is finished. + """ worker = self._run_segmentation() worker.started.connect(self.turn_off_buttons) if self.pipeline: @@ -241,6 +356,9 @@ def run_segmentation(self): @thread_worker def _run_mocap(self): + """ + Runs the mocap marking step in a separate thread. Marks the motion capture points within the segmented regions. + """ self.status = "mocap marking" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Mocap Marking file {im_num + 1}/{len(self.im_info_list)}") @@ -249,6 +367,10 @@ def _run_mocap(self): mocap_marking.run() def run_mocap(self): + """ + Starts the mocap marking step and updates the UI to reflect that mocap marking is running. + If the full pipeline is running, it automatically proceeds to tracking after mocap marking is finished. + """ worker = self._run_mocap() worker.started.connect(self.turn_off_buttons) if self.pipeline: @@ -261,6 +383,9 @@ def run_mocap(self): @thread_worker def _run_tracking(self): + """ + Runs the tracking step in a separate thread. Tracks the motion of the marked points over time. + """ self.status = "tracking" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Tracking file {im_num + 1}/{len(self.im_info_list)}") @@ -269,6 +394,10 @@ def _run_tracking(self): hu_tracking.run() def run_tracking(self): + """ + Starts the tracking step and updates the UI to reflect that tracking is running. + If the full pipeline is running, it automatically proceeds to voxel reassignment or feature extraction depending on the settings. + """ worker = self._run_tracking() worker.started.connect(self.turn_off_buttons) if self.pipeline: @@ -283,6 +412,9 @@ def run_tracking(self): @thread_worker def _run_reassign(self): + """ + Runs the voxel reassignment step in a separate thread. Reassigns voxel labels based on the tracked motion. + """ self.status = "voxel reassignment" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Voxel Reassignment file {im_num + 1}/{len(self.im_info_list)}") @@ -291,6 +423,10 @@ def _run_reassign(self): vox_reassign.run() def run_reassign(self): + """ + Starts the voxel reassignment step and updates the UI to reflect that voxel reassignment is running. + If the full pipeline is running, it automatically proceeds to feature extraction after voxel reassignment is finished. + """ worker = self._run_reassign() worker.started.connect(self.turn_off_buttons) if self.pipeline: @@ -303,6 +439,9 @@ def run_reassign(self): @thread_worker def _run_feature_export(self): + """ + Runs the feature extraction step in a separate thread. Extracts various features from the processed image data for analysis. + """ self.status = "feature export" for im_num, im_info in enumerate(self.im_info_list): show_info(f"Nellie is running: Feature export file {im_num + 1}/{len(self.im_info_list)}") @@ -320,6 +459,9 @@ def _run_feature_export(self): self.nellie.analyzer.rewrite_dropdown() def run_feature_export(self): + """ + Starts the feature extraction step and updates the UI to reflect that feature extraction is running. + """ worker = self._run_feature_export() worker.started.connect(self.turn_off_buttons) worker.finished.connect(self.check_file_existence) @@ -329,17 +471,29 @@ def run_feature_export(self): worker.start() def turn_off_pipeline(self): + """ + Turns off the pipeline flag to indicate that the full pipeline is no longer running. + """ self.pipeline = False def run_nellie(self): + """ + Starts the entire Nellie pipeline from preprocessing to feature extraction. + """ self.pipeline = True self.run_preprocessing() def set_status(self): + """ + Sets the status of the processor to indicate that a process is running, and starts the status update timer. + """ self.running = True self.status_timer.start(500) # Update every 250 ms def update_status(self): + """ + Updates the status label with an ellipsis effect to indicate ongoing processing. + """ if self.running: self.status_label.setText(f"Running {self.status}{'.' * (self.num_ellipses % 4)}") self.num_ellipses += 1 @@ -347,12 +501,18 @@ def update_status(self): self.status_timer.stop() def reset_status(self): + """ + Resets the status label to indicate that no process is running. + """ self.running = False self.status_label.setText("Awaiting your input") self.num_ellipses = 1 self.status_timer.stop() def turn_off_buttons(self): + """ + Disables all buttons to prevent multiple processes from running simultaneously. + """ self.run_button.setEnabled(False) self.preprocess_button.setEnabled(False) self.segment_button.setEnabled(False) @@ -362,6 +522,9 @@ def turn_off_buttons(self): self.feature_export_button.setEnabled(False) def open_directory(self): + """ + Opens the output directory of the current image in the system file explorer. + """ directory = self.current_im_info.file_info.output_dir if os.path.exists(directory): if os.name == 'nt': # For Windows diff --git a/nellie_napari/nellie_settings.py b/nellie_napari/nellie_settings.py index 8f9a44d..79f7f3f 100644 --- a/nellie_napari/nellie_settings.py +++ b/nellie_napari/nellie_settings.py @@ -3,7 +3,55 @@ class Settings(QWidget): + """ + The Settings class provides a user interface for configuring various options and settings for the Nellie pipeline + and visualizations. Users can enable or disable specific processing options, control track visualization settings, + and configure voxel visualization parameters. + + Attributes + ---------- + nellie : object + Reference to the Nellie instance managing the pipeline. + viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + remove_edges_checkbox : QCheckBox + Checkbox for enabling or disabling the removal of image edges. + remove_intermediates_checkbox : QCheckBox + Checkbox for enabling or disabling the removal of intermediate files after processing. + voxel_reassign : QCheckBox + Checkbox to enable or disable the automatic voxel reassignment step after tracking. + analyze_node_level : QCheckBox + Checkbox to enable or disable node-level analysis during feature extraction. + track_all_frames : QCheckBox + Checkbox to enable or disable the visualization of voxel tracks for all frames. + skip_vox_label : QLabel + Label describing the setting for skipping voxels during track visualization. + skip_vox : QSpinBox + Spin box for selecting the value of N to visualize tracks for every Nth voxel. + initialized : bool + Flag to indicate whether the settings interface has been initialized. + + Methods + ------- + post_init() + Post-initialization method that sets the initialized flag to True. + set_ui() + Initializes and sets the layout and UI components for the Settings class. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the Settings class, setting up the user interface and options for configuring + processing and track visualization. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the Nellie instance that manages the pipeline. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.viewer = napari_viewer @@ -48,9 +96,17 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.initialized = False def post_init(self): + """ + Post-initialization method that sets the initialized flag to True. + """ self.initialized = True def set_ui(self): + """ + Initializes and sets the layout and UI components for the Settings class. This includes checkboxes for + configuring the processing pipeline and track visualization options, as well as a spin box for setting + voxel track visualization parameters. + """ main_layout = QVBoxLayout() # Processor settings diff --git a/nellie_napari/nellie_visualizer.py b/nellie_napari/nellie_visualizer.py index 865f109..9c8b428 100644 --- a/nellie_napari/nellie_visualizer.py +++ b/nellie_napari/nellie_visualizer.py @@ -8,7 +8,90 @@ class NellieVisualizer(QWidget): + """ + The NellieVisualizer class provides an interface for visualizing different stages of the Nellie pipeline, such as raw images, + preprocessed images, segmentation labels, mocap markers, and reassigned labels. It also enables track visualization for specific + labels and all frame labels using napari. + + Attributes + ---------- + nellie : object + Reference to the Nellie instance managing the pipeline. + viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + scale : tuple of floats + Scaling factors for visualizing the images, based on the resolution of the image dimensions. + im_memmap : ndarray + Memory-mapped array for the raw image. + raw_layer : ImageLayer + The napari image layer for displaying the raw image. + im_branch_label_reassigned_layer : LabelsLayer + The napari labels layer for displaying reassigned branch labels. + im_branch_label_reassigned : ndarray + Memory-mapped array for reassigned branch labels. + im_obj_label_reassigned_layer : LabelsLayer + The napari labels layer for displaying reassigned object labels. + im_obj_label_reassigned : ndarray + Memory-mapped array for reassigned object labels. + im_marker_layer : ImageLayer + The napari image layer for displaying mocap markers. + im_marker : ndarray + Memory-mapped array for mocap marker images. + im_skel_relabelled_layer : LabelsLayer + The napari labels layer for displaying skeleton relabeled images. + im_instance_label_layer : LabelsLayer + The napari labels layer for displaying instance labels. + frangi_layer : ImageLayer + The napari image layer for displaying preprocessed images (Frangi-filtered). + im_skel_relabelled : ndarray + Memory-mapped array for skeleton relabeled images. + im_instance_label : ndarray + Memory-mapped array for instance labels. + im_frangi : ndarray + Memory-mapped array for the preprocessed (Frangi-filtered) images. + initialized : bool + Flag indicating whether the visualizer has been initialized. + + Methods + ------- + set_ui() + Initializes and sets the layout and UI components for the NellieVisualizer. + post_init() + Initializes the visualizer by setting the scale and making the scale bar visible in napari. + check_3d() + Ensures that the napari viewer is in 3D mode if the dataset contains Z-dimension data. + set_scale() + Sets the scale for image display based on the image resolution in Z, Y, and X dimensions. + open_preprocess_image() + Opens and displays the preprocessed (Frangi-filtered) image in the napari viewer. + open_segment_image() + Opens and displays the segmentation labels (skeleton relabeled and instance labels) in the napari viewer. + on_track_selected() + Visualizes the tracks for the currently selected label in the napari viewer. + track_all() + Visualizes tracks for all labels across frames in the napari viewer. + open_mocap_image() + Opens and displays the mocap marker image in the napari viewer. + open_reassign_image() + Opens and displays the reassigned branch and object labels in the napari viewer. + open_raw() + Opens and displays the raw image in the napari viewer. + check_file_existence() + Checks for the existence of files related to different steps of the pipeline, enabling or disabling buttons accordingly. + """ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): + """ + Initializes the NellieVisualizer class, setting up buttons and layout for opening and visualizing images and tracks. + + Parameters + ---------- + napari_viewer : napari.viewer.Viewer + Reference to the napari viewer instance. + nellie : object + Reference to the Nellie instance managing the pipeline. + parent : QWidget, optional + Optional parent widget (default is None). + """ super().__init__(parent) self.nellie = nellie self.viewer = napari_viewer @@ -77,6 +160,10 @@ def __init__(self, napari_viewer: 'napari.viewer.Viewer', nellie, parent=None): self.initialized = False def set_ui(self): + """ + Initializes and sets the layout and UI components for the NellieVisualizer. It groups the buttons for image + and track visualization into separate sections and arranges them within a vertical layout. + """ main_layout = QVBoxLayout() # visualization group @@ -101,18 +188,27 @@ def set_ui(self): self.setLayout(main_layout) def post_init(self): + """ + Post-initialization method that sets the image scale based on the image resolution and makes the scale bar visible. + """ self.set_scale() self.viewer.scale_bar.visible = True self.viewer.scale_bar.unit = 'um' self.initialized = True def check_3d(self): + """ + Ensures that the napari viewer is in 3D mode if the dataset contains Z-dimension data. + """ if not self.nellie.im_info.no_z and self.viewer.dims.ndim != 3: # ndimensions should be 3 for viewer self.viewer.dims.ndim = 3 self.viewer.dims.ndisplay = 3 def set_scale(self): + """ + Sets the scale for image display based on the resolution of the Z, Y, and X dimensions of the image. + """ dim_res = self.nellie.im_info.dim_res if self.nellie.im_info.no_z: self.scale = (dim_res['Y'], dim_res['X']) @@ -120,6 +216,9 @@ def set_scale(self): self.scale = (dim_res['Z'], dim_res['Y'], dim_res['X']) def open_preprocess_image(self): + """ + Opens and displays the preprocessed (Frangi-filtered) image in the napari viewer. + """ self.im_frangi = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_preprocessed']) self.check_3d() self.frangi_layer = self.viewer.add_image(self.im_frangi, name='Pre-processed', colormap='turbo', scale=self.scale) @@ -127,6 +226,9 @@ def open_preprocess_image(self): self.viewer.layers.selection.active = self.frangi_layer def open_segment_image(self): + """ + Opens and displays the segmentation labels (skeleton relabeled and instance labels) in the napari viewer. + """ self.im_instance_label = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_instance_label']) self.im_skel_relabelled = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_skel_relabelled']) @@ -139,6 +241,9 @@ def open_segment_image(self): self.viewer.layers.selection.active = self.im_instance_label_layer def on_track_selected(self): + """ + Visualizes the tracks for the currently selected label in the napari viewer, based on the active image layer. + """ # if flow_vector_array path does not point to an existing file, return if not os.path.exists(self.nellie.im_info.pipeline_paths['flow_vector_array']): return @@ -201,6 +306,9 @@ def on_track_selected(self): self.check_file_existence() def track_all(self): + """ + Visualizes tracks for all labels across frames in the napari viewer, based on the active image layer. + """ layer = self.viewer.layers.selection.active if layer == self.im_instance_label_layer: label_path = self.nellie.im_info.pipeline_paths['im_instance_label'] @@ -243,6 +351,9 @@ def track_all(self): self.check_file_existence() def open_mocap_image(self): + """ + Opens and displays the mocap marker image in the napari viewer. + """ self.check_3d() self.im_marker = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_marker']) @@ -252,6 +363,9 @@ def open_mocap_image(self): self.viewer.layers.selection.active = self.im_marker_layer def open_reassign_image(self): + """ + Opens and displays the reassigned branch and object labels in the napari viewer. + """ self.check_3d() self.im_branch_label_reassigned = tifffile.memmap(self.nellie.im_info.pipeline_paths['im_branch_label_reassigned']) @@ -262,6 +376,9 @@ def open_reassign_image(self): self.viewer.layers.selection.active = self.im_obj_label_reassigned_layer def open_raw(self): + """ + Opens and displays the raw image in the napari viewer. + """ self.check_3d() self.im_memmap = tifffile.memmap(self.nellie.im_info.im_path) self.raw_layer = self.viewer.add_image(self.im_memmap, name='raw', colormap='gray', @@ -271,6 +388,9 @@ def open_raw(self): self.viewer.layers.selection.active = self.raw_layer def check_file_existence(self): + """ + Checks for the existence of files related to different steps of the pipeline, enabling or disabling buttons accordingly. + """ # set all other buttons to disabled first self.raw_button.setEnabled(False) self.open_preprocess_button.setEnabled(False)