File Coverage

blib/lib/App/AFNI/SiemensPhysio.pm
Criterion Covered Total %
statement 49 172 28.4
branch 7 82 8.5
condition 1 34 2.9
subroutine 11 20 55.0
pod 6 13 46.1
total 74 321 23.0


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3             package App::AFNI::SiemensPhysio;
4 1     1   10850 use strict;
  1         2  
  1         30  
5 1     1   3 use warnings;
  1         2  
  1         20  
6 1     1   3 use Carp;
  1         4  
  1         67  
7 1     1   468 use List::MoreUtils qw/minmax uniq/;
  1         8067  
  1         7  
8 1     1   469 use File::Basename;
  1         1  
  1         82  
9 1     1   4 use feature 'say';
  1         1  
  1         256  
10              
11              
12             =pod
13              
14             =head1 NAME
15            
16             App::AFNI:SiemensPhysio - Physio from Siemens into format suitable for AFNI's RetroTS retroicor routine
17              
18             =head1 SYNOPSIS
19              
20             Get slice based respiration volume per time (RVT) regressors from physio collected on Siemens scanner
21              
22             my $p = SiemensPhysio->new({VERB=>1});
23             # read MR data (get times, TR, nslices)
24             # looks at all files in this directory with "dicom_hinfo"
25             $p->readMRdir('MRRaw/10824_20111108/rest_384x384.21/');
26              
27             # read pulse
28             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.puls');
29             # write card: $protocol_$sessionTime.puls.dat
30             $p->writeMRPhys;
31              
32             # read card
33             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.resp');
34             # write resp: $protocol_$sessionTime.resp.dat
35             $p->writeMRPhys;
36              
37             #
38             $p->retroTS('matlab')
39              
40             # we could get the raw data
41             # in this case, card was resp was loaded last
42             # thats what will be returned)
43             my @pval = $p->getMRPhys();
44              
45             =head1 DESCRIPTION
46              
47              
48              
49             =head2 Pipeline
50              
51             =over
52              
53             =item read Siemens physio files
54              
55             =item read timing from MR DICOM files
56              
57             =item snip physio files relative to MR
58              
59             =item prepare/run AFNI's RetroTS
60              
61             =back
62              
63              
64             =head2 prior art
65              
66              
67             =over
68              
69             =item https://cfn.upenn.edu/aguirre/public/exvolt/
70              
71             =item https://cfn.upenn.edu/aguirre/wiki/public:pulse-oximetry_during_fmri_scanning
72              
73             =back
74              
75             =cut
76              
77              
78              
79             =head2 new
80              
81             initialize object
82              
83             =head3 OPTIONS
84              
85             =over
86              
87             =item timetype
88              
89             MDH (default) or MPCU
90              
91             =item PhRate
92              
93             Physio sample rate
94             defaults to .2
95              
96             =item pulsStart and respStart
97             numeric sequence to be remove, also identifies stream type
98              
99             defaults:
100              
101             pulsStart =>'1 2 40 280'
102             respStart =>'1 2 20 2'
103              
104              
105             =item sliceOrder
106              
107             alt+z (default)
108             other options: alt-z,seq+z,seq-z,filename # slice order
109              
110             =item VERB
111              
112             set to true to be verbose, defaults false
113              
114             =item trustIdx
115              
116             don't check sample rate and index count against end-start time
117             none=> check both
118             MR => trust MR (TR)
119             phys=> trust physio (PhRate as set by init)
120             all => trust both
121              
122             Note: just have to match reg exp, so MRphys is same as all
123              
124             =back
125              
126             =cut
127              
128             sub new {
129 0     0 1 0 my $class = shift;
130 0         0 my $self = shift;
131             # default to MDH becaues MPCPU doesn't align to MR time
132 0         0 my %defaults = (
133             #run => 'matlab', # matlab|McRetroTs|none
134             timetyp => 'MDH', # MPCU|MDH
135              
136             PhRate =>'.02',
137             # parameters of the acquisition.
138             # third is ticktime, but unlcear what the transfor is to get to freq/samplerate
139             pulsStart =>'1 2 40 280',
140             respStart =>'1 2 20 2',
141             sliceOrder => 'alt+z',
142             VERB=>0,
143             trustIdx=>'none',
144              
145             );
146             # use defaults when we didn't set anything
147 0         0 for my $k (keys %defaults) {
148 0 0 0     0 $self->{$k} = $defaults{$k} unless $self and $self->{$k};
149             }
150              
151 0         0 return bless $self, $class;
152             }
153              
154             =head2 readPhysio
155              
156             after intializing p, provide a file name
157              
158             $p->readPhysio('10824/20111108/wpc4951_10824_20111108_110811.puls');
159              
160             =head3 input file format
161              
162             1 2 40 280 ...
163             ECG Freq Per: 0 0
164             PULS Freq Per: 74 807
165             RESP Freq Per: 20 2860
166             EXT Freq Per: 0 0
167             ECG Min Max Avg StdDiff: 0 0 0 0
168             PULS Min Max Avg StdDiff: 527 1586 828 4
169             RESP Min Max Avg StdDiff: 2380 6700 3477 86
170             EXT Min Max Avg StdDiff: 0 0 0 0
171             NrTrig NrMP NrArr AcqWin: 0 0 0 0
172             LogStartMDHTime: 66439690
173             LogStopMDHTime: 71116595
174             LogStartMPCUTime: 66439512
175             LogStopMPCUTime: 71114802
176             6003
177              
178             =cut
179              
180             sub readPhysio {
181 0     0 1 0 my $self=shift;
182 0         0 my $fname=shift;
183              
184 0 0 0     0 croak "cannot open $fname"
185             unless $fname and open my $fh, '<', $fname;
186              
187 0         0 my $pulsStart = $self->{pulsStart};
188 0         0 my $respStart = $self->{respStart};
189 0         0 my $timetyp = $self->{timetyp};
190              
191             # first line is physio measures
192             # puls and resp have unique start sequences
193 0         0 my $values = <$fh>;
194              
195 0 0       0 croak "$fname does not start with expected resp or pulse prefix sequence values"
196             unless $values=~s/^((?$pulsStart)|(?$respStart))\W*//;
197              
198             # we can get the type by which regex we matched
199 1 0   1   451 $self->{ptype} = join('',map { $+{$_}?$_:"" } qw/puls resp/);
  1         443  
  1         2105  
  0         0  
  0         0  
200              
201             # break values by whitespace, remove 5000 and above
202             # 5000 is a scanner trigger, 5003 is end
203 0         0 $self->{measures} = [ grep { $_ < 5000 } split(/\W+/,$values) ];
  0         0  
204              
205             # get settings matching
206 0         0 my %settings;
207 0         0 while($_=<$fh>){
208             # remove dos chars
209 0         0 s/ //g;
210              
211             # parse settings, primarily for start and end time
212 0 0       0 $settings{$1} = $2 if m/(.*):\W+(.*)/;
213              
214             # check file integrity; assume last line is 6003
215 0 0 0     0 croak "corrupt file $fname. does not end with 6003"
216             if eof and ! m/^6003$/;
217            
218             }
219              
220              
221              
222             # get start and end from settings
223 0         0 $self->{physStart} = $settings{"LogStart${timetyp}Time"}/1000;
224 0         0 $self->{physEnd} = $settings{"LogStop${timetyp}Time"}/1000;
225              
226              
227              
228            
229             # reset rate if its only off by a very small amount
230             # and we don't trust the sample rate we provided
231 0         0 my $newrate = abs($self->{physStart}- $self->{physEnd})/$#{$self->{measures}};
  0         0  
232 0 0 0     0 $self->{PhRate} = $newrate
233             if abs($newrate-$self->{PhRate}) < .00001 and
234             $self->{trustIdx}!~/All|Phys/i;
235              
236              
237 0 0       0 say "file is $self->{ptype} with $#{$self->{measures}} samples, " ,
  0         0  
238             "$self->{physStart}s - $self->{physEnd}s, ",
239             "sample rate adjusted to $self->{PhRate}"
240             if $self->{VERB};
241              
242             # does the time match the sample rate and number of samples
243 0         0 timeCheck($self->{physStart},
244             $self->{physEnd},
245 0 0       0 $#{$self->{measures}},
246             $self->{PhRate} ) unless $self->{trustIdx}=~/All|Phys/i;
247             }
248              
249              
250             =head2 readMRdir
251              
252             after intializing p, read in MR info from raw DICOM directory
253              
254             $p->readMRdir('MRRaw/10824_20111108/rest_384x384.21/');
255              
256             sets
257              
258             =over
259              
260             =item timing (MRstart and MRend)
261              
262             =item protcol info (protocol,TR,ET,nslices,Series)
263              
264             =back
265              
266             =head3 Example Info
267              
268             dicom header info
269              
270             dicom_hdr MRRaw/10824_20111108/rest_384x384.21/MR* |egrep 'protocol|acquisition Time|Echo Time|Repetition Time' -i
271             0008 0031 14 [620 ] // ID Series Time//164627.359000
272             0008 0032 14 [642 ] // ID Acquisition Time//164932.315000
273             0018 0080 4 [1418 ] // ACQ Repetition Time//1500
274             0018 0081 2 [1430 ] // ACQ Echo Time//29
275             0018 1030 4 [1612 ] // ACQ Protocol Name//rest
276             0019 100a 2 [1788 ] // // 29
277              
278             shortend to
279              
280             dicom_hinfo -tag 0008,0032 0008,0031 0018,0080 0018,0081 0018,1030 MR*
281              
282             =cut
283              
284             # sets MRstart MRend protocol TR ET protocol nslices Series
285             sub readMRdir {
286 0     0 1 0 my $self=shift;
287 0         0 my $dicomdir=shift;
288 0 0       0 croak "$dicomdir is not a directory!" if ! -d $dicomdir;
289 0         0 my $dcmcmd = "dicom_hinfo -tag 0008,0031 0008,0032 0018,0080 0018,0081 0018,1030 0019,100a $dicomdir/*";
290 0         0 our @returns = qw/Filename Series AcqTime TR ET protocol nslice/;
291             # N.B. nslices/"Number Of Images In Mosaic" (0019,100a) is Siemens specific
292             # which is okay, because thats the kind of physio we have
293              
294              
295             # the index at which we can find the item we want
296             sub getidx {
297 0     0 0 0 my $name=shift;
298 0         0 return (grep { $returns[$_] eq $name } (0..$#returns))[0];
  0         0  
299             }
300              
301             # @v is an element for each dcm (line of output)
302 0 0       0 my @v=`$dcmcmd` or croak "could not run $dcmcmd";
303              
304             # make each line an array
305             # so we have an array of arrays
306             # v[0] is all info on first dicom
307             # v[0][0] is the first dicom's file name
308 0         0 @v= map { [split / /] } @v;
  0         0  
309              
310             # record some constant settings/values
311 0         0 for my $vals (qw/protocol TR ET Series nslice/) {
312 0         0 my $vidx=getidx($vals);
313             # make sure it's constant
314 0         0 my @allvals = uniq(map {$_->[$vidx]} @v);
  0         0  
315 0 0       0 croak "$vals is not constant: ".join(",",@allvals) if $#allvals>0;
316              
317 0         0 chomp($allvals[0]);
318 0         0 $self->{$vals} = $allvals[0];
319             }
320 0         0 $self->{nDcms} = $#v+1;
321 0         0 $self->{TR} /=1000;
322            
323              
324              
325             # Acquistion index
326 0         0 my $ATidx= getidx('AcqTime');
327              
328             # find max and min acq time from all MR*s
329 0         0 my ($starttime, $endtime) = minmax( map {$_->[$ATidx] } @v);
  0         0  
330              
331              
332             ## set start and end
333 0         0 $self->{MRstart} = getMRAcqSecs($starttime);
334 0         0 $self->{MRend} = getMRAcqSecs($endtime);
335              
336 0 0       0 say "MR starts $self->{MRstart} (DICOM $starttime) and ends $self->{MRend} (DICOM $endtime)"
337             if $self->{VERB};
338              
339 0 0       0 timeCheck($self->{MRstart},
340             $self->{MRend},
341             $self->{nDcms},
342             $self->{TR}) unless $self->{trustIdx}=~/All|MR/i;
343             }
344              
345             =head2 writeMRPhys
346              
347             write phys during MR to file
348             works on most recently loaded physio file
349              
350             $p->writeMRPhys
351              
352             =over
353              
354             =item use getMRphys to get values
355              
356             =item use writeDat to write values
357              
358             =back
359              
360             =cut
361              
362             sub writeMRPhys {
363 0     0 1 0 my $self=shift;
364 0         0 my $outfile = shift;
365 0 0       0 $outfile="$self->{protocol}_$self->{Series}.$self->{ptype}.dat" if ! $outfile;
366            
367             # if we provided a prefix
368 0 0       0 if($self->{prefix}){
369             # get dir
370 0         0 my $bn=dirname($self->{prefix});
371 0 0       0 croak "prefix directory ($bn) does not exist!" if ! -d $bn;
372 0         0 $outfile=$self->{prefix}.$outfile;
373             }
374              
375 0         0 $self->{dat}->{$self->{ptype}}=$outfile;
376              
377 0         0 my @pvals = $self->getMRPhys;
378              
379 0 0       0 say "saving to $outfile" if $self->{VERB};
380 0         0 writeDat($outfile,@pvals)
381             }
382              
383              
384              
385              
386             =head2 retroTS
387              
388             This is kludgy code hacked together and untested :)
389             =over
390              
391             =item use a bunch of hacks to find the matlab binary
392              
393             =item construct a matlab call using options in self object
394              
395             =item execute matlab or McRetroTS
396              
397             =item move outputs to fit local naming convention
398              
399             =back
400              
401              
402              
403             get/run command to get Resp. Vol./Time (RVT) via AFNI's retroTS a la http://www.ncbi.nlm.nih.gov/pmc/articles/PMC2715870/
404             MUST have already read MR and written card and resp dat files
405              
406             $p->retroTS('matlab')
407              
408             how this step is handled is defined by the first argument
409              
410              
411              
412             =over
413              
414             =item matlab: use RetroTS.m
415              
416             =item McRetroTs: use compiled matlab verson of RetroTS.m
417              
418             =item show: print commands for both matlab and McRetroTS
419              
420             =item none: do nothing (why'd you call me then!?)
421              
422             =back
423              
424              
425             =head3 External Commands
426              
427             see
428              
429             =over
430              
431             =item http://afni.nimh.nih.gov/afni/matlab/
432              
433             =item http://afni.nimh.nih.gov/sscc/dglen/McRetroTS
434              
435             =item http://fieldtrip.googlecode.com/svn/trunk/external/afni/RetroTS.m
436              
437             =back
438              
439             if using matlab+retroTS, the path to retroTS.m should be in your MATLABPATH
440             export MATLABPATH="$HOME/afni_matlab/matlab/:$MATLABPATH"
441              
442             =cut
443              
444              
445             sub retroTS {
446 0     0 1 0 my $self=shift;
447 0         0 my $runtype=shift;
448 0 0 0     0 return if $runtype and $runtype =~ /none/i;
449              
450             # default to using matlab
451 0         0 my $matlabbin="matlab";
452              
453             # find where matlab script points to
454 0         0 my $acutalmatlab=`perl -ne 'print \$& if /^.*matlab /' \`which matlab\``;
455 0 0       0 $matlabbin=$acutalmatlab if $acutalmatlab;
456              
457             # or use env MATLABBIN
458 0 0       0 $matlabbin= $ENV{MATLABBIN} if $ENV{MATLABBIN};
459              
460              
461             # we need to have both data types
462 0 0 0     0 croak "need writeMRPhys for both puls and resp"
      0        
463             if ! $self->{dat} || ! -e $self->{dat}->{resp} || ! -e $self->{dat}->{resp};
464              
465 0         0 my %params = (
466             "Opts.Respfile" => "'".$self->{dat}->{resp}."'", # Respiration data file
467             "Opts.Cardfile" => "'".$self->{dat}->{puls}."'", # Cardiac data file
468             "Opts.PhysFS" => 1/$self->{PhRate}, # Physioliogical signal sampling frequency in Hz.
469             "Opts.Nslices" => $self->{nslice}, # Number of slices
470             "Opts.VolTR" => $self->{TR}, # Volume TR in seconds
471             "Opts.SliceOrder" => "'".$self->{sliceOrder}."'" # ['alt+z']/'alt-z'/'seq+z'/'seq-z'/'Custom'/filename.1D
472             );
473              
474             # McRetroTS Respdatafile ECGdatafile VolTR Nslices SamplingFreq(PhysFS) ShowGraphs
475 0         0 my @mcrts = qw/Opts.Respfile Opts.Cardfile Opts.VolTR Opts.Nslices Opts.PhysFS/;
476 0         0 my $mccmd = "McRetroTs @params{@mcrts}";
477 0 0       0 say $mccmd if $runtype !~ /matlab|McRetroTs/g ;;
478              
479              
480             # if have matlab and singal toolbox, can use this
481 0         0 my $cmd = join("; ", map { join("=",$_,$params{$_}) } keys %params);
  0         0  
482 0         0 $cmd .= "; Opts.ShowGraphs=0;Opts.Quiet=0;"; # turn off graphs, turn on verbose
483 0         0 $cmd .= " rts = RetroTS(Opts)";
484              
485             # we should wrap matlab up in a try+quit so we dont hang in ML command window on a failure
486 0         0 my $matlabwrap= qq/$matlabbin -nodisplay -r "try; $cmd; catch err; err, exit(1); end; rts, quit;"/;
487              
488 0 0       0 say $matlabwrap if $runtype !~ /matlab|McRetroTs/i;
489             # eg
490             # matlab -nodisplay -r "try; Opts.Cardfile='rest_164627.359000.puls.dat'; Opts.VolTR=1.5; Opts.Nslices=29; Opts.SliceOrder='alt+z'; Opts.PhysFS=50.0074711455304; Opts.Respfile='rest_164627.359000.resp.dat'; rts = RetroTS(Opts); catch; exit(666); end; quit;"
491              
492             # with either command, the original output name will be "oba.slibase.1D"
493             # change that to our basename (assume resp and puls have same basename, use one from resp)
494 0         0 my $outputname = $self->{dat}->{resp};
495 0         0 $outputname =~ s/.resp.dat$/.slibase.1D/;
496            
497             # or rename to specified input
498             #my $outputname=shift if $#_;
499            
500            
501 0         0 my $runcmd="";
502 0 0       0 if($runtype =~ /matlab/i){
    0          
503 0         0 $runcmd=$matlabwrap
504             }elsif($runtype =~ /McRetroTs/i){
505 0         0 $runcmd=$mccmd;
506             }
507              
508 0 0       0 system($runcmd) if $runcmd;
509              
510 0 0 0     0 if( $runcmd && ! -e "oba.slibase.1D" ){
511 0         0 croak "failed to run $runcmd";
512             } else {
513             # move file to output name if told to
514 0 0       0 rename "oba.slibase.1D", $outputname if $outputname;
515             }
516             }
517              
518              
519              
520             #################
521             #### helpers ####
522             #################
523              
524              
525             # do start end and tr times make sense?
526             sub timeCheck {
527 3     3 0 4 my ($start,$end,$n,$tau) = @_;
528              
529             #my $maxDiffSec=$tau*2;
530 3         3 my $maxDiffSec=$tau;
531 3         4 my $dur=$end-$start;
532 3         4 my $ideal=$n *$tau;
533              
534             # start and end time are sane
535 3 50       7 croak "time starts ($start) before or on end time ($end)!"
536             if($end<=$start);
537              
538             # samples * sample rate == actual duration
539 3         32 my $offby = sprintf('%.3f',$ideal-$dur);
540 3         9 my $offbyN = sprintf('%.0f',$offby/$tau);
541 3 100       173 croak "off by $offby s ($offbyN samples) >$maxDiffSec diff: $n samples at $tau should be $ideal secs not $dur ($end - $start)"
542             if(abs($offby) > $maxDiffSec);
543              
544 2         10 return 1;
545             }
546              
547             # DICOM Acq Time is fmt like HHMMSS.SS
548             sub getMRAcqSecs {
549 5     5 0 14 $_=shift;
550 5 50       27 m/^(?\d{2})(?\d{2})(?\d{2}\.\d+)$/
551             or croak "$_ from MR time does not look like HHMMSS.sssss";
552              
553 5         51 my $secs = ($+{HH}*60*60) + ($+{MM}*60) + $+{SS} ;
554 5         25 return $secs;
555             }
556              
557              
558              
559             # returns vector of phys for the timing of an MR file
560             sub getMRPhys {
561 0     0 0 0 my $self=shift;
562 0         0 my ($s,$e) = sandwichIdx(
563 0         0 [@{$self}{qw/physStart physEnd/}],
564 0         0 [@{$self}{qw/MRstart MRend/}],
565 0         0 $#{$self->{measures}},
566             $self->{PhRate} );
567 0         0 $self->{MRstartIdx} = $s;
568 0         0 $self->{MRendIdx} = $e;
569              
570             ## print out where data is coming from/how sandwiching worked
571 0 0       0 $self->sayIndex if $self->{VERB};
572              
573 0         0 my @pval = @{$self->{measures}}[$s..$e];
  0         0  
574              
575 0 0       0 croak "no pvals!? bad indexes?" if $#pval < 0;
576 0         0 return @pval;
577             }
578              
579              
580              
581             # write values to file
582             sub writeDat {
583 0     0 1 0 my ($outname,@pvals) = @_;
584 0 0       0 croak "do not have a file name to save data" if(!$outname);
585 0 0       0 croak "have 1 or fewer values to write" if($#pvals < 1);
586              
587 0 0       0 open my $OH, '>', $outname or croak "could not open $outname to save physio";
588 0         0 print $OH join("\n",@pvals);
589 0         0 close $OH;
590             }
591              
592             #
593             # get the start and end index of the meat given start,end time of meat and bread
594             # need: start,end pairs in array ref + size and sample rate
595             # sandwichIdx([bread start,end],[meat start, end], size, rate)
596             #
597             sub sandwichIdx {
598 1     1 0 2 my ($bread, $meat, $n, $r) = @_;
599              
600             # make sure the meat is inside the bread
601 1         1 my ($MRstart,$MRend) = @{$meat};
  1         3  
602 1         1 my ($physStart,$physEnd) = @{$bread};
  1         2  
603              
604 1 50       4 croak "MR starts ($MRstart) before physio ($physStart)"
605             if( $MRstart < $physStart );
606              
607 1 50       3 croak "MR ends ($MRend) after physio ($physEnd)"
608             if( $MRend > $physEnd);
609              
610             # calc start and end by adding sandwitch top (start as ref)
611 1         3 my $sIdxS = timeToSamples($bread->[0],$meat->[0],$r);
612 1         4 my $eIdxS = timeToSamples($meat->[0] ,$meat->[1],$r ) + $sIdxS;
613              
614             # calc start and end by subt sandwitch bottom (use end as ref)
615 1         5 my $eIdxE = $n - timeToSamples($bread->[1],$meat->[1],$r );
616 1         3 my $sIdxE = $eIdxE - timeToSamples($meat->[0], $meat->[1],$r );
617              
618             # are the two the same?
619 1 50 33     8 carp "Inconsistant index calculation. ".
620             "Using start time vs end time as ref grabs different sample of measurements\n".
621             "(ref=start) $sIdxS to $eIdxS VS $sIdxE to $eIdxE (ref=end) @ $r secs/sample\n"
622             if $sIdxS != $sIdxE || $eIdxS != $eIdxE;
623              
624 1         3 return ($sIdxS,$eIdxS);
625             }
626              
627             # the diff of two times over the sampling rate gives the number of samples
628             sub timeToSamples {
629 11     11 0 14 my ($start, $end, $rate) = @_;
630 11         51 return sprintf( '%.0f', abs($end - $start)/$rate ) ;
631             }
632              
633             ## print whats up
634             sub sayIndex {
635 0     0 0   my $self=shift;
636              
637             # print out start and stop index for ps and pe
638 0           my $ps = timeToSamples($self->{physStart},$self->{physStart},$self->{PhRate});
639 0           my $pe = timeToSamples($self->{physStart},$self->{physEnd},$self->{PhRate});
640 0   0       my $s = $self->{MRstartIdx} || undef;
641 0   0       my $e = $self->{MRendIdx} || undef;
642             # lets talk about what we have
643 0           say "(ps ) $self->{physStart} | MR $self->{MRstart} $self->{MRend} | $self->{physEnd} (pe) ";
644 0 0 0       say "(sidx) $ps | MR $s $e | $pe (eidx)" if $s and $e;
645              
646             }
647              
648              
649              
650             1;
651              
652