brainmap-ImageData 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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