Class: DICOM::DObject

Inherits:
ImageItem show all
Includes:
Logging
Defined in:
lib/dicom/d_object.rb

Overview

The DObject class is the main class for interacting with the DICOM object. Reading from and writing to files is executed from instances of this class.

Inheritance

As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class, all ImageItem and Parent methods are also available to instances of DObject.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Logging

included, #logger

Methods inherited from ImageItem

#add_element, #add_sequence, #color?, #compression?, #decode_pixels, #delete_sequences, #encode_pixels, #image, #image=, #image_from_file, #image_strings, #image_to_file, #images, #narray, #num_cols, #num_frames, #num_rows, #pixels, #pixels=

Methods included from ImageProcessor

#decompress, #export_pixels, #import_pixels, #valid_image_objects

Methods inherited from Parent

#[], #add, #add_item, #children, #children?, #count, #count_all, #delete, #delete_children, #delete_group, #delete_private, #delete_retired, #each, #each_element, #each_item, #each_sequence, #each_tag, #elements, #elements?, #encode_children, #exists?, #group, #handle_print, #inspect, #is_parent?, #items, #items?, #length=, #max_lengths, #method_missing, #print, #reset_length, #respond_to?, #sequences, #sequences?, #to_hash, #to_json, #to_yaml, #value

Constructor Details

#initializeDObject

Creates a DObject instance (DObject is an abbreviation for “DICOM object”).

The DObject instance holds references to the different types of objects (Element, Item, Sequence) that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a binary string (with DObject::read or ::parse), but can also be buildt from an empty state by this method.

To customize logging behaviour, refer to the Logging module documentation.

Examples:

Create an empty DICOM object

require 'dicom'
dcm = DICOM::DObject.new

Increasing the log message threshold (default level is INFO)

DICOM.logger.level = Logger::ERROR


182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/dicom/d_object.rb', line 182

def initialize
  # Initialization of variables that DObject share with other parent elements:
  initialize_parent
  # Structural information (default values):
  @explicit = true
  @str_endian = false
  # Control variables:
  @read_success = nil
  # Initialize a Stream instance which is used for encoding/decoding:
  @stream = Stream.new(nil, @str_endian)
  # The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
  @parent = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class DICOM::Parent

Instance Attribute Details

#parentObject (readonly)

An attribute set as nil. This attribute is included to provide consistency with the other element types which usually have a parent defined.



30
31
32
# File 'lib/dicom/d_object.rb', line 30

def parent
  @parent
end

#read_successObject Also known as: read?

A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).



32
33
34
# File 'lib/dicom/d_object.rb', line 32

def read_success
  @read_success
end

#sourceObject

The source of the DObject (nil, :str or file name string).



34
35
36
# File 'lib/dicom/d_object.rb', line 34

def source
  @source
end

#streamObject (readonly)

The Stream instance associated with this DObject instance (this attribute is mostly used internally).



36
37
38
# File 'lib/dicom/d_object.rb', line 36

def stream
  @stream
end

#was_dcm_on_inputObject

An attribute (used by e.g. DICOM.load) to indicate that a DObject-type instance was given to the load method (instead of e.g. a file).



38
39
40
# File 'lib/dicom/d_object.rb', line 38

def was_dcm_on_input
  @was_dcm_on_input
end

#write_successObject (readonly) Also known as: written?

A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).



40
41
42
# File 'lib/dicom/d_object.rb', line 40

def write_success
  @write_success
end

Class Method Details

.get(link) ⇒ DObject

Note:

Highly experimental and un-tested!

Note:

Designed for the HTTP protocol only.

Note:

Whether this method should be included or removed from ruby-dicom is up for debate.

Creates a DObject instance by downloading a DICOM file specified by a hyperlink, and parsing the retrieved file.

Parameters:

  • link (String)

    a hyperlink string which specifies remote location of the DICOM file to be loaded.

Returns:

  • (DObject)

    the created DObject instance

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/dicom/d_object.rb', line 55

def self.get(link)
  raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
  raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
  require 'open-uri'
  bin = nil
  file = nil
  # Try to open the remote file using open-uri:
  retrials = 0
  begin
    file = open(link, 'rb') # binary encoding (ASCII-8BIT)
  rescue Exception => e
    if retrials > 3
      retrials = 0
      raise "Unable to retrieve the file. File does not exist?"
    else
      logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
      logger.debug("Retrying... #{retrials}")
      retrials += 1
      retry
    end
  end
  bin = File.open(file, "rb") { |f| f.read }
  # Parse the file contents and create the DICOM object:
  if bin
    dcm = self.parse(bin)
  else
    dcm = self.new
    dcm.read_success = false
  end
  dcm.source = link
  return dcm
end

.parse(string, options = {}) ⇒ Object

Creates a DObject instance by parsing an encoded binary DICOM string.

Examples:

Parse a DICOM file that has already been loaded to a binary string

require 'dicom'
dcm = DICOM::DObject.parse(str)

Parse a header-less DICOM string with explicit little endian transfer syntax

dcm = DICOM::DObject.parse(str, :syntax => '1.2.840.10008.1.2.1')

Parameters:

  • string (String)

    an encoded binary string containing DICOM information

  • options (Hash) (defaults to: {})

    the options to use for parsing the DICOM string

Options Hash (options):

  • :overwrite (Boolean)

    for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates

  • :signature (Boolean)

    if set as false, the parsing algorithm will not be looking for the DICOM header signature (defaults to true)

  • :syntax (String)

    if a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string

Raises:

  • (ArgumentError)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/dicom/d_object.rb', line 101

def self.parse(string, options={})
  raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
  raise ArgumentError, "Invalid option :syntax. Expected String, got #{options[:syntax].class}." if options[:syntax] && !options[:syntax].is_a?(String)
  signature = options[:signature].nil? ? true : options[:signature]
  dcm = self.new
  dcm.send(:read, string, signature, :overwrite => options[:overwrite], :syntax => options[:syntax])
  if dcm.read?
    logger.debug("DICOM string successfully parsed.")
  else
    logger.warn("Failed to parse this string as DICOM.")
  end
  dcm.source = :str
  return dcm
end

.read(file, options = {}) ⇒ Object

Creates a DObject instance by reading and parsing a DICOM file.

Examples:

Load a DICOM file

require 'dicom'
dcm = DICOM::DObject.read('test.dcm')

Parameters:

  • file (String)

    a string which specifies the path of the DICOM file to be loaded

  • options (Hash) (defaults to: {})

    the options to use for reading the DICOM file

Options Hash (options):

  • :overwrite (Boolean)

    for the rare case of a DICOM file containing duplicate elements, setting this as true instructs the parsing algorithm to overwrite the original element with duplicates

Raises:

  • (ArgumentError)


125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/dicom/d_object.rb', line 125

def self.read(file, options={})
  raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
  # Read the file content:
  bin = nil
  unless File.exist?(file)
    logger.error("Invalid (non-existing) file: #{file}")
  else
    unless File.readable?(file)
      logger.error("File exists but I don't have permission to read it: #{file}")
    else
      if File.directory?(file)
        logger.error("Expected a file, got a directory: #{file}")
      else
        if File.size(file) < 8
          logger.error("This file is too small to contain any DICOM information: #{file}.")
        else
          bin = File.open(file, "rb") { |f| f.read }
        end
      end
    end
  end
  # Parse the file contents and create the DICOM object:
  if bin
    dcm = self.parse(bin, options)
    # If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
    # This will help for some rare cases where the DICOM file is saved (erroneously, Im sure) with explicit encoding without specifying the transfer syntax tag.
    if !dcm.read? and !dcm.exists?("0002,0010")
      logger.info("Attempting a second decode pass (assuming Explicit Little Endian transfer syntax).")
      options[:syntax] = EXPLICIT_LITTLE_ENDIAN
      dcm = self.parse(bin, options)
    end
  else
    dcm = self.new
  end
  if dcm.read?
    logger.info("DICOM file successfully read: #{file}")
  else
    logger.warn("Reading DICOM file failed: #{file}")
  end
  dcm.source = file
  return dcm
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Checks for equality.

Other and self are considered equivalent if they are of compatible types and their attributes are equivalent.

Parameters:

  • other

    an object to be compared with self.

Returns:

  • (Boolean)

    true if self and other are considered equivalent



204
205
206
207
208
# File 'lib/dicom/d_object.rb', line 204

def ==(other)
  if other.respond_to?(:to_dcm)
    other.send(:state) == state
  end
end

#anonymize(a = Anonymizer.new) ⇒ Object

Performs de-identification (anonymization) on the DICOM object.

Parameters:

  • a (Anonymizer) (defaults to: Anonymizer.new)

    an Anonymizer instance to use for the anonymization



216
217
218
# File 'lib/dicom/d_object.rb', line 216

def anonymize(a=Anonymizer.new)
  a.to_anonymizer.anonymize(self)
end

#encode_segments(max_size, transfer_syntax = IMPLICIT_LITTLE_ENDIAN) ⇒ Array<String>

Encodes the DICOM object into a series of binary string segments with a specified maximum length.

Returns the encoded binary strings in an array.

Examples:

Encode the DObject to strings of max length 2^14 bytes

encoded_strings = dcm.encode_segments(16384)

Parameters:

  • max_size (Integer)

    the maximum allowed size of the binary data strings to be encoded

  • transfer_syntax (String) (defaults to: IMPLICIT_LITTLE_ENDIAN)

    the transfer syntax string to be used when encoding the DICOM object to string segments. When this method is used for making network packets, the transfer_syntax is not part of the object, and thus needs to be specified.

Returns:

Raises:

  • (ArgumentError)


230
231
232
233
234
235
# File 'lib/dicom/d_object.rb', line 230

def encode_segments(max_size, transfer_syntax=IMPLICIT_LITTLE_ENDIAN)
  raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
  raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
  raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
  encode_in_segments(max_size, :syntax => transfer_syntax)
end

#hashFixnum

Note:

Two objects with the same attributes will have the same hash code.

Computes a hash code for this object.

Returns:

  • (Fixnum)

    the object’s hash code



243
244
245
# File 'lib/dicom/d_object.rb', line 243

def hash
  state.hash
end

Prints information of interest related to the DICOM object. Calls the Parent#print method as well as DObject#summary.



250
251
252
253
254
# File 'lib/dicom/d_object.rb', line 250

def print_all
  puts ""
  print(:value_max => 30)
  summary
end

#summaryArray<String>

Gathers key information about the DObject as well as some system data, and prints this information to the screen. This information includes properties like encoding, byte order, modality and various image properties.

Returns:

  • (Array<String>)

    strings describing the properties of the DICOM object



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/dicom/d_object.rb', line 261

def summary
  # FIXME: Perhaps this method should be split up in one or two separate methods
  # which just builds the information arrays, and a third method for printing this to the screen.
  sys_info = Array.new
  info = Array.new
  # Version of Ruby DICOM used:
  sys_info << "Ruby DICOM version:   #{VERSION}"
  # System endian:
  cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
  sys_info << "Byte Order (CPU):     #{cpu}"
  # Source (file name):
  if @source
    if @source == :str
      source = "Binary string #{@read_success ? '(successfully parsed)' : '(failed to parse)'}"
    else
      source = "File #{@read_success ? '(successfully read)' : '(failed to read)'}: #{@source}"
    end
  else
    source = 'Created from scratch'
  end
  info << "Source:               #{source}"
  # Modality:
  modality = (LIBRARY.uid(value('0008,0016')) ? LIBRARY.uid(value('0008,0016')).name : "SOP Class unknown or not specified!")
  info << "Modality:             #{modality}"
  # Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
  ts_status = self['0002,0010'] ? '' : ' (Assumed)'
  ts = LIBRARY.uid(transfer_syntax)
  explicit = ts ? ts.explicit? : true
  endian = ts ? ts.big_endian? : false
  meta_comment = ts ? "" : " (But unknown/invalid transfer syntax: #{transfer_syntax})"
  info << "Meta Header:          #{self['0002,0010'] ? 'Yes' : 'No'}#{meta_comment}"
  info << "Value Representation: #{explicit ? 'Explicit' : 'Implicit'}#{ts_status}"
  info << "Byte Order (File):    #{endian ? 'Big Endian' : 'Little Endian'}#{ts_status}"
  # Pixel data:
  pixels = self[PIXEL_TAG]
  unless pixels
    info << "Pixel Data:           No"
  else
    info << "Pixel Data:           Yes"
    # Image size:
    cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
    rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
    info << "Image Size:           #{cols}*#{rows}"
    # Frames:
    frames = value("0028,0008") || "1"
    unless frames == "1" or frames == 1
      # Encapsulated or 3D pixel data:
      if pixels.is_a?(Element)
        frames = frames.to_s + " (3D Pixel Data)"
      else
        frames = frames.to_s + " (Encapsulated Multiframe Image)"
      end
    end
    info << "Number of frames:     #{frames}"
    # Color:
    colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
    info << "Photometry:           #{colors}"
    # Compression:
    compression = (ts ? (ts.compressed_pixels? ? ts.name : 'No') : 'No' )
    info << "Compression:          #{compression}#{ts_status}"
    # Pixel bits (allocated):
    bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
    info << "Bits per Pixel:       #{bits}"
  end
  # Print the DICOM object's key properties:
  separator = "-------------------------------------------"
  puts "System Properties:"
  puts separator + "\n"
  puts sys_info
  puts "\n"
  puts "DICOM Object Properties:"
  puts separator
  puts info
  puts separator
  return info
end

#to_dcmDObject

Returns self.

Returns:



342
343
344
# File 'lib/dicom/d_object.rb', line 342

def to_dcm
  self
end

#transfer_syntaxString

Gives the transfer syntax string of the DObject.

If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.

Returns:

  • (String)

    the DObject’s transfer syntax



352
353
354
# File 'lib/dicom/d_object.rb', line 352

def transfer_syntax
  return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
end

#transfer_syntax=(new_syntax) ⇒ Object

Note:

This method does not change the compressed state of the pixel data element. Changing

Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all numerical values if a switch of endianness is implied.

the transfer syntax between an uncompressed and compressed state will NOT change the pixel data accordingly (this must be taken care of manually).

Parameters:

  • new_syntax (String)

    the new transfer syntax string to be applied to the DObject

Raises:

  • (ArgumentError)


365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/dicom/d_object.rb', line 365

def transfer_syntax=(new_syntax)
  # Verify old and new transfer syntax:
  new_uid = LIBRARY.uid(new_syntax)
  old_uid = LIBRARY.uid(transfer_syntax)
  raise ArgumentError, "Invalid/unknown transfer syntax specified: #{new_syntax}" unless new_uid && new_uid.transfer_syntax?
  raise ArgumentError, "Invalid/unknown existing transfer syntax: #{new_syntax} Unable to reliably handle byte order encoding. Modify the transfer syntax element directly instead." unless old_uid && old_uid.transfer_syntax?
  # Set the new transfer syntax:
  if exists?("0002,0010")
    self["0002,0010"].value = new_syntax
  else
    add(Element.new("0002,0010", new_syntax))
  end
  # Update our Stream instance with the new encoding:
  @stream.endian = new_uid.big_endian?
  # If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
  encode_children(old_uid.big_endian?) if old_uid.big_endian? != new_uid.big_endian?
end

#write(file_name, options = {}) ⇒ Object

Note:

The goal of the Ruby DICOM library is to yield maximum conformance with the DICOM

Writes the DICOM object to file.

standard when outputting DICOM files. Therefore, when encoding the DICOM file, manipulation of items such as the meta group, group lengths and header signature may occur. Therefore, the file that is written may not be an exact bitwise copy of the file that was read, even if no DObject manipulation has been done by the user.

Examples:

Encode a DICOM file from a DObject

dcm.write('C:/dicom/test.dcm')

Parameters:

  • file_name (String)

    the path of the DICOM file which is to be written to disk

  • options (Hash) (defaults to: {})

    the options to use for writing the DICOM file

Options Hash (options):

  • :ignore_meta (Boolean)

    if true, no manipulation of the DICOM object’s meta group will be performed before the DObject is written to file

  • :include_empty_parents (Boolean)

    if true, childless parents (sequences & items) are written to the DICOM file

Raises:

  • (ArgumentError)


398
399
400
401
402
403
# File 'lib/dicom/d_object.rb', line 398

def write(file_name, options={})
  raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
  @include_empty_parents = options[:include_empty_parents]
  insert_missing_meta unless options[:ignore_meta]
  write_elements(:file_name => file_name, :signature => true, :syntax => transfer_syntax)
end