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 +21 -17
- data/VERSION +1 -1
- data/bin/list_visit +18 -9
- data/lib/metamri/core_additions.rb +32 -10
- data/lib/metamri/image_dataset_quality_check_resource.rb +26 -0
- data/lib/metamri/raw_image_dataset.rb +76 -32
- data/lib/metamri/raw_image_dataset_resource.rb +5 -1
- data/lib/metamri/raw_image_dataset_thumbnail.rb +25 -6
- data/lib/metamri/raw_image_file.rb +242 -119
- data/lib/metamri/visit_raw_data_directory.rb +29 -22
- data/metamri.gemspec +51 -45
- data/{test → spec}/helper_spec.rb +0 -2
- data/{test → spec/unit}/nifti_builder_spec.rb +0 -0
- data/spec/unit/raw_image_dataset_spec.rb +35 -0
- data/{test → spec/unit}/raw_image_dataset_thumbnail_spec.rb +18 -12
- data/spec/unit/raw_image_file_spec.rb +45 -0
- data/test/fixtures/s03_bravo.0156.yml +937 -0
- data/test/raw_image_file_test.rb +13 -12
- data/test/test_helper.rb +2 -0
- metadata +22 -15
- data/.gitignore +0 -7
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
|
-
|
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
|
+
0.2.0
|
data/bin/list_visit
CHANGED
@@ -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
|
46
|
-
input_directories =
|
45
|
+
unless ARGV.length >= 1
|
46
|
+
input_directories = [Dir.pwd]
|
47
47
|
else
|
48
|
-
input_directories = ARGV
|
48
|
+
input_directories = ARGV
|
49
49
|
end
|
50
50
|
|
51
|
-
input_directories.
|
52
|
-
|
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
|
-
|
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 ==
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
7
|
-
A #
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
*
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
81
|
-
Generates an SQL insert statement for this dataset that can be used to
|
82
|
-
the Johnson Lab rails TransferScans application database backend.
|
83
|
-
for this is that many dataset inserts can be collected into one db
|
84
|
-
at the visit level, or even higher when doing a whole file system
|
85
|
-
|
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
|
-
|
195
|
-
Returns a globbing wildcard that is used by to3D to gather files for
|
196
|
-
reconstruction.
|
197
|
-
This is always the case for pfiles. For example if the first file in
|
198
|
-
|
199
|
-
<tt
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
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
|
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
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
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
|
-
|
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
|
-
|
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
|
69
|
-
attr_reader :
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
108
|
-
rescue
|
109
|
-
raise
|
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
|
-
|
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 (
|
130
|
+
return ( VALID_HEADERS.include? @hdr_reader )
|
124
131
|
end
|
125
132
|
|
126
133
|
|
127
|
-
|
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
|
-
|
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
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
193
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
210
|
-
|
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
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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
|
-
|
302
|
-
|
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
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
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
|
-
|
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
|
-
|
323
|
-
|
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
|
-
|
435
|
-
|
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
|