metamri 0.1.23 → 0.2.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.
data/Rakefile CHANGED
@@ -1,22 +1,7 @@
1
- #
2
- # To change this template, choose Tools | Templates
3
- # and open the template in the editor.
4
-
5
1
  require 'rubygems'
6
2
  require 'rake'
7
- # require 'echoe'
8
- #
9
- # Echoe.new('metamri', '0.1.0') do |p|
10
- # p.description = "Extraction of MRI metadata and insertion into compatible sqlite3 databases."
11
- # p.url = "http://github.com/brainmap/metamri"
12
- # p.author = "Kristopher J. Kosmatka"
13
- # p.email = "kk4@medicine.wisc.edu"
14
- # p.ignore_pattern = ["nbproject/*"]
15
- # p.development_dependencies = []
16
- # end
17
- #
18
- # Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
19
-
3
+ require 'rake/rdoctask'
4
+ require 'rake/testtask'
20
5
 
21
6
  begin
22
7
  require 'jeweler'
@@ -35,4 +20,23 @@ begin
35
20
  Jeweler::GemcutterTasks.new
36
21
  rescue LoadError
37
22
  puts "Jeweler not available. Install it with: sudo gem install jeweler"
23
+ end
24
+
25
+ begin
26
+ require 'spec/rake/spectask'
27
+ Spec::Rake::SpecTask.new do |test|
28
+ test.warning = true
29
+ test.rcov = true
30
+ test.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+ rescue LoadError
33
+ task :spec do
34
+ abort "RSpec is not available. In order to run specs, you must: sudo gem install rspec"
35
+ end
36
+ end
37
+
38
+ Rake::TestTask.new do |t|
39
+ t.libs << "test"
40
+ t.test_files = FileList['test/*test*.rb']
41
+ t.verbose = true
38
42
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.23
1
+ 0.2.0
@@ -42,14 +42,18 @@ def run!
42
42
 
43
43
  # Default to scanning the current directory if no argument was given,
44
44
  # otherwise go through each list.
45
- unless ARGV.length == 1
46
- input_directories = ARGV
45
+ unless ARGV.length >= 1
46
+ input_directories = [Dir.pwd]
47
47
  else
48
- input_directories = ARGV[0]
48
+ input_directories = ARGV
49
49
  end
50
50
 
51
- input_directories.each do |raw_directory|
52
- list_visit raw_directory, options
51
+ unless input_directories.empty?
52
+ input_directories.each do |raw_directory|
53
+ list_visit raw_directory, options
54
+ end
55
+ else
56
+ raise IOError, "No input directories specified."
53
57
  end
54
58
 
55
59
  end
@@ -73,10 +77,11 @@ def list_visit(raw_directory, options = {})
73
77
  begin
74
78
  raise ScriptError, "Scaning filesystem directly..." if options[:force_scan]
75
79
 
76
- visit = VisitRawDataDirectoryResource.find(:first, :params => {:search => {:path => raw_directory}})
80
+ lookup_path = File.dirname(raw_directory).split(File::Separator).last == "dicoms" ? File.join(raw_directory, '..') : raw_directory
81
+ visit = VisitRawDataDirectoryResource.find(:first, :params => {:search => {:path => File.expand_path(lookup_path)}})
77
82
 
78
- raise IOError.new("Could not lookup visit using path.") unless visit
79
- raise IOError.new("Returned visit does not match path.") unless visit.path == raw_directory
83
+ raise IOError.new("Could not lookup visit using path #{lookup_path}.") unless visit
84
+ raise IOError.new("Returned visit's path #{visit.path} does not match path.") unless visit.path == File.expand_path(lookup_path)
80
85
  rescue ScriptError, IOError => e
81
86
  puts e unless options[:verbose] == false
82
87
  visit = VisitRawDataDirectory.new(raw_directory)
@@ -116,6 +121,10 @@ def parse_options
116
121
  options[:verbose] = true
117
122
  end
118
123
 
124
+ opts.on('-i', '--ignore REGEXP', Regexp, "A regular expression to ignore heavy directories.") do |regexp|
125
+ options[:ignore_patterns] = [regexp]
126
+ end
127
+
119
128
  opts.on('-g', '--grep GREP', "Search series descriptions") do |grep|
120
129
  options[:grep] = grep
121
130
  options[:verbose] = false
@@ -132,5 +141,5 @@ def parse_options
132
141
  end
133
142
 
134
143
  if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
135
- run!
144
+ run!
136
145
  end
@@ -1,4 +1,5 @@
1
1
  require 'tmpdir'
2
+
2
3
  class String
3
4
  # Does same basic string replacements to ensure valid filenames.
4
5
  def escape_filename
@@ -37,14 +38,14 @@ class Pathname
37
38
 
38
39
  def each_pfile(min_file_size = MIN_PFILE_SIZE)
39
40
  entries.each do |leaf|
40
- next unless leaf.to_s =~ /^P.*\.7|^P.*\.7\.bz2/
41
+ next unless leaf.to_s =~ /^P.{5}\.7(\.bz2)/
41
42
  branch = self + leaf
42
43
  next if branch.symlink?
43
44
  if branch.size >= min_file_size
44
45
  lc = branch.local_copy
45
46
  begin
46
47
  yield lc
47
- rescue Exception => e
48
+ rescue StandardError => e
48
49
  puts "#{e}"
49
50
  ensure
50
51
  lc.delete
@@ -56,7 +57,7 @@ class Pathname
56
57
  def first_dicom
57
58
  entries.each do |leaf|
58
59
  branch = self + leaf
59
- if leaf.to_s =~ /^I\.|\.dcm(\.bz2)?$|\.[0-9]+(\.bz2)?$/
60
+ if leaf.to_s =~ /^I\.(\.bz2)?$|\.dcm(\.bz2)?$|[A-Za-z^P]\.[0-9]+(\.bz2)?$/
60
61
  lc = branch.local_copy
61
62
  begin
62
63
  yield lc
@@ -112,12 +113,11 @@ class Pathname
112
113
  self.to_s =~ /^\./ || self.symlink?
113
114
  end
114
115
 
115
- =begin
116
- Creates a local, unzipped copy of a file for use in scanning.
117
- Will return a pathname to the local copy if called directly, or can also be
118
- passed a block. If it is passed a block, it will create the local copy
119
- and ensure the local copy is deleted.
120
- =end
116
+
117
+ # Creates a local, unzipped copy of a file for use in scanning.
118
+ # Will return a pathname to the local copy if called directly, or can also be
119
+ # passed a block. If it is passed a block, it will create the local copy
120
+ # and ensure the local copy is deleted.
121
121
  def local_copy(tempdir = Dir.mktmpdir, &block)
122
122
  tfbase = self.to_s =~ /\.bz2$/ ? self.basename.to_s.chomp(".bz2") : self.basename.to_s
123
123
  tfbase.escape_filename
@@ -145,4 +145,26 @@ class Pathname
145
145
  end
146
146
  end
147
147
 
148
- end
148
+ end
149
+
150
+ # =begin rdoc
151
+ # Monkey-patch Float to avoid rounding errors.
152
+ # For more in-depth discussion, see: http://www.ruby-forum.com/topic/179361
153
+ # Currently not in use.
154
+ # =end
155
+ # class Float
156
+ # def <=> other
157
+ # puts epsilon = self * 1e-14
158
+ # diff = self - other
159
+ # # return -1 if diff > epsilon
160
+ # # return 1 if diff < -epsilon
161
+ # 0
162
+ # end
163
+ #
164
+ # def == other #because built-in Float#== bypasses <=>
165
+ # (self<=>other) == 0
166
+ # true
167
+ # end
168
+ # end
169
+
170
+
@@ -0,0 +1,26 @@
1
+ # An ImageQualityCheckResource is a ruby object that represents a quality check
2
+ # pulled down from the DataPanda database. It contains notes on the quality of
3
+ # specific images, with a note for each possible problem, and an overall note.
4
+ #
5
+ # Omnibus f: NA –
6
+ # User: erik
7
+ # Motion warning: NA –
8
+ # Ghosting wrapping: Pass –
9
+ # Nos concerns: NA –
10
+ # Image dataset: 12136
11
+ # Banding: Pass –
12
+ # Fov cutoff: Pass –
13
+ # Registration risk: Pass –
14
+ # Field inhomogeneity: Mild – eh - it's ok.
15
+ # Other issues: la la la
16
+ # Incomplete series: Complete –
17
+ # Spm mask: NA –
18
+ # Garbled series: Pass –
19
+ #
20
+ # Check the current schema.db file for all available fields.
21
+ class ImageDatasetQualityCheckResource < ActiveResource::Base
22
+ self.site = VisitRawDataDirectory::DATAPANDA_SERVER
23
+ self.element_name = "image_dataset_quality_check"
24
+
25
+
26
+ end
@@ -3,11 +3,10 @@ require 'sqlite3'
3
3
  require 'ftools'
4
4
  require 'metamri/nifti_builder'
5
5
 
6
- =begin rdoc
7
- A #Dataset defines a single 3D or 4D image, i.e. either a volume or a time series
8
- of volumes. This encapsulation will provide easy manipulation of groups of raw
9
- image files including basic reconstruction.
10
- =end
6
+
7
+ # A #RawImageDataset defines a single 3D or 4D image, i.e. either a volume or a time series
8
+ # of volumes. This encapsulation will provide easy manipulation of groups of raw
9
+ # image files including basic reconstruction.
11
10
  class RawImageDataset
12
11
 
13
12
  # The directory that contains all the raw images and related files that make up
@@ -31,16 +30,26 @@ class RawImageDataset
31
30
  attr_reader :scanner_source
32
31
  # A #RawImageDatasetThumbnail object that composes the thumbnail for the dataset.
33
32
  attr_reader :thumbnail
34
-
35
- =begin rdoc
36
- * dir: The directory containing the files.
37
- * files: An array of #RawImageFile objects that compose the complete data set.
33
+ # A Description of the Study as listed in the DICOM Header
34
+ attr_reader :study_description
35
+ # A Description of the Protocol as listed in the DICOM Header
36
+ attr_reader :protocol_name
37
+ # Scan Tech Initials
38
+ attr_reader :operator_name
39
+ # Patient "Name", usually StudyID or ENUM
40
+ attr_reader :patient_name
41
+ # DICOM Series UID
42
+ attr_reader :dicom_series_uid
43
+ # DICOM Study UID
44
+ attr_reader :dicom_study_uid
38
45
 
39
- Initialization raises errors in several cases:
40
- * directory doesn't exist => IOError
41
- * any of the raw image files is not actually a RawImageFile => IndexError
42
- * series description, rmr number, or timestamp cannot be extracted from the first RawImageFile => IndexError
43
- =end
46
+ # * dir: The directory containing the files.
47
+ # * files: An array of #RawImageFile objects that compose the complete data set.
48
+ #
49
+ # Initialization raises errors in several cases:
50
+ # * directory doesn't exist => IOError
51
+ # * any of the raw image files is not actually a RawImageFile => IndexError
52
+ # * series description, rmr number, or timestamp cannot be extracted from the first RawImageFile => IndexError
44
53
  def initialize(directory, raw_image_files)
45
54
  @directory = File.expand_path(directory)
46
55
  raise(IOError, "#{@directory} not found.") if not File.directory?(@directory)
@@ -55,7 +64,7 @@ class RawImageDataset
55
64
  @raw_image_files = raw_image_files
56
65
 
57
66
  @series_description = @raw_image_files.first.series_description
58
- raise(IndexError, "No series description found") if @series_description.nil?
67
+ validates_metainfo_for :series_description, :msg => "No series description found"
59
68
 
60
69
  @rmr_number = @raw_image_files.first.rmr_number
61
70
  raise(IndexError, "No rmr found") if @rmr_number.nil?
@@ -74,15 +83,34 @@ class RawImageDataset
74
83
  @study_id = @raw_image_files.first.study_id.nil? ? nil : @raw_image_files.first.study_id
75
84
  # raise(IndexError, "No study id / exam number found") if @study_id.nil?
76
85
 
86
+ @study_description = @raw_image_files.first.study_description
87
+ validates_metainfo_for :study_description, :msg => "No study description found" if dicom?
88
+
89
+ @protocol_name = @raw_image_files.first.protocol_name
90
+ validates_metainfo_for :protocol_name, :msg => "No protocol name found" if dicom?
91
+
92
+ @operator_name = @raw_image_files.first.operator_name
93
+ validates_metainfo_for :operator_name if dicom?
94
+
95
+ @patient_name = @raw_image_files.first.patient_name
96
+ validates_metainfo_for :patient_name if dicom?
97
+
98
+ @dicom_series_uid = @raw_image_files.first.dicom_series_uid
99
+ validates_metainfo_for :dicom_series_uid if dicom?
100
+
101
+ @dicom_study_uid = @raw_image_files.first.dicom_study_uid
102
+ validates_metainfo_for :dicom_study_uid if dicom?
103
+
77
104
  $LOG ||= Logger.new(STDOUT)
78
105
  end
106
+
79
107
 
80
- =begin rdoc
81
- Generates an SQL insert statement for this dataset that can be used to populate
82
- the Johnson Lab rails TransferScans application database backend. The motivation
83
- for this is that many dataset inserts can be collected into one db transaction
84
- at the visit level, or even higher when doing a whole file system scan.
85
- =end
108
+
109
+ # Generates an SQL insert statement for this dataset that can be used to
110
+ # populate the Johnson Lab rails TransferScans application database backend. The
111
+ # motivation for this is that many dataset inserts can be collected into one db
112
+ # transaction at the visit level, or even higher when doing a whole file system
113
+ # scan.
86
114
  def db_insert(visit_id)
87
115
  "INSERT INTO image_datasets
88
116
  (rmr, series_description, path, timestamp, created_at, updated_at, visit_id,
@@ -191,15 +219,15 @@ Returns a path to the created dataset as a string if successful.
191
219
  return nifti_conversion_command, nifti_output_file
192
220
  end
193
221
 
194
- =begin rdoc
195
- Returns a globbing wildcard that is used by to3D to gather files for
196
- reconstruction. If no compatible glob is found for the data set, nil is returned.
197
- This is always the case for pfiles. For example if the first file in a data set is I.001, then:
198
- <tt>dataset.glob</tt>
199
- <tt>=> "I.*"</tt>
200
- including the quotes, which are necessary becuase some data sets (functional dicoms)
201
- have more component files than shell commands can handle.
202
- =end
222
+
223
+ # Returns a globbing wildcard that is used by to3D to gather files for
224
+ # reconstruction. If no compatible glob is found for the data set, nil is
225
+ # returned. This is always the case for pfiles. For example if the first file in
226
+ # a data set is I.001, then:
227
+ # <tt>dataset.glob</tt>
228
+ # <tt>=> "I.*"</tt>
229
+ # including the quotes, which are necessary becuase some data sets (functional dicoms)
230
+ # have more component files than shell commands can handle.
203
231
  def glob
204
232
  case @raw_image_files.first.filename
205
233
  when /^E.*dcm$/
@@ -273,12 +301,18 @@ have more component files than shell commands can handle.
273
301
  return relative_dataset_path
274
302
  end
275
303
 
276
- # Reports series details, including description and possilby image quality
277
- # check comments.
304
+ # Reports series details, including description and possibly image quality
305
+ # check comments for #RawImageDatasetResource objects.
278
306
  def series_details
279
307
  @series_description
280
308
  end
281
309
 
310
+ # Helper predicate method to check whether the dataset is a DICOM dataset or not.
311
+ # This just sends dicom? to the first raw file in the dataset.
312
+ def dicom?
313
+ @raw_image_files.first.dicom?
314
+ end
315
+
282
316
  private
283
317
 
284
318
  # Gets the earliest timestamp among the raw image files in this dataset.
@@ -291,5 +325,15 @@ private
291
325
  File.basename(@directory)
292
326
  end
293
327
 
328
+ # Ensure that metadata is present in instance variables.
329
+ # validates_metainfo_for :study_description, :msg => "No study description found"
330
+ def validates_metainfo_for(info_variable, options = {})
331
+ raise StandardError, "#{info_variable} must be a symbol" unless info_variable.kind_of? Symbol
332
+ if self.instance_variable_get("@" + info_variable.to_s).nil?
333
+ raise IndexError, options[:msg] ||= "Couldn't find #{info_variable.to_s}"
334
+ end
335
+ end
336
+
337
+
294
338
  end
295
339
  #### END OF CLASS ####
@@ -70,7 +70,7 @@ class RawImageDatasetResource < ActiveResource::Base
70
70
  # end
71
71
 
72
72
  def pfile?
73
- scanned_file =~ /^P.*.7$/
73
+ scanned_file =~ /^P.{5}.7$/
74
74
  end
75
75
 
76
76
 
@@ -98,6 +98,10 @@ class RawImageDatasetResource < ActiveResource::Base
98
98
  return relative_dataset_path
99
99
  end
100
100
 
101
+ def image_quality_checks
102
+
103
+ end
104
+
101
105
  # Creates an Hirb Table for pretty output of dataset info.
102
106
  # It takes an array of either RawImageDatasets or RawImageDatasetResources
103
107
  def self.to_table(datasets)
@@ -20,6 +20,7 @@ class RawImageDatasetThumbnail
20
20
  # The processor for creating the thumbnail (:rubydicom or :slicer)
21
21
  attr_reader :processor
22
22
 
23
+ # Creates a RawImageDatasetThumbnail instance by passing in a parent dataset to thumbnail.
23
24
  def initialize(dataset)
24
25
  if dataset.class == RawImageDataset
25
26
  @dataset = dataset
@@ -37,14 +38,32 @@ class RawImageDatasetThumbnail
37
38
  # Raises a StandardError if the format is incorrect (i.e. P-file instead of DICOM)
38
39
  #
39
40
  # Be sure your filename is a valid unix filename - no spaces.
40
- # Sets the @path instance variable and returns the full filename to the thumbnail.
41
- #
42
- # Pass in either a absolute or relative path or filename for the output image,
43
- # and an options hash to manually specify the processor (Ruby Dicom or FSL Slicer).
44
- # {:processor => :rubydicom or :slicer}
41
+ #
42
+ # Returns the full absolute filename to the new thumbnail image and sets it to @path instance variable.
43
+ #
44
+ # === Parameters
45
+ #
46
+ # * <tt>output</tt>: An optional string which specifies a directory or filename for the thumbnail image.
47
+ # * <tt>options</tt>: A hash of additional options.
48
+ #
49
+ # === Options
50
+ #
51
+ # * <tt>:processor</tt> -- Symbol. Specifies which thumbnail processor to use. Defaults to :rubydicom, alternatively it could be :slicer
52
+ #
53
+ # === Examples
54
+ #
55
+ # # Load a RawImageDataset
56
+ # ds = RawImageDataset('s01_assetcal', RawImageFile.new('./s01_assetcal/I0001.dcm'))
57
+ # # Create a RawImageDatasetThumbnail instance
58
+ # thumb = RawImageDatasetThumbnail.new(ds)
59
+ # # Create a thumbnail in a temp directory without options, save it to a destination image, or force it to use FSL Slicer.
60
+ # thumb.create_thumbnail
61
+ # thumb.create_thumbnail('/tmp/asset_cal.png')
62
+ # thumb.create_thumbnail('/tmp/asset_cal.png', :processor => :slicer)
45
63
  #
46
64
  def create_thumbnail(output = nil, options = {:processor => :rubydicom})
47
65
  raise StandardError, "Thumbnail available only for DICOM format." unless dataset.raw_image_files.first.dicom?
66
+ raise ArgumentError, "Invalid :processor option #{options[:processor]}" unless VALID_PROCESSORS.include?(options[:processor])
48
67
  if output
49
68
  if File.directory?(output)
50
69
  # output is a directory. Set the output directory but leave filepath nil.
@@ -60,7 +79,7 @@ class RawImageDatasetThumbnail
60
79
  end
61
80
 
62
81
  @processor = options[:processor]
63
-
82
+
64
83
  # Set a default filepath unless one was explicitly passed in.
65
84
  default_name = @dataset.series_description.escape_filename
66
85
  filepath ||= File.join(output_directory, default_name + '.png')
@@ -4,21 +4,23 @@ require 'yaml';
4
4
  require 'sqlite3';
5
5
  require 'dicom'
6
6
 
7
- =begin rdoc
8
- Implements a collection of metadata associated with a raw image file. In
9
- this case, by image we mean one single file. For the case of Pfiles one file
10
- corresponds to a complete 4D data set. For dicoms one file corresponds to a single
11
- 2D slice, many of which are assembled later during reconstruction to create a
12
- 4D data set. The motivation for this class is to provide access to the metadata
13
- stored in image file headers so that they can be later reconstructed into nifti
14
- data sets.
15
- =end
7
+
8
+ # Implements a collection of metadata associated with a raw image file. In
9
+ # this case, by image we mean one single file. For the case of Pfiles one file
10
+ # corresponds to a complete 4D data set. For dicoms one file corresponds to a single
11
+ # 2D slice, many of which are assembled later during reconstruction to create a
12
+ # 4D data set. The motivation for this class is to provide access to the metadata
13
+ # stored in image file headers so that they can be later reconstructed into nifti
14
+ # data sets.
15
+ #
16
+ # Primarily used to instantiate a #RawImageDataset
16
17
  class RawImageFile
17
18
  #:stopdoc:
18
19
  MIN_HDR_LENGTH = 400
19
20
  DICOM_HDR = "dicom_hdr"
20
21
  RDGEHDR = "rdgehdr"
21
22
  RUBYDICOM_HDR = "rubydicom"
23
+ VALID_HEADERS = [DICOM_HDR, RDGEHDR, RUBYDICOM_HDR]
22
24
  MONTHS = {
23
25
  :jan => "01", :feb => "02", :mar => "03", :apr => "04", :may => "05",
24
26
  :jun => "06", :jul => "07", :aug => "08", :sep => "09", :oct => "10",
@@ -43,6 +45,10 @@ class RawImageFile
43
45
  # A short string describing the acquisition sequence. These come from the scanner.
44
46
  # code and are used to initialise SeriesDescription objects to find related attributes.
45
47
  attr_reader :series_description
48
+ # A short string describing the study sequence. These come from the scanner.
49
+ attr_reader :study_description
50
+ # A short string describing the study protocol. These come from the scanner.
51
+ attr_reader :protocol_name
46
52
  # M or F.
47
53
  attr_reader :gender
48
54
  # Number of slices in the data set that includes this file, used by AFNI for reconstruction.
@@ -65,23 +71,25 @@ class RawImageFile
65
71
  attr_reader :warnings
66
72
  # Serialized RubyDicomHeader Object (for DICOMs only)
67
73
  attr_reader :dicom_header
68
- # DICOM Sequence UID
69
- attr_reader :dicom_sequence_uid
74
+ # Hash of all DICOM Tags including their Names and Values (See #dicom_taghash for more information on the structure)
75
+ attr_reader :dicom_taghash
70
76
  # DICOM Series UID
71
77
  attr_reader :dicom_series_uid
72
78
  # DICOM Study UID
73
79
  attr_reader :dicom_study_uid
74
-
75
- =begin rdoc
76
- Creates a new instance of the class given a path to a valid image file.
77
-
78
- Throws IOError if the file given is not found or if the available header reading
79
- utilities cannot read the image header. Also raises IOError if any of the
80
- attributes cannot be found in the header. Be aware that the filename used to
81
- initialize your instance is used to set the "file" attribute. If you need to
82
- unzip a file to a temporary location, be sure to keep the same filename for the
83
- temporary file.
84
- =end
80
+ # Scan Tech Initials
81
+ attr_reader :operator_name
82
+ # Patient "Name", usually StudyID or ENUM
83
+ attr_reader :patient_name
84
+
85
+ # Creates a new instance of the class given a path to a valid image file.
86
+ #
87
+ # Throws IOError if the file given is not found or if the available header reading
88
+ # utilities cannot read the image header. Also raises IOError if any of the
89
+ # attributes cannot be found in the header. Be aware that the filename used to
90
+ # initialize your instance is used to set the "file" attribute. If you need to
91
+ # unzip a file to a temporary location, be sure to keep the same filename for the
92
+ # temporary file.
85
93
  def initialize(pathtofile)
86
94
  # raise an error if the file doesn't exist
87
95
  absfilepath = File.expand_path(pathtofile)
@@ -93,7 +101,7 @@ temporary file.
93
101
  begin
94
102
  @hdr_data, @hdr_reader = read_header(absfilepath)
95
103
  rescue Exception => e
96
- raise(IOError, "Header not readable for file #{@filename}. #{e}")
104
+ raise(IOError, "Header not readable for file #{@filename} using #{@current_hdr_reader ? @current_hdr_reader : "unknown header reader."}. #{e}")
97
105
  end
98
106
 
99
107
  # file type is based on file name but only if the header was read successfully
@@ -103,10 +111,10 @@ temporary file.
103
111
  # are not found
104
112
  begin
105
113
  import_hdr
106
- rescue ScriptError => e
107
- raise ScriptError, "Could not find required DICOM Header Meta Element: #{e}"
108
- rescue Exception => e
109
- raise IOError, "Header import failed for file #{@filename}. #{e}"
114
+ rescue ScriptError, NoMethodError => e
115
+ raise IOError, "Could not find required DICOM Header Meta Element: #{e}"
116
+ rescue StandardError => e
117
+ raise e, "Header import failed for file #{@filename}. #{e}"
110
118
  end
111
119
 
112
120
  # deallocate the header data to save memory space.
@@ -114,37 +122,31 @@ temporary file.
114
122
  end
115
123
 
116
124
 
117
- =begin rdoc
118
- Predicate method that tells whether or not the file is actually an image. This
119
- judgement is based on whether one of the available header reading utilities can
120
- actually read the header information.
121
- =end
125
+
126
+ # Predicate method that tells whether or not the file is actually an image. This
127
+ # judgement is based on whether one of the available header reading utilities can
128
+ # actually read the header information.
122
129
  def image?
123
- return ( @hdr_reader == RDGEHDR or @hdr_reader == DICOM_HDR )
130
+ return ( VALID_HEADERS.include? @hdr_reader )
124
131
  end
125
132
 
126
133
 
127
- =begin rdoc
128
- Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
129
- =end
134
+
135
+ # Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
130
136
  def pfile?
131
137
  return @file_type == "pfile"
132
138
  end
133
139
 
134
140
 
135
- =begin rdoc
136
- Predicate simply returns true if "dicom" is stored in the img_type instance variable.
137
- =end
141
+ # Predicate simply returns true if "dicom" is stored in the img_type instance variable.
138
142
  def dicom?
139
143
  return @file_type == "dicom"
140
144
  end
141
145
 
142
146
 
143
- =begin rdoc
144
- Returns a yaml string based on a subset of the attributes. Specifically,
145
- the @hdr_data is not included. This is used to generate .yaml files that are
146
- placed in image directories for later scanning by YamlScanner.
147
- =end
147
+ # Returns a yaml string based on a subset of the attributes. Specifically,
148
+ # the @hdr_data is not included. This is used to generate .yaml files that are
149
+ # placed in image directories for later scanning by YamlScanner.
148
150
  def to_yaml
149
151
  yamlhash = {}
150
152
  instance_variables.each do |var|
@@ -154,11 +156,9 @@ placed in image directories for later scanning by YamlScanner.
154
156
  end
155
157
 
156
158
 
157
- =begin rdoc
158
- Returns the internal, parsed data fields in an array. This is used when scanning
159
- dicom slices, to compare each dicom slice in a folder and make sure they all hold the
160
- same data.
161
- =end
159
+ # Returns the internal, parsed data fields in an array. This is used when scanning
160
+ # dicom slices, to compare each dicom slice in a folder and make sure they all hold the
161
+ # same data.
162
162
  def to_array
163
163
  return [@filename,
164
164
  @timestamp,
@@ -173,11 +173,9 @@ same data.
173
173
  @acquisition_matrix_y]
174
174
  end
175
175
 
176
- =begin rdoc
177
- Returns an SQL statement to insert this image into the raw_images table of a
178
- compatible database (sqlite3). This is intended for inserting into the rails
179
- backend database.
180
- =end
176
+ # Returns an SQL statement to insert this image into the raw_images table of a
177
+ # compatible database (sqlite3). This is intended for inserting into the rails
178
+ # backend database.
181
179
  def db_insert(image_dataset_id)
182
180
  "INSERT INTO raw_image_files
183
181
  (filename, header_reader, file_type, timestamp, source, rmr_number, series_description,
@@ -189,27 +187,21 @@ backend database.
189
187
  #{@bold_reps}, '#{DateTime.now}', '#{DateTime.now}', #{image_dataset_id})"
190
188
  end
191
189
 
192
- =begin rdoc
193
- Returns an SQL statement to select this image file row from the raw_image_files table
194
- of a compatible database.
195
- =end
190
+ # Returns an SQL statement to select this image file row from the raw_image_files table
191
+ # of a compatible database.
196
192
  def db_fetch
197
193
  "SELECT *" + from_table_where + sql_match_conditions
198
194
  end
199
195
 
200
- =begin rdoc
201
- Returns and SQL statement to remove this image file from the raw_image_files table
202
- of a compatible database.
203
- =end
196
+ # Returns and SQL statement to remove this image file from the raw_image_files table
197
+ # of a compatible database.
204
198
  def db_remove
205
199
  "DELETE" + from_table_where + sql_match_conditions
206
200
  end
207
201
 
208
202
 
209
- =begin rdoc
210
- Uses the db_insert method to actually perform the database insert using the
211
- specified database file.
212
- =end
203
+ # Uses the db_insert method to actually perform the database insert using the
204
+ # specified database file.
213
205
  def db_insert!( db_file )
214
206
  db = SQLite3::Database.new( db_file )
215
207
  db.transaction do |database|
@@ -221,20 +213,16 @@ specified database file.
221
213
  db.close
222
214
  end
223
215
 
224
- =begin rdoc
225
- Removes this instance from the raw_image_files table of the specified database.
226
- =end
216
+ # Removes this instance from the raw_image_files table of the specified database.
227
217
  def db_remove!( db_file )
228
218
  db = SQLite3::Database.new( db_file )
229
219
  db.execute( db_remove )
230
220
  db.close
231
221
  end
232
222
 
233
- =begin rdoc
234
- Finds the row in the raw_image_files table of the given db file that matches this object.
235
- ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
236
- as an array of values (see 'sqlite3' gem docs).
237
- =end
223
+ # Finds the row in the raw_image_files table of the given db file that matches this object.
224
+ # ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
225
+ # as an array of values (see 'sqlite3' gem docs).
238
226
  def db_fetch!( db_file )
239
227
  db = SQLite3::Database.new( db_file )
240
228
  db_row = db.execute( db_fetch )
@@ -257,40 +245,54 @@ private
257
245
  "rmr_number = '#{@rmr_number}' AND timestamp = '#{@timestamp.to_s}' AND filename = '#{@filename}'"
258
246
  end
259
247
 
260
- =begin rdoc
261
- Reads the file header using one of the available header reading utilities.
262
- Returns both the header data as either a RubyDicom object or one big string, and the name of the utility
263
- used to read it.
264
-
265
- Note: The rdgehdr is a binary file; the correct version for your architecture must be installed in the path.
266
- =end
248
+ # Reads the file header using one of the available header reading utilities.
249
+ # Returns both the header data as either a RubyDicom object or one big string, and the name of the utility
250
+ # used to read it.
251
+ #
252
+ # Note: The rdgehdr is a binary file; the correct version for your architecture must be installed in the path.
267
253
  def read_header(absfilepath)
268
- # header = DICOM::DObject.new(absfilepath)
269
- # return [header, RUBYDICOM_HDR] if defined? header.read_success && header.read_success
270
-
271
- header = `#{DICOM_HDR} '#{absfilepath}' 2> /dev/null`
272
- #header = `#{DICOM_HDR} #{absfilepath}`
273
- if ( header.index("ERROR") == nil and
274
- header.chomp != "" and
275
- header.length > MIN_HDR_LENGTH )
276
- return [ header, DICOM_HDR ]
277
- end
278
- header = `#{RDGEHDR} '#{absfilepath}' 2> /dev/null`
279
- #header = `#{RDGEHDR} #{absfilepath}`
280
- if ( header.chomp != "" and
281
- header.length > MIN_HDR_LENGTH )
282
- return [ header, RDGEHDR ]
254
+
255
+ case File.basename(absfilepath)
256
+ when /^P.{5}\.7$|^I\..{3}/
257
+ # Try reading Pfiles or Genesis I-Files with GE's rdgehdr
258
+ @current_hdr_reader = RDGEHDR
259
+ header = `#{RDGEHDR} '#{absfilepath}' 2> /dev/null`
260
+ #header = `#{RDGEHDR} #{absfilepath}`
261
+ if ( header.chomp != "" and
262
+ header.length > MIN_HDR_LENGTH )
263
+ @current_hdr_reader = nil
264
+ return [ header, RDGEHDR ]
265
+ end
266
+ else
267
+ # Try reading with RubyDICOM
268
+ @current_hdr_reader = RUBYDICOM_HDR
269
+ header = DICOM::DObject.new(absfilepath)
270
+ if defined? header.read_success && header.read_success
271
+ @current_hdr_reader = nil
272
+ return [header, RUBYDICOM_HDR]
273
+ end
274
+
275
+ # Try reading with AFNI's dicom_hdr
276
+ @current_hdr_reader = DICOM_HDR
277
+ header = `#{DICOM_HDR} '#{absfilepath}' 2> /dev/null`
278
+ #header = `#{DICOM_HDR} #{absfilepath}`
279
+ if ( header.index("ERROR") == nil and
280
+ header.chomp != "" and
281
+ header.length > MIN_HDR_LENGTH )
282
+ @current_hdr_reader = nil
283
+ return [ header, DICOM_HDR ]
284
+ end
283
285
  end
286
+
287
+ @current_hdr_reader = nil
284
288
  return [ nil, nil ]
285
289
  end
286
290
 
287
291
 
288
- =begin rdoc
289
- Returns a string that indicates the file type. This is difficult because dicom
290
- files have no consistent naming conventions/suffixes. Here we chose to call a
291
- file a "pfile" if it is an image and the file name is of the form P*.7
292
- All other images are called "dicom".
293
- =end
292
+ # Returns a string that indicates the file type. This is difficult because dicom
293
+ # files have no consistent naming conventions/suffixes. Here we chose to call a
294
+ # file a "pfile" if it is an image and the file name is of the form P*.7
295
+ # All other images are called "dicom".
294
296
  def determine_file_type
295
297
  return "pfile" if image? and (@filename =~ /^P.....\.7/) != nil
296
298
  return "dicom" if image? and (@filename =~ /^P.....\.7/) == nil
@@ -298,10 +300,8 @@ All other images are called "dicom".
298
300
  end
299
301
 
300
302
 
301
- =begin rdoc
302
- Parses the header data and extracts a collection of instance variables. If
303
- @hdr_data and @hdr_reader are not already availables, this function does nothing.
304
- =end
303
+ # Parses the header data and extracts a collection of instance variables. If
304
+ # @hdr_data and @hdr_reader are not already available, this function does nothing.
305
305
  def import_hdr
306
306
  raise(IndexError, "No Header Data Available.") if @hdr_data == nil
307
307
  case @hdr_reader
@@ -312,17 +312,142 @@ Parses the header data and extracts a collection of instance variables. If
312
312
  end
313
313
 
314
314
 
315
- =begin rdoc
316
- Extract a collection of metadata from @hdr_data retrieved using RubyDicom
317
- =end
318
- def rubydicom_hdr_import
315
+ # Extract a collection of metadata from @hdr_data retrieved using RubyDicom
316
+ #
317
+ # Here are some example DICOM Tags and Values
318
+ # 0008,0022 Acquisition Date DA 8 20101103
319
+ # 0008,0030 Study Time TM 6 101538
320
+ # 0008,0080 Institution Name LO 4 Institution
321
+ # 0008,1010 Station Name SH 8 Station
322
+ # 0008,1030 Study Description LO 12 PILOT Study
323
+ # 0008,103E Series Description LO 12 3pl loc FGRE
324
+ # 0008,1070 Operators' Name PN 2 SP
325
+ # 0008,1090 Manufacturer's Model Name LO 16 DISCOVERY MR750
326
+ # 0010,0010 Patient's Name PN 12 mosPilot
327
+ # 0010,0020 Patient ID LO 12 RMREKKPilot
328
+ # 0010,0040 Patient's Sex CS 2 F
329
+ # 0010,1010 Patient's Age AS 4 027Y
330
+ # 0010,1030 Patient's Weight DS 4 49.9
331
+ # 0018,0023 MR Acquisition Type CS 2 2D
332
+ # 0018,0050 Slice Thickness DS 2 10
333
+ # 0018,0080 Repetition Time DS 6 5.032
334
+ # 0018,0081 Echo Time DS 6 1.396
335
+ # 0018,0082 Inversion Time DS 2 0
336
+ # 0018,0083 Number of Averages DS 2 1
337
+ # 0018,0087 Magnetic Field Strength DS 2 3
338
+ # 0018,0088 Spacing Between Slices DS 4 12.5
339
+ # 0018,0091 Echo Train Length IS 2 1
340
+ # 0018,0093 Percent Sampling DS 4 100
341
+ # 0018,0094 Percent Phase Field of View DS 4 100
342
+ # 0018,0095 Pixel Bandwidth DS 8 244.141
343
+ # 0018,1000 Device Serial Number LO 16 0000006080000
344
+ # 0018,1020 Software Version(s) LO 42 21\LX\MR Software release:20..
345
+ # 0018,1030 Protocol Name LO 22 MOSAIC Pilot 02Nov2010
346
+ # 0018,1100 Reconstruction Diameter DS 4 240
347
+ # 0018,1250 Receive Coil Name SH 8 8HRBRAIN
348
+ # 0018,1310 Acquisition Matrix US 8 0\256\128\0
349
+ # 0018,1312 In-plane Phase Encoding Direction CS 4 ROW
350
+ # 0018,1314 Flip Angle DS 2 30
351
+ # 0018,1315 Variable Flip Angle Flag CS 2 N
352
+ # 0018,1316 SAR DS 8 0.498088
353
+ # 0020,000D Study Instance UID UI 52 1.2.840.113619.6.260.4.88937..
354
+ # 0020,000E Series Instance UID UI 54 1.2.840.113619.2.260.6945.23..
355
+ # 0020,0010 Study ID SH 4 1260
356
+ # 0020,0011 Series Number IS 2 1
357
+ # 0020,0012 Acquisition Number IS 2 1
358
+ # 0020,0013 Instance Number IS 2 1
359
+ # 0020,0032 Image Position (Patient) DS 22 -119.531\-159.531\-25
360
+ # 0020,1002 Images in Acquisition IS 2 15
361
+ # 0028,0010 Rows US 2 256
362
+ # 0028,0011 Columns US 2 256
363
+ # 0028,0030 Pixel Spacing DS 14 0.9375\0.9375
364
+ def rubydicom_hdr_import
365
+ dicom_tag_attributes = {
366
+ :source => "0008,0080",
367
+ :series_description => "0008,103E",
368
+ :study_description => "0008,1030",
369
+ :operator_name => "0008,1070",
370
+ :patient_name => "0010,0010",
371
+ :rmr_number => "0010,0020",
372
+ :gender => "0010,0040",
373
+ :slice_thickness => "0018,0050",
374
+ :reconstruction_diameter => "0018,1100",
375
+ :rep_time => "0018,0080",
376
+ :pixel_spacing => "0028,0030",
377
+ :flip_angle => "0018,1314",
378
+ :field_strength => "0018,0087",
379
+ :slice_spacing => "0018,0088",
380
+ :software_version => "0018,1020",
381
+ :protocol_name => "0018,1030",
382
+ :bold_reps => "0020,0105",
383
+ :dicom_series_uid => "0020,000E",
384
+ :dicom_study_uid => "0020,000D",
385
+ :study_id => "0020,0010",
386
+ :num_slices => "0020,1002",
387
+ :acquisition_matrix_x => "0028,0010",
388
+ :acquisition_matrix_y => "0028,0011"
389
+ }
390
+
391
+
392
+ dicom_tag_attributes.each_pair do |name, tag|
393
+ begin
394
+ # next if tag_hash[:type] == :datetime
395
+ value = @hdr_data[tag].value if @hdr_data[tag]
396
+ raise ScriptError, "No match found for #{name}" unless value
397
+ instance_variable_set("@#{name.to_s}", value)
398
+ rescue ScriptError => e
399
+ @warnings << "Tag #{name} could not be found."
400
+ end
401
+ end
402
+
403
+ @timestamp = DateTime.parse(@hdr_data["0008,0022"].value + @hdr_data["0008,0030"].value)
404
+ @dicom_taghash = create_dicom_taghash(@hdr_data)
405
+ # @dicom_header = remove_long_dicom_elements(@hdr_data)
406
+
407
+ end
319
408
 
320
- end
409
+ # # Remove long data elements from a rubydicom header. This essentially strips
410
+ # # lengthy image data.
411
+ # def remove_long_dicom_elements(header)
412
+ # raise ScriptError, "A DICOM::DObject instance is required" unless header.kind_of? DICOM::DObject
413
+ # h = header.dup
414
+ # h.children.select { |element| element.length > 100 }.each do |e|
415
+ # h.remove(e.tag)
416
+ # # puts "Removing #{e.tag}..."
417
+ # end
418
+ # return h
419
+ # end
420
+
421
+ # Create a super-lightweight representation of the DICOM header as a hash, where
422
+ # the tags are they keys and name and value are stored as an attribute hash in value.
423
+ #
424
+ # Creates a hash like:
425
+ # {"0018,0095"=>{:value=>"244.141", :name=>"Pixel Bandwidth"},
426
+ # "0008,1030"=>{:value=>"MOSAIC PILOT", :name=>"Study Description"} }
427
+ #
428
+ # When serialized with yaml, this looks like:
429
+ #
430
+ # 0018,0095:
431
+ # :value: "244.141"
432
+ # :name: Pixel Bandwidth
433
+ #
434
+ # 0008,1030:
435
+ # :value: MOSAIC PILOT
436
+ # :name: Study Description
437
+ #
438
+ # To filter and search, you can do something like:
439
+ # tag_hash.each_pair {|tag, attributes| puts tag, attributes[:value] if attributes[:name] =~ /Description/i }
440
+ def create_dicom_taghash(header)
441
+ raise ScriptError, "A DICOM::DObject instance is required" unless header.kind_of? DICOM::DObject
442
+ h = Hash.new
443
+ header.children.each do |element|
444
+ h[element.tag] = {:value => element.instance_variable_get(:@value), :name => element.name}
445
+ end
446
+ return h
447
+ end
321
448
 
322
- =begin rdoc
323
- Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
324
- utility.
325
- =end
449
+ # Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
450
+ # utility.
326
451
  def dicom_hdr_import
327
452
  dicom_tag_templates = {}
328
453
  dicom_tag_templates[:rmr_number] = {
@@ -431,10 +556,8 @@ utility.
431
556
  end
432
557
 
433
558
 
434
- =begin rdoc
435
- Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
436
- utility.
437
- =end
559
+ # Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
560
+ # utility.
438
561
  def rdgehdr_import
439
562
  source_pat = /hospital [Nn]ame: ([[:graph:]\t ]+)/i
440
563
  num_slices_pat = /Number of slices in this scan group: ([0-9]+)/i