diff --git a/noxfile.py b/noxfile.py index fa1e69d..525fbcb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -48,11 +48,13 @@ def install_groups( # See: https://python-poetry.org/blog/announcing-poetry-1.7.0/ argv = [ "poetry", - "config", + "config", "warnings.export", "false", ] - session.debug(f"Running command to silence warning since in Nix shell we install the patch: {' '.join(argv)}") + session.debug( + f"Running command to silence warning since in Nix shell we install the patch: {' '.join(argv)}" + ) session.run_always(*argv, external=True) session.log(f"Will generate requirements hashfile: {hashfile}") requirements_txt.parent.mkdir(parents=True, exist_ok=True) diff --git a/spotfishing/deprecated.py b/spotfishing/deprecated.py index b6a03c2..e68b005 100644 --- a/spotfishing/deprecated.py +++ b/spotfishing/deprecated.py @@ -8,10 +8,13 @@ from skimage.morphology import ball, remove_small_objects, white_tophat from skimage.segmentation import expand_labels - Numeric = Union[int, float] -CENTROID_COLUMNS_REMAPPING = {'centroid_weighted-0': 'zc', 'centroid_weighted-1': 'yc', 'centroid_weighted-2': 'xc'} +CENTROID_COLUMNS_REMAPPING = { + "centroid_weighted-0": "zc", + "centroid_weighted-1": "yc", + "centroid_weighted-2": "xc", +} def detect_spots_dog_old(input_img, spot_threshold: Numeric, expand_px: int = 10): @@ -24,16 +27,16 @@ def detect_spots_dog_old(input_img, spot_threshold: Numeric, expand_px: int = 10 spot_threshold : NumberLike Threshold to use for spots expand_px : int - Number of pixels by which to expand contiguous subregion, + Number of pixels by which to expand contiguous subregion, up to point of overlap with neighboring subregion of image - + Returns ------- - pd.DataFrame, np.ndarray, np.ndarray: - The centroids and roi_IDs of the spots found, - the image used for spot detection, and - numpy array with only sufficiently large regions - retained (bigger than threshold number of pixels), + pd.DataFrame, np.ndarray, np.ndarray: + The centroids and roi_IDs of the spots found, + the image used for spot detection, and + numpy array with only sufficiently large regions + retained (bigger than threshold number of pixels), and dilated by expansion amount (possibly) """ # TODO: Do not use hard-coded sigma values (second parameter to gaussian(...)). @@ -45,13 +48,19 @@ def detect_spots_dog_old(input_img, spot_threshold: Numeric, expand_px: int = 10 img = (img - np.mean(img)) / np.std(img) labels, _ = ndi.label(img > spot_threshold) labels = expand_labels(labels, expand_px) - + # Make a DataFrame with the ROI info. - spot_props = _reindex_to_roi_id(pd.DataFrame(regionprops_table( - label_image=labels, - intensity_image=input_img, - properties=('label', 'centroid_weighted', 'intensity_mean') - )).drop(['label'], axis=1).rename(columns=CENTROID_COLUMNS_REMAPPING)) + spot_props = _reindex_to_roi_id( + pd.DataFrame( + regionprops_table( + label_image=labels, + intensity_image=input_img, + properties=("label", "centroid_weighted", "intensity_mean"), + ) + ) + .drop(["label"], axis=1) + .rename(columns=CENTROID_COLUMNS_REMAPPING) + ) return spot_props, img, labels @@ -66,16 +75,16 @@ def detect_spots_int_old(input_img, spot_threshold: Numeric, expand_px: int = 1) spot_threshold : NumberLike Threshold to use for spots expand_px : int - Number of pixels by which to expand contiguous subregion, + Number of pixels by which to expand contiguous subregion, up to point of overlap with neighboring subregion of image - + Returns ------- - pd.DataFrame, np.ndarray, np.ndarray: - The centroids and roi_IDs of the spots found, - the image used for spot detection, and - numpy array with only sufficiently large regions - retained (bigger than threshold number of pixels), + pd.DataFrame, np.ndarray, np.ndarray: + The centroids and roi_IDs of the spots found, + the image used for spot detection, and + numpy array with only sufficiently large regions + retained (bigger than threshold number of pixels), and dilated by expansion amount (possibly) """ # TODO: enforce that output column names don't vary with code path walked. @@ -85,33 +94,59 @@ def detect_spots_int_old(input_img, spot_threshold: Numeric, expand_px: int = 1) binary = ndi.binary_fill_holes(binary) struct = ndi.generate_binary_structure(input_img.ndim, 2) labels, n_obj = ndi.label(binary, structure=struct) - if n_obj > 1: # Do not need this with area filtering below + if n_obj > 1: # Do not need this with area filtering below labels = remove_small_objects(labels, min_size=5) if expand_px > 0: labels = expand_labels(labels, expand_px) - if np.all(labels == 0): # No substructures (ROIs) exist after filtering. - spot_props = pd.DataFrame(columns=['label', 'z_min', 'y_min', 'x_min', 'z_max', 'y_max', 'x_max', 'area', 'zc', 'yc', 'xc', 'intensity_mean']) + if np.all(labels == 0): # No substructures (ROIs) exist after filtering. + spot_props = pd.DataFrame( + columns=[ + "label", + "z_min", + "y_min", + "x_min", + "z_max", + "y_max", + "x_max", + "area", + "zc", + "yc", + "xc", + "intensity_mean", + ] + ) else: spot_props = _reindex_to_roi_id( - pd.DataFrame(regionprops_table( - labels, input_img, properties=('label', 'bbox', 'area', 'centroid_weighted', 'intensity_mean') - )).rename( - columns={**CENTROID_COLUMNS_REMAPPING, - 'bbox-0': 'z_min', - 'bbox-1': 'y_min', - 'bbox-2': 'x_min', - 'bbox-3': 'z_max', - 'bbox-4': 'y_max', - 'bbox-5': 'x_max' - } - ) + pd.DataFrame( + regionprops_table( + labels, + input_img, + properties=( + "label", + "bbox", + "area", + "centroid_weighted", + "intensity_mean", + ), + ) + ).rename( + columns={ + **CENTROID_COLUMNS_REMAPPING, + "bbox-0": "z_min", + "bbox-1": "y_min", + "bbox-2": "x_min", + "bbox-3": "z_max", + "bbox-4": "y_max", + "bbox-5": "x_max", + } ) + ) return spot_props, input_img, labels def _index_as_roi_id(props_table: pd.DataFrame) -> pd.DataFrame: - return props_table.rename(columns={'index': 'roi_id'}) + return props_table.rename(columns={"index": "roi_id"}) def _reindex_to_roi_id(props_table: pd.DataFrame) -> pd.DataFrame: diff --git a/spotfishing/detectors.py b/spotfishing/detectors.py index af74238..642643f 100644 --- a/spotfishing/detectors.py +++ b/spotfishing/detectors.py @@ -9,8 +9,8 @@ from skimage.measure import regionprops_table from skimage.morphology import ball, remove_small_objects, white_tophat from skimage.segmentation import expand_labels -from .exceptions import DimensionalityError +from .exceptions import DimensionalityError __author__ = "Vince Reuter" __credits__ = ["Vince Reuter", "Kai Sandoval Beckwith"] @@ -19,10 +19,16 @@ Numeric = Union[int, float] -CENTROID_COLUMNS_REMAPPING = {'centroid_weighted-0': 'zc', 'centroid_weighted-1': 'yc', 'centroid_weighted-2': 'xc'} +CENTROID_COLUMNS_REMAPPING = { + "centroid_weighted-0": "zc", + "centroid_weighted-1": "yc", + "centroid_weighted-2": "xc", +} -def detect_spots_dog(*, input_image, spot_threshold: Numeric, expand_px: Optional[Numeric]): +def detect_spots_dog( + *, input_image, spot_threshold: Numeric, expand_px: Optional[Numeric] +): """Spot detection by difference of Gaussians filter Arguments @@ -32,26 +38,30 @@ def detect_spots_dog(*, input_image, spot_threshold: Numeric, expand_px: Optiona spot_threshold : int or float Minimum peak value after the DoG transformation to call a peak/spot expand_px : float or int or NoneType - Number of pixels by which to expand contiguous subregion, + Number of pixels by which to expand contiguous subregion, up to point of overlap with neighboring subregion of image - + Returns ------- - pd.DataFrame, np.ndarray, np.ndarray: - The centroids and roi_IDs of the spots found, - the image used for spot detection, and - numpy array with only sufficiently large regions - retained (bigger than threshold number of pixels), + pd.DataFrame, np.ndarray, np.ndarray: + The centroids and roi_IDs of the spots found, + the image used for spot detection, and + numpy array with only sufficiently large regions + retained (bigger than threshold number of pixels), and dilated by expansion amount (possibly) """ _check_input_image(input_image) img = _preprocess_for_difference_of_gaussians(input_image) labels, _ = ndi.label(img > spot_threshold) - spot_props, labels = _build_props_table(labels=labels, input_image=input_image, expand_px=expand_px) + spot_props, labels = _build_props_table( + labels=labels, input_image=input_image, expand_px=expand_px + ) return spot_props, img, labels -def detect_spots_int(*, input_image, spot_threshold: Numeric, expand_px: Optional[Numeric]): +def detect_spots_int( + *, input_image, spot_threshold: Numeric, expand_px: Optional[Numeric] +): """Spot detection by intensity filter Arguments @@ -61,16 +71,16 @@ def detect_spots_int(*, input_image, spot_threshold: Numeric, expand_px: Optiona spot_threshold : NumberLike Minimum intensity value in a pixel to consider it as part of a spot region expand_px : float or int or NoneType - Number of pixels by which to expand contiguous subregion, + Number of pixels by which to expand contiguous subregion, up to point of overlap with neighboring subregion of image - + Returns ------- - pd.DataFrame, np.ndarray, np.ndarray: - The centroids and roi_IDs of the spots found, - the image used for spot detection, and - numpy array with only sufficiently large regions - retained (bigger than threshold number of pixels), + pd.DataFrame, np.ndarray, np.ndarray: + The centroids and roi_IDs of the spots found, + the image used for spot detection, and + numpy array with only sufficiently large regions + retained (bigger than threshold number of pixels), and dilated by expansion amount (possibly) """ # TODO: enforce that output column names don't vary with code path walked. @@ -82,24 +92,37 @@ def detect_spots_int(*, input_image, spot_threshold: Numeric, expand_px: Optiona labels, num_obj = ndi.label(binary, structure=struct) if num_obj > 1: labels = remove_small_objects(labels, min_size=5) - spot_props, labels = _build_props_table(labels=labels, input_image=input_image, expand_px=expand_px) + spot_props, labels = _build_props_table( + labels=labels, input_image=input_image, expand_px=expand_px + ) return spot_props, input_image, labels -def _build_props_table(*, labels: np.ndarray, input_image: np.ndarray, expand_px: Optional[int]) -> Tuple[pd.DataFrame, np.ndarray]: +def _build_props_table( + *, labels: np.ndarray, input_image: np.ndarray, expand_px: Optional[int] +) -> Tuple[pd.DataFrame, np.ndarray]: if expand_px: labels = expand_labels(labels, expand_px) if np.all(labels == 0): # No substructures (ROIs) exist. - spot_props = pd.DataFrame(columns=['label', 'centroid_weighted-0', 'centroid_weighted-1', 'centroid_weighted-2', 'area', 'intensity_mean']) + spot_props = pd.DataFrame( + columns=[ + "label", + "centroid_weighted-0", + "centroid_weighted-1", + "centroid_weighted-2", + "area", + "intensity_mean", + ] + ) else: spot_props = regionprops_table( - label_image=labels, - intensity_image=input_image, - properties=('label', 'centroid_weighted', 'area', 'intensity_mean'), - ) + label_image=labels, + intensity_image=input_image, + properties=("label", "centroid_weighted", "area", "intensity_mean"), + ) spot_props = pd.DataFrame(spot_props) - spot_props = spot_props.drop(['label'], axis=1) + spot_props = spot_props.drop(["label"], axis=1) spot_props = spot_props.rename(columns=CENTROID_COLUMNS_REMAPPING) spot_props = spot_props.reset_index(drop=True) return spot_props, labels @@ -107,9 +130,13 @@ def _build_props_table(*, labels: np.ndarray, input_image: np.ndarray, expand_px def _check_input_image(img: np.ndarray) -> NoReturn: if not isinstance(img, np.ndarray): - raise TypeError(f"Expected numpy array for input image but got {type(img).__name__}") + raise TypeError( + f"Expected numpy array for input image but got {type(img).__name__}" + ) if img.ndim != 3: - raise DimensionalityError(f"Expected 3D input image but got {img.ndim}-dimensional") + raise DimensionalityError( + f"Expected 3D input image but got {img.ndim}-dimensional" + ) def _preprocess_for_difference_of_gaussians(input_image: np.ndarray) -> np.ndarray: diff --git a/tests/test_old_new_equivalence.py b/tests/test_old_new_equivalence.py index 2acd3a4..a1a4341 100644 --- a/tests/test_old_new_equivalence.py +++ b/tests/test_old_new_equivalence.py @@ -1,10 +1,11 @@ import os -import numpy as np from pathlib import Path + +import numpy as np import pytest -from spotfishing.deprecated import detect_spots_int_old, detect_spots_dog_old -from spotfishing import detect_spots_int, detect_spots_dog +from spotfishing import detect_spots_dog, detect_spots_int +from spotfishing.deprecated import detect_spots_dog_old, detect_spots_int_old OLD_PIXEL_EXPANSION_INT = 1 OLD_PIXEL_EXPANSION_DOG = 10 @@ -14,18 +15,32 @@ def get_img_data_file(fn: str) -> Path: return Path(os.path.dirname(__file__)) / "data" / fn -@pytest.mark.parametrize("data_path", [get_img_data_file(fn) for fn in ("img__p0_t57_c0__smaller.npy", "img__p13_t57_c0__smaller.npy")]) @pytest.mark.parametrize( - ["old_fun", "new_fun", "threshold", "expand_px"], - [(detect_spots_int_old, detect_spots_int, threshold, OLD_PIXEL_EXPANSION_INT) for threshold in (500, 300, 100)] + - [(detect_spots_dog_old, detect_spots_dog, threshold , OLD_PIXEL_EXPANSION_DOG) for threshold in (20, 15, 10)] + "data_path", + [ + get_img_data_file(fn) + for fn in ("img__p0_t57_c0__smaller.npy", "img__p13_t57_c0__smaller.npy") + ], +) +@pytest.mark.parametrize( + ["old_fun", "new_fun", "threshold", "expand_px"], + [ + (detect_spots_int_old, detect_spots_int, threshold, OLD_PIXEL_EXPANSION_INT) + for threshold in (500, 300, 100) + ] + + [ + (detect_spots_dog_old, detect_spots_dog, threshold, OLD_PIXEL_EXPANSION_DOG) + for threshold in (20, 15, 10) + ], ) def test_eqv(data_path, old_fun, new_fun, threshold, expand_px): data = np.load(data_path) old_table, _, _ = old_fun(data, threshold) - new_table, _, _ = new_fun(input_image=data, spot_threshold=threshold, expand_px=expand_px) + new_table, _, _ = new_fun( + input_image=data, spot_threshold=threshold, expand_px=expand_px + ) assert np.all(old_table.index == new_table.index) cols = ["zc", "yc", "xc", "area", "intensity_mean"] - sub_cols = [c for c in cols if c != "area"] # wasn't in DoG before + sub_cols = [c for c in cols if c != "area"] # wasn't in DoG before assert list(new_table.columns) == cols assert np.all(old_table[sub_cols].to_numpy() == new_table[sub_cols].to_numpy()) diff --git a/tests/test_spot_detectors.py b/tests/test_spot_detectors.py index e1ad498..ab36827 100644 --- a/tests/test_spot_detectors.py +++ b/tests/test_spot_detectors.py @@ -3,6 +3,7 @@ import hypothesis as hyp import numpy as np import pytest + from spotfishing import detect_spots_dog, detect_spots_int __author__ = "Vince Reuter" @@ -12,17 +13,19 @@ @pytest.mark.parametrize("detect", [detect_spots_dog, detect_spots_int]) def test_spot_table_columns__are_always_as_expected(detect): pass - #spots, img, labs = detect(np.array([], shape=())) + # spots, img, labs = detect(np.array([], shape=())) @pytest.mark.skip("not implemented") @pytest.mark.parametrize("detect", [detect_spots_dog, detect_spots_int]) @pytest.mark.parametrize(["input_image", "expect"], []) @hyp.given( - threshold=hyp.strategies.integers(), - pixel_expansion=hyp.strategies.integers(), + threshold=hyp.strategies.integers(), + pixel_expansion=hyp.strategies.integers(), ) -def test_detectors_give_proper_error_for_bad_input_image(detect, input_image, threshold, pixel_expansion, expect): +def test_detectors_give_proper_error_for_bad_input_image( + detect, input_image, threshold, pixel_expansion, expect +): with pytest.raises(type(expect)) as err_ctx: detect(input_image=input_image, threshold=threshold, expand_px=pixel_expansion) assert err_ctx.value == expect