Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions transforms/images/ftl-label-tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion transforms/images/ftl-label-tool/ftllabel.cwl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions transforms/images/ftl-label-tool/ict.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
1 change: 0 additions & 1 deletion transforms/images/ftl-label-tool/run-plugin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,4 @@ docker run --mount type=bind,source="${data_path}",target=/data/ \
--inpDir ${inpDir} \
--filePattern ${filePattern} \
--connectivity ${connectivity} \
--binarizationThreshold ${binarizationThreshold} \
--outDir ${outDir}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 18 additions & 0 deletions transforms/images/polus-ftl-label-plugin/.bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -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]
11 changes: 11 additions & 0 deletions transforms/images/polus-ftl-label-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
*.png
*.so
*.html
*.cpp
*.npy
build
dist
ftl_rust.egg-info
target
Cargo.lock
uv.lock
24 changes: 24 additions & 0 deletions transforms/images/polus-ftl-label-plugin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
authors = ["Najib Ishaq <najib.ishaq@axleinfo.com>"]
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
19 changes: 19 additions & 0 deletions transforms/images/polus-ftl-label-plugin/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions transforms/images/polus-ftl-label-plugin/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.3.11
4 changes: 4 additions & 0 deletions transforms/images/polus-ftl-label-plugin/build-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

version=$(<VERSION)
docker build . -t labshare/polus-ftl-label-plugin:"${version}"
189 changes: 189 additions & 0 deletions transforms/images/polus-ftl-label-plugin/ftl_rust/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""Python bindings and high-level API for the FTL Rust polygon backend."""
import logging
from multiprocessing import cpu_count
from pathlib import Path
from typing import Any

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"]

CONNECTIVITY_MIN = 1
CONNECTIVITY_MAX = 3
NDIM_2D = 2

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)


class PolygonSet:
"""Build and label polygons via the Rust FTL implementation."""

def __init__(self, connectivity: int) -> 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
Loading
Loading