Preprocessing (layer 2)¶
pipeline ¶
preprocess(): from the parsed frames to a recording ready for analysis (layer 2).
Fixed order of the steps (notebooks/05_preprocessing_choices.ipynb): 1. time in seconds, pixel copy with the device "no data" values as NaN; 2. frames with missing pixel data filled and excluded; 3. respiratory rate from the spectrum, cardiac step (MDN with a heart rate given by the user, otherwise only a moving average); 4. breath detection on the global signal (sum of all pixels); 5. quality flags and exclusions of each breath; 6. lung mask from the valid breaths, regions of interest, regional signals; 7. auxiliary signals (pressures, flow, volume, device markers) with "no data" as NaN, in the same units for every vendor (cmH2O, L/s, mL); 8. airway pressures of each breath: PEEP, plateau, driving pressure (notebooks/06_peep_trial_segmentation.ipynb); 9. inspiratory tidal volume of each breath, from the ventilator curves.
Every step writes what it did in EITRecording.log (saved with the results) and
in the Python logger fasteit.preprocessing.pipeline (technical log, configured by
the application, not here).
recording_time ¶
recording_time(raw: ReconstructedFrameData) -> np.ndarray
Time of each frame in seconds from the start of the recording.
Draeger .bin timestamps are a fraction of the day: they are converted to
seconds and a recording that crosses midnight is unwrapped. Other vendors already
give seconds.
pixel_values ¶
pixel_values(raw: ReconstructedFrameData) -> np.ndarray
Float copy of the pixels, with the Draeger "no data" values as NaN.
The parsed pixels are never modified (they can be a read-only memory map). Timpel pixels already have NaN from the parser.
preprocess ¶
preprocess(raw: ReconstructedFrameData, config: PreprocessingConfig | None = None, exclusions: Sequence[Segment] = (), context: RecordingContext | None = None) -> EITRecording
Preprocess one recording: cardiac step, breaths, quality, lung mask, regions.
Same inputs give the same output, so an application only needs to store the inputs (file, config, exclusions, context).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
ReconstructedFrameData
|
Parsed recording with pixels (Draeger |
required |
config
|
PreprocessingConfig | None
|
Parameters; defaults of |
None
|
exclusions
|
Sequence[Segment]
|
Parts to exclude chosen by the user ( |
()
|
context
|
RecordingContext | None
|
Patient, position, protocol and notes. |
None
|
Returns:
| Type | Description |
|---|---|
EITRecording
|
The recording ready for analysis. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the input is unusable, no breath is found, or no valid breath is left for the lung mask. |
cardiac_filter ¶
Cardiac filtering of the pixel signals.
Policy (notebooks/05_preprocessing_choices.ipynb, section 5c): - heart rate given by the user -> MDN filter; - no heart rate -> only a light moving average, the cardiac component is NOT removed.
All functions work along axis 0 (time). Rates are in Hz here; the configuration uses
breaths or beats per minute, and the caller converts with SECONDS_PER_MINUTE.
Parameters have no defaults here on purpose: they live only in
fasteit.config.PreprocessingConfig.
Valid for adult recordings only (respiratory rate 6-60/min); not for neonates, paediatric patients or high-frequency oscillation.
moving_average_window ¶
moving_average_window(fs: float, seconds: float) -> int
Number of samples of a centred moving average lasting seconds.
The number is odd so that the window is centred on each sample, so the actual
duration can differ a little from seconds (0.1 s gives 5 samples at 50 Hz,
i.e. 0.1 s, but 3 samples at 20 Hz, i.e. 0.15 s).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fs
|
float
|
Sampling frequency in Hz. |
required |
seconds
|
float
|
Window duration in seconds. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Odd number of samples, at least 1. |
moving_average ¶
moving_average(signals: ndarray, fs: float, seconds: float) -> np.ndarray
Centred moving average along the time axis (axis 0).
Fallback used when no heart rate is available (section 5c of the evidence
notebook). Same idea as the MATLAB smooth() of the original analysis
scripts, but with the width in seconds. It is a smoothing, not a cardiac filter:
a 100/min cardiac oscillation keeps about 96 % of its amplitude at 50 Hz and
about 91 % at 20 Hz with a 0.1 s window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signals
|
ndarray
|
Array with time on axis 0, e.g. pixels of shape (N, 32, 32) or a 1-D global signal of shape (N,). |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
seconds
|
float
|
Window duration in seconds. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of the same shape as |
ndarray
|
sample are repeated, so the length does not change. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
respiratory_rate ¶
respiratory_rate(global_signal: ndarray, fs: float, min_bpm: float, max_bpm: float, window_seconds: float) -> float
Respiratory rate (Hz) from the power spectrum of the global signal.
Same method as the respiratory part of eitprocessing RateDetection
(v1.8.8; Somhorst et al., JOSS 2026;11(117):8179, DOI: 10.21105/joss.08179):
Welch spectrum (Hann window, 50 % overlap, mean removed), highest peak between
min_bpm and max_bpm, refined with a parabola through the peak and its two
neighbours.
It is the mean rate of the whole signal, not of single breaths. In the evidence notebook (section 4) the largest difference from the device breath markers was 1.7 breaths/min over 11 recordings (rates 10-42/min). With a 30 s window the spectral bins are 2/min apart before the refinement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
global_signal
|
ndarray
|
1-D signal without NaN, e.g. the sum of all pixels per frame. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
min_bpm
|
float
|
Lowest respiratory rate searched, breaths/min. |
required |
max_bpm
|
float
|
Highest respiratory rate searched, breaths/min. |
required |
window_seconds
|
float
|
Welch window; shortened to the signal length if needed. |
required |
Returns:
| Type | Description |
|---|---|
float
|
Respiratory rate in Hz (multiply by 60 for breaths/min). A peak on the edge of |
float
|
the range is returned unrefined; it usually means a slow drift of the signal, |
float
|
not a real respiratory rate. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the input is not usable (non-positive |
mdn_filter ¶
mdn_filter(signals: ndarray, fs: float, respiratory_rate_hz: float, heart_rate_hz: float, notch_half_width_hz: float, upper_limit_hz: float, order: int) -> np.ndarray
Remove the cardiac component with a multiple digital notch (MDN) filter.
Wisse et al., "Improved filtering methods to suppress cardiovascular
contamination in EIT recordings", Physiol Meas 2024, DOI: 10.1088/1361-6579/ad46e3.
Implementation follows eitprocessing MDNFilter (v1.8.8):
- one band-stop (Butterworth, zero phase) around each cardiac harmonic
k * heart_rate ± notch_half_width, up toupper_limit_hz; - the first band-stop never starts below (respiratory_rate + heart_rate) / 2, so that it does not cut the breathing;
- a low-pass at
upper_limit_hzremoves what is left above it.
Zero-phase filtering (forward and backward) doubles the effective order of each filter. Apply it to the whole recording, not to short pieces: narrow band-stops of this order have transients of several seconds at both ends.
The heart rate comes from the user (section 5c of the evidence notebook). It is a
snapshot: a heart rate drifting more than about ±10/min during the recording, or
an arrhythmia such as atrial fibrillation, is not removed by fixed notches. A heart
rate above upper_limit_hz + notch_half_width_hz (about 230/min) gets no notch,
only the low-pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signals
|
ndarray
|
Array with time on axis 0 (pixels or a 1-D signal). |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
respiratory_rate_hz
|
float
|
Respiratory rate in Hz. |
required |
heart_rate_hz
|
float
|
Heart rate in Hz. |
required |
notch_half_width_hz
|
float
|
Half width of each band-stop, in Hz. |
required |
upper_limit_hz
|
float
|
Highest frequency filtered, in Hz. |
required |
order
|
int
|
Butterworth order of each band-stop and of the low-pass. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Filtered array of the same shape as |
Raises:
| Type | Description |
|---|---|
ValueError
|
if a rate is not positive, the heart rate is not above the
respiratory rate, |
heart_rate_warnings ¶
heart_rate_warnings(heart_rate_bpm: float, respiratory_rate_bpm: float, low_bpm: float, high_bpm: float, margin_bpm: float) -> list[str]
Warnings about a heart rate entered by the user, before the MDN filter.
The heart rate is not checked against the data (it is never estimated, section
5c of the evidence notebook), so only two plausibility checks are made:
- outside low_bpm-high_bpm (adult range of eitprocessing
RateDetection), e.g. a typing error;
- within margin_bpm of 1, 2 or 3 times the respiratory rate: the cardiac
notches then fall on the breathing or its harmonics and can change the shape
of the breaths (section 5b).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
heart_rate_bpm
|
float
|
Heart rate given by the user, beats/min. |
required |
respiratory_rate_bpm
|
float
|
Respiratory rate from the spectrum, breaths/min. |
required |
low_bpm
|
float
|
Lowest plausible heart rate. |
required |
high_bpm
|
float
|
Highest plausible heart rate. |
required |
margin_bpm
|
float
|
Distance from a multiple of the respiratory rate below which a warning is given. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of warning messages, empty if none. |
exclusions ¶
Parts of a recording excluded from the analysis.
Exclusions come from the user (source="manual") or from the device
(source="device": frames with a timing error or with missing pixel data). They
are widened by a margin, because the forward-backward filters spread an artifact
over the neighbouring samples, and a breath that touches an exclusion is marked
excluded: it stays in the breath table (fastEIT design decision D5).
Frames with missing pixel data are also filled with the nearest valid frame, so that the filters can run on a continuous signal; the filled frames are excluded.
frames_to_segments ¶
frames_to_segments(time: ndarray, bad_frames: ndarray, fs: float, label: str) -> list[Segment]
One device exclusion per run of consecutive bad frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
time
|
ndarray
|
Time of each frame in seconds from the start of the recording. |
required |
bad_frames
|
ndarray
|
Boolean array, True for the frames to exclude. |
required |
fs
|
float
|
Sampling frequency in Hz; a run ends one sample after its last frame, so that a single bad frame gives a segment of 1/fs. |
required |
label
|
str
|
Why the frames are excluded, e.g. |
required |
Returns:
| Type | Description |
|---|---|
list[Segment]
|
List of segments with |
missing_frames ¶
missing_frames(pixels: ndarray) -> np.ndarray
Frames where some pixel has no value (NaN or inf).
A pixel that has no value in the whole recording (e.g. outside the belt) does not make a frame missing: it is simply not part of the image.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixels
|
ndarray
|
Array (N, rows, cols). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Boolean array (N,). |
fill_missing_frames ¶
fill_missing_frames(pixels: ndarray, missing: ndarray) -> np.ndarray
Replace the missing frames with the nearest valid frame.
Same rule as breaths.fill_with_nearest, applied to whole frames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixels
|
ndarray
|
Array (N, rows, cols). |
required |
missing
|
ndarray
|
Boolean array (N,) from |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A copy of |
Raises:
| Type | Description |
|---|---|
ValueError
|
if every frame is missing. |
widen ¶
widen(segments: list[Segment], margin: float, duration: float) -> list[Segment]
Each segment made longer by margin seconds on both sides.
The result is clipped to the recording, from 0 to duration seconds.
Raises:
| Type | Description |
|---|---|
ValueError
|
if a segment starts after the end of the recording. |
merge_overlapping ¶
merge_overlapping(segments: list[Segment]) -> list[Segment]
Join the segments of the same source that overlap or touch.
Manual and device segments are never joined, so a manual exclusion stays apart
from the device ones (both are widened by the margin before). The labels of
joined segments are listed together, e.g. "artifact, movement"; the
parameters are those of the first segment.
Returns:
| Type | Description |
|---|---|
list[Segment]
|
Segments sorted by start time. |
breaths_in_segments ¶
breaths_in_segments(breath_starts: ndarray, breath_ends: ndarray, segments: list[Segment]) -> tuple[np.ndarray, list[str | None]]
Which breaths touch an exclusion, and why.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
breath_starts
|
ndarray
|
Start time of each breath, in seconds. |
required |
breath_ends
|
ndarray
|
End time of each breath, in seconds. |
required |
segments
|
list[Segment]
|
Exclusions. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
list[str | None]
|
labels of the exclusions it touches, e.g. |
tuple[ndarray, list[str | None]]
|
if none has a label), or None if the breath is not excluded. |
breaths ¶
Breath detection on the global impedance signal.
Reimplementation of eitprocessing BreathDetection (v1.8.8; Somhorst et al.,
JOSS 2026;11(117):8179, DOI: 10.21105/joss.08179). It gives the same breaths as
eitprocessing on 11 regular real recordings (notebooks/05_preprocessing_choices.ipynb,
section 8) and on synthetic signals with a spike and with small breaths. This shows
equivalence with eitprocessing, not accuracy against a gold standard.
One change: breaths close to invalid data are flagged, not removed.
A breath is a triplet of frame indices: start (valley), peak, end (next valley). Impedance is assumed to rise with inspiration (as in the Draeger .bin pixels).
Limitations (to keep in mind when reading Ti, Te and the respiratory rate): - the valley is the minimum of impedance, i.e. of lung volume, not the start of the inspiratory effort or the ventilator trigger; the peak includes any inspiratory pause. Timings are impedance-based and not interchangeable with the ventilator values; - patient-ventilator asynchrony is not detected: an ineffective effort is usually not a peak, and two inspirations without a valley between them become one breath; - the 15 s baseline follows slow EELI drift, but after a sudden EELI step (PEEP change, recruitment) the breaths within about 15 s of the step are unreliable.
invalid_samples ¶
invalid_samples(signal: ndarray, percentile: float, multiplier: float) -> np.ndarray
Indices of the gross outliers of the signal (step 1).
A sample is invalid when it is below mean - multiplier * (mean - low) or above
mean + multiplier * (high - mean), where low and high are the
percentile and 100 - percentile percentiles of the signal. With the
defaults (5, 4) only gross outliers are caught: artefacts such as a disconnection
spike, but also extreme manoeuvres such as a sustained inflation or a large
recruitment step, which are physiological.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D global signal. |
required |
percentile
|
float
|
Lower percentile; the upper one is |
required |
multiplier
|
float
|
How many times the mean-to-percentile distance is allowed. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Sorted indices of the invalid samples (empty if none). |
fill_with_nearest ¶
fill_with_nearest(signal: ndarray, missing: ndarray) -> np.ndarray
Replace the samples at missing with the nearest valid sample (step 1).
In a run of missing samples the first len(run) // 2 samples take the value
just before the run and the others the value just after it (so the middle sample
of an odd run takes the value after). A run at the start or at the end of the
signal takes the only valid neighbour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
missing
|
ndarray
|
Indices of the samples to replace. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A copy of |
Raises:
| Type | Description |
|---|---|
ValueError
|
if every sample is missing. |
local_baseline ¶
local_baseline(signal: ndarray, fs: float, seconds: float) -> np.ndarray
Slow moving average of the signal, used as a local threshold (step 2).
Weighted with a Blackman window, centred, with the first and last value repeated at
the edges. The window has an odd number of samples and is shortened to fit a signal
shorter than seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
seconds
|
float
|
Window width in seconds. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of the same length as |
peaks_and_valleys ¶
peaks_and_valleys(signal: ndarray, baseline: ndarray, fs: float, min_duration: float) -> tuple[np.ndarray, np.ndarray]
Candidate peaks above the baseline and valleys below it (step 3).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
baseline
|
ndarray
|
Local threshold of the same length ( |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
min_duration
|
float
|
Minimum time in seconds between two peaks and between two valleys. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
Indices of the peaks and indices of the valleys. |
trim_edges ¶
trim_edges(signal: ndarray, peaks: ndarray, valleys: ndarray, baseline: ndarray) -> tuple[np.ndarray, np.ndarray]
Drop the incomplete breaths at the start and end of the signal (step 4).
The first valley is dropped if the signal before it never rises above the baseline (the recording started in the middle of an expiration), and the same for the last valley. Then only the peaks between the first and the last valley are kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
peaks
|
ndarray
|
Candidate peak indices. |
required |
valleys
|
ndarray
|
Candidate valley indices, never the first or last sample (true for
the output of |
required |
baseline
|
ndarray
|
Local threshold of the same length as |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
Peaks and valleys without the edge cases (both empty if no breath is left). |
drop_doubles ¶
drop_doubles(signal: ndarray, peaks: ndarray, valleys: ndarray) -> tuple[np.ndarray, np.ndarray]
Keep one peak between two valleys and one valley between two peaks (step 5).
Walking from the first valley: if two valleys have no peak between them, the higher valley is dropped; if two valleys have more than one peak between them, the lower of the first two peaks is dropped. Repeat until every pair of consecutive valleys holds exactly one peak.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
peaks
|
ndarray
|
Peak indices. |
required |
valleys
|
ndarray
|
Valley indices. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
Peaks and valleys, alternating. |
drop_low_amplitude ¶
drop_low_amplitude(signal: ndarray, peaks: ndarray, valleys: ndarray, fraction: float) -> tuple[np.ndarray, np.ndarray]
Merge peaks that are too small to be a breath into the breath around them (step 6).
The amplitude of a peak is the mean of its rise from the valley before and its fall
to the valley after. Peaks below fraction times the median amplitude are
removed, then drop_doubles keeps the lower of the two valleys left next to each
other, so the small peak becomes part of a longer breath.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D signal. |
required |
peaks
|
ndarray
|
Peak indices, alternating with |
required |
valleys
|
ndarray
|
Valley indices. |
required |
fraction
|
float
|
Fraction of the median amplitude below which a peak is removed. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
Peaks and valleys, alternating. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if there is not exactly one more valley than peaks. |
flag_near_invalid ¶
flag_near_invalid(breaths: ndarray, invalid: ndarray, margin: int) -> np.ndarray
Which breaths are close to an invalid sample (step 8).
Same window as eitprocessing: from start - margin (included) to
end + margin (excluded), like a Python slice. eitprocessing removes these
breaths; fastEIT keeps them with a flag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
breaths
|
ndarray
|
Integer array of shape (n_breaths, 3): start, peak, end frame. |
required |
invalid
|
ndarray
|
Indices of the invalid samples ( |
required |
margin
|
int
|
Margin in samples. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Boolean array of length n_breaths. |
detect_breaths ¶
detect_breaths(signal: ndarray, fs: float, invalid_percentile: float, invalid_multiplier: float, baseline_seconds: float, min_duration_seconds: float, low_amplitude_fraction: float, invalid_margin_seconds: float) -> tuple[np.ndarray, np.ndarray]
Find the breaths in the global signal (steps 1-8 of eitprocessing BreathDetection).
- invalid samples are replaced with the nearest valid value;
- a slow Blackman moving average is the local baseline;
- peaks above it and valleys below it, at least
min_duration_secondsapart; - incomplete breaths at the edges are dropped;
- one peak between two valleys, one valley between two peaks;
- too small peaks are merged into the breath around them;
- breaths = valley, peak, next valley;
- breaths closer than
invalid_margin_secondsto invalid data are flagged (eitprocessing removes them).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D global signal without NaN (after the cardiac filter). |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
invalid_percentile
|
float
|
See |
required |
invalid_multiplier
|
float
|
See |
required |
baseline_seconds
|
float
|
Width of the local baseline. |
required |
min_duration_seconds
|
float
|
Minimum time between two peaks and between two valleys. |
required |
low_amplitude_fraction
|
float
|
See |
required |
invalid_margin_seconds
|
float
|
Margin around invalid samples for the flag. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
ndarray
|
frame of each breath; |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
breath_quality ¶
Quality flags of the detected breaths.
Flags mark a breath, they never remove it: the clinician decides (fastEIT design decision D5). Thresholds and the evidence for them are in notebooks/05_preprocessing_choices.ipynb, section 8; they are fastEIT choices, not taken from a paper.
All measures come from the global impedance signal, so Ti and Ttot are impedance-based (valley to peak, valley to valley) and not the ventilator values; the amplitude is in arbitrary impedance units, not a volume.
breath_measures ¶
breath_measures(signal: ndarray, fs: float, breaths: ndarray) -> pd.DataFrame
Duration, Ti/Ttot and amplitude of each breath.
The amplitude is the mean of the rise from the start valley to the peak and the
fall from the peak to the end valley, as in eitprocessing BreathDetection
(v1.8.8; Somhorst et al., JOSS 2026;11(117):8179, DOI: 10.21105/joss.08179).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D global signal used for the breath detection. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
breaths
|
ndarray
|
Integer array of shape (n_breaths, 3): start, peak, end frame. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with one row per breath and columns |
DataFrame
|
|
DataFrame
|
zero duration has |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
ratio_to_local_median ¶
ratio_to_local_median(values: ndarray, window_breaths: int, min_breaths: int) -> np.ndarray
Each value divided by the median of the breaths around it.
The window is centred on the breath and includes it. At the start and end of the
recording the window is shorter; with fewer than min_breaths values the ratio
is NaN, so no outlier flag can be set there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
ndarray
|
One value per breath, in time order (e.g. amplitudes). |
required |
window_breaths
|
int
|
Number of breaths in the window (odd, e.g. 21). |
required |
min_breaths
|
int
|
Fewest breaths needed to compute the median. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array of ratios, same length as |
quality_flags ¶
quality_flags(signal: ndarray, fs: float, breaths: ndarray, near_invalid: ndarray, short_seconds: float, long_seconds: float, ti_ratio_low: float, ti_ratio_high: float, window_breaths: int, min_breaths: int, amplitude_ratio_low: float, amplitude_ratio_high: float, duration_ratio_high: float) -> pd.DataFrame
One true/false column per quality check, one row per breath.
flag_short/flag_long: duration belowshort_secondsor abovelong_seconds;flag_suspect_ti_ratio: Ti/Ttot outsideti_ratio_low-ti_ratio_high;flag_amplitude_outlier: amplitude belowamplitude_ratio_lowor aboveamplitude_ratio_hightimes the median of the surrounding breaths;flag_duration_outlier: duration aboveduration_ratio_hightimes the median of the surrounding breaths;flag_invalid_data: close to invalid data (near_invalidofdetect_breaths).
On 946 breaths of 11 regular recordings these rules flagged 9 Ti/Ttot, 1 amplitude and 3 duration outliers, all visible events (section 8 of the evidence notebook).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
ndarray
|
1-D global signal used for the breath detection. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
breaths
|
ndarray
|
Integer array (n_breaths, 3) from |
required |
near_invalid
|
ndarray
|
Boolean array (n_breaths,) from |
required |
short_seconds
|
float
|
Shortest duration in seconds. |
required |
long_seconds
|
float
|
Longest duration in seconds. |
required |
ti_ratio_low
|
float
|
Lowest Ti/Ttot. |
required |
ti_ratio_high
|
float
|
Highest Ti/Ttot. |
required |
window_breaths
|
int
|
Window of |
required |
min_breaths
|
int
|
Fewest breaths in that window. |
required |
amplitude_ratio_low
|
float
|
Lowest amplitude as a ratio to the local median. |
required |
amplitude_ratio_high
|
float
|
Highest amplitude as a ratio to the local median. |
required |
duration_ratio_high
|
float
|
Longest duration as a ratio to the local median. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame of booleans, one column per flag. The pipeline adds whether the |
DataFrame
|
breath is excluded and |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
lung_mask ¶
Functional lung mask with the watershed method.
Reimplementation of eitprocessing WatershedLungspace (v1.8.8; Somhorst et al.,
JOSS 2026;11(117):8179, DOI: 10.21105/joss.08179), with the pixel amplitude in its
"phase shift" mode (PixelBreath) and the small-region filter FilterROIBySize.
Checked to give the same masks as eitprocessing on 11 real recordings when every
breath is used (notebooks/05_preprocessing_choices.ipynb, sections 7b and 9).
The mask is the VENTILATED lung, not the anatomical lung: a collapsed region with a TIV below the threshold is outside it.
Steps: 1. TIV map: pixel value at the end of inspiration of the global signal minus the value at its start, mean over the breaths; 2. amplitude map: the same, but on each pixel's own timing, found by cross-correlation with the global signal (it catches pixels that inflate late, e.g. pendelluft); 3. threshold: a fraction of the maximum of the TIV map, applied to both maps; 4. watershed: the amplitude map is split into regions around its local maxima; a region is kept if its maximum is inside the TIV mask. Mask = kept regions inside the amplitude mask; 5. islands smaller than a minimum size are removed.
The watershed assumes smooth images, with high amplitude in the centre of the lung and low amplitude at its edges, as reconstructed EIT images are.
tiv_per_breath ¶
tiv_per_breath(pixels: ndarray, breaths: ndarray) -> np.ndarray
Tidal impedance variation of each pixel at the timing of the global signal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixels
|
ndarray
|
Array (N, rows, cols). |
required |
breaths
|
ndarray
|
Integer array (n_breaths, 3): start, peak, end frame. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
(n_breaths, rows, cols)
|
pixel at the peak minus pixel at the start. |
pixel_lag ¶
pixel_lag(global_detrended: ndarray, pixel_detrended: ndarray, max_lag: float) -> int | None
Lag between the global signal and a pixel, in samples.
The cross-correlation peak closest to zero lag within ± max_lag (the higher
one if two are equally close), as in eitprocessing PixelBreath. The lag is
minus the delay of the pixel: a pixel that inflates 10 samples after the global
signal gives -10, and its own peaks are at global_peaks - lag.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
global_detrended
|
ndarray
|
Global signal without its linear trend. |
required |
pixel_detrended
|
ndarray
|
Pixel signal without its linear trend. |
required |
max_lag
|
float
|
Largest lag searched, in samples. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The lag, or None if the cross-correlation has no clear positive peak. |
amplitude_per_breath ¶
amplitude_per_breath(pixels: ndarray, global_signal: ndarray, breaths: ndarray) -> np.ndarray
Amplitude of each pixel in each breath, on the pixel's own timing.
For each pixel: the global peaks are moved to the pixel's own timing
(pixel_lag); between two shifted peaks the pixel minimum is its start of
inspiration, between two minima its maximum is its peak; amplitude = peak -
start. As in eitprocessing, the first and last breath have no value, a breath
whose shifted peak falls outside the recording or whose order is wrong is
skipped, and a pixel that loses more than MAX_SKIPPED_FRACTION of its
breaths is left out.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixels
|
ndarray
|
Array (N, rows, cols); a pixel is either NaN in every frame or in none. |
required |
global_signal
|
ndarray
|
1-D signal used for the breath detection. |
required |
breaths
|
ndarray
|
Integer array (n_breaths, 3), consecutive breaths. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Array (n_breaths, rows, cols), NaN where there is no amplitude. |
watershed_mask ¶
watershed_mask(pixels: ndarray, global_signal: ndarray, breaths: ndarray, use_breath: ndarray, threshold_fraction: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]
Lung mask from the TIV and amplitude maps (steps 1-4).
TIV and amplitude are computed on all breaths, because the pixel timing needs
consecutive breaths, and averaged only over the breaths in use_breath that
have both a TIV and an amplitude (e.g. the valid breaths). With every breath used
the result is the same as eitprocessing WatershedLungspace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pixels
|
ndarray
|
Array (N, rows, cols), after the cardiac filter. |
required |
global_signal
|
ndarray
|
1-D signal used for the breath detection. |
required |
breaths
|
ndarray
|
Integer array (n_breaths, 3) from |
required |
use_breath
|
ndarray
|
Boolean array (n_breaths,): breaths to average over. |
required |
threshold_fraction
|
float
|
Fraction of the maximum TIV (0.15 in eitprocessing). |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
|
ndarray
|
first and last breath are never in the means (they have no amplitude); a |
ndarray
|
pixel without data is NaN in the maps. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if no breath can be used, the TIV map has no positive maximum (inverted signal), or no amplitude peak lies inside the TIV mask. |
remove_small_regions ¶
remove_small_regions(mask: ndarray, min_pixels: int) -> np.ndarray
Remove the islands of the mask smaller than min_pixels (step 5).
Pixels belong to the same island when they share a side (4-connectivity), as in
eitprocessing FilterROIBySize.
regions ¶
Regions of interest inside the lung mask.
Orientation of Draeger images (PulmoVista 500 instructions for use): seen from the feet, like a CT slice. Row 0 is ventral; column 0 is the patient's RIGHT, column 31 the patient's left. Consistent with the power at the heart rate per pixel (notebooks/05_preprocessing_choices.ipynb, section 9). Verified on Draeger only.
Regions (section 9):
- ventral/dorsal halves and four layers are cut on the vertical extent of the lung
(first to last row of the mask), not on the image, so "dorsal" is always the
dorsal part of that lung wherever the lungs sit in the image. Same approach as
the createSummersROIs.m script of the original MATLAB analysis; the published
source of this approach is still to be found;
- right/left halves are split at the image midline (between columns 15 and 16),
the sternum-spine axis with the belt centred on the patient;
- quadrants = ventral/dorsal x right/left;
- every region is intersected with the lung mask.
Limit: these regions are relative to the VENTILATED lung. If the dorsal lung collapses, its pixels leave the mask and "dorsal" becomes the dorsal half of what is still ventilated, so the ventral/dorsal split hides part of a ventral shift of ventilation. Indices meant to show a shift of ventilation (centre of ventilation, dorsal fraction, silent spaces) must be computed in image coordinates, not on these regions.
Not the same as ReconstructedFrameData.roi_signals (layer 1), which are four
fixed strips of 8 image rows each.
lung_regions ¶
lung_regions(mask: ndarray) -> dict[str, np.ndarray]
All the regions of interest of a lung mask.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mask
|
ndarray
|
Boolean lung mask (rows, cols), row 0 ventral, column 0 patient right. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, ndarray]
|
Dict of boolean masks, each inside |
dict[str, ndarray]
|
|
dict[str, ndarray]
|
dorsal), |
dict[str, ndarray]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
if the mask is empty. |
auxiliary ¶
Auxiliary signals recorded with the images: pressures, flow, volume, device markers.
The parsers keep them as they are in the file (raw.aux_signals): for Draeger
.bin the channels that were not available hold "no data" values (-1000, or a
bit pattern read as about -3.4e38) instead of NaN, and each vendor has its own units.
Here they are cleaned once, so the next layers read every vendor in the same way:
"no data" as NaN and the same units, pressures in cmH2O, flow in L/s, volumes in mL.
The signals are not filtered and not filled: a missing value stays NaN.
common_unit ¶
common_unit(name: str, unit: str | None) -> tuple[str | None, float]
Common unit of a channel and the factor that converts it from its parser unit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Channel name, e.g. |
required |
unit
|
str | None
|
Unit given by the parser (None if unknown). |
required |
Returns:
| Type | Description |
|---|---|
tuple[str | None, float]
|
The common unit and the factor; unknown or other units are kept (factor 1). |
auxiliary_signals ¶
auxiliary_signals(raw: ReconstructedFrameData) -> tuple[pd.DataFrame, dict[str, str | None]]
One column per auxiliary channel that has data, one row per frame, and its unit.
- Draeger "no data" values become NaN (Timpel channels are already NaN from the parser);
- channels without any value are left out (e.g. Medibus when no ventilator was connected);
- units become common to every vendor: pressures in cmH2O (Draeger gives mbar), flow in L/s, volumes in mL (Timpel gives L), and the same for the values derived from them (e.g. compliance in mL/cmH2O); other units are kept;
- Draeger
min_max_flag(+1 end of inspiration, -1 end of expiration, 0 neither) andevent_markerare added; Timpelmin_flag,max_flagandqrs_flagare already among its channels.
Rows are frame numbers, the same as start_idx, peak_idx and end_idx
of the breath table. The units of the parsers are in
fasteit.parsers.draeger.bin.draeger_dtypes and timpel_dtypes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
ReconstructedFrameData
|
Parsed recording; never modified. |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with N rows (empty columns if there are no auxiliary signals), and |
dict[str, str | None]
|
the unit of each column ("" for markers and flags, None if unknown). |
breath_pressures ¶
Airway pressures of each breath: PEEP, plateau and driving pressure.
Read on the airway pressure curve (cmH2O), all from the same channel so that a constant offset of the sensor cancels in the driving pressure (notebooks/06_peep_trial_segmentation.ipynb, sections 2 and 4). The PEEP of a breath is the end-expiratory pressure at the airway opening: the PEEP applied by the ventilator, without intrinsic PEEP (that would need an end-expiratory occlusion), so it is a lower bound of the total PEEP; with intrinsic PEEP the driving pressure is overestimated and compliance underestimated. A breath whose pressure is missing (NaN) near its start gets NaN.
inspiration_onsets ¶
inspiration_onsets(paw: ndarray, fs: float, eit_starts: ndarray) -> np.ndarray
Start of inspiration on the pressure curve, near the start of each EIT breath.
The steepest rise of the pressure is searched from 1 s before to 0.5 s after the start of the EIT breath; the start of inspiration is the last minimum of the pressure in the 0.5 s before it. The EIT valley falls within about 0.1 s of it, but there the pressure already rises quickly (notebook 06, section 2).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paw
|
ndarray
|
Airway pressure, one value per frame. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
eit_starts
|
ndarray
|
Frame of the start of each EIT breath ( |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Frame of the start of inspiration of each breath; -1 where the pressure is |
ndarray
|
missing in the search window. |
pressure_before ¶
pressure_before(paw: ndarray, fs: float, frames: ndarray, seconds: float) -> np.ndarray
Median pressure in the seconds before each frame (the PEEP of a breath).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paw
|
ndarray
|
Airway pressure, one value per frame. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
frames
|
ndarray
|
Frames, e.g. the starts of inspiration (-1 = unknown). |
required |
seconds
|
float
|
Length of the reading. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One median per frame; NaN if the frame is unknown, too close to the start of the |
ndarray
|
recording, or the pressure is missing in the reading. |
plateau_pressures ¶
plateau_pressures(paw: ndarray, fs: float, onsets: ndarray, peeps: ndarray, window: tuple[float, float]) -> np.ndarray
Plateau pressure of each breath, read at the end of the end-inspiratory hold.
Between the start of inspiration of a breath and the next one: after the highest
part of the breath (the peak in volume control, the flat pressure in pressure
control), the start of expiration is the steepest fall of the pressure in the 0.3 s
before it comes back within 2 of the next PEEP; the plateau is the median pressure
from window[0] to window[1] seconds before it (notebook 06, section 4).
A valid plateau needs at least 0.5 s between the highest part and the expiration: an end-inspiratory hold with zero flow in volume control (continuous pause, or an inspiratory hold done by hand), or the flat inspiratory pressure in pressure control, which is a plateau only if the flow is zero at the end of inspiration. Otherwise the plateau is NaN: the reading would fall on the relaxation after the peak, and the peak pressure is never used instead because it includes the resistive pressure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paw
|
ndarray
|
Airway pressure, one value per frame. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
onsets
|
ndarray
|
Start of inspiration of each breath ( |
required |
peeps
|
ndarray
|
PEEP of each breath ( |
required |
window
|
tuple[float, float]
|
(start, end) of the reading, in seconds before the start of expiration. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One plateau per breath; NaN for the last breath (no next breath), for unknown |
ndarray
|
onsets, for breaths with missing pressure and where no hold is found. |
tidal_volume ¶
Tidal volume of each breath from the ventilator curves (mL).
The inspiratory tidal volume: in volume control it is the volume delivered, the one set on the ventilator. It is read on the volume curve, or on the flow curve by integrating the inspiratory flow, in a window around the inspiration of each EIT breath. It is the volume measured by the ventilator: with a leak (non-invasive ventilation, a leaking cuff) it includes the leak, and without compensation of the compressible volume of the circuit it includes that too.
Tested on synthetic curves only: the recordings available have no volume or flow curve. Inspiratory flow is taken as positive, as recorded by the ventilators.
inspiration_windows ¶
inspiration_windows(starts: ndarray, peaks: ndarray, ends: ndarray, n_frames: int) -> tuple[np.ndarray, np.ndarray]
Frames around the inspiration of each breath, from the EIT breath.
From half an expiration before the start of the breath (but not before the middle of the expiration of the breath before) to the middle of its own expiration. The window holds the whole inspiration even when the EIT valley falls a little after the start of the inspiratory flow (within about 0.1 s in notebooks/06_peep_trial_segmentation.ipynb, section 2), and nothing of the inspirations before and after it, also after a long inspiratory hold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
starts
|
ndarray
|
Frame of the start of each breath. |
required |
peaks
|
ndarray
|
Frame of the peak of each breath. |
required |
ends
|
ndarray
|
Frame of the end of each breath. |
required |
n_frames
|
int
|
Number of frames of the recording. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
First frame and frame after the last of each window. |
volumes_from_volume_curve ¶
volumes_from_volume_curve(volume: ndarray, first: ndarray, after_last: ndarray) -> np.ndarray
Inspiratory tidal volume on a volume curve (mL).
The highest volume in the window minus the lowest one before it, so that it works whether or not the ventilator sets the volume back to zero at each breath. If it does, and more volume came out than went in during the breath before (e.g. the first breaths after a decrease of PEEP, as the lung empties), the difference is added: the lowest value is then the end of that expiration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
volume
|
ndarray
|
Volume curve in mL, one value per frame. |
required |
first
|
ndarray
|
First frame of the window of each breath ( |
required |
after_last
|
ndarray
|
Frame after the last of each window. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One volume per breath; NaN if the volume is missing in the window. |
volumes_from_flow_curve ¶
volumes_from_flow_curve(flow: ndarray, fs: float, first: ndarray, after_last: ndarray) -> np.ndarray
Inspiratory tidal volume on a flow curve (mL): integral of the inspiratory flow.
The flow is integrated over the inspiration: the run of positive flow around its highest value in the window. Noise and cardiac oscillations around zero flow (end-inspiratory pause, end of expiration) are left out, unlike a sum of all the positive flow of the window. A zero offset of the flow sensor adds offset x duration of the inspiration (e.g. 0.01 L/s over 1 s = 10 mL).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flow
|
ndarray
|
Flow curve in L/s, one value per frame, inspiration positive. |
required |
fs
|
float
|
Sampling frequency in Hz. |
required |
first
|
ndarray
|
First frame of the window of each breath ( |
required |
after_last
|
ndarray
|
Frame after the last of each window. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
One volume per breath; NaN if the flow is missing in the window, 0 if it is |
ndarray
|
never positive. |