brainmap-ImageData 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,411 @@
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
+
73
+ # raise an error if the file doesn't exist
74
+ absfilepath = File.expand_path(pathtofile)
75
+ raise(IOError, "File not found.") if not File.exists?(absfilepath)
76
+ @filename = File.basename(absfilepath)
77
+
78
+ # try to read the header, raise an ioerror if unsuccessful
79
+ @hdr_data, @hdr_reader = read_header(absfilepath)
80
+ raise(IOError, "Header not readable.") if @hdr_reader.nil?
81
+
82
+ # file type is based on file name but only if the header was read successfully
83
+ @file_type = determine_file_type
84
+
85
+ # try to import attributes from the header, raise an ioerror if any attributes
86
+ # are not found
87
+ begin
88
+ import_hdr
89
+ rescue
90
+ raise(IOError, "Header import failed.")
91
+ end
92
+
93
+ # deallocate the header data to save memory space.
94
+ @hdr_data = nil
95
+ end
96
+
97
+
98
+ =begin rdoc
99
+ Predicate method that tells whether or not the file is actually an image. This
100
+ judgement is based on whether one of the available header reading utilities can
101
+ actually read the header information.
102
+ =end
103
+ def image?
104
+ return ( @hdr_reader == RDGEHDR or @hdr_reader == DICOM_HDR )
105
+ end
106
+
107
+
108
+ =begin rdoc
109
+ Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
110
+ =end
111
+ def pfile?
112
+ return @file_type == "pfile"
113
+ end
114
+
115
+
116
+ =begin rdoc
117
+ Predicate simply returns true if "dicom" is stored in the img_type instance variable.
118
+ =end
119
+ def dicom?
120
+ return @file_type == "dicom"
121
+ end
122
+
123
+
124
+ =begin rdoc
125
+ Returns a yaml string based on a subset of the attributes. Specifically,
126
+ the @hdr_data is not included. This is used to generate .yaml files that are
127
+ placed in image directories for later scanning by YamlScanner.
128
+ =end
129
+ def to_yaml
130
+ yamlhash = {}
131
+ instance_variables.each do |var|
132
+ yamlhash[var[1..-1]] = instance_variable_get(var) if (var != "@hdr_data")
133
+ end
134
+ return yamlhash.to_yaml
135
+ end
136
+
137
+
138
+ =begin rdoc
139
+ Returns the internal, parsed data fields in an array. This is used when scanning
140
+ dicom slices, to compare each dicom slice in a folder and make sure they all hold the
141
+ same data.
142
+ =end
143
+ def to_array
144
+ return [@filename,
145
+ @timestamp,
146
+ @source,
147
+ @rmr_number,
148
+ @series_description,
149
+ @gender,
150
+ @slice_thickness,
151
+ @slice_spacing,
152
+ @reconstruction_diameter,
153
+ @acquisition_matrix_x,
154
+ @acquisition_matrix_y]
155
+ end
156
+
157
+ =begin rdoc
158
+ Returns an SQL statement to insert this image into the raw_images table of a
159
+ compatible database (sqlite3). This is intended for inserting into the rails
160
+ backend database.
161
+ =end
162
+ def db_insert(image_dataset_id)
163
+ "INSERT INTO raw_image_files
164
+ (filename, header_reader, file_type, timestamp, source, rmr_number, series_description,
165
+ gender, num_slices, slice_thickness, slice_spacing, reconstruction_diameter,
166
+ acquisition_matrix_x, acquisition_matrix_y, rep_time, bold_reps, created_at, updated_at, image_dataset_id)
167
+ VALUES ('#{@filename}', '#{@hdr_reader}', '#{@file_type}', '#{@timestamp.to_s}', '#{@source}', '#{@rmr_number}',
168
+ '#{@series_description}', '#{@gender}', #{@num_slices}, #{@slice_thickness}, #{@slice_spacing},
169
+ #{@reconstruction_diameter}, #{@acquisition_matrix_x}, #{@acquisition_matrix_y}, #{@rep_time},
170
+ #{@bold_reps}, '#{DateTime.now}', '#{DateTime.now}', #{image_dataset_id})"
171
+ end
172
+
173
+ =begin rdoc
174
+ Returns an SQL statement to select this image file row from the raw_image_files table
175
+ of a compatible database.
176
+ =end
177
+ def db_fetch
178
+ "SELECT *" + from_table_where + sql_match_conditions
179
+ end
180
+
181
+ =begin rdoc
182
+ Returns and SQL statement to remove this image file from the raw_image_files table
183
+ of a compatible database.
184
+ =end
185
+ def db_remove
186
+ "DELETE" + from_table_where + sql_match_conditions
187
+ end
188
+
189
+
190
+ =begin rdoc
191
+ Uses the db_insert method to actually perform the database insert using the
192
+ specified database file.
193
+ =end
194
+ def db_insert!( db_file )
195
+ db = SQLite3::Database.new( db_file )
196
+ db.transaction do |database|
197
+ if not database.execute( db_fetch ).empty?
198
+ raise(IndexError, "Entry exists for #{filename}, #{@rmr_number}, #{@timestamp.to_s}... Skipping.")
199
+ end
200
+ database.execute( db_insert )
201
+ end
202
+ db.close
203
+ end
204
+
205
+ =begin rdoc
206
+ Removes this instance from the raw_image_files table of the specified database.
207
+ =end
208
+ def db_remove!( db_file )
209
+ db = SQLite3::Database.new( db_file )
210
+ db.execute( db_remove )
211
+ db.close
212
+ end
213
+
214
+ =begin rdoc
215
+ Finds the row in the raw_image_files table of the given db file that matches this object.
216
+ ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
217
+ as an array of values (see 'sqlite3' gem docs).
218
+ =end
219
+ def db_fetch!( db_file )
220
+ db = SQLite3::Database.new( db_file )
221
+ db_row = db.execute( db_fetch )
222
+ db.close
223
+ return db_row
224
+ end
225
+
226
+
227
+
228
+
229
+ private
230
+
231
+
232
+
233
+ def from_table_where
234
+ " FROM raw_image_files WHERE "
235
+ end
236
+
237
+ def sql_match_conditions
238
+ "rmr_number = '#{@rmr_number}' AND timestamp = '#{@timestamp.to_s}' AND filename = '#{@filename}'"
239
+ end
240
+
241
+ =begin rdoc
242
+ Reads the file header using one of the available header reading utilities.
243
+ Returns both the header data as a one big string, and the name of the utility
244
+ used to read it.
245
+ =end
246
+ def read_header(absfilepath)
247
+ header = `#{DICOM_HDR} #{absfilepath} 2> /dev/null`
248
+ if ( header.index("ERROR") == nil and
249
+ header.chomp != "" and
250
+ header.length > MIN_HDR_LENGTH )
251
+ return [ header, DICOM_HDR ]
252
+ end
253
+ header = `#{RDGEHDR} #{absfilepath} 2> /dev/null`
254
+ if ( header.chomp != "" and
255
+ header.length > MIN_HDR_LENGTH )
256
+ return [ header, RDGEHDR ]
257
+ end
258
+ return [ nil, nil ]
259
+ end
260
+
261
+
262
+ =begin rdoc
263
+ Returns a string that indicates the file type. This is difficult because dicom
264
+ files have no consistent naming conventions/suffixes. Here we chose to call a
265
+ file a "pfile" if it is an image and the file name is of the form P*.7
266
+ All other images are called "dicom".
267
+ =end
268
+ def determine_file_type
269
+ return "pfile" if image? and (@filename =~ /^P.....\.7/) != nil
270
+ return "dicom" if image? and (@filename =~ /^P.....\.7/) == nil
271
+ return nil
272
+ end
273
+
274
+
275
+ =begin rdoc
276
+ Parses the header data and extracts a collection of instance variables. If
277
+ @hdr_data and @hdr_reader are not already availables, this function does nothing.
278
+ =end
279
+ def import_hdr
280
+ return if @hdr_data == nil
281
+ dicom_hdr_import if (@hdr_reader == "dicom_hdr")
282
+ rdgehdr_import if (@hdr_reader == "rdgehdr")
283
+ end
284
+
285
+
286
+ =begin rdoc
287
+ Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
288
+ utility.
289
+ =end
290
+ def dicom_hdr_import
291
+ date_pat = /ID STUDY DATE\/\/(.*)\n/i
292
+ time_pat = /ID Series Time\/\/(.*)\n/i
293
+ source_pat = /ID INSTITUTION NAME\/\/(.*)\n/i
294
+ rmr_number_pat = /[ID Accession Number|ID Study Description]\/\/(RMR.*)\n/i
295
+ series_description_pat = /ID SERIES DESCRIPTION\/\/(.*)\n/i
296
+ gender_pat = /PAT PATIENT SEX\/\/(.)/i
297
+ slice_thickness_pat = /ACQ SLICE THICKNESS\/\/(.*)\n/i
298
+ slice_spacing_pat = /ACQ SPACING BETWEEN SLICES\/\/(.*)\n/i
299
+ recon_diam_pat = /ACQ RECONSTRUCTION DIAMETER\/\/([0-9]+)/i
300
+ #acquisition_matrix_pat = /ACQ ACQUISITION MATRIX\/\/ ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)/i
301
+ acq_mat_x_pat = /IMG Rows\/\/ ([0-9]+)/i
302
+ acq_mat_y_pat = /IMG Columns\/\/ ([0-9]+)/i
303
+ num_slices_pat = /REL Images in Acquisition\/\/([0-9]+)/i
304
+ bold_reps_pat = /REL Number of Temporal Positions\/\/([0-9]+)/i
305
+ rep_time_pat = /ACQ Repetition Time\/\/(.*)\n/i
306
+
307
+ rmr_number_pat =~ @hdr_data
308
+ @rmr_number = ($1).strip.chomp
309
+
310
+ source_pat =~ @hdr_data
311
+ @source = ($1).strip.chomp
312
+
313
+ num_slices_pat =~ @hdr_data
314
+ @num_slices = ($1).to_i
315
+
316
+ slice_thickness_pat =~ @hdr_data
317
+ @slice_thickness = ($1).strip.chomp.to_f
318
+
319
+ slice_spacing_pat =~ @hdr_data
320
+ @slice_spacing = ($1).strip.chomp.to_f
321
+
322
+ date_pat =~ @hdr_data
323
+ date = $1
324
+ time_pat =~ @hdr_data
325
+ time = $1
326
+ @timestamp = DateTime.parse(date + time)
327
+
328
+ gender_pat =~ @hdr_data
329
+ @gender = $1
330
+
331
+ #acquisition_matrix_pat =~ @hdr_data
332
+ #matrix = [($1).to_i, ($2).to_i, ($3).to_i, ($4).to_i]
333
+ #matrix = matrix.delete_if { |x| x == 0 }
334
+ acq_mat_x_pat =~ @hdr_data
335
+ @acquisition_matrix_x = ($1).to_i
336
+ acq_mat_y_pat =~ @hdr_data
337
+ @acquisition_matrix_y = ($1).to_i
338
+
339
+ series_description_pat =~ @hdr_data
340
+ @series_description = ($1).strip.chomp
341
+
342
+ recon_diam_pat =~ @hdr_data
343
+ @reconstruction_diameter = ($1).to_i
344
+
345
+ bold_reps_pat =~ @hdr_data
346
+ @bold_reps = ($1).to_i
347
+
348
+ rep_time_pat =~ @hdr_data
349
+ @rep_time = ($1).strip.chomp.to_f
350
+ end
351
+
352
+
353
+ =begin rdoc
354
+ Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
355
+ utility.
356
+ =end
357
+ def rdgehdr_import
358
+ source_pat = /hospital [Nn]ame: ([[:graph:]\t ]+)/i
359
+ num_slices_pat = /Number of slices in this scan group: ([0-9]+)/i
360
+ slice_thickness_pat = /slice thickness \(mm\): ([[:graph:]]+)/i
361
+ slice_spacing_pat = /spacing between scans \(mm\??\): ([[:graph:]]+)/i
362
+ date_pat = /actual image date\/time stamp: (.*)\n/i
363
+ gender_pat = /Patient Sex: (1|2)/i
364
+ acquisition_matrix_x_pat = /Image matrix size \- X: ([0-9]+)/i
365
+ acquisition_matrix_y_pat = /Image matrix size \- Y: ([0-9]+)/i
366
+ series_description_pat = /Series Description: ([[:graph:] \t]+)/i
367
+ recon_diam_pat = /Display field of view \- X \(mm\): ([0-9]+)/i
368
+ rmr_number_pat = /Patient ID for this exam: ([[:graph:]]+)/i
369
+ bold_reps_pat = /Number of excitations: ([0-9]+)/i
370
+ rep_time_pat = /Pulse repetition time \(usec\): ([0-9]+)/i
371
+
372
+ rmr_number_pat =~ @hdr_data
373
+ @rmr_number = ($1).nil? ? "rmr not found" : ($1).strip.chomp
374
+
375
+ source_pat =~ @hdr_data
376
+ @source = ($1).nil? ? "source not found" : ($1).strip.chomp
377
+
378
+ num_slices_pat =~ @hdr_data
379
+ @num_slices = ($1).to_i
380
+
381
+ slice_thickness_pat =~ @hdr_data
382
+ @slice_thickness = ($1).to_f
383
+
384
+ slice_spacing_pat =~ @hdr_data
385
+ @slice_spacing = ($1).to_f
386
+
387
+ date_pat =~ @hdr_data
388
+ @timestamp = DateTime.parse($1)
389
+
390
+ gender_pat =~ @hdr_data
391
+ @gender = $1 == 1 ? "M" : "F"
392
+
393
+ acquisition_matrix_x_pat =~ @hdr_data
394
+ @acquisition_matrix_x = ($1).to_i
395
+ acquisition_matrix_y_pat =~ @hdr_data
396
+ @acquisition_matrix_y = ($1).to_i
397
+
398
+ series_description_pat =~ @hdr_data
399
+ @series_description = ($1).strip.chomp
400
+
401
+ recon_diam_pat =~ @hdr_data
402
+ @reconstruction_diameter = ($1).to_i
403
+
404
+ bold_reps_pat =~ @hdr_data
405
+ @bold_reps = ($1).to_i
406
+
407
+ rep_time_pat =~ @hdr_data
408
+ @rep_time = ($1).to_f / 1000000
409
+ end
410
+
411
+ 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 SeriesDescription
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