diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 575f578e..58ce81ef 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ version: 2 build: os: ubuntu-24.04 tools: - python: "3.13" + python: "3.10" # Build documentation in the "docs/" directory with Sphinx sphinx: @@ -17,7 +17,8 @@ sphinx: # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt +python: + install: + - requirements: docs/requirements.txt + - requirements: requirements.txt diff --git a/README.md b/README.md index 3a5b49ef..40eec071 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,7 @@ **Linumpy** contains tools and utilities to quickly work with serial histology and microscopy data. Those tools implement the recommended workflows and parameters used in the lab. The library and scripts can be installed locally by using: -``` -pip install linumpy -``` - -To install the tool for development, clone the repository and then install them with +To install the tool, clone the repository and then install them with ``` pip install -e . diff --git a/scripts/linum_segment_brain_3d.py b/dev/linum_segment_brain_3d.py similarity index 100% rename from scripts/linum_segment_brain_3d.py rename to dev/linum_segment_brain_3d.py diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e5437588..a4a0a625 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,8 @@ # `linumpy` coding standards * We use the [PEP8](https://peps.python.org/pep-0008/) coding standard -* OME-Zarr format extension should be `ome.zarr`, as shown in the NGFF documentation ([URL](https://ngff.openmicroscopy.org/0.4/index.html#bf2raw-layout)) +* OME-Zarr format extension should be `.ome.zarr`, as shown in the NGFF documentation ([URL](https://ngff.openmicroscopy.org/0.4/index.html#bf2raw-layout)) * All scripts/methods should explicitly document the expected resolutions and dimensions units +* We use Numpy style docstrings: https://numpydoc.readthedocs.io/en/latest/format.html +* The axis order should be Z, Y, X diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..41c270bb --- /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) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..8f55b408 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +# 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 + +import os +import sys + +sys.path.insert(0, os.path.abspath("../")) +sys.path.insert(1, os.path.abspath("../scripts")) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'linumpy' +copyright = '2024, LINUM' +author = 'LINUM' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'autoapi.extension', + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'autoapi.extension', + 'sphinxarg.ext', +] + +autoapi_dirs = ['../linumpy'] +autoapi_root = "api" +autodoc_typehints = 'description' +autoapi_ignore = ['*dev*'] +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 = 'furo' +pygments_style = 'sphinx' + +# Napoleon settings +napoleon_google_docstring = True +napoleon_numpy_docstring = True +napoleon_include_init_with_doc = False +napoleon_include_private_with_doc = False +napoleon_include_special_with_doc = True +napoleon_use_admonition_for_examples = False +napoleon_use_admonition_for_notes = False +napoleon_use_admonition_for_references = False +napoleon_use_ivar = False +napoleon_use_param = True +napoleon_use_rtype = True +napoleon_preprocess_types = False +napoleon_type_aliases = None +napoleon_attr_annotations = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..4b518769 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,17 @@ +Welcome to the LINUMPY documentation! +===================================== + +LINUMPY is a collection of tools to work with serial blockface histology data. These tools are developed by the `LINUM `_ lab at Université du Québec à Montréal (`UQAM `_, Canada). + +.. toctree:: + :maxdepth: 1 + + scripts + usage + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..370563a2 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +sphinx +sphinx-autoapi +sphinx-argparse +furo diff --git a/docs/scripts.rst b/docs/scripts.rst new file mode 100644 index 00000000..d79d18b5 --- /dev/null +++ b/docs/scripts.rst @@ -0,0 +1,198 @@ +Scripts +======= + +.. toctree:: + :maxdepth: 1 + +The following scripts are available in the `scripts` directory: + +linum_aip +--------- +.. argparse:: + :filename: ../scripts/linum_aip.py + :func: _build_arg_parser + :prog: linum_aip.py + +linum_compensate_attenuation +---------------------------- +.. argparse:: + :filename: ../scripts/linum_compensate_attenuation.py + :func: _build_arg_parser + :prog: linum_compensate_attenuation.py + +linum_compensate_for_psf +------------------------ +.. argparse:: + :filename: ../scripts/linum_compensate_for_psf.py + :func: _build_arg_parser + :prog: linum_compensate_for_psf.py + +linum_compensate_illumination_2d +-------------------------------- +.. argparse:: + :filename: ../scripts/linum_compensate_illumination_2d.py + :func: _build_arg_parser + :prog: linum_compensate_illumination_2d.py + +linum_compute_attenuation +------------------------- +.. argparse:: + :filename: ../scripts/linum_compute_attenuation.py + :func: _build_arg_parser + :prog: linum_compute_attenuation.py + +linum_compute_attenuationBiasField +---------------------------------- +.. argparse:: + :filename: ../scripts/linum_compute_attenuationBiasField.py + :func: _build_arg_parser + :prog: linum_compute_attenuationBiasField.py + +linum_convert_bin_to_nii +------------------------ +.. argparse:: + :filename: ../scripts/linum_convert_bin_to_nii.py + :func: _build_arg_parser + :prog: linum_convert_bin_to_nii.py + +linum_convert_nifti_to_zarr +--------------------------- +.. argparse:: + :filename: ../scripts/linum_convert_nifti_to_zarr.py + :func: _build_arg_parser + :prog: linum_convert_nifti_to_zarr.py + +linum_convert_omezarr_to_nifti +------------------------------ +.. argparse:: + :filename: ../scripts/linum_convert_omezarr_to_nifti.py + :func: _build_arg_parser + :prog: linum_convert_omezarr_to_nifti.py + +linum_convert_zarr_to_omezarr +----------------------------- +.. argparse:: + :filename: ../scripts/linum_convert_zarr_to_omezarr.py + :func: _build_arg_parser + :prog: linum_convert_zarr_to_omezarr.py + +linum_create_all_mosaicgrids_2d +------------------------------- +.. argparse:: + :filename: ../scripts/linum_create_all_mosaicgrids_2d.py + :func: _build_arg_parser + :prog: linum_create_all_mosaicgrids_2d.py + +linum_create_mosaic_grid_2d +--------------------------- +.. argparse:: + :filename: ../scripts/linum_create_mosaic_grid_2d.py + :func: _build_arg_parser + :prog: linum_create_mosaic_grid_2d.py + +linum_create_mosaic_grid_3d +--------------------------- +.. argparse:: + :filename: ../scripts/linum_create_mosaic_grid_3d.py + :func: _build_arg_parser + :prog: linum_create_mosaic_grid_3d.py + +linum_crop_tiles_2d +------------------- +.. argparse:: + :filename: ../scripts/linum_crop_tiles_2d.py + :func: _build_arg_parser + :prog: linum_crop_tiles_2d.py + +linum_detect_focal_curvature +---------------------------- +.. argparse:: + :filename: ../scripts/linum_detect_focal_curvature.py + :func: _build_arg_parser + :prog: linum_detect_focal_curvature.py + +linum_download_allen +-------------------- +.. argparse:: + :filename: ../scripts/linum_download_allen.py + :func: _build_arg_parser + :prog: linum_download_allen.py + +linum_estimate_illumination_2d +------------------------------ +.. argparse:: + :filename: ../scripts/linum_estimate_illumination_2d.py + :func: _build_arg_parser + :prog: linum_estimate_illumination_2d.py + +linum_estimate_transform_2d +--------------------------- +.. argparse:: + :filename: ../scripts/linum_estimate_transform_2d.py + :func: _build_arg_parser + :prog: linum_estimate_transform_2d.py + +linum_estimate_xyShift_fromMetadata +----------------------------------- +.. argparse:: + :filename: ../scripts/linum_estimate_xyShift_fromMetadata.py + :func: _build_arg_parser + :prog: linum_estimate_xyShift_fromMetadata.py + +linum_fix_lateral_illumination_3d +----------------------------------- +.. argparse:: + :filename: ../scripts/linum_fix_lateral_illumination_3d.py + :func: _build_arg_parser + :prog: linum_fix_lateral_illumination_3d.py + +linum_reorient_to_ras +--------------------- +.. argparse:: + :filename: ../scripts/linum_reorient_to_ras.py + :func: _build_arg_parser + :prog: linum_reorient_to_ras.py + +linum_resample_nifti +-------------------- +.. argparse:: + :filename: ../scripts/linum_resample_nifti.py + :func: _build_arg_parser + :prog: linum_resample_nifti.py + +linum_stack_slices +------------------ +.. argparse:: + :filename: ../scripts/linum_stack_slices.py + :func: _build_arg_parser + :prog: linum_stack_slices.py + +linum_stitch_2d +--------------- +.. argparse:: + :filename: ../scripts/linum_stitch_2d.py + :func: _build_arg_parser + :prog: linum_stitch_2d.py + +linum_stitch_3d +--------------- +.. argparse:: + :filename: ../scripts/linum_stitch_3d.py + :func: _build_arg_parser + :prog: linum_stitch_3d.py + +linum_view_omezarr +------------------ +.. argparse:: + :filename: ../scripts/linum_view_omezarr.py + :func: _build_arg_parser + :prog: linum_view_omezarr.py + +linum_view_zarr +--------------- +.. argparse:: + :filename: ../scripts/linum_view_zarr.py + :func: _build_arg_parser + :prog: linum_view_zarr.py + + diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 00000000..22a8d159 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,40 @@ +Usage +===== + +The Nextflow workflows in the `workflow` directory can be used to perform reconstruction of the serial OCT data. + +Cluster Reconstruction +---------------------- +To submit a reconstruction on a DigitalAlliance cluster (e.g. Béluga or Narval), you need to prepare a bash script describing the job parameters. +Here's an example of such a script for a 2.5D reconstruction. + +.. note:: + + For this example, the nextflow pipeline file, nextflow configuration file and the Singularity image containing a compiled version of linumpy are in the same directory as the reconstruction directory. **See the workflow documentation to know how to adapt this example for your specific pipeline** + +.. code-block:: bash + + #!/bin/sh + #SBATCH --nodes=1 + #SBATCH --cpus-per-task=40 + #SBATCH --mem=92G + #SBATCH --time=1:00:00 + #SBATCH --mail-type=ALL + #SBATCH --mail-user= + #SBATCH --account= + + module load nextflow + module load apptainer + + # Parameters + DIRECTORY= + WORKFLOW_FILE=$DIRECTORY/workflow_reconstruction_2.5d.nf + CONFIG_FILE=$DIRECTORY/reconstruction_2.5d_beluga.config + SINGULARITY=$DIRECTORY/linumpy.sif + + nextflow run $WORKFLOW_FILE -c $CONFIG_FILE -with-singularity $SINGULARITY \ + --directory $DIRECTORY -resume + + + + diff --git a/linumpy/io/allen.py b/linumpy/io/allen.py index 1b91403a..92dd4949 100644 --- a/linumpy/io/allen.py +++ b/linumpy/io/allen.py @@ -15,6 +15,7 @@ def download_template(resolution: int, cache: bool = True, cache_dir: str = ".data/") -> sitk.Image: """Download a 3D average mouse brain + Parameters ---------- resolution @@ -23,6 +24,7 @@ def download_template(resolution: int, cache: bool = True, cache_dir: str = ".da Keep the downloaded volume in cache cache_dir Cache directory + Returns ------- Allen average mouse brain. diff --git a/linumpy/io/npz.py b/linumpy/io/npz.py index 9ea80168..654b89a9 100644 --- a/linumpy/io/npz.py +++ b/linumpy/io/npz.py @@ -2,20 +2,25 @@ from typing import Any, Optional, Tuple -def write_numpy(npz_path: str, *, data: Optional[Any]=None, metadata: Optional[Any]=None): +def write_numpy(npz_path: str, *, data: Optional[Any] = None, metadata: Optional[Any] = None): """ Writes data and metadata to a compressed numpy (.npz) file. + Data and metadata are wrapped in a numpy array before being written to the file. Data and metadata are stored in the 'data' and 'metadata' keys of the .npz file. - Parameters: - npz_path (str): The path to the .npz file to write to. - data (Any, optional): The data to write to the file. Defaults to None. - metadata (Any, optional): The metadata to write to the file. Defaults to None. + Parameters + ---------- + npz_path: + The path to the .npz file to write to. + data: + The data to write to the file. Defaults to None. + metadata: + The metadata to write to the file. Defaults to None. """ np.savez_compressed( - npz_path, - data=np.array([data]), + npz_path, + data=np.array([data]), metadata=np.array([metadata]), types=np.array([{ 'data': type(data), @@ -28,11 +33,15 @@ def read_numpy(npz_path: str) -> Tuple[Any, Any]: """ Reads data and metadata from a compressed numpy (.npz) file. - Parameters: - npz_path (str): The path to the .npz file to read from. + Parameters + ---------- + npz_path: + The path to the .npz file to read from. - Returns: - Tuple[Any, Any]: A tuple containing the data and metadata read from the file. + Returns + ------- + Tuple[Any, Any]: + A tuple containing the data and metadata read from the file. """ npz = np.load(npz_path, allow_pickle=True) return npz['data'][0], npz['metadata'][0] @@ -42,11 +51,15 @@ def read_numpy_data(npz_path: str) -> Tuple[Any, type]: """ Reads only the data from a compressed numpy (.npz) file. - Parameters: - npz_path (str): The path to the .npz file to read from. + Parameters + ---------- + npz_path: + The path to the .npz file to read from. - Returns: - Tuple[Any, type]: The data and its type read from the file. + Returns + ------- + Tuple[Any, type]: + The data and its type read from the file. """ npz = np.load(npz_path, allow_pickle=True) return npz['data'][0], npz['types'][0]['data'] @@ -56,24 +69,28 @@ def read_numpy_metadata(npz_path: str) -> Tuple[Any, type]: """ Reads only the metadata from a compressed numpy (.npz) file. - Parameters: - npz_path (str): The path to the .npz file to read from. + Parameters + ---------- + npz_path: + The path to the .npz file to read from. - Returns: - Tuple[Any, type]: The metadata and its type read from the file. + Returns + ------- + Tuple[Any, type]: + The metadata and its type read from the file. """ npz = np.load(npz_path, allow_pickle=True) return npz['metadata'][0], npz['types'][0]['metadata'] def _example_one(): - # Usage example + """Usage example""" from skimage import data as skdata array = skdata.coins() write_numpy( - './coins.npz', - data=array, + './coins.npz', + data=array, metadata={ 'example': 'this is an example of metadata' } @@ -85,6 +102,7 @@ def _example_one(): def _example_two(): + """Usage example with a custom class""" class Person: def __init__(self, name, age): self.name = name @@ -92,16 +110,16 @@ def __init__(self, name, age): def __repr__(self): return f'Person(name={self.name} age={self.age})' - + person = Person('John', 30) write_numpy( - './person.npz', - data=person, + './person.npz', + data=person, metadata={ 'example': 'this file contains a person object' } ) - + data, data_type = read_numpy_data('./person.npz') print(data) print(data_type) @@ -109,4 +127,3 @@ def __repr__(self): metadata, metadata_type = read_numpy_metadata('./person.npz') print(metadata) print(metadata_type) - diff --git a/linumpy/microscope/oct.py b/linumpy/microscope/oct.py index 72e01166..7305676d 100644 --- a/linumpy/microscope/oct.py +++ b/linumpy/microscope/oct.py @@ -10,7 +10,13 @@ class OCT: def __init__(self, directory: str): - """ Spectral-domain OCT class to reconstruct the data""" + """Class to work with our custom Spectral OCT data format + + Attributes + ---------- + directory : str + + """ self.directory = Path(directory) self.info_filename = self.directory / "info.txt" self.info = {} @@ -20,9 +26,10 @@ def __init__(self, directory: str): def read_scan_info(self, filename: str): """ Read the scan information file + Parameters ---------- - filename + filename: Path to the scan_file written by the OCT (.txt) """ with open(filename, "r") as f: diff --git a/linumpy/reconstruction.py b/linumpy/reconstruction.py index 86b55b1e..4dd6269a 100644 --- a/linumpy/reconstruction.py +++ b/linumpy/reconstruction.py @@ -1,11 +1,31 @@ +import re from pathlib import Path +from typing import Tuple, List + +import numpy as np from tqdm.auto import tqdm -import re + from linumpy.microscope.oct import OCT -import numpy as np -def get_tiles_ids(directory, z: int = None): - """Analyzes a directory and detects all the tiles in contains""" + +def get_tiles_ids(directory, z: int = None) -> Tuple[List, List]: + """ + Analyzes a directory and detects all the tiles in contains + + Parameters + ---------- + directory : str + Full path to the directory containing the raw mosaic tiles + z : int, optional + Slice to process + + Returns + ------- + tiles: List(str) + List of tiles path + tile_ids : List(Tuple(int, int)) + List of mosaic position IDS for each tile + """ input_directory = Path(directory) # Get a list of the input tiles @@ -30,7 +50,29 @@ def get_tiles_ids(directory, z: int = None): return tiles, tile_ids -def get_mosaic_info(directory, z: int, overlap_fraction: float = 0.2, use_stage_positions: bool = False): + +def get_mosaic_info(directory, z: int, overlap_fraction: float = 0.2, + use_stage_positions: bool = False) -> dict: + """ + Reads the mosaic info from a directory + + Parameters + ---------- + directory : str + Full path to a directory containing the raw mosaic tiles + z : + Slice to process + overlap_fraction : + Approximate overlap fraction between tiles (from 0 to 1) + use_stage_positions: + Use the recorded stage positions instead of the overlap fraction + to compute the tile positions. + + Returns + ------- + dict + Mosaic information dictionary + """ input_directory = Path(directory) # Get a list of the input tiles @@ -81,7 +123,7 @@ def get_mosaic_info(directory, z: int, overlap_fraction: float = 0.2, use_stage_ ymin_mm = np.min([p[1] for p in tiles_positions_mm]) - oct.dimension[1] / 2 xmax_mm = np.max([p[0] for p in tiles_positions_mm]) + oct.dimension[0] / 2 ymax_mm = np.max([p[1] for p in tiles_positions_mm]) + oct.dimension[1] / 2 - mosaic_center_mm = ((xmin_mm+xmax_mm)/2, (ymin_mm+ymax_mm)/2) + mosaic_center_mm = ((xmin_mm + xmax_mm) / 2, (ymin_mm + ymax_mm) / 2) mosaic_width_mm = xmax_mm - xmin_mm mosaic_height_mm = ymax_mm - ymin_mm @@ -108,4 +150,4 @@ def get_mosaic_info(directory, z: int, overlap_fraction: float = 0.2, use_stage_ "tile_shape_mm": oct.dimension, "tile_resolution": oct.resolution, } - return info \ No newline at end of file + return info diff --git a/linumpy/segmentation.py b/linumpy/segmentation.py index 86a109aa..f6760494 100644 --- a/linumpy/segmentation.py +++ b/linumpy/segmentation.py @@ -8,6 +8,7 @@ def segmentOCT3D(vol: np.ndarray, k:int=5, useLog:bool=True, thresholdMethod:str="otsu") -> np.ndarray: """To segment an S-OCT brain in 3D using thresholding and morphological watershed + Parameters ---------- vol @@ -18,6 +19,7 @@ def segmentOCT3D(vol: np.ndarray, k:int=5, useLog:bool=True, thresholdMethod:str Transform the pixel intensity with a log before computing mask thresholdMethod 'ostu', 'triangle' + Returns ------- ndarray @@ -53,10 +55,12 @@ def segmentOCT3D(vol: np.ndarray, k:int=5, useLog:bool=True, thresholdMethod:str def fillHoles_2Dand3D(mask: np.ndarray) -> np.ndarray: """Fill holes in a 2D or 3D mask + Parameters ---------- mask The mask to fill + Returns ------- ndarray @@ -81,6 +85,7 @@ def fillHoles_2Dand3D(mask: np.ndarray) -> np.ndarray: def removeBottom(mask: np.ndarray, k:int=10, axis:int=2, inverse:bool=False, fillHoles:bool=False) -> np.ndarray: """Remove the bottom side of the mask. + Parameters ---------- mask @@ -93,6 +98,7 @@ def removeBottom(mask: np.ndarray, k:int=10, axis:int=2, inverse:bool=False, fil Inverse the operation fillHoles Fill holes in the mask + Returns ------- ndarray diff --git a/linumpy/stitching/topology.py b/linumpy/stitching/topology.py index 52ce6fe1..e4eec268 100644 --- a/linumpy/stitching/topology.py +++ b/linumpy/stitching/topology.py @@ -1,20 +1,19 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -""" This module uses graph theory to describe and interact with the mosaic topology. - -.. moduleauthor:: Joël Lefebvre - +""" +This module uses graph theory to describe and interact with the mosaic topology. """ -import sys - +import SimpleITK as sitk import networkx as nx import numpy as np -import SimpleITK as sitk def generate_default(nX, nY): - """Generates a default topology where all tiles in mosaic are nodes and all neighbor relation is an edge. + """ + Generates a default topology where all tiles in mosaic are nodes and all + neighbor relation is an edge. + Parameters ---------- nX: int @@ -58,7 +57,7 @@ def generate_default(nX, nY): outX = np.tile(np.arange(1, nX), (nY,)) y = np.zeros(inX.shape, dtype=np.int) for iY in range(nY): - y[iY * (nX - 1) : iY * (nX - 1) + nX - 1] = iY + y[iY * (nX - 1): iY * (nX - 1) + nX - 1] = iY inX += nX * y outX += nX * y @@ -82,11 +81,20 @@ def generate_default(nX, nY): def generate_graphFromEdges(sources, targets): - """Generates a graph for a list of source and target nodes + """ + Generates a graph for a list of source and target nodes + + Parameters + ---------- + sources: + List of source node position. + targets: + List of target node position. - :param sources: List of source node position. - :param targets: List of target node position. - :returns: topo : NetworkX graph object describing the mosaic topology + Returns + ------- + topo : + NetworkX graph object describing the mosaic topology """ topo = nx.DiGraph() @@ -104,11 +112,21 @@ def generate_graphFromEdges(sources, targets): def remove_agarose(topo, tissueMask): - """Remove agarose nodes from the mosaic topology + """ + Remove agarose nodes from the mosaic topology - :param topo: NetworkX graph object describing the mosaic topology - :param tissueMask: (m, n) bool ndarray of the tissue mask for this slice. - :returns: topo: Updated mosaic topology (without the agarose nodes) + Parameters + ---------- + topo: + NetworkX graph object describing the mosaic topology + + tissueMask: + (m, n) bool ndarray of the tissue mask for this slice. + + Returns + ------- + topo: + Updated mosaic topology (without the agarose nodes) """ agarosePos = np.where(tissueMask == 0) diff --git a/linumpy/utils/mosaic_grid.py b/linumpy/utils/mosaic_grid.py index 16ced938..94b9728f 100644 --- a/linumpy/utils/mosaic_grid.py +++ b/linumpy/utils/mosaic_grid.py @@ -23,7 +23,7 @@ class MosaicGrid(): .. note:: This class can only deal with 2D mosaic grids for now. To generate a 2D mosaic grid from a collection of volumetric - tiles for a given slice, you can use the :ref:`script-linum-create-mosaic-grid` script. + tiles for a given slice, you can use the `linum_create_mosaic_grid_3d.py` script. """ diff --git a/linumpy/utils_images.py b/linumpy/utils_images.py index d87fced6..2b6972f5 100644 --- a/linumpy/utils_images.py +++ b/linumpy/utils_images.py @@ -8,12 +8,14 @@ def normalize(img: np.ndarray, saturation: float = 99.7) -> np.ndarray: """Normalize an image between 0 and 1. + Parameters ---------- img : np.ndarray The image to normalize. saturation : float, optional The saturation value for the normalization + Returns ------- np.ndarray @@ -28,12 +30,14 @@ def normalize(img: np.ndarray, saturation: float = 99.7) -> np.ndarray: def get_overlay_as_rgb(img1: np.ndarray, img2: np.ndarray) -> np.ndarray: """Combine the two images into a single RGB image. + Parameters ---------- img1 : np.ndarray The first image. img2 : np.ndarray The second image. + Returns ------- np.ndarray @@ -48,12 +52,14 @@ def get_overlay_as_rgb(img1: np.ndarray, img2: np.ndarray) -> np.ndarray: def match_shape(img1: np.ndarray, img2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Match the shape of two images by padding the smallest one. + Parameters ---------- img1 : np.ndarray The first image. img2 : np.ndarray The second image. + Returns ------- Tuple[np.ndarray, np.ndarray] @@ -75,8 +81,22 @@ def match_shape(img1: np.ndarray, img2: np.ndarray) -> Tuple[np.ndarray, np.ndar return padded_images -def display_overlap(img1, img2, title=None, do_normalization=False): - if do_normalization: +def display_overlap(img1: np.ndarray, img2: np.ndarray, title: str = None, + normalize: bool = False) -> None: + """Display the overlap of two images. + + Parameters + ---------- + img1 + The first image. + img2 + The second image. + title + The title of the plot. + normalize + Whether to normalize the images. + """ + if normalize: img1 = normalize(img1) img2 = normalize(img2) img1, img2 = match_shape(img1, img2) @@ -91,6 +111,7 @@ def display_overlap(img1, img2, title=None, do_normalization=False): def apply_xy_shift(img: np.ndarray, reference: np.ndarray, dx: int, dy: int) -> np.ndarray: """Apply a shift to the image in the xy plane. + Parameters ---------- img : np.ndarray @@ -101,6 +122,11 @@ def apply_xy_shift(img: np.ndarray, reference: np.ndarray, dx: int, dy: int) -> The shift in x. dy : int The shift in y. + + Returns + ------- + np.ndarray + The shifted image """ fixed = sitk.GetImageFromArray(reference) moving = sitk.GetImageFromArray(img) @@ -117,4 +143,5 @@ def apply_xy_shift(img: np.ndarray, reference: np.ndarray, dx: int, dy: int) -> resampler.SetTransform(transform) warped_moving_image = resampler.Execute(moving) img_warped = sitk.GetArrayFromImage(warped_moving_image) + return img_warped diff --git a/requirements.txt b/requirements.txt index 7f4662dc..f736eee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ dask>=2024.10.0 SimpleITK tqdm pqdm -pybasic-illumination-correction +pybasic-illumination-correction==0.1.1 zarr<=2.18.2 ome-zarr>=0.9.0 +napari \ No newline at end of file diff --git a/scripts/linum_axis_XYZ_to_ZYX.py b/scripts/linum_axis_XYZ_to_ZYX.py deleted file mode 100644 index 65b0aaad..00000000 --- a/scripts/linum_axis_XYZ_to_ZYX.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import nibabel as nib -import numpy as np -import argparse -from pathlib import Path - -""" Change the axis from XYZ order to ZYX, necessary before converting to .zarr format -""" - -def _build_arg_parser(): - p = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) - p.add_argument("input_image", - help="Full path to a .nii image, with axis in XYZ order.") - p.add_argument("output_image", - help="Full path to the output .nii image, with axis in ZYX order") - p.add_argument("--resolution_xy", type=float, default=3.0, - help="Lateral (xy) resolution in micron. (default=%(default)s)") - p.add_argument("--resolution_z", type=float, default=200, - help="Axial (z) resolution in microns. (default=%(default)s)") - return p - -def main(): - # Parse arguments - p = _build_arg_parser() - args = p.parse_args() - - # Load the input image - img = nib.load(Path(args.input_image)) - img_array = img.get_fdata() - print("input array dimension :", np.shape(img_array)) - # Check the number of dimensions - num_dimensions = img_array.ndim - if num_dimensions == 4: - # If there's a 4th axis (singleton dimension), remove it - img_array = np.squeeze(img_array, axis=3) - - # Change the order of the axis to ZYX - output_array = np.transpose(img_array, (2, 1, 0)) - print("output array dimension :", np.shape(output_array)) - - # Save the image - affine = np.eye(4) - affine[0, 0] = args.resolution_xy / 1000.0 # x resolution in mm - affine[1, 1] = args.resolution_xy / 1000.0 # y resolution in mm - affine[2, 2] = args.resolution_z / 1000.0 # z resolution in mm - - img = nib.Nifti1Image(output_array, affine) - output_file = Path(args.output_image) - output_file.parent.mkdir(exist_ok=True, parents=True) - nib.save(img, output_file) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/linum_compensate_attenuation.py b/scripts/linum_compensate_attenuation.py index ae696d60..d00a8b2e 100644 --- a/scripts/linum_compensate_attenuation.py +++ b/scripts/linum_compensate_attenuation.py @@ -6,7 +6,6 @@ """ import argparse -from pathlib import Path import dask.array as da from linumpy.io.zarr import save_zarr, read_omezarr diff --git a/scripts/linum_compensate_illumination.py b/scripts/linum_compensate_illumination_2d.py similarity index 95% rename from scripts/linum_compensate_illumination.py rename to scripts/linum_compensate_illumination_2d.py index 678ee5b2..13f4c5a6 100644 --- a/scripts/linum_compensate_illumination.py +++ b/scripts/linum_compensate_illumination_2d.py @@ -1,7 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""Uses the BaSiC algorithm to estimate and compensate illumination inhomogeneities in a mosaic grid""" +""" +Estimate and compensate illumination inhomogeneities in a 2D mosaic grid with BaSiC +""" + import argparse from pathlib import Path @@ -40,9 +43,9 @@ def main(): # Parameters input_file = Path(args.input_image) - if args.output_image is not None : + if args.output_image is not None: output_file = Path(args.output_image) - else : + else: output_file = input_file.parent / Path(input_file.stem + "_compensated" + input_file.suffix) flatfield_file = Path(args.flatfield) darkfield_file = Path(args.darkfield) @@ -92,5 +95,6 @@ def main(): output_file.parent.mkdir(exist_ok=True, parents=True) sitk.WriteImage(sitk.GetImageFromArray(fixed_image), str(output_file)) + if __name__ == "__main__": main() diff --git a/scripts/linum_convert_tiff_to_nifti.py b/scripts/linum_convert_tiff_to_nifti.py deleted file mode 100644 index 21c601dc..00000000 --- a/scripts/linum_convert_tiff_to_nifti.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import argparse -import SimpleITK as sitk -from pathlib import Path - -def _build_arg_parser(): - p = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) - p.add_argument("input_folder", - help="Full path to a folder containing TIFF images") - p.add_argument("output_folder", - help="Full path to the output folder which will contain the nifti (.nii.gz) images") - return p - -def main(): - # Parse arguments - p = _build_arg_parser() - args = p.parse_args() - - input_folder = Path(args.input_folder) - output_folder = Path(args.output_folder) - - output_folder.mkdir(exist_ok=True, parents=True) - - # List the TIFF files in the input folder - tiff_files = [file for file in input_folder.glob('*') if file.suffix in [".tif", ".tiff",".TIF",".TIFF"] ] - for tiff_file in tiff_files: - - # Create the output path - output_nifti_file = output_folder / Path(tiff_file.stem + '.nii.gz') - - # Load the TIFF image - image = sitk.ReadImage(tiff_file) - - # Save the image as a nifti file - sitk.WriteImage(image, output_nifti_file) - -if __name__ == "__main__": - main() - diff --git a/scripts/linum_create_all_mosaicgrids.py b/scripts/linum_create_all_mosaicgrids_2d.py similarity index 100% rename from scripts/linum_create_all_mosaicgrids.py rename to scripts/linum_create_all_mosaicgrids_2d.py diff --git a/scripts/linum_create_mosaic_grid.py b/scripts/linum_create_mosaic_grid_2d.py similarity index 100% rename from scripts/linum_create_mosaic_grid.py rename to scripts/linum_create_mosaic_grid_2d.py diff --git a/scripts/linum_crop_tiles.py b/scripts/linum_crop_tiles_2d.py similarity index 100% rename from scripts/linum_crop_tiles.py rename to scripts/linum_crop_tiles_2d.py diff --git a/scripts/linum_estimate_illumination.py b/scripts/linum_estimate_illumination_2d.py similarity index 100% rename from scripts/linum_estimate_illumination.py rename to scripts/linum_estimate_illumination_2d.py diff --git a/scripts/linum_estimate_transform.py b/scripts/linum_estimate_transform_2d.py similarity index 100% rename from scripts/linum_estimate_transform.py rename to scripts/linum_estimate_transform_2d.py diff --git a/scripts/linum_fix_illumination_3d.py b/scripts/linum_fix_lateral_illumination_3d.py similarity index 99% rename from scripts/linum_fix_illumination_3d.py rename to scripts/linum_fix_lateral_illumination_3d.py index bb154ea1..123b4ddf 100644 --- a/scripts/linum_fix_illumination_3d.py +++ b/scripts/linum_fix_lateral_illumination_3d.py @@ -12,7 +12,6 @@ import shutil import tempfile from pathlib import Path -from os.path import join as pjoin import dask.array as da @@ -24,6 +23,7 @@ from pqdm.processes import pqdm from linumpy.io.zarr import save_zarr, read_omezarr + # TODO: add option to export the flatfields and darkfields def _build_arg_parser(): diff --git a/scripts/linum_intensity_normalization.py b/scripts/linum_intensity_normalization.py deleted file mode 100644 index d79aaded..00000000 --- a/scripts/linum_intensity_normalization.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -"""Normalize the intensity in a given nifty image""" - -import nibabel as nib -import numpy as np -import numpy as np -import argparse -from pathlib import Path -from tqdm import tqdm - -def _build_arg_parser(): - p = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawTextHelpFormatter) - p.add_argument("input_image", - help="Full path to the input nifti volume.") - p.add_argument("output_image", - help="Full path to the output volume") - p.add_argument("--resolution_xy", type=float, default=3.0, - help="Lateral (xy) resolution in micron. (default=%(default)s)") - p.add_argument("--resolution_z", type=float, default=3.5, - help="Axial (z) resolution in micron. (default=%(default)s)") - return p - -def main() : - # Parse arguments - p = _build_arg_parser() - args = p.parse_args() - input_path = Path(args.input_image) - output_path = Path(args.output_image) - output_path.parent.mkdir(exist_ok=True, parents=True) - - # Load the image and create empty output array - img = nib.load(input_path) - array = img.get_fdata() - dim = np.shape(array) - output_array = np.zeros(dim, dtype=np.uint32) - - # The intensity of each slice is normalized - for z in tqdm(range(dim[2])): - # Calculate the minimum and the 99th percentile values of the slice - min_intensity = np.min(array[:,:,z]) - percentile_99 = np.percentile(array[:,:,z], 99) - # Perform intensity normalization on the slice - normalized_array = np.clip(array[:,:,z], min_intensity, percentile_99) - normalized_array = (normalized_array - min_intensity) / (percentile_99 - min_intensity) * 255 - # Save the normalized slice in the output array - output_array[:,:,z]=normalized_array - - # Save the image - affine = np.eye(4) - affine[0, 0] = args.resolution_xy / 1000.0 # x resolution in mm - affine[1, 1] = args.resolution_xy / 1000.0 # y resolution in mm - affine[2, 2] = args.resolution_z / 1000.0 # z resolution in mm - - img_normalized = nib.Nifti1Image(output_array, affine) - nib.save(img_normalized, output_path) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/linum_resample.py b/scripts/linum_resample_nifti.py similarity index 100% rename from scripts/linum_resample.py rename to scripts/linum_resample_nifti.py diff --git a/shell_scripts/cmd_2.5d_reconstruction_docker.sh b/shell_scripts/cmd_2.5d_reconstruction_docker.sh deleted file mode 100644 index 17597c11..00000000 --- a/shell_scripts/cmd_2.5d_reconstruction_docker.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -# Parameters -DIRECTORY=/Users/jlefebvre/Downloads/2024-05-16-Mounier-15-Horizontal -WORKFLOW_FILE=$DIRECTORY/workflow_reconstruction_2.5d.nf -CONFIG_FILE=$DIRECTORY/reconstruction_2.5d_macbook.config - -nextflow run $WORKFLOW_FILE -c $CONFIG_FILE --directory $DIRECTORY -resume diff --git a/shell_scripts/cmd_soct_reconstruction_2-5d.sh b/shell_scripts/cmd_soct_reconstruction_2-5d.sh deleted file mode 100644 index b335b7a5..00000000 --- a/shell_scripts/cmd_soct_reconstruction_2-5d.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh -#SBATCH --nodes=1 -#SBATCH --cpus-per-task=40 -#SBATCH --mem=92G -#SBATCH --time=1:00:00 -#SBATCH --mail-type=ALL -#SBATCH --mail-user=lefebvre.joel@uqam.ca -#SBATCH --account=def-jolefc - -module load nextflow -module load apptainer - -# Parameters -DIRECTORY=/lustre04/scratch/jolefc/2024-06-05-S34-Coronal -WORKFLOW_FILE=$DIRECTORY/workflow_reconstruction_2.5d.nf -CONFIG_FILE=$DIRECTORY/reconstruction_2.5d_beluga.config -SINGULARITY=$DIRECTORY/linumpy.sif - -nextflow run $WORKFLOW_FILE -c $CONFIG_FILE -with-singularity $SINGULARITY --directory $DIRECTORY -resume diff --git a/workflows/workflow_reconstruction_2.5d.nf b/workflows/workflow_reconstruction_2.5d.nf index c050acb2..7833843f 100644 --- a/workflows/workflow_reconstruction_2.5d.nf +++ b/workflows/workflow_reconstruction_2.5d.nf @@ -51,7 +51,7 @@ process crop_tiles { tuple val(mosaic_directory.baseName), path("${mosaic_directory.baseName}_cropped.tif") script: """ - linum_crop_tiles $mosaic_directory ${mosaic_directory.baseName}_cropped.tif --xmin ${params.xmin} --xmax ${params.xmax} --ymin ${params.ymin} --ymax ${params.ymax} --tile_shape ${params.tile_nx} ${params.tile_ny} + linum_crop_tiles_2d.py $mosaic_directory ${mosaic_directory.baseName}_cropped.tif --xmin ${params.xmin} --xmax ${params.xmax} --ymin ${params.ymin} --ymax ${params.ymax} --tile_shape ${params.tile_nx} ${params.tile_ny} """ } @@ -63,7 +63,7 @@ process estimate_illumination_bias { tuple val(key), path("${key}_flatfield.nii"), path("${key}_darkfield.nii") script: """ - linum_estimate_illumination $mosaic_grid ${key}_flatfield.nii --tile_shape ${params.nx} ${params.ny} --output_darkfield ${key}_darkfield.nii + linum_estimate_illumination_2d.py $mosaic_grid ${key}_flatfield.nii --tile_shape ${params.nx} ${params.ny} --output_darkfield ${key}_darkfield.nii """ } @@ -75,7 +75,7 @@ process compensate_illumination_bias { tuple val(key), path("${key}_mosaic_grid_compensated.nii.gz") script: """ - linum_compensate_illumination $mosaic_grid ${key}_mosaic_grid_compensated.nii.gz --flatfield $flatfield --darkfield $darkfield --tile_shape ${params.nx} ${params.ny} + linum_compensate_illumination_2d.py $mosaic_grid ${key}_mosaic_grid_compensated.nii.gz --flatfield $flatfield --darkfield $darkfield --tile_shape ${params.nx} ${params.ny} """ } @@ -87,7 +87,7 @@ process estimate_position { path "position_transform.npy" script: """ - linum_estimate_transform $mosaic_grids position_transform.npy --tile_shape ${params.nx} ${params.ny} --initial_overlap ${params.initial_overlap} + linum_estimate_transform_2d.py $mosaic_grids position_transform.npy --tile_shape ${params.nx} ${params.ny} --initial_overlap ${params.initial_overlap} """ } @@ -99,7 +99,7 @@ process stitch_mosaic { tuple val(key), path("${key}_stitched.nii") script: """ - linum_stitch_2d $image $transform ${key}_stitched.nii --blending_method diffusion --tile_shape ${params.nx} ${params.ny} + linum_stitch_2d.py $image $transform ${key}_stitched.nii --blending_method diffusion --tile_shape ${params.nx} ${params.ny} """ } @@ -113,7 +113,7 @@ process stack_mosaic { publishDir path: "${params.output_directory}", mode: 'copy' script: """ - linum_stack_slices $images stack.zarr --xy_shifts $xy_shifts --resolution_xy ${params.spacing_xy} --resolution_z ${params.spacing_z} + linum_stack_slices.py $images stack.zarr --xy_shifts $xy_shifts --resolution_xy ${params.spacing_xy} --resolution_z ${params.spacing_z} """ } @@ -126,7 +126,7 @@ process resample_stack { publishDir path: "${params.output_directory}", mode: 'copy' script: """ - linum_convert_omezarr_to_nifti $stack stack_10um.nii --resolution 10.0 + linum_convert_omezarr_to_nifti.py $stack stack_10um.nii --resolution 10.0 """ } @@ -152,7 +152,7 @@ process convert_to_omezarr { publishDir path: "${params.output_directory}", mode: 'move' script: """ - linum_convert_zarr_to_omezarr $stack stack.ome_zarr -r ${params.spacing_z} ${params.spacing_xy} ${params.spacing_xy} + linum_convert_zarr_to_omezarr.py $stack stack.ome_zarr -r ${params.spacing_z} ${params.spacing_xy} ${params.spacing_xy} """ } diff --git a/workflows/workflow_soct_3d_slice_reconstruction.nf b/workflows/workflow_soct_3d_slice_reconstruction.nf index eb2fecb4..698f950c 100644 --- a/workflows/workflow_soct_3d_slice_reconstruction.nf +++ b/workflows/workflow_soct_3d_slice_reconstruction.nf @@ -46,7 +46,7 @@ process fix_illumination { //publishDir path: "${params.outputDir}", mode: 'copy' script: """ - linum_fix_illumination_3d.py ${mosaic_grid} mosaic_grid_3d_${params.resolution}um_illuminationFix.ome.zarr + linum_fix_lateral_illumination_3d.py ${mosaic_grid} mosaic_grid_3d_${params.resolution}um_illuminationFix.ome.zarr """ }