diff --git a/transforms/images/ftl-label-tool/README.md b/transforms/images/ftl-label-tool/README.md index 7b5c356c2..a8b53ebf6 100644 --- a/transforms/images/ftl-label-tool/README.md +++ b/transforms/images/ftl-label-tool/README.md @@ -31,13 +31,8 @@ To see detailed documentation for the `Rust` implementation you need to: ## 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: +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. ```bash cargo doc --open @@ -82,13 +77,23 @@ This plugin takes one input argument and one output argument: ## Usage # Run FTL label -```bash -python -m polus.images.transforms.images.ftl_label \ - --inpDir /path/to/input \ - --filePattern ".*.ome.tif" \ - --connectivity 1 \ - --binarizationThreshold 0.5 \ - --outDir /path/to/output +# Convert the *.tif files to *.ome.tif tiled tif format using bfio. +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.11 \ +--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 src/simple_tiled_tiff_viewer.py --inpDir images_ome/ --outDir output/ ``` ## Docker diff --git a/transforms/images/ftl-label-tool/ftllabel.cwl b/transforms/images/ftl-label-tool/ftllabel.cwl index 2a4afe941..13b4b4b78 100644 --- a/transforms/images/ftl-label-tool/ftllabel.cwl +++ b/transforms/images/ftl-label-tool/ftllabel.cwl @@ -28,7 +28,7 @@ outputs: type: Directory requirements: DockerRequirement: - dockerPull: polusai/ftl-label-tool:1.0.0-dev0 + dockerPull: labshare/polus-ftl-label-plugin:0.3.11 InitialWorkDirRequirement: listing: - entry: $(inputs.outDir) diff --git a/transforms/images/ftl-label-tool/ict.yaml b/transforms/images/ftl-label-tool/ict.yaml index 933af59fa..bbe4530ff 100644 --- a/transforms/images/ftl-label-tool/ict.yaml +++ b/transforms/images/ftl-label-tool/ict.yaml @@ -3,7 +3,7 @@ author: - Najib Ishaq - Hamdah Shafqat Abbasi contact: nick.schaub@nih.gov -container: polusai/ftl-label-tool:1.0.0-dev0 +container: labshare/polus-ftl-label-plugin:0.3.11 description: Label objects in a 2d or 3d binary image. entrypoint: python3 -m polus.images.transforms.images.ftl_label inputs: @@ -50,5 +50,5 @@ ui: - description: City block connectivity key: inputs.connectivity title: Connectivity - type: integer -version: 1.0.0-dev0 + type: number +version: 0.3.11 diff --git a/transforms/images/ftl-label-tool/run-plugin.sh b/transforms/images/ftl-label-tool/run-plugin.sh index e0d3a6b79..281e99370 100644 --- a/transforms/images/ftl-label-tool/run-plugin.sh +++ b/transforms/images/ftl-label-tool/run-plugin.sh @@ -26,5 +26,4 @@ docker run --mount type=bind,source="${data_path}",target=/data/ \ --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/ftl.pyx b/transforms/images/ftl-label-tool/src/polus/images/transforms/images/ftl_label/ftl.pyx index eb6fb2e97..b019a302d 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 @@ -555,7 +555,7 @@ cdef rle_index(tuple image_shape, 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.intp), + ld_change = np.vstack((np.array(0,dtype=np), ld_change, np.array(rle_objects.shape[0]))).astype(int) 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..a2ded6e89 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/.bumpversion.cfg @@ -0,0 +1,18 @@ +[bumpversion] +current_version = 0.3.11 +commit = False +tag = False + +[bumpversion:file:VERSION] + +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:plugin.json] + +[bumpversion:file:README.md] + +[bumpversion:file:ict.yaml] + +[bumpversion:file:ftllabel.cwl] diff --git a/transforms/images/polus-ftl-label-plugin/.gitignore b/transforms/images/polus-ftl-label-plugin/.gitignore new file mode 100644 index 000000000..92cf65da5 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/.gitignore @@ -0,0 +1,11 @@ +*.png +*.so +*.html +*.cpp +*.npy +build +dist +ftl_rust.egg-info +target +Cargo.lock +uv.lock diff --git a/transforms/images/polus-ftl-label-plugin/Cargo.toml b/transforms/images/polus-ftl-label-plugin/Cargo.toml new file mode 100644 index 000000000..3fd5d68d9 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/Cargo.toml @@ -0,0 +1,24 @@ +[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 diff --git a/transforms/images/polus-ftl-label-plugin/Dockerfile b/transforms/images/polus-ftl-label-plugin/Dockerfile new file mode 100644 index 000000000..eeb21611b --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/Dockerfile @@ -0,0 +1,19 @@ +# Build from repo root (monorepo) or from this tool directory — both work. +# setuptools_rust needs a Rust toolchain for this plugin. +FROM polusai/bfio:2.5.0 +RUN apt-get update && apt-get install -y --no-install-recommends curl build-essential \ + && curl -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable \ + && rm -rf /var/lib/apt/lists/* +ENV PATH="/root/.cargo/bin:${PATH}" +ENV EXEC_DIR="/opt/executables" POLUS_IMG_EXT=".ome.tif" POLUS_TAB_EXT=".csv" POLUS_LOG="INFO" +WORKDIR ${EXEC_DIR} +ENV TOOL_DIR="transforms/images/polus-ftl-label-plugin" +RUN mkdir -p image-tools +COPY . ${EXEC_DIR}/image-tools +RUN pip3 install -U pip setuptools wheel \ + && python3 -c 'import sys; assert sys.version_info>=(3,11)' \ + && R="${EXEC_DIR}/image-tools" && M="$R/$TOOL_DIR" \ + && if [ -f "$M/pyproject.toml" ]; then pip3 install --no-cache-dir "$M"; \ + else pip3 install --no-cache-dir "$R"; fi +ENTRYPOINT ["python3", "-m", "main"] +CMD ["--help"] diff --git a/transforms/images/polus-ftl-label-plugin/VERSION b/transforms/images/polus-ftl-label-plugin/VERSION new file mode 100644 index 000000000..208059121 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/VERSION @@ -0,0 +1 @@ +0.3.11 diff --git a/transforms/images/polus-ftl-label-plugin/build-docker.sh b/transforms/images/polus-ftl-label-plugin/build-docker.sh new file mode 100755 index 000000000..fc30a216e --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/build-docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +version=$( None: + """Create a PolygonSet for the given pixel connectivity. + + Args: + connectivity: Neighbor model; must be 1, 2, or 3. See README. + """ + if not (CONNECTIVITY_MIN <= connectivity <= CONNECTIVITY_MAX): + msg = ( + f"connectivity must be {CONNECTIVITY_MIN}, 2 or {CONNECTIVITY_MAX}. " + f"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) -> type[Any]: + """Minimal integer dtype for label values from object count.""" + 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": + """Read an .ome.tif, detect objects, and build polygons. + + 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 + logger.debug( + "added tile #%s (%s:%s, %s:%s, %s:%s)", + tile_count, + z, + z_max, + y, + y_max, + x, + x_max, + ) + denom = num_slices * num_cols * num_rows + pct = 100 * tile_count / denom + logger.info("Reading Progress %6.3f%%...", pct) + + 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": + """Write a labelled ome.tif using input metadata and chosen dtype. + + 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("writing %s with dtype %s...", outfile.name, 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( + "Wrote tile %s, (%s, %s:%s, %s:%s)", + tile_count, + z, + y, + y_max, + x, + x_max, + ) + denom = num_cols * num_rows * writer.Z + pct = 100 * tile_count / denom + logger.info("Writing Progress %6.3f%%...", pct) + return self diff --git a/transforms/images/polus-ftl-label-plugin/plugin.json b/transforms/images/polus-ftl-label-plugin/plugin.json new file mode 100644 index 000000000..1f23bdef1 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/plugin.json @@ -0,0 +1,45 @@ +{ + "name": "FTL Label", + "version": "0.3.11", + "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": "labshare/polus-ftl-label-plugin:0.3.11", + "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 + } + ], + "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" + } + ] +} diff --git a/transforms/images/polus-ftl-label-plugin/pyproject.toml b/transforms/images/polus-ftl-label-plugin/pyproject.toml new file mode 100644 index 000000000..826ee8e57 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "polus-ftl-label-plugin" +version = "0.3.11" +requires-python = ">=3.11" +dependencies = [ + "numpy>=1.26.4", + "bfio>=2.5.0", +] + +[build-system] +requires = ["setuptools>=41.0.0", "wheel", "setuptools_rust>=0.10.2"] +build-backend = "setuptools.build_meta" + +[tool.mypy] +python_version = "3.11" +explicit_package_bases = true +ignore_missing_imports = true diff --git a/transforms/images/polus-ftl-label-plugin/src/__init__.py b/transforms/images/polus-ftl-label-plugin/src/__init__.py new file mode 100644 index 000000000..90b1c35b9 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/__init__.py @@ -0,0 +1 @@ +"""FTL label plugin Python package (main entry and benchmarks).""" diff --git a/transforms/images/polus-ftl-label-plugin/src/bench_rust.py b/transforms/images/polus-ftl-label-plugin/src/bench_rust.py new file mode 100644 index 000000000..a3be9dcc3 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/bench_rust.py @@ -0,0 +1,34 @@ +"""Benchmark script for Rust-backed PolygonSet read/write.""" +import logging +import time +from pathlib import Path + +from ftl_rust import PolygonSet + +logger = logging.getLogger(__name__) + + +def bench_rust() -> None: + """Time PolygonSet read, digest, and write on a fixed test image.""" + 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() + logger.info("took %.3f seconds to read and digest...", end - start) + + found = len(polygon_set) + if count != found: + msg = f"found {found} objects instead of {count}." + raise ValueError(msg) + + polygon_set.write_to(outfile) + logger.info("took %.3f seconds to write...", time.time() - end) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + bench_rust() diff --git a/transforms/images/polus-ftl-label-plugin/src/main.py b/transforms/images/polus-ftl-label-plugin/src/main.py new file mode 100644 index 000000000..3c5167712 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/main.py @@ -0,0 +1,198 @@ +"""CLI for FTL label plugin: Cython path for small images, Rust for large.""" +import argparse +import logging +import os +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +import ftl +import numpy +from bfio import BioReader +from bfio import BioWriter +from ftl_rust import PolygonSet + +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: + """Strip .ome* suffix and apply POLUS_EXT for output filename.""" + name = filename.split(".ome", 1)[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: + 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) -> bool: + """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. + """ + max_workers = max(1, (os.cpu_count() or 4) // 2) + with BioReader(input_path, max_workers=max_workers) as reader, BioWriter( + output_path, + max_workers=max_workers, + metadata=reader.metadata, + ) as writer: + # Load an image and convert to binary + image = numpy.squeeze(reader[..., 0, 0]) + + if not numpy.any(image): + writer.dtype = numpy.uint8 + writer[:] = numpy.zeros_like(image, dtype=numpy.uint8) + return True + + image = image > 0 + if connectivity > image.ndim: + logger.warning( + "%s: Connectivity is not less than or equal to the number of " + "image dimensions, skipping this image. connectivity=%s, ndim=%s", + input_path.name, + connectivity, + image.ndim, + ) + return False + + # 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=str, + required=True, + help=( + "City block connectivity, must be less than or equal to the number " + "of dimensions" + ), + ) + + 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}") + + _input_dir = Path(args.inpDir).resolve() + if not _input_dir.exists(): + msg = f"{_input_dir} does not exist." + raise FileNotFoundError(msg) + 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() + if not _output_dir.exists(): + msg = f"{_output_dir} does not exist." + raise FileNotFoundError(msg) + logger.info(f"outDir = {_output_dir}") + + # 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("processing %s images in total...", len(_files)) + logger.info("processing %s small images with cython...", len(_small_files)) + logger.info("processing %s large images with rust", len(_large_files)) + + if _small_files: + max_workers = max(1, (os.cpu_count() or 4) // 2) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [ + executor.submit( + label_cython, + _infile, + _output_dir.joinpath(get_output_name(_infile.name)), + _connectivity, + ) + for _infile in _small_files + ] + for f in futures: + f.result() + + 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) diff --git a/transforms/images/polus-ftl-label-plugin/src/requirements.txt b/transforms/images/polus-ftl-label-plugin/src/requirements.txt new file mode 100644 index 000000000..41e8ac2bd --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/requirements.txt @@ -0,0 +1,4 @@ +Cython>=3.0.0 +numpy>=2.4.3 +bfio[all]>=2.5.0 +filepattern>=2.2.1 diff --git a/transforms/images/polus-ftl-label-plugin/src/rust_setup.py b/transforms/images/polus-ftl-label-plugin/src/rust_setup.py new file mode 100644 index 000000000..dab9695a1 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/rust_setup.py @@ -0,0 +1,12 @@ +"""Setuptools entry for building the ftl_rust Rust extension.""" +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/setup.py b/transforms/images/polus-ftl-label-plugin/src/setup.py new file mode 100644 index 000000000..d6865a6ec --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/setup.py @@ -0,0 +1,16 @@ +"""Build configuration for the ftl Cython extension.""" +import os + +import numpy +from Cython.Build import cythonize +from Cython.Compiler import Options +from setuptools import setup + +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()], +) diff --git a/transforms/images/polus-ftl-label-plugin/src/simple_tiled_tiff_viewer.py b/transforms/images/polus-ftl-label-plugin/src/simple_tiled_tiff_viewer.py new file mode 100644 index 000000000..d92561a36 --- /dev/null +++ b/transforms/images/polus-ftl-label-plugin/src/simple_tiled_tiff_viewer.py @@ -0,0 +1,65 @@ +"""Simple tiled TIFF viewer for comparing input and FTL label outputs.""" +import argparse +import logging +from pathlib import Path + +import matplotlib.pyplot as plt +from bfio import BioReader + +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() + input_dir = Path(args.inpDir) + logger.info(f"inpDir = {input_dir}") + output_dir = Path(args.outDir) + logger.info(f"outDir = {output_dir}") + + # Get all file names in inpDir image collection + files = [ + f for f in input_dir.iterdir() if f.is_file() and f.name.endswith(".tif") + ] + + for file in files: + # Set up the BioReader + with BioReader(input_dir / file.name) as br_in: + img_in = br_in[:] + + with BioReader(output_dir / 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.close()