From bf0c0fa39d5347728ce2a1080a4ba12b86ac3f6d Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Tue, 13 Feb 2024 12:57:53 -0500 Subject: [PATCH 01/16] wip: fixing ftl-label --- .../images/polus-ftl-label-plugin/Dockerfile | 11 ++++---- .../images/polus-ftl-label-plugin/README.md | 25 +++++++++++-------- .../images/polus-ftl-label-plugin/VERSION | 2 +- .../polus-ftl-label-plugin/build-docker.sh | 2 +- .../ftl_rust/__init__.py | 13 ++++++++-- .../images/polus-ftl-label-plugin/plugin.json | 20 +++++++++++++-- .../polus-ftl-label-plugin/run-plugin.sh | 9 ++++--- .../images/polus-ftl-label-plugin/src/main.py | 20 ++++++++++++--- 8 files changed, 74 insertions(+), 28 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/Dockerfile b/transforms/images/polus-ftl-label-plugin/Dockerfile index 41f074c89..5124b8cc8 100644 --- a/transforms/images/polus-ftl-label-plugin/Dockerfile +++ b/transforms/images/polus-ftl-label-plugin/Dockerfile @@ -39,10 +39,11 @@ RUN pip3 install -r ${EXEC_DIR}/src/requirements.txt --no-cache-dir \ WORKDIR ${EXEC_DIR}/src -# Change to .ome.zarr to save output images as zarr files. -ENV POLUS_EXT=".ome.tif" - -# Change to WARNING for fewer logs, or DEBUG for debugging. +ENV POLUS_IMG_EXT=".ome.tif" +ENV POLUS_TAB_EXT=".csv" ENV POLUS_LOG="INFO" -ENTRYPOINT ["python3", "/opt/executables/src/main.py"] +RUN ls -la ${EXEC_DIR} + +ENTRYPOINT [ "python3", "main.py", "--help" ] +CMD ["--help"] diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index 2ec2fc5cd..8d102b260 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label +# FTL Label (v0.3.12-dev0) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. @@ -24,9 +24,10 @@ However, most of the bottleneck is in the interface between `Python` and `Rust`. The Rust implementation works with 2d and 3d images. To see detailed documentation for the `Rust` implementation you need to: - * Install [Rust](https://doc.rust-lang.org/stable/book/ch01-01-installation.html), - * add Cargo to your `PATH`, and - * run from the terminal (in this directory): `cargo doc --open`. + +* Install [Rust](https://doc.rust-lang.org/stable/book/ch01-01-installation.html), +* add Cargo to your `PATH`, and +* run from the terminal (in this directory): `cargo doc --open`. That last command will generate documentation and open a new tab in your default web browser. @@ -39,6 +40,7 @@ For more information on WIPP, visit the ## To do The following optimizations should be added to increase the speed or decrease the memory used by the plugin. + 1. Implement existing specialized C++ methods that accelerate the run length encoding operation by a factor of 5-10 ## Building @@ -54,13 +56,15 @@ Paste the contents of `plugin.json` into the pop-up window and submit. This plugin takes one input argument and one output argument: -| Name | Description | I/O | Type | -|------------------|-------------------------------------------------------|--------|------------| -| `--inpDir` | Input image collection to be processed by this plugin | Input | collection | -| `--connectivity` | City block connectivity | Input | number | -| `--outDir` | Output collection | Output | collection | +| Name | Description | I/O | Type | +| ------------------------- | -------------------------------------------------------------------- | ------ | ---------- | +| `--inpDir` | Input image collection to be processed by this plugin | Input | collection | +| `--connectivity` | City block connectivity | Input | number | +| `--binarizationThreshold` | For images containing probability values. Must be between 0 and 1.0. | Input | number | +| `--outDir` | Output collection | Output | collection | ## Example Code + ```Linux # Download some example *.tif files wget https://github.com/stardist/stardist/releases/download/0.1.0/dsb2018.zip @@ -95,7 +99,8 @@ Each new jump must be orthogonal to all previous jumps. This means that `connectivity` should have a minimum value of `1` and a maximum value equal to the dimensionality of the images. SciKit's documentation has a good illustration for 2D: -``` + +```text 1-connectivity 2-connectivity diagonal connection close-up [ ] [ ] [ ] [ ] [ ] diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index 5503126d5..302fb6164 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.10 +0.3.12-dev0 diff --git a/transforms/images/polus-ftl-label-plugin/build-docker.sh b/transforms/images/polus-ftl-label-plugin/build-docker.sh index 2a0c728f2..638c0aa38 100755 --- a/transforms/images/polus-ftl-label-plugin/build-docker.sh +++ b/transforms/images/polus-ftl-label-plugin/build-docker.sh @@ -1,4 +1,4 @@ #!/bin/bash version=$( self.bin_thresh).astype(numpy.uint8) + + # If the image is not binary, make it binary tile = (tile != 0).astype(numpy.uint8) + if tile.ndim == 2: tile = tile[numpy.newaxis, :, :] else: diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 84c99b4d1..f17a4a270 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.9", + "version": "0.3.12-dev0", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,7 +8,12 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "labshare/polus-ftl-label-plugin:0.3.8", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev0", + "baseCommand": [ + "python3", + "-m", + "polus.plugins.transforms.images.montage" + ], "inputs": [ { "name": "inpDir", @@ -21,6 +26,12 @@ "type": "number", "description": "City block connectivity", "required": true + }, + { + "name": "binarizationThreshold", + "type": "number", + "description": "For images containing probability values. Must be between 0 and 1.0.", + "required": true } ], "outputs": [ @@ -40,6 +51,11 @@ "key": "inputs.connectivity", "title": "Connectivity", "description": "City block connectivity" + }, + { + "key": "inputs.binarizationThreshold", + "title": "Binarization Threshold", + "description": "For images containing probability values. Must be between 0 and 1.0." } ] } diff --git a/transforms/images/polus-ftl-label-plugin/run-plugin.sh b/transforms/images/polus-ftl-label-plugin/run-plugin.sh index 8265a5b04..9daece870 100644 --- a/transforms/images/polus-ftl-label-plugin/run-plugin.sh +++ b/transforms/images/polus-ftl-label-plugin/run-plugin.sh @@ -7,12 +7,12 @@ data_path=$(readlink --canonicalize ../../../../data/ftl-label) POLUS_LOG="INFO" # Change to .ome.zarr to save output images as zarr files. -POLUS_EXT=".ome.tif" +POLUS_IMG_EXT=".ome.tif" # Inputs inpDir=/data/input-2d connectivity="1" - +binarizationThreshold="0.5" # Output paths outDir=/data/output @@ -20,8 +20,9 @@ outDir=/data/output docker run --mount type=bind,source="${data_path}",target=/data/ \ --user "$(id -u)":"$(id -g)" \ --env POLUS_LOG="${POLUS_LOG}" \ - --env POLUS_EXT="${POLUS_EXT}" \ - labshare/polus-ftl-label-plugin:"${version}" \ + --env POLUS_IMG_EXT="${POLUS_IMG_EXT}" \ + polusaiftl-label-plugin:"${version}" \ --inpDir ${inpDir} \ --connectivity ${connectivity} \ + --binarizationThreshold ${binarizationThreshold} \ --outDir ${outDir} \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/src/main.py b/transforms/images/polus-ftl-label-plugin/src/main.py index c24c8e587..dd8c4cce8 100644 --- a/transforms/images/polus-ftl-label-plugin/src/main.py +++ b/transforms/images/polus-ftl-label-plugin/src/main.py @@ -52,7 +52,7 @@ def filter_by_size(file_paths: List[Path], size_threshold: int) -> Tuple[List[Pa pixel_bytes = 8 elif dtype == numpy.uint16: pixel_bytes = 16 - elif dtype == numpy.uint32: + elif dtype == numpy.uint32 or dtype == numpy.float32: pixel_bytes = 32 else: pixel_bytes = 64 @@ -63,13 +63,14 @@ def filter_by_size(file_paths: List[Path], size_threshold: int) -> Tuple[List[Pa return small_files, large_files -def label_cython(input_path: Path, output_path: Path, connectivity: int): +def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thresh: float): """ Label the input image and writes labels back out. Args: input_path: Path to input image. output_path: Path for output image. connectivity: Connectivity kind. + bin_thresh: Binarization threshold. """ with ProcessManager.thread() as active_threads: with BioReader( @@ -85,6 +86,10 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int): # Load an image and convert to binary image = numpy.squeeze(reader[..., 0, 0]) + # If the image has float values, binarize it using the threshold + if image.dtype == numpy.float32 or image.dtype == numpy.float64: + image = (image > bin_thresh).astype(numpy.uint8) + if not numpy.any(image): writer.dtype = numpy.uint8 writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) @@ -125,6 +130,11 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int): help='City block connectivity, must be less than or equal to the number of dimensions', ) + parser.add_argument( + '--binarizationThreshold', dest='bin_thresh', type=str, required=True, + help='For images containing probability values. Must be between 0 and 1.0.', + ) + parser.add_argument( '--outDir', dest='outDir', type=str, required=True, help='Output collection', @@ -136,6 +146,10 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int): _connectivity = int(args.connectivity) logger.info(f'connectivity = {_connectivity}') + _bin_thresh = float(args.bin_thresh) + assert 0 <= _bin_thresh <= 1, 'bin_thresh must be between 0 and 1' + logger.info(f'bin_thresh = {_bin_thresh:.2f}') + _input_dir = Path(args.inpDir).resolve() assert _input_dir.exists(), f'{_input_dir } does not exist.' if _input_dir.joinpath('images').is_dir(): @@ -174,4 +188,4 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int): if _large_files: for _infile in _large_files: _outfile = _output_dir.joinpath(get_output_name(_infile.name)) - PolygonSet(_connectivity).read_from(_infile).write_to(_outfile) + PolygonSet(_connectivity, _bin_thresh).read_from(_infile).write_to(_outfile) From ed7a51e86f0d3d12c14a4c10766e4e60ea4f5f51 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Tue, 13 Feb 2024 13:01:06 -0500 Subject: [PATCH 02/16] fix: plugin manifest and dockerfile entrypoint --- transforms/images/polus-ftl-label-plugin/Dockerfile | 2 +- transforms/images/polus-ftl-label-plugin/README.md | 2 +- transforms/images/polus-ftl-label-plugin/VERSION | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 7 +++---- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/Dockerfile b/transforms/images/polus-ftl-label-plugin/Dockerfile index 5124b8cc8..a0e9e6d7f 100644 --- a/transforms/images/polus-ftl-label-plugin/Dockerfile +++ b/transforms/images/polus-ftl-label-plugin/Dockerfile @@ -45,5 +45,5 @@ ENV POLUS_LOG="INFO" RUN ls -la ${EXEC_DIR} -ENTRYPOINT [ "python3", "main.py", "--help" ] +ENTRYPOINT ["python3", "main.py"] CMD ["--help"] diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index 8d102b260..af83626f5 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev0) +# FTL Label (v0.3.12-dev1) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index 302fb6164..08a5116f7 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.12-dev0 +0.3.12-dev1 diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index f17a4a270..006d5efcb 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.12-dev0", + "version": "0.3.12-dev1", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,11 +8,10 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev0", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev1", "baseCommand": [ "python3", - "-m", - "polus.plugins.transforms.images.montage" + "main.py" ], "inputs": [ { From 4523a0563d597ceb6c806f1296ec6820d764b8db Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 10:04:00 -0500 Subject: [PATCH 03/16] chore: formmated the manifest --- .../images/polus-ftl-label-plugin/plugin.json | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 006d5efcb..0db779b5e 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,60 +1,60 @@ { - "name": "FTL Label", - "version": "0.3.12-dev1", - "title": "FTL Label", - "description": "Label objects in a 2d or 3d binary image.", - "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", - "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", - "repository": "https://github.com/labshare/polus-plugins", - "website": "https://ncats.nih.gov/preclinical/core/informatics", - "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev1", - "baseCommand": [ - "python3", - "main.py" - ], - "inputs": [ - { - "name": "inpDir", - "type": "collection", - "description": "Input image collection to be processed by this plugin", - "required": true - }, - { - "name": "connectivity", - "type": "number", - "description": "City block connectivity", - "required": true - }, - { - "name": "binarizationThreshold", - "type": "number", - "description": "For images containing probability values. Must be between 0 and 1.0.", - "required": true - } - ], - "outputs": [ - { - "name": "outDir", - "type": "collection", - "description": "Output collection" - } - ], - "ui": [ - { - "key": "inputs.inpDir", - "title": "Input collection", - "description": "Input image collection to be processed by this plugin" - }, - { - "key": "inputs.connectivity", - "title": "Connectivity", - "description": "City block connectivity" - }, - { - "key": "inputs.binarizationThreshold", - "title": "Binarization Threshold", - "description": "For images containing probability values. Must be between 0 and 1.0." - } - ] + "name": "FTL Label", + "version": "0.3.12-dev1", + "title": "FTL Label", + "description": "Label objects in a 2d or 3d binary image.", + "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/labshare/polus-plugins", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev1", + "baseCommand": [ + "python3", + "main.py" + ], + "inputs": [ + { + "name": "inpDir", + "type": "collection", + "description": "Input image collection to be processed by this plugin", + "required": true + }, + { + "name": "connectivity", + "type": "number", + "description": "City block connectivity", + "required": true + }, + { + "name": "binarizationThreshold", + "type": "number", + "description": "For images containing probability values. Must be between 0 and 1.0.", + "required": true + } + ], + "outputs": [ + { + "name": "outDir", + "type": "collection", + "description": "Output collection" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input collection", + "description": "Input image collection to be processed by this plugin" + }, + { + "key": "inputs.connectivity", + "title": "Connectivity", + "description": "City block connectivity" + }, + { + "key": "inputs.binarizationThreshold", + "title": "Binarization Threshold", + "description": "For images containing probability values. Must be between 0 and 1.0." + } + ] } From e552aae26b60164dce5055fbf8848f1784b15ef3 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 10:04:57 -0500 Subject: [PATCH 04/16] fix: missing bin_thresh arg to label_cython call --- .../images/polus-ftl-label-plugin/src/main.py | 114 ++++++++++-------- 1 file changed, 67 insertions(+), 47 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/src/main.py b/transforms/images/polus-ftl-label-plugin/src/main.py index dd8c4cce8..9065c38bc 100644 --- a/transforms/images/polus-ftl-label-plugin/src/main.py +++ b/transforms/images/polus-ftl-label-plugin/src/main.py @@ -2,35 +2,36 @@ import logging import os from pathlib import Path -from typing import List, Tuple +import ftl import numpy from bfio import BioReader from bfio import BioWriter -from preadator import ProcessManager - -import ftl from ftl_rust import PolygonSet +from preadator import ProcessManager -POLUS_LOG = getattr(logging, os.environ.get('POLUS_LOG', 'INFO')) -POLUS_EXT = os.environ.get('POLUS_EXT', '.ome.tif') # TODO: Figure out how to use this +POLUS_LOG = getattr(logging, os.environ.get("POLUS_LOG", "INFO")) +POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") # TODO: Figure out how to use this # Initialize the logger logging.basicConfig( - format='%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s', - datefmt='%d-%b-%y %H:%M:%S', + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", ) logger = logging.getLogger("main") logger.setLevel(POLUS_LOG) def get_output_name(filename: str) -> str: - name = filename.split('.ome')[0] - return f'{name}{POLUS_EXT}' + name = filename.split(".ome")[0] + return f"{name}{POLUS_EXT}" -def filter_by_size(file_paths: List[Path], size_threshold: int) -> Tuple[List[Path], List[Path]]: - """ Partitions the input files by the memory-footprint for the images. +def filter_by_size( + file_paths: list[Path], + size_threshold: int, +) -> tuple[list[Path], list[Path]]: + """Partitions the input files by the memory-footprint for the images. Args: file_paths: The list of files to partition. @@ -40,7 +41,7 @@ def filter_by_size(file_paths: List[Path], size_threshold: int) -> Tuple[List[Pa A 2-tuple of lists of paths. The first list contains small images and the second list contains large images. """ - small_files, large_files = list(), list() + small_files, large_files = [], [] threshold: int = size_threshold * 1024 * 1024 for file_path in file_paths: @@ -63,8 +64,13 @@ def filter_by_size(file_paths: List[Path], size_threshold: int) -> Tuple[List[Pa return small_files, large_files -def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thresh: float): - """ Label the input image and writes labels back out. +def label_cython( + input_path: Path, + output_path: Path, + connectivity: int, + bin_thresh: float, +): + """Label the input image and writes labels back out. Args: input_path: Path to input image. @@ -77,7 +83,6 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thr input_path, max_workers=active_threads.count, ) as reader: - with BioWriter( output_path, max_workers=active_threads.count, @@ -93,15 +98,15 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thr if not numpy.any(image): writer.dtype = numpy.uint8 writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) - return + return None - image = (image > 0) + image = image > 0 if connectivity > image.ndim: ProcessManager.log( - f'{input_path.name}: Connectivity is not less than or equal to the number of image dimensions, ' - f'skipping this image. connectivity={connectivity}, ndim={image.ndim}' + f"{input_path.name}: Connectivity is not less than or equal to the number of image dimensions, " + f"skipping this image. connectivity={connectivity}, ndim={image.ndim}", ) - return + return None # Run the labeling algorithm labels = ftl.label_nd(image, connectivity) @@ -116,64 +121,78 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thr # Setup the argument parsing logger.info("Parsing arguments...") parser = argparse.ArgumentParser( - prog='main', - description='Label objects in a 2d or 3d binary image.', + prog="main", + description="Label objects in a 2d or 3d binary image.", ) parser.add_argument( - '--inpDir', dest='inpDir', type=str, required=True, - help='Input image collection to be processed by this plugin', + "--inpDir", + dest="inpDir", + type=str, + required=True, + help="Input image collection to be processed by this plugin", ) parser.add_argument( - '--connectivity', dest='connectivity', type=str, required=True, - help='City block connectivity, must be less than or equal to the number of dimensions', + "--connectivity", + dest="connectivity", + type=str, + required=True, + help="City block connectivity, must be less than or equal to the number of dimensions", ) parser.add_argument( - '--binarizationThreshold', dest='bin_thresh', type=str, required=True, - help='For images containing probability values. Must be between 0 and 1.0.', + "--binarizationThreshold", + dest="bin_thresh", + type=str, + required=True, + help="For images containing probability values. Must be between 0 and 1.0.", ) parser.add_argument( - '--outDir', dest='outDir', type=str, required=True, - help='Output collection', + "--outDir", + dest="outDir", + type=str, + required=True, + help="Output collection", ) # Parse the arguments args = parser.parse_args() _connectivity = int(args.connectivity) - logger.info(f'connectivity = {_connectivity}') + logger.info(f"connectivity = {_connectivity}") _bin_thresh = float(args.bin_thresh) - assert 0 <= _bin_thresh <= 1, 'bin_thresh must be between 0 and 1' - logger.info(f'bin_thresh = {_bin_thresh:.2f}') + assert 0 <= _bin_thresh <= 1, "bin_thresh must be between 0 and 1" + logger.info(f"bin_thresh = {_bin_thresh:.2f}") _input_dir = Path(args.inpDir).resolve() - assert _input_dir.exists(), f'{_input_dir } does not exist.' - if _input_dir.joinpath('images').is_dir(): - _input_dir = _input_dir.joinpath('images') - logger.info(f'inpDir = {_input_dir}') + assert _input_dir.exists(), f"{_input_dir } does not exist." + if _input_dir.joinpath("images").is_dir(): + _input_dir = _input_dir.joinpath("images") + logger.info(f"inpDir = {_input_dir}") _output_dir = Path(args.outDir).resolve() - assert _output_dir.exists(), f'{_output_dir } does not exist.' - logger.info(f'outDir = {_output_dir}') + assert _output_dir.exists(), f"{_output_dir } does not exist." + logger.info(f"outDir = {_output_dir}") # We only need a thread manager since labeling and image reading/writing # release the gil ProcessManager.init_threads() # Get all file names in inpDir image collection - _files = list(filter( - lambda _file: _file.is_file() and _file.name.endswith('.ome.tif'), - _input_dir.iterdir() - )) + _files = list( + filter( + lambda _file: _file.is_file() and _file.name.endswith(".ome.tif"), + _input_dir.iterdir(), + ), + ) _small_files, _large_files = filter_by_size(_files, 500) - logger.info(f'processing {len(_files)} images in total...') - logger.info(f'processing {len(_small_files)} small images with cython...') - logger.info(f'processing {len(_large_files)} large images with rust') + logger.info(f"processing {len(_files)} images in total...") + logger.info(f"processing {len(_small_files)} small images with cython...") + logger.info(f"processing {len(_large_files)} large images with rust") if _small_files: for _infile in _small_files: @@ -182,6 +201,7 @@ def label_cython(input_path: Path, output_path: Path, connectivity: int, bin_thr _infile, _output_dir.joinpath(get_output_name(_infile.name)), _connectivity, + _bin_thresh, ) ProcessManager.join_threads() From 88167c58b2e668c622a22cff35068e4486130a3a Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 10:07:14 -0500 Subject: [PATCH 05/16] build: added bump2version config --- .../polus-ftl-label-plugin/.bumpversion.cfg | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 transforms/images/polus-ftl-label-plugin/.bumpversion.cfg diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg new file mode 100644 index 000000000..227a46e8b --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -0,0 +1,23 @@ +[bumpversion] +current_version = 0.3.12-dev1 +commit = True +tag = False +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? +serialize = + {major}.{minor}.{patch}-{release}{dev} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = _ +first_value = dev +values = + dev + _ + +[bumpversion:part:dev] + +[bumpversion:file:plugin.json] + +[bumpversion:file:VERSION] + +[bumpversion:file:README.md] From 6c0b5d002cb29a341cb885c11c6ede1506d71caa Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 10:08:27 -0500 Subject: [PATCH 06/16] build: bumped version --- transforms/images/polus-ftl-label-plugin/.bumpversion.cfg | 2 +- transforms/images/polus-ftl-label-plugin/README.md | 4 ++-- transforms/images/polus-ftl-label-plugin/VERSION | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg index 227a46e8b..2c671c687 100644 --- a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.12-dev1 +current_version = 0.3.12-dev2 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index af83626f5..e909ca95d 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev1) +# FTL Label (v0.3.12-dev2) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. @@ -32,7 +32,7 @@ To see detailed documentation for the `Rust` implementation you need to: That last command will generate documentation and open a new tab in your default web browser. We determine whether to use the `Cython` or `Rust` implementation on a per-image basis depending on the size of that image. -If we expect the image to occupy less than `500MB` of memory, we use the `Cython` implementation otherwise we use the `Rust` implementation. +If we expect the image to occupy less than `500MB` of memory, we use the `Cython` implementation otherwise we use the `Rust` implementation. For more information on WIPP, visit the [official WIPP page](https://isg.nist.gov/deepzoomweb/software/wipp). diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index 08a5116f7..eb66aaa72 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.12-dev1 +0.3.12-dev2 diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 0db779b5e..f4b16c070 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.12-dev1", + "version": "0.3.12-dev2", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,7 +8,7 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev1", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev2", "baseCommand": [ "python3", "main.py" From 3eacc3db035b205bb7a1df800dafb832d9fa54b5 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 11:04:43 -0500 Subject: [PATCH 07/16] fix: using absolute path in dockerfile --- transforms/images/polus-ftl-label-plugin/Dockerfile | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/Dockerfile b/transforms/images/polus-ftl-label-plugin/Dockerfile index a0e9e6d7f..ea56265cf 100644 --- a/transforms/images/polus-ftl-label-plugin/Dockerfile +++ b/transforms/images/polus-ftl-label-plugin/Dockerfile @@ -45,5 +45,5 @@ ENV POLUS_LOG="INFO" RUN ls -la ${EXEC_DIR} -ENTRYPOINT ["python3", "main.py"] +ENTRYPOINT ["python3", "/ftl-rust/src/main.py"] CMD ["--help"] diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index f4b16c070..567315937 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -11,7 +11,7 @@ "containerId": "polusai/ftl-label-plugin:0.3.12-dev2", "baseCommand": [ "python3", - "main.py" + "/ftl-rust/src/main.py" ], "inputs": [ { From e117736d7b58c1eb31fbef0ac6b6cf2e2a9b2e2b Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 11:04:50 -0500 Subject: [PATCH 08/16] =?UTF-8?q?Bump=20version:=200.3.12-dev2=20=E2=86=92?= =?UTF-8?q?=200.3.12-dev3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- transforms/images/polus-ftl-label-plugin/.bumpversion.cfg | 2 +- transforms/images/polus-ftl-label-plugin/README.md | 2 +- transforms/images/polus-ftl-label-plugin/VERSION | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg index 2c671c687..6105944e8 100644 --- a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.12-dev2 +current_version = 0.3.12-dev3 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index e909ca95d..6bebfeff4 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev2) +# FTL Label (v0.3.12-dev3) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index eb66aaa72..ce1365b24 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.12-dev2 +0.3.12-dev3 diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 567315937..6e92a8ef2 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.12-dev2", + "version": "0.3.12-dev3", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,7 +8,7 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev2", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev3", "baseCommand": [ "python3", "/ftl-rust/src/main.py" From 9d39b42d0c29c6b5ab17ca3332bca7f4c40b7ce2 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 11:15:01 -0500 Subject: [PATCH 09/16] chore: updated types in manifest --- transforms/images/polus-ftl-label-plugin/plugin.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 6e92a8ef2..62979e008 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -22,13 +22,13 @@ }, { "name": "connectivity", - "type": "number", + "type": "integer", "description": "City block connectivity", "required": true }, { "name": "binarizationThreshold", - "type": "number", + "type": "float", "description": "For images containing probability values. Must be between 0 and 1.0.", "required": true } From 5f946faf415194f9c7a2adfe10e54b0924cc6c5a Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 11:16:17 -0500 Subject: [PATCH 10/16] chore: updated types in main.py --- transforms/images/polus-ftl-label-plugin/run-plugin.sh | 6 +++--- transforms/images/polus-ftl-label-plugin/src/main.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/run-plugin.sh b/transforms/images/polus-ftl-label-plugin/run-plugin.sh index 9daece870..9665a0dbf 100644 --- a/transforms/images/polus-ftl-label-plugin/run-plugin.sh +++ b/transforms/images/polus-ftl-label-plugin/run-plugin.sh @@ -11,8 +11,8 @@ POLUS_IMG_EXT=".ome.tif" # Inputs inpDir=/data/input-2d -connectivity="1" -binarizationThreshold="0.5" +connectivity=1 +binarizationThreshold=0.5 # Output paths outDir=/data/output @@ -25,4 +25,4 @@ docker run --mount type=bind,source="${data_path}",target=/data/ \ --inpDir ${inpDir} \ --connectivity ${connectivity} \ --binarizationThreshold ${binarizationThreshold} \ - --outDir ${outDir} \ No newline at end of file + --outDir ${outDir} diff --git a/transforms/images/polus-ftl-label-plugin/src/main.py b/transforms/images/polus-ftl-label-plugin/src/main.py index 9065c38bc..f072530ef 100644 --- a/transforms/images/polus-ftl-label-plugin/src/main.py +++ b/transforms/images/polus-ftl-label-plugin/src/main.py @@ -136,7 +136,7 @@ def label_cython( parser.add_argument( "--connectivity", dest="connectivity", - type=str, + type=int, required=True, help="City block connectivity, must be less than or equal to the number of dimensions", ) @@ -144,7 +144,7 @@ def label_cython( parser.add_argument( "--binarizationThreshold", dest="bin_thresh", - type=str, + type=float, required=True, help="For images containing probability values. Must be between 0 and 1.0.", ) From 2bb765ed9cc38278074e2393c206f07e3d80a374 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 11:16:26 -0500 Subject: [PATCH 11/16] =?UTF-8?q?Bump=20version:=200.3.12-dev3=20=E2=86=92?= =?UTF-8?q?=200.3.12-dev4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- transforms/images/polus-ftl-label-plugin/.bumpversion.cfg | 2 +- transforms/images/polus-ftl-label-plugin/README.md | 2 +- transforms/images/polus-ftl-label-plugin/VERSION | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg index 6105944e8..2a3e89d3b 100644 --- a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.12-dev3 +current_version = 0.3.12-dev4 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index 6bebfeff4..35f96ba55 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev3) +# FTL Label (v0.3.12-dev4) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index ce1365b24..725491d5c 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.12-dev3 +0.3.12-dev4 diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 62979e008..194c8ee0b 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.12-dev3", + "version": "0.3.12-dev4", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,7 +8,7 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev3", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev4", "baseCommand": [ "python3", "/ftl-rust/src/main.py" From 2a4298d4df389a04dd829ed27c325b8081732779 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 15:14:23 -0500 Subject: [PATCH 12/16] fix: number type in manifest --- transforms/images/polus-ftl-label-plugin/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 194c8ee0b..575951e43 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -28,7 +28,7 @@ }, { "name": "binarizationThreshold", - "type": "float", + "type": "number", "description": "For images containing probability values. Must be between 0 and 1.0.", "required": true } From 47951d77c0e22fc39d6396154dbe467fea3314ad Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 14 Feb 2024 15:14:29 -0500 Subject: [PATCH 13/16] =?UTF-8?q?Bump=20version:=200.3.12-dev4=20=E2=86=92?= =?UTF-8?q?=200.3.12-dev5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- transforms/images/polus-ftl-label-plugin/.bumpversion.cfg | 2 +- transforms/images/polus-ftl-label-plugin/README.md | 2 +- transforms/images/polus-ftl-label-plugin/VERSION | 2 +- transforms/images/polus-ftl-label-plugin/plugin.json | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg index 2a3e89d3b..2d2d187bd 100644 --- a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.12-dev4 +current_version = 0.3.12-dev5 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/polus-ftl-label-plugin/README.md index 35f96ba55..51e7a4448 100644 --- a/transforms/images/polus-ftl-label-plugin/README.md +++ b/transforms/images/polus-ftl-label-plugin/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev4) +# FTL Label (v0.3.12-dev5) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION index 725491d5c..d37752338 100644 --- a/transforms/images/polus-ftl-label-plugin/VERSION +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -1 +1 @@ -0.3.12-dev4 +0.3.12-dev5 diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json index 575951e43..639ba65c4 100644 --- a/transforms/images/polus-ftl-label-plugin/plugin.json +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "FTL Label", - "version": "0.3.12-dev4", + "version": "0.3.12-dev5", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", @@ -8,7 +8,7 @@ "repository": "https://github.com/labshare/polus-plugins", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev4", + "containerId": "polusai/ftl-label-plugin:0.3.12-dev5", "baseCommand": [ "python3", "/ftl-rust/src/main.py" From 5b61a93a7b6af512f38cbf12e53edc496679ccfb Mon Sep 17 00:00:00 2001 From: hamshkhawar Date: Wed, 11 Mar 2026 16:51:12 -0400 Subject: [PATCH 14/16] updating ftl-label-plugin --- .../.bumpversion.cfg | 0 .../.gitignore | 0 transforms/images/ftl-label-tool/Cargo.toml | 28 ++ .../Dockerfile | 8 +- .../MANIFEST.in | 0 .../README.md | 0 .../SimpleTiledTiffViewer.py | 0 .../VERSION | 0 .../benches/ftl_rust.rs | 0 .../build-docker.sh | 0 .../ftl_rust/__init__.py | 0 .../ftllabel.cwl | 0 .../ict.yaml | 0 .../plugin.json | 0 .../images/ftl-label-tool/pyproject.toml | 106 +++++ .../run-plugin.sh | 2 +- .../rust_requirements.txt | 0 .../images/ftl-label-tool/rust_setup.py | 3 + transforms/images/ftl-label-tool/setup.py | 74 +++ .../transforms/images/ftl_label}/__init__.py | 0 .../transforms/images/ftl_label/__main__.py | 224 +++++++++ .../images/ftl_label}/bench_rust.py | 0 .../transforms/images/ftl_label}/ftl.pyx | 0 .../transforms/images/ftl_label}/lib.rs | 15 +- .../transforms/images/ftl_label/polygons.rs | 283 +++++++++++ .../images/ftl_label}/requirements.txt | 0 .../images/polus-ftl-label-plugin/Cargo.toml | 24 - .../polus-ftl-label-plugin/pyproject.toml | 2 - .../polus-ftl-label-plugin/rust_setup.py | 12 - .../images/polus-ftl-label-plugin/src/main.py | 211 --------- .../polus-ftl-label-plugin/src/polygons.rs | 441 ------------------ .../polus-ftl-label-plugin/src/setup.py | 11 - 32 files changed, 729 insertions(+), 715 deletions(-) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/.bumpversion.cfg (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/.gitignore (100%) create mode 100644 transforms/images/ftl-label-tool/Cargo.toml rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/Dockerfile (87%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/MANIFEST.in (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/README.md (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/SimpleTiledTiffViewer.py (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/VERSION (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/benches/ftl_rust.rs (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/build-docker.sh (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/ftl_rust/__init__.py (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/ftllabel.cwl (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/ict.yaml (100%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/plugin.json (100%) create mode 100644 transforms/images/ftl-label-tool/pyproject.toml rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/run-plugin.sh (93%) rename transforms/images/{polus-ftl-label-plugin => ftl-label-tool}/rust_requirements.txt (100%) create mode 100644 transforms/images/ftl-label-tool/rust_setup.py create mode 100644 transforms/images/ftl-label-tool/setup.py rename transforms/images/{polus-ftl-label-plugin/src => ftl-label-tool/src/polus/images/transforms/images/ftl_label}/__init__.py (100%) create mode 100644 transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py rename transforms/images/{polus-ftl-label-plugin/src => ftl-label-tool/src/polus/images/transforms/images/ftl_label}/bench_rust.py (100%) rename transforms/images/{polus-ftl-label-plugin/src => ftl-label-tool/src/polus/images/transforms/images/ftl_label}/ftl.pyx (100%) rename transforms/images/{polus-ftl-label-plugin/src => ftl-label-tool/src/polus/images/transforms/images/ftl_label}/lib.rs (88%) create mode 100644 transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/polygons.rs rename transforms/images/{polus-ftl-label-plugin/src => ftl-label-tool/src/polus/images/transforms/images/ftl_label}/requirements.txt (100%) delete mode 100644 transforms/images/polus-ftl-label-plugin/Cargo.toml delete mode 100644 transforms/images/polus-ftl-label-plugin/pyproject.toml delete mode 100644 transforms/images/polus-ftl-label-plugin/rust_setup.py delete mode 100644 transforms/images/polus-ftl-label-plugin/src/main.py delete mode 100644 transforms/images/polus-ftl-label-plugin/src/polygons.rs delete mode 100644 transforms/images/polus-ftl-label-plugin/src/setup.py diff --git a/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg b/transforms/images/ftl-label-tool/.bumpversion.cfg similarity index 100% rename from transforms/images/polus-ftl-label-plugin/.bumpversion.cfg rename to transforms/images/ftl-label-tool/.bumpversion.cfg diff --git a/transforms/images/polus-ftl-label-plugin/.gitignore b/transforms/images/ftl-label-tool/.gitignore similarity index 100% rename from transforms/images/polus-ftl-label-plugin/.gitignore rename to transforms/images/ftl-label-tool/.gitignore diff --git a/transforms/images/ftl-label-tool/Cargo.toml b/transforms/images/ftl-label-tool/Cargo.toml new file mode 100644 index 000000000..7c544ba4d --- /dev/null +++ b/transforms/images/ftl-label-tool/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = ["Najib Ishaq "] +name = "ftl-rust" +version = "0.1.0" +edition = "2021" + +[lib] +name = "ftl_rust" +path = "src/polus/images/transforms/images/ftl_label/lib.rs" +crate-type = ["cdylib", "rlib"] + +[dependencies] +# numpy 0.22 is the first release supporting Python numpy >=2.0 and ndarray 0.16. +# pyo3, numpy, and ndarray must be kept in lockstep: +# numpy 0.22 → pyo3 0.22 + ndarray 0.16 +pyo3 = { version = "0.22", features = ["extension-module"] } +numpy = "0.22" +ndarray = { version = "0.16", features = ["rayon"] } +rayon = "1.10" + +[dev-dependencies] +memmap2 = "0.9" +ndarray-npy = "0.9" +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "ftl_rust" +harness = false \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/Dockerfile b/transforms/images/ftl-label-tool/Dockerfile similarity index 87% rename from transforms/images/polus-ftl-label-plugin/Dockerfile rename to transforms/images/ftl-label-tool/Dockerfile index ea56265cf..2a7266fc2 100644 --- a/transforms/images/polus-ftl-label-plugin/Dockerfile +++ b/transforms/images/ftl-label-tool/Dockerfile @@ -1,16 +1,16 @@ # Compile ftl.pyx -FROM python:3.9 +FROM python:3.13 ARG EXEC_DIR="/opt/executables" RUN mkdir -p ${EXEC_DIR}/executables COPY src ${EXEC_DIR}/. WORKDIR ${EXEC_DIR} -RUN pip3 install --upgrade cython==3.0.0a9 \ - && pip3 install numpy==1.21.4 --no-cache-dir \ +RUN pip3 install --upgrade cython==3.0.11 setuptools \ + && pip3 install numpy==2.1.0 --no-cache-dir \ && python3 setup.py build_ext --inplace RUN cp executables/* . && rm -rf executables # Build the plugin container -FROM python:3.9-slim +FROM python:3.13-slim # Get essential packages RUN apt-get update && \ diff --git a/transforms/images/polus-ftl-label-plugin/MANIFEST.in b/transforms/images/ftl-label-tool/MANIFEST.in similarity index 100% rename from transforms/images/polus-ftl-label-plugin/MANIFEST.in rename to transforms/images/ftl-label-tool/MANIFEST.in diff --git a/transforms/images/polus-ftl-label-plugin/README.md b/transforms/images/ftl-label-tool/README.md similarity index 100% rename from transforms/images/polus-ftl-label-plugin/README.md rename to transforms/images/ftl-label-tool/README.md diff --git a/transforms/images/polus-ftl-label-plugin/SimpleTiledTiffViewer.py b/transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py similarity index 100% rename from transforms/images/polus-ftl-label-plugin/SimpleTiledTiffViewer.py rename to transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/ftl-label-tool/VERSION similarity index 100% rename from transforms/images/polus-ftl-label-plugin/VERSION rename to transforms/images/ftl-label-tool/VERSION diff --git a/transforms/images/polus-ftl-label-plugin/benches/ftl_rust.rs b/transforms/images/ftl-label-tool/benches/ftl_rust.rs similarity index 100% rename from transforms/images/polus-ftl-label-plugin/benches/ftl_rust.rs rename to transforms/images/ftl-label-tool/benches/ftl_rust.rs diff --git a/transforms/images/polus-ftl-label-plugin/build-docker.sh b/transforms/images/ftl-label-tool/build-docker.sh similarity index 100% rename from transforms/images/polus-ftl-label-plugin/build-docker.sh rename to transforms/images/ftl-label-tool/build-docker.sh diff --git a/transforms/images/polus-ftl-label-plugin/ftl_rust/__init__.py b/transforms/images/ftl-label-tool/ftl_rust/__init__.py similarity index 100% rename from transforms/images/polus-ftl-label-plugin/ftl_rust/__init__.py rename to transforms/images/ftl-label-tool/ftl_rust/__init__.py diff --git a/transforms/images/polus-ftl-label-plugin/ftllabel.cwl b/transforms/images/ftl-label-tool/ftllabel.cwl similarity index 100% rename from transforms/images/polus-ftl-label-plugin/ftllabel.cwl rename to transforms/images/ftl-label-tool/ftllabel.cwl diff --git a/transforms/images/polus-ftl-label-plugin/ict.yaml b/transforms/images/ftl-label-tool/ict.yaml similarity index 100% rename from transforms/images/polus-ftl-label-plugin/ict.yaml rename to transforms/images/ftl-label-tool/ict.yaml diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/ftl-label-tool/plugin.json similarity index 100% rename from transforms/images/polus-ftl-label-plugin/plugin.json rename to transforms/images/ftl-label-tool/plugin.json diff --git a/transforms/images/ftl-label-tool/pyproject.toml b/transforms/images/ftl-label-tool/pyproject.toml new file mode 100644 index 000000000..0f67bd654 --- /dev/null +++ b/transforms/images/ftl-label-tool/pyproject.toml @@ -0,0 +1,106 @@ +# pyproject.toml for ftl-label +# +# Two metadata tables coexist here: +# [project] – read by setuptools (PEP 621, required when +# build-backend = "setuptools.build_meta") +# [tool.poetry] – read by Poetry for virtualenv / lock-file management +# +# They must agree on name and version. + +# ── PEP 621 metadata (setuptools reads this) ────────────────────────────────── +[project] +name = "polus-images-segmentation-ftl-label" +version = "0.3.12.dev5" +description = "Label objects in a 2D or 3D binary image using the FTL connected component algorithm." +readme = "README.md" +requires-python = ">=3.11,<3.13" +authors = [ + {name = "Nick Schaub", email = "nick.schaub@nih.gov"}, + {name = "Najib Ishaq", email = "najib.ishaq@axleinfo.com"}, +] +# Runtime dependencies (mirrors [tool.poetry.dependencies]) +dependencies = [ + "typer>=0.9.0", + "numpy>2.0.0", + "bfio[all]==2.5.0", + "filepattern==2.1.4", +] + +# ── Poetry metadata (Poetry reads this for venv / lock-file) ────────────────── +[tool.poetry] +name = "polus-images-segmentation-ftl-label" +version = "0.3.12-dev5" +description = "Label objects in a 2D or 3D binary image using the FTL connected component algorithm." +authors = [ + "Nick Schaub ", + "Najib Ishaq ", +] +readme = "README.md" +packages = [{include = "polus", from = "src"}] + +[tool.poetry.dependencies] +python = ">=3.11,<3.13" +typer = ">=0.9.0" +numpy = ">2.0.0" +bfio = "2.5.0" +filepattern = "2.1.4" + +[tool.poetry.group.dev.dependencies] +bump2version = "^1.0.1" +pre-commit = "^3.1.0" +black = "^23.1.0" +flake8 = "^6.0.0" +mypy = "^1.0.1" +pytest = "^7.2.1" + +[tool.poetry.group.build.dependencies] +setuptools = ">=68.0" +setuptools-rust = ">=1.9.0" +cython = ">=3.0.11" + +# ── Build system ─────────────────────────────────────────────────────────────── +# setuptools.build_meta is required (not poetry-core) so that setuptools-rust +# can hook into the build and compile the Rust/Cython extensions via rust_setup.py. +[build-system] +requires = [ + "setuptools>=68.0", + "setuptools-rust>=1.9.0", + "wheel", + "cython>=3.0.11", + "numpy>2.0.0", +] +build-backend = "setuptools.build_meta" + +# ── setuptools package discovery ────────────────────────────────────────────── +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +# ── mypy ────────────────────────────────────────────────────────────────────── +[tool.mypy] +mypy_path = "src" +strict = true +warn_unreachable = true +warn_no_return = true + +[[tool.mypy.overrides]] +module = "ftl" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "ftl_rust" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "bfio" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "bfio.*" +ignore_missing_imports = true + +# ── pytest ──────────────────────────────────────────────────────────────────── +[tool.pytest.ini_options] +pythonpath = ["."] \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/run-plugin.sh b/transforms/images/ftl-label-tool/run-plugin.sh similarity index 93% rename from transforms/images/polus-ftl-label-plugin/run-plugin.sh rename to transforms/images/ftl-label-tool/run-plugin.sh index 9665a0dbf..ccdd34e4f 100644 --- a/transforms/images/polus-ftl-label-plugin/run-plugin.sh +++ b/transforms/images/ftl-label-tool/run-plugin.sh @@ -21,7 +21,7 @@ docker run --mount type=bind,source="${data_path}",target=/data/ \ --user "$(id -u)":"$(id -g)" \ --env POLUS_LOG="${POLUS_LOG}" \ --env POLUS_IMG_EXT="${POLUS_IMG_EXT}" \ - polusaiftl-label-plugin:"${version}" \ + polusai/ftl-label-tool:"${version}" \ --inpDir ${inpDir} \ --connectivity ${connectivity} \ --binarizationThreshold ${binarizationThreshold} \ diff --git a/transforms/images/polus-ftl-label-plugin/rust_requirements.txt b/transforms/images/ftl-label-tool/rust_requirements.txt similarity index 100% rename from transforms/images/polus-ftl-label-plugin/rust_requirements.txt rename to transforms/images/ftl-label-tool/rust_requirements.txt diff --git a/transforms/images/ftl-label-tool/rust_setup.py b/transforms/images/ftl-label-tool/rust_setup.py new file mode 100644 index 000000000..4b0b1b890 --- /dev/null +++ b/transforms/images/ftl-label-tool/rust_setup.py @@ -0,0 +1,3 @@ +"""Backward-compatibility shim – delegates to setup.py.""" +with open("setup.py") as _f: + exec(_f.read()) # noqa: S102 \ No newline at end of file diff --git a/transforms/images/ftl-label-tool/setup.py b/transforms/images/ftl-label-tool/setup.py new file mode 100644 index 000000000..4a48f52ed --- /dev/null +++ b/transforms/images/ftl-label-tool/setup.py @@ -0,0 +1,74 @@ +"""Build script for ftl-label native extensions. + +Architecture handling +--------------------- +ftl (Cython) – uses x86/x64 AVX2+BMI2 intrinsics (x86intrin.h). + Compiled ONLY on x86_64. Skipped silently on ARM64 / Apple Silicon. + +ftl_rust (Rust) – pure Rust with Rayon; compiles on all platforms including ARM64. + +When ftl is unavailable (ARM64), main.py routes ALL images through the Rust path. +""" + +import os +import platform +import sys +from pathlib import Path + +from setuptools import setup +from setuptools_rust import Binding, RustExtension + +# ── Detect architecture ──────────────────────────────────────────────────────── +machine = platform.machine().lower() +IS_X86 = machine in ("x86_64", "amd64", "i686", "i386") + +SRC = Path("src/polus/images/transforms/images/ftl_label") + +ext_modules = [] + +# ── Cython extension (x86/x64 only) ─────────────────────────────────────────── +if IS_X86: + try: + import numpy + from Cython.Build import cythonize + from Cython.Compiler import Options + + Options.annotate = True + os.environ["CFLAGS"] = "-march=native -O3" + os.environ["CXXFLAGS"] = "-march=native -O3" + + cython_exts = cythonize( + str(SRC / "ftl.pyx"), + compiler_directives={"language_level": 3}, + ) + # Override the deep dotted path Cython infers → plain "ftl" + for ext in cython_exts: + ext.name = "ftl" + ext.include_dirs = [numpy.get_include()] + + ext_modules.extend(cython_exts) + print(f"[setup.py] x86_64 detected – Cython extension will be compiled.") + + except Exception as exc: # noqa: BLE001 + print(f"[setup.py] WARNING: Cython build skipped ({exc}).") +else: + print( + f"[setup.py] Non-x86 architecture detected ({machine}) – " + "Cython AVX extension is not supported here.\n" + " All images will be processed via the Rust backend." + ) + +# ── Rust/PyO3 extension (all platforms) ─────────────────────────────────────── +rust_ext = RustExtension( + target="ftl_rust.ftl_rust", + path="Cargo.toml", + binding=Binding.PyO3, + debug=False, +) + +# ── Setup ────────────────────────────────────────────────────────────────────── +setup( + rust_extensions=[rust_ext], + ext_modules=ext_modules, + zip_safe=False, +) \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/src/__init__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py similarity index 100% rename from transforms/images/polus-ftl-label-plugin/src/__init__.py rename to transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py new file mode 100644 index 000000000..78ab2676b --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py @@ -0,0 +1,224 @@ +"""FTL Label Tool.""" +import logging +import os +from pathlib import Path + +import typer +import ftl +import numpy +from bfio import BioReader +from bfio import BioWriter +from ftl_rust import PolygonSet +from multiprocessing.pool import ThreadPool + +try: + import ftl + FTL_CYTHON_AVAILABLE = True +except ImportError: + ftl = None # type: ignore[assignment] + FTL_CYTHON_AVAILABLE = False + +app = typer.Typer() + +POLUS_LOG = getattr(logging, os.environ.get("POLUS_LOG", "INFO")) +POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") +_NUM_THREADS: int = int(os.environ.get("NUM_THREADS", os.cpu_count() or 1)) + +# Initialize the logger +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger("main") +logger.setLevel(POLUS_LOG) + + +def get_output_name(filename: str) -> str: + name = filename.split(".ome")[0] + return f"{name}{POLUS_EXT}" + + +def filter_by_size( + file_paths: list[Path], + size_threshold: int, +) -> tuple[list[Path], list[Path]]: + """Partitions the input files by the memory-footprint for the images. + + Args: + file_paths: The list of files to partition. + size_threshold: The memory-size (in MB) to use as a threshold. + + Returns: + A 2-tuple of lists of paths. + The first list contains small images and the second list contains large images. + """ + small_files: list[Path] = [] + large_files: list[Path] = [] + + threshold: int = size_threshold * 1024 * 1024 + + for file_path in file_paths: + with BioReader(file_path) as reader: + num_pixels = numpy.prod(reader.shape) + dtype = reader.dtype + + if dtype in (numpy.uint8, bool): + pixel_bytes = 8 + elif dtype == numpy.uint16: + pixel_bytes = 16 + elif dtype == numpy.uint32 or dtype == numpy.float32: + pixel_bytes = 32 + else: + pixel_bytes = 64 + + image_size = num_pixels * (pixel_bytes / 8) # Convert bits to bytes + (small_files if image_size <= threshold else large_files).append(file_path) + + return small_files, large_files + + +def label_cython(args: tuple[Path, Path, int, float]) -> bool | None: + """Label a small image using the Cython implementation. + + Accepts a single tuple so it can be used directly with ThreadPool.map. + + Args: + args: (input_path, output_path, connectivity, bin_thresh) + + Returns: + True on success, None if the image was skipped. + """ + input_path, output_path, connectivity, bin_thresh = args + with BioReader(input_path, max_workers=_NUM_THREADS) as reader: + with BioWriter( + output_path, + max_workers=_NUM_THREADS, + metadata=reader.metadata, + ) as writer: + # Load an image and convert to binary + image = numpy.squeeze(reader[..., 0, 0]) + + # If the image has float values, binarize it using the threshold + if image.dtype == numpy.float32 or image.dtype == numpy.float64: + image = (image > bin_thresh).astype(numpy.uint8) + + if not numpy.any(image): + writer.dtype = numpy.uint8 + writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) + return None + + image = image > 0 + if connectivity > image.ndim: + logger.warning( + f"{input_path.name}: Connectivity is not less than or equal to the number of image dimensions, " + f"skipping this image. connectivity={connectivity}, ndim={image.ndim}", + ) + return None + + # Run the labeling algorithm + labels = ftl.label_nd(image, connectivity) + + # Save the image + writer.dtype = labels.dtype + writer[:] = labels + return True + + +@app.command() +def main( + inp_dir: Path = typer.Option( + ..., + "--inpDir", + help="Input image collection to be processed by this plugin.", + exists=True, + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + ), + connectivity: int = typer.Option( + ..., + "--connectivity", + help="City block connectivity. Must be <= number of image dimensions.", + min=1, + max=3, + ), + binarization_threshold: float = typer.Option( + 0.5, + "--binarizationThreshold", + help="Binarization threshold for probability images. Must be between 0 and 1.", + min=0.0, + max=1.0, + ), + out_dir: Path = typer.Option( + ..., + "--outDir", + help="Output collection for labelled images.", + exists=True, + file_okay=False, + dir_okay=True, + writable=True, + resolve_path=True, + ), +) -> None: + """Label objects in a 2D or 3D binary image using the FTL algorithm. + + Small images (< 500 MB) are processed with the Cython implementation via a + ThreadPool. Large images are processed with the Rust tiled implementation + sequentially (the Rust layer manages its own parallelism internally). + + Args: + inp_dir: Path to the input image collection. + connectivity: City block connectivity (1 = face, 2 = edge, 3 = corner). + binarization_threshold: Threshold for binarizing float probability images. + out_dir: Path to the output image collection. + """ + + logger.info(f"inpDir = {inp_dir}") + logger.info(f"connectivity = {connectivity}") + logger.info(f"binarizationThreshold = {binarization_threshold:.2f}") + logger.info(f"outDir = {out_dir}") + logger.info(f"threads = {_NUM_THREADS}") + + + # Get all file names in inpDir image collection + if inp_dir.joinpath("images").is_dir(): + inp_dir = inp_dir / "images" + + files = [f for f in inp_dir.iterdir() if f.is_file() and f.name.endswith(POLUS_EXT)] + if not files: + logger.warning(f"No {POLUS_EXT} files found in {inp_dir}") + raise typer.Exit(0) + + small_files, large_files = filter_by_size(files, 500) + + logger.info(f"Processing {len(files)} image(s) total...") + logger.info(f" {len(small_files)} small -> Cython / ThreadPool path") + logger.info(f" {len(large_files)} large -> Rust path") + + # Small files: run label_cython in a thread pool + if small_files: + task_args = [ + ( + infile, + out_dir / get_output_name(infile.name), + connectivity, + binarization_threshold, + ) + for infile in small_files + ] + with ThreadPool(processes=_NUM_THREADS) as pool: + results = pool.map(label_cython, task_args) + + skipped = results.count(None) + if skipped: + logger.warning(f"{skipped} small image(s) were skipped (empty or mismatched connectivity).") + + # Large files: Rust handles tiling and internal parallelism + if large_files: + for infile in large_files: + outfile = out_dir / get_output_name(infile.name) + PolygonSet(connectivity, binarization_threshold).read_from(infile).write_to(outfile) + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/src/bench_rust.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py similarity index 100% rename from transforms/images/polus-ftl-label-plugin/src/bench_rust.py rename to transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py diff --git a/transforms/images/polus-ftl-label-plugin/src/ftl.pyx b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx similarity index 100% rename from transforms/images/polus-ftl-label-plugin/src/ftl.pyx rename to transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx diff --git a/transforms/images/polus-ftl-label-plugin/src/lib.rs b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs similarity index 88% rename from transforms/images/polus-ftl-label-plugin/src/lib.rs rename to transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs index 536d31a2b..6f9409782 100644 --- a/transforms/images/polus-ftl-label-plugin/src/lib.rs +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs @@ -11,17 +11,17 @@ pub fn extract_tile<'py>( py: Python<'py>, polygon_set: &PolygonSet, coordinates: (usize, usize, usize, usize, usize, usize), -) -> &'py PyArrayDyn { +) -> Bound<'py, PyArrayDyn> { let tile = polygon_set._extract_tile(coordinates); - tile.into_pyarray(py) + // numpy 0.22: into_pyarray_bound returns Bound<'py, PyArrayDyn> + tile.into_pyarray_bound(py) } -/// Generates a Python-class for interfacing with Python. +/// Python module — pyo3 0.22 signature. #[pymodule] -fn ftl_rust(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn ftl_rust(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(extract_tile, m)?)?; - Ok(()) } @@ -42,7 +42,6 @@ mod tests { path.push("input_array"); path.push(format!("test_infile_{}.npy", count)); println!("reading path {:?}", path); - read_npy(path).unwrap() } @@ -61,7 +60,6 @@ mod tests { ys.par_iter().for_each(|&y| { let y_max = std::cmp::min(n_rows, y + tile_size); - xs.par_iter().for_each(|&x| { let x_max = std::cmp::min(n_cols, x + tile_size); let tile = data.slice(s![.., y..y_max, x..x_max]).into_dyn(); @@ -70,7 +68,6 @@ mod tests { }); polygon_set.digest(); - assert_eq!(polygon_set.len(), count, "wrong number of polygons"); } -} +} \ No newline at end of file diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/polygons.rs b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/polygons.rs new file mode 100644 index 000000000..bf0aeba35 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/polygons.rs @@ -0,0 +1,283 @@ +use std::cmp::max; +use std::cmp::min; +use std::cmp::Ordering; +use std::sync::Arc; +use std::sync::RwLock; + +// In numpy 0.22, ndarray types are re-exported via numpy::ndarray +use numpy::ndarray::{ArrayD, ArrayViewD, Array3}; +use numpy::ndarray::prelude::*; +use numpy::PyReadonlyArrayDyn; +use pyo3::prelude::*; +use rayon::prelude::*; + +type Slice = (usize, (usize, (usize, usize))); // (z, (y, (x_min, x_max))) +type PolyVec = Vec>; + +fn do_slices_overlap(left: Slice, right: Slice, connectivity: u8) -> bool { + let (left_z, (left_y, (left_start, left_stop))) = left; + let (right_z, (right_y, (right_start, right_stop))) = right; + + if (left_start > right_stop) || (right_start > left_stop) { + return false; + } + + let z_diff = left_z.abs_diff(right_z); + if z_diff > 1 { + return false; + } + + let y_diff = left_y.abs_diff(right_y); + if y_diff > 1 { + return false; + } + + if z_diff == 0 && y_diff == 0 { + (left_start == right_stop) || (right_start == left_stop) + } else if y_diff == 1 && z_diff == 1 { + if connectivity == 3 { + (left_start <= right_stop) || (right_start <= left_stop) + } else { + (left_start < right_stop) || (right_start < left_stop) + } + } else if connectivity == 1 { + (left_start < right_stop) || (right_start < left_stop) + } else { + (left_start <= right_stop) || (right_start <= left_stop) + } +} + +/// A `Polygon` represents a single connected object to be labelled. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Polygon { + connectivity: u8, + slices: Vec, + x_min: usize, + x_max: usize, + y_min: usize, + y_max: usize, + z_min: usize, + z_max: usize, +} + +impl Polygon { + pub fn new(connectivity: u8, slices: Vec) -> Self { + assert!(!slices.is_empty(), "Cannot create Polygon without any slices."); + + let (zs, rest): (Vec<_>, Vec<_>) = slices.iter().copied().unzip(); + let z_min = *zs.iter().min().unwrap(); + let z_max = zs.into_iter().max().unwrap() + 1; + + let (ys, rest): (Vec<_>, Vec<_>) = rest.into_iter().unzip(); + let y_min = *ys.iter().min().unwrap(); + let y_max = ys.into_iter().max().unwrap() + 1; + + let (starts, stops): (Vec<_>, Vec<_>) = rest.into_iter().unzip(); + let x_min = starts.into_iter().min().unwrap(); + let x_max = stops.into_iter().max().unwrap(); + + Polygon { connectivity, slices, x_min, x_max, y_min, y_max, z_min, z_max } + } + + pub fn is_empty(&self) -> bool { self.slices.is_empty() } + pub fn len(&self) -> usize { self.slices.len() } + + pub fn bbox_overlap(&self, other: &Self) -> bool { + self.x_min <= other.x_max + && self.y_min <= other.y_max + && self.z_min <= other.z_max + && self.x_max >= other.x_min + && self.y_max >= other.y_min + && self.z_max >= other.z_min + } + + pub fn boundary_connects(&self, other: &Self) -> bool { + if self.bbox_overlap(other) { + self.slices.par_iter().any(|&left| { + other.slices.par_iter() + .any(|&right| do_slices_overlap(left, right, self.connectivity)) + }) + } else { + false + } + } + + pub fn absorb(&mut self, other: &mut Self) { + self.x_min = min(self.x_min, other.x_min); + self.y_min = min(self.y_min, other.y_min); + self.z_min = min(self.z_min, other.z_min); + self.x_max = max(self.x_max, other.x_max); + self.y_max = max(self.y_max, other.y_max); + self.z_max = max(self.z_max, other.z_max); + self.slices.extend(other.slices.drain(..)); + } +} + +fn bft_partition(polygons: &mut Vec, connectivity: u8) -> Vec { + let mut merged: Vec = Vec::new(); + + polygons.iter_mut().for_each(|target| { + merged.iter_mut() + .filter(|c| !c.is_empty()) + .for_each(|candidate| { + if target.boundary_connects(candidate) { + target.absorb(candidate); + } + }); + merged.push(Polygon { + connectivity, + slices: target.slices.drain(..).collect(), + x_min: target.x_min, y_min: target.y_min, z_min: target.z_min, + x_max: target.x_max, y_max: target.y_max, z_max: target.z_max, + }); + }); + + merged.iter_mut() + .filter(|p| !p.is_empty()) + .map(|p| Polygon { + connectivity, + slices: p.slices.drain(..).collect(), + x_min: p.x_min, y_min: p.y_min, z_min: p.z_min, + x_max: p.x_max, y_max: p.y_max, z_max: p.z_max, + }) + .collect() +} + +impl Ord for Polygon { + fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap() } +} + +impl PartialOrd for Polygon { + fn partial_cmp(&self, other: &Self) -> Option { + match self.z_min.cmp(&other.z_min) { + Ordering::Equal => match self.y_min.cmp(&other.y_min) { + Ordering::Equal => match self.x_min.cmp(&other.x_min) { + Ordering::Equal => match self.z_max.cmp(&other.z_max) { + Ordering::Equal => match self.y_max.cmp(&other.y_max) { + Ordering::Equal => Some(self.x_max.cmp(&other.x_max)), + other => Some(other), + }, + other => Some(other), + }, + other => Some(other), + }, + other => Some(other), + }, + other => Some(other), + } + } +} + +/// A `PolygonSet` handles and maintains `Polygons` in an image. +#[pyclass] +#[derive(Clone, Debug)] +pub struct PolygonSet { + connectivity: u8, + polygons: Arc>, +} + +#[pymethods] +impl PolygonSet { + #[new] + pub fn new(connectivity: u8) -> Self { + PolygonSet { connectivity, polygons: Arc::new(RwLock::new(Vec::new())) } + } + + pub fn is_empty(&self) -> bool { self.polygons.read().unwrap().is_empty() } + pub fn len(&self) -> usize { self.polygons.read().unwrap().len() } + + pub fn add_tile(&self, tile: PyReadonlyArrayDyn<'_, u8>, top_left_point: (usize, usize, usize)) { + self._add_tile(tile.as_array(), top_left_point) + } + + pub fn digest(&self) { + let mut polygons = self.polygons.write().unwrap() + .drain(..) + .map(|p| Polygon { + connectivity: p.connectivity, + slices: p.slices.iter().copied().collect(), + x_min: p.x_min, y_min: p.y_min, z_min: p.z_min, + x_max: p.x_max, y_max: p.y_max, z_max: p.z_max, + }) + .collect(); + let mut polygons = bft_partition(&mut polygons, self.connectivity); + polygons.sort(); + self.polygons.write().unwrap() + .extend(polygons.drain(..).map(Arc::new)); + } +} + +impl PolygonSet { + pub fn _add_tile(&self, tile: ArrayViewD<'_, u8>, top_left_point: (usize, usize, usize)) { + let (z_min, y_min, x_min) = top_left_point; + + let mut slices = tile + .outer_iter().into_par_iter().enumerate() + .flat_map(|(z, plane)| { + let mut slices = plane + .outer_iter().into_par_iter().enumerate() + .flat_map(|(y, row)| { + let runs: Vec<_> = row.iter() + .chain([0].iter()) + .zip([0].iter().chain(row.iter())) + .enumerate() + .filter(|(_, (&a, &b))| a != b) + .map(|(i, _)| i) + .collect(); + + if runs.is_empty() { + Vec::new() + } else { + let starts: Vec<_> = runs.iter().step_by(2).cloned().collect(); + let ends: Vec<_> = runs.into_iter().skip(1).step_by(2).collect(); + starts.into_par_iter().zip(ends.into_par_iter()) + .map(|(start, stop)| Polygon::new( + self.connectivity, + vec![(z + z_min, (y + y_min, (start + x_min, stop + x_min)))], + )) + .collect::>() + } + }) + .collect::>(); + bft_partition(&mut slices, self.connectivity) + }) + .collect::>(); + + let mut polygons = bft_partition(&mut slices, self.connectivity); + self.polygons.write().unwrap() + .extend(polygons.drain(..).map(Arc::new)); + } + + pub fn _extract_tile(&self, coordinates: (usize, usize, usize, usize, usize, usize)) -> ArrayD { + let (z_min, z_max, y_min, y_max, x_min, x_max) = coordinates; + + let tile_polygon = Polygon::new( + self.connectivity, + vec![ + (z_min, (y_min, (x_min, x_max))), + (z_max, (y_max, (x_min, x_max))), + ], + ); + + let mut tile: Array3 = Array3::zeros((z_max - z_min, y_max - y_min, x_max - x_min)); + + self.polygons.read().unwrap().iter().enumerate() + .for_each(|(i, polygon)| { + if tile_polygon.bbox_overlap(polygon) { + polygon.slices.iter().copied() + .for_each(|(z, (y, (start, stop)))| { + if z_min <= z && z < z_max && y_min <= y && y < y_max { + let start = max(x_min, start); + let stop = min(x_max, stop); + let mut section = tile.slice_mut(s![ + z - z_min, y - y_min, (start - x_min)..(stop - x_min) + ]); + section.fill(i + 1); + } + }); + } + }); + + tile.into_dyn() + } +} diff --git a/transforms/images/polus-ftl-label-plugin/src/requirements.txt b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt similarity index 100% rename from transforms/images/polus-ftl-label-plugin/src/requirements.txt rename to transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt diff --git a/transforms/images/polus-ftl-label-plugin/Cargo.toml b/transforms/images/polus-ftl-label-plugin/Cargo.toml deleted file mode 100644 index 13c2f3a87..000000000 --- a/transforms/images/polus-ftl-label-plugin/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -authors = ["Najib Ishaq "] -name = "ftl-rust" -version = "0.1.0" -edition = "2018" - -[lib] -name = "ftl_rust" -crate-type = ["cdylib", "rlib"] - -[dependencies] -pyo3 = { version = "0.14", features = ["extension-module"] } -rayon = "1.0" -ndarray = { version = "0.15", features = ["rayon"] } -numpy = "0.14" - -[dev-dependencies] -memmap2 = "0.3" -ndarray-npy = "0.8" -criterion = { version = "^0.3", features = ["html_reports"] } - -[[bench]] -name = "ftl_rust" -harness = false \ No newline at end of file diff --git a/transforms/images/polus-ftl-label-plugin/pyproject.toml b/transforms/images/polus-ftl-label-plugin/pyproject.toml deleted file mode 100644 index 650da5fae..000000000 --- a/transforms/images/polus-ftl-label-plugin/pyproject.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build-system] -requires = ["setuptools>=41.0.0", "wheel", "setuptools_rust>=0.10.2"] diff --git a/transforms/images/polus-ftl-label-plugin/rust_setup.py b/transforms/images/polus-ftl-label-plugin/rust_setup.py deleted file mode 100644 index 5da317d3a..000000000 --- a/transforms/images/polus-ftl-label-plugin/rust_setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup -from setuptools_rust import RustExtension - - -setup( - name="ftl-rust", - version="0.1.0", - packages=["ftl_rust"], - rust_extensions=[RustExtension("ftl_rust.ftl_rust", "Cargo.toml", debug=False)], - include_package_data=True, - zip_safe=False, -) diff --git a/transforms/images/polus-ftl-label-plugin/src/main.py b/transforms/images/polus-ftl-label-plugin/src/main.py deleted file mode 100644 index f072530ef..000000000 --- a/transforms/images/polus-ftl-label-plugin/src/main.py +++ /dev/null @@ -1,211 +0,0 @@ -import argparse -import logging -import os -from pathlib import Path - -import ftl -import numpy -from bfio import BioReader -from bfio import BioWriter -from ftl_rust import PolygonSet -from preadator import ProcessManager - -POLUS_LOG = getattr(logging, os.environ.get("POLUS_LOG", "INFO")) -POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") # TODO: Figure out how to use this - -# Initialize the logger -logging.basicConfig( - format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", - datefmt="%d-%b-%y %H:%M:%S", -) -logger = logging.getLogger("main") -logger.setLevel(POLUS_LOG) - - -def get_output_name(filename: str) -> str: - name = filename.split(".ome")[0] - return f"{name}{POLUS_EXT}" - - -def filter_by_size( - file_paths: list[Path], - size_threshold: int, -) -> tuple[list[Path], list[Path]]: - """Partitions the input files by the memory-footprint for the images. - - Args: - file_paths: The list of files to partition. - size_threshold: The memory-size (in MB) to use as a threshold. - - Returns: - A 2-tuple of lists of paths. - The first list contains small images and the second list contains large images. - """ - small_files, large_files = [], [] - threshold: int = size_threshold * 1024 * 1024 - - for file_path in file_paths: - with BioReader(file_path) as reader: - num_pixels = numpy.prod(reader.shape) - dtype = reader.dtype - - if dtype in (numpy.uint8, bool): - pixel_bytes = 8 - elif dtype == numpy.uint16: - pixel_bytes = 16 - elif dtype == numpy.uint32 or dtype == numpy.float32: - pixel_bytes = 32 - else: - pixel_bytes = 64 - - image_size = num_pixels * (pixel_bytes / 8) # Convert bits to bytes - (small_files if image_size <= threshold else large_files).append(file_path) - - return small_files, large_files - - -def label_cython( - input_path: Path, - output_path: Path, - connectivity: int, - bin_thresh: float, -): - """Label the input image and writes labels back out. - - Args: - input_path: Path to input image. - output_path: Path for output image. - connectivity: Connectivity kind. - bin_thresh: Binarization threshold. - """ - with ProcessManager.thread() as active_threads: - with BioReader( - input_path, - max_workers=active_threads.count, - ) as reader: - with BioWriter( - output_path, - max_workers=active_threads.count, - metadata=reader.metadata, - ) as writer: - # Load an image and convert to binary - image = numpy.squeeze(reader[..., 0, 0]) - - # If the image has float values, binarize it using the threshold - if image.dtype == numpy.float32 or image.dtype == numpy.float64: - image = (image > bin_thresh).astype(numpy.uint8) - - if not numpy.any(image): - writer.dtype = numpy.uint8 - writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) - return None - - image = image > 0 - if connectivity > image.ndim: - ProcessManager.log( - f"{input_path.name}: Connectivity is not less than or equal to the number of image dimensions, " - f"skipping this image. connectivity={connectivity}, ndim={image.ndim}", - ) - return None - - # Run the labeling algorithm - labels = ftl.label_nd(image, connectivity) - - # Save the image - writer.dtype = labels.dtype - writer[:] = labels - return True - - -if __name__ == "__main__": - # Setup the argument parsing - logger.info("Parsing arguments...") - parser = argparse.ArgumentParser( - prog="main", - description="Label objects in a 2d or 3d binary image.", - ) - - parser.add_argument( - "--inpDir", - dest="inpDir", - type=str, - required=True, - help="Input image collection to be processed by this plugin", - ) - - parser.add_argument( - "--connectivity", - dest="connectivity", - type=int, - required=True, - help="City block connectivity, must be less than or equal to the number of dimensions", - ) - - parser.add_argument( - "--binarizationThreshold", - dest="bin_thresh", - type=float, - required=True, - help="For images containing probability values. Must be between 0 and 1.0.", - ) - - parser.add_argument( - "--outDir", - dest="outDir", - type=str, - required=True, - help="Output collection", - ) - - # Parse the arguments - args = parser.parse_args() - - _connectivity = int(args.connectivity) - logger.info(f"connectivity = {_connectivity}") - - _bin_thresh = float(args.bin_thresh) - assert 0 <= _bin_thresh <= 1, "bin_thresh must be between 0 and 1" - logger.info(f"bin_thresh = {_bin_thresh:.2f}") - - _input_dir = Path(args.inpDir).resolve() - assert _input_dir.exists(), f"{_input_dir } does not exist." - if _input_dir.joinpath("images").is_dir(): - _input_dir = _input_dir.joinpath("images") - logger.info(f"inpDir = {_input_dir}") - - _output_dir = Path(args.outDir).resolve() - assert _output_dir.exists(), f"{_output_dir } does not exist." - logger.info(f"outDir = {_output_dir}") - - # We only need a thread manager since labeling and image reading/writing - # release the gil - ProcessManager.init_threads() - - # Get all file names in inpDir image collection - _files = list( - filter( - lambda _file: _file.is_file() and _file.name.endswith(".ome.tif"), - _input_dir.iterdir(), - ), - ) - _small_files, _large_files = filter_by_size(_files, 500) - - logger.info(f"processing {len(_files)} images in total...") - logger.info(f"processing {len(_small_files)} small images with cython...") - logger.info(f"processing {len(_large_files)} large images with rust") - - if _small_files: - for _infile in _small_files: - ProcessManager.submit_thread( - label_cython, - _infile, - _output_dir.joinpath(get_output_name(_infile.name)), - _connectivity, - _bin_thresh, - ) - ProcessManager.join_threads() - - if _large_files: - for _infile in _large_files: - _outfile = _output_dir.joinpath(get_output_name(_infile.name)) - PolygonSet(_connectivity, _bin_thresh).read_from(_infile).write_to(_outfile) diff --git a/transforms/images/polus-ftl-label-plugin/src/polygons.rs b/transforms/images/polus-ftl-label-plugin/src/polygons.rs deleted file mode 100644 index 8bbb52d07..000000000 --- a/transforms/images/polus-ftl-label-plugin/src/polygons.rs +++ /dev/null @@ -1,441 +0,0 @@ -use std::cmp::max; -use std::cmp::min; -use std::cmp::Ordering; -use std::sync::Arc; -use std::sync::RwLock; - -use ndarray::prelude::*; -use numpy::PyReadonlyArrayDyn; -use pyo3::prelude::*; -use rayon::prelude::*; - -type Slice = (usize, (usize, (usize, usize))); // (z, (y, (x_min, x_max))) -type PolyVec = Vec>; - -fn do_slices_overlap(left: Slice, right: Slice, connectivity: u8) -> bool { - let (left_z, (left_y, (left_start, left_stop))) = left; - let (right_z, (right_y, (right_start, right_stop))) = right; - - if (left_start > right_stop) || (right_start > left_stop) { - return false; - } - - let z_diff = if left_z > right_z { - left_z - right_z - } else { - right_z - left_z - }; - if z_diff > 1 { - return false; - } - - let y_diff = if left_y > right_y { - left_y - right_y - } else { - right_y - left_y - }; - if y_diff > 1 { - return false; - } - - if z_diff == 0 && y_diff == 0 { - (left_start == right_stop) || (right_start == left_stop) - } else if y_diff == 1 && z_diff == 1 { - if connectivity == 3 { - (left_start <= right_stop) || (right_start <= left_stop) - } else { - (left_start < right_stop) || (right_start < left_stop) - } - } else if connectivity == 1 { - (left_start < right_stop) || (right_start < left_stop) - } else { - (left_start <= right_stop) || (right_start <= left_stop) - } -} - -/// A `Polygon` represents a single connected object to be labelled. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Polygon { - /// Connectivity determines how we find neighbors for pixels. - connectivity: u8, - /// A collection of slices that represents the exact bounds of the polygon. - slices: Vec, - /// minimum x-value of the bounding-box around the polygon. - x_min: usize, - /// maximum x-value of the bounding-box around the polygon. - x_max: usize, - /// minimum y-value of the bounding-box around the polygon. - y_min: usize, - /// maximum y-value of the bounding-box around the polygon. - y_max: usize, - /// minimum z-value of the bounding-box around the polygon. - z_min: usize, - /// maximum z-value of the bounding-box around the polygon. - z_max: usize, -} - -impl Polygon { - /// Creates a new `Polygon` from the given slices. - /// - /// # Arguments - /// - /// * `connectivity` - A `u8` to represent the connectivity used for creating a `Polygon`. - /// * `slices` - A Vec of Slices that represent the exact boundaries of the `Polygon`. - /// This must be non-empty. We assume that the caller has verified that the slices are connected. - /// Each slice is represented as a nested tuple `(z, (y, (x_min, x_max)))` - pub fn new(connectivity: u8, slices: Vec) -> Self { - assert!( - !slices.is_empty(), - "Cannot create Polygon without any slices." - ); - - let (zs, rest): (Vec<_>, Vec<_>) = slices.iter().copied().unzip(); - let z_min = *zs.iter().min().unwrap(); - let z_max = zs.into_iter().max().unwrap() + 1; - - let (ys, rest): (Vec<_>, Vec<_>) = rest.into_iter().unzip(); - let y_min = *ys.iter().min().unwrap(); - let y_max = ys.into_iter().max().unwrap() + 1; - - let (starts, stops): (Vec<_>, Vec<_>) = rest.into_iter().unzip(); - let x_min = starts.into_iter().min().unwrap(); - let x_max = stops.into_iter().max().unwrap(); - - Polygon { - connectivity, - slices, - x_min, - x_max, - y_min, - y_max, - z_min, - z_max, - } - } - - /// Returns whether this `Polygon` has any slices. - pub fn is_empty(&self) -> bool { - self.slices.is_empty() - } - - /// Returns the number of slices in this `Polygon`. - pub fn len(&self) -> usize { - self.slices.len() - } - - /// Returns whether this `Polygon's` bounding-box intersects with that of another `Polygon`. - /// - /// This is useful as an early filter for the `boundary_connects` method. - /// - /// # Arguments - /// - /// * `other` - Another `Polygon`. - /// - /// # Returns - /// - /// * Whether the bounding-boxes of the two `Polygons` overlap. - pub fn bbox_overlap(&self, other: &Self) -> bool { - self.x_min <= other.x_max - && self.y_min <= other.y_max - && self.z_min <= other.z_max - && self.x_max >= other.x_min - && self.y_max >= other.y_min - && self.z_max >= other.z_min - } - - /// Returns whether this `Polygon` connects with another `Polygon`. - /// - /// # Arguments - /// - /// * `other` - Another `Polygon`. - ///1 - /// # Returns - /// - /// * Whether the two `Polygons` intersect. - pub fn boundary_connects(&self, other: &Self) -> bool { - if self.bbox_overlap(other) { - self.slices.par_iter().any(|&left| { - other - .slices - .par_iter() - .any(|&right| do_slices_overlap(left, right, self.connectivity)) - }) - } else { - false - } - } - - /// Absorbs the other `Polygon` into itself. - /// - /// This leaves the other `Polygon` empty. The caller is responsible for properly handling the other `Polygon`. - pub fn absorb(&mut self, other: &mut Self) { - self.x_min = min(self.x_min, other.x_min); - self.y_min = min(self.y_min, other.y_min); - self.z_min = min(self.z_min, other.z_min); - self.x_max = max(self.x_max, other.x_max); - self.y_max = max(self.y_max, other.y_max); - self.z_max = max(self.z_max, other.z_max); - - self.slices.extend(other.slices.drain(..)); - } -} - -/// Given a `Vec` of `Polygons` and a connectivity, partitions the `Polygons` into groups that are connected, merges each group into a single polygon, and returns the merged polygons. -/// -/// # Arguments -/// -/// `polygons` - A `Vec` of `Polygons` to process. This Vec will be consumed by the function. -/// `connectivity` - A `u8` to represent the connectivity used for determining neighbors. -/// -/// # Returns -/// -/// A `Vec` of the merged `Polygons`. These `Polygons` do not connect with each other. -fn bft_partition(polygons: &mut Vec, connectivity: u8) -> Vec { - let mut merged: Vec = Vec::new(); - - polygons.iter_mut().for_each(|target| { - merged - .iter_mut() - .filter(|candidate| !candidate.is_empty()) - .for_each(|mut candidate| { - if target.boundary_connects(candidate) { - target.absorb(&mut candidate); - } - }); - - merged.push(Polygon { - connectivity, - slices: target.slices.drain(..).collect(), - x_min: target.x_min, - y_min: target.y_min, - z_min: target.z_min, - x_max: target.x_max, - y_max: target.y_max, - z_max: target.z_max, - }); - }); - - merged - .iter_mut() - .filter(|polygon| !polygon.is_empty()) - .map(|polygon| Polygon { - connectivity, - slices: polygon.slices.drain(..).collect(), - x_min: polygon.x_min, - y_min: polygon.y_min, - z_min: polygon.z_min, - x_max: polygon.x_max, - y_max: polygon.y_max, - z_max: polygon.z_max, - }) - .collect() -} - -impl Ord for Polygon { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap() - } -} - -impl PartialOrd for Polygon { - fn partial_cmp(&self, other: &Self) -> Option { - match self.z_min.cmp(&other.z_min) { - Ordering::Less => Some(Ordering::Less), - Ordering::Greater => Some(Ordering::Greater), - Ordering::Equal => match self.y_min.cmp(&other.y_min) { - Ordering::Less => Some(Ordering::Less), - Ordering::Greater => Some(Ordering::Greater), - Ordering::Equal => match self.x_min.cmp(&other.x_min) { - Ordering::Less => Some(Ordering::Less), - Ordering::Greater => Some(Ordering::Greater), - Ordering::Equal => match self.z_max.cmp(&other.z_max) { - Ordering::Less => Some(Ordering::Less), - Ordering::Greater => Some(Ordering::Greater), - Ordering::Equal => match self.y_max.cmp(&other.y_max) { - Ordering::Less => Some(Ordering::Less), - Ordering::Greater => Some(Ordering::Greater), - Ordering::Equal => Some(self.x_max.cmp(&other.x_max)), - }, - }, - }, - }, - } - } -} - -/// A `PolygonSet` handles and maintains `Polygons` in an image. This provides utilities for adding tiles from an image, reconciling labels within and across tiles, and extracting tiles with labelled objects. -#[pyclass] -#[derive(Clone, Debug)] -pub struct PolygonSet { - /// A `u8` to represent the connectivity used for determining neighbors. - connectivity: u8, - /// A collection of `Polygons`. The Read-Write lock allows us to behave, for `Python`, that the object is mutable - /// by default while behaving, for `Rust`, the at object is immutable. - polygons: Arc>, -} - -#[pymethods] -impl PolygonSet { - /// Creates a new `PolygonSet` with an empty `Vec` of `Polygons`. - #[new] - pub fn new(connectivity: u8) -> Self { - PolygonSet { - connectivity, - polygons: Arc::new(RwLock::new(Vec::new())), - } - } - - /// Returns whether the `PolygonSet` is empty. This method is a Rust-recommended complement to the `len` method. - pub fn is_empty(&self) -> bool { - self.polygons.read().unwrap().is_empty() - } - - /// Returns the number of `Polygons` in this set. - pub fn len(&self) -> usize { - self.polygons.read().unwrap().len() - } - - pub fn add_tile(&self, tile: PyReadonlyArrayDyn, top_left_point: (usize, usize, usize)) { - self._add_tile(tile.as_array(), top_left_point) - } - - /// Restores the invariant that no two `Polygons` in the `PolygonSet` connect with each other. - pub fn digest(&self) { - let mut polygons = self - .polygons - .write() - .unwrap() - .drain(..) - .map(|polygon| Polygon { - connectivity: polygon.connectivity, - slices: polygon.slices.iter().copied().collect(), - x_min: polygon.x_min, - y_min: polygon.y_min, - z_min: polygon.z_min, - x_max: polygon.x_max, - y_max: polygon.y_max, - z_max: polygon.z_max, - }) - .collect(); - let mut polygons = bft_partition(&mut polygons, self.connectivity); - polygons.sort(); - self.polygons - .write() - .unwrap() - .extend(polygons.drain(..).map(Arc::new)); - } -} - -impl PolygonSet { - /// Detects `Polygons` in a tile and adds them to the set. - /// - /// This might break the invariant that no two `Polygons` in the `PolygonSet` connect with each other. - /// Therefore, The user must call the `digest` method after all tiles have been added. - pub fn _add_tile(&self, tile: ArrayViewD, top_left_point: (usize, usize, usize)) { - // TODO: Figure out how to add tiles in parallel, with the GIL being the main problem. - // TODO: Is it possible to have Rust directly call methods from bfio? - let (z_min, y_min, x_min) = top_left_point; - - let mut slices = tile - .outer_iter() - .into_par_iter() - .enumerate() - .flat_map(|(z, plane)| { - let mut slices = plane - .outer_iter() - .into_par_iter() - .enumerate() - .flat_map(|(y, row)| { - // TODO: Insert AVX here. - let runs: Vec<_> = row - .iter() - .chain([0].iter()) - .zip([0].iter().chain(row.iter())) - .enumerate() - .filter(|(_, (&a, &b))| a != b) - .map(|(i, _)| i) - .collect(); - - if runs.is_empty() { - Vec::new() - } else { - let starts: Vec<_> = runs.iter().step_by(2).cloned().collect(); - let ends: Vec<_> = runs.into_iter().skip(1).step_by(2).collect(); - - starts - .into_par_iter() - .zip(ends.into_par_iter()) - .map(|(start, stop)| { - Polygon::new( - self.connectivity, - vec![( - z + z_min, - (y + y_min, (start + x_min, stop + x_min)), - )], - ) - }) - .collect::>() - } - }) - .collect::>(); - bft_partition(&mut slices, self.connectivity) - }) - .collect::>(); - - let mut polygons = bft_partition(&mut slices, self.connectivity); - - self.polygons - .write() - .unwrap() - .extend(polygons.drain(..).map(Arc::new)); - } - - /// Once all tiles have been added and digested, this method can be used to extract tiles with properly labelled objects. - pub fn _extract_tile( - &self, - coordinates: (usize, usize, usize, usize, usize, usize), - ) -> ArrayD { - let (z_min, z_max, y_min, y_max, x_min, x_max) = coordinates; - - let tile_polygon = Polygon::new( - self.connectivity, - vec![ - (z_min, (y_min, (x_min, x_max))), - (z_max, (y_max, (x_min, x_max))), - ], - ); - - // We use a Read-Write lock on each row to allow writing to rows in parallel. - // This really only shines when we try to extract very large tiles. - // Otherwise, it is no slower than a single-threaded implementation without the RwLocks. - let mut tile: Array3 = Array3::zeros((z_max - z_min, y_max - y_min, x_max - x_min)); - - self.polygons - .read() - .unwrap() - .iter() - .enumerate() - .for_each(|(i, polygon)| { - if tile_polygon.bbox_overlap(polygon) { - polygon - .slices - .iter() - .copied() - .for_each(|(z, (y, (start, stop)))| { - if z_min <= z && z < z_max && y_min <= y && y < y_max { - let start = max(x_min, start); - let stop = min(x_max, stop); - let mut section = tile.slice_mut(s![ - z - z_min, - y - y_min, - (start - x_min)..(stop - x_min) - ]); - section.fill(i + 1); - } - }); - } - }); - - tile.into_dyn() - } -} diff --git a/transforms/images/polus-ftl-label-plugin/src/setup.py b/transforms/images/polus-ftl-label-plugin/src/setup.py deleted file mode 100644 index 4ad66cc27..000000000 --- a/transforms/images/polus-ftl-label-plugin/src/setup.py +++ /dev/null @@ -1,11 +0,0 @@ -from setuptools import setup -import numpy, os -from Cython.Build import cythonize -from Cython.Compiler import Options - -Options.annotate = True - -os.environ['CFLAGS'] = '-march=haswell -O3' -os.environ['CXXFLAGS'] = '-march=haswell -O3' -setup(ext_modules=cythonize("ftl.pyx",compiler_directives={'language_level' : "3"}), - include_dirs=[numpy.get_include()]) From bf73ae6724cd9a61dccd03270e1c176d18902a77 Mon Sep 17 00:00:00 2001 From: Nazanin Donyapour Date: Thu, 12 Mar 2026 21:35:15 +0000 Subject: [PATCH 15/16] Update flt_label tool --- .pre-commit-config.yaml | 2 +- .../images/ftl-label-tool/.bumpversion.cfg | 12 +- transforms/images/ftl-label-tool/.gitignore | 28 +- transforms/images/ftl-label-tool/Cargo.lock | 1073 +++++++++++++++++ transforms/images/ftl-label-tool/Cargo.toml | 2 +- transforms/images/ftl-label-tool/Dockerfile | 80 +- transforms/images/ftl-label-tool/README.md | 116 +- .../ftl-label-tool/SimpleTiledTiffViewer.py | 48 - transforms/images/ftl-label-tool/VERSION | 2 +- .../images/ftl-label-tool/build-docker.sh | 20 +- .../ftl-label-tool/ftl_rust/__init__.py | 143 --- transforms/images/ftl-label-tool/ftllabel.cwl | 2 +- transforms/images/ftl-label-tool/ict.yaml | 11 +- transforms/images/ftl-label-tool/plugin.json | 11 +- .../images/ftl-label-tool/pyproject.toml | 71 +- .../ftl-label-tool/rust_requirements.txt | 1 - .../images/ftl-label-tool/rust_setup.py | 3 - transforms/images/ftl-label-tool/setup.py | 74 +- .../images/ftl-label-tool/src/__init__.py | 1 + .../ftl-label-tool/src/ftl_rust/__init__.py | 176 +++ .../ftl-label-tool/src/polus/__init__.py | 1 + .../src/polus/images/__init__.py | 1 + .../src/polus/images/transforms/__init__.py | 1 + .../images/transforms/images/__init__.py | 1 + .../transforms/images/ftl_label/__init__.py | 2 + .../transforms/images/ftl_label/__main__.py | 93 +- .../transforms/images/ftl_label/bench_rust.py | 26 - .../transforms/images/ftl_label/ftl.pyx | 144 +-- .../images/transforms/images/ftl_label/lib.rs | 2 +- .../images/ftl_label/requirements.txt | 1 - .../images/ftl-label-tool/tests/__init__.py | 1 + .../images/ftl-label-tool/tests/conftest.py | 80 ++ .../images/ftl-label-tool/tests/test_main.py | 61 + 33 files changed, 1758 insertions(+), 532 deletions(-) create mode 100644 transforms/images/ftl-label-tool/Cargo.lock delete mode 100644 transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py delete mode 100644 transforms/images/ftl-label-tool/ftl_rust/__init__.py delete mode 100644 transforms/images/ftl-label-tool/rust_requirements.txt delete mode 100644 transforms/images/ftl-label-tool/rust_setup.py create mode 100644 transforms/images/ftl-label-tool/src/__init__.py create mode 100644 transforms/images/ftl-label-tool/src/ftl_rust/__init__.py create mode 100644 transforms/images/ftl-label-tool/src/polus/__init__.py create mode 100644 transforms/images/ftl-label-tool/src/polus/images/__init__.py create mode 100644 transforms/images/ftl-label-tool/src/polus/images/transforms/__init__.py create mode 100644 transforms/images/ftl-label-tool/src/polus/images/transforms/images/__init__.py delete mode 100644 transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py create mode 100644 transforms/images/ftl-label-tool/tests/__init__.py create mode 100644 transforms/images/ftl-label-tool/tests/conftest.py create mode 100644 transforms/images/ftl-label-tool/tests/test_main.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e78c94e6..f40d4ad65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: rev: "23.3.0" hooks: - id: black - language_version: python3.9 + language_version: python3.12 exclude: | (?x)( ^src\/polus\/plugins\/_plugins\/models\/pydanticv1\/\w*Schema.py$| diff --git a/transforms/images/ftl-label-tool/.bumpversion.cfg b/transforms/images/ftl-label-tool/.bumpversion.cfg index 2d2d187bd..7f9d4bedc 100644 --- a/transforms/images/ftl-label-tool/.bumpversion.cfg +++ b/transforms/images/ftl-label-tool/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.3.12-dev5 +current_version = 1.0.0-dev0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? @@ -16,8 +16,18 @@ values = [bumpversion:part:dev] +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + [bumpversion:file:plugin.json] +[bumpversion:file:ftllabel.cwl] + +[bumpversion:file:ict.yaml] + [bumpversion:file:VERSION] [bumpversion:file:README.md] + +[bumpversion:file:src/polus/images/transforms/images/ftl_label/__init__.py] diff --git a/transforms/images/ftl-label-tool/.gitignore b/transforms/images/ftl-label-tool/.gitignore index 0d8e39e08..94b3b3d5f 100644 --- a/transforms/images/ftl-label-tool/.gitignore +++ b/transforms/images/ftl-label-tool/.gitignore @@ -1,10 +1,22 @@ -*.png +# Compiled extensions *.so +*.pyd + +# Cython annotation *.html -*.cpp -*.npy -build -dist -ftl_rust.egg-info -target -Cargo.lock + +# Python cache +__pycache__/ +*.pyc +*.pyo + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Rust build output +target/ + +# Logs +*.log diff --git a/transforms/images/ftl-label-tool/Cargo.lock b/transforms/images/ftl-label-tool/Cargo.lock new file mode 100644 index 000000000..7b1ecf71d --- /dev/null +++ b/transforms/images/ftl-label-tool/Cargo.lock @@ -0,0 +1,1073 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "ftl-rust" +version = "0.1.0" +dependencies = [ + "criterion", + "memmap2", + "ndarray", + "ndarray-npy", + "numpy", + "pyo3", + "rayon", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", + "rayon", +] + +[[package]] +name = "ndarray-npy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b313788c468c49141a9d9b6131fc15f403e6ef4e8446a0b2e18f664ddb278a9" +dependencies = [ + "byteorder", + "ndarray", + "num-complex", + "num-traits", + "py_literal", + "zip", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numpy" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "py_literal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1" +dependencies = [ + "num-bigint", + "num-complex", + "num-traits", + "pest", + "pest_derive", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/transforms/images/ftl-label-tool/Cargo.toml b/transforms/images/ftl-label-tool/Cargo.toml index 7c544ba4d..16643f10c 100644 --- a/transforms/images/ftl-label-tool/Cargo.toml +++ b/transforms/images/ftl-label-tool/Cargo.toml @@ -25,4 +25,4 @@ criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "ftl_rust" -harness = false \ No newline at end of file +harness = false diff --git a/transforms/images/ftl-label-tool/Dockerfile b/transforms/images/ftl-label-tool/Dockerfile index 2a7266fc2..85b0e5f83 100644 --- a/transforms/images/ftl-label-tool/Dockerfile +++ b/transforms/images/ftl-label-tool/Dockerfile @@ -1,49 +1,49 @@ -# Compile ftl.pyx -FROM python:3.13 -ARG EXEC_DIR="/opt/executables" -RUN mkdir -p ${EXEC_DIR}/executables -COPY src ${EXEC_DIR}/. -WORKDIR ${EXEC_DIR} -RUN pip3 install --upgrade cython==3.0.11 setuptools \ - && pip3 install numpy==2.1.0 --no-cache-dir \ - && python3 setup.py build_ext --inplace -RUN cp executables/* . && rm -rf executables - -# Build the plugin container -FROM python:3.13-slim - -# Get essential packages -RUN apt-get update && \ - apt-get install -y build-essential curl && \ - apt-get update - -# Get Rust -RUN curl https://sh.rustup.rs -sSf | bash -s -- -y - -ENV PATH="/root/.cargo/bin:${PATH}" - -ARG EXEC_DIR="/ftl-rust" -ARG DATA_DIR="/data" - -RUN mkdir -p ${EXEC_DIR}/{src, ftl_rust} && mkdir -p ${DATA_DIR}/{inputs, outputs} +FROM polusai/bfio:2.5.0 -COPY . ${EXEC_DIR}/ - -COPY --from=0 /opt/executables ${EXEC_DIR}/src +# Environment variables defined in polusai/bfio +ENV EXEC_DIR="/opt/executables" +ENV POLUS_IMG_EXT=".ome.tif" +ENV POLUS_TAB_EXT=".csv" +ENV POLUS_LOG="INFO" +ENV NUM_THREADS=8 WORKDIR ${EXEC_DIR} -RUN pip3 install -r ${EXEC_DIR}/src/requirements.txt --no-cache-dir \ - && pip3 install -r rust_requirements.txt --no-cache-dir \ - && python3 rust_setup.py install +ENV TOOL_DIR="transforms/images/ftl-label-tool" -WORKDIR ${EXEC_DIR}/src +# Install system dependencies: +# - python3.11-dev: Python headers required to compile C/C++ extensions (Python.h) +# - build-essential: gcc/g++ compiler +# - curl: needed to download rustup +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + python3.11-dev \ + build-essential \ + curl && \ + rm -rf /var/lib/apt/lists/* -ENV POLUS_IMG_EXT=".ome.tif" -ENV POLUS_TAB_EXT=".csv" -ENV POLUS_LOG="INFO" +# Install Rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -RUN ls -la ${EXEC_DIR} +ENV PATH="/root/.cargo/bin:${PATH}" -ENTRYPOINT ["python3", "/ftl-rust/src/main.py"] +# Copy the repository +RUN mkdir -p image-tools +COPY . ${EXEC_DIR}/image-tools + +# Install build dependencies +RUN pip3 install \ + "setuptools>=68.0" \ + "setuptools-rust>=1.9.0" \ + "cython>=3.0.11" \ + "numpy>2.0.0" \ + wheel \ + --no-cache-dir + +# Install the tool (compiles Cython + Rust extensions) +RUN pip3 install "${EXEC_DIR}/image-tools/${TOOL_DIR}" \ + --no-build-isolation \ + --no-cache-dir + +ENTRYPOINT ["python3", "-m", "polus.images.transforms.images.ftl_label"] CMD ["--help"] diff --git a/transforms/images/ftl-label-tool/README.md b/transforms/images/ftl-label-tool/README.md index 51e7a4448..7dc4135c2 100644 --- a/transforms/images/ftl-label-tool/README.md +++ b/transforms/images/ftl-label-tool/README.md @@ -1,4 +1,4 @@ -# FTL Label (v0.3.12-dev5) +# FTL Label (v1.0.0-dev0) This plugin performs a transformation on binary images which, in a certain limiting case, can be thought of as segmentation. @@ -23,29 +23,36 @@ This lets it scale to arbitrarily large sizes but does make it slower than the C However, most of the bottleneck is in the interface between `Python` and `Rust`. The Rust implementation works with 2d and 3d images. +We determine whether to use the `Cython` or `Rust` implementation on a per-image basis depending on the size of that image. If we expect the image to occupy less than `500MB` of memory, we use the `Cython` implementation otherwise we use the `Rust` implementation. +On macOS ARM64 (Apple Silicon), all images are automatically routed through the Rust backend. + + To see detailed documentation for the `Rust` implementation you need to: * Install [Rust](https://doc.rust-lang.org/stable/book/ch01-01-installation.html), -* add Cargo to your `PATH`, and -* run from the terminal (in this directory): `cargo doc --open`. +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "$HOME/.cargo/env" +``` -That last command will generate documentation and open a new tab in your default web browser. +## Installation +#### From source (recommended for development) +```bash +# 1. Clone the repo +git clone +cd ftl-label-tool -We determine whether to use the `Cython` or `Rust` implementation on a per-image basis depending on the size of that image. -If we expect the image to occupy less than `500MB` of memory, we use the `Cython` implementation otherwise we use the `Rust` implementation. +# 2. Create a virtual environment +uv venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate -For more information on WIPP, visit the -[official WIPP page](https://isg.nist.gov/deepzoomweb/software/wipp). +# 3. Install the package (compiles Cython + Rust extensions) +uv pip install -e . -## To do - -The following optimizations should be added to increase the speed or decrease the memory used by the plugin. - -1. Implement existing specialized C++ methods that accelerate the run length encoding operation by a factor of 5-10 +# 4. Install the package with optional dependencies +uv pip install ".[dev]" -## Building - -To build the Docker image for the conversion plugin, run `./build-docker.sh`. +``` ## Install WIPP Plugin @@ -63,34 +70,46 @@ This plugin takes one input argument and one output argument: | `--binarizationThreshold` | For images containing probability values. Must be between 0 and 1.0. | Input | number | | `--outDir` | Output collection | Output | collection | -## Example Code +## Usage +```bash +python -m polus.images.transforms.images.ftl_label \ + --inpDir /path/to/input \ + --outDir /path/to/output \ + --connectivity 1 \ + --binarizationThreshold 0.5 +``` + +## Docker +#### Building +To build the Docker image for the conversion plugin, run `./build-docker.sh`. -```Linux -# Download some example *.tif files -wget https://github.com/stardist/stardist/releases/download/0.1.0/dsb2018.zip -unzip dsb2018.zip -mv dsb2018/test/masks/ images/ +#### Run -# Convert the *.tif files to *.ome.tif tiled tif format using bfio. +```bash basedir=$(basename ${PWD}) -docker run -v ${PWD}:/$basedir labshare/polus-tiledtiff-converter-plugin:1.1.0 \ - --input /$basedir/images/ \ - --output /$basedir/images_ome/ - -# Run the FTL label plugin -mkdir output -docker run -v ${PWD}:/$basedir labshare/polus-ftl-label-plugin:0.3.10 \ ---inpDir /$basedir/"images_ome/" \ ---outDir /$basedir/"output/" \ ---connectivity 1 - -# View the results using bfio and matplotlib -# Let's run directly on the host since we just need the python backend. -pip install bfio==2.1.9 matplotlib==3.5.1 -python3 SimpleTiledTiffViewer.py --inpDir images_ome/ --outDir output/ + +docker run -v ${PWD}:/$basedir polusai/ftl-label-tool:0.3.12 \ + --inpDir /$basedir/images/ \ + --outDir /$basedir/output/ \ + --connectivity 1 \ + --binarizationThreshold 0.5 +``` + +## Example + +```bash + +# Run FTL label + +python -m polus.images.transforms.images.ftl_label \ + --inpDir /path/to/images/ \ + --outDir /path/to/output/ \ + --connectivity 1 \ + --binarizationThreshold 0.5 ``` -**NOTE:** + +**Connectivity:** Connectivity uses [SciKit's](https://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.label) notation for connectedness, which we call cityblock notation. As you increase the connectivity, you increase the number of pixel jumps away from the center point. For example, in 2D there are 4 neighbors using 1-connectivity and 8 neighbors using 2-connectivity, @@ -109,3 +128,22 @@ SciKit's documentation has a good illustration for 2D: | / | \ hop 1 [ ] [ ] [ ] [ ] ``` + + +## Rust documentation +To generate and view the full Rust API docs: + +```bash +cargo doc --open +``` + +## To Do +The following optimizations should be added to increase the speed or decrease the memory used by the plugin. + +1. Implement existing specialized C++ methods that accelerate the run length encoding operation by a factor of 5-10 + +## For more information +To generate and view the full Rust API docs: + +For more information on WIPP, visit the +[official WIPP page](https://isg.nist.gov/deepzoomweb/software/wipp). diff --git a/transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py b/transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py deleted file mode 100644 index ed3409514..000000000 --- a/transforms/images/ftl-label-tool/SimpleTiledTiffViewer.py +++ /dev/null @@ -1,48 +0,0 @@ -from bfio import BioReader -from pathlib import Path -import argparse -import logging -import matplotlib.pyplot as plt - -if __name__=="__main__": - # Initialize the logger - logging.basicConfig(format='%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s', - datefmt='%d-%b-%y %H:%M:%S') - logger = logging.getLogger("main") - logger.setLevel(logging.INFO) - - # Setup the argument parsing - logger.info("Parsing arguments...") - parser = argparse.ArgumentParser(prog='main', description='View *.ome.tif images and labels from FTL plugin.') - parser.add_argument('--inpDir', dest='inpDir', type=str, - help='Input image collection to be processed by this plugin', required=True) - parser.add_argument('--outDir', dest='outDir', type=str, - help='Output collection', required=True) - - # Parse the arguments - args = parser.parse_args() - inpDir = Path(args.inpDir) - logger.info('inpDir = {}'.format(inpDir)) - outDir = Path(args.outDir) - logger.info('outDir = {}'.format(outDir)) - - # Get all file names in inpDir image collection - files = [f for f in inpDir.iterdir() if f.is_file() and f.name.endswith('.tif')] - - for file in files: - # Set up the BioReader - with BioReader(inpDir / file.name) as br_in: - img_in = br_in[:] - - with BioReader(outDir / file.name) as br_out: - img_out = br_out[:] - - fig, ax = plt.subplots(1, 2, figsize=(16,8)) - ax[0].imshow(img_in), ax[0].set_title("Original Image") - ax[1].imshow(img_out), ax[1].set_title("Labelled Image") - fig.suptitle(file.name) - plt.show() - # Use savefig if you are on a headless machine, i.e. AWS EC2 instance - # plt.savefig(outDir / (file.stem.split('.ome')[0] + '.png')) - plt.close() - diff --git a/transforms/images/ftl-label-tool/VERSION b/transforms/images/ftl-label-tool/VERSION index d37752338..059dbff91 100644 --- a/transforms/images/ftl-label-tool/VERSION +++ b/transforms/images/ftl-label-tool/VERSION @@ -1 +1 @@ -0.3.12-dev5 +1.0.0-dev0 diff --git a/transforms/images/ftl-label-tool/build-docker.sh b/transforms/images/ftl-label-tool/build-docker.sh index 638c0aa38..c9c5e9c45 100755 --- a/transforms/images/ftl-label-tool/build-docker.sh +++ b/transforms/images/ftl-label-tool/build-docker.sh @@ -1,4 +1,22 @@ #!/bin/bash +# Change the name of the tool here +tool_dir="transforms/images" +tool_name="ftl-label-tool" + +# The version is read from the VERSION file version=$( 1 else (1024 * 5) - - num_slices = z_shape // tile_size - if z_shape % tile_size != 0: - num_slices += 1 - - num_cols = y_shape // tile_size - if y_shape % tile_size != 0: - num_cols += 1 - - num_rows = x_shape // tile_size - if x_shape % tile_size != 0: - num_rows += 1 - - return tile_size, num_slices, num_cols, num_rows - - def read_from(self, infile: Path): - """ Reads from a .ome.tif file and finds and labels all objects. - - Args: - infile: Path to an ome.tif file for which to produce labels. - """ - logger.info(f'Processing {infile.name}...') - with BioReader(infile) as reader: - self.metadata = reader.metadata - - tile_size, num_slices, num_cols, num_rows = self._get_iteration_params(reader.Z, reader.Y, reader.X) - tile_count = 0 - for z in range(0, reader.Z, tile_size): - z_max = min(reader.Z, z + tile_size) - for y in range(0, reader.Y, tile_size): - y_max = min(reader.Y, y + tile_size) - for x in range(0, reader.X, tile_size): - x_max = min(reader.X, x + tile_size) - - tile = numpy.squeeze(reader[y:y_max, x:x_max, z::z_max, 0, 0]) - - # If the image has float values, binarize it using the threshold - if tile.dtype == numpy.float32 or tile.dtype == numpy.float64: - tile = (tile > self.bin_thresh).astype(numpy.uint8) - - # If the image is not binary, make it binary - tile = (tile != 0).astype(numpy.uint8) - - if tile.ndim == 2: - tile = tile[numpy.newaxis, :, :] - else: - tile = tile.transpose(2, 0, 1) - self.__polygon_set.add_tile(tile, (z, y, x)) - tile_count += 1 - logger.debug(f'added tile #{tile_count} ({z}:{z_max}, {y}:{y_max}, {x}:{x_max})') - logger.info(f'Reading Progress {100 * tile_count / (num_slices * num_cols * num_rows):6.3f}%...') - - logger.info('digesting polygons...') - self.__polygon_set.digest() - - self.num_polygons = self.__polygon_set.len() - logger.info(f'collected {self.num_polygons} polygons') - return self - - def write_to(self, outfile: Path): - """ Writes a labelled ome.tif to the given path. - - This uses the metadata of the input file and sets the dtype depending on the number of labelled objects. - - Args: - outfile: Path where the labelled image will be written. - """ - with BioWriter(outfile, metadata=self.metadata, max_workers=cpu_count()) as writer: - writer.dtype = self.dtype() - logger.info(f'writing {outfile.name} with dtype {self.dtype()}...') - - tile_size, _, num_cols, num_rows = self._get_iteration_params(writer.Z, writer.Y, writer.X) - tile_count = 0 - for z in range(writer.Z): - for y in range(0, writer.Y, tile_size): - y_max = min(writer.Y, y + tile_size) - for x in range(0, writer.X, tile_size): - x_max = min(writer.X, x + tile_size) - - tile = extract_tile(self.__polygon_set, (z, z + 1, y, y_max, x, x_max)) - writer[y:y_max, x:x_max, z:z + 1, 0, 0] = tile.transpose(1, 2, 0) - tile_count += 1 - logger.debug(f'Wrote tile {tile_count}, ({z}, {y}:{y_max}, {x}:{x_max})') - logger.info(f'Writing Progress {100 * tile_count / (num_cols * num_rows * writer.Z):6.3f}%...') - return self diff --git a/transforms/images/ftl-label-tool/ftllabel.cwl b/transforms/images/ftl-label-tool/ftllabel.cwl index aa989e0fe..ca71210f1 100644 --- a/transforms/images/ftl-label-tool/ftllabel.cwl +++ b/transforms/images/ftl-label-tool/ftllabel.cwl @@ -20,7 +20,7 @@ outputs: type: Directory requirements: DockerRequirement: - dockerPull: labshare/polus-ftl-label-plugin:0.3.8 + dockerPull: polusai/ftl-label-tool:1.0.0-dev0 InitialWorkDirRequirement: listing: - entry: $(inputs.outDir) diff --git a/transforms/images/ftl-label-tool/ict.yaml b/transforms/images/ftl-label-tool/ict.yaml index 84c629953..80ee333ba 100644 --- a/transforms/images/ftl-label-tool/ict.yaml +++ b/transforms/images/ftl-label-tool/ict.yaml @@ -1,10 +1,11 @@ author: - Nick Schaub - Najib Ishaq +- Hamdah Shafqat Abbasi contact: nick.schaub@nih.gov -container: labshare/polus-ftl-label-plugin:0.3.8 +container: polusai/ftl-label-tool:1.0.0-dev0 description: Label objects in a 2d or 3d binary image. -entrypoint: '[python3, main.py]' +entrypoint: python3 -m polus.images.transforms.images.ftl_label inputs: - description: Input image collection to be processed by this plugin format: @@ -18,7 +19,7 @@ inputs: name: connectivity required: true type: number -name: labshare/FTLLabel +name: polusai/FTLLabel outputs: - description: Output collection format: @@ -26,7 +27,7 @@ outputs: name: outDir required: true type: path -repository: https://github.com/labshare/polus-plugins +repository: https://github.com/PolusAI/image-tools specVersion: 1.0.0 title: FTL Label ui: @@ -38,4 +39,4 @@ ui: key: inputs.connectivity title: Connectivity type: number -version: 0.3.9 +version: 1.0.0-dev0 diff --git a/transforms/images/ftl-label-tool/plugin.json b/transforms/images/ftl-label-tool/plugin.json index 639ba65c4..4bf64b7cf 100644 --- a/transforms/images/ftl-label-tool/plugin.json +++ b/transforms/images/ftl-label-tool/plugin.json @@ -1,17 +1,18 @@ { "name": "FTL Label", - "version": "0.3.12-dev5", + "version": "1.0.0-dev0", "title": "FTL Label", "description": "Label objects in a 2d or 3d binary image.", - "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com)", + "author": "Nick Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@axleinfo.com), Hamdah Shafqat Abbasi(hamdah.abbasi@axleinfo.com)", "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", - "repository": "https://github.com/labshare/polus-plugins", + "repository": "https://github.com/PolusAI/image-tools", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/ftl-label-plugin:0.3.12-dev5", + "containerId": "polusai/ftl-label-tool:1.0.0-dev0", "baseCommand": [ "python3", - "/ftl-rust/src/main.py" + "-m", + "polus.images.transforms.images.ftl_label" ], "inputs": [ { diff --git a/transforms/images/ftl-label-tool/pyproject.toml b/transforms/images/ftl-label-tool/pyproject.toml index 0f67bd654..40748b984 100644 --- a/transforms/images/ftl-label-tool/pyproject.toml +++ b/transforms/images/ftl-label-tool/pyproject.toml @@ -1,66 +1,32 @@ -# pyproject.toml for ftl-label -# -# Two metadata tables coexist here: -# [project] – read by setuptools (PEP 621, required when -# build-backend = "setuptools.build_meta") -# [tool.poetry] – read by Poetry for virtualenv / lock-file management -# -# They must agree on name and version. - -# ── PEP 621 metadata (setuptools reads this) ────────────────────────────────── [project] name = "polus-images-segmentation-ftl-label" -version = "0.3.12.dev5" +version = "1.0.0-dev0" description = "Label objects in a 2D or 3D binary image using the FTL connected component algorithm." readme = "README.md" requires-python = ">=3.11,<3.13" authors = [ {name = "Nick Schaub", email = "nick.schaub@nih.gov"}, {name = "Najib Ishaq", email = "najib.ishaq@axleinfo.com"}, + {name = "Hamdah Shafqat Abbasi", email = "hamdah.abbasi@axleinfo.com"} ] -# Runtime dependencies (mirrors [tool.poetry.dependencies]) dependencies = [ "typer>=0.9.0", "numpy>2.0.0", - "bfio[all]==2.5.0", + "bfio==2.5.0", "filepattern==2.1.4", ] -# ── Poetry metadata (Poetry reads this for venv / lock-file) ────────────────── -[tool.poetry] -name = "polus-images-segmentation-ftl-label" -version = "0.3.12-dev5" -description = "Label objects in a 2D or 3D binary image using the FTL connected component algorithm." -authors = [ - "Nick Schaub ", - "Najib Ishaq ", +[project.optional-dependencies] +dev = [ + "bump2version>=1.0.1", + "pre-commit>=3.1.0", + "black>=23.1.0", + "flake8>=6.0.0", + "mypy>=1.0.1", + "pytest>=7.2.1", + "huggingface_hub>=1.6.0" ] -readme = "README.md" -packages = [{include = "polus", from = "src"}] -[tool.poetry.dependencies] -python = ">=3.11,<3.13" -typer = ">=0.9.0" -numpy = ">2.0.0" -bfio = "2.5.0" -filepattern = "2.1.4" - -[tool.poetry.group.dev.dependencies] -bump2version = "^1.0.1" -pre-commit = "^3.1.0" -black = "^23.1.0" -flake8 = "^6.0.0" -mypy = "^1.0.1" -pytest = "^7.2.1" - -[tool.poetry.group.build.dependencies] -setuptools = ">=68.0" -setuptools-rust = ">=1.9.0" -cython = ">=3.0.11" - -# ── Build system ─────────────────────────────────────────────────────────────── -# setuptools.build_meta is required (not poetry-core) so that setuptools-rust -# can hook into the build and compile the Rust/Cython extensions via rust_setup.py. [build-system] requires = [ "setuptools>=68.0", @@ -71,14 +37,13 @@ requires = [ ] build-backend = "setuptools.build_meta" -# ── setuptools package discovery ────────────────────────────────────────────── [tool.setuptools] package-dir = {"" = "src"} [tool.setuptools.packages.find] -where = ["src"] +where = ["src"] +include = ["polus*", "ftl_rust*"] -# ── mypy ────────────────────────────────────────────────────────────────────── [tool.mypy] mypy_path = "src" strict = true @@ -101,6 +66,10 @@ ignore_missing_imports = true module = "bfio.*" ignore_missing_imports = true -# ── pytest ──────────────────────────────────────────────────────────────────── +# Ruff / pre-commit configuration +[tool.ruff] +# Ignore setup.py to prevent INP001 / E402 errors +exclude = ["setup.py"] + [tool.pytest.ini_options] -pythonpath = ["."] \ No newline at end of file +pythonpath = ["."] diff --git a/transforms/images/ftl-label-tool/rust_requirements.txt b/transforms/images/ftl-label-tool/rust_requirements.txt deleted file mode 100644 index e897c9909..000000000 --- a/transforms/images/ftl-label-tool/rust_requirements.txt +++ /dev/null @@ -1 +0,0 @@ -setuptools-rust==0.12.1 diff --git a/transforms/images/ftl-label-tool/rust_setup.py b/transforms/images/ftl-label-tool/rust_setup.py deleted file mode 100644 index 4b0b1b890..000000000 --- a/transforms/images/ftl-label-tool/rust_setup.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Backward-compatibility shim – delegates to setup.py.""" -with open("setup.py") as _f: - exec(_f.read()) # noqa: S102 \ No newline at end of file diff --git a/transforms/images/ftl-label-tool/setup.py b/transforms/images/ftl-label-tool/setup.py index 4a48f52ed..c05821db3 100644 --- a/transforms/images/ftl-label-tool/setup.py +++ b/transforms/images/ftl-label-tool/setup.py @@ -1,24 +1,25 @@ -"""Build script for ftl-label native extensions. - -Architecture handling ---------------------- -ftl (Cython) – uses x86/x64 AVX2+BMI2 intrinsics (x86intrin.h). - Compiled ONLY on x86_64. Skipped silently on ARM64 / Apple Silicon. - -ftl_rust (Rust) – pure Rust with Rayon; compiles on all platforms including ARM64. - -When ftl is unavailable (ARM64), main.py routes ALL images through the Rust path. -""" - -import os +"""FTL Label Tool.""" +import logging import platform -import sys from pathlib import Path +from setuptools import Extension from setuptools import setup -from setuptools_rust import Binding, RustExtension +from setuptools_rust import Binding +from setuptools_rust import RustExtension + +# Optional Cython imports +try: + import numpy + from Cython.Build import cythonize + from Cython.Compiler import Options +except ImportError: + numpy = None + cythonize = None + Options = None -# ── Detect architecture ──────────────────────────────────────────────────────── +logger = logging.getLogger(__name__) +# Detect architecture machine = platform.machine().lower() IS_X86 = machine in ("x86_64", "amd64", "i686", "i386") @@ -26,39 +27,30 @@ ext_modules = [] -# ── Cython extension (x86/x64 only) ─────────────────────────────────────────── +# Cython extension (x86/x64 only) if IS_X86: try: - import numpy - from Cython.Build import cythonize - from Cython.Compiler import Options - Options.annotate = True - os.environ["CFLAGS"] = "-march=native -O3" - os.environ["CXXFLAGS"] = "-march=native -O3" cython_exts = cythonize( - str(SRC / "ftl.pyx"), + Extension( + name="ftl", + sources=[str(SRC / "ftl.pyx")], + include_dirs=[numpy.get_include()], + extra_compile_args=["-march=native", "-O3"], + extra_link_args=["-O3"], + language="c++", + ), compiler_directives={"language_level": 3}, ) - # Override the deep dotted path Cython infers → plain "ftl" - for ext in cython_exts: - ext.name = "ftl" - ext.include_dirs = [numpy.get_include()] - ext_modules.extend(cython_exts) - print(f"[setup.py] x86_64 detected – Cython extension will be compiled.") + logger.info("[setup.py] Cython found - compiling ftl from ftl.pyx") - except Exception as exc: # noqa: BLE001 - print(f"[setup.py] WARNING: Cython build skipped ({exc}).") -else: - print( - f"[setup.py] Non-x86 architecture detected ({machine}) – " - "Cython AVX extension is not supported here.\n" - " All images will be processed via the Rust backend." - ) + except ImportError as exc: + logger.info(f"[setup.py] WARNING: Cython build skipped ({exc}).") -# ── Rust/PyO3 extension (all platforms) ─────────────────────────────────────── + +# Rust/PyO3 extension (all platforms) rust_ext = RustExtension( target="ftl_rust.ftl_rust", path="Cargo.toml", @@ -66,9 +58,9 @@ debug=False, ) -# ── Setup ────────────────────────────────────────────────────────────────────── +# Setup setup( rust_extensions=[rust_ext], ext_modules=ext_modules, zip_safe=False, -) \ No newline at end of file +) diff --git a/transforms/images/ftl-label-tool/src/__init__.py b/transforms/images/ftl-label-tool/src/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/src/ftl_rust/__init__.py b/transforms/images/ftl-label-tool/src/ftl_rust/__init__.py new file mode 100644 index 000000000..81ab0271f --- /dev/null +++ b/transforms/images/ftl-label-tool/src/ftl_rust/__init__.py @@ -0,0 +1,176 @@ +"""FTL Label Tool.""" + +import logging +from multiprocessing import cpu_count +from pathlib import Path + +import numpy +from bfio import BioReader +from bfio import BioWriter + +from .ftl_rust import PolygonSet as RustPolygonSet +from .ftl_rust import extract_tile + +__all__ = ["PolygonSet"] + +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger("PolygonSet") +logger.setLevel(logging.INFO) + +MIN_CONNECTIVITY = 1 +MAX_CONNECTIVITY = 3 +TILE_SIZE_3D = 512 +TILE_SIZE_2D = 1024 * 5 +NDIM_2D = 2 + + +class PolygonSet: + """Creates a PolygonSet interface for the Rust implementation.""" + + def __init__(self, connectivity: int) -> None: + """Creates a PolygonSet interface for the Rust implementation. + + Args: + connectivity: Determines neighbors among pixels. Must be 1, 2 or 3. + See the README for more details. + """ + if not (MIN_CONNECTIVITY <= connectivity <= MAX_CONNECTIVITY): + msg = f"connectivity must be 1, 2 or 3. Got {connectivity} instead" + raise ValueError( + msg, + ) + + self.__polygon_set: RustPolygonSet = RustPolygonSet(connectivity) + self.connectivity: int = connectivity + self.metadata = None + self.num_polygons = 0 + + def __len__(self) -> int: + """Returns the number of objects that were detected.""" + return self.num_polygons + + def dtype(self) -> numpy.dtype: + """Chooses the minimal dtype for labels depending on the number of objects.""" + if self.num_polygons < 2**8: + dtype = numpy.uint8 + elif self.num_polygons < 2**16: + dtype = numpy.uint16 + else: + dtype = numpy.uint32 + return dtype + + @staticmethod + def _get_iteration_params( + z_shape: int, + y_shape: int, + x_shape: int, + ) -> tuple[int, int, int, int]: + tile_size = 512 if z_shape > 1 else (1024 * 5) + + num_slices = z_shape // tile_size + if z_shape % tile_size != 0: + num_slices += 1 + + num_cols = y_shape // tile_size + if y_shape % tile_size != 0: + num_cols += 1 + + num_rows = x_shape // tile_size + if x_shape % tile_size != 0: + num_rows += 1 + + return tile_size, num_slices, num_cols, num_rows + + def read_from(self, infile: Path) -> "PolygonSet": + """Reads from a .ome.tif file and finds and labels all objects. + + Args: + infile: Path to an ome.tif file for which to produce labels. + """ + logger.info(f"Processing {infile.name}...") + with BioReader(infile) as reader: + self.metadata = reader.metadata + + tile_size, num_slices, num_cols, num_rows = self._get_iteration_params( + reader.Z, + reader.Y, + reader.X, + ) + tile_count = 0 + + for z in range(0, reader.Z, tile_size): + z_max = min(reader.Z, z + tile_size) + for y in range(0, reader.Y, tile_size): + y_max = min(reader.Y, y + tile_size) + for x in range(0, reader.X, tile_size): + x_max = min(reader.X, x + tile_size) + + tile = numpy.squeeze(reader[y:y_max, x:x_max, z:z_max, 0, 0]) + tile = (tile != 0).astype(numpy.uint8) + if tile.ndim == NDIM_2D: + tile = tile[numpy.newaxis, :, :] + else: + tile = tile.transpose(2, 0, 1) + self.__polygon_set.add_tile(tile, (z, y, x)) + tile_count += 1 + msg = f"tile#{tile_count} ({z}-{z_max},{y}-{y_max},{x}-{x_max})" + logger.debug(msg) + msg = ( + f"Reading {100*tile_count/(num_slices*num_cols*num_rows):6.2f}%" + ) + logger.info(msg) + + logger.info("digesting polygons...") + self.__polygon_set.digest() + + self.num_polygons = self.__polygon_set.len() + logger.info(f"collected {self.num_polygons} polygons") + return self + + def write_to(self, outfile: Path) -> "PolygonSet": + """Writes a labelled ome.tif to the given path. + + Uses input metadata and sets dtype based on the number of label objects. + + Args: + outfile: Path where the labelled image will be written. + """ + with BioWriter( + outfile, + metadata=self.metadata, + max_workers=cpu_count(), + ) as writer: + writer.dtype = self.dtype() + logger.info(f"writing {outfile.name} with dtype {self.dtype()}...") + + tile_size, _, num_cols, num_rows = self._get_iteration_params( + writer.Z, + writer.Y, + writer.X, + ) + tile_count = 0 + for z in range(writer.Z): + for y in range(0, writer.Y, tile_size): + y_max = min(writer.Y, y + tile_size) + for x in range(0, writer.X, tile_size): + x_max = min(writer.X, x + tile_size) + + tile = extract_tile( + self.__polygon_set, + (z, z + 1, y, y_max, x, x_max), + ) + writer[y:y_max, x:x_max, z : z + 1, 0, 0] = tile.transpose( + 1, + 2, + 0, + ) + tile_count += 1 + logger.debug( + f"Wrote tile {tile_count}, ({z}, {y}:{y_max}, {x}:{x_max})", + ) + msg = f"{100*tile_count/(num_cols*num_rows*writer.Z):.2f}% done" + logger.info(msg) + return self diff --git a/transforms/images/ftl-label-tool/src/polus/__init__.py b/transforms/images/ftl-label-tool/src/polus/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/src/polus/images/__init__.py b/transforms/images/ftl-label-tool/src/polus/images/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/images/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/__init__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/__init__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py index e69de29bb..53860718c 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__init__.py @@ -0,0 +1,2 @@ +"""FTL Label Tool.""" +__version__ = "1.0.0-dev0" diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py index 78ab2676b..86366b2b4 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py @@ -1,18 +1,18 @@ """FTL Label Tool.""" import logging import os +from multiprocessing.pool import ThreadPool from pathlib import Path -import typer -import ftl import numpy +import typer from bfio import BioReader from bfio import BioWriter from ftl_rust import PolygonSet -from multiprocessing.pool import ThreadPool try: import ftl + FTL_CYTHON_AVAILABLE = True except ImportError: ftl = None # type: ignore[assignment] @@ -21,7 +21,7 @@ app = typer.Typer() POLUS_LOG = getattr(logging, os.environ.get("POLUS_LOG", "INFO")) -POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") +POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") _NUM_THREADS: int = int(os.environ.get("NUM_THREADS", os.cpu_count() or 1)) # Initialize the logger @@ -34,6 +34,7 @@ def get_output_name(filename: str) -> str: + """Generate the output filename using the configured extension.""" name = filename.split(".ome")[0] return f"{name}{POLUS_EXT}" @@ -89,38 +90,40 @@ def label_cython(args: tuple[Path, Path, int, float]) -> bool | None: True on success, None if the image was skipped. """ input_path, output_path, connectivity, bin_thresh = args - with BioReader(input_path, max_workers=_NUM_THREADS) as reader: - with BioWriter( - output_path, - max_workers=_NUM_THREADS, - metadata=reader.metadata, - ) as writer: - # Load an image and convert to binary - image = numpy.squeeze(reader[..., 0, 0]) - - # If the image has float values, binarize it using the threshold - if image.dtype == numpy.float32 or image.dtype == numpy.float64: - image = (image > bin_thresh).astype(numpy.uint8) - - if not numpy.any(image): - writer.dtype = numpy.uint8 - writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) - return None - - image = image > 0 - if connectivity > image.ndim: - logger.warning( - f"{input_path.name}: Connectivity is not less than or equal to the number of image dimensions, " - f"skipping this image. connectivity={connectivity}, ndim={image.ndim}", - ) - return None - - # Run the labeling algorithm - labels = ftl.label_nd(image, connectivity) - - # Save the image - writer.dtype = labels.dtype - writer[:] = labels + with BioReader(input_path, max_workers=_NUM_THREADS) as reader, BioWriter( + output_path, + max_workers=_NUM_THREADS, + metadata=reader.metadata, + ) as writer: + # Load an image and convert to binary + image = numpy.squeeze(reader[..., 0, 0]) + + # If the image has float values, binarize it using the threshold + if image.dtype == numpy.float32 or image.dtype == numpy.float64: + image = (image > bin_thresh).astype(numpy.uint8) + + if not numpy.any(image): + writer.dtype = numpy.uint8 + writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) + return None + + image = image > 0 + if connectivity > image.ndim: + logger.warning( + ( + f"{input_path.name}: Connectivity is not less than or equal to " + f"the number of image dimensions, skipping this image. " + f"connectivity={connectivity}, ndim={image.ndim}" + ), + ) + return None + + # Run the labeling algorithm + labels = ftl.label_nd(image, connectivity) + + # Save the image + writer.dtype = labels.dtype + writer[:] = labels return True @@ -173,14 +176,12 @@ def main( binarization_threshold: Threshold for binarizing float probability images. out_dir: Path to the output image collection. """ - logger.info(f"inpDir = {inp_dir}") logger.info(f"connectivity = {connectivity}") logger.info(f"binarizationThreshold = {binarization_threshold:.2f}") logger.info(f"outDir = {out_dir}") logger.info(f"threads = {_NUM_THREADS}") - # Get all file names in inpDir image collection if inp_dir.joinpath("images").is_dir(): inp_dir = inp_dir / "images" @@ -212,13 +213,21 @@ def main( skipped = results.count(None) if skipped: - logger.warning(f"{skipped} small image(s) were skipped (empty or mismatched connectivity).") + logger.warning( + ( + f"{skipped} small image(s) were skipped " + " (empty or mismatched connectivity)." + ), + ) - # Large files: Rust handles tiling and internal parallelism + # Large files: Rust handles tiling and internal parallelism if large_files: for infile in large_files: outfile = out_dir / get_output_name(infile.name) - PolygonSet(connectivity, binarization_threshold).read_from(infile).write_to(outfile) + PolygonSet(connectivity, binarization_threshold).read_from(infile).write_to( + outfile, + ) + if __name__ == "__main__": - app() \ No newline at end of file + app() diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py deleted file mode 100644 index 17f3d12aa..000000000 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/bench_rust.py +++ /dev/null @@ -1,26 +0,0 @@ -import time -from pathlib import Path - -from ftl_rust import PolygonSet - - -def bench_rust(): - count = 2209 - infile = Path(f'../../data/input_array/test_infile_{count}.ome.tif').resolve() - outfile = Path(f'../../data/input_array/test_outfile_{count}.ome.tif').resolve() - polygon_set = PolygonSet(connectivity=1) - - start = time.time() - polygon_set.read_from(infile) - end = time.time() - print(f'took {end - start:.3f} seconds to read and digest...') - - assert count == len(polygon_set), f'found {len(polygon_set)} objects instead of {count}.' - - polygon_set.write_to(outfile) - print(f'took {time.time() - end:.3f} seconds to write...') - return - - -if __name__ == '__main__': - bench_rust() diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx index 1b491d6fc..eb6fb2e97 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx @@ -46,7 +46,7 @@ cdef extern from "x86intrin.h": __m128i _mm_shuffle_epi8(__m128i a,__m128i b) nogil __m256i _mm256_add_epi16(__m256i a, __m256i b) nogil - + __m128i _mm_set_epi8(char e15,char e14,char e13,char e12, char e11,char e10,char e9, char e8, char e7, char e6, char e5, char e4, @@ -60,7 +60,7 @@ cdef extern from "x86intrin.h": char e11,char e10,char e9, char e8, char e7, char e6, char e5, char e4, char e3, char e2, char e1, char e0) nogil - + __m256i _mm256_set_epi64x(long long e3,long long e2, long long e1,long long e0) nogil @@ -117,7 +117,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): image (numpy.ndarray): An n-dimensional image reshaped to a linear array of pixels. shape (tuple): The shape of the image - + Outputs: numpy.ndarray: Indices of pixel objects """ @@ -125,7 +125,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): ''' Set up AVX vectors ''' # Vector to hold pixel values cdef __m128i v - # Vector to hold + # Vector to hold cdef __m128i mask # Vector for pixel indices cdef __m256i edges @@ -160,7 +160,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): cdef vector[np.uint16_t] temp temp.resize(16,0) cdef vector[np.uint32_t] output - + ''' Looping variables ''' cdef unsigned long p,i,j,n,r cdef unsigned long position = 0 @@ -182,9 +182,9 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): strides[j] = strides[j]*np.uint32(shape[ndim - i]) cdef Py_ssize_t last_stride = shape[ndim-1] positions = strides[0]*shape[0]//last_stride - + ''' - + Loop through all points and find the start and stopping edges of consecutive nonzero values. The way this works is that pixels are laid out linearly in memory according to the last dimension of the matrix. So, in a 2-d matrix @@ -199,7 +199,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): matrix is 3-d with dimensions (128x128x64), then there are 128x128 positions to evaluate. Each position is starting point for a new line of pixels along the last dimension. - + ''' for p in range(positions): @@ -217,15 +217,15 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): for i in range(ndim-1): output.push_back(coords[i]) output.push_back(0) - + ''' - + The following loop is designed for speed. It analyzes 15 pixels at a time, and if all 15 pixels have the same value then it quickly escapes to the next iteration of the loop. It stops looping when it gets less than 15 pixels from the end of the line of pixels so that it doesn't run into the next line of pixels. - + ''' r = 0 # manually register the pixel index, can be optimized with modulo outside the loop for n in range(0,last_stride-15,15): @@ -272,7 +272,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): else: on_obj=False output.push_back(temp[i]) - + # Advance the vector index i_vec = _mm256_add_epi16(i_vec,cnst15) @@ -304,7 +304,7 @@ cdef np.ndarray run_length_encode_16(unsigned char [:] image,tuple shape): # Turn the uint16 vector into a numpy.ndarray of appropriate size row_objects = np.asarray(output,dtype=np.uint32).reshape(-1,ndim+1) - + return row_objects @cython.boundscheck(False) @@ -325,7 +325,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): image (numpy.ndarray): An n-dimensional image reshaped to a linear array of pixels. shape (tuple): The shape of the image - + Outputs: numpy.ndarray: Indices of pixel objects """ @@ -333,7 +333,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): ''' Set up AVX vectors ''' # Vector to hold pixel values cdef __m128i v - # Vector to hold + # Vector to hold cdef __m128i mask # Vector for pixel indices cdef __m256i edges,edges_shf @@ -372,7 +372,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): cdef vector[np.uint32_t] temp temp.resize(16,0) cdef vector[np.uint32_t] output - + ''' Looping variables ''' cdef unsigned long long p,i,j,n,r cdef unsigned long long position = 0 @@ -394,9 +394,9 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): strides[j] = strides[j]*np.uint64(shape[ndim - i]) cdef Py_ssize_t last_stride = shape[ndim-1] positions = strides[0]*shape[0]//last_stride - + ''' - + Loop through all points and find the start and stopping edges of consecutive nonzero values. The way this works is that pixels are laid out linearly in memory according to the last dimension of the matrix. @@ -411,7 +411,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): the matrix is 3-d with dimensions (128x128x64), then there are 128x128 positions to evaluate. Each position is starting point for a new line of pixels along the last dimension. - + ''' for p in range(positions): @@ -429,15 +429,15 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): for i in range(ndim-1): output.push_back(coords[i]) output.push_back(0) - + ''' - + The following loop is designed for speed. It analyzes 15 pixels at a time, and if all 15 pixels have the same value then it quickly escapes to the next iteration of the loop. It stops loops when it gets less than 15 pixels from the end of the line of pixels so that it doesn't run into the next line of pixels. - + ''' r = 0 # manualy register the pixel index, can be optimized with modulo outside the loop for n in range(0,last_stride-15,15): @@ -491,7 +491,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): on_obj=False output.push_back(temp[i]) - + # Advance the vector index i_vec = _mm256_add_epi32(i_vec,cnst15) @@ -523,7 +523,7 @@ cdef np.ndarray run_length_encode_32(unsigned char [:] image,tuple shape): # Turn the uint16 vector into a numpy.ndarray of appropriate size row_objects = np.asarray(output,dtype=np.uint32).reshape(-1,ndim+1) - + return row_objects @cython.boundscheck(False) @@ -543,28 +543,28 @@ cdef rle_index(tuple image_shape, shape (tuple): The shape of the image rle_objects (numpy.ndarray): An n-dimensional image reshaped to a linear array of pixels. - + Outputs: numpy.ndarray: Indices of pixel objects """ - + # Get indices of lower dimension transitions cdef Py_ssize_t shape0 = rle_objects.shape[0] cdef Py_ssize_t shape1 = rle_objects.shape[1] cdef np.ndarray ld_change = np.argwhere(np.any((rle_objects[1:,:shape1-2] - rle_objects[:shape0-1,:shape1-2]) != 0,axis=1)) + 1 cdef Py_ssize_t ld_shape0 = ld_change.shape[0] cdef Py_ssize_t ld_shape1 = ld_change.shape[1] - - ld_change = np.vstack((np.array(0,dtype=np), + + ld_change = np.vstack((np.array(0,dtype=np.intp), ld_change, np.array(rle_objects.shape[0]))).astype(int) - + # Initialize the index matrix shape = 2 for i in range(len(image_shape)-1): shape *= (image_shape[i] + 2) cdef np.ndarray rle_indices = np.full(shape,np.iinfo(np.uint64).max,dtype=np.uint64) - + # Assign values to the index matrix cdef np.ndarray rle_sparse = np.zeros(ld_change.shape[0]-1,dtype=np.uint32) for i in range(rle_objects.shape[1]-3): @@ -572,11 +572,11 @@ cdef rle_index(tuple image_shape, rle_sparse = rle_sparse * (image_shape[i+1] + 2) rle_sparse += rle_objects[ld_change[:ld_change.shape[0]-1],shape1-3].squeeze() + 1 rle_sparse *= 2 - + # Set the indices rle_indices[rle_sparse] = ld_change[:ld_change.shape[0]-1].squeeze() rle_indices[rle_sparse + 1] = ld_change[1:].squeeze() - + return rle_sparse,rle_indices @cython.boundscheck(False) @@ -599,13 +599,13 @@ cdef void compare_objects(unsigned long [:] range1, labels (np.ndarray): 1d array of labels for each object """ cdef unsigned long long current_row = range1[0] - + cdef unsigned long long next_row = range2[0] cdef unsigned long long ind_start = rle_objects.shape[1] - 2 cdef unsigned long long ind_end = rle_objects.shape[1] - 1 - + # Loop through all row objects in the current and next rows while current_row < range1[1] and next_row < range2[1]: # if the current objects do not overlap, move to the next one @@ -615,7 +615,7 @@ cdef void compare_objects(unsigned long [:] range1, elif rle_objects[current_row,ind_start] > rle_objects[next_row,ind_end]: next_row += 1 continue - + # relabel the overlapping object in the next row if labels[labels[labels[next_row]]] < labels[labels[labels[current_row]]]: labels[labels[labels[current_row]]] = labels[labels[labels[next_row]]] @@ -624,7 +624,7 @@ cdef void compare_objects(unsigned long [:] range1, labels[labels[labels[next_row]]] = labels[labels[labels[current_row]]] labels[next_row] = labels[labels[current_row]] next_row += 1 - + # relabel additional objects in the next row while next_row < range2[1] and rle_objects[current_row,ind_end] >= rle_objects[next_row,ind_start]: if labels[labels[labels[next_row]]] < labels[labels[labels[current_row]]]: @@ -634,7 +634,7 @@ cdef void compare_objects(unsigned long [:] range1, labels[labels[labels[next_row]]] = labels[labels[labels[current_row]]] labels[next_row] = labels[labels[labels[current_row]]] next_row += 1 - + # relabel collisions while current_row+1 < range1[1] and rle_objects[current_row+1,ind_start] <= rle_objects[next_row-1,ind_end]: current_row += 1 @@ -655,16 +655,16 @@ cdef void reconcile_labels(uint_ind labels) nogil: label of the two objects that overlap. While objects are being relabeled, only the root label is generally changed, reducing the number of the memory calls by not relabeling all - rle objects when a new label is assigned. At the end of a + rle objects when a new label is assigned. At the end of a comparison in a given dimension, the labels need to be re-assigned based on their root label, where the root label is the label that is an index to itself. Args: labels (np.ndarray): 1d array of labels for each object - + """ - + cdef Py_ssize_t i cdef Py_ssize_t size = labels.shape[0] @@ -690,9 +690,9 @@ cdef np.ndarray generate_output_8(unsigned int [:,:] rle_objects, Outputs: np.ndarray: 8-bit labeled image - + """ - + # Initialize iteration counter cdef long long i @@ -705,9 +705,9 @@ cdef np.ndarray generate_output_8(unsigned int [:,:] rle_objects, cdef long long obj_start = rle_objects.shape[1] - 2 cdef long long obj_end = rle_objects.shape[1] - 1 cdef np.ndarray start_ind = np.zeros(rle_objects.shape[0],dtype=np.uint64) - + for i in range(ndims): - start_ind += rle_objects[:,i] + start_ind += rle_objects[:,i] start_ind *= image_shape[i+1] start_ind += rle_objects[:,obj_start] @@ -716,7 +716,7 @@ cdef np.ndarray generate_output_8(unsigned int [:,:] rle_objects, fill_n(&linear_image[start_ind_memview[i]], rle_objects[i,obj_end] - rle_objects[i,obj_start], labels[i]) - + return label_image @cython.boundscheck(False) @@ -737,9 +737,9 @@ cdef np.ndarray generate_output_16(unsigned int [:,:] rle_objects, Outputs: np.ndarray: 16-bit labeled image - + """ - + # Initialize iteration counter cdef long i @@ -752,9 +752,9 @@ cdef np.ndarray generate_output_16(unsigned int [:,:] rle_objects, cdef long long obj_start = rle_objects.shape[1] - 2 cdef long long obj_end = rle_objects.shape[1] - 1 cdef np.ndarray start_ind = np.zeros(rle_objects.shape[0],dtype=np.uint64) - + for i in range(ndims): - start_ind += rle_objects[:,i] + start_ind += rle_objects[:,i] start_ind *= image_shape[i+1] start_ind += rle_objects[:,obj_start] @@ -763,7 +763,7 @@ cdef np.ndarray generate_output_16(unsigned int [:,:] rle_objects, fill_n(&linear_image[start_ind_memview[i]], rle_objects[i,obj_end] - rle_objects[i,obj_start], labels[i]) - + return label_image @cython.boundscheck(False) @@ -784,9 +784,9 @@ cdef np.ndarray generate_output_32(unsigned int [:,:] rle_objects, Outputs: np.ndarray: 32-bit labeled image - + """ - + # Initialize iteration counter cdef long long i @@ -799,9 +799,9 @@ cdef np.ndarray generate_output_32(unsigned int [:,:] rle_objects, cdef long long obj_start = rle_objects.shape[1] - 2 cdef long long obj_end = rle_objects.shape[1] - 1 cdef np.ndarray start_ind = np.zeros(rle_objects.shape[0],dtype=np.uint64) - + for i in range(ndims): - start_ind += rle_objects[:,i] + start_ind += rle_objects[:,i] start_ind *= image_shape[i+1] start_ind += rle_objects[:,obj_start] @@ -810,7 +810,7 @@ cdef np.ndarray generate_output_32(unsigned int [:,:] rle_objects, fill_n(&linear_image[start_ind_memview[i]], rle_objects[i,obj_end] - rle_objects[i,obj_start], labels[i]) - + return label_image @cython.boundscheck(False) @@ -830,7 +830,7 @@ cdef unsigned int human_labels(unsigned long [:] labels) nogil: Outputs: np.uint32: Number of labels - + """ cdef Py_ssize_t size = labels.shape[0] cdef unsigned int num = 0 @@ -842,7 +842,7 @@ cdef unsigned int human_labels(unsigned long [:] labels) nogil: labels[i] = num continue labels[i] = labels[labels[i]] - + return num @cython.boundscheck(False) @@ -860,7 +860,7 @@ cdef np.ndarray label(unsigned char [:] image, rle_objects = run_length_encode_16(image,shape) else: rle_objects = run_length_encode_32(image,shape) - + # Get evaluation coordinates ndims = rle_objects.shape[1] - 2 if ndims == 1: @@ -873,7 +873,7 @@ cdef np.ndarray label(unsigned char [:] image, ind_d = ind_d[1] offsets = np.argwhere(ind_mat>0) - 1 offsets = offsets[np.argwhere(np.sum(np.absolute(offsets),axis=1)<=connectivity).squeeze(),:] - + # Adjust pixel coordinates to account for connectivity rle_objects_less_one = rle_objects.copy() rle_objects_less_one[...,rle_objects.shape[1]-1] -= 1 @@ -884,27 +884,27 @@ cdef np.ndarray label(unsigned char [:] image, rle_objects_mats.append(rle_objects_less_one) else: rle_objects_mats.append(rle_objects) - + # Get indices of higher coordinate changes rle_sparse,rle_indices = rle_index(shape,rle_objects) cdef unsigned long [:] rle_indices_memview = rle_indices cdef unsigned int [:] rle_sparse_memview = rle_sparse num_points = rle_sparse.shape[0] - 1 - + # Initalize the output cdef unsigned long[:] labels = np.arange(rle_objects.shape[0],dtype=np.uint64) - + # null value cdef unsigned long null_val = np.iinfo(np.uint64).max cdef unsigned long [:] offset_index,current_index cdef unsigned int [:,:] rle_objects_mat - cdef unsigned int [:] rle_sparse_offset_memview + cdef unsigned int [:] rle_sparse_offset_memview # Loop over the dimensions compare_time = 0 reconcile_time = 0 for d in range(offsets.shape[0]): - + rle_sparse_offset = rle_sparse.copy() o = 0 for i in range(offsets.shape[1]-1): @@ -914,7 +914,7 @@ cdef np.ndarray label(unsigned char [:] image, rle_sparse_offset_memview = rle_sparse_offset rle_objects_mat = rle_objects_mats[d] - + # Loop over points for index in range(num_points): offset_index = rle_indices_memview[rle_sparse_offset_memview[index]:rle_sparse_offset_memview[index+1]] @@ -922,18 +922,18 @@ cdef np.ndarray label(unsigned char [:] image, if offset_index[0] == null_val: continue - + compare_objects(current_index, offset_index, rle_objects_mat, labels) - + # Reconcile object labels after each offset is analyzed reconcile_labels(labels) - + # Make labels for humans num_objects = human_labels(labels) - + # Generate the output with smallest data type if num_objects < 2**8-1: label_image = generate_output_8(rle_objects,labels,shape) @@ -941,14 +941,14 @@ cdef np.ndarray label(unsigned char [:] image, label_image = generate_output_16(rle_objects,labels,shape) else: label_image = generate_output_32(rle_objects,labels,shape) - + return label_image def label_nd(image,connectivity): if connectivity == None: connectivity = image.ndim - + # Error checking assert connectivity<=image.ndim,\ "connectivity must be less than or equal to the number of image dimensions" - return label(image.reshape(-1),image.shape,connectivity) \ No newline at end of file + return label(image.reshape(-1),image.shape,connectivity) diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs index 6f9409782..567315d1e 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/lib.rs @@ -70,4 +70,4 @@ mod tests { polygon_set.digest(); assert_eq!(polygon_set.len(), count, "wrong number of polygons"); } -} \ No newline at end of file +} diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt index 17958cbd9..ee27563d6 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/requirements.txt @@ -3,4 +3,3 @@ preadator==0.2.0 numpy==1.21.4 bfio[all]==2.1.9 filepattern==1.4.7 - diff --git a/transforms/images/ftl-label-tool/tests/__init__.py b/transforms/images/ftl-label-tool/tests/__init__.py new file mode 100644 index 000000000..ec22f31d7 --- /dev/null +++ b/transforms/images/ftl-label-tool/tests/__init__.py @@ -0,0 +1 @@ +"""FTL Label Tool.""" diff --git a/transforms/images/ftl-label-tool/tests/conftest.py b/transforms/images/ftl-label-tool/tests/conftest.py new file mode 100644 index 000000000..93387cf70 --- /dev/null +++ b/transforms/images/ftl-label-tool/tests/conftest.py @@ -0,0 +1,80 @@ +"""FTL Label Tool.""" +import pathlib +import shutil +import tempfile +import zipfile + +import pytest +import requests +from bfio import BioReader +from bfio import BioWriter +from typing import Generator +from huggingface_hub import snapshot_download + + +@pytest.fixture() +def output_directory() -> Generator[pathlib.Path, None, None]: + """Temporary output directory.""" + out_dir = pathlib.Path(tempfile.mkdtemp(suffix="_out_dir")) + yield out_dir + shutil.rmtree(out_dir) + + +@pytest.fixture() +def download_ftl_dataset() -> Generator[pathlib.Path, None, None]: + """Download the FTL label test image dataset from Hugging Face Hub. + + Cleans up after the test. + """ + local_dir = snapshot_download( + repo_id="hamshkhawar/ftl_labe_test_images", + repo_type="dataset", + ) + yield pathlib.Path(local_dir) + shutil.rmtree(local_dir, ignore_errors=True) + + +@pytest.fixture() +def download_dsb2018_dataset() -> Generator[pathlib.Path, None, None]: + """Download DSB2018 test masks, convert to .ome.tif using bfio. + + Creates a temporary folder, downloads and extracts the dataset, converts .tif masks + to .ome.tif, and cleans up after the test. + """ + tmp_dir = pathlib.Path(tempfile.mkdtemp(suffix="_dsb2018")) + zip_path = tmp_dir / "dsb2018.zip" + + # Download DSB2018 zip + url = "https://github.com/stardist/stardist/releases/download/0.1.0/dsb2018.zip" + response = requests.get(url, timeout=120) + response.raise_for_status() + with zip_path.open("wb") as zip_file: + zip_file.write(response.content) + + # Extract the zip + with zipfile.ZipFile(zip_path, "r") as zip_ref: + zip_ref.extractall(tmp_dir) + + # Move masks to images_dir + images_dir = tmp_dir / "images" + masks_dir = tmp_dir / "dsb2018/test/masks" + images_dir.mkdir(exist_ok=True) + for mask_file in masks_dir.iterdir(): + shutil.move(str(mask_file), images_dir) + + # Convert .tif to .ome.tif using bfio + ome_dir = tmp_dir / "images_ome" + ome_dir.mkdir() + for image_file in images_dir.iterdir(): + if image_file.suffix.lower() != ".tif": + continue + out_file = ome_dir / f"{image_file.stem}.ome.tif" + with BioReader(image_file) as reader, BioWriter( + out_file, metadata=reader.metadata + ) as writer: + writer[:] = reader[:] + + yield ome_dir + + # Cleanup temporary folder + shutil.rmtree(tmp_dir, ignore_errors=True) diff --git a/transforms/images/ftl-label-tool/tests/test_main.py b/transforms/images/ftl-label-tool/tests/test_main.py new file mode 100644 index 000000000..a7211fb2c --- /dev/null +++ b/transforms/images/ftl-label-tool/tests/test_main.py @@ -0,0 +1,61 @@ +"""FTL Label Tool.""" +import pathlib +import pytest +from typer.testing import CliRunner +from polus.images.transforms.images.ftl_label.__main__ import app +from ftl_rust import PolygonSet + +runner = CliRunner() + + +def run_cli_test(dataset_dir: pathlib.Path, output_dir: pathlib.Path): + """Helper to run CLI and check outputs.""" + result = runner.invoke( + app, + [ + "--inpDir", + str(dataset_dir), + "--outDir", + str(output_dir), + "--connectivity", + "1", + ], + ) + assert result.exit_code == 0 + # Ensure output directory has results + assert len(list(output_dir.iterdir())) > 0 + + +def test_cli_ftl_dataset(download_ftl_dataset, output_directory): + """Run CLI on Hugging Face FTL dataset.""" + run_cli_test(download_ftl_dataset, output_directory) + + +def test_cli_dsb2018_dataset(download_dsb2018_dataset, output_directory): + """Run CLI on DSB2018 test masks dataset.""" + run_cli_test(download_dsb2018_dataset, output_directory) + + +@pytest.mark.skipif(PolygonSet is None, reason="ftl_rust not installed") +def test_bench_rust_dsb2018( + download_dsb2018_dataset: pathlib.Path, tmp_path: pathlib.Path +): + """Run PolygonSet read/write on one real .ome.tif from DSB2018 dataset.""" + # Pick the first .ome.tif file from the downloaded dataset + ome_files = list(download_dsb2018_dataset.glob("*.ome.tif")) + assert ome_files, "No .ome.tif files found in DSB2018 dataset" + input_file = ome_files[0] + + # Output file in temporary directory + output_file = tmp_path / f"{input_file.stem}_out.ome.tif" + + polygon_set = PolygonSet(connectivity=1) + + # Read from real dataset + polygon_set.read_from(input_file) + + # Write to temp directory + polygon_set.write_to(output_file) + + # Check that output file exists + assert output_file.exists() From 50ad52578abf33bac96c37387e4dfe7401c8dd9f Mon Sep 17 00:00:00 2001 From: Nazanin Donyapour Date: Fri, 13 Mar 2026 14:02:07 +0000 Subject: [PATCH 16/16] added filepattern and updated manifests --- transforms/images/ftl-label-tool/README.md | 47 ++++++++----------- transforms/images/ftl-label-tool/ftllabel.cwl | 8 ++++ transforms/images/ftl-label-tool/ict.yaml | 14 +++++- transforms/images/ftl-label-tool/plugin.json | 11 +++++ .../images/ftl-label-tool/run-plugin.sh | 2 + .../transforms/images/ftl_label/__main__.py | 18 +++++-- 6 files changed, 68 insertions(+), 32 deletions(-) diff --git a/transforms/images/ftl-label-tool/README.md b/transforms/images/ftl-label-tool/README.md index 7dc4135c2..7b5c356c2 100644 --- a/transforms/images/ftl-label-tool/README.md +++ b/transforms/images/ftl-label-tool/README.md @@ -29,13 +29,21 @@ On macOS ARM64 (Apple Silicon), all images are automatically routed through the To see detailed documentation for the `Rust` implementation you need to: -* Install [Rust](https://doc.rust-lang.org/stable/book/ch01-01-installation.html), +## Installation + +Install [Rust](https://doc.rust-lang.org/stable/book/ch01-01-installation.html), ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" ``` +## Rust documentation +To generate and view the full Rust API docs: + +```bash +cargo doc --open +``` + -## Installation #### From source (recommended for development) ```bash # 1. Clone the repo @@ -66,17 +74,21 @@ This plugin takes one input argument and one output argument: | Name | Description | I/O | Type | | ------------------------- | -------------------------------------------------------------------- | ------ | ---------- | | `--inpDir` | Input image collection to be processed by this plugin | Input | collection | +| `--filePattern` | File pattern used to select input images from the input collection | Input | string | | `--connectivity` | City block connectivity | Input | number | | `--binarizationThreshold` | For images containing probability values. Must be between 0 and 1.0. | Input | number | | `--outDir` | Output collection | Output | collection | ## Usage +# Run FTL label + ```bash python -m polus.images.transforms.images.ftl_label \ --inpDir /path/to/input \ - --outDir /path/to/output \ + --filePattern ".*.ome.tif" \ --connectivity 1 \ - --binarizationThreshold 0.5 + --binarizationThreshold 0.5 \ + --outDir /path/to/output ``` ## Docker @@ -88,24 +100,12 @@ To build the Docker image for the conversion plugin, run `./build-docker.sh`. ```bash basedir=$(basename ${PWD}) -docker run -v ${PWD}:/$basedir polusai/ftl-label-tool:0.3.12 \ +docker run -v ${PWD}:/$basedir polusai/ftl-label-tool:1.0.0-dev0 \ --inpDir /$basedir/images/ \ - --outDir /$basedir/output/ \ - --connectivity 1 \ - --binarizationThreshold 0.5 -``` - -## Example - -```bash - -# Run FTL label - -python -m polus.images.transforms.images.ftl_label \ - --inpDir /path/to/images/ \ - --outDir /path/to/output/ \ + --filePattern ".*.ome.tif" \ --connectivity 1 \ - --binarizationThreshold 0.5 + --binarizationThreshold 0.5 \ + --outDir /$basedir/output/ ``` @@ -130,13 +130,6 @@ SciKit's documentation has a good illustration for 2D: ``` -## Rust documentation -To generate and view the full Rust API docs: - -```bash -cargo doc --open -``` - ## To Do The following optimizations should be added to increase the speed or decrease the memory used by the plugin. diff --git a/transforms/images/ftl-label-tool/ftllabel.cwl b/transforms/images/ftl-label-tool/ftllabel.cwl index ca71210f1..2a4afe941 100644 --- a/transforms/images/ftl-label-tool/ftllabel.cwl +++ b/transforms/images/ftl-label-tool/ftllabel.cwl @@ -1,10 +1,18 @@ class: CommandLineTool cwlVersion: v1.2 inputs: + filePattern: + inputBinding: + prefix: --filePattern + type: string connectivity: inputBinding: prefix: --connectivity type: double + binarizationThreshold: + inputBinding: + prefix: --binarizationThreshold + type: double inpDir: inputBinding: prefix: --inpDir diff --git a/transforms/images/ftl-label-tool/ict.yaml b/transforms/images/ftl-label-tool/ict.yaml index 80ee333ba..933af59fa 100644 --- a/transforms/images/ftl-label-tool/ict.yaml +++ b/transforms/images/ftl-label-tool/ict.yaml @@ -13,11 +13,23 @@ inputs: name: inpDir required: true type: path +- description: A filepattern, used to select data to be converted + format: + - string + name: filePattern + required: false + type: string - description: City block connectivity format: - number name: connectivity required: true + type: integer +- description: Binarization threshold for probability images + format: + - number + name: binarizationThreshold + required: false type: number name: polusai/FTLLabel outputs: @@ -38,5 +50,5 @@ ui: - description: City block connectivity key: inputs.connectivity title: Connectivity - type: number + type: integer version: 1.0.0-dev0 diff --git a/transforms/images/ftl-label-tool/plugin.json b/transforms/images/ftl-label-tool/plugin.json index 4bf64b7cf..0731a10fb 100644 --- a/transforms/images/ftl-label-tool/plugin.json +++ b/transforms/images/ftl-label-tool/plugin.json @@ -21,6 +21,12 @@ "description": "Input image collection to be processed by this plugin", "required": true }, + { + "name": "filePattern", + "type": "string", + "description": "A filepattern, used to select data to be labelled", + "required": true + }, { "name": "connectivity", "type": "integer", @@ -47,6 +53,11 @@ "title": "Input collection", "description": "Input image collection to be processed by this plugin" }, + { + "key": "inputs.filePattern", + "title": "Filepattern", + "description": "A filepattern, used to select data for conversion" + }, { "key": "inputs.connectivity", "title": "Connectivity", diff --git a/transforms/images/ftl-label-tool/run-plugin.sh b/transforms/images/ftl-label-tool/run-plugin.sh index ccdd34e4f..e0d3a6b79 100644 --- a/transforms/images/ftl-label-tool/run-plugin.sh +++ b/transforms/images/ftl-label-tool/run-plugin.sh @@ -11,6 +11,7 @@ POLUS_IMG_EXT=".ome.tif" # Inputs inpDir=/data/input-2d +filePattern=".*" connectivity=1 binarizationThreshold=0.5 @@ -23,6 +24,7 @@ docker run --mount type=bind,source="${data_path}",target=/data/ \ --env POLUS_IMG_EXT="${POLUS_IMG_EXT}" \ polusai/ftl-label-tool:"${version}" \ --inpDir ${inpDir} \ + --filePattern ${filePattern} \ --connectivity ${connectivity} \ --binarizationThreshold ${binarizationThreshold} \ --outDir ${outDir} diff --git a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py index 86366b2b4..eda3551a8 100644 --- a/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py +++ b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/__main__.py @@ -6,6 +6,7 @@ import numpy import typer +import filepattern as fp from bfio import BioReader from bfio import BioWriter from ftl_rust import PolygonSet @@ -21,7 +22,7 @@ app = typer.Typer() POLUS_LOG = getattr(logging, os.environ.get("POLUS_LOG", "INFO")) -POLUS_EXT = os.environ.get("POLUS_EXT", ".ome.tif") +POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tif") _NUM_THREADS: int = int(os.environ.get("NUM_THREADS", os.cpu_count() or 1)) # Initialize the logger @@ -36,7 +37,7 @@ def get_output_name(filename: str) -> str: """Generate the output filename using the configured extension.""" name = filename.split(".ome")[0] - return f"{name}{POLUS_EXT}" + return f"{name}{POLUS_IMG_EXT}" def filter_by_size( @@ -139,6 +140,11 @@ def main( readable=True, resolve_path=True, ), + file_pattern: str = typer.Option( + ".+", + "--filePattern", + help="A filepattern defining the mask images to be labelled", + ), connectivity: int = typer.Option( ..., "--connectivity", @@ -172,11 +178,13 @@ def main( Args: inp_dir: Path to the input image collection. + file_pattern: Input file pattern. connectivity: City block connectivity (1 = face, 2 = edge, 3 = corner). binarization_threshold: Threshold for binarizing float probability images. out_dir: Path to the output image collection. """ logger.info(f"inpDir = {inp_dir}") + logger.info(f"filePattern = {file_pattern}") logger.info(f"connectivity = {connectivity}") logger.info(f"binarizationThreshold = {binarization_threshold:.2f}") logger.info(f"outDir = {out_dir}") @@ -186,9 +194,11 @@ def main( if inp_dir.joinpath("images").is_dir(): inp_dir = inp_dir / "images" - files = [f for f in inp_dir.iterdir() if f.is_file() and f.name.endswith(POLUS_EXT)] + fps = fp.FilePattern(inp_dir, file_pattern) + + files = [files[1][0] for files in fps()] if not files: - logger.warning(f"No {POLUS_EXT} files found in {inp_dir}") + logger.warning(f"No files found in {inp_dir}, check provided filepattern") raise typer.Exit(0) small_files, large_files = filter_by_size(files, 500)