Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c5bca1f
Limit version of pytest
psomhorst Aug 18, 2025
e4ff11d
Set stack level and type of warnings
psomhorst Aug 18, 2025
ddffe25
Add subtraction to PixelMask class
psomhorst Aug 13, 2025
287a1ad
Add support for PixelMask plotting in PixelMap plotting
psomhorst Aug 18, 2025
9e33fb0
Add function to get summed impedance to EITData
psomhorst Aug 18, 2025
3248b61
Rename absolute argument to use_magnitude and add fraction_of_max arg…
psomhorst Aug 18, 2025
27905b3
Add captures to PixelMap.apply
psomhorst Aug 18, 2025
5aa5e0c
Add IntegerMap including PlotConfig
psomhorst Aug 18, 2025
dfc27bf
Add to_..._array convenience methods to PixelMap
psomhorst Aug 18, 2025
7291afd
Add default PixelMapPlotConfig to registry
psomhorst Aug 18, 2025
5734478
Update deprecated function name
psomhorst Aug 18, 2025
cac298c
Add PixelMask.values alias to .mask
psomhorst Aug 18, 2025
d43f8ff
Add PixelMask.shape property
psomhorst Aug 18, 2025
ca952fb
Add __eq__ method to check equivalence of PixelMasks
psomhorst Aug 18, 2025
1fc168b
Replace fixed default masks with generated masks, based on shape
psomhorst Aug 18, 2025
f24510b
Add contour and surface plots to PixelMap
psomhorst Aug 18, 2025
bdb9b1c
Update tests for PixelMasks
psomhorst Aug 18, 2025
513f28b
Update tests for PixelMap
psomhorst Aug 18, 2025
23cb2bb
Add option for simulated data to Vendor
psomhorst Aug 18, 2025
69f5c2b
Add WatershedLungspace class and accompanying tests
psomhorst Aug 18, 2025
c69dea0
Add notebook with Watershed demonstration
psomhorst Aug 18, 2025
5bac9b0
Add watershed and notebook to documentation
psomhorst Aug 18, 2025
a67d7fe
Update PixelMaskCollection tests to use real EITData
psomhorst Aug 18, 2025
4e0ff70
Improve IntegerMap with dtype class variable and lossless conversion …
psomhorst Aug 22, 2025
dff9811
Add test value conversion at init
psomhorst Aug 22, 2025
207321f
Update IntegerMap plot config
psomhorst Aug 22, 2025
251e48d
Update notebook to show integer map plotting
psomhorst Aug 22, 2025
48b3dc9
Update to_boolean_array to optionally keep zero values as True
psomhorst Aug 22, 2025
d6e6cb4
Update pixelmap tests
psomhorst Aug 22, 2025
17a8530
Add check for no markers inside TIV mask
psomhorst Aug 22, 2025
90a8b34
Fix issue where BreathDetection fails if no peaks are found
psomhorst Aug 22, 2025
672e47d
Raise error if no breaths are found in breath detection
psomhorst Aug 22, 2025
7e82552
Add test for watershed with no amplitude
psomhorst Aug 22, 2025
95af7b4
Add test for captures
psomhorst Aug 22, 2025
b3aa395
Fix langauge issues
psomhorst Aug 22, 2025
c85340e
Add TIVLungspace class
psomhorst Aug 25, 2025
f2fb3a6
Add tests for TIVLungspace class
psomhorst Aug 25, 2025
0ae5e52
Add TIV lungspace to docs
psomhorst Aug 25, 2025
6eacd1b
Use TIVLungspace in WatershedLungspace
psomhorst Aug 25, 2025
fedebb7
Add AmplitudeMap, AmplitudeMapPlotConfig
psomhorst Aug 25, 2025
8a1812a
Update notebook to show AmplitudeMap
psomhorst Aug 25, 2025
9d69897
Create separate AmplitudeLungspace class, and remove modes from TIVLu…
psomhorst Aug 25, 2025
a9042fe
Update documentation for AmplitudeLungspace
psomhorst Aug 25, 2025
1f0aaa6
Update Watershed to use AmplitudeLungspace
psomhorst Aug 25, 2025
dd072d5
Re-add warning for AmplitudeLungspace
psomhorst Aug 25, 2025
1c56417
Update tests for AmplitudeLungspace
psomhorst Aug 25, 2025
8abfd6c
Add comment about test coverage
psomhorst Aug 25, 2025
1b434c3
Add AmplitudeMap and IntegerMap to pixelmap docs
psomhorst Aug 25, 2025
93c9a7e
Fix typo
psomhorst Aug 25, 2025
51b42ef
Only consider pixels that are not all-NaN when aggregating
psomhorst Aug 25, 2025
3daeba8
Suppress zero warning when subtracting masks
psomhorst Aug 25, 2025
b909af1
Add exception when trying to normalize a all-NaN PixelMap
psomhorst Aug 25, 2025
650c5f2
Add explicit check for all-NaN values
psomhorst Aug 25, 2025
9c33751
Catch or prevent warnings in tests
psomhorst Aug 25, 2025
f45fc39
Test butterworth filters with apply instead of deprecated apply_filter
psomhorst Aug 25, 2025
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
114 changes: 114 additions & 0 deletions eitprocessing/roi/tiv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import sys
from dataclasses import dataclass
from typing import Literal

import numpy as np

from eitprocessing.datahandling.continuousdata import ContinuousData
from eitprocessing.datahandling.eitdata import EITData
from eitprocessing.datahandling.pixelmap import TIVMap
from eitprocessing.features.pixel_breath import PixelBreath
from eitprocessing.parameters.tidal_impedance_variation import TIV
from eitprocessing.roi import PixelMask
from eitprocessing.utils import make_capture


@dataclass(kw_only=True, frozen=True)
class TIVLungspace:
"""Create a pixel mask by thresholding the mean TIV or amplitude.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should the AmplitudeLungspace be a class on its own?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure since the warning is very relevant that amplitude should probably not be used for FLS. However, if someone decides to anyway, it might be misleading if that results in a TIVLungspace object?


This defines the functional lung space as all pixels with a tidal impedance variation (TIV, default) or amplitude of
at least the provided fractional threshold of the maximum TIV or amplitude.

Warning:
The amplitude mode is not recommended for functional lung space detection, as it potentially includes
reconstruction artifacts. The option is provided for completeness and use in other algorithms, namely
WatershedLungspace.

Example usage:
```python
>>> mask = TIVLungspace(threshold=0.15, mode="TIV").apply(eit_data)
>>> masked_eit_data = mask.apply(eit_data)
```

Args:
threshold (float):
The fraction of the maximum TIV or amplitude that is used as threshold. Defaults to 0.15 (15%).
mode (Literal["TIV", "amplitude"]): Whether to use TIV (default) or amplitude for the mask definition.

"""

threshold: float = 0.15
mode: Literal["TIV", "amplitude"] = "TIV"

def __post_init__(self):
if not isinstance(self.threshold, float):
msg = "Threshold must be a float."
raise TypeError(msg)
if not (0 < self.threshold < 1):
msg = "Threshold must be between 0 and 1."
raise ValueError(msg)

if self.mode not in ("TIV", "amplitude"):
msg = f"Unknown mode '{self.mode}'. Supported modes are 'TIV' and 'amplitude'."
raise ValueError(msg)

def apply(
self, eit_data: EITData, *, timing_data: ContinuousData | None = None, captures: dict | None = None
) -> PixelMask:
"""Apply the TIV or amplitude thresholding to the EIT data.

`BreathDetection` is used to find breaths in timing data. By default, the timing data is the summed pixel
impedance. Alternative timing data, e.g., pressure data, can be provided.

Then, `TIV` is used to compute the mean tidal impedance variation (TIV) or mean pixel amplitude over the
breaths.

Args:
eit_data (EITData): The EIT data to process.
timing_data (ContinuousData | None, optional):
Optionally, alternative continuous data to use for breath detection. If `None`, the summed pixel
impedance is used. Defaults to `None`.
captures (dict | None, optional):
A dictionary to store intermediate results. If `None`, no intermediate results are stored. Defaults to
`None`.
"""
capture = make_capture(captures)

if timing_data is None:
timing_data = eit_data.get_summed_impedance()

if self.mode == "TIV":
magnitude = TIV(
pixel_breath=PixelBreath(phase_correction_mode="negative amplitude")
).compute_pixel_parameter(
eit_data=eit_data,
continuous_data=timing_data,
tiv_timing="continuous",
sequence=None,
)
else:
magnitude = TIV(pixel_breath=PixelBreath(phase_correction_mode="phase shift")).compute_pixel_parameter(
eit_data=eit_data,
continuous_data=timing_data,
tiv_timing="pixel",
sequence=None,
)

if not len(magnitude):
msg = f"No breaths were detected. Cannot compute {self.mode}"
raise ValueError(msg)

if np.all(np.isnan(magnitude.values)):
msg = f"No non-nan {self.mode} were found."
exc = ValueError(msg)
if sys.version_info >= (3, 11):
exc.add_note("This may be due to too few breaths being detected in the data.")
raise exc

capture(f"{self.mode} values", magnitude)

mean_magnitude = TIVMap.from_aggregate(np.array(magnitude.values), np.nanmean, suppress_negative_warning=True)
capture(f"mean {self.mode}", mean_magnitude)

return mean_magnitude.create_mask_from_threshold(self.threshold, fraction_of_max=True)