QT Interval Measurement: The PhysioNet/Computing in Cardiology Challenge 2006 1.0.0
(21,199 bytes)
#! /usr/bin/perl
#
# Program to get QT values for the Computers in Cardiology Challenge 2006.
#
# Copyright 2006 by Raphael Schneider (rasch@librasch.org)
#
# All rights reserved. This program is free software; you can redistribute
# it and/or modify it under the same terms as Perl itself.
#
# This Perl script needs libRASCH version 0.8.0pre2 or greater from www.librasch.org
# and the Perl API bindings have to be installed also (see INSTALL file coming
# with the libRASCH distribution).
#
# Usage:
#
# perl get_qt_cinc_2006.pl --template=templ-file --file=out-file
# [--detect] [--ch=X] [--check-morphology]
# [--skip-powerline] <ecg-dir(s)>
#
# with --template=templ-file selecting the template of the entry-form
# --file=out-file selecting the name of the final entry-form
# --detect if given, a beat detection is performed, if not,
# the beat information (position and annotation) which
# is available is used
# --ch=X do the QT measurement using channel 'X'
# (channel-numbers start with 0 -> lead I is 0, lead II is 1, ...)
# --check-morphology if given, the morphologies of the QRS complexes and T-waves
# are forced to be the same for all beats (e.g. when the Q-wave
# on some beats is too small to be detected but seen on the
# majority of the beats, the threshold used to detect Q-waves
# is reduced so if small Q-waves will be detected)
# --skip-powerline if given, the powerline-noise filter is NOT applied to
# the raw ecg data
# <ecg-dir(s)> the directory(s) with the ECGs to process
#
# Example:
#
# perl get_qt_cinc_2006.pl --detect --ch=1
# --template=template_div3.txt --file=results_div3.txt
# ./patient*
#
#
# Version 0.0: rasch@librasch.org (2006-04-19)
# first verision
# Version 1.0: rasch@librasch.org (2006-09-11)
# final changes before sending it to PhysioNet
#
use strict;
use warnings;
$|++;
use RASCH;
use Getopt::Long;
my $debug = 1;
########## get and check options ##########
my $do_detect;
my $ch = 1; # ch-1 is lead II
my $check_morphology;
my $method = 0;
my $outfile; # default is print to stdout
my $template_file = './template.txt';
my $skip_powerline_filter;
GetOptions("detect" => \$do_detect,
"ch=s" => \$ch,
"check-morphology" => \$check_morphology,
"method=s" => \$method,
"file=s" => \$outfile,
"template=s" => \$template_file,
"skip-powerline" => \$skip_powerline_filter);
# if no arguments left -> no files/dirs given to process
exit 0 if ($#ARGV < 0);
# check if file-/dir-names are given on stdin
my @files_use;
if ($ARGV[0] eq '-')
{
while (<>)
{
my $fn = $_;
chomp($fn);
push(@files_use, $fn);
}
}
else
{
@files_use = @ARGV;
}
########## start processing the files/dirs ##########
my $ra = new RASCH or die "can't open libRASCH\n";
my $meas_cnt = 0;
my %results = ();
for my $curr (@files_use)
{
my $meas = $ra->open_meas($curr);
if (defined($meas))
{
$meas_cnt++;
print STDERR "process measurement $curr (#$meas_cnt)...";
process_meas($ra, $meas, $do_detect, $ch, $check_morphology, $method, $skip_powerline_filter, \%results);
print STDERR "\n";
}
else
{
# open was not sucessful, maybe it is a directory
my @sigs = @{$ra->find_meas($curr)};
if ($#sigs < 0)
{
print STDERR "can't handle file/dir $curr\n";
next;
}
for my $curr2 (@sigs)
{
my $fn = $curr2->filename();
$meas_cnt++;
print STDERR "process measurement $fn in directory $curr (#$meas_cnt)...";
$meas = $ra->open_meas($fn);
if (not defined($meas))
{
print STDERR "\nError opening $fn. This should not happen\n";
next;
}
process_meas($ra, $meas, $do_detect, $ch, $check_morphology, $method, $skip_powerline_filter, \%results);
print STDERR "\n";
}
}
}
########## write results ##########
write_results($template_file, $outfile, \%results);
exit 0;
######################### sub-routines #########################
sub process_meas
{
my ($ra, $meas, $do_detect, $ch, $check_morph, $method, $skip_powerline_filter, $results) = @_;
my $clh = undef;
if ($do_detect)
{
$clh = do_detect($ra, $meas, $ch, $check_morph, $skip_powerline_filter);
return undef if (not defined($clh));
}
else
{
my $eva = $meas->get_default_eval();
if (not defined($eva))
{
print STDERR "nok";
return undef;
}
my $all_clh = $eva->get_class('heartbeat');
$clh = $all_clh->[0];
if ((not defined($clh)) || ($clh == 0))
{
print STDERR "nok";
return undef;
}
}
my $rec = $meas->get_first_rec();
my ($value, $name, $desc) = $rec->get_info(info => 'rec_name');
my $rec_name = $value->value();
($value, $name, $desc) = $rec->get_info(info => 'ch_samplerate', ch => $ch);
my $samplerate = $value->value();
my $res;
if ($method == 0)
{
$res = qt_method0($clh, $ch);
}
elsif ($method == 1)
{
$res = qt_method1($clh, $ch);
}
elsif ($method == 2)
{
$res = qt_method2($clh, $ch);
}
else
{
print STDERR "Unknown method to get QT times. Please select a known one (0-2).\n";
exit -1;
}
$res->[2] = $samplerate;
$results->{$rec_name} = $res;
return $res;
} # sub process_meas()
#################### beat detection code ####################
sub do_detect
{
my ($raq, $meas, $ch, $check_morph, $skip_powerline_filter) = @_;
my $eva = $meas->add_eval("get_qt.pl", "evaluation for CinC Challenge 2006");
if (not defined($eva))
{
print STDERR "Error creating evaluation";
return 0;
}
do_beat_detect($ra, $meas, $eva, $ch, $skip_powerline_filter);
my $all_clh = $eva->get_class('heartbeat');
my $clh = $all_clh->[0];
if ((not defined($clh)) || ($clh == 0))
{
print STDERR "nok";
return undef;
}
# get most used morphologies for qrs and t
my ($qrs_morph_use, $t_morph_use) = get_max_morph($clh);
# now do beat-detection again forcing to use the
# maxed used morphologies
do_beat_detect($ra, $meas, $eva, $ch, $skip_powerline_filter, $clh, $qrs_morph_use, $t_morph_use);
$all_clh = $eva->get_class('heartbeat');
$clh = $all_clh->[0];
if ((not defined($clh)) || ($clh == 0))
{
print STDERR "nok";
return undef;
}
do_fiducial($ra, $meas, $clh);
do_template($ra, $meas, $clh);
if ($check_morph)
{
do_morphology_check($ra, $meas, $eva, $clh, $skip_powerline_filter);
}
do_ecg($ra, $meas, $clh);
$meas->save_eval();
return $clh;
} # sub do_detect()
sub do_beat_detect
{
my $ra = shift;
my $meas = shift;
my $eva = shift;
my $ch = shift;
my $skip_powerline_filter = shift;
my $clh = (defined(@_) ? shift : undef);
my $qrs_morph_force = (defined(@_) ? shift : undef);
my $t_morph_force = (defined(@_) ? shift : undef);
if (defined($clh))
{
$clh->delete();
}
my $pl = $ra->get_plugin_by_name("detect-ecg", 0);
if (not defined($pl))
{
print STDERR "Can not find 'detect-ecg' plugin";
return 0;
}
my $proc = $pl->get_process($meas->get_handle());
if (not defined($proc))
{
print STDERR "can't initialize processing";
return -1;
}
my $ret = $proc->set_option('save_in_eval', 'l', 1);
$ret = $proc->set_option('do_interpolation', 'l', 1);
$ret = $proc->set_option('eh', 'h', $eva->get_handle());
$ret = $proc->set_option('filter_powerline_noise', 'l', ($skip_powerline_filter ? 0 : 1));
$ret = $proc->set_option('force_qrs_type', 'l', $qrs_morph_force) if (defined($qrs_morph_force));
$ret = $proc->set_option('force_t_type', 'l', $t_morph_force) if (defined($t_morph_force));
my $rec = $meas->get_first_rec();
my $num_ch = $rec->get_info(info => 'rec_num_channel')->value();
my @chs = ();
if ($ch == -1)
{
for (my $i = 0; $i < $num_ch; $i++)
{
$chs[$i] = $i;
}
}
else
{
$chs[0] = $ch;
}
$ret = $proc->set_option('ch', 'pl', \@chs);
my $results = $proc->process();
return 0;
} # sub do_beat_detect()
sub get_max_morph
{
my ($clh) = @_;
my $events = $clh->get_events();
# get morphology types
my $prop_qrs_morph = $clh->get_prop('ecg-qrs-type');
if (not defined($prop_qrs_morph))
{
print STDERR "no 'ecg-qrs-type' event-property in the event-class";
return -1;
}
my $prop_t_morph = $clh->get_prop('ecg-t-type');
if (not defined($prop_t_morph))
{
print STDERR "no 'ecg-t-type' event-property in the event-class";
return -1;
}
my %qrs_morph = ();
my %t_morph = ();
my $cnt_used = 0;
for (@$events)
{
my $qrs_type = $prop_qrs_morph->get_value($_, $ch);
next if ($qrs_type <= 0);
my $t_type = $prop_t_morph->get_value($_, $ch);
next if ($t_type <= 0);
$qrs_morph{$qrs_type} = 0 if (not defined($qrs_morph{$qrs_type}));
$qrs_morph{$qrs_type}++;
$t_morph{$t_type} = 0 if (not defined($t_morph{$t_type}));
$t_morph{$t_type}++;
$cnt_used++;
}
my $qrs_morph_use;
my $num = 0;
for (keys(%qrs_morph))
{
if ($num < $qrs_morph{$_})
{
$qrs_morph_use = $_;
$num = $qrs_morph{$_};
}
}
my $t_morph_use;
$num = 0;
for (keys(%t_morph))
{
# if bi-phasic, use only when more than 75% of the beats where found
# having a bi-phasic T-wave
next if (($_ > 100) && (($t_morph{$_}/$cnt_used) < 0.75));
if ($num < $t_morph{$_})
{
$t_morph_use = $_;
$num = $t_morph{$_};
}
}
return ($qrs_morph_use, $t_morph_use);
} # sub get_max_morph()
sub do_fiducial
{
my ($ra, $meas, $clh) = @_;
my $pl = $ra->get_plugin_by_name("fiducial-point", 0);
if (not defined($pl))
{
print STDERR "Can not find 'fiducial-point' plugin";
return 0;
}
my $proc = $pl->get_process($meas->get_handle());
if (not defined($proc))
{
print STDERR "can't initialize processing";
return -1;
}
my $ret = $proc->set_option('use_class', 'l', 1);
$ret = $proc->set_option('clh', 'h', $clh->get_handle());
$ret = $proc->set_option('save_in_class', 'l', 1);
$ret = $proc->set_option('ch', 'l', 0);
my $results = $proc->process();
return 0;
} # sub do_fiducial()
sub do_template
{
my ($ra, $meas, $clh) = @_;
my $pl = $ra->get_plugin_by_name("template", 0);
if (not defined($pl))
{
print STDERR "Can not find 'template' plugin";
return 0;
}
my $proc = $pl->get_process($meas->get_handle());
if (not defined($proc))
{
print STDERR "can't initialize processing";
return -1;
}
my $ret = $proc->set_option('use_class', 'l', 1);
$ret = $proc->set_option('clh', 'h', $clh->get_handle());
$ret = $proc->set_option('save_in_class', 'l', 1);
$ret = $proc->set_option('pos_name', 'c', 'qrs-pos');
$ret = $proc->set_option('templ_name', 'c', 'qrs-template');
$ret = $proc->set_option('templ_corr', 'c', 'qrs-template-corr');
$ret = $proc->set_option('accept', 'd', 0.85);
$ret = $proc->set_option('slope_accept_low', 'd', 0.6);
$ret = $proc->set_option('slope_accept_high', 'd', 1.4);
$ret = $proc->set_option('win_before', 'd', 0.15);
$ret = $proc->set_option('win_after', 'd', 0.15);
my $rec = $meas->get_first_rec();
my $num_ch = $rec->get_info(info => 'rec_num_channel')->value();
my @chs = ();
for (my $i = 0; $i < $num_ch; $i++)
{
$chs[$i] = $i;
}
$ret = $proc->set_option('ch', 'pl', \@chs);
my $results = $proc->process();
return 0;
} # sub do_template()
sub do_morphology_check
{
my ($ra, $meas, $eva, $clh, $skip_powerline_filter) = @_;
my $pl = $ra->get_plugin_by_name("detect-ecg", 0);
if (not defined($pl))
{
print STDERR "Can not find 'detect-ecg' plugin";
return 0;
}
my $proc = $pl->get_process($meas->get_handle());
if (not defined($proc))
{
print STDERR "can't initialize processing";
return -1;
}
my $ret = $proc->set_option('save_in_eval', 'l', 1);
$ret = $proc->set_option('do_interpolation', 'l', 1);
$ret = $proc->set_option('eh', 'h', $eva->get_handle());
$ret = $proc->set_option('clh', 'h', $clh->get_handle());
$ret = $proc->set_option('filter_powerline_noise', 'l', ($skip_powerline_filter ? 0 : 1));
my $rec = $meas->get_first_rec();
my $num_ch = $rec->get_info(info => 'rec_num_channel')->value();
my @chs = ();
if ($ch == -1)
{
for (my $i = 0; $i < $num_ch; $i++)
{
$chs[$i] = $i;
}
}
else
{
$chs[0] = $ch;
}
$ret = $proc->set_option('ch', 'pl', \@chs);
# my $events = get_max_template_events($clh);
my $events = $clh->get_events();
$ret = $proc->set_option('events', 'pl', $events);
# get morphology types
my $prop_qrs_morph = $clh->get_prop('ecg-qrs-type');
if (not defined($prop_qrs_morph))
{
print STDERR "no 'ecg-qrs-type' event-property in the event-class";
return -1;
}
my $prop_t_morph = $clh->get_prop('ecg-t-type');
if (not defined($prop_t_morph))
{
print STDERR "no 'ecg-t-type' event-property in the event-class";
return -1;
}
my %qrs_morph = ();
my %t_morph = ();
for (@$events)
{
my $qrs_type = $prop_qrs_morph->get_value($_, $ch);
next if ($qrs_type <= 0);
my $t_type = $prop_t_morph->get_value($_, $ch);
next if ($t_type <= 0);
$qrs_morph{$qrs_type} = 0 if (not defined($qrs_morph{$qrs_type}));
$qrs_morph{$qrs_type}++;
$t_morph{$t_type} = 0 if (not defined($t_morph{$t_type}));
$t_morph{$t_type}++;
}
my $qrs_morph_use;
my $num = 0;
for (keys(%qrs_morph))
{
if ($num < $qrs_morph{$_})
{
$qrs_morph_use = $_;
$num = $qrs_morph{$_};
}
}
my $t_morph_use;
$num = 0;
for (keys(%t_morph))
{
if ($num < $t_morph{$_})
{
$t_morph_use = $_;
$num = $t_morph{$_};
}
}
print STDERR "qrs-force=$qrs_morph_use t-force=$t_morph_use " if ($debug);
$ret = $proc->set_option('force_qrs_type', 'l', $qrs_morph_use);
$ret = $proc->set_option('force_t_type', 'l', $t_morph_use);
my $results = $proc->process();
} # sub do_morphology_check()
sub do_ecg
{
my ($ra, $meas, $clh) = @_;
my $pl = $ra->get_plugin_by_name("ecg", 0);
if (not defined($pl))
{
print STDERR "Can not find 'ecg' plugin";
return 0;
}
my $proc = $pl->get_process($meas->get_handle());
if (not defined($proc))
{
print STDERR "can't initialize processing";
return -1;
}
my $ret = $proc->set_option('use_class', 'l', 1);
$ret = $proc->set_option('clh', 'h', $clh->get_handle());
$ret = $proc->set_option('save_in_class', 'l', 1);
my $results = $proc->process();
return 0;
} # sub do_ecg()
#################### QT time assessment ####################
#
# (1) select template containing the most beats
# (2) use beat with lowest noise
#
sub qt_method0
{
my ($clh, $ch) = @_;
my $prop_noise = $clh->get_prop('ecg-noise');
if (not defined($prop_noise))
{
print STDERR "no 'ecg-noise' event-property in the event-class";
return undef;
}
my $prop_qt = $clh->get_prop('ecg-qt');
if (not defined($prop_qt))
{
print STDERR "no 'ecg-qt' event-property in the event-class";
return undef;
}
my $events = get_max_template_events($clh);
# get event-id with lowest noise
my $ev_id_min_noise;
my $min_noise = 10000000;
for (@{$events})
{
# skip first beat because of possible measurement problems for it
next if ($_ == 1);
my $value = $prop_qt->get_value($_, $ch);
next if ($value == -1);
$value = $prop_noise->get_value($_, $ch);
next if ($value == -1);
if ($value < $min_noise)
{
$ev_id_min_noise = $_;
$min_noise = $value;
}
}
return undef if (not defined($ev_id_min_noise));
print STDERR "min-noise=$min_noise ev-id=$ev_id_min_noise " if ($debug);
# get QT related values for export and return it
return get_qt_values($clh, $ev_id_min_noise, $ch);
} # sub qt_method0()
#
# (1) select template containing the most beats
# (2) use beat with median QT value in the template-class
#
sub qt_method1
{
my ($clh, $ch) = @_;
my $prop_qt = $clh->get_prop('ecg-qt');
if (not defined($prop_qt))
{
print STDERR "no 'ecg-qt' event-property in the event-class";
return undef;
}
my $events = get_max_template_events($clh);
# get event-id with median QT value
my @data = ();
for (@{$events})
{
my $value = $prop_qt->get_value($_, $ch);
next if ($value == -1);
my @curr = ($value, $_);
push(@data, \@curr);
}
my @data_sort = sort { $a->[0] <=> $b->[0] } @data;
my $len = @data_sort;
my $ev_use = int($len/2);
my $ev_id = $data_sort[$ev_use]->[1];
# skip first beat because of possible measurement problems for it
if ($ev_id == 1)
{
$ev_use++;
$ev_id = $data_sort[$ev_use]->[1];
}
print STDERR "median-qt=$data_sort[$ev_use]->[0] ev-id=$ev_id " if ($debug);
# get QT related values for export and return it
return get_qt_values($clh, $ev_id, $ch);
} # sub qt_method1()
#
# (1) use beat with median QT value over all beats
#
sub qt_method2
{
my ($clh, $ch) = @_;
my $prop_qt = $clh->get_prop('ecg-qt');
if (not defined($prop_qt))
{
print STDERR "no 'ecg-qt' event-property in the event-class";
return undef;
}
my $events = $clh->get_events();
# get event-id with median QT value
my @data = ();
for (@{$events})
{
my $value = $prop_qt->get_value($_, $ch);
next if ($value == -1);
my @curr = ($value, $_);
push(@data, \@curr);
}
my @data_sort = sort { $a->[0] <=> $b->[0] } @data;
my $len = @data_sort;
my $ev_use = int($len/2);
my $ev_id = $data_sort[$len]->[1];
# skip first beat because of possible measurement problems for it
if ($ev_id == 1)
{
$ev_use++;
$ev_id = $data_sort[$ev_use]->[1];
}
print STDERR "median-qt=$data_sort[$ev_use]->[0] ev-id=$ev_id " if ($debug);
# get QT related values for export and return it
return get_qt_values($clh, $ev_id, $ch);
} # sub qt_method2()
########## QT time assessment helper functions ##########
sub get_max_template_events
{
my ($clh) = @_;
my $summaries = $clh->get_sum('template');
if (not defined($summaries))
{
print STDERR "no 'template' summaries in the event-class";
return undef;
}
my $templ = $summaries->[0];
my $parts = $templ->get_part_all();
# get template (and the event-id's) containing the most beats
my @events_all = ();
my $num_ev_max = 0;
my $ev_idx_max;
my $cnt = 0;
for (@{$parts})
{
my $ev = $templ->get_part_events($_);
push(@events_all, $ev);
if (@{$ev} > $num_ev_max)
{
$num_ev_max = @{$ev};
$ev_idx_max = $cnt;
}
$cnt++;
}
print STDERR "num-ev=$num_ev_max " if ($debug);
return $events_all[$ev_idx_max];
} # sub get_max_template_events()
sub get_qt_values
{
my ($clh, $ev_id, $ch) = @_;
my $prop_qrs_pos = $clh->get_prop('qrs-pos');
if (not defined($prop_qrs_pos))
{
print STDERR "no 'qrs-pos' event-property in the event-class";
return undef;
}
my $prop_q = $clh->get_prop('ecg-qrs-start');
if (not defined($prop_q))
{
print STDERR "no 'ecg-qrs-start' event-property in the event-class";
return undef;
}
my $prop_t = $clh->get_prop('ecg-t-end');
if (not defined($prop_t))
{
print STDERR "no 'ecg-t-end' event-property in the event-class";
return undef;
}
my @res = ();
my $qrs_pos = $prop_qrs_pos->get_value($ev_id, $ch);
$res[0] = $qrs_pos + $prop_q->get_value($ev_id, $ch);
$res[1] = $qrs_pos + $prop_t->get_value($ev_id, $ch);
return \@res;
} # sub get_qt_values()
#################### writing results ####################
sub write_results
{
my ($template_file, $outfile, $results) = @_;
my $f = *STDOUT;
if (defined($outfile))
{
open OUT_FILE, ">$outfile" or die "can't create file $outfile: $!\n";
$f = *OUT_FILE;
}
open TEMPL_FILE, "<$template_file" or die "can't open template-file $template_file: $!\n";
while (<TEMPL_FILE>)
{
my $curr_line = $_;
chomp($curr_line);
$curr_line =~ m|^patient[\w]+\/([\w]+)$|;
if (defined($1))
{
my $values = $results->{$1};
if ((not defined($values)) || ($values->[0] == 0))
{
print $f $curr_line . "\n";
}
else
{
my $scale = 1000 / $values->[2];
$values->[0] *= $scale;
$values->[1] *= $scale;
print $f $curr_line . "\t$values->[0]\t$values->[1]\n";
if ($debug)
{
my $qt = $values->[1] - $values->[0];
print $curr_line . ": $qt\n";
}
}
}
else
{
print $f $curr_line . "\n";
}
}
close(TEMPL_FILE);
close($f) if (defined($outfile));
} # sub write_results()