Class: RTKIT::Image

Inherits:
PixelData show all
Defined in:
lib/rtkit/image.rb

Overview

Contains the DICOM data and methods related to an image.

Inheritance

  • As the Image class inherits from the PixelData class, all PixelData methods are available to instances of Image.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from PixelData

#coordinates_from_indices, #coordinates_to_indices, #draw_lines, #flood_fill, #indices_general_to_specific, #indices_specific_to_general, #print_img

Constructor Details

#initialize(sop_uid, series) ⇒ Image

Creates a new Image instance. The SOP Instance UID tag value is used to uniquely identify an image.

Parameters

  • sop_uid – The SOP Instance UID string.

  • series – The Series instance that this Image belongs to.

Raises:

  • (ArgumentError)


65
66
67
68
69
70
71
72
73
74
# File 'lib/rtkit/image.rb', line 65

def initialize(sop_uid, series)
  raise ArgumentError, "Invalid argument 'sop_uid'. Expected String, got #{sop_uid.class}." unless sop_uid.is_a?(String)
  raise ArgumentError, "Invalid argument 'series'. Expected Series, got #{series.class}." unless series.is_a?(Series)
  raise ArgumentError, "Invalid argument 'series'. Expected Series to have an image related modality, got #{series.modality}." unless IMAGE_MODALITIES.include?(series.modality)
  # Key attributes:
  @uid = sop_uid
  @series = series
  # Register ourselves with the ImageSeries:
  @series.add_image(self)
end

Instance Attribute Details

#col_spacingObject

The physical distance (in millimeters) between columns in the pixel data (i.e. horisontal spacing).



12
13
14
# File 'lib/rtkit/image.rb', line 12

def col_spacing
  @col_spacing
end

#columnsObject

The number of columns in the pixel data.



14
15
16
# File 'lib/rtkit/image.rb', line 14

def columns
  @columns
end

#cosinesObject

The values of the Image Orientation (Patient) element.



16
17
18
# File 'lib/rtkit/image.rb', line 16

def cosines
  @cosines
end

#dateObject (readonly)

The Instance Creation Date.



18
19
20
# File 'lib/rtkit/image.rb', line 18

def date
  @date
end

#dcmObject (readonly)

The DICOM object of this Image instance.



20
21
22
# File 'lib/rtkit/image.rb', line 20

def dcm
  @dcm
end

#narrayObject

The 2d NArray holding the pixel data of this Image instance.



22
23
24
# File 'lib/rtkit/image.rb', line 22

def narray
  @narray
end

#pos_sliceObject

The physical position (in millimeters) of the image slice.



24
25
26
# File 'lib/rtkit/image.rb', line 24

def pos_slice
  @pos_slice
end

#pos_xObject

The physical position (in millimeters) of the first (left) column in the pixel data.



26
27
28
# File 'lib/rtkit/image.rb', line 26

def pos_x
  @pos_x
end

#pos_yObject

The physical position (in millimeters) of the first (top) row in the pixel data.



28
29
30
# File 'lib/rtkit/image.rb', line 28

def pos_y
  @pos_y
end

#row_spacingObject

The physical distance (in millimeters) between rows in the pixel data (i.e. vertical spacing).



30
31
32
# File 'lib/rtkit/image.rb', line 30

def row_spacing
  @row_spacing
end

#rowsObject

The number of rows in the pixel data.



32
33
34
# File 'lib/rtkit/image.rb', line 32

def rows
  @rows
end

#seriesObject (readonly)

The Image’s Series (volume) reference.



34
35
36
# File 'lib/rtkit/image.rb', line 34

def series
  @series
end

#timeObject (readonly)

The Instance Creation Time.



36
37
38
# File 'lib/rtkit/image.rb', line 36

def time
  @time
end

#uidObject (readonly)

The SOP Instance UID.



38
39
40
# File 'lib/rtkit/image.rb', line 38

def uid
  @uid
end

Class Method Details

.load(dcm, series) ⇒ Object

Creates a new Image instance by loading image information from the specified DICOM object. The Image object’s SOP Instance UID string value is used to uniquely identify an image.

Parameters

  • dcm – An instance of a DICOM object (DObject).

  • series – The Series instance that this Image belongs to.

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
# File 'lib/rtkit/image.rb', line 48

def self.load(dcm, series)
  raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
  raise ArgumentError, "Invalid argument 'series'. Expected Series, got #{series.class}." unless series.is_a?(Series)
  raise ArgumentError, "Invalid argument 'dcm'. Expected an image related modality, got #{dcm.value(MODALITY)}." unless IMAGE_MODALITIES.include?(dcm.value(MODALITY))
  sop_uid = dcm.value(SOP_UID)
  image = self.new(sop_uid, series)
  image.load_pixel_data(dcm)
  return image
end

Instance Method Details

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

Returns true if the argument is an instance with attributes equal to self.



78
79
80
81
82
# File 'lib/rtkit/image.rb', line 78

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

#binary_image(coords_x, coords_y, coords_z) ⇒ Object

Creates and returns a filled, binary NArray image (a ‘segmented’ image) based on the provided contour coordinates.

Parameters

  • coords_x – An Array/NArray of a contour’s X coordinates. Must have at least 3 elements.

  • coords_y – An Array/NArray of a contour’s Y coordinates. Must have at least 3 elements.

  • coords_z – An Array/NArray of a contour’s Z coordinates. Must have at least 3 elements.

Raises:

  • (ArgumentError)


94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/rtkit/image.rb', line 94

def binary_image(coords_x, coords_y, coords_z)
  raise ArgumentError, "Invalid argument 'coords_x'. Expected at least 3 elements, got #{coords_x.length}" unless coords_x.length >= 3
  raise ArgumentError, "Invalid argument 'coords_y'. Expected at least 3 elements, got #{coords_y.length}" unless coords_y.length >= 3
  raise ArgumentError, "Invalid argument 'coords_z'. Expected at least 3 elements, got #{coords_z.length}" unless coords_z.length >= 3
  # Values that will be used for image geometry:
  empty_value = 0
  line_value = 1
  fill_value = 2
  # Convert physical coordinates to image indices:
  column_indices, row_indices = coordinates_to_indices(NArray.to_na(coords_x), NArray.to_na(coords_y), NArray.to_na(coords_z))
  # Create an empty array and fill in the gathered points:
  empty_array = NArray.byte(@columns, @rows)
  delineated_array = draw_lines(column_indices.to_a, row_indices.to_a, empty_array, line_value)
  # Establish starting point indices for the coming flood fill algorithm:
  # (Using a rather simple approach by finding the average column and row index among the selection of indices)
  start_col = column_indices.mean
  start_row = row_indices.mean
  # Perform a flood fill to enable us to extract all pixels contained in a specific ROI:
  filled_array = flood_fill(start_col, start_row, delineated_array, fill_value)
  # Extract the indices of 'ROI pixels':
  if filled_array[0,0] != fill_value
    # ROI has been filled as expected. Extract indices of value line_value and fill_value:
    filled_array[(filled_array.eq line_value).where] = fill_value
    indices = (filled_array.eq fill_value).where
  else
    # An inversion has occured. The entire image except our ROI has been filled. Extract indices of value line_value and empty_value:
    filled_array[(filled_array.eq line_value).where] = empty_value
    indices = (filled_array.eq empty_value).where
  end
  # Create binary image:
  bin_image = NArray.byte(@columns, @rows)
  bin_image[indices] = 1
  return bin_image
end

#hashObject

Generates a Fixnum hash value for this instance.



151
152
153
# File 'lib/rtkit/image.rb', line 151

def hash
  state.hash
end

#load_pixel_data(dcm) ⇒ Object

Transfers the pixel data, as well as the related image properties and the DObject instance itself, to the Image instance.

Parameters

  • dcm – A DICOM object containing image data that will be applied to the Image instance.

Raises:

  • (ArgumentError)


162
163
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/rtkit/image.rb', line 162

def load_pixel_data(dcm)
  raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
  raise ArgumentError, "Invalid argument 'dcm'. Expected an image related modality, got #{dcm.value(MODALITY)}." unless IMAGE_MODALITIES.include?(dcm.value(MODALITY))
  # Set attributes common for all image modalities, i.e. CT, MR, RTDOSE & RTIMAGE:
  @dcm = dcm
  @narray = dcm.narray
  @date = dcm.value(IMAGE_DATE)
  @time = dcm.value(IMAGE_TIME)
  @uid = dcm.value(SOP_UID)
  @columns = dcm.value(COLUMNS)
  @rows = dcm.value(ROWS)
  # Some difference in where we pick our values depending on if we have an RTIMAGE or another type:
  if @series.modality == 'RTIMAGE'
    image_position = dcm.value(RT_IMAGE_POSITION).split("\\")
    raise "Invalid DICOM image: 2 basckslash-separated values expected for RT Image Position (Patient), got: #{image_position}" unless image_position.length == 2
    @pos_x = image_position[0].to_f
    @pos_y = image_position[1].to_f
    @pos_slice = nil
    spacing = dcm.value(IMAGE_PLANE_SPACING).split("\\")
    raise "Invalid DICOM image: 2 basckslash-separated values expected for Image Plane Pixel Spacing, got: #{spacing}" unless spacing.length == 2
    @col_spacing = spacing[1].to_f
    @row_spacing = spacing[0].to_f
  else
    image_position = dcm.value(IMAGE_POSITION).split("\\")
    raise "Invalid DICOM image: 3 basckslash-separated values expected for Image Position (Patient), got: #{image_position}" unless image_position.length == 3
    @pos_x = image_position[0].to_f
    @pos_y = image_position[1].to_f
    self.pos_slice = image_position[2].to_f
    spacing = dcm.value(SPACING).split("\\")
    raise "Invalid DICOM image: 2 basckslash-separated values expected for Pixel Spacing, got: #{spacing}" unless spacing.length == 2
    @col_spacing = spacing[1].to_f
    @row_spacing = spacing[0].to_f
    raise "Invalid DICOM image: Direction cosines missing (DICOM tag '#{IMAGE_ORIENTATION}')." unless dcm.exists?(IMAGE_ORIENTATION)
    @cosines = dcm.value(IMAGE_ORIENTATION).split("\\").collect {|val| val.to_f} if dcm.value(IMAGE_ORIENTATION)
    raise "Invalid DICOM image: 6 values expected for direction cosines (DICOM tag '#{IMAGE_ORIENTATION}'), got #{@cosines.length}." unless @cosines.length == 6
  end
end

#pixel_areaObject

Calculates the area of a single pixel of this image. Returns a float value, in units of millimeters squared.



212
213
214
# File 'lib/rtkit/image.rb', line 212

def pixel_area
  return @row_spacing * @col_spacing
end

#pixel_values(selection) ⇒ Object

Extracts pixel values from the image based on the given indices.

Raises:

  • (ArgumentError)


218
219
220
221
# File 'lib/rtkit/image.rb', line 218

def pixel_values(selection)
  raise ArgumentError, "Invalid argument 'selection'. Expected Selection, got #{selection.class}" unless selection.is_a?(Selection)
  return @narray[selection.indices]
end

#set_resolution(columns, rows, options = {}) ⇒ Object

Sets the resolution of the image. This modifies the pixel data (in the specified way) and the column/row attributes as well. The image will either be expanded or cropped depending on whether the specified resolution is bigger or smaller than the existing one.

Parameters

  • columns – Integer. The number of columns applied to the cropped/expanded image.

  • rows – Integer. The number of rows applied to the cropped/expanded image.

Options

  • :hor – Symbol. The side (in the horisontal image direction) to apply the crop/border (:left, :right or :even (default)).

  • :ver – Symbol. The side (in the vertical image direction) to apply the crop/border (:bottom, :top or :even (default)).



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
# File 'lib/rtkit/image.rb', line 270

def set_resolution(columns, rows, options={})
  options[:hor] = :even unless options[:hor]
  options[:ver] = :even unless options[:ver]
  old_cols = @narray.shape[0]
  old_rows = @narray.shape[1]
  if @narray
    # Modify the width only if changed:
    if columns != old_cols
      self.columns = columns.to_i
      old_arr = @narray.dup
      @narray = NArray.int(@columns, @rows)
      if @columns > old_cols
        # New array is larger:
        case options[:hor]
        when :left then @narray[(@columns-old_cols)..(@columns-1), true] = old_arr
        when :right then @narray[0..(old_cols-1), true] = old_arr
        when :even then @narray[((@columns-old_cols)/2+(@columns-old_cols).remainder(2))..(@columns-1-(@columns-old_cols)/2), true] = old_arr
        end
      else
        # New array is smaller:
        case options[:hor]
        when :left then @narray = old_arr[(old_cols-@columns)..(old_cols-1), true]
        when :right then @narray = old_arr[0..(@columns-1), true]
        when :even then @narray = old_arr[((old_cols-@columns)/2+(old_cols-@columns).remainder(2))..(old_cols-1-(old_cols-@columns)/2), true]
        end
      end
    end
    # Modify the height only if changed:
    if rows != old_rows
      self.rows = rows.to_i
      old_arr = @narray.dup
      @narray = NArray.int(@columns, @rows)
      if @rows > old_rows
        # New array is larger:
        case options[:ver]
        when :top then @narray[true, (@rows-old_rows)..(@rows-1)] = old_arr
        when :bottom then @narray[true, 0..(old_rows-1)] = old_arr
        when :even then @narray[true, ((@rows-old_rows)/2+(@rows-old_rows).remainder(2))..(@rows-1-(@rows-old_rows)/2)] = old_arr
        end
      else
        # New array is smaller:
        case options[:ver]
        when :top then @narray = old_arr[true, (old_rows-@rows)..(old_rows-1)]
        when :bottom then @narray = old_arr[true, 0..(@rows-1)]
        when :even then @narray = old_arr[true, ((old_rows-@rows)/2+(old_rows-@rows).remainder(2))..(old_rows-1-(old_rows-@rows)/2)]
        end
      end
    end
  end
end

#to_dcmObject

Dumps the Image instance to a DObject. This overwrites the dcm instance attribute. Returns the DObject instance.



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/rtkit/image.rb', line 325

def to_dcm
  # Use the original DICOM object as a starting point,
  # and update all image related parameters:
  @dcm.add(DICOM::Element.new(IMAGE_DATE, @date))
  @dcm.add(DICOM::Element.new(IMAGE_TIME, @time))
  @dcm.add(DICOM::Element.new(SOP_UID, @uid))
  @dcm.add(DICOM::Element.new(COLUMNS, @columns))
  @dcm.add(DICOM::Element.new(ROWS, @rows))
  if @series.modality == 'RTIMAGE'
    @dcm.add(DICOM::Element.new(RT_IMAGE_POSITION, [@pos_x, @pos_y].join("\\")))
    @dcm.add(DICOM::Element.new(IMAGE_PLANE_SPACING, [@row_spacing, @col_spacing].join("\\")))
  else
    @dcm.add(DICOM::Element.new(IMAGE_POSITION, [@pos_x, @pos_y, @pos_slice].join("\\")))
    @dcm.add(DICOM::Element.new(SPACING, [@row_spacing, @col_spacing].join("\\")))
    @dcm.add(DICOM::Element.new(IMAGE_ORIENTATION, [@cosines].join("\\")))
  end
  # Write pixel data:
  @dcm.pixels = @narray
  return @dcm
end

#to_imageObject

Returns self.



348
349
350
# File 'lib/rtkit/image.rb', line 348

def to_image
  self
end

#write(file_name) ⇒ Object

Writes the Image to a DICOM file given by the specified file string.



354
355
356
357
# File 'lib/rtkit/image.rb', line 354

def write(file_name)
  to_dcm
  @dcm.write(file_name)
end