Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9b22f18
update doctring typehints for MatchedBranch
ubdbra001 Mar 4, 2026
da0dd7d
update type hints for UnMatchedBranch
ubdbra001 Mar 4, 2026
676b9e9
set molecule_data outside then inside if-statement
ubdbra001 Mar 4, 2026
9cd75f9
correct return type hint for remove_common_values
ubdbra001 Mar 4, 2026
aed52e7
updating OrderedTrace doctsrings
ubdbra001 Mar 4, 2026
1cb93c1
add error to collate_molecule_statistics
ubdbra001 Mar 4, 2026
1398614
update str dunder for when images are None
ubdbra001 Mar 4, 2026
d27c270
update type hints for GrainCrop image get & set
ubdbra001 Mar 4, 2026
67da943
update return type hint for calculate_region_connection_regions
ubdbra001 Mar 4, 2026
9e66b26
Set directories to Path types in process_scan
ubdbra001 Mar 5, 2026
05f8e38
update param type and dir type in process_filters
ubdbra001 Mar 5, 2026
d403998
update param type and dir type in process_grains
ubdbra001 Mar 5, 2026
e9248fa
update return type and dir type in process_grainstats
ubdbra001 Mar 5, 2026
7e447bf
[pre-commit.ci] Fixing issues with pre-commit
pre-commit-ci[bot] Mar 5, 2026
2b25eb4
Update run_nodestats_tracing docstring
ubdbra001 Mar 5, 2026
67baf5f
Update docstring for process_grainstats
ubdbra001 Mar 5, 2026
1333866
Update return description for process_grainstats docstring
ubdbra001 Mar 5, 2026
a8c39f2
Update return typehint for order_from_end
ubdbra001 Mar 5, 2026
305ac95
Update identify_writhes return type hints
ubdbra001 Mar 5, 2026
eb68419
Update param type hints in Filters class and methods
ubdbra001 Mar 17, 2026
879fc64
[pre-commit.ci] Fixing issues with pre-commit
pre-commit-ci[bot] Mar 17, 2026
9fa544c
update typehints in theme module
ubdbra001 Mar 17, 2026
480363e
update return type hint for remove_scars
ubdbra001 Mar 18, 2026
2bec3a7
add inline typehints for image dict values
ubdbra001 Mar 18, 2026
22951a3
update typing for bound_padded_coordinates_to_image and check functions
ubdbra001 Mar 18, 2026
ab6ebcb
update type hints for threshold functions
ubdbra001 Mar 18, 2026
15b5d60
convert return val of triangle height to float
ubdbra001 Mar 18, 2026
0fbbef8
update return type hints for min_max_feret and get_feret_from_mask
ubdbra001 Mar 18, 2026
09a4faa
[pre-commit.ci] Fixing issues with pre-commit
pre-commit-ci[bot] Mar 20, 2026
66e74f1
use NDArray not ArrayLike
ubdbra001 May 27, 2026
f627bd9
update doctrings for topostats_objects input arguments
ubdbra001 May 27, 2026
dc4a239
Merge branch 'main' into i1299-improve-typing-consistency
ubdbra001 May 27, 2026
88895ef
update remove_common_values input args
ubdbra001 Jun 9, 2026
2134a1f
updates to remove_scars
ubdbra001 Jun 9, 2026
a9bedc2
update return docstrings for feret functions
ubdbra001 Jun 9, 2026
9db4b99
address sylvia's feedback
ubdbra001 Jun 17, 2026
86492cf
[pre-commit.ci] Fixing issues with pre-commit
pre-commit-ci[bot] Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 27 additions & 24 deletions topostats/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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``.
"""

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been going around replacing | None with , optional in the docstrings. Probably doesn't have a real impact but I thought I'd make it all consistent.

Let me know if you'd prefer to retain | None

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.
"""

Expand All @@ -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}"
Expand All @@ -1128,17 +1128,20 @@ 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.

The resulting dictionary can be easily converted to Pandas Dataframes for writing to CSV.

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")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you'd like me to change this to something else


self.molecule_statistics = {
molecule_number: molecule.collate_molecule_statistics()
for molecule_number, molecule in self.molecule_data.items()
Expand Down
48 changes: 28 additions & 20 deletions topostats/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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).

Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -445,15 +445,15 @@ 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.

Parameters
----------
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
Expand Down Expand Up @@ -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(
Expand All @@ -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
)

Expand All @@ -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(
Expand All @@ -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"])
Expand Down
2 changes: 1 addition & 1 deletion topostats/grains.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 8 additions & 9 deletions topostats/measure/feret.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -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)
Expand All @@ -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.

Expand All @@ -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
Expand Down
Loading
Loading