fix: Correct rfft array size calculation in vital signs detection

Fixed IndexError in breathing and heartbeat detection caused by incorrect
rfft output length calculation. The rfft output length is n//2+1 for input
of length n, so original length is (len(spectrum)-1)*2, not len(spectrum)*2.

Also added array length alignment to prevent edge case dimension mismatches.
This commit is contained in:
Claude
2026-01-14 18:33:28 +00:00
parent 2ca107c10c
commit b4a739402b

View File

@@ -175,15 +175,22 @@ class BreathingDetector:
max_freq: float max_freq: float
) -> Optional[Tuple[float, float]]: ) -> Optional[Tuple[float, float]]:
"""Find the dominant frequency in a given range.""" """Find the dominant frequency in a given range."""
n = len(spectrum) * 2 # Original signal length # rfft output length is n//2 + 1 for input of length n
# So original length n = (len(spectrum) - 1) * 2
n = (len(spectrum) - 1) * 2
freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate) freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate)
# Ensure freqs and spectrum have same length
min_len = min(len(freqs), len(spectrum))
freqs = freqs[:min_len]
spectrum_trimmed = spectrum[:min_len]
# Find indices in the frequency range # Find indices in the frequency range
mask = (freqs >= min_freq) & (freqs <= max_freq) mask = (freqs >= min_freq) & (freqs <= max_freq)
if not np.any(mask): if not np.any(mask):
return None return None
masked_spectrum = spectrum.copy() masked_spectrum = spectrum_trimmed.copy()
masked_spectrum[~mask] = 0 masked_spectrum[~mask] = 0
# Find peak # Find peak
@@ -191,7 +198,7 @@ class BreathingDetector:
if masked_spectrum[peak_idx] == 0: if masked_spectrum[peak_idx] == 0:
return None return None
return freqs[peak_idx], spectrum[peak_idx] return freqs[peak_idx], spectrum_trimmed[peak_idx]
def _calculate_regularity( def _calculate_regularity(
self, self,
@@ -200,7 +207,7 @@ class BreathingDetector:
sample_rate: float sample_rate: float
) -> float: ) -> float:
"""Calculate how regular the breathing pattern is.""" """Calculate how regular the breathing pattern is."""
n = len(spectrum) * 2 n = (len(spectrum) - 1) * 2
freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate) freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate)
# Look at energy concentration around dominant frequency # Look at energy concentration around dominant frequency
@@ -395,15 +402,22 @@ class HeartbeatDetector:
max_freq: float max_freq: float
) -> Optional[Tuple[float, float]]: ) -> Optional[Tuple[float, float]]:
"""Find heartbeat frequency in the spectrum.""" """Find heartbeat frequency in the spectrum."""
n = len(spectrum) * 2 # rfft output length is n//2 + 1 for input of length n
# So original length n = (len(spectrum) - 1) * 2
n = (len(spectrum) - 1) * 2
freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate) freqs = scipy.fft.rfftfreq(n, 1.0 / sample_rate)
# Ensure freqs and spectrum have same length
min_len = min(len(freqs), len(spectrum))
freqs = freqs[:min_len]
spectrum_trimmed = spectrum[:min_len]
# Find indices in the frequency range # Find indices in the frequency range
mask = (freqs >= min_freq) & (freqs <= max_freq) mask = (freqs >= min_freq) & (freqs <= max_freq)
if not np.any(mask): if not np.any(mask):
return None return None
masked_spectrum = spectrum.copy() masked_spectrum = spectrum_trimmed.copy()
masked_spectrum[~mask] = 0 masked_spectrum[~mask] = 0
# Find peak # Find peak
@@ -411,7 +425,7 @@ class HeartbeatDetector:
if masked_spectrum[peak_idx] == 0: if masked_spectrum[peak_idx] == 0:
return None return None
return freqs[peak_idx], spectrum[peak_idx] return freqs[peak_idx], spectrum_trimmed[peak_idx]
def _classify_signal_strength(self, strength: float) -> SignalStrength: def _classify_signal_strength(self, strength: float) -> SignalStrength:
"""Classify signal strength level.""" """Classify signal strength level."""