-
Notifications
You must be signed in to change notification settings - Fork 7
/
omconvert.py
203 lines (160 loc) · 9.46 KB
/
omconvert.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
"""
Python wrapper for executing the omconvert binary (part of the Open Movement Project).
This processes .CWA and .OMX files, and can perform some operations on triaxial .WAV files; including:
* Export at a fixed sample rate to .CSV or .WAV
* Signal Vector Magnitude/Euclidean Norm Minus One
* "PAEE" cut-points categorizing epochs into a physical activity level. A free choice of cut-points can be made, for example, levels from:
* Esliger, D. W., Rowlands, A. V., Hurst, T. L., Catt, M., Murray, P., & Eston, R. G. (2011). Validation of the GENEA Accelerometer.
* Dillon, C. B., Fitzgerald, A. P., Kearney, P. M., Perry, I. J., Rennie, K. L., Kozarski, R., & Phillips, C. M. (2016). Number of days required to estimate habitual activity using wrist-worn GENEActiv accelerometer: a cross-sectional study. PloS one, 11(5), e0109913.
* Powell, C., Carson, B. P., Dowd, K. P., & Donnelly, A. E. (2017). Simultaneous validation of five activity monitors for use in adult populations. Scandinavian journal of medicine & science in sports, 27(12), 1881-1892.
* Wear-time validation, an implementation of the algorithm described in:
* van Hees, V. T., Renström, F., Wright, A., Gradmark, A., Catt, M., Chen, K. Y., ... & Franks, P. W. (2011). Estimation of daily energy expenditure in pregnant and non-pregnant women using a wrist-worn tri-axial accelerometer. PloS one, 6(7), e22922.
* (Not recommended) Step counts, an implementation of the algorithm described in:
* Cho, Y., Cho, H., & Kyung, C. M. (2016). Design and implementation of practical step detection algorithm for wrist-worn devices. IEEE Sensors Journal, 16(21), 7720-7730.
* (Not recommended) Sleep, an implementation of the algorithm described in:
* Borazio, M., Berlin, E., Kücükyildiz, N., Scholl, P., & Van Laerhoven, K. (2014, September). Towards benchmarked sleep detection with wrist-worn sensing units. In 2014 IEEE International Conference on Healthcare Informatics (pp. 125-134). IEEE.
* AG-Counts, an implementation of the algorithm described in:
* Brønd, J. C., Andersen, L. B., & Arvidsson, D. (2017). Generating ActiGraph counts from raw acceleration recorded by an alternative monitor.
"""
import os
import subprocess
import tempfile
import uuid
import datetime
class OmConvert:
"""An implementation of the analysis functions using the external binary executable 'omconvert'."""
@staticmethod
def locate_executable(executable_name = None):
# Default
if executable_name == None:
executable_name = 'omconvert'
# Search OS 'PATH'
search_path = os.environ["PATH"].split(os.pathsep)
# Prefer file in current working directory
search_path.insert(0, os.getcwd())
# Prefer binary in 'bin' folder in parent folder of source file
search_path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../bin'))
# Prefer binary co-located with source file
search_path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Prefer a full path if specified
parts = os.path.split(executable_name)
executable_name = parts[1]
if parts[0] != '':
search_path.insert(0, parts[0])
# If on Windows, append .exe and search in a default installation location (as a last resort)
if (os.name == 'nt'):
if os.path.splitext(executable_name)[1] == '':
executable_name = executable_name + '.exe'
search_path.append(os.path.expandvars('%PROGRAMFILES(x86)%\Open Movement\OM GUI\Plugins\OmConvertPlugin'))
# Search in locations
for path in search_path:
filename = os.path.join(path, executable_name)
if os.path.isfile(filename):
if not os.access(filename, os.X_OK):
raise FileNotFoundError('Binary file exists but is not executable, consider: chmod +x ' + filename)
return filename
return None
def __init__(self):
pass
def execute(self, source_file, options):
"""
Options:
"resample": 0, # resample frequency (not specified uses the configured rate)
"interpolate_mode": 3, # 1=nearest, 2=linear, 3=cubic
"header_csv": 1, # 0=none, 1=header in first row
"time": 0, # 0=date/time, 1=UNIX epoch
"calibrate": 1, # 0=off, 1=auto
"csv_file": "file.csv", # output resampled data to CSV
"svm_file": "file.svm.csv", # SVM analysis output
"svm_epoch": 60, # SVM analysis epoch period (seconds)
"svm_filter": 1, # SVM analysis filter (0=off, 1=BP 0.5-20 Hz)
"svm_mode": 0, # SVM analysis mode (0=abs(SVM-1), 1=max(SVM-1, 0), 2=SVM-1)
"wtv_file": "file.svm.csv", # WTV analysis output
"wtv_epoch": 1, # WTV analysis epochs of 30-minute windows
"paee_file": "file.paee.csv", # PAEE analysis
"paee_model": "wrist", # PAEE analysis cut-point values (see 'Cut-point thresholds strings')
"paee_epoch": 1, # PAEE analysis count of 1-minute epochs
"paee_filter": 1, # PAEE analysis filter (0=off, 1=BP 0.5-20 Hz)
"counts_file": "file.svm.csv", # WTV analysis output
"counts_epoch": 1, # WTV analysis epochs of 30-minute windows
"""
executable = self.locate_executable(options.get('executable'))
if executable is None:
raise FileNotFoundError('omconvert executable not found: specify "executable" option or place binary in path or working directory. For build instructions, see: https://github.com/digitalinteraction/omconvert/')
result = {}
print('OMCONVERT: Using executable: ' + executable)
parameters = [
executable,
source_file,
]
# Key/value options
for key, value in options.items():
if key == 'executable':
continue
parameters.append('-' + key.replace('_', '-'))
if value is not None:
parameters.append(str(value))
# Use an info file to get detailed output of the process
info_file = os.path.join(tempfile.gettempdir(), str(uuid.uuid4()) + '.yml')
parameters.append('-info')
parameters.append(info_file)
try:
# Execute the external binary
completed_process = subprocess.run(parameters)
result['returncode'] = completed_process.returncode
# Non-zero return code on failure
if result['returncode'] != 0:
raise RuntimeError('Conversion failed: ' + str(result['returncode']))
# Zero return codes should have an info file
if not os.path.isfile(info_file):
raise RuntimeError('Expected info file not found')
# Read and parse the info file
with open(info_file, 'r') as info_file_handle:
info_file_content = info_file_handle.read().splitlines()
for line in info_file_content:
line = line.strip()
if line == '' or line.startswith('#'):
continue
parts = line.split(':', 1)
key = parts[0].strip()
value = None
if len(parts) > 1:
value = parts[1].strip()
if value.isdigit() or (len(value) > 2 and value[0] == '-' and value[1:].isdigit()):
value = int(value, 10)
if key == 'Processed':
# This is in local timezone
value = datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f')
if key == 'Start' or key == 'Stop' or key == 'Time' or key == 'ClearTime' or key == 'ChangeTime':
# Is in no defined timezone
value = datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f').replace(tzinfo=None) # datetime.timezone.utc
# Ignore keys that change on each run
if key == 'Processed' or key == 'Results-output':
continue
result[key] = value
finally:
try:
os.remove(info_file)
except OSError:
# It's OK if it can't be removed (e.g. if it doesn't exist)
pass
print('...done')
return result
# Cut-point thresholds strings:
#
# "'wrist': 386/80/60 542/80/60 1811/80/60"
# "Esliger(40-63)-wristR: 386/80/60 440/80/60 2099/80/60"
# "Esliger(40-63)-wristL: 217/80/60 645/80/60 1811/80/60"
# "Esliger(40-63)-waist: 77/80/60 220/80/60 2057/80/60"
# "Schaefer(6-11)-wristND: 0.190 0.314 0.998"
# "Phillips(8-14)-wristR: 6/80 22/80 56/80"
# "Phillips(8-14)-wristL: 7/80 20/80 60/80"
# "Phillips(8-14)-hip: 3/80 17/80 51/80"
# "Roscoe(4-5)-wristND: 5.3/87.5 8.6/87.5"
# "Roscoe(4-5)-wristD: 8.1/87.5 9.3/87.5"
# "Dillon-wristD: 191.8/100/15 281.6/100/15 595/100/15"
# "Dillon-wristND: 158.5/100/15 261.9/100/15 495/100/15"
# "Dillon-wristD (2016) same as Dillon-wristD (2015): 230/30/60 338/30/60 714/30/60"
# "Dillon-wristND (2016) same as Dillon-wristND (2015): 190/30/60 314/30/60 594/30/60"
# "Powell-wristD: 51/30/15 68/30/15 142/30/15"
# "Powell-wristND: 47/30/15 64/30/15 157/30/15"