Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #15

Merged
merged 34 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fbc12e6
Add typing info
kostrykin Mar 3, 2024
1260127
Add type hints
kostrykin Mar 3, 2024
55b2992
Fix linting issues
kostrykin Mar 3, 2024
250e76a
Fix workflow
kostrykin Mar 3, 2024
21294de
Fix workflow
kostrykin Mar 3, 2024
bacfdd8
Fix workflow
kostrykin Mar 3, 2024
1a7e64d
Fix workflow
kostrykin Mar 3, 2024
ba4289f
Fix workflow
kostrykin Mar 3, 2024
2d68cd9
Fix workflow
kostrykin Mar 3, 2024
c168a30
Fix dependencies
kostrykin Mar 3, 2024
b8eebbf
Fix bug
kostrykin Mar 3, 2024
0c1c5d2
Fix Python 3.8 compatibility
kostrykin Mar 3, 2024
202dc06
Merge remote-tracking branch 'origin/develop' into dev/typing
kostrykin Mar 3, 2024
d700823
Fix typing
kostrykin Mar 3, 2024
6cf7119
Fix workflow
kostrykin Mar 3, 2024
e6abf72
Fix workflow
kostrykin Mar 3, 2024
7f797e2
Fix bug
kostrykin Mar 3, 2024
afaed5b
Merge branch 'develop' into dev/typing
kostrykin Mar 3, 2024
4987476
Fix bug
kostrykin Mar 3, 2024
9ddb227
Add more type hints
kostrykin Mar 5, 2024
b879670
[skip ci] Fix bug in `ObjectMeasureAdapter`
kostrykin Mar 5, 2024
4705c96
[skip ci] Fix type hints
kostrykin Mar 10, 2024
15734c3
Fix bug
kostrykin Mar 10, 2024
5a19dd3
Fix issues
kostrykin Mar 10, 2024
69075e3
Fix linting issues
kostrykin Mar 10, 2024
c210cc8
Add type hints for regional.py
kostrykin Mar 10, 2024
3301a6c
Add Literal types
kostrykin Mar 10, 2024
354c70d
Update type hints
kostrykin Mar 10, 2024
6973ade
Add type hints for contour.py
kostrykin Mar 10, 2024
d596869
Add type hints for detection.py
kostrykin Mar 10, 2024
17d3d80
Add `python>=3.8` requirement
kostrykin Mar 10, 2024
3830ea8
Fix requirements.txt
kostrykin Mar 10, 2024
c3e1644
Fix bug
kostrykin Mar 10, 2024
7bd2d92
Add type hints for parallel.py
kostrykin Mar 10, 2024
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
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