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 +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
|