metamri 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,418 @@
1
+
2
+ require 'rubygems';
3
+ require 'yaml';
4
+ require 'sqlite3';
5
+
6
+ =begin rdoc
7
+ Implements a collection of metadata associated with a raw image file. In
8
+ this case, by image we mean one single file. For the case of Pfiles one file
9
+ corresponds to a complete 4D data set. For dicoms one file corresponds to a single
10
+ 2D slice, many of which are assembled later during reconstruction to create a
11
+ 4D data set. The motivation for this class is to provide access to the metadata
12
+ stored in image file headers so that they can be later reconstructed into nifti
13
+ data sets.
14
+ =end
15
+ class RawImageFile
16
+ #:stopdoc:
17
+ MIN_HDR_LENGTH = 400
18
+ DICOM_HDR = "dicom_hdr"
19
+ RDGEHDR = "rdgehdr"
20
+ MONTHS = {
21
+ :jan => "01", :feb => "02", :mar => "03", :apr => "04", :may => "05",
22
+ :jun => "06", :jul => "07", :aug => "08", :sep => "09", :oct => "10",
23
+ :nov => "11", :dec => "12"
24
+ }
25
+ #:startdoc:
26
+
27
+ # The file name that the instance represents.
28
+ attr_reader :filename
29
+ # Which header reading utility reads this file, currently 'rdgehdr' or 'dicom_hdr'.
30
+ attr_reader :hdr_reader
31
+ # File types are either 'dicom' or 'pfile'.
32
+ attr_reader :file_type
33
+ # The date on which this scan was acquired, this is a ruby DateTime object.
34
+ attr_reader :timestamp
35
+ # The scanner used to perform this scan, e.g. 'Andys3T'.
36
+ attr_reader :source
37
+ # An identifier unique to a 'visit', these are assigned by the scanner techs at scan time.
38
+ attr_reader :rmr_number
39
+ # A short string describing the acquisition sequence. These come from the scanner.
40
+ # code and are used to initialise SeriesDescription objects to find related attributes.
41
+ attr_reader :series_description
42
+ # M or F.
43
+ attr_reader :gender
44
+ # Number of slices in the data set that includes this file, used by AFNI for reconstruction.
45
+ attr_reader :num_slices
46
+ # Given in millimeters.
47
+ attr_reader :slice_thickness
48
+ # Gap between slices in millimeters.
49
+ attr_reader :slice_spacing
50
+ # AKA Field of View, in millimeters.
51
+ attr_reader :reconstruction_diameter
52
+ # Voxels in x axis.
53
+ attr_reader :acquisition_matrix_x
54
+ # Voxels in y axis.
55
+ attr_reader :acquisition_matrix_y
56
+ # Time for each bold repetition, relevent for functional scans.
57
+ attr_reader :rep_time
58
+ # Number of bold reps in the complete functional task run.
59
+ attr_reader :bold_reps
60
+
61
+ =begin rdoc
62
+ Creates a new instance of the class given a path to a valid image file.
63
+
64
+ Throws IOError if the file given is not found or if the available header reading
65
+ utilities cannot read the image header. Also raises IOError if any of the
66
+ attributes cannot be found in the header. Be aware that the filename used to
67
+ initialize your instance is used to set the "file" attribute. If you need to
68
+ unzip a file to a temporary location, be sure to keep the same filename for the
69
+ temporary file.
70
+ =end
71
+ def initialize(pathtofile)
72
+ # raise an error if the file doesn't exist
73
+ absfilepath = File.expand_path(pathtofile)
74
+ raise(IOError, "File not found.") if not File.exists?(absfilepath)
75
+ @filename = File.basename(absfilepath)
76
+
77
+ # try to read the header, raise an ioerror if unsuccessful
78
+ begin
79
+ @hdr_data, @hdr_reader = read_header(absfilepath)
80
+ #puts "@hdr_data: #{@hdr_data}; @hdr_reader: #{@hdr_reader}"
81
+ rescue Exception => e
82
+ raise(IOError, "Header not readable. #{e}")
83
+ end
84
+
85
+ # file type is based on file name but only if the header was read successfully
86
+ @file_type = determine_file_type
87
+
88
+ # try to import attributes from the header, raise an ioerror if any attributes
89
+ # are not found
90
+ begin
91
+ import_hdr
92
+ rescue Exception => e
93
+ raise(IOError, "Header import failed. #{e}")
94
+ end
95
+
96
+ # deallocate the header data to save memory space.
97
+ @hdr_data = nil
98
+ end
99
+
100
+
101
+ =begin rdoc
102
+ Predicate method that tells whether or not the file is actually an image. This
103
+ judgement is based on whether one of the available header reading utilities can
104
+ actually read the header information.
105
+ =end
106
+ def image?
107
+ return ( @hdr_reader == RDGEHDR or @hdr_reader == DICOM_HDR )
108
+ end
109
+
110
+
111
+ =begin rdoc
112
+ Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
113
+ =end
114
+ def pfile?
115
+ return @file_type == "pfile"
116
+ end
117
+
118
+
119
+ =begin rdoc
120
+ Predicate simply returns true if "dicom" is stored in the img_type instance variable.
121
+ =end
122
+ def dicom?
123
+ return @file_type == "dicom"
124
+ end
125
+
126
+
127
+ =begin rdoc
128
+ Returns a yaml string based on a subset of the attributes. Specifically,
129
+ the @hdr_data is not included. This is used to generate .yaml files that are
130
+ placed in image directories for later scanning by YamlScanner.
131
+ =end
132
+ def to_yaml
133
+ yamlhash = {}
134
+ instance_variables.each do |var|
135
+ yamlhash[var[1..-1]] = instance_variable_get(var) if (var != "@hdr_data")
136
+ end
137
+ return yamlhash.to_yaml
138
+ end
139
+
140
+
141
+ =begin rdoc
142
+ Returns the internal, parsed data fields in an array. This is used when scanning
143
+ dicom slices, to compare each dicom slice in a folder and make sure they all hold the
144
+ same data.
145
+ =end
146
+ def to_array
147
+ return [@filename,
148
+ @timestamp,
149
+ @source,
150
+ @rmr_number,
151
+ @series_description,
152
+ @gender,
153
+ @slice_thickness,
154
+ @slice_spacing,
155
+ @reconstruction_diameter,
156
+ @acquisition_matrix_x,
157
+ @acquisition_matrix_y]
158
+ end
159
+
160
+ =begin rdoc
161
+ Returns an SQL statement to insert this image into the raw_images table of a
162
+ compatible database (sqlite3). This is intended for inserting into the rails
163
+ backend database.
164
+ =end
165
+ def db_insert(image_dataset_id)
166
+ "INSERT INTO raw_image_files
167
+ (filename, header_reader, file_type, timestamp, source, rmr_number, series_description,
168
+ gender, num_slices, slice_thickness, slice_spacing, reconstruction_diameter,
169
+ acquisition_matrix_x, acquisition_matrix_y, rep_time, bold_reps, created_at, updated_at, image_dataset_id)
170
+ VALUES ('#{@filename}', '#{@hdr_reader}', '#{@file_type}', '#{@timestamp.to_s}', '#{@source}', '#{@rmr_number}',
171
+ '#{@series_description}', '#{@gender}', #{@num_slices}, #{@slice_thickness}, #{@slice_spacing},
172
+ #{@reconstruction_diameter}, #{@acquisition_matrix_x}, #{@acquisition_matrix_y}, #{@rep_time},
173
+ #{@bold_reps}, '#{DateTime.now}', '#{DateTime.now}', #{image_dataset_id})"
174
+ end
175
+
176
+ =begin rdoc
177
+ Returns an SQL statement to select this image file row from the raw_image_files table
178
+ of a compatible database.
179
+ =end
180
+ def db_fetch
181
+ "SELECT *" + from_table_where + sql_match_conditions
182
+ end
183
+
184
+ =begin rdoc
185
+ Returns and SQL statement to remove this image file from the raw_image_files table
186
+ of a compatible database.
187
+ =end
188
+ def db_remove
189
+ "DELETE" + from_table_where + sql_match_conditions
190
+ end
191
+
192
+
193
+ =begin rdoc
194
+ Uses the db_insert method to actually perform the database insert using the
195
+ specified database file.
196
+ =end
197
+ def db_insert!( db_file )
198
+ db = SQLite3::Database.new( db_file )
199
+ db.transaction do |database|
200
+ if not database.execute( db_fetch ).empty?
201
+ raise(IndexError, "Entry exists for #{filename}, #{@rmr_number}, #{@timestamp.to_s}... Skipping.")
202
+ end
203
+ database.execute( db_insert )
204
+ end
205
+ db.close
206
+ end
207
+
208
+ =begin rdoc
209
+ Removes this instance from the raw_image_files table of the specified database.
210
+ =end
211
+ def db_remove!( db_file )
212
+ db = SQLite3::Database.new( db_file )
213
+ db.execute( db_remove )
214
+ db.close
215
+ end
216
+
217
+ =begin rdoc
218
+ Finds the row in the raw_image_files table of the given db file that matches this object.
219
+ ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
220
+ as an array of values (see 'sqlite3' gem docs).
221
+ =end
222
+ def db_fetch!( db_file )
223
+ db = SQLite3::Database.new( db_file )
224
+ db_row = db.execute( db_fetch )
225
+ db.close
226
+ return db_row
227
+ end
228
+
229
+
230
+
231
+
232
+ private
233
+
234
+
235
+
236
+ def from_table_where
237
+ " FROM raw_image_files WHERE "
238
+ end
239
+
240
+ def sql_match_conditions
241
+ "rmr_number = '#{@rmr_number}' AND timestamp = '#{@timestamp.to_s}' AND filename = '#{@filename}'"
242
+ end
243
+
244
+ =begin rdoc
245
+ Reads the file header using one of the available header reading utilities.
246
+ Returns both the header data as a one big string, and the name of the utility
247
+ used to read it.
248
+
249
+ Note: The rdgehdr is a binary file; the correct version for your architecture must be installed in the path.
250
+ =end
251
+ def read_header(absfilepath)
252
+ header = `#{DICOM_HDR} #{absfilepath} 2> /dev/null`
253
+ #header = `#{DICOM_HDR} #{absfilepath}`
254
+ if ( header.index("ERROR") == nil and
255
+ header.chomp != "" and
256
+ header.length > MIN_HDR_LENGTH )
257
+ return [ header, DICOM_HDR ]
258
+ end
259
+ header = `#{RDGEHDR} #{absfilepath} 2> /dev/null`
260
+ #header = `#{RDGEHDR} #{absfilepath}`
261
+ if ( header.chomp != "" and
262
+ header.length > MIN_HDR_LENGTH )
263
+ return [ header, RDGEHDR ]
264
+ end
265
+ return [ nil, nil ]
266
+ end
267
+
268
+
269
+ =begin rdoc
270
+ Returns a string that indicates the file type. This is difficult because dicom
271
+ files have no consistent naming conventions/suffixes. Here we chose to call a
272
+ file a "pfile" if it is an image and the file name is of the form P*.7
273
+ All other images are called "dicom".
274
+ =end
275
+ def determine_file_type
276
+ return "pfile" if image? and (@filename =~ /^P.....\.7/) != nil
277
+ return "dicom" if image? and (@filename =~ /^P.....\.7/) == nil
278
+ return nil
279
+ end
280
+
281
+
282
+ =begin rdoc
283
+ Parses the header data and extracts a collection of instance variables. If
284
+ @hdr_data and @hdr_reader are not already availables, this function does nothing.
285
+ =end
286
+ def import_hdr
287
+ raise(IndexError, "No Header Data Available.") if @hdr_data == nil
288
+ dicom_hdr_import if (@hdr_reader == "dicom_hdr")
289
+ rdgehdr_import if (@hdr_reader == "rdgehdr")
290
+ end
291
+
292
+
293
+ =begin rdoc
294
+ Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
295
+ utility.
296
+ =end
297
+ def dicom_hdr_import
298
+ date_pat = /ID STUDY DATE\/\/(.*)\n/i
299
+ time_pat = /ID Series Time\/\/(.*)\n/i
300
+ source_pat = /ID INSTITUTION NAME\/\/(.*)\n/i
301
+ rmr_number_pat = /[ID Accession Number|ID Study Description]\/\/(RMR.*)\n/i
302
+ series_description_pat = /ID SERIES DESCRIPTION\/\/(.*)\n/i
303
+ gender_pat = /PAT PATIENT SEX\/\/(.)/i
304
+ slice_thickness_pat = /ACQ SLICE THICKNESS\/\/(.*)\n/i
305
+ slice_spacing_pat = /ACQ SPACING BETWEEN SLICES\/\/(.*)\n/i
306
+ recon_diam_pat = /ACQ RECONSTRUCTION DIAMETER\/\/([0-9]+)/i
307
+ #acquisition_matrix_pat = /ACQ ACQUISITION MATRIX\/\/ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)/i
308
+ acq_mat_x_pat = /IMG Rows\/\/ ([0-9]+)/i
309
+ acq_mat_y_pat = /IMG Columns\/\/ ([0-9]+)/i
310
+ num_slices_pat = /REL Images in Acquisition\/\/([0-9]+)/i
311
+ bold_reps_pat = /REL Number of Temporal Positions\/\/([0-9]+)/i
312
+ rep_time_pat = /ACQ Repetition Time\/\/(.*)\n/i
313
+
314
+ rmr_number_pat =~ @hdr_data
315
+ @rmr_number = ($1).strip.chomp
316
+
317
+ source_pat =~ @hdr_data
318
+ @source = ($1).strip.chomp
319
+
320
+ num_slices_pat =~ @hdr_data
321
+ @num_slices = ($1).to_i
322
+
323
+ slice_thickness_pat =~ @hdr_data
324
+ @slice_thickness = ($1).strip.chomp.to_f
325
+
326
+ slice_spacing_pat =~ @hdr_data
327
+ @slice_spacing = ($1).strip.chomp.to_f
328
+
329
+ date_pat =~ @hdr_data
330
+ date = $1
331
+ time_pat =~ @hdr_data
332
+ time = $1
333
+ @timestamp = DateTime.parse(date + time)
334
+
335
+ gender_pat =~ @hdr_data
336
+ @gender = $1
337
+
338
+ #acquisition_matrix_pat =~ @hdr_data
339
+ #matrix = [($1).to_i, ($2).to_i, ($3).to_i, ($4).to_i]
340
+ #matrix = matrix.delete_if { |x| x == 0 }
341
+ acq_mat_x_pat =~ @hdr_data
342
+ @acquisition_matrix_x = ($1).to_i
343
+ acq_mat_y_pat =~ @hdr_data
344
+ @acquisition_matrix_y = ($1).to_i
345
+
346
+ series_description_pat =~ @hdr_data
347
+ @series_description = ($1).strip.chomp
348
+
349
+ recon_diam_pat =~ @hdr_data
350
+ @reconstruction_diameter = ($1).to_i
351
+
352
+ bold_reps_pat =~ @hdr_data
353
+ @bold_reps = ($1).to_i
354
+
355
+ rep_time_pat =~ @hdr_data
356
+ @rep_time = ($1).strip.chomp.to_f
357
+ end
358
+
359
+
360
+ =begin rdoc
361
+ Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
362
+ utility.
363
+ =end
364
+ def rdgehdr_import
365
+ source_pat = /hospital [Nn]ame: ([[:graph:]\t ]+)/i
366
+ num_slices_pat = /Number of slices in this scan group: ([0-9]+)/i
367
+ slice_thickness_pat = /slice thickness \(mm\): ([[:graph:]]+)/i
368
+ slice_spacing_pat = /spacing between scans \(mm\??\): ([[:graph:]]+)/i
369
+ date_pat = /actual image date\/time stamp: (.*)\n/i
370
+ gender_pat = /Patient Sex: (1|2)/i
371
+ acquisition_matrix_x_pat = /Image matrix size \- X: ([0-9]+)/i
372
+ acquisition_matrix_y_pat = /Image matrix size \- Y: ([0-9]+)/i
373
+ series_description_pat = /Series Description: ([[:graph:] \t]+)/i
374
+ recon_diam_pat = /Display field of view \- X \(mm\): ([0-9]+)/i
375
+ rmr_number_pat = /Patient ID for this exam: ([[:graph:]]+)/i
376
+ bold_reps_pat = /Number of excitations: ([0-9]+)/i
377
+ rep_time_pat = /Pulse repetition time \(usec\): ([0-9]+)/i
378
+
379
+ rmr_number_pat =~ @hdr_data
380
+ @rmr_number = ($1).nil? ? "rmr not found" : ($1).strip.chomp
381
+
382
+ source_pat =~ @hdr_data
383
+ @source = ($1).nil? ? "source not found" : ($1).strip.chomp
384
+
385
+ num_slices_pat =~ @hdr_data
386
+ @num_slices = ($1).to_i
387
+
388
+ slice_thickness_pat =~ @hdr_data
389
+ @slice_thickness = ($1).to_f
390
+
391
+ slice_spacing_pat =~ @hdr_data
392
+ @slice_spacing = ($1).to_f
393
+
394
+ date_pat =~ @hdr_data
395
+ @timestamp = DateTime.parse($1)
396
+
397
+ gender_pat =~ @hdr_data
398
+ @gender = $1 == 1 ? "M" : "F"
399
+
400
+ acquisition_matrix_x_pat =~ @hdr_data
401
+ @acquisition_matrix_x = ($1).to_i
402
+ acquisition_matrix_y_pat =~ @hdr_data
403
+ @acquisition_matrix_y = ($1).to_i
404
+
405
+ series_description_pat =~ @hdr_data
406
+ @series_description = ($1).strip.chomp
407
+
408
+ recon_diam_pat =~ @hdr_data
409
+ @reconstruction_diameter = ($1).to_i
410
+
411
+ bold_reps_pat =~ @hdr_data
412
+ @bold_reps = ($1).to_i
413
+
414
+ rep_time_pat =~ @hdr_data
415
+ @rep_time = ($1).to_f / 1000000
416
+ end
417
+
418
+ end
@@ -0,0 +1,81 @@
1
+ =begin rdoc
2
+ Provides a mapping between series descriptions that are extracted from raw image
3
+ headers and some associated attributes.
4
+ =end
5
+ class SeriesDescriptionParameters
6
+ #:stopdoc:
7
+ SERIES_DESCRIPTIONS = {
8
+ "3-P,Localizer" => [ "3PlaneLoc", "anat", nil ],
9
+ "gre field map rhrcctrl = 15" => [ "Fieldmap", "epan", nil ],
10
+ "SAG EPI Test 1 2 3" => [ "EPITest", "epan", "sag" ],
11
+ "3D IR AX T1 - NEW" => [ "T1_EFGRE3D", "anat", "ax" ],
12
+ "AX T2 W FR FSE 1.7 skip 0.3" => [ "T2_FSE", "fse", "ax" ],
13
+ "High Order Shim 28cm" => [ "Shim", "anat", nil ],
14
+ "SAG gre field map rhrcctr =15" => [ "Fieldmap", "epan", "sag" ],
15
+ "dti w/ card gate" => [ "DTI", "epan", nil ],
16
+ "HOS Head coil" => [ "3PlaneLoc", "anat", nil ],
17
+ "Localizer" => [ "3PlaneLoc", "anat", nil ],
18
+ "F Map; rhrcctrl 15; te7, 10" => [ "Fieldmap", "epan", nil ],
19
+ "SAG EPI TEST" => [ "EPITest", "epan", "sag" ],
20
+ "ASL CBF" => [ "AlsopsASL", "anat", nil ],
21
+ "DTI - prev 39 slices" => [ "DTI", "epan", nil ],
22
+ "3D IR COR T1 - NEW" => [ "T1_EFGRE3D", "anat", "cor" ],
23
+ "SAG T2 W FSE 1.7 skip 0.3" => [ "T2_FSE", "fse", "sag" ],
24
+ "COR T2 W FSE 1.7 skip 0.3" => [ "T2_FSE", "fse", "cor" ],
25
+ "3plane - hirez" => [ "3PlaneLoc", "anat", nil ],
26
+ "SAG gre field map rhrcctr =1?" => [ "Fieldmap", "epan", "sag" ],
27
+ "SAG EPI Test 1 2 3" => [ "EPITest", "epan", "sag" ],
28
+ "dti w/o card gate" => [ "DTI", "epan", nil ],
29
+ "Ax Flair irFSE" => [ "T1_Flair", "fse", "ax" ],
30
+ "DTI" => [ "DTI", "epan", nil ],
31
+ "AX T2 Flair" => [ "T2_Flair", "fse", "ax" ],
32
+ "AX T2 FLAIR" => [ "T2_Flair", "fse", "ax" ],
33
+ "SAG EPI Snod" => [ "fMRI_snod", "epan", "sag" ],
34
+ "SAG EPI Resting" => [ "fMRI_rest", "epan", "sag" ],
35
+ "SAG EPI Snod (141 x 2)" => [ "fMRI_snod", "epan", "sag" ],
36
+ "DTI - 10 Dir 1.8mm" => [ "DTI", "epan", nil ],
37
+ "F Map; rhrcctrl 15; te6, 9" => [ "Fieldmap", "epan", nil ],
38
+ "ASSET CAL" => [ "ASSET_Calibration", "epan", nil ],
39
+ "SAG EPI Resting (180)" => [ "fMRI_rest", "epan", "sag" ],
40
+ "Sag SMAPS" => [ "smaps", "anat", "sag" ]
41
+ }
42
+ #:startdoc:
43
+
44
+ # A string used to build nice file names for reconstructed data sets
45
+ attr_reader :scan_type
46
+
47
+ # Used as an argument to to3d, the AFNI command used to reconstruct a collection
48
+ # of dicom files into a single nifti data set
49
+ attr_reader :anat_type
50
+
51
+ # The scan acquisition plane: axial, coronal, or sagittal
52
+ attr_reader :acq_plane
53
+
54
+ =begin rdoc
55
+ Creates a new object based on a series description string.
56
+ The series description for an image is conveniently available as an attribute
57
+ of the RawImageFile class.
58
+
59
+ <i>Note that the series descriptions inside image headers sometimes have trailing</i>
60
+ <i>white space, the constructor here strips and chomps it. Be advised of this behavior.</i>
61
+
62
+ <tt>sd = SeriesDescription.new('3D IR AX T1 - NEW')</tt>
63
+
64
+ <tt>sd.scan_type</tt>
65
+
66
+ <tt>=> "T1_EFGRE3D"</tt>
67
+
68
+ <tt>sd.anat_type</tt>
69
+
70
+ <tt>=> "anat"</tt>
71
+
72
+ <tt>sd.acq_plane</tt>
73
+
74
+ <tt>=> "axial"</tt>
75
+ =end
76
+ def initialize(series_description)
77
+ @series_description = series_description.strip.chomp
78
+ raise IndexError if not SERIES_DESCRIPTIONS.has_key?(@series_description)
79
+ @scan_type, @anat_type, @acq_plane = SERIES_DESCRIPTIONS[@series_description]
80
+ end
81
+ end