metamri 0.1.23 → 0.2.0

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