metamri 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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