Skip to content

Commit 089cf7b

Browse files
authored
Update Polus-rolling-ball-plugin (PolusAI#600)
* add pyproject.toml, update pakcages for cp313, pass unittest * update main.py and tests functions to pass pre-commit * add a bumpversion file * bump release * update gitignore * update Dockerfile, pyproject.toml
1 parent e0b8d59 commit 089cf7b

11 files changed

Lines changed: 231 additions & 143 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[bumpversion]
2+
current_version = 1.0.3
3+
commit = False
4+
tag = False
5+
6+
[bumpversion:file:VERSION]
7+
8+
[bumpversion:file:plugin.json]
Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
FROM polusai/bfio:2.1.9
2-
3-
COPY VERSION /
4-
5-
ARG EXEC_DIR="/opt/executables"
6-
ARG DATA_DIR="/data"
7-
8-
RUN mkdir -p ${EXEC_DIR} && mkdir -p ${DATA_DIR}/{input, output}
9-
10-
COPY src ${EXEC_DIR}/
1+
# Build from repo root (monorepo) or from this tool directory — both work.
2+
FROM polusai/bfio:2.5.0
3+
ENV EXEC_DIR="/opt/executables" POLUS_IMG_EXT=".ome.tif" POLUS_TAB_EXT=".csv" POLUS_LOG="INFO"
114
WORKDIR ${EXEC_DIR}
12-
13-
RUN pip3 install -r ${EXEC_DIR}/requirements.txt --no-cache-dir
14-
15-
ENTRYPOINT ["python3", "/opt/executables/main.py"]
5+
ENV TOOL_DIR="transforms/images/polus-rolling-ball-plugin"
6+
RUN mkdir -p image-tools
7+
COPY . ${EXEC_DIR}/image-tools
8+
RUN pip3 install -U pip setuptools wheel \
9+
&& python3 -c 'import sys; assert sys.version_info>=(3,11)' \
10+
&& R="${EXEC_DIR}/image-tools" && M="$R/$TOOL_DIR" \
11+
&& if [ -f "$M/pyproject.toml" ]; then pip3 install --no-cache-dir "$M"; \
12+
else pip3 install --no-cache-dir "$R"; fi
13+
ENTRYPOINT ["python3", "-m", "main"]
14+
CMD ["--help"]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.2
1+
1.0.3

transforms/images/polus-rolling-ball-plugin/plugin.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"name": "Rolling Ball",
3-
"version": "1.0.2",
3+
"version": "1.0.3",
44
"title": "Rolling Ball",
55
"description": "A WIPP plugin to perform background subtraction using the rolling-ball algorithm.",
66
"author": "Najib Ishaq (najib.ishaq@axleinfo.com)",
77
"institution": "National Center for Advancing Translational Sciences, National Institutes of Health",
88
"repository": "https://github.com/labshare/polus-plugins",
99
"website": "https://ncats.nih.gov/preclinical/core/informatics",
1010
"citation": "",
11-
"containerId": "polusai/rolling-ball-plugin:1.0.2",
11+
"containerId": "polusai/rolling-ball-plugin:1.0.3",
1212
"inputs": [
1313
{
1414
"name": "inputDir",
@@ -55,4 +55,4 @@
5555
"default": false
5656
}
5757
]
58-
}
58+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "polus-rolling-ball-plugin"
3+
version = "1.0.2"
4+
requires-python = ">=3.11"
5+
dependencies = [
6+
"bfio>=2.5.0",
7+
"scikit-image>=0.18.1",
8+
]
9+
10+
[build-system]
11+
requires = ["setuptools>=61.0", "wheel"]
12+
build-backend = "setuptools.build_meta"
13+
14+
[tool.setuptools.package-dir]
15+
"" = "src"
16+
17+
[tool.setuptools]
18+
py-modules = ["main", "rolling_ball"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Rolling-ball background subtraction plugin package."""

transforms/images/polus-rolling-ball-plugin/src/main.py

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,125 @@
1+
"""CLI entrypoint for rolling-ball background subtraction on image collections."""
2+
from __future__ import annotations
3+
14
import argparse
25
import logging
36
from multiprocessing import cpu_count
47
from pathlib import Path
58

69
from bfio.bfio import BioReader
710
from bfio.bfio import BioWriter
8-
911
from rolling_ball import rolling_ball
1012

1113
# Initialize the logger
12-
logging.basicConfig(format='%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s',
13-
datefmt='%d-%b-%y %H:%M:%S')
14+
logging.basicConfig(
15+
format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s",
16+
datefmt="%d-%b-%y %H:%M:%S",
17+
)
1418
logger = logging.getLogger("main")
1519
logger.setLevel(logging.INFO)
1620

1721

1822
def main(
19-
input_dir: Path,
20-
ball_radius: int,
21-
light_background: bool,
22-
output_dir: Path,
23+
input_dir: Path,
24+
ball_radius: int,
25+
light_background: bool,
26+
output_dir: Path,
2327
) -> None:
24-
""" Main execution function
28+
"""Process each image in ``input_dir`` and write results to ``output_dir``.
2529
2630
Args:
27-
input_dir: path to directory containing the input images.
28-
ball_radius: radius of ball to use for the rolling-ball algorithm.
29-
light_background: whether the image has a light or dark background.
30-
output_dir: path to directory where to store the output images.
31+
input_dir: Directory containing input images.
32+
ball_radius: Radius for the rolling-ball algorithm.
33+
light_background: Whether the image has a light or dark background.
34+
output_dir: Directory for output images.
3135
"""
32-
33-
for in_path in input_dir.iterdir():
34-
in_path = Path(in_path)
36+
for path in input_dir.iterdir():
37+
in_path = Path(path)
3538
out_path = Path(output_dir).joinpath(in_path.name)
3639

3740
# Load the input image
3841
with BioReader(in_path) as reader:
39-
logger.info(f'Working on {in_path.name} with shape {reader.shape}')
42+
logger.info(f"Working on {in_path.name} with shape {reader.shape}")
4043

4144
# Initialize the output image
42-
with BioWriter(out_path, metadata=reader.metadata, max_workers=cpu_count()) as writer:
45+
with BioWriter(
46+
out_path,
47+
metadata=reader.metadata,
48+
max_workers=cpu_count(),
49+
) as writer:
4350
rolling_ball(
4451
reader=reader,
4552
writer=writer,
4653
ball_radius=ball_radius,
4754
light_background=light_background,
4855
)
49-
return
5056

5157

5258
if __name__ == "__main__":
53-
""" Argument parsing """
5459
logger.info("Parsing arguments...")
55-
parser = argparse.ArgumentParser(prog='main', description='A WIPP plugin to perform background subtraction using the rolling-ball algorithm.')
56-
60+
parser = argparse.ArgumentParser(
61+
prog="main",
62+
description=(
63+
"A WIPP plugin to perform background subtraction using the "
64+
"rolling-ball algorithm."
65+
),
66+
)
67+
5768
# Input arguments
5869
parser.add_argument(
59-
'--inputDir',
60-
dest='input_dir',
70+
"--inputDir",
71+
dest="input_dir",
6172
type=str,
62-
help='Input image collection to be processed by this plugin.',
73+
help="Input image collection to be processed by this plugin.",
6374
required=True,
6475
)
6576
parser.add_argument(
66-
'--ballRadius',
67-
dest='ball_radius',
77+
"--ballRadius",
78+
dest="ball_radius",
6879
type=str,
69-
default='25',
70-
help='Radius of the ball used to perform background subtraction.',
80+
default="25",
81+
help="Radius of the ball used to perform background subtraction.",
7182
required=False,
7283
)
7384
parser.add_argument(
74-
'--lightBackground',
75-
dest='light_background',
85+
"--lightBackground",
86+
dest="light_background",
7687
type=str,
77-
default='false',
78-
help='Whether the image has a light or dark background.',
88+
default="false",
89+
help="Whether the image has a light or dark background.",
7990
required=False,
8091
)
8192
# Output arguments
8293
parser.add_argument(
83-
'--outputDir',
84-
dest='output_dir',
94+
"--outputDir",
95+
dest="output_dir",
8596
type=str,
86-
help='Output collection',
97+
help="Output collection",
8798
required=True,
8899
)
89-
100+
90101
# Parse the arguments
91102
args = parser.parse_args()
92103

93104
_input_dir = Path(args.input_dir).resolve()
94-
if _input_dir.joinpath('images').is_dir():
105+
if _input_dir.joinpath("images").is_dir():
95106
# switch to images folder if present
96-
_input_dir = _input_dir.joinpath('images').resolve()
97-
logger.info(f'inputDir = {_input_dir}')
107+
_input_dir = _input_dir.joinpath("images").resolve()
108+
logger.info(f"inputDir = {_input_dir}")
98109

99110
_ball_radius = int(args.ball_radius)
100-
logger.info(f'ballRadius = {_ball_radius}')
111+
logger.info(f"ballRadius = {_ball_radius}")
101112

102113
_light_background = args.light_background
103-
if _light_background in {'true', 'false'}:
104-
_light_background = (_light_background == 'true')
114+
if _light_background in {"true", "false"}:
115+
_light_background = _light_background == "true"
105116
else:
106-
raise ValueError(f'lightBackground must be either \'true\' or \'false\'')
107-
logger.info(f'lightBackground = {_light_background}')
117+
msg = "lightBackground must be either 'true' or 'false'"
118+
raise ValueError(msg)
119+
logger.info(f"lightBackground = {_light_background}")
108120

109121
_output_dir = args.output_dir
110-
logger.info(f'outputDir = {_output_dir}')
122+
logger.info(f"outputDir = {_output_dir}")
111123

112124
main(
113125
input_dir=_input_dir,
Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import numpy
1+
"""Tiled rolling-ball background subtraction using scikit-image."""
2+
from __future__ import annotations
3+
4+
import numpy as np
25
from bfio.bfio import BioReader
36
from bfio.bfio import BioWriter
47
from skimage import restoration
@@ -8,43 +11,58 @@
811
TILE_SIZE = 1024
912

1013

11-
def _rolling_ball(tile, ball_radius: int, light_background: bool):
12-
""" Applies the rolling-ball algorithm to a single tile.
14+
def _rolling_ball(
15+
tile: np.ndarray,
16+
ball_radius: int,
17+
light_background: bool,
18+
) -> np.ndarray:
19+
"""Apply rolling-ball to a single tile.
1320
1421
Args:
1522
tile: A tile, usually from an ome.tif file.
16-
ball_radius: The radius of the ball to use for calculating the background.
23+
ball_radius: Radius of the ball for background estimation.
1724
light_background: Whether the image has a light background.
1825
1926
Returns:
20-
An image with its background subtracted away.
27+
Image with background subtracted.
2128
"""
2229
# Get the shape of the original image, so we can reshape the result at the end.
23-
shape = numpy.shape(tile)
30+
shape = np.shape(tile)
2431

2532
# squeeze the image into a 2-d array
26-
tile = numpy.squeeze(tile)
33+
tile = np.squeeze(tile)
2734

2835
# invert the image if it has a light background
2936
if light_background:
3037
tile = util.invert(tile)
3138

32-
# use the rolling ball algorithm to calculate the background and subtract it from the image.
39+
# rolling ball background, then subtract from the image
3340
background = restoration.rolling_ball(tile, radius=ball_radius)
3441
tile = tile - background
3542

36-
# if the image had a light backend, invert the result.
43+
# if the image had a light background, invert the result.
3744
result = util.invert(tile) if light_background else tile
3845

39-
result = numpy.reshape(result, shape)
40-
return result
46+
return np.reshape(result, shape)
47+
4148

49+
def _bounds(
50+
x: int,
51+
x_max: int,
52+
ball_radius: int,
53+
) -> tuple[int, int, int, int, int]:
54+
"""Compute tile and padding indices along one axis.
4255
43-
def _bounds(x, x_max, ball_radius):
44-
""" Calculates the indices for handling the edges of tiles.
56+
Each tile is padded with up to ``ball_radius`` pixels from the full image along
57+
the edges.
58+
59+
Args:
60+
x: Start index along the axis.
61+
x_max: Image extent along the axis.
62+
ball_radius: Ball radius used for padding.
4563
46-
We pad each tile with 'ball_radius' pixels from the full image along the
47-
top, bottom, left, and right edges of each tile.
64+
Returns:
65+
``row_max, pad_left, pad_right, tile_left, tile_right``
4866
"""
4967
row_max = min(x_max, x + TILE_SIZE)
5068
pad_left = max(0, x - ball_radius)
@@ -56,33 +74,39 @@ def _bounds(x, x_max, ball_radius):
5674

5775

5876
def rolling_ball(
59-
reader: BioReader,
60-
writer: BioWriter,
61-
ball_radius: int,
62-
light_background: bool,
63-
):
64-
""" Applies the rolling-ball algorithm from skimage to perform background subtraction.
77+
reader: BioReader,
78+
writer: BioWriter,
79+
ball_radius: int,
80+
light_background: bool,
81+
) -> None:
82+
"""Apply rolling-ball per Z slice, tiled in XY, and write to ``writer``.
6583
66-
This function processes the image in tiles and, therefore, scales to images of any size.
67-
It writes the resulting image to the given BioWriter object.
84+
Processes the image in tiles so it scales to large images.
6885
6986
Args:
70-
reader: BioReader object from which to read the image.
71-
writer: BioWriter object to which to write the image.
72-
ball_radius: The radius of the ball to use for calculating the background.
73-
This should be greater than the radii of relevant objects in the image.
74-
light_background: Whether the image has a light background.
75-
87+
reader: Source image reader.
88+
writer: Destination writer (metadata should match reader).
89+
ball_radius: Rolling-ball radius; should exceed object radii of interest.
90+
light_background: Whether the scene has a light background.
7691
"""
7792
for z in range(reader.Z):
78-
7993
for y in range(0, reader.Y, TILE_SIZE):
80-
y_max, pad_top, pad_bottom, tile_top, tile_bottom = _bounds(y, reader.Y, ball_radius)
94+
y_max, pad_top, pad_bottom, tile_top, tile_bottom = _bounds(
95+
y,
96+
reader.Y,
97+
ball_radius,
98+
)
8199

82100
for x in range(0, reader.X, TILE_SIZE):
83-
x_max, pad_left, pad_right, tile_left, tile_right = _bounds(x, reader.X, ball_radius)
101+
x_max, pad_left, pad_right, tile_left, tile_right = _bounds(
102+
x,
103+
reader.X,
104+
ball_radius,
105+
)
84106

85-
tile = reader[pad_top:pad_bottom, pad_left:pad_right, z:z + 1, 0, 0]
107+
tile = reader[pad_top:pad_bottom, pad_left:pad_right, z : z + 1, 0, 0]
86108
result = _rolling_ball(tile, ball_radius, light_background)
87-
writer[y:y_max, x:x_max, z:z + 1, 0, 0] = result[tile_top:tile_bottom, tile_left:tile_right]
88-
return
109+
writer[y:y_max, x:x_max, z : z + 1, 0, 0] = result[
110+
tile_top:tile_bottom,
111+
tile_left:tile_right,
112+
]

0 commit comments

Comments
 (0)