diff --git a/topostats/classes.py b/topostats/classes.py index f0f3d7239ca..ff0391d9baa 100644 --- a/topostats/classes.py +++ b/topostats/classes.py @@ -201,7 +201,7 @@ def __str__(self) -> str: ) @property - def image(self) -> npt.NDArray[float]: + def image(self) -> npt.NDArray[np.floating]: """ Getter for the ``image`` attribute. @@ -213,7 +213,7 @@ def image(self) -> npt.NDArray[float]: return self._image @image.setter - def image(self, value: npt.NDArray[float]): + def image(self, value: npt.NDArray[np.floating]): """ Setter for the ``image`` attribute. @@ -892,21 +892,21 @@ class MatchedBranch: Attributes ---------- - ordered_coords : npt.NDArray[np.int32] + ordered_coords : npt.NDArray[np.int32], optional Numpy array of ordered coordinates. - heights : npt.NDArray[np.number] + heights : npt.NDArray[np.number], optional Numpy array of heights. - distances : npt.NDArray[np.number] + distances : npt.NDArray[np.number], optional Numpy array of distances. - fwhm : float + fwhm : float, optional Full-width half maximum. - fwhm_half_maxs : list[float] + fwhm_half_maxs : list[float], optional Half-maximums from a matched branch. - fwhm_peaks : list[int | float] + fwhm_peaks : list[float], optional Peaks from a matched branch. - angles : float + angles : float | list[float], optional Angle between branches. - branch_statistics : dict[str, float | int | list[Any] | str] + branch_statistics : dict[str, float | int | list[Any] | str], optional Dictionary of branch statistics, ``fwhm``, ``fwhm_half_maxs`` and ``fwhm_peaks``. """ @@ -971,11 +971,11 @@ class UnMatchedBranch: Attributes ---------- - angles : float + angles : float | list[float] Angle between branches. """ - angles: float | list[float] | None = None + angles: float | list[float] def __str__(self) -> str: """ @@ -1061,23 +1061,23 @@ class OrderedTrace: """ Class for Ordered Trace data and attributes. - molecule_data : dict[int, Molecule] + molecule_data : dict[int, Molecule], optional Dictionary of ``Molecule`` objects indexed by molecule number. - tracing_stats : dict | None + tracing_stats : dict, optional Tracing statistics. - grain_molstats : Any | None + grain_molstats : Any, optional Grain molecule statistics. - molecules : int + molecules : int, optional Number of molecules within the grain. - writhe : str + writhe : str, optional The writhe sign, can be either `+`, `-` or `0` for positive, negative or no writhe. - pixel_to_nm_scaling: np.float64 | None + pixel_to_nm_scaling: np.float64, optional Pixel to nm scaling. - images: dict[str, npt.NDArray] | None + images: dict[str, npt.NDArray], optional Diagnostic images produced during processing. - error: bool | None + error: bool, optional Errors encountered? - molecule_statistics : dict[int, dict[str, bool | str | float | None]] | None + molecule_statistics : dict[int, dict[str, bool | str | float | None]], optional Dictionary of molecule statistics, with one entry for each molecule. """ @@ -1104,7 +1104,7 @@ def __str__(self) -> str: writhe = {"+": "positive", "-": "negative", "0": "no writhe"}.get(self.writhe) return ( f"number of molecules : {self.molecules}\n" - f"number of images : {len(self.images)}\n" + f"number of images : {len(self.images) if self.images is not None else 'None'}\n" f"writhe : {writhe}\n" f"pixel to nm scaling : {self.pixel_to_nm_scaling}\n" f"error : {self.error}" @@ -1128,7 +1128,7 @@ def require_molecule_data(self) -> dict[int, Molecule]: raise RuntimeError("molecule_data is None") return self.molecule_data - def collate_molecule_statistics(self) -> dict[int, dict[str, bool | int | str | None]]: + def collate_molecule_statistics(self) -> dict[int, dict[str, bool | int | str | float | None]]: """ Collate molecule statistics for all molecules to dictionary. @@ -1136,9 +1136,12 @@ def collate_molecule_statistics(self) -> dict[int, dict[str, bool | int | str | Returns ------- - dict[int, dict[str, bool | int | str | None]] + dict[int, dict[str, bool | int | str float | None]] Dictionary, indexed by molecule where the value is the molecules statistics for the given molecule. """ + if self.molecule_data is None: + raise ValueError("No molecule data found") + self.molecule_statistics = { molecule_number: molecule.collate_molecule_statistics() for molecule_number, molecule in self.molecule_data.items() diff --git a/topostats/filters.py b/topostats/filters.py index a2f1afb5a90..b10dfba8b10 100644 --- a/topostats/filters.py +++ b/topostats/filters.py @@ -66,9 +66,9 @@ def __init__( otsu_threshold_multiplier: float = 1.7, threshold_std_dev: dict | None = None, threshold_absolute: dict | None = None, - gaussian_size: float = None, + gaussian_size: float | None = None, gaussian_mode: str = "nearest", - remove_scars: dict = None, + remove_scars: dict | None = None, ): """ Initialise the class. @@ -153,7 +153,7 @@ def __init__( } def median_flatten( - self, image: npt.NDArray, mask: npt.NDArray = None, row_alignment_quantile: float = 0.5 + self, image: npt.NDArray, mask: npt.NDArray | None = None, row_alignment_quantile: float = 0.5 ) -> npt.NDArray: """ Flatten images using median differences. @@ -167,7 +167,7 @@ def median_flatten( ---------- image : npt.NDArray 2-D image of the data to align the rows of. - mask : npt.NDArray + mask : npt.NDArray, optional Boolean array of points to mask (ignore). row_alignment_quantile : float Quantile (in the range 0.0 to 1.0) used for defining the average background. @@ -196,7 +196,7 @@ def median_flatten( return image - def remove_tilt(self, image: npt.NDArray, mask: npt.NDArray = None) -> npt.NDArray: + def remove_tilt(self, image: npt.NDArray, mask: npt.NDArray | None = None) -> npt.NDArray: """ Remove the planar tilt from an image (linear in 2D spaces). @@ -207,7 +207,7 @@ def remove_tilt(self, image: npt.NDArray, mask: npt.NDArray = None) -> npt.NDArr ---------- image : npt.NDArray 2-D image of the data to remove the planar tilt from. - mask : npt.NDArray + mask : npt.NDArray, optional Boolean array of points to mask (ignore). Returns @@ -361,7 +361,7 @@ def model_func(x: float, y: float, a: float, b: float, c: float, d: float) -> fl return image - def remove_quadratic(self, image: npt.NDArray, mask: npt.NDArray = None) -> npt.NDArray: + def remove_quadratic(self, image: npt.NDArray, mask: npt.NDArray | None = None) -> npt.NDArray: """ Remove the quadratic bowing that can be seen in some large-scale AFM images. @@ -372,7 +372,7 @@ def remove_quadratic(self, image: npt.NDArray, mask: npt.NDArray = None) -> npt. ---------- image : npt.NDArray 2-D image of the data to remove the quadratic from. - mask : npt.NDArray + mask : npt.NDArray, optional Boolean array of points to mask (ignore). Returns @@ -445,7 +445,7 @@ def calc_gradient(self, array: npt.NDArray, shape: int) -> npt.NDArray: """ return self.calc_diff(array) / shape - def average_background(self, image: npt.NDArray, mask: npt.NDArray = None) -> npt.NDArray: + def average_background(self, image: npt.NDArray, mask: npt.NDArray | None = None) -> npt.NDArray: """ Zero the background by subtracting the non-masked mean from all pixels. @@ -453,7 +453,7 @@ def average_background(self, image: npt.NDArray, mask: npt.NDArray = None) -> np ---------- image : npt.NDArray Numpy array representing the image. - mask : npt.NDArray + mask : npt.NDArray, optional Mask of the array, should have the same dimensions as image. Returns @@ -515,17 +515,22 @@ def filter_image(self) -> None: # numpydoc: ignore=GL07 ... threshold_method='otsu') filter.filter_image() """ - self.images["initial_median_flatten"] = self.median_flatten( + self.images["initial_median_flatten"]: npt.NDArray = self.median_flatten( self.images["pixels"], mask=None, row_alignment_quantile=self.row_alignment_quantile ) - self.images["initial_tilt_removal"] = self.remove_tilt(self.images["initial_median_flatten"], mask=None) - self.images["initial_quadratic_removal"] = self.remove_quadratic(self.images["initial_tilt_removal"], mask=None) - self.images["initial_nonlinear_polynomial_removal"] = self.remove_nonlinear_polynomial( + self.images["initial_tilt_removal"]: npt.NDArray = self.remove_tilt( + self.images["initial_median_flatten"], mask=None + ) + self.images["initial_quadratic_removal"]: npt.NDArray = self.remove_quadratic( + self.images["initial_tilt_removal"], mask=None + ) + self.images["initial_nonlinear_polynomial_removal"]: npt.NDArray = self.remove_nonlinear_polynomial( self.images["initial_quadratic_removal"], mask=None ) # Remove scars run_scar_removal = self.remove_scars_config.pop("run") + self.images["initial_scar_removal"]: npt.NDArray if run_scar_removal: LOGGER.debug(f"[{self.filename}] : Initial scar removal") self.images["initial_scar_removal"], _ = scars.remove_scars( @@ -538,7 +543,7 @@ def filter_image(self) -> None: # numpydoc: ignore=GL07 self.images["initial_scar_removal"] = self.images["initial_nonlinear_polynomial_removal"] # Zero the data before thresholding, helps with absolute thresholding - self.images["initial_zero_average_background"] = self.average_background( + self.images["initial_zero_average_background"]: npt.NDArray = self.average_background( self.images["initial_scar_removal"], mask=None ) @@ -558,19 +563,22 @@ def filter_image(self) -> None: # numpydoc: ignore=GL07 thresholds=self.thresholds, img_name=self.filename, ) - self.images["masked_median_flatten"] = self.median_flatten( + self.images["masked_median_flatten"]: npt.NDArray = self.median_flatten( self.images["initial_tilt_removal"], self.images["mask"], row_alignment_quantile=self.row_alignment_quantile, ) - self.images["masked_tilt_removal"] = self.remove_tilt(self.images["masked_median_flatten"], self.images["mask"]) - self.images["masked_quadratic_removal"] = self.remove_quadratic( + self.images["masked_tilt_removal"]: npt.NDArray = self.remove_tilt( + self.images["masked_median_flatten"], self.images["mask"] + ) + self.images["masked_quadratic_removal"]: npt.NDArray = self.remove_quadratic( self.images["masked_tilt_removal"], self.images["mask"] ) - self.images["masked_nonlinear_polynomial_removal"] = self.remove_nonlinear_polynomial( + self.images["masked_nonlinear_polynomial_removal"]: npt.NDArray = self.remove_nonlinear_polynomial( self.images["masked_quadratic_removal"], self.images["mask"] ) # Remove scars + self.images["secondary_scar_removal"]: npt.NDArray if run_scar_removal: LOGGER.debug(f"[{self.filename}] : Secondary scar removal") self.images["secondary_scar_removal"], scar_mask = scars.remove_scars( @@ -582,7 +590,7 @@ def filter_image(self) -> None: # numpydoc: ignore=GL07 else: LOGGER.debug(f"[{self.filename}] : Skipping scar removal as requested from config") self.images["secondary_scar_removal"] = self.images["masked_nonlinear_polynomial_removal"] - self.images["final_zero_average_background"] = self.average_background( + self.images["final_zero_average_background"]: npt.NDArray = self.average_background( self.images["secondary_scar_removal"], self.images["mask"] ) self.images["gaussian_filtered"] = self.gaussian_filter(self.images["final_zero_average_background"]) diff --git a/topostats/grains.py b/topostats/grains.py index fb572580eef..5bfa0b37ba4 100644 --- a/topostats/grains.py +++ b/topostats/grains.py @@ -1252,7 +1252,7 @@ def keep_largest_labelled_region_classes( def calculate_region_connection_regions( grain_mask_tensor: npt.NDArray, classes: tuple[int, int], - ) -> tuple[int, npt.NDArray, dict[int, npt.NDArray[int]]]: + ) -> tuple[int, npt.NDArray, dict[int, npt.NDArray[np.integer]]]: """ Get a list of connection regions between two classes. diff --git a/topostats/measure/feret.py b/topostats/measure/feret.py index 218ec6903b4..30d13e513b9 100644 --- a/topostats/measure/feret.py +++ b/topostats/measure/feret.py @@ -14,6 +14,7 @@ from collections.abc import Generator from math import sqrt from pathlib import Path +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -233,7 +234,7 @@ def triangle_height(base1: npt.NDArray | list, base2: npt.NDArray | list, apex: base1_base2 = np.asarray(base1) - np.asarray(base2) base1_apex = np.asarray(base1) - np.asarray(apex) # Ensure arrays are 3-D see note in https://numpy.org/doc/2.0/reference/generated/numpy.cross.html - return np.linalg.norm(cross2d(base1_base2, base1_apex)) / np.linalg.norm(base1_base2) + return np.linalg.norm(cross2d(base1_base2, base1_apex)) / np.linalg.norm(base1_base2).astype(float) def cross2d(x: npt.NDArray, y: npt.NDArray) -> npt.NDArray: @@ -349,9 +350,7 @@ def sort_clockwise(coordinates: npt.NDArray) -> npt.NDArray: return coordinates[order] -def min_max_feret( - points: npt.NDArray, axis: int = 0, precision: int = 13 -) -> dict[float, tuple[int, int], float, tuple[int, int]]: +def min_max_feret(points: npt.NDArray, axis: int = 0, precision: int = 13) -> dict[str, Any]: """ Given a list of 2-D points, returns the minimum and maximum feret diameters. @@ -368,8 +367,8 @@ def min_max_feret( Returns ------- - dictionary - Tuple of the minimum feret distance and its coordinates and the maximum feret distance and its coordinates. + dict + Dictionary containing the minimum and maximum feret diameters of the grain and their corresponding coordinates of contact with the calipers. """ caliper_min_feret = list(rotating_calipers(points, axis)) min_ferets, calipers, min_feret_coords = zip(*caliper_min_feret) @@ -394,7 +393,7 @@ def min_max_feret( } -def get_feret_from_mask(mask_im: npt.NDArray, axis: int = 0) -> tuple[float, tuple[int, int], float, tuple[int, int]]: +def get_feret_from_mask(mask_im: npt.NDArray, axis: int = 0) -> dict[str, Any]: """ Calculate the minimum and maximum feret diameter of the foreground object of a binary mask. @@ -409,8 +408,8 @@ def get_feret_from_mask(mask_im: npt.NDArray, axis: int = 0) -> tuple[float, tup Returns ------- - Tuple[float, Tuple[int, int], float, Tuple[int, int]] - Returns a tuple of the minimum feret and its coordinates and the maximum feret and its coordinates. + dict + Dictionary containing the minimum and maximum feret diameters of the grain and their corresponding coordinates of contact with the calipers. """ eroded = skimage.morphology.erosion(mask_im) outline = mask_im ^ eroded diff --git a/topostats/processing.py b/topostats/processing.py index deac673fc74..1996bf2342c 100644 --- a/topostats/processing.py +++ b/topostats/processing.py @@ -1031,8 +1031,8 @@ def process_scan( Parameters ---------- topostats_object : TopoStats - A dictionary with keys 'image', 'img_path' and 'pixel_to_nm_scaling' containing a file or frames' image, it's - path and it's pixel to namometre scaling value. + A Topostats object with attributes 'image', 'img_path' and 'pixel_to_nm_scaling' containing a file or frames' image, its + path and its pixel to namometre scaling value. base_dir : str | Path Directory to recursively search for files, if not specified the current directory is scanned. filter_config : dict @@ -1065,7 +1065,7 @@ def process_scan( """ # Setup configuration, we use that from the topostats_object.config if not explicitly given an option config = topostats_object.config.copy() - base_dir = config["base_dir"] if base_dir is None else base_dir + base_dir = Path(config["base_dir"] if base_dir is None else base_dir) filter_config = config["filter"] if filter_config is None else filter_config grains_config = config["grains"] if grains_config is None else grains_config grainstats_config = config["grainstats"] if grainstats_config is None else grainstats_config @@ -1077,7 +1077,7 @@ def process_scan( splining_config = config["splining"] if splining_config is None else splining_config curvature_config = config["curvature"] if curvature_config is None else curvature_config plotting_config = config["plotting"].copy() if plotting_config is None else plotting_config - output_dir = config["output_dir"] if output_dir is None else output_dir + output_dir = Path(config["output_dir"] if output_dir is None else output_dir) # Get output paths core_out_path, filter_out_path, grain_out_path, tracing_out_path = get_out_paths( @@ -1273,7 +1273,7 @@ def process_scan( def process_filters( - topostats_object: dict, + topostats_object: TopoStats, base_dir: str | Path, filter_config: dict, plotting_config: dict, @@ -1287,9 +1287,8 @@ def process_filters( Parameters ---------- - topostats_object : dict[str, Union[npt.NDArray, Path, float]] - A dictionary with keys 'image', 'img_path' and 'pixel_to_nm_scaling' containing a file or frames' image, it's - path and it's pixel to namometre scaling value. + topostats_object : Topostats + An object of type ``TopoStats`` class with a minimum of ``image_original``, ``filename`` and ``pixel_to_nm_scaling`` attributes which allow filtering to be run. base_dir : str | Path Directory to recursively search for files, if not specified the current directory is scanned. filter_config : dict @@ -1307,7 +1306,7 @@ def process_filters( """ # Setup configuration, we use that from the topostats_object.config if not explicitly given an option config = topostats_object.config.copy() - base_dir = config["base_dir"] if base_dir is None else base_dir + base_dir = Path(config["base_dir"] if base_dir is None else base_dir) filter_config = config["filter"] if filter_config is None else filter_config plotting_config = config["plotting"] if plotting_config is None else plotting_config output_dir = config["output_dir"] @@ -1341,7 +1340,7 @@ def process_filters( def process_grains( - topostats_object: dict, + topostats_object: TopoStats, base_dir: str | Path, grains_config: dict, plotting_config: dict, @@ -1355,9 +1354,9 @@ def process_grains( Parameters ---------- - topostats_object : dict[str, Union[npt.NDArray, Path, float]] - A dictionary with keys 'image', 'img_path' and 'pixel_to_nm_scaling' containing a file or frames' image, it's - path and it's pixel to namometre scaling value. + topostats_object : TopoStats + An object of type ``TopoStats`` class. + base_dir : str | Path Directory to recursively search for files, if not specified the current directory is scanned. grains_config : dict @@ -1375,7 +1374,7 @@ def process_grains( """ # Setup configuration, we use that from the topostats_object.config if not explicitly given an option config = topostats_object.config.copy() - base_dir = config["base_dir"] if base_dir is None else base_dir + base_dir = Path(config["base_dir"] if base_dir is None else base_dir) grains_config = config["grains"] if grains_config is None else grains_config plotting_config = config["plotting"] if plotting_config is None else plotting_config output_dir = config["output_dir"] @@ -1413,7 +1412,7 @@ def process_grainstats( grainstats_config: dict, plotting_config: dict, output_dir: str | Path = "output", -) -> tuple[str, bool]: +) -> tuple[str | None, TopoStats, pd.DataFrame | None]: """ Calculate grain statistics in an image where grains have already been detected. @@ -1436,12 +1435,12 @@ def process_grainstats( Returns ------- - tuple[str, pd.DataFrame] - A tuple of the image and a boolean indicating if the image was successfully processed. + tuple[str | None, TopoStats, pd.DataFrame | None] + A tuple of the image name, the updated TopoStats object, and the grain statistics DataFrame or None.``` """ # Setup configuration, we use that from the topostats_object.config if not explicitly given an option config = topostats_object.config.copy() - base_dir = config["base_dir"] if base_dir is None else base_dir + base_dir = Path(config["base_dir"] if base_dir is None else base_dir) grainstats_config = config["grainstats"] if grainstats_config is None else grainstats_config plotting_config = config["plotting"] if plotting_config is None else plotting_config output_dir = config["output_dir"] @@ -1470,7 +1469,7 @@ def process_grainstats( ) except: # noqa: E722 # pylint: disable=bare-except LOGGER.info(f"Grain detection failed for image : {topostats_object.filename}") - return topostats_object + return None, topostats_object, None # Grain Statistics grain_stats = { grain_number: grain_crop.stats for grain_number, grain_crop in topostats_object.grain_crops.items() diff --git a/topostats/scars.py b/topostats/scars.py index cd1f21677ab..4038e381eb2 100644 --- a/topostats/scars.py +++ b/topostats/scars.py @@ -348,7 +348,7 @@ def remove_scars( threshold_high: float = 0.666, max_scar_width: int = 4, min_scar_length: int = 16, -): +) -> tuple[npt.NDArray, npt.NDArray]: """ Remove scars from an image. @@ -385,10 +385,14 @@ class was instantiated. Returns ------- - self.img + img: npt.NDArray The original 2-D image with scars removed, unless the config has run set to False, in which case it will not remove the scars. + first_marked_mask: npt.NDArray + The scars detected during the first iteration. """ + assert removal_iterations > 0 + LOGGER.info(f"[{filename}] : Removing scars") first_marked_mask = None @@ -418,5 +422,6 @@ class was instantiated. _remove_marked_scars(img, np.copy(marked_both)) LOGGER.debug("Scars removed") + assert first_marked_mask is not None return img, first_marked_mask diff --git a/topostats/theme.py b/topostats/theme.py index 2dc782b1119..7f8a9fccd23 100644 --- a/topostats/theme.py +++ b/topostats/theme.py @@ -1,6 +1,8 @@ """Custom Bruker Nanoscope colorscale.""" import logging +from collections.abc import Sequence +from typing import Literal import matplotlib as mpl import matplotlib.cm @@ -68,13 +70,13 @@ def set_cmap(self, name: str) -> None: self.cmap = mpl.colormaps[name] LOGGER.debug(f"[theme] Colormap set to : {name}") - def get_cmap(self) -> matplotlib.cm: + def get_cmap(self) -> mpl.colors.Colormap | None: """ Return the matplotlib.cm colormap object. Returns ------- - matplotlib.cm + matplotlib.colors.Colormap | None Matplotlib Color map object. """ return self.cmap @@ -91,7 +93,7 @@ def nanoscope() -> LinearSegmentedColormap: LinearSegmentedColormap MatplotLib LinearSegmentedColourmap that replicates Bruker Nanoscope colorscale. """ - cdict = { + cdict: dict[Literal["red", "green", "blue", "alpha"], Sequence[tuple[int | float, ...]]] = { "red": ( (0.0, 0.0, 0.0), (0.124464, 0.0, 0.0), diff --git a/topostats/thresholds.py b/topostats/thresholds.py index 6c5b06bb5ae..171e940d731 100644 --- a/topostats/thresholds.py +++ b/topostats/thresholds.py @@ -15,7 +15,7 @@ # pylint: disable=unused-argument -def threshold(image: npt.NDArray, method: str = None, otsu_threshold_multiplier: float = None, **kwargs: dict) -> float: +def threshold(image: npt.NDArray, method: str, otsu_threshold_multiplier: float | None = None, **kwargs: dict) -> float: """ Thresholding for producing masks. @@ -25,7 +25,7 @@ def threshold(image: npt.NDArray, method: str = None, otsu_threshold_multiplier: 2-D Numpy array of image for thresholding. method : str Method to use for thresholding, currently supported methods are otsu (default), mean and minimum. - otsu_threshold_multiplier : float + otsu_threshold_multiplier : float, optional Factor for scaling the Otsu threshold. **kwargs : dict Additional keyword arguments to pass to skimage methods. @@ -71,7 +71,7 @@ def _get_threshold(method: str = "otsu") -> Callable: raise ValueError(method) -def _threshold_otsu(image: npt.NDArray, otsu_threshold_multiplier: float = None, **kwargs) -> float: +def _threshold_otsu(image: npt.NDArray, otsu_threshold_multiplier: float, **kwargs) -> float: """ Calculate the Otsu threshold. @@ -95,7 +95,7 @@ def _threshold_otsu(image: npt.NDArray, otsu_threshold_multiplier: float = None, return threshold_otsu(image, **kwargs) * otsu_threshold_multiplier -def _threshold_mean(image: npt.NDArray, otsu_threshold_multiplier: float = None, **kwargs) -> float: +def _threshold_mean(image: npt.NDArray, otsu_threshold_multiplier: float | None = None, **kwargs) -> float: """ Calculate the Mean threshold. @@ -106,7 +106,7 @@ def _threshold_mean(image: npt.NDArray, otsu_threshold_multiplier: float = None, ---------- image : npt.NDArray 2-D Numpy array of image for thresholding. - otsu_threshold_multiplier : float + otsu_threshold_multiplier : float, optional Factor for scaling (not used). **kwargs : dict Dictionary of keyword arguments to pass to 'skimage.filters.threshold_mean(**kwargs)'. @@ -119,7 +119,7 @@ def _threshold_mean(image: npt.NDArray, otsu_threshold_multiplier: float = None, return threshold_mean(image, **kwargs) -def _threshold_minimum(image: npt.NDArray, otsu_threshold_multiplier: float = None, **kwargs) -> float: +def _threshold_minimum(image: npt.NDArray, otsu_threshold_multiplier: float | None = None, **kwargs) -> float: """ Calculate the Minimum threshold. @@ -130,7 +130,7 @@ def _threshold_minimum(image: npt.NDArray, otsu_threshold_multiplier: float = No ---------- image : npt.NDArray 2-D Numpy array of image for thresholding. - otsu_threshold_multiplier : float + otsu_threshold_multiplier : float, optional Factor for scaling (not used). **kwargs : dict Dictionary of keyword arguments to pass to 'skimage.filters.threshold_minimum(**kwargs)'. @@ -143,7 +143,7 @@ def _threshold_minimum(image: npt.NDArray, otsu_threshold_multiplier: float = No return threshold_minimum(image, **kwargs) -def _threshold_yen(image: npt.NDArray, otsu_threshold_multiplier: float = None, **kwargs) -> float: +def _threshold_yen(image: npt.NDArray, otsu_threshold_multiplier: float | None = None, **kwargs) -> float: """ Calculate the Yen threshold. @@ -154,7 +154,7 @@ def _threshold_yen(image: npt.NDArray, otsu_threshold_multiplier: float = None, ---------- image : npt.NDArray 2-D Numpy array of image for thresholding. - otsu_threshold_multiplier : float + otsu_threshold_multiplier : float, optional Factor for scaling (not used). **kwargs : dict Dictionary of keyword arguments to pass to 'skimage.filters.threshold_yen(**kwargs)'. @@ -167,7 +167,7 @@ def _threshold_yen(image: npt.NDArray, otsu_threshold_multiplier: float = None, return threshold_yen(image, **kwargs) -def _threshold_triangle(image: npt.NDArray, otsu_threshold_multiplier: float = None, **kwargs) -> float: +def _threshold_triangle(image: npt.NDArray, otsu_threshold_multiplier: float | None = None, **kwargs) -> float: """ Calculate the triangle threshold. @@ -178,7 +178,7 @@ def _threshold_triangle(image: npt.NDArray, otsu_threshold_multiplier: float = N ---------- image : npt.NDArray 2-D Numpy array of image for thresholding. - otsu_threshold_multiplier : float + otsu_threshold_multiplier : float, optional Factor for scaling (not used). **kwargs : dict Dictionary of keyword arguments to pass to 'skimage.filters.threshold_triangle(**kwargs)'. diff --git a/topostats/tracing/ordered_tracing.py b/topostats/tracing/ordered_tracing.py index ea822389610..da4457a83d4 100644 --- a/topostats/tracing/ordered_tracing.py +++ b/topostats/tracing/ordered_tracing.py @@ -213,8 +213,8 @@ def compile_images( @staticmethod def remove_common_values( - ordered_array: npt.NDArray, common_value_check_array: npt.NDArray, retain: list = () - ) -> np.array: + ordered_array: npt.NDArray, common_value_check_array: npt.NDArray, retain: list | None = None + ) -> npt.NDArray: """ Remove common values in common_value_check_array from ordered_array while retaining specified coordinates. @@ -225,13 +225,16 @@ def remove_common_values( common_value_check_array : npt.NDArray Coordinate array containing any common values to be removed from ordered_array. retain : list, optional - List of possible coordinates to keep, by default (). + List of possible coordinates to keep. Returns ------- - np.array + npt.NDArray Unique ordered_array values and retained coordinates. Retains the order of ordered_array. """ + if retain is None: + retain = [] + # Convert the arrays to sets for faster common value lookup set_arr2 = {tuple(row) for row in common_value_check_array} set_retain = {tuple(row) for row in retain} @@ -442,7 +445,7 @@ def get_trace_segment(remaining_img: npt.NDArray, ordered_segment_coords: list, return ordered_segment_coords[coord_idx][::-1] # end is endpoint @staticmethod - def order_from_end(last_segment_coord: npt.NDArray, current_segment: npt.NDArray) -> npt.NDArray: + def order_from_end(last_segment_coord: npt.NDArray, current_segment: npt.NDArray) -> tuple[npt.NDArray, bool]: """ Order the current segment to follow from the end of the previous one. @@ -587,14 +590,16 @@ def check_node_errorless(self) -> bool: return False return True - def identify_writhes(self) -> str | dict: + def identify_writhes(self) -> tuple[str, dict]: """ Identify the writhe topology at each crossing in the image. Returns ------- - str | dict - A string of the whole grain writhe sign, and a dictionary linking each node to it's sign. + writhe_string: str + A string of the whole grain writhe sign + node_to_writhe: dict + a dictionary linking each node to its sign. """ # compile all vectors for each node and their z_idx # - want for each node, ordered vectors according to z_idx @@ -651,7 +656,8 @@ def run_nodestats_tracing(self) -> dict[str, npt.NDArray]: Returns ------- tuple[list, dict, dict] - A list of each molecules ordered trace coordinates, the ordered_tracing stats, and the images. + dict[str, npt.NDArray] + A dictionary of ordered trace images. """ ordered_traces, topology, cross_add, crossing_coords, fwhms = self.compile_trace( reverse_min_conf_crossing=False @@ -668,8 +674,9 @@ def run_nodestats_tracing(self) -> dict[str, npt.NDArray]: # each molecule this is stored along with the original topology_flip = self.compile_trace(reverse_min_conf_crossing=True)[1] - molecule_data = {} if ordered_traces is not None else None + molecule_data = None if ordered_traces is not None: + molecule_data = {} for i, mol_trace in enumerate(ordered_traces): if len(mol_trace) > 3: # if > 4 coords to trace self.mol_tracing_stats["circular"] = linear_or_circular(mol_trace[:, :2]) diff --git a/topostats/utils.py b/topostats/utils.py index 108e15e06ea..878f759c585 100644 --- a/topostats/utils.py +++ b/topostats/utils.py @@ -246,7 +246,9 @@ def create_empty_dataframe(column_set: str = "grainstats") -> pd.DataFrame: return pd.DataFrame(columns=COLUMN_SETS[column_set]) -def bound_padded_coordinates_to_image(coordinates: npt.NDArray, padding: int, image_shape: tuple) -> tuple: +def bound_padded_coordinates_to_image( + coordinates: npt.NDArray, padding: int, image_shape: tuple[int, int] +) -> tuple[int, int]: """ Ensure the padding of coordinates points does not fall outside of the image shape. @@ -262,15 +264,15 @@ def bound_padded_coordinates_to_image(coordinates: npt.NDArray, padding: int, im Parameters ---------- coordinates : npt.NDArray - Coordinates of a point on the mask based skeleton. + Coordinates of a point on the mask based skeleton (length = 2). padding : int Number of pixels/cells to pad around the point. - image_shape : tuple + image_shape : tuple[int, int] The shape of the original image from which the pixel is obtained. Returns ------- - tuple + tuple[int, int] Returns a tuple of coordinates that ensure that when the point is padded by the noted padding width in subsequent calculations it will not be outside of the image shape. """ @@ -279,14 +281,14 @@ def bound_padded_coordinates_to_image(coordinates: npt.NDArray, padding: int, im max_col = image_shape[1] - 1 row_coord, col_coord = coordinates - def check(coord: npt.NDArray, max_val: int, padding: int) -> npt.NDArray: + def check(coord: int, max_val: int, padding: int) -> int: """ Check coordinates are within the bounds of the padding. Parameters ---------- - coord : npt.NDArray - Coordinates (length = 2). + coord : int + Coordinate (row_coord or col coord). max_val : int Maximum width in the dimension being checked (max_row or max_col). padding : int @@ -294,8 +296,8 @@ def check(coord: npt.NDArray, max_val: int, padding: int) -> npt.NDArray: Returns ------- - npt.NDArray - Coordinates adjusted for padding. + int + Coordinate adjusted for padding. """ if coord - padding < 0: coord = padding