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