Skip to content

Commit 2ffc767

Browse files
committed
add tff reader
1 parent 93c9c66 commit 2ffc767

File tree

2 files changed

+223
-0
lines changed

2 files changed

+223
-0
lines changed

wfdb/io/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .annotation import (Annotation, rdann, wrann, show_ann_labels,
55
show_ann_classes)
66
from .download import get_dbs, get_record_list, dl_files
7+
from .tff import rdtff

wfdb/io/tff.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""
2+
Module for reading ME6000 .tff format files.
3+
4+
http://www.biomation.com/kin/me6000.htm
5+
6+
"""
7+
import datetime
8+
import os
9+
import struct
10+
11+
import numpy as np
12+
13+
14+
def rdtff(file_name, cut_end=False):
15+
"""
16+
Read values from a tff file
17+
18+
Parameters
19+
----------
20+
file_name : str
21+
Name of the .tff file to read
22+
cut_end : bool, optional
23+
If True, cuts out the last sample for all channels. This is for
24+
reading files which appear to terminate with the incorrect
25+
number of samples (ie. sample not present for all channels).
26+
27+
Returns
28+
-------
29+
signal : numpy array
30+
A 2d numpy array storing the physical signals from the record.
31+
fields : dict
32+
A dictionary containing several key attributes of the read record.
33+
markers : numpy array
34+
A 1d numpy array storing the marker locations.
35+
triggers : numpy array
36+
A 1d numpy array storing the trigger locations.
37+
38+
Notes
39+
-----
40+
This function is slow because tff files may contain any number of
41+
escape sequences interspersed with the signals. There is no way to
42+
know the number of samples/escape sequences beforehand, so the file
43+
is inefficiently parsed a small chunk at a time.
44+
45+
It is recommended that you convert your tff files to wfdb format.
46+
47+
"""
48+
file_size = os.path.getsize(file_name)
49+
with open(file_name, 'rb') as fp:
50+
fields, file_fields = _rdheader(fp)
51+
signal, markers, triggers = _rdsignal(fp, file_size=file_size,
52+
header_size=file_fields['header_size'],
53+
n_sig=file_fields['n_sig'],
54+
bit_width=file_fields['bit_width'],
55+
is_signed=file_fields['is_signed'],
56+
cut_end=cut_end)
57+
return signal, fields, markers, triggers
58+
59+
60+
def _rdheader(fp):
61+
"""
62+
Read header info of the windaq file
63+
"""
64+
tag = None
65+
# The '2' tag indicates the end of tags.
66+
while tag != 2:
67+
# For each header element, there is a tag indicating data type,
68+
# followed by the data size, followed by the data itself. 0's
69+
# pad the content to the nearest 4 bytes. If data_len=0, no pad.
70+
tag = struct.unpack('>H', fp.read(2))[0]
71+
data_size = struct.unpack('>H', fp.read(2))[0]
72+
pad_len = (4 - (data_size % 4)) % 4
73+
pos = fp.tell()
74+
# Currently, most tags will be ignored...
75+
# storage method
76+
if tag == 1001:
77+
storage_method = fs = struct.unpack('B', fp.read(1))[0]
78+
storage_method = {0:'recording', 1:'manual', 2:'online'}[storage_method]
79+
# fs, unit16
80+
elif tag == 1003:
81+
fs = struct.unpack('>H', fp.read(2))[0]
82+
# sensor type
83+
elif tag == 1007:
84+
# Each byte contains information for one channel
85+
n_sig = data_size
86+
channel_data = struct.unpack('>%dB' % data_size, fp.read(data_size))
87+
# The documentation states: "0 : Channel is not used"
88+
# This means the samples are NOT saved.
89+
channel_map = ((1, 1, 'emg'),
90+
(15, 30, 'goniometer'), (31, 46, 'accelerometer'),
91+
(47, 62, 'inclinometer'),
92+
(63, 78, 'polar_interface'), (79, 94, 'ecg'),
93+
(95, 110, 'torque'), (111, 126, 'gyrometer'),
94+
(127, 142, 'sensor'))
95+
sig_name = []
96+
# The number range that the data lies between gives the
97+
# channel
98+
for data in channel_data:
99+
# Default case if byte value falls outside of channel map
100+
base_name = 'unknown'
101+
# Unused channel
102+
if data == 0:
103+
n_sig -= 1
104+
break
105+
for item in channel_map:
106+
if item[0] <= data <= item[1]:
107+
base_name = item[2]
108+
break
109+
existing_count = [base_name in name for name in sig_name].count(True)
110+
sig_name.append('%s_%d' % (base_name, existing_count))
111+
# Display scale. Probably not useful.
112+
elif tag == 1009:
113+
# 100, 500, 1000, 2500, or 8500uV
114+
display_scale = struct.unpack('>I', fp.read(4))[0]
115+
# sample format, uint8
116+
elif tag == 3:
117+
sample_fmt = struct.unpack('B', fp.read(1))[0]
118+
is_signed = bool(sample_fmt >> 7)
119+
# ie. 8 or 16 bits
120+
bit_width = sample_fmt & 127
121+
# Measurement start time - seconds from 1.1.1970 UTC
122+
elif tag == 101:
123+
n_seconds = struct.unpack('>I', fp.read(4))[0]
124+
base_datetime = datetime.datetime.utcfromtimestamp(n_seconds)
125+
base_date = base_datetime.date()
126+
base_time = base_datetime.time()
127+
# Measurement start time - minutes from UTC
128+
elif tag == 102:
129+
n_minutes = struct.unpack('>h', fp.read(2))[0]
130+
# Go to the next tag
131+
fp.seek(pos + data_size + pad_len)
132+
header_size = fp.tell()
133+
# For interpreting the waveforms
134+
fields = {'fs':fs, 'n_sig':n_sig, 'sig_name':sig_name,
135+
'base_time':base_time, 'base_date':base_date}
136+
# For reading the signal samples
137+
file_fields = {'header_size':header_size, 'n_sig':n_sig,
138+
'bit_width':bit_width, 'is_signed':is_signed}
139+
return fields, file_fields
140+
141+
142+
def _rdsignal(fp, file_size, header_size, n_sig, bit_width, is_signed, cut_end):
143+
"""
144+
Read the signal
145+
146+
Parameters
147+
----------
148+
cut_end : bool, optional
149+
If True, enables reading the end of files which appear to terminate
150+
with the incorrect number of samples (ie. sample not present for all channels),
151+
by checking and skipping the reading the end of such files.
152+
Checking this option makes reading slower.
153+
"""
154+
# Cannot initially figure out signal length because there
155+
# are escape sequences.
156+
fp.seek(header_size)
157+
signal_size = file_size - header_size
158+
byte_width = int(bit_width / 8)
159+
# numpy dtype
160+
dtype = str(byte_width)
161+
if is_signed:
162+
dtype = 'i' + dtype
163+
else:
164+
dtype = 'u' + dtype
165+
# big endian
166+
dtype = '>' + dtype
167+
# The maximum possible samples given the file size
168+
# All channels must be present
169+
max_samples = int(signal_size / byte_width)
170+
max_samples = max_samples - max_samples % n_sig
171+
# Output information
172+
signal = np.empty(max_samples, dtype=dtype)
173+
markers = []
174+
triggers = []
175+
# Number of (total) samples read
176+
sample_num = 0
177+
178+
# Read one sample for all channels at a time
179+
if cut_end:
180+
stop_byte = file_size - n_sig * byte_width + 1
181+
while fp.tell() < stop_byte:
182+
chunk = fp.read(2)
183+
sample_num = _get_sample(fp, chunk, n_sig, dtype, signal, markers, triggers, sample_num)
184+
else:
185+
while True:
186+
chunk = fp.read(2)
187+
if not chunk:
188+
break
189+
sample_num = _get_sample(fp, chunk, n_sig, dtype, signal, markers, triggers, sample_num)
190+
191+
# No more bytes to read. Reshape output arguments.
192+
signal = signal[:sample_num]
193+
signal = signal.reshape((-1, n_sig))
194+
markers = np.array(markers, dtype='int')
195+
triggers = np.array(triggers, dtype='int')
196+
return signal, markers, triggers
197+
198+
199+
def _get_sample(fp, chunk, n_sig, dtype, signal, markers, triggers, sample_num):
200+
tag = struct.unpack('>h', chunk)[0]
201+
# Escape sequence
202+
if tag == -32768:
203+
# Escape sequence structure: int16 marker, uint8 type,
204+
# uint8 length, uint8 * length data, padding % 2
205+
escape_type = struct.unpack('B', fp.read(1))[0]
206+
data_len = struct.unpack('B', fp.read(1))[0]
207+
# Marker*
208+
if escape_type == 1:
209+
# *In manual mode, this could be block start/top time.
210+
# But we are it is just a single time marker.
211+
markers.append(sample_num / n_sig)
212+
# Trigger
213+
elif escape_type == 2:
214+
triggers.append(sample_num / n_sig)
215+
fp.seek(data_len + data_len % 2, 1)
216+
# Regular samples
217+
else:
218+
fp.seek(-2, 1)
219+
signal[sample_num:sample_num + n_sig] = np.fromfile(
220+
fp, dtype=dtype, count=n_sig)
221+
sample_num += n_sig
222+
return sample_num

0 commit comments

Comments
 (0)