Skip to content

Commit 860136b

Browse files
authored
Merge pull request #468 from EIT-ALIVE/add_global_roi_definition
Add global ROI definition
2 parents 0fb3b34 + 39af2ed commit 860136b

File tree

2 files changed

+29
-34
lines changed

2 files changed

+29
-34
lines changed

eitprocessing/roi/__init__.py

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,8 @@
44
module is `PixelMask`. Any type of region of interest selection results in a `PixelMask` object. A mask can be applied
55
to any pixel dataset (EITData, PixelMap) with the same shape.
66
7-
Several default masks have been predefined. NB: the right side of the patient is to the left side of the EIT image and
8-
vice versa.
9-
10-
- `VENTRAL_MASK` includes only the first 16 rows;
11-
- `DORSAL_MASK` includes only the last 16 rows;
12-
- `ANATOMICAL_RIGHT_MASK` includes only the first 16 columns;
13-
- `ANATOMICAL_LEFT_MASK` includes only the last 16 columns;
14-
- `QUADRANT_1_MASK` includes the top right quadrant;
15-
- `QUADRANT_2_MASK` includes the top left quadrant;
16-
- `QUADRANT_3_MASK` includes the bottom right quadrant;
17-
- `QUADRANT_4_MASK` includes the bottom left quadrant;
18-
- `LAYER_1_MASK` includes only the first 8 rows;
19-
- `LAYER_2_MASK` includes only the second set of 8 rows;
20-
- `LAYER_3_MASK` includes only the third set of 8 rows;
21-
- `LAYER_4_MASK` includes only the last 8 rows.
7+
Predefined default masks can be created using `get_geometric_mask()`. These masks are based on common anatomical regions
8+
of interest.
229
"""
2310

2411
from __future__ import annotations
@@ -295,7 +282,7 @@ def __eq__(self, other: object) -> bool:
295282
return self.mask.shape == other.mask.shape and np.array_equal(self.mask, other.mask, equal_nan=True)
296283

297284

298-
def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMask:
285+
def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMask: # noqa: PLR0911
299286
"""Get a geometric mask by name.
300287
301288
Masks can be generated for appropriates shapes, provided by the shape` argument, a tuple of two integers
@@ -305,6 +292,7 @@ def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMas
305292
The function accepts both full names (e.g., "layer 1") and abbreviations (e.g., "L1").
306293
307294
The following masks are available:
295+
- "global" or "G": all pixels in the EIT image.
308296
- "ventral" or "V": the first half rows of the EIT image.
309297
- "dorsal" or "D": the last half rows of the EIT image.
310298
- "anatomical right" or "R": the first half columns of the EIT image.
@@ -331,23 +319,6 @@ def get_geometric_mask(mask: str, shape: tuple[int, int] = (32, 32)) -> PixelMas
331319
ValueError: If an unknown mask name is provided.
332320
ValueError: If the shape is not compatible with the requested mask.
333321
"""
334-
335-
def _check_dimensions(
336-
name: str, shape: tuple[int, int], *, height_divisor: int | None = None, width_divisor: int | None = None
337-
) -> None:
338-
total_height, total_width = shape
339-
if isinstance(height_divisor, int) and total_height % height_divisor != 0:
340-
msg = (
341-
f"Shape {shape} is not compatible with a {name} mask. "
342-
"The height must be a multiple of {height_divisor}."
343-
)
344-
raise ValueError(msg)
345-
if isinstance(width_divisor, int) and total_width % width_divisor != 0:
346-
msg = (
347-
f"Shape {shape} is not compatible with a {name} mask. The width must be a multiple of {width_divisor}."
348-
)
349-
raise ValueError(msg)
350-
351322
total_height, total_width = shape
352323
n_layers_quadrants = 4
353324

@@ -356,6 +327,8 @@ def _check_dimensions(
356327
mask = re.sub(r"^L([1-4]{1})$", r"layer \1", mask)
357328

358329
match mask.split(" "):
330+
case ["global"] | ["G"]:
331+
return PixelMask(np.ones(shape), label="global")
359332
case ["ventral"] | ["V"]:
360333
_check_dimensions("ventral", shape, height_divisor=2)
361334
height = total_height // 2
@@ -386,7 +359,7 @@ def _check_dimensions(
386359

387360
case ["layer", num_str] if num_str.isdigit() and 1 <= int(num_str) <= n_layers_quadrants:
388361
num = int(num_str)
389-
_check_dimensions("layer", shape, height_divisor=4)
362+
_check_dimensions("layer", shape, height_divisor=n_layers_quadrants)
390363
height = total_height // n_layers_quadrants
391364
return PixelMask(
392365
np.concatenate(
@@ -410,3 +383,17 @@ def _check_dimensions(
410383
case _:
411384
msg = f"Unknown mask name: {mask}."
412385
raise ValueError(msg)
386+
387+
388+
def _check_dimensions(
389+
name: str, shape: tuple[int, int], *, height_divisor: int | None = None, width_divisor: int | None = None
390+
) -> None:
391+
total_height, total_width = shape
392+
if isinstance(height_divisor, int) and total_height % height_divisor != 0:
393+
msg = (
394+
f"Shape {shape} is not compatible with a {name} mask. The height must be a multiple of {{height_divisor}}."
395+
)
396+
raise ValueError(msg)
397+
if isinstance(width_divisor, int) and total_width % width_divisor != 0:
398+
msg = f"Shape {shape} is not compatible with a {name} mask. The width must be a multiple of {width_divisor}."
399+
raise ValueError(msg)

tests/test_pixelmask.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ def test_pixelmask_subtract():
220220
assert np.array_equal(pm3.mask, np.array([[np.nan, np.nan, 1, np.nan], [0.1, np.nan, np.nan, 0.5]]), equal_nan=True)
221221

222222

223+
@pytest.mark.parametrize("shape", [(32, 1), (64, 1), (16, 1), (100, 1), (4, 1)])
224+
def test_predefined_global_mask(shape: tuple[int, int]):
225+
global_mask = get_geometric_mask("global", shape)
226+
assert global_mask.label == "global"
227+
assert global_mask.shape == shape
228+
assert np.all(global_mask.mask == 1.0)
229+
230+
223231
@pytest.mark.parametrize("shape", [(32, 1), (64, 1), (16, 1), (100, 1), (4, 1)])
224232
def test_predefined_layer_masks(shape: tuple[int, int]):
225233
quarter_height = shape[0] // 4

0 commit comments

Comments
 (0)