view spot_detection_2d.py @ 5:e8c9e104e109 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/spot_detection_2d/ commit f1b9207ec23c0af1681c929281bcbf1d0638368e
author imgteam
date Wed, 25 Sep 2024 08:19:30 +0000
parents 4645b356bf3b
children
line wrap: on
line source

"""
Copyright 2021-2022 Biomedical Computer Vision Group, Heidelberg University.
Authors:
- Qi Gao (qi.gao@bioquant.uni-heidelberg.de)
- Leonid Kostrykin (leonid.kostrykin@bioquant.uni-heidelberg.de)

Distributed under the MIT license.
See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
"""

import argparse

import giatools.io
import numpy as np
import pandas as pd
import scipy.ndimage as ndi
from numpy.typing import NDArray
from skimage.feature import blob_dog, blob_doh, blob_log

blob_filters = {
    'dog': blob_dog,
    'doh': blob_doh,
    'log': blob_log,
}


def mean_intensity(img: NDArray, y: int, x: int, radius: int) -> float:
    assert img.ndim == 2
    assert radius >= 0
    if radius == 0:
        return float(img[y, x])
    else:
        mask = np.ones(img.shape, bool)
        mask[y, x] = False
        mask = (ndi.distance_transform_edt(mask) <= radius)
        return img[mask].mean()


def spot_detection(
    fn_in: str,
    fn_out: str,
    frame_1st: int,
    frame_end: int,
    filter_type: str,
    min_scale: float,
    max_scale: float,
    abs_threshold: float,
    rel_threshold: float,
    boundary: int,
) -> None:

    # Load the single-channel 2-D input image (or stack thereof)
    stack = giatools.io.imread(fn_in)

    # Normalize input image so that it is a stack of images (possibly a stack of a single image)
    assert stack.ndim in (2, 3)
    if stack.ndim == 2:
        stack = stack.reshape(1, *stack.shape)

    # Slice the stack
    assert frame_1st >= 1
    assert frame_end >= 0
    stack = stack[frame_1st - 1:]
    if frame_end > 0:
        stack = stack[:-frame_end]

    # Select the blob detection filter
    assert filter_type.lower() in blob_filters.keys()
    blob_filter = blob_filters[filter_type.lower()]

    # Perform blob detection on each image of the stack
    detections = list()
    for img_idx, img in enumerate(stack):
        blobs = blob_filter(img, threshold=abs_threshold, threshold_rel=rel_threshold, min_sigma=min_scale, max_sigma=max_scale)
        for blob in blobs:
            y, x, scale = blob

            # Skip the detection if it is too close to the boundary of the image
            if y < boundary or x < boundary or y >= img.shape[0] - boundary or x >= img.shape[1] - boundary:
                continue

            # Add the detection to the list of detections
            radius = scale * np.sqrt(2) * 2
            intensity = mean_intensity(img, round(y), round(x), round(radius))
            detections.append(
                {
                    'frame': img_idx + 1,
                    'pos_x': round(x),
                    'pos_y': round(y),
                    'scale': scale,
                    'radius': radius,
                    'intensity': intensity,
                }
            )

    # Build and save dataframe
    df = pd.DataFrame.from_dict(detections)
    df.to_csv(fn_out, index=False, float_format='%.2f', sep="\t")


if __name__ == "__main__":

    parser = argparse.ArgumentParser(description="Spot detection")

    parser.add_argument("fn_in", help="Name of input image or image stack.")
    parser.add_argument("fn_out", help="Name of output file to write the detections into.")
    parser.add_argument("frame_1st", type=int, help="Index for the starting frame to detect spots (1 for first frame of the stack).")
    parser.add_argument("frame_end", type=int, help="Index for the last frame to detect spots (0 for the last frame of the stack).")
    parser.add_argument("filter_type", help="Detection filter")
    parser.add_argument("min_scale", type=float, help="The minimum scale to consider for multi-scale detection.")
    parser.add_argument("max_scale", type=float, help="The maximum scale to consider for multi-scale detection.")
    parser.add_argument("abs_threshold", type=float, help=(
        "Filter responses below this threshold will be ignored. Only filter responses above this thresholding will be considered as blobs. "
        "This threshold is ignored if the relative threshold (below) corresponds to a higher response.")
    )
    parser.add_argument("rel_threshold", type=float, help=(
        "Same as the absolute threshold (above), but as a fraction of the overall maximal filter response of an image. "
        "This threshold is ignored if it corresponds to a response below the absolute threshold.")
    )
    parser.add_argument("boundary", type=int, help="Width of image boundaries (in pixel) where spots will be ignored.")

    args = parser.parse_args()
    spot_detection(args.fn_in, args.fn_out,
                   frame_1st=args.frame_1st, frame_end=args.frame_end,
                   filter_type=args.filter_type,
                   min_scale=args.min_scale, max_scale=args.max_scale,
                   abs_threshold=args.abs_threshold, rel_threshold=args.rel_threshold,
                   boundary=args.boundary)