metamri 0.2.9 → 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/import_study.rb +7 -0
- data/lib/metamri.rb +1 -0
- data/lib/metamri/raw_image_dataset.rb +20 -7
- data/lib/metamri/raw_image_file.rb +25 -8
- data/lib/metamri/version.rb +1 -1
- data/lib/metamri/visit_raw_data_directory.rb +39 -26
- data/spec/unit/raw_image_dataset_spec.rb +1 -1
- metadata +4 -4
data/bin/import_study.rb
CHANGED
@@ -75,6 +75,11 @@ STUDIES = {
|
|
75
75
|
:filter => /^pd..._/,
|
76
76
|
:codename => 'gallagher.pd.visit1'
|
77
77
|
},
|
78
|
+
:pc_4000 => { :dir => '/Data/vtrak1/raw/pc_4000',
|
79
|
+
:logfile => 'pc_4000.scan.log',
|
80
|
+
:filter => /^pc001/,
|
81
|
+
:codename => 'johnson.pc4000.visit1'
|
82
|
+
},
|
78
83
|
:pib_pilot => { :dir => '/Data/vtrak1/raw/pib_pilot_mri',
|
79
84
|
:logfile => 'pib.mri.pilot.scan.log',
|
80
85
|
:filter => /^cpr0/,
|
@@ -148,6 +153,8 @@ def import_study(study, dbfile)
|
|
148
153
|
puts "Exception message: #{e.message}"
|
149
154
|
log.error "There was a problem scanning a dataset in #{visitdir}... skipping."
|
150
155
|
log.error "Exception message: #{e.message}"
|
156
|
+
log.error e.backtrace
|
157
|
+
raise e
|
151
158
|
ensure
|
152
159
|
v = nil
|
153
160
|
end
|
data/lib/metamri.rb
CHANGED
@@ -21,7 +21,7 @@ class RawImageDataset
|
|
21
21
|
# From the first raw image file in the dataset
|
22
22
|
attr_reader :timestamp
|
23
23
|
# From the first raw image file in the dataset
|
24
|
-
attr_reader :
|
24
|
+
attr_reader :exam_number
|
25
25
|
# A key string unique to a dataset composed of the rmr number and the timestamp.
|
26
26
|
attr_reader :dataset_key
|
27
27
|
# the file scanned
|
@@ -86,8 +86,8 @@ class RawImageDataset
|
|
86
86
|
@scanner_source = @raw_image_files.first.source
|
87
87
|
raise(IndexError, "No scanner source found") if @scanner_source.nil?
|
88
88
|
|
89
|
-
@
|
90
|
-
|
89
|
+
@exam_number = @raw_image_files.first.exam_number.nil? ? nil : @raw_image_files.first.exam_number
|
90
|
+
validates_metainfo_for :exam_number, :msg => "No study id / exam number found", :optional => true
|
91
91
|
|
92
92
|
@study_description = @raw_image_files.first.study_description
|
93
93
|
validates_metainfo_for :study_description, :msg => "No study description found" if dicom?
|
@@ -109,7 +109,10 @@ class RawImageDataset
|
|
109
109
|
|
110
110
|
@dicom_taghash = @raw_image_files.first.dicom_taghash
|
111
111
|
validates_metainfo_for :dicom_taghash if dicom?
|
112
|
-
|
112
|
+
|
113
|
+
@image_uid = @raw_image_files.first.image_uid
|
114
|
+
validates_metainfo_for :image_uid if pfile?
|
115
|
+
|
113
116
|
$LOG ||= Logger.new(STDOUT)
|
114
117
|
end
|
115
118
|
|
@@ -132,10 +135,10 @@ class RawImageDataset
|
|
132
135
|
def db_insert(visit_id)
|
133
136
|
"INSERT INTO image_datasets
|
134
137
|
(rmr, series_description, path, timestamp, created_at, updated_at, visit_id,
|
135
|
-
glob, rep_time, bold_reps, slices_per_volume, scanned_file)
|
138
|
+
glob, rep_time, bold_reps, slices_per_volume, scanned_file, 'dicom_study_uid')
|
136
139
|
VALUES ('#{@rmr_number}', '#{@series_description}', '#{@directory}', '#{@timestamp.to_s}', '#{DateTime.now}',
|
137
140
|
'#{DateTime.now}', '#{visit_id}', '#{self.glob}', '#{@raw_image_files.first.rep_time}',
|
138
|
-
'#{@raw_image_files.first.bold_reps}', '#{@raw_image_files.first.num_slices}', '#{@scanned_file}')"
|
141
|
+
'#{@raw_image_files.first.bold_reps}', '#{@raw_image_files.first.num_slices}', '#{@scanned_file}' )"
|
139
142
|
end
|
140
143
|
|
141
144
|
def db_update(dataset_id)
|
@@ -186,7 +189,8 @@ class RawImageDataset
|
|
186
189
|
:slices_per_volume => @raw_image_files.first.num_slices,
|
187
190
|
:scanned_file => @scanned_file,
|
188
191
|
:dicom_series_uid => @dicom_series_uid,
|
189
|
-
:dicom_taghash => @dicom_taghash
|
192
|
+
:dicom_taghash => @dicom_taghash,
|
193
|
+
:image_uid => @image_uid
|
190
194
|
}.merge attrs
|
191
195
|
end
|
192
196
|
|
@@ -361,6 +365,15 @@ private
|
|
361
365
|
def directory_basename
|
362
366
|
File.basename(@directory)
|
363
367
|
end
|
368
|
+
|
369
|
+
# Metamri will return both dicom_series_uid and image_uid for ActiveRecord
|
370
|
+
# Correct UID choosing will happen in the Panda.
|
371
|
+
# But, here's a shortcut to a pfile?/dicom? ternary for correct uid for dataset
|
372
|
+
def dataset_uid
|
373
|
+
@raw_image_files.first.dicom_series_uid ?
|
374
|
+
@raw_image_files.first.dicom_series_uid : @raw_image_files.first.image_uid
|
375
|
+
end
|
376
|
+
|
364
377
|
|
365
378
|
# Ensure that metadata is present in instance variables.
|
366
379
|
#
|
@@ -41,7 +41,7 @@ class RawImageFile
|
|
41
41
|
# An identifier unique to a 'visit', these are assigned by the scanner techs at scan time.
|
42
42
|
attr_reader :rmr_number
|
43
43
|
# An identifier unique to a Study Session - AKA Exam Number
|
44
|
-
attr_reader :
|
44
|
+
attr_reader :exam_number
|
45
45
|
# A short string describing the acquisition sequence. These come from the scanner.
|
46
46
|
# code and are used to initialise SeriesDescription objects to find related attributes.
|
47
47
|
attr_reader :series_description
|
@@ -73,6 +73,8 @@ class RawImageFile
|
|
73
73
|
attr_reader :dicom_header
|
74
74
|
# Hash of all DICOM Tags including their Names and Values (See #dicom_taghash for more information on the structure)
|
75
75
|
attr_reader :dicom_taghash
|
76
|
+
# DICOM SOP Instance UID (from the scanned file)
|
77
|
+
attr_reader :dicom_image_uid
|
76
78
|
# DICOM Series UID
|
77
79
|
attr_reader :dicom_series_uid
|
78
80
|
# DICOM Study UID
|
@@ -235,8 +237,16 @@ class RawImageFile
|
|
235
237
|
return db_row
|
236
238
|
end
|
237
239
|
|
238
|
-
|
239
|
-
|
240
|
+
# The series ID (dicom_series_uid [dicom] or series_uid [pfile/ifile])
|
241
|
+
# This is unique for DICOM datasets, but not for PFiles
|
242
|
+
def series_uid
|
243
|
+
@dicom_series_uid || @series_uid
|
244
|
+
end
|
245
|
+
|
246
|
+
# The UID unique to the raw image file scanned
|
247
|
+
def image_uid
|
248
|
+
@dicom_image_uid || @image_uid
|
249
|
+
end
|
240
250
|
|
241
251
|
private
|
242
252
|
|
@@ -325,6 +335,7 @@ private
|
|
325
335
|
# 0008,0030 Study Time TM 6 101538
|
326
336
|
# 0008,0080 Institution Name LO 4 Institution
|
327
337
|
# 0008,1010 Station Name SH 8 Station
|
338
|
+
# 0008,0018 SOP Instance UID 12 1.2.840.113619.2.155.3596.6906438.17031.1121881958.942
|
328
339
|
# 0008,1030 Study Description LO 12 PILOT Study
|
329
340
|
# 0008,103E Series Description LO 12 3pl loc FGRE
|
330
341
|
# 0008,1070 Operators' Name PN 2 SP
|
@@ -386,9 +397,10 @@ private
|
|
386
397
|
:software_version => "0018,1020",
|
387
398
|
:protocol_name => "0018,1030",
|
388
399
|
:bold_reps => "0020,0105",
|
389
|
-
:
|
400
|
+
:dicom_image_uid => "0008,0018", # Each DICOM Image (i.e. raw image file) has a unique SOP Instance UID
|
401
|
+
:dicom_series_uid => "0020,000E", # Series UID (shared by all dicoms in the same series)
|
390
402
|
:dicom_study_uid => "0020,000D",
|
391
|
-
:
|
403
|
+
:exam_number => "0020,0010",
|
392
404
|
:num_slices => "0020,1002",
|
393
405
|
:acquisition_matrix_x => "0028,0010",
|
394
406
|
:acquisition_matrix_y => "0028,0011"
|
@@ -461,7 +473,7 @@ private
|
|
461
473
|
:pat => /[ID Accession Number|ID Study Description]\/\/(RMR.*)\n/i,
|
462
474
|
:required => true
|
463
475
|
}
|
464
|
-
dicom_tag_templates[:
|
476
|
+
dicom_tag_templates[:exam_number] = {
|
465
477
|
:type => :string,
|
466
478
|
:pat => /STUDY ID\/\/([0-9]+)/i,
|
467
479
|
:required => true
|
@@ -580,6 +592,7 @@ private
|
|
580
592
|
rep_time_pat = /Pulse repetition time \(usec\): ([0-9]+)/i
|
581
593
|
study_uid_pat = /Study entity unique ID: ([[:graph:]]+)/i
|
582
594
|
series_uid_pat = /Series entity unique ID: ([[:graph:]]+)/i
|
595
|
+
image_uid_pat = /Image unique ID: ([[:graph:]]+)/i
|
583
596
|
|
584
597
|
rmr_number_pat =~ @hdr_data
|
585
598
|
@rmr_number = ($1).nil? ? "rmr not found" : ($1).strip.chomp
|
@@ -620,10 +633,14 @@ private
|
|
620
633
|
@rep_time = ($1).to_f / 1000000
|
621
634
|
|
622
635
|
study_uid_pat =~ @hdr_data
|
623
|
-
@
|
636
|
+
@study_uid = ($1).strip.chomp unless $1.nil?
|
624
637
|
|
625
638
|
series_uid_pat =~ @hdr_data
|
626
|
-
@
|
639
|
+
@series_uid = ($1).strip.chomp unless $1.nil?
|
640
|
+
|
641
|
+
image_uid_pat =~ @hdr_data
|
642
|
+
@image_uid = ($1).strip.chomp unless $1.nil?
|
643
|
+
|
627
644
|
end
|
628
645
|
|
629
646
|
end
|
data/lib/metamri/version.rb
CHANGED
@@ -4,7 +4,7 @@ require 'tempfile'
|
|
4
4
|
require 'yaml'
|
5
5
|
require 'tmpdir'
|
6
6
|
require 'fileutils'
|
7
|
-
|
7
|
+
require 'sqlite3'
|
8
8
|
require 'logger'
|
9
9
|
require 'pp'
|
10
10
|
require 'metamri/raw_image_file'
|
@@ -49,7 +49,7 @@ class VisitRawDataDirectory
|
|
49
49
|
# scanner source
|
50
50
|
attr_accessor :scanner_source
|
51
51
|
# scanner-defined study id / exam number
|
52
|
-
attr_accessor :
|
52
|
+
attr_accessor :exam_number
|
53
53
|
#
|
54
54
|
attr_accessor :db
|
55
55
|
# Scan ID is the short name for the scan (tbiva018, tbiva018b)
|
@@ -67,7 +67,7 @@ class VisitRawDataDirectory
|
|
67
67
|
# A new Visit instance needs to know the path to its raw data and scan_procedure name. The scan_procedure
|
68
68
|
# name must match a name in the database, if not a new scan_procedure entry will be inserted.
|
69
69
|
def initialize(directory, scan_procedure_name=nil)
|
70
|
-
raise(IOError, "Visit directory not found: #{directory}") unless File.
|
70
|
+
raise(IOError, "Visit directory not found: #{directory}") unless File.directory? File.expand_path(directory)
|
71
71
|
@visit_directory = File.expand_path(directory)
|
72
72
|
@working_directory = Dir.tmpdir
|
73
73
|
@datasets = Array.new
|
@@ -75,7 +75,7 @@ class VisitRawDataDirectory
|
|
75
75
|
@rmr_number = nil
|
76
76
|
@scan_procedure_name = scan_procedure_name.nil? ? get_scan_procedure_based_on_raw_directory : scan_procedure_name
|
77
77
|
@db = nil
|
78
|
-
@
|
78
|
+
@exam_number = nil
|
79
79
|
initialize_log
|
80
80
|
end
|
81
81
|
|
@@ -84,8 +84,10 @@ class VisitRawDataDirectory
|
|
84
84
|
# @datasets will hold an array of ImageDataset instances. Setting the rmr here can raise an
|
85
85
|
# exception if no valid rmr is found in the datasets, be prepared to catch it.
|
86
86
|
#
|
87
|
-
#
|
88
|
-
#
|
87
|
+
# === Options
|
88
|
+
#
|
89
|
+
# * <tt>:ignore_patterns</tt> -- Array of Regex'es. An array of Regular Expressions that will be used to skip heavy directories.
|
90
|
+
#
|
89
91
|
def scan(options = {})
|
90
92
|
flash "Scanning visit raw data directory #{@visit_directory}" if $LOG.level <= Logger::INFO
|
91
93
|
default_options = {:ignore_patterns => []}
|
@@ -110,8 +112,8 @@ class VisitRawDataDirectory
|
|
110
112
|
@timestamp = get_visit_timestamp
|
111
113
|
@rmr_number = get_rmr_number
|
112
114
|
@scanner_source = get_scanner_source
|
113
|
-
@
|
114
|
-
@
|
115
|
+
@exam_number = get_exam_number
|
116
|
+
@study_uid = get_study_uid unless dicom_datasets.empty?
|
115
117
|
flash "Completed scanning #{@visit_directory}" if $LOG.level <= Logger::DEBUG
|
116
118
|
else
|
117
119
|
raise(IndexError, "No datasets could be scanned for directory #{@visit_directory}")
|
@@ -125,8 +127,8 @@ class VisitRawDataDirectory
|
|
125
127
|
:rmr => @rmr_number,
|
126
128
|
:path => @visit_directory,
|
127
129
|
:scanner_source => @scanner_source ||= get_scanner_source,
|
128
|
-
:scan_number => @
|
129
|
-
:dicom_study_uid => @
|
130
|
+
:scan_number => @exam_number,
|
131
|
+
:dicom_study_uid => @study_uid
|
130
132
|
}
|
131
133
|
end
|
132
134
|
|
@@ -161,6 +163,7 @@ class VisitRawDataDirectory
|
|
161
163
|
end
|
162
164
|
rescue Exception => e
|
163
165
|
puts e.message
|
166
|
+
puts e.backtrace
|
164
167
|
ensure
|
165
168
|
@db.close
|
166
169
|
@db = nil
|
@@ -203,7 +206,7 @@ Returns an array of the created nifti files.
|
|
203
206
|
def to_s
|
204
207
|
puts; @visit_directory.length.times { print "-" }; puts
|
205
208
|
puts "#{@visit_directory}"
|
206
|
-
puts "#{@rmr_number} - #{@timestamp.strftime('%F')} - #{@scanner_source} - #{@
|
209
|
+
puts "#{@rmr_number} - #{@timestamp.strftime('%F')} - #{@scanner_source} - #{@exam_number unless @exam_number.nil?}"
|
207
210
|
puts
|
208
211
|
puts RawImageDataset.to_table(@datasets)
|
209
212
|
return
|
@@ -260,18 +263,24 @@ Returns an array of the created nifti files.
|
|
260
263
|
end
|
261
264
|
|
262
265
|
def insert_new_visit(p_id)
|
263
|
-
puts sql_insert_visit
|
264
|
-
@db.execute(sql_insert_visit
|
265
|
-
|
266
|
+
puts sql_insert_visit
|
267
|
+
@db.execute(sql_insert_visit)
|
268
|
+
visit_id = @db.last_insert_row_id
|
269
|
+
puts sql_insert_scan_procedures_visits(p_id, visit_id)
|
270
|
+
@db.execute(sql_insert_scan_procedures_visits(p_id, visit_id))
|
271
|
+
return visit_id
|
266
272
|
end
|
267
273
|
|
268
274
|
def get_existing_visit_id
|
269
275
|
return @db.execute(sql_fetch_visit_matches).first['id']
|
270
276
|
end
|
271
277
|
|
278
|
+
# ScanProcedure now in a separate table
|
279
|
+
# Ignore it for now. BAD!
|
280
|
+
# Note: wtf
|
272
281
|
def update_existing_visit(v_id, p_id)
|
273
|
-
puts sql_update_visit(v_id
|
274
|
-
@db.execute(sql_update_visit(v_id
|
282
|
+
puts sql_update_visit(v_id)
|
283
|
+
@db.execute(sql_update_visit(v_id))
|
275
284
|
end
|
276
285
|
|
277
286
|
def fetch_or_insert_scan_procedure
|
@@ -284,12 +293,11 @@ Returns an array of the created nifti files.
|
|
284
293
|
return scan_procedure_matches.empty? ? new_scan_procedure_id : scan_procedure_matches.first['id']
|
285
294
|
end
|
286
295
|
|
287
|
-
def sql_update_visit(v_id
|
296
|
+
def sql_update_visit(v_id)
|
288
297
|
"UPDATE visits SET
|
289
298
|
date = '#{@timestamp.to_s}',
|
290
299
|
rmr = '#{@rmr_number}',
|
291
300
|
path = '#{@visit_directory}',
|
292
|
-
scan_procedure_id = '#{p_id.to_s}',
|
293
301
|
scanner_source = '#{@scanner_source}'
|
294
302
|
WHERE id = '#{v_id}'"
|
295
303
|
end
|
@@ -298,6 +306,10 @@ Returns an array of the created nifti files.
|
|
298
306
|
"INSERT INTO scan_procedures (codename) VALUES ('#{@scan_procedure_name}')"
|
299
307
|
end
|
300
308
|
|
309
|
+
def sql_insert_scan_procedures_visits(scan_procedure_id, visit_id)
|
310
|
+
"INSERT INTO scan_procedures_visits (scan_procedure_id, visit_id) VALUES('#{scan_procedure_id}', '#{visit_id}')"
|
311
|
+
end
|
312
|
+
|
301
313
|
def sql_insert_series_description(sd)
|
302
314
|
"INSERT INTO series_descriptions (long_description) VALUES ('#{sd}')"
|
303
315
|
end
|
@@ -307,7 +319,7 @@ Returns an array of the created nifti files.
|
|
307
319
|
end
|
308
320
|
|
309
321
|
def sql_fetch_scan_procedure_name
|
310
|
-
"SELECT * FROM scan_procedures WHERE codename = '#{@scan_procedure_name}'"
|
322
|
+
"SELECT * FROM scan_procedures WHERE codename = '#{@scan_procedure_name}' LIMIT 1"
|
311
323
|
end
|
312
324
|
|
313
325
|
def sql_fetch_series_description(sd)
|
@@ -320,13 +332,13 @@ Returns an array of the created nifti files.
|
|
320
332
|
|
321
333
|
|
322
334
|
# generates an sql insert statement to insert this visit with a given participant id
|
323
|
-
def sql_insert_visit
|
335
|
+
def sql_insert_visit
|
324
336
|
"INSERT INTO visits
|
325
|
-
(date,
|
337
|
+
(date, scan_number, initials, rmr, radiology_outcome, notes, transfer_mri, transfer_pet,
|
326
338
|
conference, compile_folder, dicom_dvd, user_id, path, scanner_source, created_at, updated_at)
|
327
339
|
VALUES
|
328
|
-
('#{@timestamp.to_s}', '
|
329
|
-
'no', 'no', '
|
340
|
+
('#{@timestamp.to_s}', '', '', '#{@rmr_number}', 'no', '', 'yes', 'no',
|
341
|
+
'no', 'no', '', NULL, '#{@visit_directory}', '#{@scanner_source}', '#{DateTime.now}', '#{DateTime.now}')"
|
330
342
|
end
|
331
343
|
|
332
344
|
# Build a new RawImageDataset from a path to the rawfile and parent directory.
|
@@ -386,6 +398,7 @@ Returns an array of the created nifti files.
|
|
386
398
|
|
387
399
|
# retrieves a scanner source from the collection of datasets, raises Exception of none is found
|
388
400
|
def get_scanner_source
|
401
|
+
raise IOError, "No datasets available, can't look for a scanner source" if @datasets.empty?
|
389
402
|
@datasets.each do |ds|
|
390
403
|
return ds.scanner_source unless ds.scanner_source.nil?
|
391
404
|
end
|
@@ -393,15 +406,15 @@ Returns an array of the created nifti files.
|
|
393
406
|
end
|
394
407
|
|
395
408
|
# retrieves exam number / scan id from first #RawImageDataset
|
396
|
-
def
|
409
|
+
def get_exam_number
|
397
410
|
@datasets.each do |ds|
|
398
|
-
return ds.
|
411
|
+
return ds.exam_number unless ds.exam_number.nil?
|
399
412
|
end
|
400
413
|
# raise(IOError, "No valid study id / exam number found.")
|
401
414
|
end
|
402
415
|
|
403
416
|
# retrieves exam number / scan id from first #RawImageDataset
|
404
|
-
def
|
417
|
+
def get_study_uid
|
405
418
|
@datasets.each do |ds|
|
406
419
|
return ds.dicom_study_uid unless ds.dicom_study_uid.nil?
|
407
420
|
end
|
@@ -22,7 +22,7 @@ describe RawImageDataset, "for a single valid DICOM file" do
|
|
22
22
|
ds.scanned_file.should == @valid_dicom_basename
|
23
23
|
ds.scanner_source.should == "Institution"
|
24
24
|
ds.series_description.should == "Ax FSPGR BRAVO T1"
|
25
|
-
ds.
|
25
|
+
ds.exam_number.should == "1405"
|
26
26
|
ds.timestamp.should == DateTime.parse("Wed, 10 Nov 2010 00:00:00 +0000")
|
27
27
|
ds.study_description.should == "RMRMABRAVOTEST"
|
28
28
|
ds.dicom?.should be true
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: metamri
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 3
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 2
|
9
|
-
-
|
10
|
-
version: 0.2.
|
9
|
+
- 10
|
10
|
+
version: 0.2.10
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Kristopher J. Kosmatka
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2011-08-
|
19
|
+
date: 2011-08-26 00:00:00 -05:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|