Class: DICOM::ImageItem

Inherits:
Parent
  • Object
show all
Includes:
ImageProcessor
Defined in:
lib/dicom/image_item.rb

Overview

Super class which contains common code for both the DObject and Item classes. This class includes the image related methods, since images may be stored either directly in the DObject, or in items (encapsulated items in the “Pixel Data” element or in “Icon Image Sequence” items).

Inheritance

As the ImageItem class inherits from the Parent class, all Parent methods are also available to objects which has inherited ImageItem.

Direct Known Subclasses

DObject, Item

Instance Method Summary collapse

Methods included from ImageProcessor

#decompress, #export_pixels, #import_pixels, #valid_image_objects

Methods inherited from Parent

#[], #add, #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, #parse, #print, #representation, #reset_length, #respond_to?, #sequences, #sequences?, #to_hash, #to_json, #to_yaml, #value

Methods included from Logging

included, #logger

Dynamic Method Handling

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

Instance Method Details

#add_element(tag, value, options = {}) ⇒ Object

Creates an Element with the given arguments and connects it to self.

Parameters:

  • tag (String)

    an element tag

  • value (String, Integer, Float, Array, NilClass)

    an element value

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

    any options used for creating the element (see Element.new documentation)



23
24
25
26
# File 'lib/dicom/image_item.rb', line 23

def add_element(tag, value, options={})
  add(e = Element.new(tag, value, options))
  e
end

#add_sequence(tag, options = {}) ⇒ Object

Creates a Sequence with the given arguments and connects it to self.

Parameters:

  • tag (String)

    a sequence tag

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

    any options used for creating the sequence (see Sequence.new documentation)



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

def add_sequence(tag, options={})
  add(s = Sequence.new(tag, options))
  s
end

#color?Boolean

Checks if colored pixel data is present.

Returns:

  • (Boolean)

    true if the object contains colored pixels, and false if not



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/dicom/image_item.rb', line 42

def color?
  # "Photometric Interpretation" is contained in the data element "0028,0004":
  begin
    photometric = photometry
    if photometric.include?('COLOR') or photometric.include?('RGB') or photometric.include?('YBR')
      return true
    else
      return false
    end
  rescue
    return false
  end
end

#compression?Boolean

Checks if compressed pixel data is present.

Returns:

  • (Boolean)

    true if the object contains compressed pixels, and false if not



60
61
62
63
64
65
66
67
# File 'lib/dicom/image_item.rb', line 60

def compression?
  # If compression is used, the pixel data element is a Sequence (with encapsulated elements), instead of a Element:
  if self[PIXEL_TAG].is_a?(Sequence)
    return true
  else
    return false
  end
end

#decode_pixels(bin, stream = @stream) ⇒ Array<Integer>

Unpacks pixel values from a binary pixel string. The decode is performed using values defined in the image related elements of the DObject instance.

Parameters:

  • bin (String)

    a binary string containing the pixels to be decoded

  • stream (Stream) (defaults to: @stream)

    a Stream instance to be used for decoding the pixels (optional)

Returns:

  • (Array<Integer>)

    decoded pixel values

Raises:

  • (ArgumentError)


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/dicom/image_item.rb', line 76

def decode_pixels(bin, stream=@stream)
  raise ArgumentError, "Expected String, got #{bin.class}." unless bin.is_a?(String)
  pixels = false
  # We need to know what kind of bith depth and integer type the pixel data is saved with:
  bit_depth_element = self['0028,0100']
  pixel_representation_element = self['0028,0103']
  if bit_depth_element and pixel_representation_element
    # Load the binary pixel data to the Stream instance:
    stream.set_string(bin)
    template = template_string(bit_depth_element.value.to_i)
    pixels = stream.decode_all(template) if template
  else
    raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to decode pixel data." unless bit_depth_element
    raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to decode pixel data." unless pixel_representation_element
  end
  return pixels
end

#delete_sequencesObject

Delete all Sequence instances from the DObject or Item instance.



445
446
447
448
449
# File 'lib/dicom/image_item.rb', line 445

def delete_sequences
  @tags.each_value do |element|
    delete(element.tag) if element.is_a?(Sequence)
  end
end

#encode_pixels(pixels, stream = @stream) ⇒ String

Packs a pixel value array to a binary pixel string. The encoding is performed using values defined in the image related elements of the DObject instance.

Parameters:

  • pixels (Array<Integer>)

    an array containing the pixel values to be encoded

  • stream (Stream) (defaults to: @stream)

    a Stream instance to be used for encoding the pixels (optional)

Returns:

  • (String)

    encoded pixel string

Raises:

  • (ArgumentError)


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

def encode_pixels(pixels, stream=@stream)
  raise ArgumentError, "Expected Array, got #{pixels.class}." unless pixels.is_a?(Array)
  bin = false
  # We need to know what kind of bith depth and integer type the pixel data is saved with:
  bit_depth_element = self['0028,0100']
  pixel_representation_element = self['0028,0103']
  if bit_depth_element and pixel_representation_element
    template = template_string(bit_depth_element.value.to_i)
    bin = stream.encode(pixels, template) if template
  else
    raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to encode the pixel data." unless bit_depth_element
    raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to encode the pixel data." unless pixel_representation_element
  end
  return bin
end

#image(options = {}) ⇒ MagickImage, ...

Note:

Creates an image object in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.

Extracts a single image object, created from the encoded pixel data using the image related elements in the DICOM object. If the object contains multiple image frames, the first image frame is returned, unless the :frame option is used.

Examples:

Retrieve pixel data as an RMagick image object and display it

image = dcm.image
image.display

Retrieve frame index 5 in the pixel data

image = dcm.image(:frame => 5)

Parameters:

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

    the options to use for extracting the image

Options Hash (options):

  • :frame (Integer)

    for DICOM objects containing multiple frames, this option can be used to extract a specific image frame (defaults to 0)

  • :level (TrueClass, Array<Integer>)

    if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead

  • :narray (Boolean)

    if true, forces the use of NArray for the pixel remap process (for faster execution)

  • :remap (Boolean)

    if true, the returned pixel values are remapped to presentation values

Returns:

  • (MagickImage, NilClass, FalseClass)

    an image object, alternatively nil (if no image present) or false (if image decode failed)



136
137
138
139
140
141
# File 'lib/dicom/image_item.rb', line 136

def image(options={})
  options[:frame] = options[:frame] || 0
  image = images(options).first
  image = false if image.nil? && exists?(PIXEL_TAG)
  return image
end

#image=(image) ⇒ Object

Encodes pixel data from a (Magick) image object and writes it to the pixel data element (7FE0,0010).

Because of pixel value issues related to image objects (images don’t like signed integers), and the possible difference between presentation values and raw pixel values, the use of image=() may result in pixel data where the integer values differs somewhat from what is expected. Use with care! For precise pixel value processing, use the Array and NArray based pixel data methods instead.

Parameters:

  • image (MagickImage)

    the image to be assigned to the pixel data element

Raises:

  • (ArgumentError)


286
287
288
289
290
291
292
# File 'lib/dicom/image_item.rb', line 286

def image=(image)
  raise ArgumentError, "Expected one of the supported image classes: #{valid_image_objects} (got #{image.class})" unless valid_image_objects.include?(image.class.to_s)
  # Export to pixels using the proper image processor:
  pixels = export_pixels(image, photometry)
  # Encode and write to the Pixel Data Element:
  self.pixels = pixels
end

#image_from_file(file) ⇒ Object

Reads a binary string from a specified file and writes it to the value field of the pixel data element (7FE0,0010).

Examples:

Load pixel data from a file

dcm.image_from_file("custom_image.dat")

Parameters:

  • file (String)

    a string which specifies the path of the file containing pixel data

Raises:

  • (ArgumentError)


207
208
209
210
211
212
213
214
215
216
217
# File 'lib/dicom/image_item.rb', line 207

def image_from_file(file)
  raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
  f = File.new(file, 'rb')
  bin = f.read(f.stat.size)
  if bin.length > 0
    # Write the binary data to the Pixel Data Element:
    write_pixels(bin)
  else
    logger.info("The specified file (#{file}) is empty. Nothing to transfer.")
  end
end

#image_strings(split = false) ⇒ Array<String, NilClass>

Extracts the pixel data binary string(s) in an array.

Parameters:

  • split (Boolean) (defaults to: false)

    if true, a pixel data string containing 3D volumetric data will be split into N substrings (where N equals the number of frames)

Returns:

  • (Array<String, NilClass>)

    an array of pixel data strings, or an empty array (if no pixel data present)



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/dicom/image_item.rb', line 224

def image_strings(split=false)
  # Pixel data may be a single binary string in the pixel data element,
  # or located in several encapsulated item elements:
  pixel_element = self[PIXEL_TAG]
  strings = Array.new
  if pixel_element.is_a?(Element)
    if split
      strings = pixel_element.bin.dup.divide(num_frames)
    else
      strings << pixel_element.bin
    end
  elsif pixel_element.is_a?(Sequence)
    pixel_items = pixel_element.children.first.children
    pixel_items.each {|item| strings << item.bin}
  end
  return strings
end

#image_to_file(file) ⇒ Object

Dumps the binary content of the Pixel Data element to the specified file.

If the DICOM object contains multi-fragment pixel data, each fragment will be dumped to separate files (e.q. ‘fragment-0.dat’, ‘fragment-1.dat’).

Examples:

Dumping the pixel data to a file

dcm.image_to_file("exported_image.dat")

Parameters:

  • file (String)

    a string which specifies the file path to use when dumping the pixel data

Raises:

  • (ArgumentError)


251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/dicom/image_item.rb', line 251

def image_to_file(file)
  raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
  # Split the file name in case of multiple fragments:
  parts = file.split('.')
  if parts.length > 1
    base = parts[0..-2].join
    extension = '.' + parts.last
  else
    base = file
    extension = ''
  end
  # Get the binary image strings and dump them to the file(s):
  images = image_strings
  images.each_index do |i|
    if images.length == 1
      f = File.new(file, 'wb')
    else
      f = File.new("#{base}-#{i}#{extension}", 'wb')
    end
    f.write(images[i])
    f.close
  end
end

#images(options = {}) ⇒ Array<MagickImage, NilClass>

Note:

Creates an array of image objects in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.

Extracts an array of image objects, created from the encoded pixel data using the image related elements in the DICOM object.

Examples:

Retrieve the pixel data as RMagick image objects

images = dcm.images

Retrieve the pixel data as RMagick image objects, remapped to presentation values (but without any leveling)

images = dcm.images(:remap => true)

Retrieve the pixel data as RMagick image objects, remapped to presentation values and leveled using the default center/width values in the DICOM object

images = dcm.images(:level => true)

Retrieve the pixel data as RMagick image objects, remapped to presentation values, leveled with the specified center/width values and using numerical array for the rescaling (~twice as fast)

images = dcm.images(:level => [-200,1000], :narray => true)

Parameters:

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

    the options to use for extracting the images

Options Hash (options):

  • :frame (Integer)

    makes the method return an array containing only the image object corresponding to the specified frame number

  • :level (TrueClass, Array<Integer>)

    if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead

  • :narray (Boolean)

    if true, forces the use of NArray for the pixel remap process (for faster execution)

  • :remap (Boolean)

    if true, the returned pixel values are remapped to presentation values

Returns:

  • (Array<MagickImage, NilClass>)

    an array of image objects, alternatively an empty array (if no image present or image decode failed)



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/dicom/image_item.rb', line 164

def images(options={})
  images = Array.new
  if exists?(PIXEL_TAG)
    # Gather the pixel data strings, and pick a single frame if indicated by options:
    strings = image_strings(split_to_frames=true)
    strings = [strings[options[:frame]]] if options[:frame]
    if compression?
      # Decompress, either to numbers (RLE) or to an image object (image based compressions):
      if [TXS_RLE].include?(transfer_syntax)
        pixel_frames = Array.new
        strings.each {|string| pixel_frames << decode_rle(num_cols, num_rows, string)}
      else
        images = decompress(strings) || Array.new
        logger.warn("Decompressing pixel values has failed (unsupported transfer syntax: '#{transfer_syntax}' - #{LIBRARY.uid(transfer_syntax) ? LIBRARY.uid(transfer_syntax).name : 'Unknown transfer syntax!'})") unless images.length > 0
      end
    else
      # Uncompressed: Decode to numbers.
      pixel_frames = Array.new
      strings.each {|string| pixel_frames << decode_pixels(string)}
    end
    if pixel_frames
      images = Array.new
      pixel_frames.each do |pixels|
        # Pixel values and pixel order may need to be rearranged if we have color data:
        pixels = process_colors(pixels) if color?
        if pixels
          images << read_image(pixels, num_cols, num_rows, options)
        else
          logger.warn("Processing pixel values for this particular color mode failed, unable to construct image(s).")
        end
      end
    end
  end
  return images
end

#narray(options = {}) ⇒ NArray, ...

Note:

To call this method you need to have loaded the NArray library in advance (require ‘narray’).

Creates an NArray containing the pixel data. If the pixel data is an image (single frame), a 2-dimensional NArray is returned [columns, rows]. If the pixel data is 3-dimensional (more than one frame), a 3-dimensional NArray is returned [frames, columns, rows].

Examples:

Retrieve numerical pixel array

data = dcm.narray

Retrieve numerical pixel array remapped from the original pixel values to presentation values

data = dcm.narray(:remap => true)

Parameters:

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

    the options to use for extracting the pixel data

Options Hash (options):

  • :level (TrueClass, Array<Integer>)

    if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead

  • :remap (Boolean)

    if true, the returned pixel values are remapped to presentation values

  • :volume (Boolean)

    if true, the returned array will always be 3-dimensional, even if the pixel data only has one frame

Returns:

  • (NArray, NilClass, FalseClass)

    an NArray of pixel values, alternatively nil (if no image present) or false (if image decode failed)



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# File 'lib/dicom/image_item.rb', line 337

def narray(options={})
  pixels = nil
  if exists?(PIXEL_TAG)
    unless color?
      # Decode the pixel values: For now we only support returning pixel data of the first frame (if the image is located in multiple pixel data items).
      if compression?
        pixels = decompress(image_strings.first)
      else
        pixels = decode_pixels(image_strings.first)
      end
      if pixels
        # Import the pixels to NArray and give it a proper shape:
        raise "Missing Rows and/or Columns Element. Unable to construct pixel data array." unless num_rows and num_cols
        if num_frames > 1 or options[:volume]
          # Create an empty 3D NArray. fill it with pixels frame by frame, then reassign the pixels variable to it:
          narr = Numo::Int16.zeros(num_frames, num_cols, num_rows)
          num_frames.times do |i|
            narr[i, true, true] = Numo::NArray[*pixels[(i * num_cols * num_rows)..((i + 1) * num_cols * num_rows - 1)]].reshape!(num_cols, num_rows)
          end
          pixels = narr
        else
          pixels = Numo::NArray[*pixels].reshape!(num_cols, num_rows)
        end
        # Remap the image from pixel values to presentation values if the user has requested this:
        pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]) if options[:remap] or options[:level]
      else
        logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
      end
    else
      logger.warn("The DICOM object contains colored pixel data. Retrieval of colored pixels is not supported by this method yet.")
      pixels = false
    end
  end
  return pixels
end

#num_colsInteger, NilClass

Gives the number of columns in the pixel data.

Returns:

  • (Integer, NilClass)

    the number of columns, or nil (if the columns value is undefined)



298
299
300
# File 'lib/dicom/image_item.rb', line 298

def num_cols
  self['0028,0011'].value rescue nil
end

#num_framesInteger

Note:

Assumes and gives 1 if the number of frames value is not defined.

Gives the number of frames in the pixel data.

Returns:

  • (Integer)

    the number of rows



307
308
309
# File 'lib/dicom/image_item.rb', line 307

def num_frames
  (self['0028,0008'].is_a?(Element) == true ? self['0028,0008'].value.to_i : 1)
end

#num_rowsInteger, NilClass

Gives the number of rows in the pixel data.

Returns:

  • (Integer, NilClass)

    the number of rows, or nil (if the rows value is undefined)



315
316
317
# File 'lib/dicom/image_item.rb', line 315

def num_rows
  self['0028,0010'].value rescue nil
end

#pixels(options = {}) ⇒ Array, ...

Extracts the Pixel Data values in an ordinary Ruby Array. Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.

The returned array does not carry the dimensions of the pixel data: It is put in a one dimensional Array (vector).

Examples:

Simply retrieve the pixel data

pixels = dcm.pixels

Retrieve the pixel data remapped to presentation values according to window center/width settings

pixels = dcm.pixels(:remap => true)

Retrieve the remapped pixel data while using numerical array (~twice as fast)

pixels = dcm.pixels(:remap => true, :narray => true)

Parameters:

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

    the options to use for extracting the pixel data

Options Hash (options):

  • :level (TrueClass, Array<Integer>)

    if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead

  • :narray (Boolean)

    if true, forces the use of NArray for the pixel remap process (for faster execution)

  • :remap (Boolean)

    if true, the returned pixel values are remapped to presentation values

Returns:

  • (Array, NilClass, FalseClass)

    an Array of pixel values, alternatively nil (if no image present) or false (if image decode failed)



392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/dicom/image_item.rb', line 392

def pixels(options={})
  pixels = nil
  if exists?(PIXEL_TAG)
    # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
    if compression?
      pixels = decompress(image_strings.first)
    else
      pixels = decode_pixels(image_strings.first)
    end
    if pixels
      # Remap the image from pixel values to presentation values if the user has requested this:
      if options[:remap] or options[:level]
        if options[:narray]
          # Use numerical array (faster):
          pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]).to_a
        else
          # Use standard Ruby array (slower):
          pixels = process_presentation_values(pixels, -65535, 65535, options[:level])
        end
      end
    else
      logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
    end
  end
  return pixels
end

#pixels=(values) ⇒ Object

Encodes pixel data from a Ruby Array or NArray, and writes it to the pixel data element (7FE0,0010).

Parameters:

  • values (Array<Integer>, NArray)

    an Array (or NArray) containing integer pixel values

Raises:

  • (ArgumentError)


423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/dicom/image_item.rb', line 423

def pixels=(values)
  raise ArgumentError, "The given argument does not respond to #to_a (got an argument of class #{values.class})" unless values.respond_to?(:to_a)
  if values.class.ancestors.to_s.include?('NArray')
    # With an NArray argument, make sure that it gets properly converted to an Array:
    if values.shape.length > 2
      # For a 3D NArray we need to rearrange to ensure that the pixels get their
      # proper order when converting to an ordinary Array instance:
      narr = NArray.int(values.shape[1] * values.shape[2], values.shape[0])
      values.shape[0].times do |i|
        narr[true, i] = values[i, true, true].reshape(values.shape[1] * values.shape[2])
      end
      values = narr
    end
  end
  # Encode the pixel data:
  bin = encode_pixels(values.to_a.flatten)
  # Write the binary data to the Pixel Data Element:
  write_pixels(bin)
end