Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*~
*~
__pycache__
.pytest_cache
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.formatting.provider": "black"
}
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

info:
python scpinfo.py example/example.scp
32 changes: 27 additions & 5 deletions scp.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,12 @@ class Section2(Section):
def __init__(self, header, pointer):
super().__init__(header)
self.p = pointer
# Number of Huffman Tables defined (if 19 999 then the default table, defined in C.3.7.6, is used).
self.nr_huffman_tables = 0
# Number of code structures in table # 1
self.nr_code_struct = 0


class Section3(Section):
"""Section 3 - ECG Leads definition"""
def __init__(self, header, pointer):
Expand All @@ -152,14 +156,19 @@ def __init__(self, header, pointer):
self.nr_leads_sim = 0
self.leads = []


# QRS Locations if reference beats are encoded
class Section4(Section):
"""Section 4 - Reserved for legacy SCP-ECG versions"""
"""Section 4 - Reserved for legacy SCP-ECG versions (SCP Versions 1.x, 2.x)"""
def __init__(self, header, pointer):
super().__init__(header)
self.p = pointer


# Length of reference beat type 0 in milliseconds
self.ref_beat_type_len = 0
# Sample number of the fiducial point (QRS trigger point), with respect to beginning of reference beat type 0
self.sample_nr_fidpoint = 0
# Total number of QRS complexes within the entire short-term ECG rhythm record
self.total_nr_qrs = 0

class Section5(Section):
"""Section 5 with samples"""
def __init__(self, header, pointer):
Expand Down Expand Up @@ -206,6 +215,19 @@ def __init__(self, header, pointer):
super().__init__(header)
self.p = pointer

self.reference_count = 0
# number of pacemaker spikes
self.pace_count = 0
# Average RR interval in milliseconds for all QRS's
self.rr_interval = 0
# Average PP interval in milliseconds for all QRS's
self.pp_interval = 0
self.pace_times = []
self.pace_amplitudes = []
self.pace_types = []
self.pace_sources = []
self.pace_indexes = []
self.pace_widths = []

class Section8(Section):
"""Section 8 - Textual diagnosis"""
Expand Down
21 changes: 14 additions & 7 deletions scpformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
SCP section and attribute formatter
"""

from scputil import b2i, bdecode, lead_dic
from scputil import b2i, b2s, bdecode, lead_dic


class Section1TagsFormatter:
Expand Down Expand Up @@ -129,7 +129,7 @@ def __init__(self, bytes):
self._print = False
if bytes:
# in minutes
self.offset = b2i(bytes[0:2])
self.offset = b2s(bytes[0:2])
self.index = b2i(bytes[2:4])
self.desc = bdecode(bytes[4:])
self._print = True
Expand Down Expand Up @@ -402,6 +402,7 @@ def format_section2(s2, printer):
print()
printer.p('--Section2--', '----')
format_header(s2.h, printer)
printer.p('NrHuffmanTables', '19999 (default table)' if s2.nr_huffman_tables == 19999 else s2.nr_huffman_tables)


def format_section3(s3, printer):
Expand All @@ -416,7 +417,6 @@ def format_section3(s3, printer):
printer.p('LeadCount', len(s3.leads))
LeadIdFormatter(s3.leads).format(printer)


def format_section5(s5, printer):
if not s5.p.section_has_data():
return
Expand Down Expand Up @@ -495,15 +495,22 @@ def format_section7(s7, printer):
print()
printer.p('--Section7--', '----')
format_header(s7.h, printer)
printer.p('ReferenceCount', s7.reference_count)
printer.p('PaceCount', s7.pace_count)
printer.p('Avg RR Interval (ms)', "29999 (not calculated)" if 29999 == s7.rr_interval else s7.rr_interval )
printer.p('Avg PP Interval (ms)', "29999 (not calculated)" if 29999 == s7.pp_interval else s7.pp_interval)


def format_section4(s4, printer):
if not s4.p.section_has_data():
def format_section4(s, printer):
if not s.p.section_has_data():
return

print()
printer.p('--Section4--', '----')
format_header(s4.h, printer)
format_header(s.h, printer)
printer.p('RefBeatType0Len', s.ref_beat_type_len)
printer.p('SampleNr FiducialPoint', s.sample_nr_fidpoint)
printer.p('TotalNrQRS', s.total_nr_qrs)


def format_section8(s8, printer):
Expand Down
105 changes: 79 additions & 26 deletions scpreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ def readint(self, n):
return int(value)
return int.from_bytes(bytes, 'little')

def read_byte(self):
"""Read 1 byte (little endian)"""
bytes = self.file.read(1)
if len(bytes) == 0:
print('ERR: Could not read_byte, corrupt structure. File position ' + self.pos())
return struct.unpack("<B", bytes)[0]

def read_ushort(self):
"""Read 2 bytes as unsigned short (little endian)"""
bytes = self.file.read(2)
if len(bytes) == 0:
print('ERR: Could not read_ushort, corrupt structure. File position ' + self.pos())
return struct.unpack("<H", bytes)[0]

def read_uint(self):
"""Read 4 bytes as unsigned integer (little endian)"""
bytes = self.file.read(4)
if len(bytes) == 0:
print('ERR: Could not read_uint, corrupt structure. File position ' + self.pos())
return struct.unpack("<I", bytes)[0]

def read_ulong(self):
"""Read 6 bytes as unsigned long (little endian)"""
bytes = self.file.read(6)
if len(bytes) == 0:
print('ERR: Could not read_ulong, corrupt structure. File position ' + self.pos())
return struct.unpack("<Q", bytes)[0]

def move(self, n):
"""move n bytes from beginning of file"""
return self.file.seek(n, 0)
Expand Down Expand Up @@ -68,8 +96,8 @@ def close(self):
def read_scp(self):
"""Read an scp file into memory and returns a ScpRecord"""
scpRecord = ScpRecord()
scpRecord.crc = self.reader.readint(2)
scpRecord.len = self.reader.readint(4)
scpRecord.crc = self.reader.read_ushort()
scpRecord.len = self.reader.read_uint()

s0 = self._section0()
scpRecord.sections.append(s0)
Expand All @@ -93,9 +121,9 @@ def read_scp(self):
def _sectionheader(self):
"""Read and return a section header"""
header = SectionHeader()
header.crc = self.reader.readint(2)
header.id = self.reader.readint(2)
header.len = self.reader.readint(4)
header.crc = self.reader.read_ushort()
header.id = self.reader.read_ushort()
header.len = self.reader.read_uint()
header.versnr = self.reader.readint(1)
header.protnr = self.reader.readint(1)
header.reserved = self.reader.reads(6)
Expand All @@ -107,11 +135,11 @@ def _sectionheader(self):
def _sectionpointer(self):
"""Read and return a section pointer"""
p = SectionPointer()
p.id = self.reader.readint(2)
p.id = self.reader.read_ushort()
# section length
p.len = self.reader.readint(4)
p.len = self.reader.read_uint()
# index of section starting from zero
p.index = self.reader.readint(4)
p.index = self.reader.read_uint()
return p

def _section0(self):
Expand Down Expand Up @@ -139,8 +167,8 @@ def _section0(self):
def _readtag(self):
"""Read and return a scp tag"""
tag = Tag()
tag.tag = self.reader.readint(1)
tag.len = self.reader.readint(2)
tag.tag = self.reader.read_byte()
tag.len = self.reader.read_ushort()

if tag.len > 0:
tag.data = self.reader.read(tag.len)
Expand All @@ -149,9 +177,9 @@ def _readtag(self):
def _readleadid(self):
"""Read and return a LeadId"""
leadid = LeadIdentification()
leadid.startsample = self.reader.readint(4)
leadid.endsample = self.reader.readint(4)
leadid.leadid = self.reader.readint(1)
leadid.startsample = self.reader.read_uint()
leadid.endsample = self.reader.read_uint()
leadid.leadid = self.reader.read_byte()
return leadid

def _read_section(self, pointer, nr_of_leads):
Expand Down Expand Up @@ -206,6 +234,10 @@ def _section2(self, pointer):

header = self._sectionheader()
s = Section2(header, pointer)

s.nr_huffman_tables = self.reader.read_ushort()
# Number of code structures in table # 1
s.nr_code_struct = self.reader.read_ushort()
return s

def _section3(self, pointer):
Expand All @@ -215,8 +247,8 @@ def _section3(self, pointer):
header = self._sectionheader()
s = Section3(header, pointer)

s.nrleads = self.reader.readint(1)
s.flags = self.reader.readint(1)
s.nrleads = self.reader.read_byte()
s.flags = self.reader.read_byte()
# first bit
s.ref_beat_substr = bool(s.flags >> 1 & 1)
# bits 3-7
Expand All @@ -235,6 +267,10 @@ def _section4(self, pointer):
header = self._sectionheader()
s = Section4(header, pointer)

s.ref_beat_type_len = self.reader.read_ushort()
s.sample_nr_fidpoint = self.reader.read_ushort()
s.total_nr_qrs = self.reader.read_ushort()

return s

def _section5(self, pointer, nr_of_leads):
Expand All @@ -244,14 +280,14 @@ def _section5(self, pointer, nr_of_leads):
header = self._sectionheader()
s = Section5(header, pointer)

s.avm = self.reader.readint(2)
s.sample_time_interval = self.reader.readint(2)
s.sample_encoding = self.reader.readint(1)
s.reserved = self.reader.readint(1)
s.avm = self.reader.read_ushort()
s.sample_time_interval = self.reader.read_ushort()
s.sample_encoding = self.reader.read_byte()
s.reserved = self.reader.read_byte()

# nr of bytes for each lead
for _ in range(0, nr_of_leads):
s.nr_bytes_for_leads.append(self.reader.readint(2))
s.nr_bytes_for_leads.append(self.reader.read_ushort())

# samples for each lead
for nr in s.nr_bytes_for_leads:
Expand All @@ -261,7 +297,7 @@ def _section5(self, pointer, nr_of_leads):
samples_len = nr / 2

while samples_len > 0:
data.samples.append(self.reader.readint(2))
data.samples.append(self.reader.read_ushort())
samples_len = samples_len - 1

s.data.append(data)
Expand All @@ -275,16 +311,18 @@ def _section6(self, pointer, nr_of_leads):
header = self._sectionheader()
s = Section6(header, pointer)

s.avm = self.reader.readint(2)
s.sample_time_interval = self.reader.readint(2)
s.sample_encoding = self.reader.readint(1)
s.bimodal_compression = self.reader.readint(1)
s.avm = self.reader.read_ushort()
s.sample_time_interval = self.reader.read_ushort()
s.sample_encoding = self.reader.read_byte()
s.bimodal_compression = self.reader.read_byte()


# nr of bytes for each lead
for _ in range(0, nr_of_leads):
s.nr_bytes_for_leads.append(self.reader.readint(2))
s.nr_bytes_for_leads.append(self.reader.read_ushort())

# TODO: check if section2 exists -> samples are Huffman encoded

# samples for each lead
for nr in s.nr_bytes_for_leads:
data = DataSamples()
Expand All @@ -293,6 +331,7 @@ def _section6(self, pointer, nr_of_leads):
samples_len = nr / 2

while samples_len > 0:
# signed 2byte integers
data.samples.append(self.reader.readint(2))
samples_len = samples_len - 1

Expand All @@ -306,6 +345,20 @@ def _section7(self, pointer):
header = self._sectionheader()
s = Section7(header, pointer)

s.reference_count = self.reader.read_byte()
s.pace_count = self.reader.read_byte()
s.rr_interval = self.reader.read_ushort()
s.pp_interval = self.reader.read_ushort()
for i in range(0, s.pace_count):
s.pace_times[i] = self.reader.readint(2)
s.pace_amplitudes[i] = self.reader.read_ushort()

for i in range(0, s.pace_count):
s.pace_types = self.reader.read_byte()
s.pace_sources = self.reader.read_byte()
s.pace_indexes = self.reader.readint(2)
s.pace_widths = self.reader.readint(2)

return s


Expand Down
15 changes: 14 additions & 1 deletion scputil.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import struct

def b2s(bytes):
if len(bytes) != 2:
raise ValueError("bytes must be exactly 2 bytes long")
return struct.unpack('h', bytes)

def b2b(bytes):
if len(bytes) != 1:
raise ValueError("bytes must be exactly 1 byte long")
return struct.unpack('B', bytes)

def b2i(bytes):
"""Convert bytes to int (little endian)"""
"""Convert bytes to unsigned integer (little endian)"""
return int.from_bytes(bytes, 'little')

def b2si(bytes):
"""Convert bytes to signed integer (little endian)"""
return int.from_bytes(bytes, 'little', signed=True)

def bdecode(bytes):
"""Decode bytes as iso-8859-1 and remove null terminators"""
Expand Down
Loading