lib/metamri/raw_image_file.rb in metamri-0.1.23 vs lib/metamri/raw_image_file.rb in metamri-0.2.0
- old
+ new
@@ -2,25 +2,27 @@
require 'rubygems';
require 'yaml';
require 'sqlite3';
require 'dicom'
-=begin rdoc
-Implements a collection of metadata associated with a raw image file. In
-this case, by image we mean one single file. For the case of Pfiles one file
-corresponds to a complete 4D data set. For dicoms one file corresponds to a single
-2D slice, many of which are assembled later during reconstruction to create a
-4D data set. The motivation for this class is to provide access to the metadata
-stored in image file headers so that they can be later reconstructed into nifti
-data sets.
-=end
+
+# Implements a collection of metadata associated with a raw image file. In
+# this case, by image we mean one single file. For the case of Pfiles one file
+# corresponds to a complete 4D data set. For dicoms one file corresponds to a single
+# 2D slice, many of which are assembled later during reconstruction to create a
+# 4D data set. The motivation for this class is to provide access to the metadata
+# stored in image file headers so that they can be later reconstructed into nifti
+# data sets.
+#
+# Primarily used to instantiate a #RawImageDataset
class RawImageFile
#:stopdoc:
MIN_HDR_LENGTH = 400
DICOM_HDR = "dicom_hdr"
RDGEHDR = "rdgehdr"
RUBYDICOM_HDR = "rubydicom"
+ VALID_HEADERS = [DICOM_HDR, RDGEHDR, RUBYDICOM_HDR]
MONTHS = {
:jan => "01", :feb => "02", :mar => "03", :apr => "04", :may => "05",
:jun => "06", :jul => "07", :aug => "08", :sep => "09", :oct => "10",
:nov => "11", :dec => "12"
}
@@ -41,10 +43,14 @@
# An identifier unique to a Study Session - AKA Exam Number
attr_reader :study_id
# A short string describing the acquisition sequence. These come from the scanner.
# code and are used to initialise SeriesDescription objects to find related attributes.
attr_reader :series_description
+ # A short string describing the study sequence. These come from the scanner.
+ attr_reader :study_description
+ # A short string describing the study protocol. These come from the scanner.
+ attr_reader :protocol_name
# M or F.
attr_reader :gender
# Number of slices in the data set that includes this file, used by AFNI for reconstruction.
attr_reader :num_slices
# Given in millimeters.
@@ -63,27 +69,29 @@
attr_reader :bold_reps
# Import Warnings - Fields that could not be read.
attr_reader :warnings
# Serialized RubyDicomHeader Object (for DICOMs only)
attr_reader :dicom_header
- # DICOM Sequence UID
- attr_reader :dicom_sequence_uid
+ # Hash of all DICOM Tags including their Names and Values (See #dicom_taghash for more information on the structure)
+ attr_reader :dicom_taghash
# DICOM Series UID
attr_reader :dicom_series_uid
# DICOM Study UID
attr_reader :dicom_study_uid
-
-=begin rdoc
-Creates a new instance of the class given a path to a valid image file.
-
-Throws IOError if the file given is not found or if the available header reading
-utilities cannot read the image header. Also raises IOError if any of the
-attributes cannot be found in the header. Be aware that the filename used to
-initialize your instance is used to set the "file" attribute. If you need to
-unzip a file to a temporary location, be sure to keep the same filename for the
-temporary file.
-=end
+ # Scan Tech Initials
+ attr_reader :operator_name
+ # Patient "Name", usually StudyID or ENUM
+ attr_reader :patient_name
+
+ # Creates a new instance of the class given a path to a valid image file.
+ #
+ # Throws IOError if the file given is not found or if the available header reading
+ # utilities cannot read the image header. Also raises IOError if any of the
+ # attributes cannot be found in the header. Be aware that the filename used to
+ # initialize your instance is used to set the "file" attribute. If you need to
+ # unzip a file to a temporary location, be sure to keep the same filename for the
+ # temporary file.
def initialize(pathtofile)
# raise an error if the file doesn't exist
absfilepath = File.expand_path(pathtofile)
raise(IOError, "File not found at #{absfilepath}.") if not File.exists?(absfilepath)
@filename = File.basename(absfilepath)
@@ -91,76 +99,68 @@
# try to read the header, raise an IOError if unsuccessful
begin
@hdr_data, @hdr_reader = read_header(absfilepath)
rescue Exception => e
- raise(IOError, "Header not readable for file #{@filename}. #{e}")
+ raise(IOError, "Header not readable for file #{@filename} using #{@current_hdr_reader ? @current_hdr_reader : "unknown header reader."}. #{e}")
end
# file type is based on file name but only if the header was read successfully
@file_type = determine_file_type
# try to import attributes from the header, raise an ioerror if any attributes
# are not found
begin
import_hdr
- rescue ScriptError => e
- raise ScriptError, "Could not find required DICOM Header Meta Element: #{e}"
- rescue Exception => e
- raise IOError, "Header import failed for file #{@filename}. #{e}"
+ rescue ScriptError, NoMethodError => e
+ raise IOError, "Could not find required DICOM Header Meta Element: #{e}"
+ rescue StandardError => e
+ raise e, "Header import failed for file #{@filename}. #{e}"
end
# deallocate the header data to save memory space.
@hdr_data = nil
end
-=begin rdoc
-Predicate method that tells whether or not the file is actually an image. This
-judgement is based on whether one of the available header reading utilities can
-actually read the header information.
-=end
+
+ # Predicate method that tells whether or not the file is actually an image. This
+ # judgement is based on whether one of the available header reading utilities can
+ # actually read the header information.
def image?
- return ( @hdr_reader == RDGEHDR or @hdr_reader == DICOM_HDR )
+ return ( VALID_HEADERS.include? @hdr_reader )
end
-=begin rdoc
-Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
-=end
+
+ # Predicate simply returns true if "pfile" is stored in the @img_type instance variable.
def pfile?
return @file_type == "pfile"
end
-=begin rdoc
-Predicate simply returns true if "dicom" is stored in the img_type instance variable.
-=end
+ # Predicate simply returns true if "dicom" is stored in the img_type instance variable.
def dicom?
return @file_type == "dicom"
end
-=begin rdoc
-Returns a yaml string based on a subset of the attributes. Specifically,
-the @hdr_data is not included. This is used to generate .yaml files that are
-placed in image directories for later scanning by YamlScanner.
-=end
+ # Returns a yaml string based on a subset of the attributes. Specifically,
+ # the @hdr_data is not included. This is used to generate .yaml files that are
+ # placed in image directories for later scanning by YamlScanner.
def to_yaml
yamlhash = {}
instance_variables.each do |var|
yamlhash[var[1..-1]] = instance_variable_get(var) if (var != "@hdr_data")
end
return yamlhash.to_yaml
end
-=begin rdoc
-Returns the internal, parsed data fields in an array. This is used when scanning
-dicom slices, to compare each dicom slice in a folder and make sure they all hold the
-same data.
-=end
+ # Returns the internal, parsed data fields in an array. This is used when scanning
+ # dicom slices, to compare each dicom slice in a folder and make sure they all hold the
+ # same data.
def to_array
return [@filename,
@timestamp,
@source,
@rmr_number,
@@ -171,15 +171,13 @@
@reconstruction_diameter,
@acquisition_matrix_x,
@acquisition_matrix_y]
end
-=begin rdoc
-Returns an SQL statement to insert this image into the raw_images table of a
-compatible database (sqlite3). This is intended for inserting into the rails
-backend database.
-=end
+ # Returns an SQL statement to insert this image into the raw_images table of a
+ # compatible database (sqlite3). This is intended for inserting into the rails
+ # backend database.
def db_insert(image_dataset_id)
"INSERT INTO raw_image_files
(filename, header_reader, file_type, timestamp, source, rmr_number, series_description,
gender, num_slices, slice_thickness, slice_spacing, reconstruction_diameter,
acquisition_matrix_x, acquisition_matrix_y, rep_time, bold_reps, created_at, updated_at, image_dataset_id)
@@ -187,31 +185,25 @@
'#{@series_description}', '#{@gender}', #{@num_slices}, #{@slice_thickness}, #{@slice_spacing},
#{@reconstruction_diameter}, #{@acquisition_matrix_x}, #{@acquisition_matrix_y}, #{@rep_time},
#{@bold_reps}, '#{DateTime.now}', '#{DateTime.now}', #{image_dataset_id})"
end
-=begin rdoc
-Returns an SQL statement to select this image file row from the raw_image_files table
-of a compatible database.
-=end
+ # Returns an SQL statement to select this image file row from the raw_image_files table
+ # of a compatible database.
def db_fetch
"SELECT *" + from_table_where + sql_match_conditions
end
-=begin rdoc
-Returns and SQL statement to remove this image file from the raw_image_files table
-of a compatible database.
-=end
+ # Returns and SQL statement to remove this image file from the raw_image_files table
+ # of a compatible database.
def db_remove
"DELETE" + from_table_where + sql_match_conditions
end
-=begin rdoc
-Uses the db_insert method to actually perform the database insert using the
-specified database file.
-=end
+ # Uses the db_insert method to actually perform the database insert using the
+ # specified database file.
def db_insert!( db_file )
db = SQLite3::Database.new( db_file )
db.transaction do |database|
if not database.execute( db_fetch ).empty?
raise(IndexError, "Entry exists for #{filename}, #{@rmr_number}, #{@timestamp.to_s}... Skipping.")
@@ -219,24 +211,20 @@
database.execute( db_insert )
end
db.close
end
-=begin rdoc
-Removes this instance from the raw_image_files table of the specified database.
-=end
+ # Removes this instance from the raw_image_files table of the specified database.
def db_remove!( db_file )
db = SQLite3::Database.new( db_file )
db.execute( db_remove )
db.close
end
-=begin rdoc
-Finds the row in the raw_image_files table of the given db file that matches this object.
-ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
-as an array of values (see 'sqlite3' gem docs).
-=end
+ # Finds the row in the raw_image_files table of the given db file that matches this object.
+ # ORM is based on combination of rmr_number, timestamp, and filename. The row is returned
+ # as an array of values (see 'sqlite3' gem docs).
def db_fetch!( db_file )
db = SQLite3::Database.new( db_file )
db_row = db.execute( db_fetch )
db.close
return db_row
@@ -255,76 +243,213 @@
def sql_match_conditions
"rmr_number = '#{@rmr_number}' AND timestamp = '#{@timestamp.to_s}' AND filename = '#{@filename}'"
end
-=begin rdoc
-Reads the file header using one of the available header reading utilities.
-Returns both the header data as either a RubyDicom object or one big string, and the name of the utility
-used to read it.
-
-Note: The rdgehdr is a binary file; the correct version for your architecture must be installed in the path.
-=end
+ # Reads the file header using one of the available header reading utilities.
+ # Returns both the header data as either a RubyDicom object or one big string, and the name of the utility
+ # used to read it.
+ #
+ # Note: The rdgehdr is a binary file; the correct version for your architecture must be installed in the path.
def read_header(absfilepath)
- # header = DICOM::DObject.new(absfilepath)
- # return [header, RUBYDICOM_HDR] if defined? header.read_success && header.read_success
-
- header = `#{DICOM_HDR} '#{absfilepath}' 2> /dev/null`
- #header = `#{DICOM_HDR} #{absfilepath}`
- if ( header.index("ERROR") == nil and
- header.chomp != "" and
- header.length > MIN_HDR_LENGTH )
- return [ header, DICOM_HDR ]
+
+ case File.basename(absfilepath)
+ when /^P.{5}\.7$|^I\..{3}/
+ # Try reading Pfiles or Genesis I-Files with GE's rdgehdr
+ @current_hdr_reader = RDGEHDR
+ header = `#{RDGEHDR} '#{absfilepath}' 2> /dev/null`
+ #header = `#{RDGEHDR} #{absfilepath}`
+ if ( header.chomp != "" and
+ header.length > MIN_HDR_LENGTH )
+ @current_hdr_reader = nil
+ return [ header, RDGEHDR ]
+ end
+ else
+ # Try reading with RubyDICOM
+ @current_hdr_reader = RUBYDICOM_HDR
+ header = DICOM::DObject.new(absfilepath)
+ if defined? header.read_success && header.read_success
+ @current_hdr_reader = nil
+ return [header, RUBYDICOM_HDR]
+ end
+
+ # Try reading with AFNI's dicom_hdr
+ @current_hdr_reader = DICOM_HDR
+ header = `#{DICOM_HDR} '#{absfilepath}' 2> /dev/null`
+ #header = `#{DICOM_HDR} #{absfilepath}`
+ if ( header.index("ERROR") == nil and
+ header.chomp != "" and
+ header.length > MIN_HDR_LENGTH )
+ @current_hdr_reader = nil
+ return [ header, DICOM_HDR ]
+ end
end
- header = `#{RDGEHDR} '#{absfilepath}' 2> /dev/null`
- #header = `#{RDGEHDR} #{absfilepath}`
- if ( header.chomp != "" and
- header.length > MIN_HDR_LENGTH )
- return [ header, RDGEHDR ]
- end
+
+ @current_hdr_reader = nil
return [ nil, nil ]
end
-=begin rdoc
-Returns a string that indicates the file type. This is difficult because dicom
-files have no consistent naming conventions/suffixes. Here we chose to call a
-file a "pfile" if it is an image and the file name is of the form P*.7
-All other images are called "dicom".
-=end
+ # Returns a string that indicates the file type. This is difficult because dicom
+ # files have no consistent naming conventions/suffixes. Here we chose to call a
+ # file a "pfile" if it is an image and the file name is of the form P*.7
+ # All other images are called "dicom".
def determine_file_type
return "pfile" if image? and (@filename =~ /^P.....\.7/) != nil
return "dicom" if image? and (@filename =~ /^P.....\.7/) == nil
return nil
end
-=begin rdoc
-Parses the header data and extracts a collection of instance variables. If
-@hdr_data and @hdr_reader are not already availables, this function does nothing.
-=end
+ # Parses the header data and extracts a collection of instance variables. If
+ # @hdr_data and @hdr_reader are not already available, this function does nothing.
def import_hdr
raise(IndexError, "No Header Data Available.") if @hdr_data == nil
case @hdr_reader
when "rubydicom" then rubydicom_hdr_import
when "dicom_hdr" then dicom_hdr_import
when "rdgehdr" then rdgehdr_import
end
end
-=begin rdoc
-Extract a collection of metadata from @hdr_data retrieved using RubyDicom
-=end
-def rubydicom_hdr_import
+ # Extract a collection of metadata from @hdr_data retrieved using RubyDicom
+ #
+ # Here are some example DICOM Tags and Values
+ # 0008,0022 Acquisition Date DA 8 20101103
+ # 0008,0030 Study Time TM 6 101538
+ # 0008,0080 Institution Name LO 4 Institution
+ # 0008,1010 Station Name SH 8 Station
+ # 0008,1030 Study Description LO 12 PILOT Study
+ # 0008,103E Series Description LO 12 3pl loc FGRE
+ # 0008,1070 Operators' Name PN 2 SP
+ # 0008,1090 Manufacturer's Model Name LO 16 DISCOVERY MR750
+ # 0010,0010 Patient's Name PN 12 mosPilot
+ # 0010,0020 Patient ID LO 12 RMREKKPilot
+ # 0010,0040 Patient's Sex CS 2 F
+ # 0010,1010 Patient's Age AS 4 027Y
+ # 0010,1030 Patient's Weight DS 4 49.9
+ # 0018,0023 MR Acquisition Type CS 2 2D
+ # 0018,0050 Slice Thickness DS 2 10
+ # 0018,0080 Repetition Time DS 6 5.032
+ # 0018,0081 Echo Time DS 6 1.396
+ # 0018,0082 Inversion Time DS 2 0
+ # 0018,0083 Number of Averages DS 2 1
+ # 0018,0087 Magnetic Field Strength DS 2 3
+ # 0018,0088 Spacing Between Slices DS 4 12.5
+ # 0018,0091 Echo Train Length IS 2 1
+ # 0018,0093 Percent Sampling DS 4 100
+ # 0018,0094 Percent Phase Field of View DS 4 100
+ # 0018,0095 Pixel Bandwidth DS 8 244.141
+ # 0018,1000 Device Serial Number LO 16 0000006080000
+ # 0018,1020 Software Version(s) LO 42 21\LX\MR Software release:20..
+ # 0018,1030 Protocol Name LO 22 MOSAIC Pilot 02Nov2010
+ # 0018,1100 Reconstruction Diameter DS 4 240
+ # 0018,1250 Receive Coil Name SH 8 8HRBRAIN
+ # 0018,1310 Acquisition Matrix US 8 0\256\128\0
+ # 0018,1312 In-plane Phase Encoding Direction CS 4 ROW
+ # 0018,1314 Flip Angle DS 2 30
+ # 0018,1315 Variable Flip Angle Flag CS 2 N
+ # 0018,1316 SAR DS 8 0.498088
+ # 0020,000D Study Instance UID UI 52 1.2.840.113619.6.260.4.88937..
+ # 0020,000E Series Instance UID UI 54 1.2.840.113619.2.260.6945.23..
+ # 0020,0010 Study ID SH 4 1260
+ # 0020,0011 Series Number IS 2 1
+ # 0020,0012 Acquisition Number IS 2 1
+ # 0020,0013 Instance Number IS 2 1
+ # 0020,0032 Image Position (Patient) DS 22 -119.531\-159.531\-25
+ # 0020,1002 Images in Acquisition IS 2 15
+ # 0028,0010 Rows US 2 256
+ # 0028,0011 Columns US 2 256
+ # 0028,0030 Pixel Spacing DS 14 0.9375\0.9375
+ def rubydicom_hdr_import
+ dicom_tag_attributes = {
+ :source => "0008,0080",
+ :series_description => "0008,103E",
+ :study_description => "0008,1030",
+ :operator_name => "0008,1070",
+ :patient_name => "0010,0010",
+ :rmr_number => "0010,0020",
+ :gender => "0010,0040",
+ :slice_thickness => "0018,0050",
+ :reconstruction_diameter => "0018,1100",
+ :rep_time => "0018,0080",
+ :pixel_spacing => "0028,0030",
+ :flip_angle => "0018,1314",
+ :field_strength => "0018,0087",
+ :slice_spacing => "0018,0088",
+ :software_version => "0018,1020",
+ :protocol_name => "0018,1030",
+ :bold_reps => "0020,0105",
+ :dicom_series_uid => "0020,000E",
+ :dicom_study_uid => "0020,000D",
+ :study_id => "0020,0010",
+ :num_slices => "0020,1002",
+ :acquisition_matrix_x => "0028,0010",
+ :acquisition_matrix_y => "0028,0011"
+ }
+
+
+ dicom_tag_attributes.each_pair do |name, tag|
+ begin
+ # next if tag_hash[:type] == :datetime
+ value = @hdr_data[tag].value if @hdr_data[tag]
+ raise ScriptError, "No match found for #{name}" unless value
+ instance_variable_set("@#{name.to_s}", value)
+ rescue ScriptError => e
+ @warnings << "Tag #{name} could not be found."
+ end
+ end
+
+ @timestamp = DateTime.parse(@hdr_data["0008,0022"].value + @hdr_data["0008,0030"].value)
+ @dicom_taghash = create_dicom_taghash(@hdr_data)
+ # @dicom_header = remove_long_dicom_elements(@hdr_data)
+
+ end
-end
+ # # Remove long data elements from a rubydicom header. This essentially strips
+ # # lengthy image data.
+ # def remove_long_dicom_elements(header)
+ # raise ScriptError, "A DICOM::DObject instance is required" unless header.kind_of? DICOM::DObject
+ # h = header.dup
+ # h.children.select { |element| element.length > 100 }.each do |e|
+ # h.remove(e.tag)
+ # # puts "Removing #{e.tag}..."
+ # end
+ # return h
+ # end
+
+ # Create a super-lightweight representation of the DICOM header as a hash, where
+ # the tags are they keys and name and value are stored as an attribute hash in value.
+ #
+ # Creates a hash like:
+ # {"0018,0095"=>{:value=>"244.141", :name=>"Pixel Bandwidth"},
+ # "0008,1030"=>{:value=>"MOSAIC PILOT", :name=>"Study Description"} }
+ #
+ # When serialized with yaml, this looks like:
+ #
+ # 0018,0095:
+ # :value: "244.141"
+ # :name: Pixel Bandwidth
+ #
+ # 0008,1030:
+ # :value: MOSAIC PILOT
+ # :name: Study Description
+ #
+ # To filter and search, you can do something like:
+ # tag_hash.each_pair {|tag, attributes| puts tag, attributes[:value] if attributes[:name] =~ /Description/i }
+ def create_dicom_taghash(header)
+ raise ScriptError, "A DICOM::DObject instance is required" unless header.kind_of? DICOM::DObject
+ h = Hash.new
+ header.children.each do |element|
+ h[element.tag] = {:value => element.instance_variable_get(:@value), :name => element.name}
+ end
+ return h
+ end
-=begin rdoc
-Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
-utility.
-=end
+ # Extracts a collection of metadata from @hdr_data retrieved using the dicom_hdr
+ # utility.
def dicom_hdr_import
dicom_tag_templates = {}
dicom_tag_templates[:rmr_number] = {
:type => :string,
:pat => /[ID Accession Number|ID Study Description]\/\/(RMR.*)\n/i,
@@ -429,13 +554,11 @@
@timestamp = DateTime.parse(date + time)
end
-=begin rdoc
-Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
-utility.
-=end
+ # Extracts a collection of metadata from @hdr_data retrieved using the rdgehdr
+ # utility.
def rdgehdr_import
source_pat = /hospital [Nn]ame: ([[:graph:]\t ]+)/i
num_slices_pat = /Number of slices in this scan group: ([0-9]+)/i
slice_thickness_pat = /slice thickness \(mm\): ([[:graph:]]+)/i
slice_spacing_pat = /spacing between scans \(mm\??\): ([[:graph:]]+)/i
\ No newline at end of file