Skip to content

Commit 1f8920a

Browse files
author
Joseph Atkins-Turkish
committed
Merge branch 'restructure' into threaded
* restructure: Changed make_filter back to just returning a function. Added tests for all the signals functions, and did some cleaning/formatting. Fixed bug in find_periodicities which resulted in incorrect frequencies Moved signal-processing and utility functions to separate files. Conflicts: main.py
2 parents e7d52c3 + 8a6d849 commit 1f8920a

File tree

4 files changed

+215
-131
lines changed

4 files changed

+215
-131
lines changed

facetracking.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
def make_face_rects(rect):
3+
""" Given a rectangle (covering a face), return two rectangles.
4+
which cover the forehead and the area under the eyes """
5+
x, y, w, h = rect
6+
7+
rect1_x = x + w / 4.0
8+
rect1_w = w / 2.0
9+
rect1_y = y + 0.05 * h
10+
rect1_h = h * 0.9 * 0.2
11+
12+
rect2_x = rect1_x
13+
rect2_w = rect1_w
14+
15+
rect2_y = y + 0.05 * h + (h * 0.9 * 0.55)
16+
rect2_h = h * 0.9 * 0.45
17+
18+
return (
19+
(int(rect1_x), int(rect1_y), int(rect1_w), int(rect1_h)),
20+
(int(rect2_x), int(rect2_y), int(rect2_w), int(rect2_h))
21+
)

main.py

Lines changed: 2 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import itertools
88
from scipy import interpolate, signal
99
import sklearn.decomposition
10-
10+
from signals import *
1111
import threading
1212
import Queue
1313

@@ -46,7 +46,7 @@ def __init__(self,frameQueue,incrementalPCA,n_components,windowSize):
4646
self.daemon = True
4747
self.frameQueue = frameQueue
4848
self.windowSize = windowSize
49-
if incremental:
49+
if incrementalPCA:
5050
self.pca = sklearn.decomposition.IncrementalPCA(n_components=n_components)
5151
else:
5252
self.pca = sklearn.decomposition.PCA(n_components=n_components)
@@ -192,135 +192,6 @@ def get_moving_points(source_queue, do_draw=True, n_points=100):
192192
print "Heart rate by peak estimate: {} BPM".format(countbpm)
193193
########################################################################################
194194

195-
def make_face_rects(rect):
196-
""" Given a rectangle (covering a face), return two rectangles.
197-
which cover the forehead and the area under the eyes """
198-
x, y, w, h = rect
199-
200-
rect1_x = x + w / 4.0
201-
rect1_w = w / 2.0
202-
rect1_y = y + 0.05 * h
203-
rect1_h = h * 0.9 * 0.2
204-
205-
rect2_x = rect1_x
206-
rect2_w = rect1_w
207-
208-
rect2_y = y + 0.05 * h + (h * 0.9 * 0.55)
209-
rect2_h = h * 0.9 * 0.45
210-
211-
return (
212-
(int(rect1_x), int(rect1_y), int(rect1_w), int(rect1_h)),
213-
(int(rect2_x), int(rect2_y), int(rect2_w), int(rect2_h))
214-
)
215-
216-
217-
218-
219-
def window(seq, n=2, skip=1):
220-
"Returns a sliding window (of width n) over data from the iterable"
221-
" s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... "
222-
# http://stackoverflow.com/questions/6822725/rolling-or-sliding-window-iterator-in-python
223-
224-
it = iter(seq)
225-
result = tuple(itertools.islice(it, n))
226-
if len(result) == n:
227-
yield result
228-
i = 0
229-
for elem in it:
230-
result = result[1:] + (elem,)
231-
i = (i + 1) % skip
232-
if i == 0:
233-
yield result
234-
235-
236-
def interpolate_points(points, ratio=30.0 / 250.0, axis=0):
237-
""" Given an matrix of waveforms, interpolate them along an axis
238-
such that the number of new is multiplied by (1/ratio) """
239-
# Define the old time space, i.e. the index of each point
240-
N = points.shape[axis]
241-
indices = np.arange(0, N)
242-
# Make an 'interpolation function' using scikit's interp1d
243-
f = interpolate.interp1d(indices, points, kind='cubic', axis=axis)
244-
# Define the new time axis,
245-
xnew = np.arange(0, N - 1, ratio)
246-
return f(xnew)
247-
248-
249-
def filter_unstable_movements(points):
250-
""" Filter unstable movements, e.g. coughing """
251-
""" In the paper, they removed points which had maximum movements
252-
greater than the "mode" of rounded maximum movements. Or something.
253-
This didn't really work or make sense when we tried it, so we tried
254-
something else, then gave up... """
255-
maximums = np.max(np.diff(points.T), axis=1)
256-
median = np.median(maximums)
257-
return points[:, maximums > median]
258-
259-
260-
def make_filter(order=5, low_freq=0.75, high_freq=5, sample_freq=250.0):
261-
""" Make the butterworth filter function required by the pulse paper"""
262-
nyq = 0.5 * sample_freq
263-
low = low_freq / nyq
264-
high = high_freq / nyq
265-
b, a = signal.butter(order, [low, high], btype='bandpass')
266-
return lambda x: signal.lfilter(b, a, x)
267-
268-
269-
def find_periodicities(X, sample_freq=250.0):
270-
""" Find the periodicity of each signal in a matrix(along axis 0),
271-
and the associated frequencies of the periods"""
272-
273-
# We're not sure if this is quite correct, but it's what the paper
274-
# seemed to imply...
275-
# This could also be made much neater, and optimised.
276-
277-
# Find the power spectrum of the signal (absolute fft squared)
278-
power = np.abs(np.fft.rfft(X, axis=0))**2
279-
280-
# Build a list of the actual frequencies corresponding to each fft index, using numpy's rfftfreq
281-
# n.b. This is where I'm having some trouble. I don't think I'm actually getting the right
282-
# numbers out for the frequencies of these signals...
283-
284-
real_frequencies = np.fft.rfftfreq(
285-
power.shape[0], d=(1 / (sample_freq)))
286-
287-
# Find the most powerful non-zero frequency in each signal
288-
max_indices = np.argmax(power[1:, :], axis=0) + 1
289-
290-
# The first haromic component of f = f*2
291-
harmonic_indices = max_indices * 2
292-
293-
# Initialise arrays for return values
294-
periodicities = []
295-
frequencies = []
296-
i = 0
297-
298-
# Loop over each signal
299-
for i1, i2 in zip(max_indices, harmonic_indices):
300-
# Get the real frequency highest power component
301-
frequencies.append(real_frequencies[i1])
302-
303-
# Get the total power of the highest power component and its
304-
# first harmonic, as a percentage of the total signal power
305-
period_power = np.sum(power[[i1, i2], i])
306-
total_power = np.sum(power[:, i])
307-
percentage = period_power / total_power
308-
309-
# That number is (apparently) the signal periodicity
310-
periodicities.append(percentage)
311-
i += 1
312-
313-
return np.array(frequencies), np.array(periodicities)
314-
315-
316-
def getpeaks(x, winsize=151):
317-
""" Return the indices of all points in a signal which are the largest poitns in
318-
a window centered on themselves """
319-
for i, win in enumerate(window(x, winsize, 1)):
320-
ind = int(winsize / 2)
321-
if np.argmax(win) == ind:
322-
yield i + ind
323-
324195

325196
def main(incrementalPCA=False):
326197
""" Run the full algorithm on a video """

signals.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import numpy as np
2+
import itertools
3+
from scipy import interpolate, signal
4+
5+
6+
def window(seq, n=2, skip=1):
7+
"Returns a sliding window (of width n) over data from the iterable"
8+
" s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ... "
9+
# http://stackoverflow.com/questions/6822725/rolling-or-sliding-window-iterator-in-python
10+
11+
it = iter(seq)
12+
result = tuple(itertools.islice(it, n))
13+
if len(result) == n:
14+
yield result
15+
i = 0
16+
for elem in it:
17+
result = result[1:] + (elem,)
18+
i = (i + 1) % skip
19+
if i == 0:
20+
yield result
21+
22+
23+
def interpolate_points(points, ratio=30.0 / 250.0, axis=0):
24+
""" Given an matrix of waveforms, interpolate them along an axis
25+
such that the number of new is multiplied by (1/ratio) """
26+
# Define the old time space, i.e. the index of each point
27+
N = points.shape[axis]
28+
indices = np.arange(0, N)
29+
# Make an 'interpolation function' using scikit's interp1d
30+
f = interpolate.interp1d(indices, points, kind='cubic', axis=axis)
31+
# Define the new time axis,
32+
xnew = np.arange(0, N - 1, ratio)
33+
return f(xnew)
34+
35+
36+
def filter_unstable_movements(points):
37+
""" Filter unstable movements, e.g. coughing """
38+
""" In the paper, they removed points which had maximum movements
39+
greater than the "mode" of rounded maximum movements. Or something.
40+
This didn't really work or make sense when we tried it, so we tried
41+
something else, then gave up... """
42+
maximums = np.max(np.diff(points.T), axis=1)
43+
median = np.median(maximums)
44+
return points[:, maximums > median]
45+
46+
47+
def make_filter(order=5, low_freq=0.75, high_freq=5, sample_freq=250.0):
48+
""" Make the butterworth filter function required by the pulse paper"""
49+
nyq = 0.5 * sample_freq
50+
low = low_freq / nyq
51+
high = high_freq / nyq
52+
b, a = signal.butter(order, [low, high], btype='bandpass')
53+
func = lambda x: signal.lfilter(b, a, x)
54+
func.b = b
55+
func.a = a
56+
return func
57+
58+
59+
def find_periodicities(X, sample_freq=250.0):
60+
""" Find the periodicity of each signal in a matrix(along axis 0),
61+
and the associated frequencies of the periods"""
62+
63+
# We're not sure if this is quite correct, but it's what the paper
64+
# seemed to imply...
65+
# This could also be made much neater, and optimised.
66+
67+
# Find the power spectrum of the signal (absolute fft squared)
68+
power = np.abs(np.fft.rfft(X, axis=0))**2
69+
70+
# Build a list of the actual frequencies corresponding to each fft index, using numpy's rfftfreq
71+
# n.b. This is where I'm having some trouble. I don't think I'm actually getting the right
72+
# numbers out for the frequencies of these signals...
73+
74+
real_frequencies = np.fft.rfftfreq(
75+
X.shape[0], d=(1 / (sample_freq)))
76+
77+
# Find the most powerful non-zero frequency in each signal
78+
max_indices = np.argmax(power[1:, :], axis=0) + 1
79+
80+
# The first haromic component of f = f*2
81+
harmonic_indices = max_indices * 2
82+
83+
# Initialise arrays for return values
84+
periodicities = []
85+
frequencies = []
86+
i = 0
87+
88+
# Loop over each signal
89+
for fundamental, harmonic in zip(max_indices, harmonic_indices):
90+
# Get the real frequency highest power component
91+
frequencies.append(real_frequencies[fundamental])
92+
93+
# Get the total power of the highest power component and its
94+
# first harmonic, as a percentage of the total signal power
95+
period_power = np.sum(power[[fundamental, harmonic], i])
96+
total_power = np.sum(power[:, i])
97+
percentage = period_power / total_power
98+
99+
# That number is (apparently) the signal periodicity
100+
periodicities.append(percentage)
101+
i += 1
102+
103+
return np.array(frequencies), np.array(periodicities)
104+
105+
106+
def getpeaks(x, winsize=151):
107+
""" Return the indices of all points in a signal which are the largest poitns in
108+
a window centered on themselves """
109+
for i, win in enumerate(window(x, winsize, 1)):
110+
ind = int(winsize / 2)
111+
if np.argmax(win) == ind:
112+
yield i + ind

tests.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import unittest
2+
import numpy as np
3+
import signals
4+
import matplotlib.pyplot as plt
5+
import math
6+
7+
8+
class TestSignalFunctions(unittest.TestCase):
9+
10+
def make_test_signal(self, sample_rate):
11+
""" Make a 10s signal with three frequency components
12+
f = [100, 10, 1]/(2pi) """
13+
x = np.arange(0, 10, 1.0 / float(sample_rate))
14+
y = np.sin(x * 10.0)
15+
y += np.sin(x * 1.0) * 0.7
16+
y += np.sin(x * 100.0) * 0.1
17+
return (x, y)
18+
19+
def test_get_peaks(self):
20+
""" Check that get_peaks finds approximately the right locations and
21+
number of peaks in a sine wave """
22+
x = np.arange(0, math.pi * 8, 0.01)
23+
y = np.sin(x)
24+
test_peaks = x[list(signals.getpeaks(y, 151))] / (math.pi / 2.0)
25+
real_peaks = [1, 5, 9, 13]
26+
np.testing.assert_allclose(test_peaks, real_peaks, rtol=1e-3)
27+
28+
def test_make_filter(self):
29+
""" Check that the bandpass filter function filters the correct signals.
30+
N.B graph must be checked by eye! """
31+
filt = signals.make_filter(
32+
order=5, low_freq=0.75, high_freq=5, sample_freq=250.0)
33+
x, y = self.make_test_signal(250.0)
34+
35+
yf = filt(y)
36+
f = plt.figure()
37+
plt.plot(x, y)
38+
plt.plot(x, yf)
39+
plt.xlabel('Time (250 Hz sample rate)')
40+
plt.ylabel('Signal')
41+
plt.legend(['Original', 'Filtered'])
42+
f.savefig('test_make_filter.png', bbox_inches='tight')
43+
44+
def test_window(self):
45+
""" Test the moving window function """
46+
windows = list(signals.window(xrange(10), 4, 2))
47+
expected = [(0, 1, 2, 3), (2, 3, 4, 5), (4, 5, 6, 7), (6, 7, 8, 9)]
48+
self.assertEquals(windows, expected)
49+
50+
def test_interpolate_points(self):
51+
""" Test point interpolation
52+
N.B. Graph must be checked by eye! """
53+
x, y = self.make_test_signal(30.0)
54+
y = np.vstack((y, -y))
55+
y0 = signals.interpolate_points(y.T, 30.0 / 250.0, axis=0)
56+
y1 = signals.interpolate_points(y, 30.0 / 250.0, axis=1)
57+
self.assertTrue(y0.shape[0] >= 2490)
58+
self.assertTrue(y1.shape[1] >= 2490)
59+
f = plt.figure()
60+
plt.subplot(2, 1, 1)
61+
plt.plot(y.T)
62+
plt.subplot(2, 1, 2)
63+
plt.plot(y0)
64+
f.savefig('test_interpolate_points.png', bbox_inches='tight')
65+
66+
def test_find_periodicities(self):
67+
""" Test that find_periodicities picks the correct signal as the
68+
most periodic one, and returns a sensible frequency """
69+
sample_freq = 30.0
70+
x, y_periodic = self.make_test_signal(sample_freq)
71+
y_flat = x / 8.0
72+
y = np.array([y_periodic, y_flat])
73+
frequencies, periodicies = signals.find_periodicities(
74+
y.T, sample_freq=sample_freq)
75+
self.assertGreater(periodicies[0], periodicies[1])
76+
self.assertAlmostEqual(frequencies[0], 10 / (2 * math.pi), places=1)
77+
78+
79+
if __name__ == '__main__':
80+
unittest.main()

0 commit comments

Comments
 (0)