volfiros commited on
Commit
98f83eb
·
1 Parent(s): 6e95d70

fix: review issues

Browse files
backend/requirements.txt CHANGED
@@ -1,5 +1,5 @@
1
- fastapi
2
- uvicorn[standard]
3
- numpy
4
- scipy
5
- pydantic
 
1
+ fastapi>=0.115
2
+ uvicorn[standard]>=0.34
3
+ numpy>=2.0
4
+ scipy>=1.13
5
+ pydantic>=2.0
backend/signal_processor.py CHANGED
@@ -11,7 +11,6 @@ class SignalProcessor:
11
  self.r_buffer: deque[float] = deque(maxlen=self.capacity)
12
  self.g_buffer: deque[float] = deque(maxlen=self.capacity)
13
  self.b_buffer: deque[float] = deque(maxlen=self.capacity)
14
- self.timestamps: deque[float] = deque(maxlen=self.capacity)
15
  self.last_compute_time = 0.0
16
  self.compute_interval = 1.0
17
  self.low_hz = 0.7
@@ -19,12 +18,17 @@ class SignalProcessor:
19
  self.filter_order = 4
20
  self.min_calibrate_samples: int = fps * 5
21
  self.min_measuring_samples: int = fps * 15
 
 
 
 
 
 
22
 
23
  def append(self, r: float, g: float, b: float) -> None:
24
  self.r_buffer.append(r)
25
  self.g_buffer.append(g)
26
  self.b_buffer.append(b)
27
- self.timestamps.append(time.time())
28
 
29
  def _compute_chrom(self, r_arr: np.ndarray, g_arr: np.ndarray, b_arr: np.ndarray) -> np.ndarray:
30
  xs = 3.0 * g_arr - 2.0 * r_arr
@@ -38,7 +42,8 @@ class SignalProcessor:
38
  return xs_centered - alpha * ys_centered
39
 
40
  def _bandpass_filter(self, signal: np.ndarray) -> np.ndarray:
41
- if len(signal) < 30:
 
42
  return signal
43
  nyquist = self.fps / 2.0
44
  low = self.low_hz / nyquist
@@ -53,13 +58,15 @@ class SignalProcessor:
53
  return 0.0
54
  return (len(peaks) / duration) * 60.0
55
 
56
- def _compute_snr(self, filtered: np.ndarray) -> float:
57
- signal_power = np.mean(filtered ** 2)
58
- noise = filtered - np.mean(filtered)
59
- noise_power = np.var(noise)
60
- if noise_power < 1e-10:
 
 
61
  return 100.0
62
- return 10.0 * np.log10(signal_power / noise_power)
63
 
64
  def _get_status(self, n_samples: int, snr: float, bpm: float) -> str:
65
  if n_samples < self.min_calibrate_samples:
@@ -81,15 +88,16 @@ class SignalProcessor:
81
 
82
  n = len(self.r_buffer)
83
  if n < self.min_calibrate_samples:
84
- return {
85
  "bpm": 0,
86
  "confidence": 0.0,
87
  "waveform": [],
88
  "status": "buffering",
89
  }
 
90
 
91
- if now - self.last_compute_time < self.compute_interval and n >= self.min_calibrate_samples:
92
- return None
93
 
94
  self.last_compute_time = now
95
 
@@ -102,13 +110,14 @@ class SignalProcessor:
102
 
103
  duration = n / self.fps
104
  bpm = self._compute_bpm(filtered, duration)
105
- snr = self._compute_snr(filtered)
106
  status = self._get_status(n, snr, bpm)
107
  confidence = self._get_confidence(n, snr)
108
 
109
- return {
110
  "bpm": round(bpm, 1),
111
  "confidence": confidence,
112
  "waveform": filtered[-450:].tolist() if len(filtered) > 450 else filtered.tolist(),
113
  "status": status,
114
  }
 
 
11
  self.r_buffer: deque[float] = deque(maxlen=self.capacity)
12
  self.g_buffer: deque[float] = deque(maxlen=self.capacity)
13
  self.b_buffer: deque[float] = deque(maxlen=self.capacity)
 
14
  self.last_compute_time = 0.0
15
  self.compute_interval = 1.0
16
  self.low_hz = 0.7
 
18
  self.filter_order = 4
19
  self.min_calibrate_samples: int = fps * 5
20
  self.min_measuring_samples: int = fps * 15
21
+ self._last_result: dict = {
22
+ "bpm": 0,
23
+ "confidence": 0.0,
24
+ "waveform": [],
25
+ "status": "buffering",
26
+ }
27
 
28
  def append(self, r: float, g: float, b: float) -> None:
29
  self.r_buffer.append(r)
30
  self.g_buffer.append(g)
31
  self.b_buffer.append(b)
 
32
 
33
  def _compute_chrom(self, r_arr: np.ndarray, g_arr: np.ndarray, b_arr: np.ndarray) -> np.ndarray:
34
  xs = 3.0 * g_arr - 2.0 * r_arr
 
42
  return xs_centered - alpha * ys_centered
43
 
44
  def _bandpass_filter(self, signal: np.ndarray) -> np.ndarray:
45
+ min_len = 3 * (self.filter_order + 1)
46
+ if len(signal) < min_len:
47
  return signal
48
  nyquist = self.fps / 2.0
49
  low = self.low_hz / nyquist
 
58
  return 0.0
59
  return (len(peaks) / duration) * 60.0
60
 
61
+ def _compute_snr(self, filtered: np.ndarray, original: np.ndarray) -> float:
62
+ residual = original - filtered
63
+ signal_var = np.var(filtered)
64
+ residual_var = np.var(residual)
65
+ if signal_var < 1e-10:
66
+ return 0.0
67
+ if residual_var < 1e-10:
68
  return 100.0
69
+ return 10.0 * np.log10(signal_var / residual_var)
70
 
71
  def _get_status(self, n_samples: int, snr: float, bpm: float) -> str:
72
  if n_samples < self.min_calibrate_samples:
 
88
 
89
  n = len(self.r_buffer)
90
  if n < self.min_calibrate_samples:
91
+ self._last_result = {
92
  "bpm": 0,
93
  "confidence": 0.0,
94
  "waveform": [],
95
  "status": "buffering",
96
  }
97
+ return self._last_result
98
 
99
+ if now - self.last_compute_time < self.compute_interval:
100
+ return self._last_result
101
 
102
  self.last_compute_time = now
103
 
 
110
 
111
  duration = n / self.fps
112
  bpm = self._compute_bpm(filtered, duration)
113
+ snr = self._compute_snr(filtered, chrom)
114
  status = self._get_status(n, snr, bpm)
115
  confidence = self._get_confidence(n, snr)
116
 
117
+ self._last_result = {
118
  "bpm": round(bpm, 1),
119
  "confidence": confidence,
120
  "waveform": filtered[-450:].tolist() if len(filtered) > 450 else filtered.tolist(),
121
  "status": status,
122
  }
123
+ return self._last_result
tests/conftest.py CHANGED
@@ -1,6 +1,10 @@
 
 
1
  import numpy as np
2
  import pytest
3
 
 
 
4
 
5
  @pytest.fixture
6
  def sine_wave_72bpm():
 
1
+ import sys
2
+ import os
3
  import numpy as np
4
  import pytest
5
 
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
7
+
8
 
9
  @pytest.fixture
10
  def sine_wave_72bpm():
tests/test_signal_processor.py CHANGED
@@ -1,8 +1,3 @@
1
- import sys
2
- import os
3
- import time
4
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "backend"))
5
-
6
  from signal_processor import SignalProcessor
7
 
8
 
@@ -18,7 +13,6 @@ def test_calibrating_status_at_5_seconds():
18
  sp = SignalProcessor(fps=30)
19
  for i in range(150):
20
  result = sp.process(140.0 + (i % 10) * 0.1, 142.0 + (i % 8) * 0.1, 138.0)
21
- assert result is not None
22
  assert result["status"] in ("calibrating", "measuring", "poor_signal")
23
 
24
 
@@ -28,8 +22,7 @@ def test_bpm_detection_72bpm(sine_wave_72bpm):
28
  result = None
29
  for r, g, b in sine_wave_72bpm:
30
  res = sp.process(float(r), float(g), float(b))
31
- if res is not None:
32
- result = res
33
  assert result is not None
34
  assert result["status"] in ("measuring", "poor_signal")
35
  assert 65 <= result["bpm"] <= 80
@@ -41,8 +34,7 @@ def test_bpm_detection_90bpm(sine_wave_90bpm):
41
  result = None
42
  for r, g, b in sine_wave_90bpm:
43
  res = sp.process(float(r), float(g), float(b))
44
- if res is not None:
45
- result = res
46
  assert result is not None
47
  assert result["status"] in ("measuring", "poor_signal")
48
  assert 83 <= result["bpm"] <= 100
@@ -62,7 +54,7 @@ def test_confidence_increases_with_buffer():
62
  g = 142.0 + 4.0 * np.sin(2 * np.pi * freq * t[i] + 0.3)
63
  b = 138.0 + 1.0 * np.sin(2 * np.pi * freq * t[i] + 0.6)
64
  res = sp.process(r, g, b)
65
- if res is not None and res["confidence"] > 0:
66
  confidences.append(res["confidence"])
67
  assert len(confidences) >= 2
68
  assert confidences[-1] >= confidences[0]
@@ -81,9 +73,7 @@ def test_waveform_length_capped():
81
  g = 142.0 + 4.0 * np.sin(2 * np.pi * freq * t[i] + 0.3)
82
  b = 138.0 + 1.0 * np.sin(2 * np.pi * freq * t[i] + 0.6)
83
  sp.process(r, g, b)
84
- time.sleep(0.05)
85
  last = sp.process(r, g, b)
86
- assert last is not None
87
  assert len(last["waveform"]) <= 450
88
 
89
 
@@ -95,6 +85,6 @@ def test_compute_throttled_to_1hz():
95
  results = []
96
  for i in range(60):
97
  res = sp.process(140.0, 142.0, 138.0)
98
- if res is not None:
99
- results.append(res)
100
- assert len(results) <= 3
 
 
 
 
 
 
1
  from signal_processor import SignalProcessor
2
 
3
 
 
13
  sp = SignalProcessor(fps=30)
14
  for i in range(150):
15
  result = sp.process(140.0 + (i % 10) * 0.1, 142.0 + (i % 8) * 0.1, 138.0)
 
16
  assert result["status"] in ("calibrating", "measuring", "poor_signal")
17
 
18
 
 
22
  result = None
23
  for r, g, b in sine_wave_72bpm:
24
  res = sp.process(float(r), float(g), float(b))
25
+ result = res
 
26
  assert result is not None
27
  assert result["status"] in ("measuring", "poor_signal")
28
  assert 65 <= result["bpm"] <= 80
 
34
  result = None
35
  for r, g, b in sine_wave_90bpm:
36
  res = sp.process(float(r), float(g), float(b))
37
+ result = res
 
38
  assert result is not None
39
  assert result["status"] in ("measuring", "poor_signal")
40
  assert 83 <= result["bpm"] <= 100
 
54
  g = 142.0 + 4.0 * np.sin(2 * np.pi * freq * t[i] + 0.3)
55
  b = 138.0 + 1.0 * np.sin(2 * np.pi * freq * t[i] + 0.6)
56
  res = sp.process(r, g, b)
57
+ if res["confidence"] > 0:
58
  confidences.append(res["confidence"])
59
  assert len(confidences) >= 2
60
  assert confidences[-1] >= confidences[0]
 
73
  g = 142.0 + 4.0 * np.sin(2 * np.pi * freq * t[i] + 0.3)
74
  b = 138.0 + 1.0 * np.sin(2 * np.pi * freq * t[i] + 0.6)
75
  sp.process(r, g, b)
 
76
  last = sp.process(r, g, b)
 
77
  assert len(last["waveform"]) <= 450
78
 
79
 
 
85
  results = []
86
  for i in range(60):
87
  res = sp.process(140.0, 142.0, 138.0)
88
+ results.append(res)
89
+ unique_bpms = set(r["bpm"] for r in results)
90
+ assert len(unique_bpms) <= 3