Class: RTKIT::ROI

Inherits:
Object
  • Object
show all
Defined in:
lib/rtkit/roi.rb

Overview

Contains DICOM data and methods related to a Region of Interest, defined in a Structure Set.

Relations

  • An image series has many ROIs, defined through a Structure Set.

  • An image slice has only the ROIs which are contoured in that particular slice in the Structure Set.

  • A ROI has many Slices.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, number, frame, struct, options = {}) ⇒ ROI

Creates a new ROI instance.

Parameters

  • name – String. The ROI Name.

  • number – Integer. The ROI Number.

  • frame – The Frame instance that this ROI belongs to.

  • struct – The StructureSet instance that this ROI belongs to.

  • options – A hash of parameters.

Options

  • :algorithm – String. The ROI Generation Algorithm. Defaults to ‘Automatic’.

  • :color – String. The ROI Display Color. Defaults to a random color string (format: ‘xyz’ where [x,y,z] is a byte (0-255)).

  • :interpreter – String. The ROI Interpreter. Defaults to ‘RTKIT’.

  • :type – String. The ROI Interpreted Type. Defaults to ‘CONTROL’.

Raises:

  • (ArgumentError)


87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/rtkit/roi.rb', line 87

def initialize(name, number, frame, struct, options={})
  raise ArgumentError, "Invalid argument 'name'. Expected String, got #{name.class}." unless name.is_a?(String)
  raise ArgumentError, "Invalid argument 'number'. Expected Integer, got #{number.class}." unless number.is_a?(Integer)
  raise ArgumentError, "Invalid argument 'frame'. Expected Frame, got #{frame.class}." unless frame.is_a?(Frame)
  raise ArgumentError, "Invalid argument 'struct'. Expected StructureSet, got #{struct.class}." unless struct.is_a?(StructureSet)
  raise ArgumentError, "Invalid option :algorithm. Expected String, got #{options[:algorithm].class}." if options[:algorithm] && !options[:algorithm].is_a?(String)
  raise ArgumentError, "Invalid option :color. Expected String, got #{options[:color].class}." if options[:color] && !options[:color].is_a?(String)
  raise ArgumentError, "Invalid option :interpreter. Expected String, got #{options[:interpreter].class}." if options[:interpreter] && !options[:interpreter].is_a?(String)
  raise ArgumentError, "Invalid option :type. Expected String, got #{options[:type].class}." if options[:type] && !options[:type].is_a?(String)
  @slices = Array.new
  @associated_instance_uids = Hash.new
  # Set values:
  @number = number
  @name = name
  @algorithm = options[:algorithm] || 'Automatic'
  @type = options[:type] || 'CONTROL'
  @interpreter = options[:interpreter] || 'RTKIT'
  @color = options[:color] || random_color
  # Set references:
  @frame = frame
  @struct = struct
  # Register ourselves with the Frame and StructureSet:
  @frame.add_roi(self)
  @struct.add_roi(self)
end

Instance Attribute Details

#algorithmObject

ROI Generation Algorithm.



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

def algorithm
  @algorithm
end

#colorObject

ROI Display Color.



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

def color
  @color
end

#frameObject (readonly)

The Frame which this ROI belongs to.



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

def frame
  @frame
end

#interpreterObject

ROI Interpreter.



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

def interpreter
  @interpreter
end

#nameObject

ROI Name.



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

def name
  @name
end

#numberObject

ROI Number (Integer).



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

def number
  @number
end

#slicesObject (readonly)

An array containing the Slices that the ROI is defined in.



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

def slices
  @slices
end

#structObject (readonly)

The StructureSet that the ROI is defined in.



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

def struct
  @struct
end

#typeObject

RT ROI Interpreted Type.



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

def type
  @type
end

Class Method Details

.create_from_items(roi_item, contour_item, rt_item, struct) ⇒ Object

Creates a new ROI instance from the three items of the structure set which contains the information related to a particular ROI. This method also creates and connects any child structures as indicated in the items (e.g. Slices). Returns the ROI instance.

Parameters

  • roi_item – The ROI’s Item from the Structure Set ROI Sequence in the DObject of a Structure Set.

  • contour_item – The ROI’s Item from the ROI Contour Sequence in the DObject of a Structure Set.

  • rt_item – The ROI’s Item from the RT ROI Observations Sequence in the DObject of a Structure Set.

  • struct – The StructureSet instance that this ROI belongs to.

Raises:

  • (ArgumentError)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rtkit/roi.rb', line 44

def self.create_from_items(roi_item, contour_item, rt_item, struct)
  raise ArgumentError, "Invalid argument 'roi_item'. Expected DICOM::Item, got #{roi_item.class}." unless roi_item.is_a?(DICOM::Item)
  raise ArgumentError, "Invalid argument 'contour_item'. Expected DICOM::Item, got #{contour_item.class}." unless contour_item.is_a?(DICOM::Item)
  raise ArgumentError, "Invalid argument 'rt_item'. Expected DICOM::Item, got #{rt_item.class}." unless rt_item.is_a?(DICOM::Item)
  raise ArgumentError, "Invalid argument 'struct'. Expected StructureSet, got #{struct.class}." unless struct.is_a?(StructureSet)
  # Values from the Structure Set ROI Sequence Item:
  number = roi_item.value(ROI_NUMBER).to_i
  frame_of_ref = roi_item.value(REF_FRAME_OF_REF)
  name = roi_item.value(ROI_NAME) || ''
  algorithm = roi_item.value(ROI_ALGORITHM) || ''
  # Values from the RT ROI Observations Sequence Item:
  type = rt_item.value(ROI_TYPE) || ''
  interpreter = rt_item.value(ROI_INTERPRETER) || ''
  # Values from the ROI Contour Sequence Item:
  color = contour_item.value(ROI_COLOR)
  # Get the frame:
  frame = struct.study.patient.dataset.frame(frame_of_ref)
  # If the frame didnt exist, create it (assuming the frame belongs to our patient):
  frame = struct.study.patient.create_frame(frame_of_ref) unless frame
  # Create the ROI instance:
  roi = self.new(name, number, frame, struct, :algorithm => algorithm, :color => color, :interpreter => interpreter, :type => type)
  # Create the Slices in which the ROI has contours defined:
  roi.create_slices(contour_item[CONTOUR_SQ]) if contour_item[CONTOUR_SQ]
  return roi
end

Instance Method Details

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

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



115
116
117
118
119
# File 'lib/rtkit/roi.rb', line 115

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

#add_slice(slice) ⇒ Object

Adds a Slice instance to this ROI.

Raises:

  • (ArgumentError)


125
126
127
128
129
# File 'lib/rtkit/roi.rb', line 125

def add_slice(slice)
  raise ArgumentError, "Invalid argument 'slice'. Expected Slice, got #{slice.class}." unless slice.is_a?(Slice)
  @slices << slice unless @associated_instance_uids[slice.uid]
  @associated_instance_uids[slice.uid] = slice
end

#attach_to(series) ⇒ Object

Attaches a ROI to a specified ImageSeries, by setting the ROIs frame reference to the Frame which the ImageSeries belongs to, and setting the Image reference of each of the Slices belonging to the ROI to an Image instance which matches the coordinates of the Slice’s Contour(s). Raises an exception if a suitable match is not found for any Slice.

Notes

This method can be useful when you have multiple segmentations based on the same image series from multiple raters (perhaps as part of a comparison study), and the rater’s software has modified the UIDs of the original image series, so that the references of the returned Structure Set does not match your original image series. This method uses coordinate information to calculate plane equations, which allows it to identify the corresponding image slice even in the case of slice geometry being non-perpendicular with respect to the patient geometry (direction cosine values != [0,1]).

Raises:

  • (ArgumentError)


151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/rtkit/roi.rb', line 151

def attach_to(series)
  raise ArgumentError, "Invalid argument 'series'. Expected ImageSeries, got #{series.class}." unless series.is_a?(Series)
  # Change struct association if indicated:
  if series.struct != @struct
    @struct.remove_roi(self)
    series.struct.add_roi(self)
    @struct = series.struct
  end
  # Change Frame if different:
  if @frame != series.frame
    @frame = series.frame
  end
  # Update slices:
  @slices.each do |slice|
    slice.attach_to(series)
  end
end

#bin_volume(image_volume = @struct.image_series.first) ⇒ Object

Creates a binary volume object consisting of a series of binary (segmented) images, extracted from the contours defined for the slices of this ROI. Returns a BinVolume instance with binary image references equal to the number of slices defined for this ROI.

Parameters

  • image_volume – By default the BinVolume is created against the ImageSeries of the ROI’s StructureSet. Optionally, a DoseVolume can be specified.



178
179
180
# File 'lib/rtkit/roi.rb', line 178

def bin_volume(image_volume=@struct.image_series.first)
  return BinVolume.from_roi(self, image_volume)
end

#contour_itemObject

Creates and returns a ROI Contour Sequence Item from the attributes of the ROI.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/rtkit/roi.rb', line 202

def contour_item
  item = DICOM::Item.new
  item.add(DICOM::Element.new(ROI_COLOR, @color))
  s = DICOM::Sequence.new(CONTOUR_SQ)
  item.add(s)
  item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
  # Add Contour items to the Contour Sequence (one or several items per Slice):
  @slices.each do |slice|
    slice.contours.each do |contour|
      s.add_item(contour.to_item)
    end
  end
  return item
end

#create_slices(contour_sequence) ⇒ Object

Iterates the Contour Sequence Items, collects Contour Items for each slice and passes them along to the Slice class.

Raises:

  • (ArgumentError)


219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/rtkit/roi.rb', line 219

def create_slices(contour_sequence)
  raise ArgumentError, "Invalid argument 'contour_sequence'. Expected DICOM::Sequence, got #{contour_sequence.class}." unless contour_sequence.is_a?(DICOM::Sequence)
  # Sort the contours by slices:
  slice_collection = Hash.new
  contour_sequence.each do |slice_contour_item|
    sop_uid = slice_contour_item[CONTOUR_IMAGE_SQ][0].value(REF_SOP_UID)
    slice_collection[sop_uid] = Array.new unless slice_collection[sop_uid]
    slice_collection[sop_uid] << slice_contour_item
  end
  # Create slices:
  slice_collection.each_pair do |sop_uid, items|
    Slice.create_from_items(sop_uid, items, self)
  end
end

#distribution(dose_volume = @struct.plan.rt_dose.sum) ⇒ Object

Creates a DoseDistribution based on the delineation of this ROI in the specified RTDose series.

Parameters

  • dose_volume – The DoseVolume to extract the dose distribution from. Defaults to the sum of the dose volumes of the first RTDose of the first plan of the parent StructureSet.

Raises:

  • (ArgumentError)


241
242
243
244
245
246
247
248
249
# File 'lib/rtkit/roi.rb', line 241

def distribution(dose_volume=@struct.plan.rt_dose.sum)
  raise ArgumentError, "Invalid argument 'dose_volume'. Expected DoseVolume, got #{dose_volume.class}." unless dose_volume.is_a?(DoseVolume)
  raise ArgumentError, "Invalid argument 'dose_volume'. The specified DoseVolume does not belong to this ROI's StructureSet." unless dose_volume.dose_series.plan.struct == @struct
  # Extract a binary volume from the ROI, based on the dose data:
  bin_vol = bin_volume(dose_volume)
  # Create a DoseDistribution from the BinVolume:
  dose_distribution = DoseDistribution.create(bin_vol)
  return dose_distribution
end

#hashObject

Generates a Fixnum hash value for this instance.



253
254
255
# File 'lib/rtkit/roi.rb', line 253

def hash
  state.hash
end

#image_seriesObject

Returns the ImageSeries instance that this ROI is defined in.



259
260
261
# File 'lib/rtkit/roi.rb', line 259

def image_series
  return @struct.image_series.first
end

#num_contoursObject

Returns the number of Contours belonging to this ROI through its Slices.



277
278
279
280
281
282
283
# File 'lib/rtkit/roi.rb', line 277

def num_contours
  num = 0
  @slices.each do |slice|
    num += slice.contours.length
  end
  return num
end

#obs_itemObject

Creates and returns a RT ROI Obervations Sequence Item from the attributes of the ROI.



293
294
295
296
297
298
299
300
# File 'lib/rtkit/roi.rb', line 293

def obs_item
  item = DICOM::Item.new
  item.add(DICOM::Element.new(OBS_NUMBER, @number.to_s))
  item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
  item.add(DICOM::Element.new(ROI_TYPE, @type))
  item.add(DICOM::Element.new(ROI_INTERPRETER, @interpreter))
  return item
end

#remove_referencesObject

Removes the parent references of the ROI (StructureSet and Frame).



304
305
306
307
# File 'lib/rtkit/roi.rb', line 304

def remove_references
  @frame = nil
  @struct = nil
end

#sizeObject

Calculates the size (volume) of the ROI by evaluating the ROI’s delination in the referenced image series. Returns a float, giving the volume in units of cubic centimeters,



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/rtkit/roi.rb', line 313

def size
  volume = 0.0
  # Iterate each slice:
  @slices.each_index do |i|
    # Get the contoured area in this slice, convert it to volume and add to our total.
    # If the slice is the first or last, only multiply by half of the slice thickness:
    if i == 0 or i == @slices.length-1
      volume += slice.area * image_series.slice_spacing * 0.5
    else
      volume += slice.area * image_series.slice_spacing
    end
  end
  # Convert from mm^3 to cm^3:
  return volume / 1000.0
end

#slice(*args) ⇒ Object

Returns the Slice instance mathcing the specified SOP Instance UID (if an argument is used). If a specified UID doesn’t match, nil is returned. If no argument is passed, the first Slice instance associated with the ROI is returned.

Parameters

  • uid – String. The value of the SOP Instance UID element.

Raises:

  • (ArgumentError)


337
338
339
340
341
342
343
344
345
346
# File 'lib/rtkit/roi.rb', line 337

def slice(*args)
  raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
  if args.length == 1
    raise ArgumentError, "Invalid argument 'uid'. Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
    return @associated_instance_uids[args.first]
  else
    # No argument used, therefore we return the first Image instance:
    return @slices.first
  end
end

#ss_itemObject

Creates and returns a Structure Set ROI Sequence Item from the attributes of the ROI.



350
351
352
353
354
355
356
357
# File 'lib/rtkit/roi.rb', line 350

def ss_item
  item = DICOM::Item.new
  item.add(DICOM::Element.new(ROI_NUMBER, @number.to_s))
  item.add(DICOM::Element.new(REF_FRAME_OF_REF, @frame.uid))
  item.add(DICOM::Element.new(ROI_NAME, @name))
  item.add(DICOM::Element.new(ROI_ALGORITHM, @algorithm))
  return item
end

#to_roiObject

Returns self.



361
362
363
# File 'lib/rtkit/roi.rb', line 361

def to_roi
  self
end