Skip to content

Commit

Permalink
Add type hints (#15)
Browse files Browse the repository at this point in the history
* Add typing info

* Add type hints

* Fix linting issues

* Fix workflow

* Fix workflow

* Fix workflow

* Fix workflow

* Fix workflow

* Fix workflow

* Fix dependencies

* Fix bug

* Fix Python 3.8 compatibility

* Fix typing

* Fix workflow

* Fix workflow

* Fix bug

* Fix bug

* Add more type hints

* [skip ci] Fix bug in `ObjectMeasureAdapter`

The correspondance cache is removed, since correspondances may differ between subsequent calls, now that `ObjectMeasureAdapter` is also used for region-based measures.

* [skip ci] Fix type hints

* Fix bug

* Fix issues

* Fix linting issues

* Add type hints for regional.py

* Add Literal types

* Update type hints

* Add type hints for contour.py

* Add type hints for detection.py

* Add `python>=3.8` requirement

* Fix requirements.txt

* Fix bug

* Add type hints for parallel.py
  • Loading branch information
kostrykin authored Mar 10, 2024
1 parent 526b2c5 commit a35c658
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 271 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[flake8]

extend-ignore = E221,E211,E222,E202,F541,E201
extend-ignore = E221,E211,E222,E202,F541,E201,E203

per-file-ignores =
segmetrics/__init__.py:F401
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/testsuite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ jobs:
isort segmetrics --check-only
isort tests --check-only
type_checking:

name: Type checking
runs-on: ubuntu-latest

steps:

- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install mypy
- name: Run mypy
shell: bash
run: |
mypy --config-file .mypy.ini --ignore-missing-imports segmetrics
run_testsuite:

name: Tests
Expand Down
3 changes: 3 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[mypy]

disable_error_code = import-untyped
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
numpy>=1.18
numpy>=1.20
scipy
scikit-image>=0.18
scikit-learn
Expand Down
47 changes: 31 additions & 16 deletions segmetrics/contour.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
from typing import (
List,
Sequence,
)

import numpy as np
from scipy import ndimage
from skimage import morphology as morph

from segmetrics.measure import (
CorrespondanceFunction,
ImageMeasureMixin,
Measure,
)
from segmetrics.typing import (
BinaryImage,
LabelImage,
)


def _compute_binary_contour(mask, width=1):
def _compute_binary_contour(mask: BinaryImage, width: int = 1) -> BinaryImage:
dilation = morph.binary_dilation(mask, morph.disk(width))
return np.logical_and(dilation, np.logical_not(mask))


def _quantile_max(quantile, values):
def _quantile_max(quantile: float, values: Sequence[float]) -> float:
if quantile == 1:
return np.max(values)
else:
values = np.sort(values)
values = list(sorted(values))
return values[int(quantile * (len(values) - 1))]


Expand All @@ -27,7 +37,12 @@ class ContourMeasure(ImageMeasureMixin, Measure):
binary volumes (images).
"""

def __init__(self, *args, correspondance_function='min', **kwargs):
def __init__(
self,
*args,
correspondance_function: CorrespondanceFunction = 'min',
**kwargs,
) -> None:
super().__init__(
*args,
correspondance_function=correspondance_function,
Expand Down Expand Up @@ -58,18 +73,18 @@ class Hausdorff(ContourMeasure):
distance." International Journal of computer vision 24.3 (1997): 251-270.
"""

def __init__(self, quantile=1, **kwargs):
def __init__(self, quantile: float = 1, **kwargs):
super().__init__(**kwargs)
assert 0 < quantile <= 1
self.quantile = quantile

def set_expected(self, expected):
def set_expected(self, expected: LabelImage) -> None:
self.expected_contour = _compute_binary_contour(expected > 0)
self.expected_contour_distance_map = ndimage.distance_transform_edt(
np.logical_not(self.expected_contour)
)

def compute(self, actual):
def compute(self, actual: LabelImage) -> List[float]:
actual_contour = _compute_binary_contour(actual > 0)
if not self.expected_contour.any() or not actual_contour.any():
return []
Expand All @@ -80,13 +95,13 @@ def compute(self, actual):
)
]

def default_name(self):
def default_name(self) -> str:
if self.quantile == 1:
return 'HSD'
else:
return f'HSD (Q={self.quantile:g})'

def _quantile_max(self, values):
def _quantile_max(self, values: Sequence[float]) -> float:
return _quantile_max(self.quantile, values)


Expand Down Expand Up @@ -114,17 +129,17 @@ class NSD(ContourMeasure):
:math:`1`. Lower values correspond to better segmentation performance.
"""

def set_expected(self, expected):
self.expected = (expected > 0)
self.expected_contour = _compute_binary_contour(self.expected)
def set_expected(self, expected: LabelImage) -> None:
self.expected_binary: BinaryImage = (expected > 0)
self.expected_contour = _compute_binary_contour(self.expected_binary)
self.expected_contour_distance_map = ndimage.distance_transform_edt(
np.logical_not(self.expected_contour)
)

def compute(self, actual):
actual = (actual > 0)
union = np.logical_or(self.expected, actual)
intersection = np.logical_and(self.expected, actual)
def compute(self, actual: LabelImage) -> List[float]:
actual_binary: BinaryImage = (actual > 0)
union = np.logical_or(self.expected_binary, actual_binary)
intersection = np.logical_and(self.expected_binary, actual_binary)
denominator = self.expected_contour_distance_map[union].sum()
nominator = self.expected_contour_distance_map[
np.logical_and(union, np.logical_not(intersection))
Expand Down
61 changes: 43 additions & 18 deletions segmetrics/detection.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import numpy as np
from typing import (
Any,
Dict,
List,
Optional,
Set,
)

import numpy as np

from segmetrics.measure import Measure
from segmetrics.typing import (
BinaryImage,
LabelImage,
)


def _assign(assignments, key, value):
def _assign(
assignments: Dict,
key: Any,
value: Any,
) -> None:
if key not in assignments:
assignments[key] = set()
assignments[key] |= {value}


def _compute_seg_by_ref_assignments(seg, ref, include_background=False):
seg_by_ref = {}
def _compute_seg_by_ref_assignments(
seg: LabelImage,
ref: LabelImage,
include_background: bool = False,
) -> Dict[int, Set[int]]:
seg_by_ref: Dict[int, Set[int]] = dict()

if include_background:
seg_by_ref[0] = set()

for seg_label in range(1, seg.max() + 1):
seg_cc = (seg == seg_label)
seg_cc: BinaryImage = (seg == seg_label)

if not seg_cc.any():
continue
Expand All @@ -27,7 +47,12 @@ def _compute_seg_by_ref_assignments(seg, ref, include_background=False):
return seg_by_ref


def _compute_ref_by_seg_assignments(seg, ref, *args, **kwargs):
def _compute_ref_by_seg_assignments(
seg: LabelImage,
ref: LabelImage,
*args,
**kwargs,
) -> Dict[int, Set[int]]:
return _compute_seg_by_ref_assignments(ref, seg, *args, **kwargs)


Expand All @@ -42,14 +67,14 @@ class FalseSplit(Measure):
algorithms," in Proc. Int. Symp. Biomed. Imag., 2009, pp. 518–521.
"""

def compute(self, actual):
def compute(self, actual: LabelImage) -> List[float]:
seg_by_ref = _compute_seg_by_ref_assignments(actual, self.expected)
return [
sum(len(seg_by_ref[ref_label]) > 1
for ref_label in seg_by_ref.keys() if ref_label > 0)
]

def default_name(self):
def default_name(self) -> str:
return 'Split'


Expand All @@ -64,14 +89,14 @@ class FalseMerge(Measure):
algorithms," in Proc. Int. Symp. Biomed. Imag., 2009, pp. 518–521.
"""

def compute(self, actual):
def compute(self, actual: LabelImage) -> List[float]:
ref_by_seg = _compute_ref_by_seg_assignments(actual, self.expected)
return [
sum(len(ref_by_seg[seg_label]) > 1
for seg_label in ref_by_seg.keys() if seg_label > 0)
]

def default_name(self):
def default_name(self) -> str:
return 'Merge'


Expand All @@ -86,11 +111,11 @@ class FalsePositive(Measure):
algorithms," in Proc. Int. Symp. Biomed. Imag., 2009, pp. 518–521.
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.result = None
self.result: Optional[LabelImage] = None

def compute(self, actual):
def compute(self, actual: LabelImage) -> List[float]:
seg_by_ref = _compute_seg_by_ref_assignments(
actual,
self.expected,
Expand All @@ -101,7 +126,7 @@ def compute(self, actual):
self.result[actual == seg_label] = seg_label
return [len(seg_by_ref[0])]

def default_name(self):
def default_name(self) -> str:
return 'Spurious'


Expand All @@ -116,11 +141,11 @@ class FalseNegative(Measure):
algorithms," in Proc. Int. Symp. Biomed. Imag., 2009, pp. 518–521.
"""

def __init__(self, **kwargs):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.result = None
self.result: Optional[LabelImage] = None

def compute(self, actual):
def compute(self, actual: LabelImage) -> List[float]:
ref_by_seg = _compute_ref_by_seg_assignments(
actual,
self.expected,
Expand All @@ -131,5 +156,5 @@ def compute(self, actual):
self.result[self.expected == ref_label] = ref_label
return [len(ref_by_seg[0])]

def default_name(self):
def default_name(self) -> str:
return 'Missing'
Loading

0 comments on commit a35c658

Please sign in to comment.