From 18597dc01c5db09cdaecf8909eed9b4b0387a618 Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 8 May 2024 06:56:41 -0400 Subject: [PATCH 01/26] feat: added plate extraction tool --- .../.bumpversion.cfg | 31 +++ .../rt-cetsa-plate-extraction-tool/Dockerfile | 20 ++ .../rt-cetsa-plate-extraction-tool/README.md | 24 ++ .../rt-cetsa-plate-extraction-tool/VERSION | 1 + .../rt-cetsa-plate-extraction-tool/ict.yml | 51 +++++ .../plugin.json | 52 +++++ .../pyproject.toml | 81 +++++++ .../run-plugin.sh | 20 ++ .../rt_cetsa_plate_extraction/VERSION | 1 + .../rt_cetsa_plate_extraction/__init__.py | 37 ++++ .../rt_cetsa_plate_extraction/__main__.py | 87 ++++++++ .../rt_cetsa_plate_extraction/core.py | 206 ++++++++++++++++++ 12 files changed, 611 insertions(+) create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg create mode 100755 segmentation/rt-cetsa-plate-extraction-tool/Dockerfile create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/README.md create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/VERSION create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/ict.yml create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/plugin.json create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/VERSION create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py diff --git a/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg new file mode 100644 index 000000000..510a7bdfd --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg @@ -0,0 +1,31 @@ +[bumpversion] +current_version = 0.1.0 +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:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:plugin.json] + +[bumpversion:file:VERSION] + +[bumpversion:file:README.md] + +[bumpversion:file:src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py] + +[bumpversion:file:ict.yml] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile b/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile new file mode 100755 index 000000000..111df1dc8 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile @@ -0,0 +1,20 @@ +FROM polusai/bfio:2.3.6 + +# environment variables defined in polusai/bfio +ENV EXEC_DIR="/opt/executables" +ENV POLUS_IMG_EXT=".ome.tif" +ENV POLUS_TAB_EXT=".arrow" +ENV POLUS_LOG="INFO" + +# Work directory defined in the base container +WORKDIR ${EXEC_DIR} + +COPY pyproject.toml ${EXEC_DIR} +COPY VERSION ${EXEC_DIR} +COPY README.md ${EXEC_DIR} +COPY src ${EXEC_DIR}/src + +RUN pip3 install ${EXEC_DIR} --no-cache-dir + +ENTRYPOINT ["python3", "-m", "polus.images.segmentation.rt_cetsa_plate_extraction"] +CMD ["--help"] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/README.md b/segmentation/rt-cetsa-plate-extraction-tool/README.md new file mode 100644 index 000000000..3d85f4503 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/README.md @@ -0,0 +1,24 @@ +# RT_CETSA Moltprot Regression (v0.1.0) + +This WIPP plugin runs regression analysis for the RT-CETSA pipeline. +The input csv file should be sorted by `Temperature` column. + +## Building + +To build the Docker image for the conversion plugin, run +`./build-docker.sh`. + +## Install WIPP Plugin + +If WIPP is running, navigate to the plugins page and add a new plugin. Paste the contents of `plugin.json` into the pop-up window and submit. + +## Options + +This plugin takes eight input argument and one output argument: + +| Name | Description | I/O | Type | +|-------------|----------------------------------------------------|--------|-------------| +| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | +| `--pattern` | Pattern to parse input files | Input | string | +| `--outDir` | Output file | Output | genericData | +| `--preview` | Generate JSON file with outputs | Output | JSON | diff --git a/segmentation/rt-cetsa-plate-extraction-tool/VERSION b/segmentation/rt-cetsa-plate-extraction-tool/VERSION new file mode 100644 index 000000000..6e8bf73aa --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml new file mode 100644 index 000000000..d92148623 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml @@ -0,0 +1,51 @@ +author: +- Nick Schaub +- Najib Ishaq +contact: nick.schaub@nih.gov +container: polusai/rt-cetsa-plate-extraction-tool:0.1.0 +description: Rotate and crop images of plates from RT-CETSA; then label the wells. +entrypoint: python3 -m polus.images.segmentation.rt_cetsa_plate_extraction +inputs: +- description: Input data collection to be processed by this tool + format: + - inpDir + name: inpDir + required: true + type: path +- description: Filepattern to parse input files + format: + - pattern + name: pattern + required: false + type: string +- description: Generate an output preview. + format: + - preview + name: preview + required: false + type: boolean +name: polusai/RTCETSAPlateExtraction +outputs: +- description: Output collection + format: + - outDir + name: outDir + required: true + type: path +repository: https://github.com/PolusAI/tabular-tools +specVersion: 1.0.0 +title: RT-CETSA Plate Extraction +ui: +- description: Input data collection + key: inputs.inpDir + title: Input data collection + type: path +- description: Filepattern to parse input files + key: inputs.pattern + title: pattern + type: text +- description: Generate an output preview. + key: inputs.preview + title: Preview example output of this plugin + type: checkbox +version: 0.1.0 diff --git a/segmentation/rt-cetsa-plate-extraction-tool/plugin.json b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json new file mode 100644 index 000000000..66dda5111 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json @@ -0,0 +1,52 @@ +{ + "name": "RT-CETSA Plate Extraction", + "version": "0.1.0", + "title": "RT-CETSA Plate Extraction", + "description": "Run regression analysis for the RT-CETSA pipeline.", + "author": "Nicholas Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@nih.gov)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/PolusAI/image-tools", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/rt-cetsa-plate-extraction-tool:0.1.0", + "baseCommand": [ + "python3", + "-m", + "polus.images.segmentation.rt_cetsa_plate_extraction" + ], + "inputs": [ + { + "name": "inpDir", + "type": "genericData", + "description": "Input data collection to be processed by this tool", + "required": true + }, + { + "name": "pattern", + "type": "string", + "description": "Pattern to parse input files", + "default": ".+", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "genericData", + "description": "Output data collection" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input collection", + "description": "Input data collection to be processed by this plugin" + }, + { + "key": "inputs.pattern", + "title": "pattern", + "description": "Pattern to parse input files", + "default": ".+" + } + ] +} diff --git a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml new file mode 100644 index 000000000..64e3f0dbb --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml @@ -0,0 +1,81 @@ +[tool.poetry] +name = "polus_images_segmentation_rt_cetsa_plate_extraction" +version = "0.1.0" +description = "Rotate and crop images of plates from RT-CETSA; then label the wells." +authors = [ + "Nick Schaub ", + "Najib Ishaq ", +] +readme = "README.md" +packages = [{include = "polus", from = "src"}] + +[tool.poetry.dependencies] +python = ">=3.9,<3.12" +typer = "^0.7.0" +filepattern = "^2.0.5" +numpy = "^1.26.4" +scikit-image = "0.22.0" +tifffile = "^2024.5.3" +bfio = "^2.3.6" + +[tool.poetry.group.dev.dependencies] +bump2version = "^1.0.1" +pre-commit = "^3.1.0" +pytest = "^7.2.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +extend = "../../ruff.toml" +extend-ignore = [ + "RET505", # Unnecessary `else` after `return` statement + "E501", # Line too long + "ANN001", # Missing type annotation + "D102", # Missing docstring in public method + "ANN201", # Missing return type annotation + "N806", # Variable in function should be lowercase + "D205", # 1 blank line required between summary line and description + "N803", # Argument name should be lowercase + "PLR0913", # Too many arguments + "D415", # First line should end with a period, question mark, or exclamation point + "PLR2004", # Magic value used in comparison + "B006", # Do not use mutable default arguments + "D107", # Missing docstring + "D101", # Missing docstring + "E731", # Do not assign a lambda expression, use a def + "E402", # Module level import not at top of file + "PTH123", # `open()` should be replaced with `Path.open()` + "PTH118", # `os.path.join()` should be replaced with `/` operator + "PTH100", # `os.path.abspath()` should be replaced with `Path.resolve()` + "PLR0915", # Too many statements + "PLR0912", # Too many branches + "C901", # Function is too complex + "T201", # `print` used + "E722", # Do not use bare 'except' + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + "ANN202", # Missing return type annotation for private function + "ARG002", # Unused method argument + "N802", # Function name should be lowercase + "PTH103", # `os.makedirs()` should be replaced with `Path.mkdir(parents=True)` + "ANN003", # Missing type annotation for `**kwargs` + "B007", # Loop control variable not used within the loop body + "ANN204", # Missing return type annotation for magic method + "D417", # Missing argument descriptions in the docstring + "ANN205", # Missing return type annotation for static method + "PLR5501", # Use `elif` instead of `else` following `if` condition to avoid unnecessary indentation + "EM102", # Exception must not use an f-string literal + "D414", # Section has no content + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "A001", # Variable `input` is shadowing a Python builtin + "A002", # Argument `input` is shadowing a Python builtin + "E741", # Ambiguous variable name: `l` + "PTH120", # `os.path.dirname()` should be replaced by `Path.parent` + "N816", # Variable `cfFilename` in global scope should not be mixedCase + "PTH109", # `os.getcwd()` should be replaced by `Path.cwd()` + "ARG001", # Unused function argument + "S101", # Use of assert detected + "D103", # Missing docstring in public function + "D100", # Missing docstring in public module +] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh b/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh new file mode 100644 index 000000000..97d700584 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh @@ -0,0 +1,20 @@ +#!/bin/bash +version=$( tuple[numpy.ndarray, numpy.ndarray]: + """Extract plate from RT_CETSA image. + + Args: + file_path: Path to the image file. + + Returns: + Tuple containing the plate image and the mask. + """ + image = tifffile.imread(file_path) + params = core.get_plate_params(image) + rotated_image = rotate(image, params.rotate, preserve_range=True)[ + params.bbox[0] : params.bbox[1], + params.bbox[2] : params.bbox[3], + ].astype(image.dtype) + + mask = numpy.zeros_like(rotated_image, dtype=numpy.uint16) + for i, (x, y) in enumerate(itertools.product(params.X, params.Y), start=1): + rr, cc = disk((y, x), params.radii) + mask[rr, cc] = i + + return rotated_image, mask diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py new file mode 100644 index 000000000..3fe78a420 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -0,0 +1,87 @@ +"""CLI for rt-cetsa-plate-extraction-tool.""" + +import json +import logging +import os +import pathlib + +import bfio +import filepattern +import typer +from polus.images.segmentation.rt_cetsa_plate_extraction import extract_plate + +# Initialize the logger +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger("polus.images.segmentation.rt_cetsa_plate_extraction") +logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) + +POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") + +app = typer.Typer() + + +@app.command() +def main( + inp_dir: pathlib.Path = typer.Option( + ..., + help="Input directory containing the data files.", + exists=True, + dir_okay=True, + readable=True, + resolve_path=True, + ), + pattern: str = typer.Option( + ".+", + help="Pattern to match the files in the input directory.", + ), + preview: bool = typer.Option( + False, + help="Preview the files that will be processed.", + ), + out_dir: pathlib.Path = typer.Option( + ..., + help="Output directory to save the results.", + exists=True, + dir_okay=True, + writable=True, + resolve_path=True, + ), +) -> None: + """CLI for rt-cetsa-plate-extraction-tool.""" + logger.info("Starting the CLI for rt-cetsa-plate-extraction-tool.") + + logger.info(f"Input directory: {inp_dir}") + logger.info(f"File Pattern: {pattern}") + logger.info(f"Output directory: {out_dir}") + + fp = filepattern.FilePattern(inp_dir, pattern) + inp_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + + if preview: + out_json = { + "images": [ + out_dir / "images" / f"{f.stem}{POLUS_IMG_EXT}" for f in inp_files + ], + "masks": [ + out_dir / "masks" / f"{f.stem}{POLUS_IMG_EXT}" for f in inp_files + ], + } + with (out_dir / "preview.json").open("w") as f: + json.dump(out_json, f, indent=2) + return + + for f in inp_files: # type: ignore[assignment] + logger.info(f"Processing file: {f}") + image, mask = extract_plate(f) + out_name = f.stem + POLUS_IMG_EXT # type: ignore[attr-defined] + with bfio.BioWriter(out_dir / "images" / out_name) as writer: + writer[:] = image + with bfio.BioWriter(out_dir / "masks" / out_name) as writer: + writer[:] = mask + + +if __name__ == "__main__": + app() diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py new file mode 100644 index 000000000..344491790 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py @@ -0,0 +1,206 @@ +import string +from enum import Enum + +import numpy as np +from pydantic import BaseModel +from scipy import ndimage as ndi +from skimage.filters import threshold_otsu +from skimage.transform import rotate + + +class PlateSize(Enum): + SIZE_6 = 6 + SIZE_12 = 12 + SIZE_24 = 24 + SIZE_48 = 48 + SIZE_96 = 96 + SIZE_384 = 384 + SIZE_1536 = 1536 + + +PLATE_DIMS = { + PlateSize.SIZE_6: (2, 3), + PlateSize.SIZE_12: (3, 4), + PlateSize.SIZE_24: (4, 6), + PlateSize.SIZE_48: (6, 8), + PlateSize.SIZE_96: (9, 12), + PlateSize.SIZE_384: (16, 24), + PlateSize.SIZE_1536: (32, 48), +} + +ROTATION = np.vstack( + [ + -np.sin(np.arange(0, np.pi, np.pi / 180)), + np.cos(np.arange(0, np.pi, np.pi / 180)), + ], +) + + +class PlateParams(BaseModel): + rotate: int + """Counterclockwise rotation of image in degrees.""" + + bbox: tuple[int, int, int, int] + """Bounding box of plate after rotation, [ymin,ymax,xmin,xmax].""" + + size: PlateSize + """The plate size, also determines layout.""" + + radii: int + """Well radius.""" + + X: list[int] + """The the x axis points for wells.""" + + Y: list[int] + """The the y axis points for wells.""" + + +def get_wells(image: np.ndarray) -> tuple[list[float], list[float], list[float], int]: + """Get well locations and radii. + + Since RT-CETSA are generally high signal to noise, no need for anything fance + to detect wells. Simple Otsu threshold to segment the well, image labeling, + and estimation of radius based off of area (assuming the area is a circle). + + The input image is a binary image. + """ + markers, n_objects = ndi.label(image) + + radii = [] + cx = [] + cy = [] + for s in ndi.find_objects(markers): + cy.append((s[0].start + s[0].stop) / 2) + cx.append((s[1].start + s[1].stop) / 2) + radii.append(np.sqrt((markers[s] > 0).sum() / np.pi)) + + return cx, cy, radii, n_objects + + +def get_plate_params(image: np.ndarray) -> PlateParams: + # Calculate a simple threshold + threshold = threshold_otsu(image) + + # Get initial well positions + cx, cy, radii, n_objects = get_wells(image > threshold) + + # Calculate the counterclockwise rotations + locations = np.vstack([cx, cy]).T + transform = locations @ ROTATION + + # Find the rotation that aligns the long edge of the plate horizontally + angle = np.argmin(transform.max(axis=0) - transform.min(axis=0)) + + # Shortest rotation to alignment + if angle > 90: + angle -= 180 + + # Rotate the plate and recalculate well positions + image_rotated = rotate(image, angle, preserve_range=True) + + # Recalculate well positions + cx, cy, radii, n_objects = get_wells(image_rotated > threshold) + + # Determine the plate layout + n_wells = len(cx) + plate_config = None + for layout in PlateSize: + error = abs(1 - n_wells / layout.value) + if error < 0.05: + plate_config = layout + break + if plate_config is None: + msg = "Could not determine plate layout" + raise ValueError(msg) + + # Get the mean radius + radii_mean = int(np.mean(radii)) + + # Get the bounding box after rotation + cx_min, cx_max = np.min(cx) - 2 * radii_mean, np.max(cx) + 2 * radii_mean + cy_min, cy_max = np.min(cy) - 2 * radii_mean, np.max(cy) + 2 * radii_mean + bbox = (int(cy_min), int(cy_max), int(cx_min), int(cx_max)) + + # Get X and Y indices + points = [] + for p, mval in zip([cy, cx], [int(cy_min), int(cx_min)]): + z_pos = list(p) + z_pos.sort() + z_index = 0 + z_count = 1 + Z = [z_pos[0]] + for z in z_pos[1:]: + if abs(Z[z_index] - z) < radii_mean // 3: + Z[z_index] = (Z[z_index] * z_count + z) / (z_count + 1) + z_count += 1 + else: + Z[z_index] = int(Z[z_index]) + Z.append(z) + z_index += 1 + z_count = 1 + Z[-1] = int(Z[-1]) + points.append(Z) + + Y = points[0] + X = points[1] + + # TODO: In case of merged wells, try to remove the bad point + + return PlateParams( + rotate=angle, + size=plate_config, + radii=int(radii_mean), + bbox=bbox, + X=X, + Y=Y, + ) + + +def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: + """Get the well intensity. + + Args: + image: _description_ + x: x-position of the well centerpoint + y: y-position of the well centerpoint + r: radius of the well + + Returns: + int: The background corrected mean well intensity + """ + assert r >= 5 + + # get a large patch to find background pixels + x_min = max(x - r, 0) + x_max = min(x + r, image.shape[1]) + y_min = max(y - r, 0) + y_max = min(y + r, image.shape[0]) + patch = image[y_min:y_max, x_min:x_max] + background = patch.ravel() + background.sort() + + # Subtract lowest pixel values from average center pixel values + return int(np.mean(patch) - np.mean(background[: int(0.05 * background.size)])) + + +def index_to_battleship(x: int, y: int, size: PlateSize) -> str: + """Get the battleship notation of a well index. + + Args: + x: x-position of the well centerpoint + y: y-position of the well centerpoint + + Returns: + str: The string representation of the well index (i.e. A1) + """ + # The y-position should be converted to an uppercase well letter + row = "" + if y >= 26: + row = "A" + row = row + string.ascii_uppercase[y % 26] + + # TODO: uncomment this when we are ready to deploy, this is the standard notation + # if size.value >= 96: + + return f"{row}{x+1}" From b058e2a0ac7a0e56f78f65262710a8daf5e0cc5e Mon Sep 17 00:00:00 2001 From: Najib Ishaq Date: Wed, 8 May 2024 07:51:41 -0400 Subject: [PATCH 02/26] feat: added intensity extraction tool --- .../.bumpversion.cfg | 31 ++++ .../Dockerfile | 20 +++ .../README.md | 24 +++ .../VERSION | 1 + .../ict.yml | 51 ++++++ .../plugin.json | 52 +++++++ .../pyproject.toml | 80 ++++++++++ .../run-plugin.sh | 20 +++ .../rt_cetsa_intensity_extraction/VERSION | 1 + .../rt_cetsa_intensity_extraction/__init__.py | 145 ++++++++++++++++++ .../rt_cetsa_intensity_extraction/__main__.py | 83 ++++++++++ 11 files changed, 508 insertions(+) create mode 100644 features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg create mode 100755 features/rt-cetsa-intensity-extraction-tool/Dockerfile create mode 100644 features/rt-cetsa-intensity-extraction-tool/README.md create mode 100644 features/rt-cetsa-intensity-extraction-tool/VERSION create mode 100644 features/rt-cetsa-intensity-extraction-tool/ict.yml create mode 100644 features/rt-cetsa-intensity-extraction-tool/plugin.json create mode 100644 features/rt-cetsa-intensity-extraction-tool/pyproject.toml create mode 100644 features/rt-cetsa-intensity-extraction-tool/run-plugin.sh create mode 100644 features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/VERSION create mode 100644 features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py create mode 100644 features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py diff --git a/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg new file mode 100644 index 000000000..ad1d0ff9f --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg @@ -0,0 +1,31 @@ +[bumpversion] +current_version = 0.1.0 +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:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:plugin.json] + +[bumpversion:file:VERSION] + +[bumpversion:file:README.md] + +[bumpversion:file:src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py] + +[bumpversion:file:ict.yml] diff --git a/features/rt-cetsa-intensity-extraction-tool/Dockerfile b/features/rt-cetsa-intensity-extraction-tool/Dockerfile new file mode 100755 index 000000000..e6d4334ab --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/Dockerfile @@ -0,0 +1,20 @@ +FROM polusai/bfio:2.3.6 + +# environment variables defined in polusai/bfio +ENV EXEC_DIR="/opt/executables" +ENV POLUS_IMG_EXT=".ome.tif" +ENV POLUS_TAB_EXT=".arrow" +ENV POLUS_LOG="INFO" + +# Work directory defined in the base container +WORKDIR ${EXEC_DIR} + +COPY pyproject.toml ${EXEC_DIR} +COPY VERSION ${EXEC_DIR} +COPY README.md ${EXEC_DIR} +COPY src ${EXEC_DIR}/src + +RUN pip3 install ${EXEC_DIR} --no-cache-dir + +ENTRYPOINT ["python3", "-m", "polus.images.features.rt_cetsa_intensity_extraction"] +CMD ["--help"] diff --git a/features/rt-cetsa-intensity-extraction-tool/README.md b/features/rt-cetsa-intensity-extraction-tool/README.md new file mode 100644 index 000000000..3d85f4503 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/README.md @@ -0,0 +1,24 @@ +# RT_CETSA Moltprot Regression (v0.1.0) + +This WIPP plugin runs regression analysis for the RT-CETSA pipeline. +The input csv file should be sorted by `Temperature` column. + +## Building + +To build the Docker image for the conversion plugin, run +`./build-docker.sh`. + +## Install WIPP Plugin + +If WIPP is running, navigate to the plugins page and add a new plugin. Paste the contents of `plugin.json` into the pop-up window and submit. + +## Options + +This plugin takes eight input argument and one output argument: + +| Name | Description | I/O | Type | +|-------------|----------------------------------------------------|--------|-------------| +| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | +| `--pattern` | Pattern to parse input files | Input | string | +| `--outDir` | Output file | Output | genericData | +| `--preview` | Generate JSON file with outputs | Output | JSON | diff --git a/features/rt-cetsa-intensity-extraction-tool/VERSION b/features/rt-cetsa-intensity-extraction-tool/VERSION new file mode 100644 index 000000000..6e8bf73aa --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/features/rt-cetsa-intensity-extraction-tool/ict.yml b/features/rt-cetsa-intensity-extraction-tool/ict.yml new file mode 100644 index 000000000..20e19cfbe --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/ict.yml @@ -0,0 +1,51 @@ +author: +- Nick Schaub +- Najib Ishaq +contact: nick.schaub@nih.gov +container: polusai/rt-cetsa-intensity-extraction-tool:0.1.0 +description: Extract well intensities from RT-CETSA plate images and masks. +entrypoint: python3 -m polus.images.features.rt_cetsa_intensity_extraction +inputs: +- description: Input data collection to be processed by this tool + format: + - inpDir + name: inpDir + required: true + type: path +- description: Filepattern to parse input files + format: + - pattern + name: pattern + required: false + type: string +- description: Generate an output preview. + format: + - preview + name: preview + required: false + type: boolean +name: polusai/RTCETSAIntensityExtraction +outputs: +- description: Output data + format: + - outDir + name: outDir + required: true + type: path +repository: https://github.com/PolusAI/image-tools +specVersion: 1.0.0 +title: RT-CETSA Intensity Extraction +ui: +- description: Input data collection + key: inputs.inpDir + title: Input data collection + type: path +- description: Filepattern to parse input files + key: inputs.pattern + title: pattern + type: text +- description: Generate an output preview. + key: inputs.preview + title: Preview example output of this plugin + type: checkbox +version: 0.1.0 diff --git a/features/rt-cetsa-intensity-extraction-tool/plugin.json b/features/rt-cetsa-intensity-extraction-tool/plugin.json new file mode 100644 index 000000000..bcba39484 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/plugin.json @@ -0,0 +1,52 @@ +{ + "name": "RT-CETSA Intensity Extraction", + "version": "0.1.0", + "title": "RT-CETSA Intensity Extraction", + "description": "Extract well intensities from RT-CETSA images and masks.", + "author": "Nicholas Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@nih.gov)", + "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", + "repository": "https://github.com/PolusAI/image-tools", + "website": "https://ncats.nih.gov/preclinical/core/informatics", + "citation": "", + "containerId": "polusai/rt-cetsa-intensity-extraction-tool:0.1.0", + "baseCommand": [ + "python3", + "-m", + "polus.images.features.rt_cetsa_intensity_extraction" + ], + "inputs": [ + { + "name": "inpDir", + "type": "genericData", + "description": "Input data collection to be processed by this tool", + "required": true + }, + { + "name": "pattern", + "type": "string", + "description": "Pattern to parse input files", + "default": ".+", + "required": false + } + ], + "outputs": [ + { + "name": "outDir", + "type": "genericData", + "description": "Output data collection" + } + ], + "ui": [ + { + "key": "inputs.inpDir", + "title": "Input collection", + "description": "Input data collection to be processed by this plugin" + }, + { + "key": "inputs.pattern", + "title": "pattern", + "description": "Pattern to parse input files", + "default": ".+" + } + ] +} diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml new file mode 100644 index 000000000..4eeb8283a --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -0,0 +1,80 @@ +[tool.poetry] +name = "polus_images_features_rt_cetsa_intensity_extraction" +version = "0.1.0" +description = "Extract well intensities from RT-CETSA plate images and masks." +authors = [ + "Nick Schaub ", + "Najib Ishaq ", +] +readme = "README.md" +packages = [{include = "polus", from = "src"}] + +[tool.poetry.dependencies] +python = ">=3.9,<3.12" +typer = "^0.7.0" +filepattern = "^2.0.5" +pandas = "^2.2.2" +numpy = "^1.26.4" +bfio = "^2.3.6" + +[tool.poetry.group.dev.dependencies] +bump2version = "^1.0.1" +pre-commit = "^3.1.0" +pytest = "^7.2.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +extend = "../../ruff.toml" +extend-ignore = [ + "RET505", # Unnecessary `else` after `return` statement + "E501", # Line too long + "ANN001", # Missing type annotation + "D102", # Missing docstring in public method + "ANN201", # Missing return type annotation + "N806", # Variable in function should be lowercase + "D205", # 1 blank line required between summary line and description + "N803", # Argument name should be lowercase + "PLR0913", # Too many arguments + "D415", # First line should end with a period, question mark, or exclamation point + "PLR2004", # Magic value used in comparison + "B006", # Do not use mutable default arguments + "D107", # Missing docstring + "D101", # Missing docstring + "E731", # Do not assign a lambda expression, use a def + "E402", # Module level import not at top of file + "PTH123", # `open()` should be replaced with `Path.open()` + "PTH118", # `os.path.join()` should be replaced with `/` operator + "PTH100", # `os.path.abspath()` should be replaced with `Path.resolve()` + "PLR0915", # Too many statements + "PLR0912", # Too many branches + "C901", # Function is too complex + "T201", # `print` used + "E722", # Do not use bare 'except' + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling + "ANN202", # Missing return type annotation for private function + "ARG002", # Unused method argument + "N802", # Function name should be lowercase + "PTH103", # `os.makedirs()` should be replaced with `Path.mkdir(parents=True)` + "ANN003", # Missing type annotation for `**kwargs` + "B007", # Loop control variable not used within the loop body + "ANN204", # Missing return type annotation for magic method + "D417", # Missing argument descriptions in the docstring + "ANN205", # Missing return type annotation for static method + "PLR5501", # Use `elif` instead of `else` following `if` condition to avoid unnecessary indentation + "EM102", # Exception must not use an f-string literal + "D414", # Section has no content + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "A001", # Variable `input` is shadowing a Python builtin + "A002", # Argument `input` is shadowing a Python builtin + "E741", # Ambiguous variable name: `l` + "PTH120", # `os.path.dirname()` should be replaced by `Path.parent` + "N816", # Variable `cfFilename` in global scope should not be mixedCase + "PTH109", # `os.getcwd()` should be replaced by `Path.cwd()` + "ARG001", # Unused function argument + "S101", # Use of assert detected + "D103", # Missing docstring in public function + "D100", # Missing docstring in public module +] diff --git a/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh b/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh new file mode 100644 index 000000000..97d700584 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh @@ -0,0 +1,20 @@ +#!/bin/bash +version=$( list[float]: + """Extract well intensities from RT_CETSA image and mask. + + Args: + image_path: Path to the RT_CETSA image. + mask_path: Path to the mask image. + + Returns: + Pandas DataFrame with well intensities. + """ + with bfio.BioReader(image_path) as reader: + image = reader[:] + with bfio.BioReader(mask_path) as reader: + mask = reader[:] + + max_mask_index = numpy.max(mask) + intensities = [] + for i in range(1, max_mask_index + 1): + mask_index = mask == i + mask_values = image[mask_index] + + # find a square bounding box around the mask + bbox = numpy.argwhere(mask_index) + bbox_x_min = numpy.min(bbox[0]) + bbox_x_max = numpy.max(bbox[0]) + bbox_y_min = numpy.min(bbox[1]) + bbox_y_max = numpy.max(bbox[1]) + bbox_values = image[bbox_x_min:bbox_x_max, bbox_y_min:bbox_y_max] + + # find the mean intensity of the background and the mask + mean_background = (numpy.sum(bbox_values) - numpy.sum(mask_values)) / ( + bbox_values.size - mask_values.size + ) + mean_intensities = numpy.mean(mask_values) + + intensities.append(mean_intensities - mean_background) + + return intensities + + +def index_to_battleship(x: int, y: int, size: PlateSize) -> str: + """Get the battleship notation of a well index. + + Args: + x: x-position of the well centerpoint + y: y-position of the well centerpoint + + Returns: + str: The string representation of the well index (i.e. A1) + """ + # The y-position should be converted to an uppercase well letter + row = "" + if y >= 26: + row = "A" + row = row + string.ascii_uppercase[y % 26] + + # TODO: uncomment this when we are ready to deploy, this is the standard notation + # if size.value >= 96: + + return f"{row}{x+1}" + + +def build_df( + file_paths: list[tuple[pathlib.Path, pathlib.Path]], +) -> pandas.DataFrame: + """Build a DataFrame with well intensities. + + Args: + file_paths: List of tuples with image and mask paths. + + Returns: + Pandas DataFrame with well intensities. + """ + intensities: list[tuple[float, list[float]]] = [] + for i, (image_path, mask_path) in enumerate(file_paths): + temp = TEMPERATURE_RANGE[0] + i / (len(file_paths) - 1) * ( + TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] + ) + intensities.append((temp, extract_intensities(image_path, mask_path))) + + # sort intensities by temperature + intensities.sort(key=lambda x: x[0]) + + # build header + header = ["Temperature"] + plate_size = PlateSize(len(intensities[0][1])) + + for x, y in itertools.product( + range(PLATE_DIMS[plate_size][0]), + range(PLATE_DIMS[plate_size][1]), + ): + header.append(index_to_battleship(x, y, plate_size)) + + # build DataFrame + df = pandas.DataFrame(intensities, columns=header) + + # Set the temperature as the index + df.set_index("Temperature", inplace=True) + + # Sort the rows by temperature + df.sort_index(inplace=True) + + return df diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py new file mode 100644 index 000000000..cea7694c0 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -0,0 +1,83 @@ +"""CLI for rt-cetsa-intensity-extraction-tool.""" + +import json +import logging +import os +import pathlib + +import filepattern +import typer +from polus.images.features.rt_cetsa_intensity_extraction import build_df + +# Initialize the logger +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger("polus.images.features.rt_cetsa_intensity_extraction") +logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) + +POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") + +app = typer.Typer() + + +@app.command() +def main( + inp_dir: pathlib.Path = typer.Option( + ..., + help="Input directory containing the data files.", + exists=True, + dir_okay=True, + readable=True, + resolve_path=True, + ), + pattern: str = typer.Option( + ".+", + help="Pattern to match the files in the input directory.", + ), + preview: bool = typer.Option( + False, + help="Preview the files that will be processed.", + ), + out_dir: pathlib.Path = typer.Option( + ..., + help="Output directory to save the results.", + exists=True, + dir_okay=True, + writable=True, + resolve_path=True, + ), +) -> None: + """CLI for rt-cetsa-plate-extraction-tool.""" + logger.info("Starting the CLI for rt-cetsa-plate-extraction-tool.") + + logger.info(f"Input directory: {inp_dir}") + logger.info(f"File Pattern: {pattern}") + logger.info(f"Output directory: {out_dir}") + + images_dir = inp_dir / "images" + masks_dir = inp_dir / "masks" + assert images_dir.exists(), f"Images directory does not exist: {images_dir}" + assert masks_dir.exists(), f"Masks directory does not exist: {masks_dir}" + + fp = filepattern.FilePattern(images_dir, pattern) + img_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + mask_files: list[pathlib.Path] = [masks_dir / f.name for f in img_files] # type: ignore[assignment] + for f in mask_files: + assert f.exists(), f"Mask file does not exist: {f}" + + inp_files = list(zip(img_files, mask_files)) # type: ignore[assignment] + + if preview: + out_json = {"file": "plate.csv"} + with (out_dir / "preview.json").open("w") as writer: + json.dump(out_json, writer, indent=2) + return + + df = build_df(inp_files) + df.to_csv(out_dir / "plate.csv") + + +if __name__ == "__main__": + app() From df1b7d8ff3ec21385c97327be10141f14d78d4b2 Mon Sep 17 00:00:00 2001 From: agerardin Date: Fri, 10 May 2024 01:51:19 -0400 Subject: [PATCH 03/26] fix: plate extraction tool --- .../rt-cetsa-plate-extraction-tool/README.md | 18 +-- .../rt-cetsa-plate-extraction-tool/ict.yml | 7 +- .../plugin.json | 14 +- .../pyproject.toml | 1 + .../run-plugin.sh | 4 +- .../rt_cetsa_plate_extraction/VERSION | 1 - .../rt_cetsa_plate_extraction/__init__.py | 34 +---- .../rt_cetsa_plate_extraction/__main__.py | 48 ++++-- .../rt_cetsa_plate_extraction/core.py | 143 +++++++++--------- 9 files changed, 132 insertions(+), 138 deletions(-) delete mode 100644 segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/VERSION diff --git a/segmentation/rt-cetsa-plate-extraction-tool/README.md b/segmentation/rt-cetsa-plate-extraction-tool/README.md index 3d85f4503..b2c779bff 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/README.md +++ b/segmentation/rt-cetsa-plate-extraction-tool/README.md @@ -1,7 +1,7 @@ -# RT_CETSA Moltprot Regression (v0.1.0) +# RT_CETSA Plate Extraction Tool (v0.1.0) -This WIPP plugin runs regression analysis for the RT-CETSA pipeline. -The input csv file should be sorted by `Temperature` column. +This tool extracts detect wells in a RT-CETSA plate image. +It outputs a cropped and rotated image and the well detection mask. ## Building @@ -16,9 +16,9 @@ If WIPP is running, navigate to the plugins page and add a new plugin. Paste the This plugin takes eight input argument and one output argument: -| Name | Description | I/O | Type | -|-------------|----------------------------------------------------|--------|-------------| -| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | -| `--pattern` | Pattern to parse input files | Input | string | -| `--outDir` | Output file | Output | genericData | -| `--preview` | Generate JSON file with outputs | Output | JSON | +| Name | Description | I/O | Type | +|-----------------|----------------------------------------------------|--------|-------------| +| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | +| `--filePattern` | FilePattern to parse input files | Input | string | +| `--outDir` | Output dir | Output | genericData | +| `--preview` | Generate JSON file with outputs | Output | JSON | diff --git a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml index d92148623..78db1ea18 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml +++ b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml @@ -1,12 +1,13 @@ author: - Nick Schaub - Najib Ishaq +- Antoine Gerardin contact: nick.schaub@nih.gov container: polusai/rt-cetsa-plate-extraction-tool:0.1.0 description: Rotate and crop images of plates from RT-CETSA; then label the wells. entrypoint: python3 -m polus.images.segmentation.rt_cetsa_plate_extraction inputs: -- description: Input data collection to be processed by this tool +- description: Input directory containing the plate images format: - inpDir name: inpDir @@ -14,8 +15,8 @@ inputs: type: path - description: Filepattern to parse input files format: - - pattern - name: pattern + - filePattern + name: filePattern required: false type: string - description: Generate an output preview. diff --git a/segmentation/rt-cetsa-plate-extraction-tool/plugin.json b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json index 66dda5111..0635d8775 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/plugin.json +++ b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json @@ -3,7 +3,7 @@ "version": "0.1.0", "title": "RT-CETSA Plate Extraction", "description": "Run regression analysis for the RT-CETSA pipeline.", - "author": "Nicholas Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@nih.gov)", + "author": "Nicholas Schaub (nick.schaub@nih.gov), Najib Ishaq (najib.ishaq@nih.gov), Antoine Gerardin (antoine.gerardin@nih.gov)", "institution": "National Center for Advancing Translational Sciences, National Institutes of Health", "repository": "https://github.com/PolusAI/image-tools", "website": "https://ncats.nih.gov/preclinical/core/informatics", @@ -18,13 +18,13 @@ { "name": "inpDir", "type": "genericData", - "description": "Input data collection to be processed by this tool", + "description": "Input directory containing the plate images", "required": true }, { - "name": "pattern", + "name": "filePattern", "type": "string", - "description": "Pattern to parse input files", + "description": "File Pattern to parse input files", "default": ".+", "required": false } @@ -43,9 +43,9 @@ "description": "Input data collection to be processed by this plugin" }, { - "key": "inputs.pattern", - "title": "pattern", - "description": "Pattern to parse input files", + "key": "inputs.filePattern", + "title": "filePattern", + "description": "File Pattern to parse input files", "default": ".+" } ] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml index 64e3f0dbb..f743182af 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml +++ b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "Rotate and crop images of plates from RT-CETSA; then label the wells." authors = [ "Nick Schaub ", + "Antoine Gerardin ", "Najib Ishaq ", ] readme = "README.md" diff --git a/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh b/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh index 97d700584..aa4d5a2c6 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh +++ b/segmentation/rt-cetsa-plate-extraction-tool/run-plugin.sh @@ -14,7 +14,7 @@ LOGLEVEL=INFO docker run --mount type=bind,source=${datapath},target=/data/ \ --env POLUS_LOG=${LOGLEVEL} \ - polusai/rt-cetsa-moltprot-tool:${version} \ + polusai/rt-cetsa-plate-extraction-tool:${version} \ --inpDir ${inpDir} \ - --pattern ${pattern} \ + --filePattern ${pattern} \ --outDir ${outDir} diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/VERSION b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/VERSION deleted file mode 100644 index bcea87eeb..000000000 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.1-alpha diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index 37a897214..dc50c00aa 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -2,36 +2,4 @@ __version__ = "0.1.0" -import itertools -import pathlib - -import numpy -import tifffile -from skimage.draw import disk -from skimage.transform import rotate - -from . import core - - -def extract_plate(file_path: pathlib.Path) -> tuple[numpy.ndarray, numpy.ndarray]: - """Extract plate from RT_CETSA image. - - Args: - file_path: Path to the image file. - - Returns: - Tuple containing the plate image and the mask. - """ - image = tifffile.imread(file_path) - params = core.get_plate_params(image) - rotated_image = rotate(image, params.rotate, preserve_range=True)[ - params.bbox[0] : params.bbox[1], - params.bbox[2] : params.bbox[3], - ].astype(image.dtype) - - mask = numpy.zeros_like(rotated_image, dtype=numpy.uint16) - for i, (x, y) in enumerate(itertools.product(params.X, params.Y), start=1): - rr, cc = disk((y, x), params.radii) - mask[rr, cc] = i - - return rotated_image, mask +from polus.images.segmentation.rt_cetsa_plate_extraction.core import extract_plate diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py index 3fe78a420..82d3accb7 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -8,7 +8,10 @@ import bfio import filepattern import typer -from polus.images.segmentation.rt_cetsa_plate_extraction import extract_plate +from polus.images.segmentation.rt_cetsa_plate_extraction.core import ( + PlateExtractionError, +) +from polus.images.segmentation.rt_cetsa_plate_extraction.core import extract_plate # Initialize the logger logging.basicConfig( @@ -27,7 +30,8 @@ def main( inp_dir: pathlib.Path = typer.Option( ..., - help="Input directory containing the data files.", + "--inpDir", + help="Input directory containing the plate images.", exists=True, dir_okay=True, readable=True, @@ -35,14 +39,17 @@ def main( ), pattern: str = typer.Option( ".+", + "--filePattern", help="Pattern to match the files in the input directory.", ), preview: bool = typer.Option( False, + "--preview", help="Preview the files that will be processed.", ), out_dir: pathlib.Path = typer.Option( ..., + "--outDir", help="Output directory to save the results.", exists=True, dir_okay=True, @@ -63,24 +70,45 @@ def main( if preview: out_json = { "images": [ - out_dir / "images" / f"{f.stem}{POLUS_IMG_EXT}" for f in inp_files + (out_dir / "images" / f"{f.stem}{POLUS_IMG_EXT}").as_posix() + for f in inp_files ], "masks": [ - out_dir / "masks" / f"{f.stem}{POLUS_IMG_EXT}" for f in inp_files + (out_dir / "masks" / f"{f.stem}{POLUS_IMG_EXT}").as_posix() + for f in inp_files ], } with (out_dir / "preview.json").open("w") as f: json.dump(out_json, f, indent=2) return + (out_dir / "images").mkdir(parents=False, exist_ok=True) + (out_dir / "masks").mkdir(parents=False, exist_ok=True) + + failed_detections = [] + for f in inp_files: # type: ignore[assignment] logger.info(f"Processing file: {f}") - image, mask = extract_plate(f) - out_name = f.stem + POLUS_IMG_EXT # type: ignore[attr-defined] - with bfio.BioWriter(out_dir / "images" / out_name) as writer: - writer[:] = image - with bfio.BioWriter(out_dir / "masks" / out_name) as writer: - writer[:] = mask + try: + image, mask = extract_plate(f) + out_name = f.stem + POLUS_IMG_EXT # type: ignore[attr-defined] + with bfio.BioWriter(out_dir / "images" / out_name) as writer: + writer.dtype = image.dtype + writer.shape = image.shape + writer[:] = image + with bfio.BioWriter(out_dir / "masks" / out_name) as writer: + writer.dtype = mask.dtype + writer.shape = mask.shape + writer[:] = mask + except ValueError as e: + logger.error(e) + failed_detections.append(f) + + if failed_detections: + filenames = [filepath.name for filepath in failed_detections] + raise PlateExtractionError( + f"{len(failed_detections)} plates could be processed sucessfully: {filenames}", + ) if __name__ == "__main__": diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py index 344491790..0b9f9ce27 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py @@ -1,14 +1,51 @@ -import string +import itertools +import pathlib from enum import Enum import numpy as np +import tifffile from pydantic import BaseModel from scipy import ndimage as ndi +from skimage.draw import disk from skimage.filters import threshold_otsu from skimage.transform import rotate +class PlateExtractionError(Exception): + """Raised if the plate could not be processed successfully.""" + + +def extract_plate(file_path: pathlib.Path) -> tuple[np.ndarray, np.ndarray]: + """Extract wells from an RT_CETSA plate image. + + Args: + file_path: Path to the image file. + + Returns: + Tuple containing the crop and rotated image and the mask of detected wells. + """ + # TODO replace by bfio + image = tifffile.imread(file_path) + + params = get_plate_params(image) + crop_and_rotated_image = rotate(image, params.rotate, preserve_range=True)[ + params.bbox[0] : params.bbox[1], + params.bbox[2] : params.bbox[3], + ].astype(image.dtype) + + wells_mask = np.zeros_like(crop_and_rotated_image, dtype=np.uint16) + + for mask_label, (x, y) in enumerate(itertools.product(params.X, params.Y), start=1): + x_crop, y_crop = (x - params.bbox[2], y - params.bbox[0]) + rr, cc = disk((y_crop, x_crop), params.radius) + wells_mask[rr, cc] = mask_label + + return crop_and_rotated_image, wells_mask + + class PlateSize(Enum): + """Common Plate Sizes.""" + SIZE_6 = 6 SIZE_12 = 12 SIZE_24 = 24 @@ -18,16 +55,19 @@ class PlateSize(Enum): SIZE_1536 = 1536 +"""Plate layouts.""" +# Dims in row/cols PLATE_DIMS = { PlateSize.SIZE_6: (2, 3), PlateSize.SIZE_12: (3, 4), PlateSize.SIZE_24: (4, 6), PlateSize.SIZE_48: (6, 8), - PlateSize.SIZE_96: (9, 12), + PlateSize.SIZE_96: (8, 12), PlateSize.SIZE_384: (16, 24), PlateSize.SIZE_1536: (32, 48), } +"""Half rotation matrix of all degree-wise rotation on [0,180).""" ROTATION = np.vstack( [ -np.sin(np.arange(0, np.pi, np.pi / 180)), @@ -46,7 +86,7 @@ class PlateParams(BaseModel): size: PlateSize """The plate size, also determines layout.""" - radii: int + radius: int """Well radius.""" X: list[int] @@ -56,34 +96,21 @@ class PlateParams(BaseModel): """The the y axis points for wells.""" -def get_wells(image: np.ndarray) -> tuple[list[float], list[float], list[float], int]: - """Get well locations and radii. +def get_plate_params(image: np.ndarray) -> PlateParams: + """Detect wells in the image plate. - Since RT-CETSA are generally high signal to noise, no need for anything fance - to detect wells. Simple Otsu threshold to segment the well, image labeling, - and estimation of radius based off of area (assuming the area is a circle). + Args: + image: the original RT_cetsa image. - The input image is a binary image. + Returns: + PlateParams: The description of the plate. """ - markers, n_objects = ndi.label(image) - - radii = [] - cx = [] - cy = [] - for s in ndi.find_objects(markers): - cy.append((s[0].start + s[0].stop) / 2) - cx.append((s[1].start + s[1].stop) / 2) - radii.append(np.sqrt((markers[s] > 0).sum() / np.pi)) - - return cx, cy, radii, n_objects - - -def get_plate_params(image: np.ndarray) -> PlateParams: - # Calculate a simple threshold + # Since RT-CETSA are generally high signal to noise, + # we use a Simple Otsu threshold to segment the well. threshold = threshold_otsu(image) # Get initial well positions - cx, cy, radii, n_objects = get_wells(image > threshold) + cx, cy, radii, _ = detect_wells(image > threshold) # Calculate the counterclockwise rotations locations = np.vstack([cx, cy]).T @@ -100,7 +127,7 @@ def get_plate_params(image: np.ndarray) -> PlateParams: image_rotated = rotate(image, angle, preserve_range=True) # Recalculate well positions - cx, cy, radii, n_objects = get_wells(image_rotated > threshold) + cx, cy, radii, _ = detect_wells(image_rotated > threshold) # Determine the plate layout n_wells = len(cx) @@ -111,10 +138,11 @@ def get_plate_params(image: np.ndarray) -> PlateParams: plate_config = layout break if plate_config is None: - msg = "Could not determine plate layout" + msg = f"Could not determine plate layout, detected {n_wells} wells." raise ValueError(msg) # Get the mean radius + # all wells must have the same size. radii_mean = int(np.mean(radii)) # Get the bounding box after rotation @@ -145,62 +173,31 @@ def get_plate_params(image: np.ndarray) -> PlateParams: Y = points[0] X = points[1] - # TODO: In case of merged wells, try to remove the bad point - return PlateParams( rotate=angle, size=plate_config, - radii=int(radii_mean), + radius=int(radii_mean), bbox=bbox, X=X, Y=Y, ) -def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: - """Get the well intensity. - - Args: - image: _description_ - x: x-position of the well centerpoint - y: y-position of the well centerpoint - r: radius of the well - - Returns: - int: The background corrected mean well intensity - """ - assert r >= 5 - - # get a large patch to find background pixels - x_min = max(x - r, 0) - x_max = min(x + r, image.shape[1]) - y_min = max(y - r, 0) - y_max = min(y + r, image.shape[0]) - patch = image[y_min:y_max, x_min:x_max] - background = patch.ravel() - background.sort() - - # Subtract lowest pixel values from average center pixel values - return int(np.mean(patch) - np.mean(background[: int(0.05 * background.size)])) - +def detect_wells( + image: np.ndarray, +) -> tuple[list[float], list[float], list[float], int]: + """Detect well locations and radii estimations. -def index_to_battleship(x: int, y: int, size: PlateSize) -> str: - """Get the battleship notation of a well index. - - Args: - x: x-position of the well centerpoint - y: y-position of the well centerpoint - - Returns: - str: The string representation of the well index (i.e. A1) + Wells are assumed to be disks. """ - # The y-position should be converted to an uppercase well letter - row = "" - if y >= 26: - row = "A" - row = row + string.ascii_uppercase[y % 26] + markers, n_objects = ndi.label(image) - # TODO: uncomment this when we are ready to deploy, this is the standard notation - # if size.value >= 96: + radii = [] + cx = [] + cy = [] + for s in ndi.find_objects(markers): + cy.append((s[0].start + s[0].stop) / 2) + cx.append((s[1].start + s[1].stop) / 2) + radii.append(np.sqrt((markers[s] > 0).sum() / np.pi)) - return f"{row}{x+1}" + return cx, cy, radii, n_objects From f4dc409d44ea73943909af43ee3439cecb0e687e Mon Sep 17 00:00:00 2001 From: agerardin Date: Fri, 10 May 2024 05:45:58 -0400 Subject: [PATCH 04/26] fix: rt_cetsa intensity extraction tool. --- .../Dockerfile | 2 +- .../pyproject.toml | 2 + .../run-plugin.sh | 2 +- .../rt_cetsa_intensity_extraction/VERSION | 1 - .../rt_cetsa_intensity_extraction/__init__.py | 63 ++++++++++++------- .../rt_cetsa_intensity_extraction/__main__.py | 30 ++++++--- .../tests/__init__.py | 1 + .../tests/test_battleship_coordinates.py | 7 +++ 8 files changed, 71 insertions(+), 37 deletions(-) delete mode 100644 features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/VERSION create mode 100644 features/rt-cetsa-intensity-extraction-tool/tests/__init__.py create mode 100644 features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py diff --git a/features/rt-cetsa-intensity-extraction-tool/Dockerfile b/features/rt-cetsa-intensity-extraction-tool/Dockerfile index e6d4334ab..763221bd5 100755 --- a/features/rt-cetsa-intensity-extraction-tool/Dockerfile +++ b/features/rt-cetsa-intensity-extraction-tool/Dockerfile @@ -3,7 +3,7 @@ FROM polusai/bfio:2.3.6 # environment variables defined in polusai/bfio ENV EXEC_DIR="/opt/executables" ENV POLUS_IMG_EXT=".ome.tif" -ENV POLUS_TAB_EXT=".arrow" +ENV POLUS_TAB_EXT=".csv" ENV POLUS_LOG="INFO" # Work directory defined in the base container diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index 4eeb8283a..2ea198776 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "Extract well intensities from RT-CETSA plate images and masks." authors = [ "Nick Schaub ", + "Antoine Gerardin ", "Najib Ishaq ", ] readme = "README.md" @@ -16,6 +17,7 @@ filepattern = "^2.0.5" pandas = "^2.2.2" numpy = "^1.26.4" bfio = "^2.3.6" +scikit-image = "^0.22.0" [tool.poetry.group.dev.dependencies] bump2version = "^1.0.1" diff --git a/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh b/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh index 97d700584..d9367a09e 100644 --- a/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh +++ b/features/rt-cetsa-intensity-extraction-tool/run-plugin.sh @@ -14,7 +14,7 @@ LOGLEVEL=INFO docker run --mount type=bind,source=${datapath},target=/data/ \ --env POLUS_LOG=${LOGLEVEL} \ - polusai/rt-cetsa-moltprot-tool:${version} \ + polusai/rt-cetsa-intensity-extraction-tool:${version} \ --inpDir ${inpDir} \ --pattern ${pattern} \ --outDir ${outDir} diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/VERSION b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/VERSION deleted file mode 100644 index bcea87eeb..000000000 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.1-alpha diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index db83d2820..c6a5a866b 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -9,11 +9,13 @@ import bfio import numpy +import numpy as np import pandas from skimage.draw import disk from skimage.transform import rotate -TEMPERATURE_RANGE = [37, 95] +ADD_TEMP = True +TEMPERATURE_RANGE = [37, 90] class PlateSize(Enum): @@ -40,7 +42,7 @@ class PlateSize(Enum): def extract_intensities( image_path: pathlib.Path, mask_path: pathlib.Path, -) -> list[float]: +) -> list[int]: """Extract well intensities from RT_CETSA image and mask. Args: @@ -58,24 +60,25 @@ def extract_intensities( max_mask_index = numpy.max(mask) intensities = [] for i in range(1, max_mask_index + 1): - mask_index = mask == i - mask_values = image[mask_index] + current_mask = mask == i + image[current_mask] # find a square bounding box around the mask - bbox = numpy.argwhere(mask_index) + bbox = numpy.argwhere(current_mask) bbox_x_min = numpy.min(bbox[0]) bbox_x_max = numpy.max(bbox[0]) bbox_y_min = numpy.min(bbox[1]) bbox_y_max = numpy.max(bbox[1]) - bbox_values = image[bbox_x_min:bbox_x_max, bbox_y_min:bbox_y_max] - # find the mean intensity of the background and the mask - mean_background = (numpy.sum(bbox_values) - numpy.sum(mask_values)) / ( - bbox_values.size - mask_values.size + patch = image[bbox_y_min:bbox_y_max, bbox_x_min:bbox_x_max] + background = patch.ravel() + background.sort() + corrected_mean_intensity = int( + np.mean(patch) - np.mean(background[: int(0.05 * background.size)]), ) - mean_intensities = numpy.mean(mask_values) - intensities.append(mean_intensities - mean_background) + # Subtract lowest pixel values from average pixel values + intensities.append(corrected_mean_intensity) return intensities @@ -86,6 +89,7 @@ def index_to_battleship(x: int, y: int, size: PlateSize) -> str: Args: x: x-position of the well centerpoint y: y-position of the well centerpoint + size: size of the plate Returns: str: The string representation of the well index (i.e. A1) @@ -96,10 +100,7 @@ def index_to_battleship(x: int, y: int, size: PlateSize) -> str: row = "A" row = row + string.ascii_uppercase[y % 26] - # TODO: uncomment this when we are ready to deploy, this is the standard notation - # if size.value >= 96: - - return f"{row}{x+1}" + return f"{row}{x + 1:02d}" if size.value >= 96 else f"{row}{x + 1}" def build_df( @@ -113,33 +114,47 @@ def build_df( Returns: Pandas DataFrame with well intensities. """ - intensities: list[tuple[float, list[float]]] = [] + intensities: list[tuple[float, list[int]]] = [] + + if not ADD_TEMP: + raise NotImplementedError + + if len(file_paths) < 1: + raise ValueError( + "provide at least 2 images on the temperature interval" + + f"{TEMPERATURE_RANGE[0]}-{TEMPERATURE_RANGE[1]}", + ) + for i, (image_path, mask_path) in enumerate(file_paths): temp = TEMPERATURE_RANGE[0] + i / (len(file_paths) - 1) * ( TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] ) - intensities.append((temp, extract_intensities(image_path, mask_path))) + row = (temp, extract_intensities(image_path, mask_path)) + intensities.append(row) # sort intensities by temperature intensities.sort(key=lambda x: x[0]) + # check the first plate for number of wells + nb_wells = len(intensities[0][1]) + plate_size = PlateSize(nb_wells) + # build header header = ["Temperature"] - plate_size = PlateSize(len(intensities[0][1])) + plate_row = range(PLATE_DIMS[plate_size][0]) + plate_col = range(PLATE_DIMS[plate_size][1]) - for x, y in itertools.product( - range(PLATE_DIMS[plate_size][0]), - range(PLATE_DIMS[plate_size][1]), - ): + for y, x in itertools.product(plate_row, plate_col): header.append(index_to_battleship(x, y, plate_size)) # build DataFrame - df = pandas.DataFrame(intensities, columns=header) + rows = [[round(measure[0], 1), *measure[1]] for measure in intensities] + df = pandas.DataFrame(rows, columns=header) # Set the temperature as the index df.set_index("Temperature", inplace=True) - # Sort the rows by temperature + # Sort the roxws by temperature df.sort_index(inplace=True) return df diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index cea7694c0..b7358cd4a 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -26,6 +26,7 @@ def main( inp_dir: pathlib.Path = typer.Option( ..., + "--inpDir", help="Input directory containing the data files.", exists=True, dir_okay=True, @@ -34,14 +35,17 @@ def main( ), pattern: str = typer.Option( ".+", + "--filePattern", help="Pattern to match the files in the input directory.", ), preview: bool = typer.Option( False, + "--preview", help="Preview the files that will be processed.", ), out_dir: pathlib.Path = typer.Option( ..., + "--outDir", help="Output directory to save the results.", exists=True, dir_okay=True, @@ -49,8 +53,8 @@ def main( resolve_path=True, ), ) -> None: - """CLI for rt-cetsa-plate-extraction-tool.""" - logger.info("Starting the CLI for rt-cetsa-plate-extraction-tool.") + """CLI for rt-cetsa-intensity-extraction-tool.""" + logger.info("Starting the CLI for rt-cetsa-v-extraction-tool.") logger.info(f"Input directory: {inp_dir}") logger.info(f"File Pattern: {pattern}") @@ -58,24 +62,30 @@ def main( images_dir = inp_dir / "images" masks_dir = inp_dir / "masks" - assert images_dir.exists(), f"Images directory does not exist: {images_dir}" - assert masks_dir.exists(), f"Masks directory does not exist: {masks_dir}" + if not images_dir.exists(): + raise FileNotFoundError(f"Images directory does not exist: {images_dir}") + if not masks_dir.exists(): + raise FileNotFoundError(f"Masks directory does not exist: {masks_dir}") fp = filepattern.FilePattern(images_dir, pattern) img_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] mask_files: list[pathlib.Path] = [masks_dir / f.name for f in img_files] # type: ignore[assignment] + for f in mask_files: - assert f.exists(), f"Mask file does not exist: {f}" + if not f.exists(): + raise FileNotFoundError(f"Mask file does not exist: {f}") - inp_files = list(zip(img_files, mask_files)) # type: ignore[assignment] + row_files = list(zip(img_files, mask_files)) if preview: - out_json = {"file": "plate.csv"} - with (out_dir / "preview.json").open("w") as writer: - json.dump(out_json, writer, indent=2) + vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) + out_json = {"files": [f"plate_({vals[0]}-{vals[-1]}).csv"]} + # TODO check mypy complains + with (out_dir / "preview.json").open("w") as f: # type: ignore[assignment] + json.dump(out_json, f, indent=2) # type: ignore return - df = build_df(inp_files) + df = build_df(row_files) df.to_csv(out_dir / "plate.csv") diff --git a/features/rt-cetsa-intensity-extraction-tool/tests/__init__.py b/features/rt-cetsa-intensity-extraction-tool/tests/__init__.py new file mode 100644 index 000000000..27b31c200 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/tests/__init__.py @@ -0,0 +1 @@ +"""tests.""" diff --git a/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py b/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py new file mode 100644 index 000000000..2d0c1a287 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py @@ -0,0 +1,7 @@ +from polus.images.features.rt_cetsa_intensity_extraction import PlateSize +from polus.images.features.rt_cetsa_intensity_extraction import index_to_battleship + + +def test_battleship_coordinates(): + assert index_to_battleship(0, 0, PlateSize(96)) == "A01" + assert index_to_battleship(12, 8, PlateSize(96)) == "H12" From 2ee3cf56d7a9ee117b6c9e1b62209e11333e7d31 Mon Sep 17 00:00:00 2001 From: agerardin Date: Mon, 13 May 2024 07:02:35 -0400 Subject: [PATCH 05/26] fix: rt_cetsa intensity extraction --- .../rt_cetsa_intensity_extraction/__init__.py | 191 ++++++++++-------- .../rt_cetsa_intensity_extraction/__main__.py | 42 ++-- .../tests/test_alphanumeric_row.py | 28 +++ .../tests/test_battleship_coordinates.py | 7 - 4 files changed, 151 insertions(+), 117 deletions(-) create mode 100644 features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py delete mode 100644 features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index c6a5a866b..35c2aee6b 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -1,8 +1,8 @@ """RT_CETSA Intensity Extraction Tool.""" -__version__ = "0.1.0" - import itertools +import logging +import os import pathlib import string from enum import Enum @@ -11,13 +11,18 @@ import numpy import numpy as np import pandas -from skimage.draw import disk -from skimage.transform import rotate + +logger = logging.getLogger(__file__) +logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) ADD_TEMP = True TEMPERATURE_RANGE = [37, 90] +class IntensityExtractionError(Exception): + pass + + class PlateSize(Enum): SIZE_6 = 6 SIZE_12 = 12 @@ -33,24 +38,88 @@ class PlateSize(Enum): PlateSize.SIZE_12: (3, 4), PlateSize.SIZE_24: (4, 6), PlateSize.SIZE_48: (6, 8), - PlateSize.SIZE_96: (9, 12), + PlateSize.SIZE_96: (8, 12), PlateSize.SIZE_384: (16, 24), PlateSize.SIZE_1536: (32, 48), } -def extract_intensities( +def extract_signal( + img_paths: list[pathlib.Path], + mask_path: pathlib.Path, +) -> pandas.DataFrame: + """Build a DataFrame with well intensity measurements for each temperature. + + Args: + img_paths: List of image paths. + mask_path: path to the wells mask. + + Returns: + Pandas DataFrame. + """ + if not ADD_TEMP: + raise NotImplementedError + + num_images = len(img_paths) + + if num_images < 2: + raise ValueError( + "provide at least 2 images on the temperature interval " + + f"({TEMPERATURE_RANGE[0]}-{TEMPERATURE_RANGE[1]})", + ) + + measures: list[tuple[float, list[int]]] = [] + for index, image_path in enumerate(img_paths): + temp = TEMPERATURE_RANGE[0] + index / (num_images - 1) * ( + TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] + ) + try: + row = (temp, extract_wells_intensity(image_path, mask_path)) + measures.append(row) + except Exception as e: # noqa + raise IntensityExtractionError( + f"could not process image extracted intensity for image : {index+1}/{num_images}", + ) from e + logger.info(f"extracted intensity for image : {index+1}/{num_images}") + + # build header + # check the first plate for number of wells + nb_wells = len(measures[0][1]) + plate_size = PlateSize(nb_wells) + plate_row = range(PLATE_DIMS[plate_size][0]) + plate_col = range(PLATE_DIMS[plate_size][1]) + plate_coords = itertools.product(plate_row, plate_col) + + header = ["Temperature"] + [ + alphanumeric_row(row, col, plate_size) for row, col in plate_coords + ] + + # build dataframe + # roundup temperature + rows = [[round(measure[0], 1), *measure[1]] for measure in measures] + df = pandas.DataFrame(rows, columns=header) + + # Set the temperature as the index + df.set_index("Temperature", inplace=True) + + # Sort the rows by temperature + df.sort_index(inplace=True) + + return df + + +def extract_wells_intensity( image_path: pathlib.Path, mask_path: pathlib.Path, ) -> list[int]: """Extract well intensities from RT_CETSA image and mask. Args: - image_path: Path to the RT_CETSA image. - mask_path: Path to the mask image. + image_path: Path to the RT_CETSA well plate image. + mask_path: Path to the corresponding mask image. Returns: - Pandas DataFrame with well intensities. + mean intensity for each wells. """ with bfio.BioReader(image_path) as reader: image = reader[:] @@ -59,102 +128,48 @@ def extract_intensities( max_mask_index = numpy.max(mask) intensities = [] - for i in range(1, max_mask_index + 1): - current_mask = mask == i - image[current_mask] - # find a square bounding box around the mask + for mask_label in range(1, max_mask_index + 1): + # retrieve a bounding box around each well. + # NOTE this was originally much faster when relying on well positions. + current_mask = mask == mask_label + image[current_mask] bbox = numpy.argwhere(current_mask) bbox_x_min = numpy.min(bbox[0]) bbox_x_max = numpy.max(bbox[0]) bbox_y_min = numpy.min(bbox[1]) bbox_y_max = numpy.max(bbox[1]) + # compute corrected intensity patch = image[bbox_y_min:bbox_y_max, bbox_x_min:bbox_x_max] background = patch.ravel() background.sort() + # Subtract lowest pixel values from average pixel values corrected_mean_intensity = int( np.mean(patch) - np.mean(background[: int(0.05 * background.size)]), ) - # Subtract lowest pixel values from average pixel values intensities.append(corrected_mean_intensity) return intensities -def index_to_battleship(x: int, y: int, size: PlateSize) -> str: - """Get the battleship notation of a well index. - - Args: - x: x-position of the well centerpoint - y: y-position of the well centerpoint - size: size of the plate - - Returns: - str: The string representation of the well index (i.e. A1) +def alphanumeric_row(row: int, col: int, size: PlateSize) -> str: + """Return alphanumeric row: + For well plate size < 96, coordinates range from A1 to H12 + For well plate size >= 96, coordinates range from A01 to P24 + For well plate size 1536, coordinates range from A01 to AF48 """ - # The y-position should be converted to an uppercase well letter - row = "" - if y >= 26: - row = "A" - row = row + string.ascii_uppercase[y % 26] - - return f"{row}{x + 1:02d}" if size.value >= 96 else f"{row}{x + 1}" - - -def build_df( - file_paths: list[tuple[pathlib.Path, pathlib.Path]], -) -> pandas.DataFrame: - """Build a DataFrame with well intensities. - - Args: - file_paths: List of tuples with image and mask paths. - - Returns: - Pandas DataFrame with well intensities. - """ - intensities: list[tuple[float, list[int]]] = [] - - if not ADD_TEMP: - raise NotImplementedError - - if len(file_paths) < 1: - raise ValueError( - "provide at least 2 images on the temperature interval" - + f"{TEMPERATURE_RANGE[0]}-{TEMPERATURE_RANGE[1]}", - ) - - for i, (image_path, mask_path) in enumerate(file_paths): - temp = TEMPERATURE_RANGE[0] + i / (len(file_paths) - 1) * ( - TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] - ) - row = (temp, extract_intensities(image_path, mask_path)) - intensities.append(row) - - # sort intensities by temperature - intensities.sort(key=lambda x: x[0]) - - # check the first plate for number of wells - nb_wells = len(intensities[0][1]) - plate_size = PlateSize(nb_wells) - - # build header - header = ["Temperature"] - plate_row = range(PLATE_DIMS[plate_size][0]) - plate_col = range(PLATE_DIMS[plate_size][1]) - - for y, x in itertools.product(plate_row, plate_col): - header.append(index_to_battleship(x, y, plate_size)) - - # build DataFrame - rows = [[round(measure[0], 1), *measure[1]] for measure in intensities] - df = pandas.DataFrame(rows, columns=header) - - # Set the temperature as the index - df.set_index("Temperature", inplace=True) - - # Sort the roxws by temperature - df.sort_index(inplace=True) - - return df + num_row = PLATE_DIMS[size][0] + num_col = PLATE_DIMS[size][1] + if row >= num_row or col >= num_col: + msg = f"({row},{col}) out of range for plate size {size.value}[{num_row},{num_col}]" + raise ValueError(msg) + + row_alpha = "" + if row > 26: + row_alpha = "A" + row -= 26 + row_alpha = row_alpha + string.ascii_uppercase[row % 26] + + return f"{row_alpha}{col+1:02d}" if size.value >= 96 else f"{row_alpha}{col+1}" diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index b7358cd4a..346d789da 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -7,14 +7,14 @@ import filepattern import typer -from polus.images.features.rt_cetsa_intensity_extraction import build_df +from polus.images.features.rt_cetsa_intensity_extraction import extract_signal # Initialize the logger logging.basicConfig( format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", datefmt="%d-%b-%y %H:%M:%S", ) -logger = logging.getLogger("polus.images.features.rt_cetsa_intensity_extraction") +logger = logging.getLogger(__file__) logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") @@ -27,12 +27,20 @@ def main( inp_dir: pathlib.Path = typer.Option( ..., "--inpDir", - help="Input directory containing the data files.", + help="Input directory containing the well plate images.", exists=True, dir_okay=True, readable=True, resolve_path=True, ), + mask: pathlib.Path = typer.Option( + ..., + "--mask", + help="Path of the wells mask.", + exists=True, + readable=True, + resolve_path=True, + ), pattern: str = typer.Option( ".+", "--filePattern", @@ -55,38 +63,28 @@ def main( ) -> None: """CLI for rt-cetsa-intensity-extraction-tool.""" logger.info("Starting the CLI for rt-cetsa-v-extraction-tool.") - logger.info(f"Input directory: {inp_dir}") logger.info(f"File Pattern: {pattern}") logger.info(f"Output directory: {out_dir}") - images_dir = inp_dir / "images" - masks_dir = inp_dir / "masks" - if not images_dir.exists(): - raise FileNotFoundError(f"Images directory does not exist: {images_dir}") - if not masks_dir.exists(): - raise FileNotFoundError(f"Masks directory does not exist: {masks_dir}") + if (inp_dir / "images").exists(): + inp_dir = inp_dir / "images" + logger.info(f"Using images subdirectory: {inp_dir}") - fp = filepattern.FilePattern(images_dir, pattern) + fp = filepattern.FilePattern(inp_dir, pattern) img_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] - mask_files: list[pathlib.Path] = [masks_dir / f.name for f in img_files] # type: ignore[assignment] - - for f in mask_files: - if not f.exists(): - raise FileNotFoundError(f"Mask file does not exist: {f}") - row_files = list(zip(img_files, mask_files)) + vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) + out_filename = f"plate_({vals[0]}-{vals[-1]}).csv" if preview: - vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) - out_json = {"files": [f"plate_({vals[0]}-{vals[-1]}).csv"]} - # TODO check mypy complains + out_json = {"files": [out_filename]} with (out_dir / "preview.json").open("w") as f: # type: ignore[assignment] json.dump(out_json, f, indent=2) # type: ignore return - df = build_df(row_files) - df.to_csv(out_dir / "plate.csv") + df = extract_signal(img_files, mask) + df.to_csv(out_dir / out_filename) if __name__ == "__main__": diff --git a/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py b/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py new file mode 100644 index 000000000..03ead17e3 --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py @@ -0,0 +1,28 @@ +"""Test plate coordinates.""" + +from polus.images.features.rt_cetsa_intensity_extraction import PlateSize +from polus.images.features.rt_cetsa_intensity_extraction import alphanumeric_row + + +def test_alphanumeric_row_48(): + """Test plate coordinates for plate size 48.""" + assert alphanumeric_row(0, 0, PlateSize(48)) == "A1" + assert alphanumeric_row(5, 7, PlateSize(48)) == "F8" + + +def test_alphanumeric_row_96(): + """Test plate coordinates for plate size 96.""" + assert alphanumeric_row(0, 0, PlateSize(96)) == "A01" + assert alphanumeric_row(7, 11, PlateSize(96)) == "H12" + + +def test_alphanumeric_row_384(): + """Test plate coordinates for plate size 384.""" + assert alphanumeric_row(0, 0, PlateSize(384)) == "A01" + assert alphanumeric_row(15, 23, PlateSize(384)) == "P24" + + +def test_alphanumeric_row_1536(): + """Test plate coordinates for plate size 1536.""" + assert alphanumeric_row(0, 0, PlateSize(1536)) == "A01" + assert alphanumeric_row(31, 47, PlateSize(1536)) == "AF48" diff --git a/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py b/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py deleted file mode 100644 index 2d0c1a287..000000000 --- a/features/rt-cetsa-intensity-extraction-tool/tests/test_battleship_coordinates.py +++ /dev/null @@ -1,7 +0,0 @@ -from polus.images.features.rt_cetsa_intensity_extraction import PlateSize -from polus.images.features.rt_cetsa_intensity_extraction import index_to_battleship - - -def test_battleship_coordinates(): - assert index_to_battleship(0, 0, PlateSize(96)) == "A01" - assert index_to_battleship(12, 8, PlateSize(96)) == "H12" From 7657bde0082fe5b5594f61d70f650a08492e89f8 Mon Sep 17 00:00:00 2001 From: agerardin Date: Mon, 13 May 2024 07:04:46 -0400 Subject: [PATCH 06/26] fix: rt_cetsa segmentation. --- .../rt_cetsa_plate_extraction/__init__.py | 68 ++++++++++++++++++- .../rt_cetsa_plate_extraction/__main__.py | 60 +++++----------- .../rt_cetsa_plate_extraction/core.py | 52 ++++++-------- 3 files changed, 105 insertions(+), 75 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index dc50c00aa..00d997f75 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -1,5 +1,69 @@ """RT_CETSA Plate Extraction Tool.""" - __version__ = "0.1.0" -from polus.images.segmentation.rt_cetsa_plate_extraction.core import extract_plate +import logging +import os +from pathlib import Path + +import bfio +import filepattern +import tifffile +from polus.images.segmentation.rt_cetsa_plate_extraction.core import PlateParams +from polus.images.segmentation.rt_cetsa_plate_extraction.core import create_mask +from polus.images.segmentation.rt_cetsa_plate_extraction.core import crop_and_rotate +from polus.images.segmentation.rt_cetsa_plate_extraction.core import get_plate_params + +# Initialize the logger +logging.basicConfig( + format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", +) +logger = logging.getLogger(__file__) +logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) + +POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") + + +def extract_plates(inp_dir, pattern, out_dir): + """Preprocess RT_cetsa images. + + Using the first plate image, determine plate params and create a plate mask. + Then crop and rotate all RT_cetsa images. + """ + fp = filepattern.FilePattern(inp_dir, pattern) + inp_files: list[Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + + if len(inp_files) < 1: + msg = "no input files captured by the pattern." + raise ValueError(msg) + + (out_dir / "images").mkdir(parents=False, exist_ok=True) + (out_dir / "masks").mkdir(parents=False, exist_ok=True) + + # extract plate params from first image + first_image_path = inp_files[0] + # TODO Switch to bfio + first_image = tifffile.imread(first_image_path) + params: PlateParams = get_plate_params(first_image) + logger.info(f"Processing plate of size: {params.size.value}") + + # extract mask from first image + mask = create_mask(params) + mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) + with bfio.BioWriter(mask_path) as writer: + writer.dtype = mask.dtype + writer.shape = mask.shape + writer[:] = mask + logger.info(f"Generate plate mask: {mask_path}") + + # crop and rotate each image + num_images = len(inp_files) + for index, f in enumerate(inp_files): + logger.info(f"Processing Image {index}/{num_images}: {f}") + image = tifffile.imread(f) + cropped_and_rotated = crop_and_rotate(image, params) + out_name = f.stem + POLUS_IMG_EXT + with bfio.BioWriter(out_dir / "images" / out_name) as writer: + writer.dtype = cropped_and_rotated.dtype + writer.shape = cropped_and_rotated.shape + writer[:] = cropped_and_rotated diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py index 82d3accb7..d0cac0306 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -3,22 +3,18 @@ import json import logging import os -import pathlib +from pathlib import Path -import bfio import filepattern import typer -from polus.images.segmentation.rt_cetsa_plate_extraction.core import ( - PlateExtractionError, -) -from polus.images.segmentation.rt_cetsa_plate_extraction.core import extract_plate +from polus.images.segmentation.rt_cetsa_plate_extraction import extract_plates # Initialize the logger logging.basicConfig( format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", datefmt="%d-%b-%y %H:%M:%S", ) -logger = logging.getLogger("polus.images.segmentation.rt_cetsa_plate_extraction") +logger = logging.getLogger(__file__) logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") @@ -28,7 +24,7 @@ @app.command() def main( - inp_dir: pathlib.Path = typer.Option( + inp_dir: Path = typer.Option( ..., "--inpDir", help="Input directory containing the plate images.", @@ -47,7 +43,7 @@ def main( "--preview", help="Preview the files that will be processed.", ), - out_dir: pathlib.Path = typer.Option( + out_dir: Path = typer.Option( ..., "--outDir", help="Output directory to save the results.", @@ -64,51 +60,29 @@ def main( logger.info(f"File Pattern: {pattern}") logger.info(f"Output directory: {out_dir}") - fp = filepattern.FilePattern(inp_dir, pattern) - inp_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] - if preview: + fp = filepattern.FilePattern(inp_dir, pattern) + inp_files: list[Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + + if len(inp_files) < 1: + msg = "no input files captured by the pattern." + raise ValueError(msg) + out_json = { "images": [ - (out_dir / "images" / f"{f.stem}{POLUS_IMG_EXT}").as_posix() + (Path("images") / f"{f.stem}{POLUS_IMG_EXT}").as_posix() for f in inp_files ], "masks": [ - (out_dir / "masks" / f"{f.stem}{POLUS_IMG_EXT}").as_posix() - for f in inp_files + (Path("masks") / f"{inp_files[0].stem}{POLUS_IMG_EXT}").as_posix(), ], } + with (out_dir / "preview.json").open("w") as f: json.dump(out_json, f, indent=2) - return - - (out_dir / "images").mkdir(parents=False, exist_ok=True) - (out_dir / "masks").mkdir(parents=False, exist_ok=True) - - failed_detections = [] - - for f in inp_files: # type: ignore[assignment] - logger.info(f"Processing file: {f}") - try: - image, mask = extract_plate(f) - out_name = f.stem + POLUS_IMG_EXT # type: ignore[attr-defined] - with bfio.BioWriter(out_dir / "images" / out_name) as writer: - writer.dtype = image.dtype - writer.shape = image.shape - writer[:] = image - with bfio.BioWriter(out_dir / "masks" / out_name) as writer: - writer.dtype = mask.dtype - writer.shape = mask.shape - writer[:] = mask - except ValueError as e: - logger.error(e) - failed_detections.append(f) - if failed_detections: - filenames = [filepath.name for filepath in failed_detections] - raise PlateExtractionError( - f"{len(failed_detections)} plates could be processed sucessfully: {filenames}", - ) + else: + extract_plates(inp_dir, pattern, out_dir) if __name__ == "__main__": diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py index 0b9f9ce27..287017c7b 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py @@ -1,9 +1,7 @@ import itertools -import pathlib from enum import Enum import numpy as np -import tifffile from pydantic import BaseModel from scipy import ndimage as ndi from skimage.draw import disk @@ -15,34 +13,6 @@ class PlateExtractionError(Exception): """Raised if the plate could not be processed successfully.""" -def extract_plate(file_path: pathlib.Path) -> tuple[np.ndarray, np.ndarray]: - """Extract wells from an RT_CETSA plate image. - - Args: - file_path: Path to the image file. - - Returns: - Tuple containing the crop and rotated image and the mask of detected wells. - """ - # TODO replace by bfio - image = tifffile.imread(file_path) - - params = get_plate_params(image) - crop_and_rotated_image = rotate(image, params.rotate, preserve_range=True)[ - params.bbox[0] : params.bbox[1], - params.bbox[2] : params.bbox[3], - ].astype(image.dtype) - - wells_mask = np.zeros_like(crop_and_rotated_image, dtype=np.uint16) - - for mask_label, (x, y) in enumerate(itertools.product(params.X, params.Y), start=1): - x_crop, y_crop = (x - params.bbox[2], y - params.bbox[0]) - rr, cc = disk((y_crop, x_crop), params.radius) - wells_mask[rr, cc] = mask_label - - return crop_and_rotated_image, wells_mask - - class PlateSize(Enum): """Common Plate Sizes.""" @@ -96,6 +66,28 @@ class PlateParams(BaseModel): """The the y axis points for wells.""" +def crop_and_rotate(image: np.ndarray, params: PlateParams): + """Crop and rotate image according to plate params.""" + return rotate(image, params.rotate, preserve_range=True)[ + params.bbox[0] : params.bbox[1], + params.bbox[2] : params.bbox[3], + ].astype(image.dtype) + + +def create_mask(params: PlateParams): + """Create a mask for all wells given the plate parameters.""" + width = params.bbox[3] - params.bbox[2] + heigth = params.bbox[1] - params.bbox[0] + wells_mask = np.zeros((heigth, width), dtype=np.uint16) + + for mask_label, (y, x) in enumerate(itertools.product(params.Y, params.X), start=1): + x_crop, y_crop = (x - params.bbox[2], y - params.bbox[0]) + rr, cc = disk((y_crop, x_crop), params.radius) + wells_mask[rr, cc] = mask_label + + return wells_mask + + def get_plate_params(image: np.ndarray) -> PlateParams: """Detect wells in the image plate. From f11ce9d4d031a81c2a34a775106a92cfa0ade782 Mon Sep 17 00:00:00 2001 From: agerardin Date: Mon, 13 May 2024 07:33:58 -0400 Subject: [PATCH 07/26] fix: intensity extraction. Filepattern provides lexicographic ordering. feat: containerized plate extraction and run with cwl. --- .../rt_cetsa_intensity_extraction/__main__.py | 5 ++- .../rt-cetsa-plate-extraction-tool/Dockerfile | 4 +-- .../build-docker.sh | 4 +++ .../rt-cetsa-plate-extraction-tool/ict.yml | 2 +- .../plugin.json | 8 ++++- .../rt_cetsa_plate_extraction.cwl | 32 +++++++++++++++++++ 6 files changed, 50 insertions(+), 5 deletions(-) create mode 100755 segmentation/rt-cetsa-plate-extraction-tool/build-docker.sh create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/rt_cetsa_plate_extraction.cwl diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index 346d789da..852c15f26 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -72,7 +72,10 @@ def main( logger.info(f"Using images subdirectory: {inp_dir}") fp = filepattern.FilePattern(inp_dir, pattern) - img_files: list[pathlib.Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + print(*[f[0] for f in fp()]) + + sorted_fp = sorted(fp, key=lambda f: f[0]["index"]) + img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) out_filename = f"plate_({vals[0]}-{vals[-1]}).csv" diff --git a/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile b/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile index 111df1dc8..d63c46878 100755 --- a/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile +++ b/segmentation/rt-cetsa-plate-extraction-tool/Dockerfile @@ -1,9 +1,9 @@ -FROM polusai/bfio:2.3.6 +FROM polusai/bfio:2.1.9 # environment variables defined in polusai/bfio ENV EXEC_DIR="/opt/executables" ENV POLUS_IMG_EXT=".ome.tif" -ENV POLUS_TAB_EXT=".arrow" +ENV POLUS_TAB_EXT=".csv" ENV POLUS_LOG="INFO" # Work directory defined in the base container diff --git a/segmentation/rt-cetsa-plate-extraction-tool/build-docker.sh b/segmentation/rt-cetsa-plate-extraction-tool/build-docker.sh new file mode 100755 index 000000000..84a7ebe79 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/build-docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +version=$( Date: Tue, 14 May 2024 05:34:11 -0400 Subject: [PATCH 08/26] feat: containerized and make intensity extraction runnable in a cwl workflow. --- .../Dockerfile | 2 +- .../build-docker.sh | 4 ++ .../ict.yml | 7 ++++ .../plugin.json | 35 ++++++++++++---- .../pyproject.toml | 1 + .../rt_cetsa_intensity_extraction.cwl | 36 ++++++++++++++++ .../run-plugin.sh | 4 +- .../rt_cetsa_intensity_extraction/__main__.py | 42 +++++++++++-------- 8 files changed, 102 insertions(+), 29 deletions(-) create mode 100755 features/rt-cetsa-intensity-extraction-tool/build-docker.sh create mode 100644 features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl diff --git a/features/rt-cetsa-intensity-extraction-tool/Dockerfile b/features/rt-cetsa-intensity-extraction-tool/Dockerfile index 763221bd5..1eb75067f 100755 --- a/features/rt-cetsa-intensity-extraction-tool/Dockerfile +++ b/features/rt-cetsa-intensity-extraction-tool/Dockerfile @@ -1,4 +1,4 @@ -FROM polusai/bfio:2.3.6 +FROM polusai/bfio:2.1.9 # environment variables defined in polusai/bfio ENV EXEC_DIR="/opt/executables" diff --git a/features/rt-cetsa-intensity-extraction-tool/build-docker.sh b/features/rt-cetsa-intensity-extraction-tool/build-docker.sh new file mode 100755 index 000000000..44ec5d12b --- /dev/null +++ b/features/rt-cetsa-intensity-extraction-tool/build-docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +version=$( Date: Sun, 19 May 2024 03:26:22 -0400 Subject: [PATCH 09/26] feat: save plate params when extracting plates. --- .../segmentation/rt_cetsa_plate_extraction/__init__.py | 10 +++++++++- .../segmentation/rt_cetsa_plate_extraction/__main__.py | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index 00d997f75..9df554676 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -24,7 +24,7 @@ POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") -def extract_plates(inp_dir, pattern, out_dir): +def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: """Preprocess RT_cetsa images. Using the first plate image, determine plate params and create a plate mask. @@ -39,6 +39,7 @@ def extract_plates(inp_dir, pattern, out_dir): (out_dir / "images").mkdir(parents=False, exist_ok=True) (out_dir / "masks").mkdir(parents=False, exist_ok=True) + (out_dir / "params").mkdir(parents=False, exist_ok=True) # extract plate params from first image first_image_path = inp_files[0] @@ -47,6 +48,11 @@ def extract_plates(inp_dir, pattern, out_dir): params: PlateParams = get_plate_params(first_image) logger.info(f"Processing plate of size: {params.size.value}") + # save plate parameters + plate_path = out_dir / "params" / "plate.csv" + with plate_path.open("w") as f: + f.write(params.model_dump_json()) + # extract mask from first image mask = create_mask(params) mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) @@ -67,3 +73,5 @@ def extract_plates(inp_dir, pattern, out_dir): writer.dtype = cropped_and_rotated.dtype writer.shape = cropped_and_rotated.shape writer[:] = cropped_and_rotated + + return params diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py index d0cac0306..e2ffe74ac 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -76,6 +76,7 @@ def main( "masks": [ (Path("masks") / f"{inp_files[0].stem}{POLUS_IMG_EXT}").as_posix(), ], + "params": [Path("params") / "plate.csv"], } with (out_dir / "preview.json").open("w") as f: From 2550cdf81103da6c68f6ef1d8f387a98810860aa Mon Sep 17 00:00:00 2001 From: agerardin Date: Sun, 19 May 2024 03:27:04 -0400 Subject: [PATCH 10/26] dev: faster intensity extraction. --- .../pyproject.toml | 1 + .../rt_cetsa_intensity_extraction/__init__.py | 129 +++++++++++++++--- .../rt_cetsa_intensity_extraction/__main__.py | 30 ++-- .../tests/test_alphanumeric_row.py | 21 +-- 4 files changed, 141 insertions(+), 40 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index b0f9f7639..7c1b020fe 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -19,6 +19,7 @@ numpy = "^1.26.4" bfio = "^2.3.6" scikit-image = "^0.22.0" imagecodecs = "^2024.1.1" +polus-tabular-regression-rt-cetsa-moltenprot = {path = "/Users/antoinegerardin/Documents/projects/tabular-tools/regression/rt-cetsa-moltenprot-tool"} [tool.poetry.group.dev.dependencies] bump2version = "^1.0.1" diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 35c2aee6b..6a832646e 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -11,6 +11,8 @@ import numpy import numpy as np import pandas +from pydantic import BaseModel +from pydantic_core import from_json logger = logging.getLogger(__file__) logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) @@ -23,6 +25,7 @@ class IntensityExtractionError(Exception): pass +# TODO REMOVE Duplicate : This will cause problems when using plugins together class PlateSize(Enum): SIZE_6 = 6 SIZE_12 = 12 @@ -33,6 +36,7 @@ class PlateSize(Enum): SIZE_1536 = 1536 +# TODO REMOVE Duplicate : This will cause problems when using plugins together PLATE_DIMS = { PlateSize.SIZE_6: (2, 3), PlateSize.SIZE_12: (3, 4), @@ -44,6 +48,26 @@ class PlateSize(Enum): } +class PlateParams(BaseModel): + rotate: int + """Counterclockwise rotation of image in degrees.""" + + bbox: tuple[int, int, int, int] + """Bounding box of plate after rotation, [ymin,ymax,xmin,xmax].""" + + size: PlateSize + """The plate size, also determines layout.""" + + radius: int + """Well radius.""" + + X: list[int] + """The the x axis points for wells.""" + + Y: list[int] + """The the y axis points for wells.""" + + def extract_signal( img_paths: list[pathlib.Path], mask_path: pathlib.Path, @@ -74,7 +98,7 @@ def extract_signal( TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] ) try: - row = (temp, extract_wells_intensity(image_path, mask_path)) + row = (temp, extract_wells_intensity_fast(image_path, mask_path)) measures.append(row) except Exception as e: # noqa raise IntensityExtractionError( @@ -86,12 +110,17 @@ def extract_signal( # check the first plate for number of wells nb_wells = len(measures[0][1]) plate_size = PlateSize(nb_wells) - plate_row = range(PLATE_DIMS[plate_size][0]) - plate_col = range(PLATE_DIMS[plate_size][1]) - plate_coords = itertools.product(plate_row, plate_col) + plate_row_count = range(PLATE_DIMS[plate_size][0]) + plate_col_count = range(PLATE_DIMS[plate_size][1]) + plate_coords = itertools.product(plate_row_count, plate_col_count) header = ["Temperature"] + [ - alphanumeric_row(row, col, plate_size) for row, col in plate_coords + alphanumeric_row( + row, + col, + (PLATE_DIMS[plate_size][0], PLATE_DIMS[plate_size][1]), + ) + for row, col in plate_coords ] # build dataframe @@ -108,6 +137,60 @@ def extract_signal( return df +def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: + """Get the well intensity + + Args: + image: _description_ + x: x-position of the well centerpoint + y: y-position of the well centerpoint + r: radius of the well + + Returns: + int: The background corrected mean well intensity + """ + assert r >= 5 + + # get a large patch to find background pixels + x_min = max(x - r, 0) + x_max = min(x + r, image.shape[1]) + y_min = max(y - r, 0) + y_max = min(y + r, image.shape[0]) + patch = image[y_min:y_max, x_min:x_max] + background = patch.ravel() + background.sort() + + # Subtract lowest pixel values from average center pixel values + return int(np.mean(patch) - np.median(background[: int(0.05 * background.size)])) + + +def extract_wells_intensity_fast( + image_path: pathlib.Path, + mask_path: pathlib.Path, +) -> list[int]: + """Extract well intensities from RT_CETSA image and mask. + + Args: + image_path: Path to the RT_CETSA well plate image. + mask_path: Path to the corresponding params file. + + Returns: + mean intensity for each wells. + """ + with bfio.BioReader(image_path) as reader: + image = reader[:] + with mask_path.open("r") as f: + params = PlateParams(**from_json(f.read())) + + intensities = [] + for y, x in itertools.product(range(len(params.Y)), range(len(params.X))): + intensity = extract_intensity(image, params.X[x], params.Y[y], params.radius) + print(intensity) + intensities.append(intensity) + + return intensities + + def extract_wells_intensity( image_path: pathlib.Path, mask_path: pathlib.Path, @@ -140,30 +223,40 @@ def extract_wells_intensity( bbox_y_min = numpy.min(bbox[1]) bbox_y_max = numpy.max(bbox[1]) - # compute corrected intensity - patch = image[bbox_y_min:bbox_y_max, bbox_x_min:bbox_x_max] - background = patch.ravel() - background.sort() - # Subtract lowest pixel values from average pixel values - corrected_mean_intensity = int( - np.mean(patch) - np.mean(background[: int(0.05 * background.size)]), + intensity = compute_well_intensity( + image, + bbox_y_min, + bbox_y_max, + bbox_x_min, + bbox_x_max, ) - intensities.append(corrected_mean_intensity) + intensities.append(intensity) return intensities -def alphanumeric_row(row: int, col: int, size: PlateSize) -> str: +def compute_well_intensity(image, bbox_y_min, bbox_y_max, bbox_x_min, bbox_x_max): + # compute corrected intensity + patch = image[bbox_y_min:bbox_y_max, bbox_x_min:bbox_x_max] + background = patch.ravel() + background.sort() + # Subtract lowest pixel values from average pixel values + return int( + np.mean(patch) - np.mean(background[: int(0.05 * background.size)]), + ) + + +def alphanumeric_row(row: int, col: int, dims: tuple[int, int]) -> str: """Return alphanumeric row: For well plate size < 96, coordinates range from A1 to H12 For well plate size >= 96, coordinates range from A01 to P24 For well plate size 1536, coordinates range from A01 to AF48 """ - num_row = PLATE_DIMS[size][0] - num_col = PLATE_DIMS[size][1] + num_row, num_col = dims + size = num_row * num_col if row >= num_row or col >= num_col: - msg = f"({row},{col}) out of range for plate size {size.value}[{num_row},{num_col}]" + msg = f"({row},{col}) out of range for plate size {size}[{num_row},{num_col}]" raise ValueError(msg) row_alpha = "" @@ -172,4 +265,4 @@ def alphanumeric_row(row: int, col: int, size: PlateSize) -> str: row -= 26 row_alpha = row_alpha + string.ascii_uppercase[row % 26] - return f"{row_alpha}{col+1:02d}" if size.value >= 96 else f"{row_alpha}{col+1}" + return f"{row_alpha}{col+1:02d}" if size >= 96 else f"{row_alpha}{col+1}" diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index 08d261272..fc95b8028 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -33,7 +33,7 @@ def main( readable=True, resolve_path=True, ), - mask: str = typer.Option(None, "--mask", help="plate mask filename."), + params: str = typer.Option(None, "--params", help="plate params filename."), filePattern: str = typer.Option( ".+", "--filePattern", @@ -57,7 +57,6 @@ def main( """CLI for rt-cetsa-intensity-extraction-tool.""" logger.info("Starting the CLI for rt-cetsa-v-extraction-tool.") logger.info(f"Input directory: {inp_dir}") - logger.info(f"Mask filename: {mask}") logger.info(f"File Pattern: {filePattern}") logger.info(f"Output directory: {out_dir}") @@ -66,19 +65,22 @@ def main( img_dir = inp_dir / "images" logger.info(f"Using images subdirectory: {img_dir}") - if not (inp_dir / "masks").exists(): - raise FileNotFoundError(f"no masks subdirectory found in: {inp_dir}") + if params and not (inp_dir / "params").exists(): + raise FileNotFoundError(f"no params subdirectory found in: {inp_dir}") - mask_dir = inp_dir / "masks" - if mask: - mask_file = mask_dir / mask - if not mask_file.exists(): - raise FileNotFoundError(f"file {mask} does not exist in: {mask_dir}") + # Try the get params file + params_dir = inp_dir / "params" + if params: + params_file = params_dir / params + if not params_file.exists(): + raise FileNotFoundError(f"file {params} does not exist in: {params_dir}") else: - if len(list(mask_dir.iterdir())) != 1: - raise FileExistsError(f"There should be a single mask in {mask_dir}") - mask_file = next(mask_dir.iterdir()) - logger.info(f"Using mask: {mask_file}") + if len(list(params_dir.iterdir())) != 1: + raise FileExistsError( + f"There should be a single plate params file in {params_dir}", + ) + params_file = next(params_dir.iterdir()) + logger.info(f"Using plate params: {params_file}") fp = filepattern.FilePattern(img_dir, filePattern) @@ -94,7 +96,7 @@ def main( json.dump(out_json, f, indent=2) # type: ignore return - df = extract_signal(img_files, mask_file) + df = extract_signal(img_files, params_file) df.to_csv(out_dir / out_filename) diff --git a/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py b/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py index 03ead17e3..ce2bd9306 100644 --- a/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py +++ b/features/rt-cetsa-intensity-extraction-tool/tests/test_alphanumeric_row.py @@ -1,28 +1,33 @@ """Test plate coordinates.""" +from polus.images.features.rt_cetsa_intensity_extraction import PLATE_DIMS from polus.images.features.rt_cetsa_intensity_extraction import PlateSize from polus.images.features.rt_cetsa_intensity_extraction import alphanumeric_row def test_alphanumeric_row_48(): """Test plate coordinates for plate size 48.""" - assert alphanumeric_row(0, 0, PlateSize(48)) == "A1" - assert alphanumeric_row(5, 7, PlateSize(48)) == "F8" + rows, cols = PLATE_DIMS[PlateSize(48)] + assert alphanumeric_row(0, 0, (rows, cols)) == "A1" + assert alphanumeric_row(5, 7, (rows, cols)) == "F8" def test_alphanumeric_row_96(): """Test plate coordinates for plate size 96.""" - assert alphanumeric_row(0, 0, PlateSize(96)) == "A01" - assert alphanumeric_row(7, 11, PlateSize(96)) == "H12" + rows, cols = PLATE_DIMS[PlateSize(96)] + assert alphanumeric_row(0, 0, (rows, cols)) == "A01" + assert alphanumeric_row(7, 11, (rows, cols)) == "H12" def test_alphanumeric_row_384(): """Test plate coordinates for plate size 384.""" - assert alphanumeric_row(0, 0, PlateSize(384)) == "A01" - assert alphanumeric_row(15, 23, PlateSize(384)) == "P24" + rows, cols = PLATE_DIMS[PlateSize(384)] + assert alphanumeric_row(0, 0, (rows, cols)) == "A01" + assert alphanumeric_row(15, 23, (rows, cols)) == "P24" def test_alphanumeric_row_1536(): """Test plate coordinates for plate size 1536.""" - assert alphanumeric_row(0, 0, PlateSize(1536)) == "A01" - assert alphanumeric_row(31, 47, PlateSize(1536)) == "AF48" + rows, cols = PLATE_DIMS[PlateSize(1536)] + assert alphanumeric_row(0, 0, (rows, cols)) == "A01" + assert alphanumeric_row(31, 47, (rows, cols)) == "AF48" From ad82637e8480778a65461cbb0ca9d487716386b6 Mon Sep 17 00:00:00 2001 From: agerardin Date: Sun, 19 May 2024 04:23:47 -0400 Subject: [PATCH 11/26] fix: cropped plate params. --- .../rt_cetsa_plate_extraction/__init__.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index 9df554676..c70151013 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -31,7 +31,8 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: Then crop and rotate all RT_cetsa images. """ fp = filepattern.FilePattern(inp_dir, pattern) - inp_files: list[Path] = [f[1][0] for f in fp()] # type: ignore[assignment] + sorted_fp = sorted(fp(), key=lambda f: f[0]["index"]) + inp_files: list[Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] if len(inp_files) < 1: msg = "no input files captured by the pattern." @@ -48,11 +49,6 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: params: PlateParams = get_plate_params(first_image) logger.info(f"Processing plate of size: {params.size.value}") - # save plate parameters - plate_path = out_dir / "params" / "plate.csv" - with plate_path.open("w") as f: - f.write(params.model_dump_json()) - # extract mask from first image mask = create_mask(params) mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) @@ -65,13 +61,23 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: # crop and rotate each image num_images = len(inp_files) for index, f in enumerate(inp_files): - logger.info(f"Processing Image {index}/{num_images}: {f}") + logger.info(f"Processing Image {index+1}/{num_images}: {f}") image = tifffile.imread(f) cropped_and_rotated = crop_and_rotate(image, params) - out_name = f.stem + POLUS_IMG_EXT - with bfio.BioWriter(out_dir / "images" / out_name) as writer: + + if index == 1: + first_image = cropped_and_rotated + + out_path = out_dir / "images" / (f.stem + POLUS_IMG_EXT) + with bfio.BioWriter(out_path) as writer: writer.dtype = cropped_and_rotated.dtype writer.shape = cropped_and_rotated.shape writer[:] = cropped_and_rotated + # save plate parameters for the first processed image. + params = get_plate_params(first_image) + plate_path = out_dir / "params" / "plate.csv" + with plate_path.open("w") as f: + f.write(params.model_dump_json()) # type: ignore + return params From 618cd307da9b48111d5bc0539d5b85aeb15c0f5f Mon Sep 17 00:00:00 2001 From: agerardin Date: Sun, 19 May 2024 04:29:14 -0400 Subject: [PATCH 12/26] fix: intensity extraction: fast implementation based on plate params. --- .../features/rt_cetsa_intensity_extraction/__init__.py | 1 - .../features/rt_cetsa_intensity_extraction/__main__.py | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 6a832646e..d7c86c128 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -185,7 +185,6 @@ def extract_wells_intensity_fast( intensities = [] for y, x in itertools.product(range(len(params.Y)), range(len(params.X))): intensity = extract_intensity(image, params.X[x], params.Y[y], params.radius) - print(intensity) intensities.append(intensity) return intensities diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index fc95b8028..f348e1719 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -82,9 +82,15 @@ def main( params_file = next(params_dir.iterdir()) logger.info(f"Using plate params: {params_file}") + # if not (inp_dir / "masks").exists(): + + # if mask: + # if not mask_file.exists(): + # if len(list(mask_dir.iterdir())) != 1: + fp = filepattern.FilePattern(img_dir, filePattern) - sorted_fp = sorted(fp, key=lambda f: f[0]["index"]) + sorted_fp = sorted(fp(), key=lambda f: f[0]["index"]) img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) From a6a9b288d046fb1d2ec01bc26258ef84384daf6f Mon Sep 17 00:00:00 2001 From: agerardin Date: Mon, 20 May 2024 10:22:43 -0400 Subject: [PATCH 13/26] fix: intensity extraction. Now extract a larger area to get close to original intensities. --- .../rt_cetsa_intensity_extraction/__init__.py | 24 ++++++++++++++----- .../rt_cetsa_intensity_extraction/__main__.py | 6 ----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index d7c86c128..4edc4e471 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -144,23 +144,22 @@ def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: image: _description_ x: x-position of the well centerpoint y: y-position of the well centerpoint - r: radius of the well + r: radius of the circle inscribed in the square area of interest. Returns: int: The background corrected mean well intensity """ - assert r >= 5 - - # get a large patch to find background pixels + # we take a square area around the well center x_min = max(x - r, 0) x_max = min(x + r, image.shape[1]) y_min = max(y - r, 0) y_max = min(y + r, image.shape[0]) + patch = image[y_min:y_max, x_min:x_max] background = patch.ravel() background.sort() - # Subtract lowest pixel values from average center pixel values + # Subtract lowest pixel values from average patch pixel values return int(np.mean(patch) - np.median(background[: int(0.05 * background.size)])) @@ -183,8 +182,21 @@ def extract_wells_intensity_fast( params = PlateParams(**from_json(f.read())) intensities = [] + + # NOTE take half of the smallest distance between wells. + # this is close to what the original matlab code is doing + # and get us close to the value it was computing. + R = min(params.X[1] - params.X[0], params.Y[1] - params.Y[0]) // 2 + + logger.debug( + f""" + Average radius detected : {params.radius} + Radius + """, + ) + for y, x in itertools.product(range(len(params.Y)), range(len(params.X))): - intensity = extract_intensity(image, params.X[x], params.Y[y], params.radius) + intensity = extract_intensity(image, params.X[x], params.Y[y], R) intensities.append(intensity) return intensities diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index f348e1719..df35e4fc1 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -82,12 +82,6 @@ def main( params_file = next(params_dir.iterdir()) logger.info(f"Using plate params: {params_file}") - # if not (inp_dir / "masks").exists(): - - # if mask: - # if not mask_file.exists(): - # if len(list(mask_dir.iterdir())) != 1: - fp = filepattern.FilePattern(img_dir, filePattern) sorted_fp = sorted(fp(), key=lambda f: f[0]["index"]) From 7140088b2f90a5d86fecadab3e98062c79143473 Mon Sep 17 00:00:00 2001 From: agerardin Date: Mon, 20 May 2024 10:47:16 -0400 Subject: [PATCH 14/26] dev: cleanup how segmentation mask is generated. --- .../rt_cetsa_plate_extraction/__init__.py | 30 ++++++++++--------- .../rt_cetsa_plate_extraction/__main__.py | 1 - .../rt_cetsa_plate_extraction/core.py | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index c70151013..9a8f2c9fa 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -13,7 +13,6 @@ from polus.images.segmentation.rt_cetsa_plate_extraction.core import crop_and_rotate from polus.images.segmentation.rt_cetsa_plate_extraction.core import get_plate_params -# Initialize the logger logging.basicConfig( format="%(asctime)s - %(name)-8s - %(levelname)-8s - %(message)s", datefmt="%d-%b-%y %H:%M:%S", @@ -27,7 +26,8 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: """Preprocess RT_cetsa images. - Using the first plate image, determine plate params and create a plate mask. + Using the first plate image, determine plate params. + Create a plate mask and a plate parameters file. Then crop and rotate all RT_cetsa images. """ fp = filepattern.FilePattern(inp_dir, pattern) @@ -44,19 +44,12 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: # extract plate params from first image first_image_path = inp_files[0] + # TODO Switch to bfio first_image = tifffile.imread(first_image_path) params: PlateParams = get_plate_params(first_image) - logger.info(f"Processing plate of size: {params.size.value}") - # extract mask from first image - mask = create_mask(params) - mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) - with bfio.BioWriter(mask_path) as writer: - writer.dtype = mask.dtype - writer.shape = mask.shape - writer[:] = mask - logger.info(f"Generate plate mask: {mask_path}") + logger.info(f"Processing plate of size: {params.size.value}") # crop and rotate each image num_images = len(inp_files) @@ -75,9 +68,18 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: writer[:] = cropped_and_rotated # save plate parameters for the first processed image. - params = get_plate_params(first_image) + processed_params = get_plate_params(first_image) plate_path = out_dir / "params" / "plate.csv" with plate_path.open("w") as f: - f.write(params.model_dump_json()) # type: ignore + f.write(processed_params.model_dump_json()) # type: ignore + + # save the corresponding mask for reference. + mask = create_mask(processed_params) + mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) + with bfio.BioWriter(mask_path) as writer: + writer.dtype = mask.dtype + writer.shape = mask.shape + writer[:] = mask + logger.info(f"Generate plate mask: {mask_path}") - return params + return processed_params diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py index e2ffe74ac..f70c663c1 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -55,7 +55,6 @@ def main( ) -> None: """CLI for rt-cetsa-plate-extraction-tool.""" logger.info("Starting the CLI for rt-cetsa-plate-extraction-tool.") - logger.info(f"Input directory: {inp_dir}") logger.info(f"File Pattern: {pattern}") logger.info(f"Output directory: {out_dir}") diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py index 287017c7b..b68c31ebf 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py @@ -81,7 +81,7 @@ def create_mask(params: PlateParams): wells_mask = np.zeros((heigth, width), dtype=np.uint16) for mask_label, (y, x) in enumerate(itertools.product(params.Y, params.X), start=1): - x_crop, y_crop = (x - params.bbox[2], y - params.bbox[0]) + y_crop, x_crop = (y, x) rr, cc = disk((y_crop, x_crop), params.radius) wells_mask[rr, cc] = mask_label From 9fcde28d9d3793cafab1ae1c465fc35c418f019f Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 00:38:13 -0400 Subject: [PATCH 15/26] dev: replace tifffile by bfio. Stored roi_radius in plate_params. --- .../pyproject.toml | 1 - .../rt_cetsa_plate_extraction/__init__.py | 28 +++++++++---------- .../rt_cetsa_plate_extraction/core.py | 7 +++++ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml index f743182af..ad5e79102 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml +++ b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml @@ -16,7 +16,6 @@ typer = "^0.7.0" filepattern = "^2.0.5" numpy = "^1.26.4" scikit-image = "0.22.0" -tifffile = "^2024.5.3" bfio = "^2.3.6" [tool.poetry.group.dev.dependencies] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index 9a8f2c9fa..8f9a8637b 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -7,7 +7,6 @@ import bfio import filepattern -import tifffile from polus.images.segmentation.rt_cetsa_plate_extraction.core import PlateParams from polus.images.segmentation.rt_cetsa_plate_extraction.core import create_mask from polus.images.segmentation.rt_cetsa_plate_extraction.core import crop_and_rotate @@ -44,9 +43,9 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: # extract plate params from first image first_image_path = inp_files[0] + with bfio.BioReader(first_image_path) as reader: + first_image = reader[:] - # TODO Switch to bfio - first_image = tifffile.imread(first_image_path) params: PlateParams = get_plate_params(first_image) logger.info(f"Processing plate of size: {params.size.value}") @@ -55,25 +54,26 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: num_images = len(inp_files) for index, f in enumerate(inp_files): logger.info(f"Processing Image {index+1}/{num_images}: {f}") - image = tifffile.imread(f) - cropped_and_rotated = crop_and_rotate(image, params) + with bfio.BioReader(f) as reader: + image = reader[:] + cropped_and_rotated = crop_and_rotate(image, params) - if index == 1: - first_image = cropped_and_rotated + if index == 1: + first_image = cropped_and_rotated - out_path = out_dir / "images" / (f.stem + POLUS_IMG_EXT) - with bfio.BioWriter(out_path) as writer: - writer.dtype = cropped_and_rotated.dtype - writer.shape = cropped_and_rotated.shape - writer[:] = cropped_and_rotated + out_path = out_dir / "images" / (f.stem + POLUS_IMG_EXT) + with bfio.BioWriter(out_path) as writer: + writer.dtype = cropped_and_rotated.dtype + writer.shape = cropped_and_rotated.shape + writer[:] = cropped_and_rotated - # save plate parameters for the first processed image. + # save plate parameters for the first processed image processed_params = get_plate_params(first_image) plate_path = out_dir / "params" / "plate.csv" with plate_path.open("w") as f: f.write(processed_params.model_dump_json()) # type: ignore - # save the corresponding mask for reference. + # save the corresponding mask for reference mask = create_mask(processed_params) mask_path = out_dir / "masks" / (first_image_path.stem + POLUS_IMG_EXT) with bfio.BioWriter(mask_path) as writer: diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py index b68c31ebf..8bf9ae4e3 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/core.py @@ -59,6 +59,9 @@ class PlateParams(BaseModel): radius: int """Well radius.""" + roi_radius: int + """Radius of the region of interest.""" + X: list[int] """The the x axis points for wells.""" @@ -165,10 +168,14 @@ def get_plate_params(image: np.ndarray) -> PlateParams: Y = points[0] X = points[1] + # estimated distance from the well center we need to consider + roi_radius = min(X[1] - X[0], Y[1] - Y[0]) // 2 + return PlateParams( rotate=angle, size=plate_config, radius=int(radii_mean), + roi_radius=int(roi_radius), bbox=bbox, X=X, Y=Y, From d813629dc70395ece9fe805ed020745d7fb311cd Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 01:34:36 -0400 Subject: [PATCH 16/26] dev: improve cli. Sorting now available from package methods. --- .../rt_cetsa_intensity_extraction/__init__.py | 51 +++++++++++++++++-- .../rt_cetsa_intensity_extraction/__main__.py | 33 +++++++----- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 4edc4e471..6b578fa4d 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -8,6 +8,7 @@ from enum import Enum import bfio +import filepattern import numpy import numpy as np import pandas @@ -68,14 +69,38 @@ class PlateParams(BaseModel): """The the y axis points for wells.""" +def sort_and_extract_signal( + img_dir: pathlib.Path, + plate_params: pathlib.Path, + file_pattern: str = "{index:d+}.ome.tiff", +): + """Build a DataFrame with well intensity measurements for each temperature. + + This is the top level method for this module and should be called by client's code. + In addition to extracting signal, + it sort images provided in the path provided according to file pattern. + For convenience, a default pattern using the existing RT Cetsa naming scheme is provided. + + Args: + img_dir: path to the image input directory. + file_pattern: filepattern used to sort the image. + mask_path: path to the plate params file. + + Returns: + Pandas DataFrame. + """ + img_files = sort_input_images(img_dir, file_pattern) + extract_signal(img_files, plate_params) + + def extract_signal( img_paths: list[pathlib.Path], - mask_path: pathlib.Path, + plate_params: pathlib.Path, ) -> pandas.DataFrame: """Build a DataFrame with well intensity measurements for each temperature. Args: - img_paths: List of image paths. + img_paths: List of sorted image paths. mask_path: path to the wells mask. Returns: @@ -98,7 +123,7 @@ def extract_signal( TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] ) try: - row = (temp, extract_wells_intensity_fast(image_path, mask_path)) + row = (temp, extract_wells_intensity_fast(image_path, plate_params)) measures.append(row) except Exception as e: # noqa raise IntensityExtractionError( @@ -277,3 +302,23 @@ def alphanumeric_row(row: int, col: int, dims: tuple[int, int]) -> str: row_alpha = row_alpha + string.ascii_uppercase[row % 26] return f"{row_alpha}{col+1:02d}" if size >= 96 else f"{row_alpha}{col+1}" + + +def sort_input_images(img_dir: pathlib.Path, file_pattern: str) -> list[pathlib.Path]: + fp = filepattern.FilePattern(img_dir, file_pattern) + + if len(fp.get_variables()) == 0: + msg = "A filepattern with one indexing variable is needed to sort the input images." + raise ValueError( + msg, + ) + + index = fp.get_variables()[0] + if len(fp.get_variables()) > 1: + logger.warning( + f"multiple indexing variables found in filepattern: {file_pattern}. Sorting filenames by {index}", + ) + + sorted_fp = sorted(fp(), key=lambda f: f[0][index]) + img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] + return img_files diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index df35e4fc1..790b7581d 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -8,6 +8,7 @@ import filepattern import typer from polus.images.features.rt_cetsa_intensity_extraction import extract_signal +from polus.images.features.rt_cetsa_intensity_extraction import sort_input_images # Initialize the logger logging.basicConfig( @@ -19,6 +20,7 @@ POLUS_IMG_EXT = os.environ.get("POLUS_IMG_EXT", ".ome.tiff") +# CLI options app = typer.Typer() @@ -33,16 +35,10 @@ def main( readable=True, resolve_path=True, ), - params: str = typer.Option(None, "--params", help="plate params filename."), filePattern: str = typer.Option( - ".+", + "{index:d+}.ome.tiff", "--filePattern", - help="FilePattern to match the files in the input directory.", - ), - preview: bool = typer.Option( - False, - "--preview", - help="Preview the files that will be processed.", + help="FilePattern to match the files in the input images sub directory.", ), out_dir: pathlib.Path = typer.Option( ..., @@ -53,6 +49,16 @@ def main( writable=True, resolve_path=True, ), + params: str = typer.Option( + None, + "--params", + help="(Optional) plate params filename in the input params subdirectory.", + ), + preview: bool = typer.Option( + False, + "--preview", + help="(Optional) Preview the files that will be processed.", + ), ) -> None: """CLI for rt-cetsa-intensity-extraction-tool.""" logger.info("Starting the CLI for rt-cetsa-v-extraction-tool.") @@ -65,10 +71,9 @@ def main( img_dir = inp_dir / "images" logger.info(f"Using images subdirectory: {img_dir}") + # Get plate params file and validate if params and not (inp_dir / "params").exists(): raise FileNotFoundError(f"no params subdirectory found in: {inp_dir}") - - # Try the get params file params_dir = inp_dir / "params" if params: params_file = params_dir / params @@ -82,11 +87,11 @@ def main( params_file = next(params_dir.iterdir()) logger.info(f"Using plate params: {params_file}") - fp = filepattern.FilePattern(img_dir, filePattern) - - sorted_fp = sorted(fp(), key=lambda f: f[0]["index"]) - img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] + # validate filePattern and sort input images + img_files = sort_input_images(img_dir, filePattern) + # generate a unique name for the output file + fp = filepattern.FilePattern(img_dir, filePattern) vals = list(fp.get_unique_values(fp.get_variables()[0])[fp.get_variables()[0]]) out_filename = f"plate_({vals[0]}-{vals[-1]}).csv" From cdd16621d70510e55d27655f729dd48ccd09ef79 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 03:53:40 -0400 Subject: [PATCH 17/26] feat: updated intensity extraction tool. --- .../.bumpversion.cfg | 2 + .../README.md | 19 +-- .../ict.yml | 24 +++- .../plugin.json | 30 ++++- .../pyproject.toml | 5 +- .../rt_cetsa_intensity_extraction.cwl | 18 ++- .../rt_cetsa_intensity_extraction/__init__.py | 124 ++++++------------ .../rt_cetsa_intensity_extraction/__main__.py | 8 ++ 8 files changed, 122 insertions(+), 108 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg index ad1d0ff9f..cb44c8f17 100644 --- a/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg +++ b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg @@ -29,3 +29,5 @@ replace = version = "{new_version}" [bumpversion:file:src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py] [bumpversion:file:ict.yml] + +[bumpversion:file:rt_cetsa_intensity_extraction.cwl] diff --git a/features/rt-cetsa-intensity-extraction-tool/README.md b/features/rt-cetsa-intensity-extraction-tool/README.md index 3d85f4503..f1b97d187 100644 --- a/features/rt-cetsa-intensity-extraction-tool/README.md +++ b/features/rt-cetsa-intensity-extraction-tool/README.md @@ -1,7 +1,6 @@ -# RT_CETSA Moltprot Regression (v0.1.0) +# RT_CETSA Intensity Extraction Tool (v0.1.0) -This WIPP plugin runs regression analysis for the RT-CETSA pipeline. -The input csv file should be sorted by `Temperature` column. +This tool runs well intensities extraction for the RT-CETSA pipeline. ## Building @@ -16,9 +15,11 @@ If WIPP is running, navigate to the plugins page and add a new plugin. Paste the This plugin takes eight input argument and one output argument: -| Name | Description | I/O | Type | -|-------------|----------------------------------------------------|--------|-------------| -| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | -| `--pattern` | Pattern to parse input files | Input | string | -| `--outDir` | Output file | Output | genericData | -| `--preview` | Generate JSON file with outputs | Output | JSON | +| Name | Description | I/O | Type | +|-----------------|----------------------------------------------------|--------|-------------| +| `--inpDir` | Input data collection to be processed by this tool | Input | genericData | +| `--filePattern` | Pattern to parse input files | Input | string | +| `--outDir` | Output file | Output | genericData | +| `--preview` | Generate JSON file with outputs | Output | JSON | Optional +| `--params` | Plate params to use | Output | JSON | Optional +| `--temp` | Temperature range. Default to [37,90] | Output | JSON | Optional diff --git a/features/rt-cetsa-intensity-extraction-tool/ict.yml b/features/rt-cetsa-intensity-extraction-tool/ict.yml index 5c4dddeab..f175d74ad 100644 --- a/features/rt-cetsa-intensity-extraction-tool/ict.yml +++ b/features/rt-cetsa-intensity-extraction-tool/ict.yml @@ -13,16 +13,22 @@ inputs: name: inpDir required: true type: path -- description: filename of the plate mask +- description: filename of the plate params format: - - mask - name: mask + - params + name: params required: false type: string +- description: temp interval on which to collect intensities + format: + - temp + name: temp + required: false + type: array[int] - description: Filepattern to parse input files format: - - pattern - name: pattern + - filePattern + name: filePattern required: false type: string - description: Generate an output preview. @@ -51,6 +57,14 @@ ui: key: inputs.pattern title: pattern type: text +- description: Plate params to use + key: inputs.params + title: Plate Params + type: text +- description: Temperature interval + key: inputs.temp + title: Temperature interval + type: text - description: Generate an output preview. key: inputs.preview title: Preview example output of this plugin diff --git a/features/rt-cetsa-intensity-extraction-tool/plugin.json b/features/rt-cetsa-intensity-extraction-tool/plugin.json index d274ae8c3..d6a8c22fa 100644 --- a/features/rt-cetsa-intensity-extraction-tool/plugin.json +++ b/features/rt-cetsa-intensity-extraction-tool/plugin.json @@ -21,12 +21,6 @@ "description": "RT_Cetsa image collection to be processed by this tool.", "required": true }, - { - "name": "mask", - "type": "string", - "description": "filename of the plate mask.", - "required": false - }, { "name": "filePattern", "type": "string", @@ -39,6 +33,18 @@ "type": "boolean", "description": "Generate preview of outputs.", "required": false + }, + { + "name": "params", + "type": "boolean", + "description": "Plate params to use.", + "required": false + }, + { + "name": "temp", + "type": "boolean", + "description": "Temperature range. Default to [37,90].", + "required": false } ], "outputs": [ @@ -61,9 +67,19 @@ }, { "key": "inputs.filePattern", - "title": "filePattern", + "title": "FilePattern", "description": "File Pattern to parse input files.", "default": ".+" + }, + { + "name": "inputs.params", + "title": "Params", + "description": "Plate params to use." + }, + { + "name": "inputs.temp", + "title": "Temp", + "description": "Temperature range. Default to [37,90]." } ] } diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index 7c1b020fe..6754d87df 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "polus_images_features_rt_cetsa_intensity_extraction" version = "0.1.0" -description = "Extract well intensities from RT-CETSA plate images and masks." +description = "Extract well intensities from RT-CETSA plate images." authors = [ "Nick Schaub ", "Antoine Gerardin ", @@ -19,7 +19,8 @@ numpy = "^1.26.4" bfio = "^2.3.6" scikit-image = "^0.22.0" imagecodecs = "^2024.1.1" -polus-tabular-regression-rt-cetsa-moltenprot = {path = "/Users/antoinegerardin/Documents/projects/tabular-tools/regression/rt-cetsa-moltenprot-tool"} +polus-images-segmentation-rt-cetsa-plate-extraction = {path = "/Users/antoinegerardin/Documents/projects/polus-plugins/segmentation/rt-cetsa-plate-extraction-tool"} + [tool.poetry.group.dev.dependencies] bump2version = "^1.0.1" diff --git a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl index 6bb087aa3..ba08aee00 100644 --- a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl +++ b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl @@ -13,14 +13,24 @@ inputs: inputBinding: prefix: --filePattern type: string? - preview: - inputBinding: - prefix: --preview - type: boolean? outDir: inputBinding: prefix: --outDir type: Directory + params: + inputBinding: + prefix: --params + type: string? + temp: + type: int[] + inputBinding: + prefix: -temp= + itemSeparator: " " + separate: false + preview: + inputBinding: + prefix: --preview + type: boolean? outputs: outDir: outputBinding: diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 6b578fa4d..aea8515aa 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -1,78 +1,37 @@ """RT_CETSA Intensity Extraction Tool.""" +__version__ = "0.1.0" import itertools import logging import os import pathlib import string -from enum import Enum import bfio import filepattern import numpy import numpy as np import pandas -from pydantic import BaseModel +from polus.images.segmentation.rt_cetsa_plate_extraction.core import PLATE_DIMS +from polus.images.segmentation.rt_cetsa_plate_extraction.core import PlateParams +from polus.images.segmentation.rt_cetsa_plate_extraction.core import PlateSize from pydantic_core import from_json logger = logging.getLogger(__file__) logger.setLevel(os.environ.get("POLUS_LOG", logging.INFO)) -ADD_TEMP = True -TEMPERATURE_RANGE = [37, 90] - class IntensityExtractionError(Exception): - pass - - -# TODO REMOVE Duplicate : This will cause problems when using plugins together -class PlateSize(Enum): - SIZE_6 = 6 - SIZE_12 = 12 - SIZE_24 = 24 - SIZE_48 = 48 - SIZE_96 = 96 - SIZE_384 = 384 - SIZE_1536 = 1536 - - -# TODO REMOVE Duplicate : This will cause problems when using plugins together -PLATE_DIMS = { - PlateSize.SIZE_6: (2, 3), - PlateSize.SIZE_12: (3, 4), - PlateSize.SIZE_24: (4, 6), - PlateSize.SIZE_48: (6, 8), - PlateSize.SIZE_96: (8, 12), - PlateSize.SIZE_384: (16, 24), - PlateSize.SIZE_1536: (32, 48), -} - - -class PlateParams(BaseModel): - rotate: int - """Counterclockwise rotation of image in degrees.""" + """Raise if an image could not be processed.""" - bbox: tuple[int, int, int, int] - """Bounding box of plate after rotation, [ymin,ymax,xmin,xmax].""" - - size: PlateSize - """The plate size, also determines layout.""" - - radius: int - """Well radius.""" - - X: list[int] - """The the x axis points for wells.""" - - Y: list[int] - """The the y axis points for wells.""" + pass def sort_and_extract_signal( img_dir: pathlib.Path, plate_params: pathlib.Path, file_pattern: str = "{index:d+}.ome.tiff", + temp_interval: tuple[int, int] = (37, 90), ): """Build a DataFrame with well intensity measurements for each temperature. @@ -85,45 +44,47 @@ def sort_and_extract_signal( img_dir: path to the image input directory. file_pattern: filepattern used to sort the image. mask_path: path to the plate params file. + temp_interval: temperature range on which the img_dir are collected. + we assume a linear temperature increase to build the result dataframe. Returns: Pandas DataFrame. """ img_files = sort_input_images(img_dir, file_pattern) - extract_signal(img_files, plate_params) + extract_signal(img_files, plate_params, temp_interval) def extract_signal( img_paths: list[pathlib.Path], plate_params: pathlib.Path, + temp_interval: tuple[int, int] = (37, 90), ) -> pandas.DataFrame: """Build a DataFrame with well intensity measurements for each temperature. Args: img_paths: List of sorted image paths. mask_path: path to the wells mask. + temp_interval: temperature range on which the img_dir are collected. + we assume a linear temperature increase to build the result dataframe. Returns: Pandas DataFrame. """ - if not ADD_TEMP: - raise NotImplementedError + start_temp, end_temp = temp_interval num_images = len(img_paths) if num_images < 2: raise ValueError( "provide at least 2 images on the temperature interval " - + f"({TEMPERATURE_RANGE[0]}-{TEMPERATURE_RANGE[1]})", + + f"({start_temp}-{end_temp})", ) measures: list[tuple[float, list[int]]] = [] for index, image_path in enumerate(img_paths): - temp = TEMPERATURE_RANGE[0] + index / (num_images - 1) * ( - TEMPERATURE_RANGE[1] - TEMPERATURE_RANGE[0] - ) + temp = start_temp + index / (num_images - 1) * (end_temp - start_temp) try: - row = (temp, extract_wells_intensity_fast(image_path, plate_params)) + row = (temp, extract_wells_intensity(image_path, plate_params)) measures.append(row) except Exception as e: # noqa raise IntensityExtractionError( @@ -132,7 +93,7 @@ def extract_signal( logger.info(f"extracted intensity for image : {index+1}/{num_images}") # build header - # check the first plate for number of wells + # check the first plate for the plate dimensions nb_wells = len(measures[0][1]) plate_size = PlateSize(nb_wells) plate_row_count = range(PLATE_DIMS[plate_size][0]) @@ -150,7 +111,7 @@ def extract_signal( # build dataframe # roundup temperature - rows = [[round(measure[0], 1), *measure[1]] for measure in measures] + rows = [[round(measure[0], 2), *measure[1]] for measure in measures] df = pandas.DataFrame(rows, columns=header) # Set the temperature as the index @@ -188,7 +149,7 @@ def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: return int(np.mean(patch) - np.median(background[: int(0.05 * background.size)])) -def extract_wells_intensity_fast( +def extract_wells_intensity( image_path: pathlib.Path, mask_path: pathlib.Path, ) -> list[int]: @@ -208,30 +169,26 @@ def extract_wells_intensity_fast( intensities = [] - # NOTE take half of the smallest distance between wells. - # this is close to what the original matlab code is doing - # and get us close to the value it was computing. - R = min(params.X[1] - params.X[0], params.Y[1] - params.Y[0]) // 2 - - logger.debug( - f""" - Average radius detected : {params.radius} - Radius - """, - ) - for y, x in itertools.product(range(len(params.Y)), range(len(params.X))): - intensity = extract_intensity(image, params.X[x], params.Y[y], R) + intensity = extract_intensity( + image, + params.X[x], + params.Y[y], + params.roi_radius, + ) intensities.append(intensity) return intensities -def extract_wells_intensity( +def extract_wells_intensity_from_mask( image_path: pathlib.Path, mask_path: pathlib.Path, ) -> list[int]: - """Extract well intensities from RT_CETSA image and mask. + """Extract well intensities from RT_CETSA images using a labeled mask. + + This method is degree of magnitude solwer than extract_wells_intensity + and is just provided for convenience. Consider using extract_wells_intensity instead. Args: image_path: Path to the RT_CETSA well plate image. @@ -247,10 +204,8 @@ def extract_wells_intensity( max_mask_index = numpy.max(mask) intensities = [] - for mask_label in range(1, max_mask_index + 1): # retrieve a bounding box around each well. - # NOTE this was originally much faster when relying on well positions. current_mask = mask == mask_label image[current_mask] bbox = numpy.argwhere(current_mask) @@ -258,7 +213,6 @@ def extract_wells_intensity( bbox_x_max = numpy.max(bbox[0]) bbox_y_min = numpy.min(bbox[1]) bbox_y_max = numpy.max(bbox[1]) - intensity = compute_well_intensity( image, bbox_y_min, @@ -266,14 +220,18 @@ def extract_wells_intensity( bbox_x_min, bbox_x_max, ) - intensities.append(intensity) - return intensities -def compute_well_intensity(image, bbox_y_min, bbox_y_max, bbox_x_min, bbox_x_max): - # compute corrected intensity +def compute_well_intensity( + image: np.ndarray, + bbox_y_min: int, + bbox_y_max: int, + bbox_x_min: int, + bbox_x_max: int, +): + """Compute corrected intensity.""" patch = image[bbox_y_min:bbox_y_max, bbox_x_min:bbox_x_max] background = patch.ravel() background.sort() @@ -305,6 +263,10 @@ def alphanumeric_row(row: int, col: int, dims: tuple[int, int]) -> str: def sort_input_images(img_dir: pathlib.Path, file_pattern: str) -> list[pathlib.Path]: + """Sort image with the provided filepattern. + + If multiple indexing variables are provided, we only index using the first one. + """ fp = filepattern.FilePattern(img_dir, file_pattern) if len(fp.get_variables()) == 0: diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py index 790b7581d..adc8cbe0f 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__main__.py @@ -54,6 +54,11 @@ def main( "--params", help="(Optional) plate params filename in the input params subdirectory.", ), + temp_interval: tuple[int, int] = typer.Option( + (37, 90), + "--temp", + help="(Optional) Temperature range. Default to [37,90]", + ), preview: bool = typer.Option( False, "--preview", @@ -71,6 +76,9 @@ def main( img_dir = inp_dir / "images" logger.info(f"Using images subdirectory: {img_dir}") + if temp_interval: + logger.info(f"Temperature interval: {temp_interval}") + # Get plate params file and validate if params and not (inp_dir / "params").exists(): raise FileNotFoundError(f"no params subdirectory found in: {inp_dir}") From 2d2b4c11cb84950311586cb0bc309b158ecf0914 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 03:55:26 -0400 Subject: [PATCH 18/26] =?UTF-8?q?Bump=20version:=200.1.0=20=E2=86=92=200.2?= =?UTF-8?q?.0-dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg | 2 +- features/rt-cetsa-intensity-extraction-tool/README.md | 2 +- features/rt-cetsa-intensity-extraction-tool/VERSION | 2 +- features/rt-cetsa-intensity-extraction-tool/ict.yml | 4 ++-- features/rt-cetsa-intensity-extraction-tool/plugin.json | 4 ++-- features/rt-cetsa-intensity-extraction-tool/pyproject.toml | 2 +- .../rt_cetsa_intensity_extraction.cwl | 2 +- .../images/features/rt_cetsa_intensity_extraction/__init__.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg index cb44c8f17..9dd023a2b 100644 --- a/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg +++ b/features/rt-cetsa-intensity-extraction-tool/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.0 +current_version = 0.2.0-dev0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/features/rt-cetsa-intensity-extraction-tool/README.md b/features/rt-cetsa-intensity-extraction-tool/README.md index f1b97d187..b40841642 100644 --- a/features/rt-cetsa-intensity-extraction-tool/README.md +++ b/features/rt-cetsa-intensity-extraction-tool/README.md @@ -1,4 +1,4 @@ -# RT_CETSA Intensity Extraction Tool (v0.1.0) +# RT_CETSA Intensity Extraction Tool (v0.2.0-dev0) This tool runs well intensities extraction for the RT-CETSA pipeline. diff --git a/features/rt-cetsa-intensity-extraction-tool/VERSION b/features/rt-cetsa-intensity-extraction-tool/VERSION index 6e8bf73aa..ce0f6f878 100644 --- a/features/rt-cetsa-intensity-extraction-tool/VERSION +++ b/features/rt-cetsa-intensity-extraction-tool/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0-dev0 diff --git a/features/rt-cetsa-intensity-extraction-tool/ict.yml b/features/rt-cetsa-intensity-extraction-tool/ict.yml index f175d74ad..5746f1a4d 100644 --- a/features/rt-cetsa-intensity-extraction-tool/ict.yml +++ b/features/rt-cetsa-intensity-extraction-tool/ict.yml @@ -3,7 +3,7 @@ author: - Antoine Gerardin - Najib Ishaq contact: nick.schaub@nih.gov -container: polusai/rt-cetsa-intensity-extraction-tool:0.1.0 +container: polusai/rt-cetsa-intensity-extraction-tool:0.2.0-dev0 description: Extract well intensities from RT-CETSA plate images and masks. entrypoint: python3 -m polus.images.features.rt_cetsa_intensity_extraction inputs: @@ -69,4 +69,4 @@ ui: key: inputs.preview title: Preview example output of this plugin type: checkbox -version: 0.1.0 +version: 0.2.0-dev0 diff --git a/features/rt-cetsa-intensity-extraction-tool/plugin.json b/features/rt-cetsa-intensity-extraction-tool/plugin.json index d6a8c22fa..3f81fd1b6 100644 --- a/features/rt-cetsa-intensity-extraction-tool/plugin.json +++ b/features/rt-cetsa-intensity-extraction-tool/plugin.json @@ -1,6 +1,6 @@ { "name": "RT-CETSA Intensity Extraction", - "version": "0.1.0", + "version": "0.2.0-dev0", "title": "RT-CETSA Intensity Extraction", "description": "Extract well intensities from RT-CETSA images and masks.", "author": "Nicholas Schaub (nick.schaub@nih.gov), Antoine Gerardin (antoine.gerardin@nih.gov), Najib Ishaq (najib.ishaq@nih.gov)", @@ -8,7 +8,7 @@ "repository": "https://github.com/PolusAI/image-tools", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/rt-cetsa-intensity-extraction-tool:0.1.0", + "containerId": "polusai/rt-cetsa-intensity-extraction-tool:0.2.0-dev0", "baseCommand": [ "python3", "-m", diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index 6754d87df..82caf351b 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polus_images_features_rt_cetsa_intensity_extraction" -version = "0.1.0" +version = "0.2.0-dev0" description = "Extract well intensities from RT-CETSA plate images." authors = [ "Nick Schaub ", diff --git a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl index ba08aee00..3492162c8 100644 --- a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl +++ b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl @@ -38,7 +38,7 @@ outputs: type: Directory requirements: DockerRequirement: - dockerPull: polusai/rt-cetsa-intensity-extraction-tool:0.1.0 + dockerPull: polusai/rt-cetsa-intensity-extraction-tool:0.2.0-dev0 InitialWorkDirRequirement: listing: - entry: $(inputs.outDir) diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index aea8515aa..0f9e8b8d0 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -1,5 +1,5 @@ """RT_CETSA Intensity Extraction Tool.""" -__version__ = "0.1.0" +__version__ = "0.2.0-dev0" import itertools import logging From d6c8c376c30a19a9d8140ce0f393d74ecfe7c687 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 04:00:31 -0400 Subject: [PATCH 19/26] feat: update extraction tool. --- segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg | 2 ++ segmentation/rt-cetsa-plate-extraction-tool/README.md | 2 +- segmentation/rt-cetsa-plate-extraction-tool/ict.yml | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg index 510a7bdfd..b47864477 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg +++ b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg @@ -29,3 +29,5 @@ replace = version = "{new_version}" [bumpversion:file:src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py] [bumpversion:file:ict.yml] + +[bumpversion:file:rt_cetsa_plate_extraction.cwl] diff --git a/segmentation/rt-cetsa-plate-extraction-tool/README.md b/segmentation/rt-cetsa-plate-extraction-tool/README.md index b2c779bff..d92ceace3 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/README.md +++ b/segmentation/rt-cetsa-plate-extraction-tool/README.md @@ -21,4 +21,4 @@ This plugin takes eight input argument and one output argument: | `--inpDir` | Input data collection to be processed by this tool | Input | genericData | | `--filePattern` | FilePattern to parse input files | Input | string | | `--outDir` | Output dir | Output | genericData | -| `--preview` | Generate JSON file with outputs | Output | JSON | +| `--preview` | Generate JSON file with outputs | Output | JSON | Optional diff --git a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml index 4bafb31e6..03faea4a1 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml +++ b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml @@ -42,8 +42,8 @@ ui: title: Input data collection type: path - description: Filepattern to parse input files - key: inputs.pattern - title: pattern + key: inputs.filePattern + title: filePattern type: text - description: Generate an output preview. key: inputs.preview From f3849c3dfd44c6c3d6dfaeb9bb418e36cb3ada1f Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 04:00:53 -0400 Subject: [PATCH 20/26] =?UTF-8?q?Bump=20version:=200.1.0=20=E2=86=92=200.2?= =?UTF-8?q?.0-dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg | 2 +- segmentation/rt-cetsa-plate-extraction-tool/README.md | 2 +- segmentation/rt-cetsa-plate-extraction-tool/VERSION | 2 +- segmentation/rt-cetsa-plate-extraction-tool/ict.yml | 4 ++-- segmentation/rt-cetsa-plate-extraction-tool/plugin.json | 4 ++-- segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml | 2 +- .../rt_cetsa_plate_extraction.cwl | 2 +- .../images/segmentation/rt_cetsa_plate_extraction/__init__.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg index b47864477..98f348a50 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg +++ b/segmentation/rt-cetsa-plate-extraction-tool/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.0 +current_version = 0.2.0-dev0 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/segmentation/rt-cetsa-plate-extraction-tool/README.md b/segmentation/rt-cetsa-plate-extraction-tool/README.md index d92ceace3..f51c93572 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/README.md +++ b/segmentation/rt-cetsa-plate-extraction-tool/README.md @@ -1,4 +1,4 @@ -# RT_CETSA Plate Extraction Tool (v0.1.0) +# RT_CETSA Plate Extraction Tool (v0.2.0-dev0) This tool extracts detect wells in a RT-CETSA plate image. It outputs a cropped and rotated image and the well detection mask. diff --git a/segmentation/rt-cetsa-plate-extraction-tool/VERSION b/segmentation/rt-cetsa-plate-extraction-tool/VERSION index 6e8bf73aa..ce0f6f878 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/VERSION +++ b/segmentation/rt-cetsa-plate-extraction-tool/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0-dev0 diff --git a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml index 03faea4a1..83330cb1d 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/ict.yml +++ b/segmentation/rt-cetsa-plate-extraction-tool/ict.yml @@ -3,7 +3,7 @@ author: - Antoine Gerardin - Najib Ishaq contact: nick.schaub@nih.gov -container: polusai/rt-cetsa-plate-extraction-tool:0.1.0 +container: polusai/rt-cetsa-plate-extraction-tool:0.2.0-dev0 description: Rotate and crop images of plates from RT-CETSA; then label the wells. entrypoint: python3 -m polus.images.segmentation.rt_cetsa_plate_extraction inputs: @@ -49,4 +49,4 @@ ui: key: inputs.preview title: Preview example output of this plugin type: checkbox -version: 0.1.0 +version: 0.2.0-dev0 diff --git a/segmentation/rt-cetsa-plate-extraction-tool/plugin.json b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json index 9f63ced74..c9372fbc4 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/plugin.json +++ b/segmentation/rt-cetsa-plate-extraction-tool/plugin.json @@ -1,6 +1,6 @@ { "name": "RT-CETSA Plate Extraction", - "version": "0.1.0", + "version": "0.2.0-dev0", "title": "RT-CETSA Plate Extraction", "description": "Run regression analysis for the RT-CETSA pipeline.", "author": "Nicholas Schaub (nick.schaub@nih.gov),Antoine Gerardin (antoine.gerardin@nih.gov), Najib Ishaq (najib.ishaq@nih.gov)", @@ -8,7 +8,7 @@ "repository": "https://github.com/PolusAI/image-tools", "website": "https://ncats.nih.gov/preclinical/core/informatics", "citation": "", - "containerId": "polusai/rt-cetsa-plate-extraction-tool:0.1.0", + "containerId": "polusai/rt-cetsa-plate-extraction-tool:0.2.0-dev0", "baseCommand": [ "python3", "-m", diff --git a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml index ad5e79102..5fca52b76 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml +++ b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "polus_images_segmentation_rt_cetsa_plate_extraction" -version = "0.1.0" +version = "0.2.0-dev0" description = "Rotate and crop images of plates from RT-CETSA; then label the wells." authors = [ "Nick Schaub ", diff --git a/segmentation/rt-cetsa-plate-extraction-tool/rt_cetsa_plate_extraction.cwl b/segmentation/rt-cetsa-plate-extraction-tool/rt_cetsa_plate_extraction.cwl index 757b3d817..e9ab03f61 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/rt_cetsa_plate_extraction.cwl +++ b/segmentation/rt-cetsa-plate-extraction-tool/rt_cetsa_plate_extraction.cwl @@ -24,7 +24,7 @@ outputs: type: Directory requirements: DockerRequirement: - dockerPull: polusai/rt-cetsa-plate-extraction-tool:0.1.0 + dockerPull: polusai/rt-cetsa-plate-extraction-tool:0.2.0-dev0 InitialWorkDirRequirement: listing: - entry: $(inputs.outDir) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index 8f9a8637b..df6d51bb6 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -1,5 +1,5 @@ """RT_CETSA Plate Extraction Tool.""" -__version__ = "0.1.0" +__version__ = "0.2.0-dev0" import logging import os From 3345e3ded97b3d913bac21e6d9e429391a1f5750 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 04:04:16 -0400 Subject: [PATCH 21/26] fix: return extract signal df. optional temp in cwl --- .../rt_cetsa_intensity_extraction.cwl | 2 +- .../images/features/rt_cetsa_intensity_extraction/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl index 3492162c8..0d5e9ebe6 100644 --- a/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl +++ b/features/rt-cetsa-intensity-extraction-tool/rt_cetsa_intensity_extraction.cwl @@ -22,7 +22,7 @@ inputs: prefix: --params type: string? temp: - type: int[] + type: int[]? inputBinding: prefix: -temp= itemSeparator: " " diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 0f9e8b8d0..4112d1ce8 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -51,7 +51,7 @@ def sort_and_extract_signal( Pandas DataFrame. """ img_files = sort_input_images(img_dir, file_pattern) - extract_signal(img_files, plate_params, temp_interval) + return extract_signal(img_files, plate_params, temp_interval) def extract_signal( @@ -187,7 +187,7 @@ def extract_wells_intensity_from_mask( ) -> list[int]: """Extract well intensities from RT_CETSA images using a labeled mask. - This method is degree of magnitude solwer than extract_wells_intensity + This method is degree of magnitude slower than extract_wells_intensity and is just provided for convenience. Consider using extract_wells_intensity instead. Args: From c884aec528614c92fd4b9f0e07ca3006444f0fc0 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 04:24:30 -0400 Subject: [PATCH 22/26] fix: package dependencies. --- features/rt-cetsa-intensity-extraction-tool/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index 82caf351b..8176ee1b4 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -19,7 +19,7 @@ numpy = "^1.26.4" bfio = "^2.3.6" scikit-image = "^0.22.0" imagecodecs = "^2024.1.1" -polus-images-segmentation-rt-cetsa-plate-extraction = {path = "/Users/antoinegerardin/Documents/projects/polus-plugins/segmentation/rt-cetsa-plate-extraction-tool"} +polus-images-segmentation-rt-cetsa-plate-extraction = {path = "../../segmentation/rt-cetsa-plate-extraction-tool"} [tool.poetry.group.dev.dependencies] From ece52fcbc77a6938cd9130705755875b9ef7b625 Mon Sep 17 00:00:00 2001 From: agerardin Date: Tue, 21 May 2024 04:36:02 -0400 Subject: [PATCH 23/26] chore: cleanup before PR. --- .../rt_cetsa_intensity_extraction/__init__.py | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py index 4112d1ce8..5414e99c4 100644 --- a/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py +++ b/features/rt-cetsa-intensity-extraction-tool/src/polus/images/features/rt_cetsa_intensity_extraction/__init__.py @@ -54,6 +54,30 @@ def sort_and_extract_signal( return extract_signal(img_files, plate_params, temp_interval) +def sort_input_images(img_dir: pathlib.Path, file_pattern: str) -> list[pathlib.Path]: + """Sort image with the provided filepattern. + + If multiple indexing variables are provided, we only index using the first one. + """ + fp = filepattern.FilePattern(img_dir, file_pattern) + + if len(fp.get_variables()) == 0: + msg = "A filepattern with one indexing variable is needed to sort the input images." + raise ValueError( + msg, + ) + + index = fp.get_variables()[0] + if len(fp.get_variables()) > 1: + logger.warning( + f"multiple indexing variables found in filepattern: {file_pattern}. Sorting filenames by {index}", + ) + + sorted_fp = sorted(fp(), key=lambda f: f[0][index]) + img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] + return img_files + + def extract_signal( img_paths: list[pathlib.Path], plate_params: pathlib.Path, @@ -123,48 +147,22 @@ def extract_signal( return df -def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: - """Get the well intensity - - Args: - image: _description_ - x: x-position of the well centerpoint - y: y-position of the well centerpoint - r: radius of the circle inscribed in the square area of interest. - - Returns: - int: The background corrected mean well intensity - """ - # we take a square area around the well center - x_min = max(x - r, 0) - x_max = min(x + r, image.shape[1]) - y_min = max(y - r, 0) - y_max = min(y + r, image.shape[0]) - - patch = image[y_min:y_max, x_min:x_max] - background = patch.ravel() - background.sort() - - # Subtract lowest pixel values from average patch pixel values - return int(np.mean(patch) - np.median(background[: int(0.05 * background.size)])) - - def extract_wells_intensity( image_path: pathlib.Path, - mask_path: pathlib.Path, + params_path: pathlib.Path, ) -> list[int]: """Extract well intensities from RT_CETSA image and mask. Args: image_path: Path to the RT_CETSA well plate image. - mask_path: Path to the corresponding params file. + params_path: Path to the corresponding params file. Returns: - mean intensity for each wells. + corrected intensity for each well. """ with bfio.BioReader(image_path) as reader: image = reader[:] - with mask_path.open("r") as f: + with params_path.open("r") as f: params = PlateParams(**from_json(f.read())) intensities = [] @@ -181,6 +179,32 @@ def extract_wells_intensity( return intensities +def extract_intensity(image: np.ndarray, x: int, y: int, r: int) -> int: + """Get the well intensity + + Args: + image: _description_ + x: x-position of the well centerpoint + y: y-position of the well centerpoint + r: radius of the circle inscribed in the square area of interest. + + Returns: + int: The background corrected mean well intensity + """ + # we take a square area around the well center + x_min = max(x - r, 0) + x_max = min(x + r, image.shape[1]) + y_min = max(y - r, 0) + y_max = min(y + r, image.shape[0]) + + patch = image[y_min:y_max, x_min:x_max] + background = patch.ravel() + background.sort() + + # Subtract lowest pixel values from average patch pixel values + return int(np.mean(patch) - np.median(background[: int(0.05 * background.size)])) + + def extract_wells_intensity_from_mask( image_path: pathlib.Path, mask_path: pathlib.Path, @@ -260,27 +284,3 @@ def alphanumeric_row(row: int, col: int, dims: tuple[int, int]) -> str: row_alpha = row_alpha + string.ascii_uppercase[row % 26] return f"{row_alpha}{col+1:02d}" if size >= 96 else f"{row_alpha}{col+1}" - - -def sort_input_images(img_dir: pathlib.Path, file_pattern: str) -> list[pathlib.Path]: - """Sort image with the provided filepattern. - - If multiple indexing variables are provided, we only index using the first one. - """ - fp = filepattern.FilePattern(img_dir, file_pattern) - - if len(fp.get_variables()) == 0: - msg = "A filepattern with one indexing variable is needed to sort the input images." - raise ValueError( - msg, - ) - - index = fp.get_variables()[0] - if len(fp.get_variables()) > 1: - logger.warning( - f"multiple indexing variables found in filepattern: {file_pattern}. Sorting filenames by {index}", - ) - - sorted_fp = sorted(fp(), key=lambda f: f[0][index]) - img_files: list[pathlib.Path] = [f[1][0] for f in sorted_fp] # type: ignore[assignment] - return img_files From 82362f54e186198726e1d0259df5abb3198356b1 Mon Sep 17 00:00:00 2001 From: agerardin Date: Wed, 22 May 2024 05:44:49 -0400 Subject: [PATCH 24/26] fix dependency on plate extraction to fix docker build. --- features/rt-cetsa-intensity-extraction-tool/Dockerfile | 3 +++ features/rt-cetsa-intensity-extraction-tool/pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/features/rt-cetsa-intensity-extraction-tool/Dockerfile b/features/rt-cetsa-intensity-extraction-tool/Dockerfile index 1eb75067f..a13861a27 100755 --- a/features/rt-cetsa-intensity-extraction-tool/Dockerfile +++ b/features/rt-cetsa-intensity-extraction-tool/Dockerfile @@ -14,6 +14,9 @@ COPY VERSION ${EXEC_DIR} COPY README.md ${EXEC_DIR} COPY src ${EXEC_DIR}/src +RUN apt-get -y update +RUN apt-get -y install git + RUN pip3 install ${EXEC_DIR} --no-cache-dir ENTRYPOINT ["python3", "-m", "polus.images.features.rt_cetsa_intensity_extraction"] diff --git a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml index 8176ee1b4..171dce997 100644 --- a/features/rt-cetsa-intensity-extraction-tool/pyproject.toml +++ b/features/rt-cetsa-intensity-extraction-tool/pyproject.toml @@ -19,7 +19,7 @@ numpy = "^1.26.4" bfio = "^2.3.6" scikit-image = "^0.22.0" imagecodecs = "^2024.1.1" -polus-images-segmentation-rt-cetsa-plate-extraction = {path = "../../segmentation/rt-cetsa-plate-extraction-tool"} +polus-images-segmentation-rt-cetsa-plate-extraction = {git = "https://github.com/agerardin/image-tools.git", branch = "feat/rt_cetsa", subdirectory = "segmentation/rt-cetsa-plate-extraction-tool"} [tool.poetry.group.dev.dependencies] From bf32f5c342ab32108e35eff8cb794b2f1f1e65e8 Mon Sep 17 00:00:00 2001 From: agerardin Date: Wed, 22 May 2024 06:13:07 -0400 Subject: [PATCH 25/26] chore: add dummy tests to pacify git actions. --- .../rt-cetsa-plate-extraction-tool/tests/__init__.py | 1 + .../tests/test_extraction.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/tests/__init__.py create mode 100644 segmentation/rt-cetsa-plate-extraction-tool/tests/test_extraction.py diff --git a/segmentation/rt-cetsa-plate-extraction-tool/tests/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/tests/__init__.py new file mode 100644 index 000000000..d420712d8 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/tests/__init__.py @@ -0,0 +1 @@ +"""Tests.""" diff --git a/segmentation/rt-cetsa-plate-extraction-tool/tests/test_extraction.py b/segmentation/rt-cetsa-plate-extraction-tool/tests/test_extraction.py new file mode 100644 index 000000000..a07761886 --- /dev/null +++ b/segmentation/rt-cetsa-plate-extraction-tool/tests/test_extraction.py @@ -0,0 +1,10 @@ +"""Tests.""" + +from polus.images.segmentation.rt_cetsa_plate_extraction import extract_plates + + +def test_extraction(): + """To pacify git actions. + + Original files to use for testing are too large to be shipped as testing data.""" + pass From 9ba335b2781f13088fdbfcee98f99f2e39eca99e Mon Sep 17 00:00:00 2001 From: agerardin Date: Wed, 22 May 2024 12:05:25 -0400 Subject: [PATCH 26/26] fix: remove bfio and use tifffile for docker integration. --- .../pyproject.toml | 2 +- .../rt_cetsa_plate_extraction/__init__.py | 27 ++++++++++--------- .../rt_cetsa_plate_extraction/__main__.py | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml index 5fca52b76..cb7b1343c 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml +++ b/segmentation/rt-cetsa-plate-extraction-tool/pyproject.toml @@ -16,7 +16,7 @@ typer = "^0.7.0" filepattern = "^2.0.5" numpy = "^1.26.4" scikit-image = "0.22.0" -bfio = "^2.3.6" +bfio = "2.3.6" [tool.poetry.group.dev.dependencies] bump2version = "^1.0.1" diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py index df6d51bb6..47b457f26 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__init__.py @@ -7,6 +7,7 @@ import bfio import filepattern +import tifffile from polus.images.segmentation.rt_cetsa_plate_extraction.core import PlateParams from polus.images.segmentation.rt_cetsa_plate_extraction.core import create_mask from polus.images.segmentation.rt_cetsa_plate_extraction.core import crop_and_rotate @@ -43,8 +44,8 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: # extract plate params from first image first_image_path = inp_files[0] - with bfio.BioReader(first_image_path) as reader: - first_image = reader[:] + + first_image = tifffile.imread(first_image_path) params: PlateParams = get_plate_params(first_image) @@ -54,24 +55,24 @@ def extract_plates(inp_dir, pattern, out_dir) -> PlateParams: num_images = len(inp_files) for index, f in enumerate(inp_files): logger.info(f"Processing Image {index+1}/{num_images}: {f}") - with bfio.BioReader(f) as reader: - image = reader[:] - cropped_and_rotated = crop_and_rotate(image, params) + image = tifffile.imread(f) + cropped_and_rotated = crop_and_rotate(image, params) - if index == 1: - first_image = cropped_and_rotated + if index == 1: + first_image = cropped_and_rotated - out_path = out_dir / "images" / (f.stem + POLUS_IMG_EXT) - with bfio.BioWriter(out_path) as writer: - writer.dtype = cropped_and_rotated.dtype - writer.shape = cropped_and_rotated.shape - writer[:] = cropped_and_rotated + out_path = out_dir / "images" / (f.stem + POLUS_IMG_EXT) + with bfio.BioWriter(out_path) as writer: + writer.dtype = cropped_and_rotated.dtype + writer.shape = cropped_and_rotated.shape + writer[:] = cropped_and_rotated # save plate parameters for the first processed image processed_params = get_plate_params(first_image) - plate_path = out_dir / "params" / "plate.csv" + plate_path = out_dir / "params" / "plate.json" with plate_path.open("w") as f: f.write(processed_params.model_dump_json()) # type: ignore + logger.info(f"Plate params saved: {plate_path}") # save the corresponding mask for reference mask = create_mask(processed_params) diff --git a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py index f70c663c1..8bfc5513b 100644 --- a/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py +++ b/segmentation/rt-cetsa-plate-extraction-tool/src/polus/images/segmentation/rt_cetsa_plate_extraction/__main__.py @@ -75,7 +75,7 @@ def main( "masks": [ (Path("masks") / f"{inp_files[0].stem}{POLUS_IMG_EXT}").as_posix(), ], - "params": [Path("params") / "plate.csv"], + "params": [Path("params") / "plate.json"], } with (out_dir / "preview.json").open("w") as f: