-
Notifications
You must be signed in to change notification settings - Fork 4
Fix pixel breath negative TIV #369
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
Changes from 15 commits
d39a815
07f0c90
367d3e0
98ef67e
8382461
0dfee24
ab67491
67da634
7b35415
a26e490
480ba5b
fb89764
189600a
62ced5f
adb9e10
70423ce
aa65186
cc1518a
cee67e3
de4db42
3178504
b210653
06e0c57
9237110
0c495b4
12be567
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| import itertools | ||
| import warnings | ||
| from collections.abc import Callable | ||
| from dataclasses import dataclass, field | ||
| from dataclasses import InitVar, dataclass, field | ||
| from typing import Final | ||
|
|
||
| import numpy as np | ||
| from scipy import signal | ||
|
|
||
| from eitprocessing.datahandling.breath import Breath | ||
| from eitprocessing.datahandling.continuousdata import ContinuousData | ||
|
|
@@ -11,6 +14,15 @@ | |
| from eitprocessing.datahandling.sequence import Sequence | ||
| from eitprocessing.features.breath_detection import BreathDetection | ||
|
|
||
| _SENTINAL_BREATH_DETECTION: Final = BreathDetection() | ||
| MAX_XCORR_LAG = 0.75 | ||
|
|
||
|
|
||
| def _return_sentinal_breath_detection() -> BreathDetection: | ||
| # Returns a sential of a BreathDetection, which only exists to signal that the default value for breath_detection | ||
| # was used. | ||
| return _SENTINAL_BREATH_DETECTION | ||
|
|
||
|
|
||
| @dataclass | ||
| class PixelBreath: | ||
|
|
@@ -30,20 +42,31 @@ class PixelBreath: | |
| ``` | ||
|
|
||
| Args: | ||
| breath_detection_kwargs (dict): A dictionary of keyword arguments for breath detection. | ||
| The available keyword arguments are: | ||
| minimum_duration: minimum expected duration of breaths, defaults to 2/3 of a second | ||
| averaging_window_duration: duration of window used for averaging the data, defaults to 15 seconds | ||
| averaging_window_function: function used to create a window for averaging the data, defaults to np.blackman | ||
| amplitude_cutoff_fraction: fraction of the median amplitude below which breaths are removed, defaults to 0.25 | ||
| invalid_data_removal_window_length: window around invalid data in which breaths are removed, defaults to 0.5 | ||
| invalid_data_removal_percentile: the nth percentile of values used to remove outliers, defaults to 5 | ||
| invalid_data_removal_multiplier: the multiplier used to remove outliers, defaults to 4 | ||
| breath_detection (BreathDetection): BreathDetection object to use for detecing breaths. | ||
psomhorst marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| allow_negative_amplitude (bool): whether to asume out-of-phase pixels have negative amplitude instead. | ||
psomhorst marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
|
|
||
| breath_detection_kwargs: dict = field(default_factory=dict) | ||
|
|
||
| def find_pixel_breaths( | ||
| breath_detection: BreathDetection = field(default_factory=_return_sentinal_breath_detection) | ||
| breath_detection_kwargs: InitVar[dict | None] = None | ||
| allow_negative_amplitude: bool = True | ||
| correct_for_phase_shift: bool = True | ||
psomhorst marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| def __post_init__(self, breath_detection_kwargs: dict | None): | ||
| if breath_detection_kwargs is not None: | ||
| if self.breath_detection is not _SENTINAL_BREATH_DETECTION: | ||
| msg = ( | ||
| "`breath_detection_kwargs` is deprecated, and can't be used at the same time as `breath_detection`." | ||
| ) | ||
| raise TypeError(msg) | ||
|
|
||
| self.breath_detection = BreathDetection(**breath_detection_kwargs) | ||
| warnings.warn( | ||
| "`breath_detection_kwargs` is deprecated and will be removed soon. " | ||
| "Replace with `breath_detection=BreathDetection(**breath_detection_kwargs)`.", | ||
| DeprecationWarning, | ||
| ) | ||
|
|
||
| def find_pixel_breaths( # noqa: C901, PLR0912, PLR0915 | ||
psomhorst marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self, | ||
| eit_data: EITData, | ||
| continuous_data: ContinuousData, | ||
|
|
@@ -103,9 +126,7 @@ def find_pixel_breaths( | |
| msg = "To store the result a Sequence dataclass must be provided." | ||
| raise ValueError(msg) | ||
|
|
||
| bd_kwargs = self.breath_detection_kwargs.copy() | ||
| breath_detection = BreathDetection(**bd_kwargs) | ||
| continuous_breaths = breath_detection.find_breaths(continuous_data) | ||
| continuous_breaths = self.breath_detection.find_breaths(continuous_data) | ||
|
|
||
| indices_breath_middles = np.searchsorted( | ||
| eit_data.time, | ||
|
|
@@ -144,28 +165,57 @@ def find_pixel_breaths( | |
|
|
||
| pixel_breaths = np.full((len(continuous_breaths), n_rows, n_cols), None) | ||
|
|
||
| lags = signal.correlation_lags(len(continuous_data), len(continuous_data), mode="same") | ||
|
|
||
| for row, col in itertools.product(range(n_rows), range(n_cols)): | ||
| mean_tiv = mean_tiv_pixel[row, col] | ||
|
|
||
| if np.any(pixel_impedance[:, row, col] > 0): | ||
| if mean_tiv < 0: | ||
| start_func, middle_func = np.argmax, np.argmin | ||
| else: | ||
| start_func, middle_func = np.argmin, np.argmax | ||
|
|
||
| outsides = self._find_extreme_indices(pixel_impedance, indices_breath_middles, row, col, start_func) | ||
| starts = outsides[:-1] | ||
| ends = outsides[1:] | ||
| middles = self._find_extreme_indices(pixel_impedance, outsides, row, col, middle_func) | ||
| # TODO discuss; this block of code is implemented to prevent noisy pixels from breaking the code. | ||
| # Quick solve is to make entire breath object None if any breath in a pixel does not have | ||
| # consecutive start, middle and end. | ||
| # However, this might cause problems elsewhere. | ||
| if (starts >= middles).any() or (middles >= ends).any(): | ||
| pixel_breath = None | ||
| if np.std(pixel_impedance[:, row, col]) == 0: | ||
| # pixel has no amplitude | ||
| continue | ||
|
|
||
| if self.allow_negative_amplitude and mean_tiv < 0: | ||
| start_func, middle_func = np.argmax, np.argmin | ||
| lagged_indices_breath_middles = indices_breath_middles | ||
| else: | ||
| start_func, middle_func = np.argmin, np.argmax | ||
|
|
||
| cd = np.copy(continuous_data.values) | ||
| cd -= np.nanmean(cd) | ||
| pi = np.copy(pixel_impedance[:, row, col]) | ||
| pi -= np.nanmean(pixel_impedance[:, row, col]) | ||
|
|
||
| if self.correct_for_phase_shift: | ||
| # search for maximum cross correlation within MAX_XCORR_LAG times the average | ||
| # duration of a breath | ||
| xcorr = signal.correlate(cd, pi, mode="same") | ||
| max_lag = MAX_XCORR_LAG * np.mean(np.diff(indices_breath_middles)) | ||
| lag_range = (lags > -max_lag) & (lags < max_lag) | ||
| # TODO: if this does not work, implement robust peak detection | ||
| lag = lags[lag_range][np.argmax(xcorr[lag_range])] | ||
| # positive lag: pixel inflates later than summed | ||
|
|
||
| # shift search area | ||
| lagged_indices_breath_middles = indices_breath_middles - lag | ||
| lagged_indices_breath_middles = lagged_indices_breath_middles[ | ||
| (lagged_indices_breath_middles >= 0) & (lagged_indices_breath_middles < len(cd)) | ||
| ] | ||
|
||
| else: | ||
| pixel_breath = self._construct_breaths(starts, middles, ends, time) | ||
| pixel_breaths[:, row, col] = pixel_breath | ||
| lagged_indices_breath_middles = indices_breath_middles | ||
|
|
||
| outsides = self._find_extreme_indices(pixel_impedance, lagged_indices_breath_middles, row, col, start_func) | ||
| starts = outsides[:-1] | ||
| ends = outsides[1:] | ||
| middles = self._find_extreme_indices(pixel_impedance, outsides, row, col, middle_func) | ||
| # TODO discuss; this block of code is implemented to prevent noisy pixels from breaking the code. | ||
| # Quick solve is to make entire breath object None if any breath in a pixel does not have | ||
| # consecutive start, middle and end. | ||
| # However, this might cause problems elsewhere. | ||
| if (starts >= middles).any() or (middles >= ends).any(): | ||
| pixel_breath = None | ||
| else: | ||
| pixel_breath = self._construct_breaths(starts, middles, ends, time) | ||
| pixel_breaths[:, row, col] = pixel_breath | ||
|
|
||
| intervals = [(breath.start_time, breath.end_time) for breath in continuous_breaths.values] | ||
|
|
||
|
|
@@ -178,15 +228,14 @@ def find_pixel_breaths( | |
| values=list( | ||
| pixel_breaths, | ||
| ), ## TODO: change back to pixel_breaths array when IntervalData works with 3D array | ||
| parameters=self.breath_detection_kwargs, | ||
| derived_from=[eit_data], | ||
| ) | ||
| if store: | ||
| sequence.interval_data.add(pixel_breaths_container) | ||
|
|
||
| return pixel_breaths_container | ||
|
|
||
| def _construct_breaths(self, start: list, middle: list, end: list, time: np.ndarray) -> list: | ||
| def _construct_breaths(self, start: list[int], middle: list[int], end: list[int], time: np.ndarray) -> list: | ||
| breaths = [Breath(time[s], time[m], time[e]) for s, m, e in zip(start, middle, end, strict=True)] | ||
| # First and last breath are not detected by definition (need two breaths to find one breath) | ||
| return [None, *breaths, None] | ||
|
|
@@ -222,5 +271,5 @@ def _find_extreme_indices( | |
| are located for each time segment. | ||
| """ | ||
| return np.array( | ||
| [function(pixel_impedance[times[i] : times[i + 1], row, col]) + times[i] for i in range(len(times) - 1)], | ||
| [function(pixel_impedance[t1:t2, row, col]) + t1 for t1, t2 in itertools.pairwise(times)], | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.