rtkit 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +10 -0
- data/COPYING +674 -0
- data/README.rdoc +107 -0
- data/lib/rtkit.rb +68 -0
- data/lib/rtkit/beam.rb +346 -0
- data/lib/rtkit/bin_image.rb +578 -0
- data/lib/rtkit/bin_matcher.rb +241 -0
- data/lib/rtkit/bin_volume.rb +263 -0
- data/lib/rtkit/collimator.rb +157 -0
- data/lib/rtkit/collimator_setup.rb +143 -0
- data/lib/rtkit/constants.rb +215 -0
- data/lib/rtkit/contour.rb +213 -0
- data/lib/rtkit/control_point.rb +371 -0
- data/lib/rtkit/coordinate.rb +83 -0
- data/lib/rtkit/data_set.rb +264 -0
- data/lib/rtkit/dose.rb +70 -0
- data/lib/rtkit/dose_distribution.rb +206 -0
- data/lib/rtkit/dose_volume.rb +280 -0
- data/lib/rtkit/frame.rb +164 -0
- data/lib/rtkit/image.rb +372 -0
- data/lib/rtkit/image_series.rb +290 -0
- data/lib/rtkit/logging.rb +158 -0
- data/lib/rtkit/methods.rb +105 -0
- data/lib/rtkit/mixins/image_parent.rb +40 -0
- data/lib/rtkit/patient.rb +229 -0
- data/lib/rtkit/pixel_data.rb +237 -0
- data/lib/rtkit/plan.rb +259 -0
- data/lib/rtkit/plane.rb +165 -0
- data/lib/rtkit/roi.rb +388 -0
- data/lib/rtkit/rt_dose.rb +237 -0
- data/lib/rtkit/rt_image.rb +179 -0
- data/lib/rtkit/ruby_extensions.rb +165 -0
- data/lib/rtkit/selection.rb +189 -0
- data/lib/rtkit/series.rb +77 -0
- data/lib/rtkit/setup.rb +198 -0
- data/lib/rtkit/slice.rb +184 -0
- data/lib/rtkit/staple.rb +305 -0
- data/lib/rtkit/structure_set.rb +442 -0
- data/lib/rtkit/study.rb +214 -0
- data/lib/rtkit/variables.rb +23 -0
- data/lib/rtkit/version.rb +6 -0
- metadata +159 -0
@@ -0,0 +1,237 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# The RTDose class contains methods that are specific for this modality (RTDOSE).
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# * RTDose inherits all methods and attributes from the Series class.
|
8
|
+
#
|
9
|
+
class RTDose < Series
|
10
|
+
|
11
|
+
# The Plan which this RTDose series belongs to.
|
12
|
+
attr_reader :plan
|
13
|
+
# An array of dose Volume instances associated with this RTDose series.
|
14
|
+
attr_accessor :volumes
|
15
|
+
|
16
|
+
# Creates a new RTDose instance by loading the relevant information from the specified DICOM object.
|
17
|
+
# The Series Instance UID string value is used to uniquely identify a RTDose instance.
|
18
|
+
#
|
19
|
+
# === Parameters
|
20
|
+
#
|
21
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTDOSE'.
|
22
|
+
# * <tt>study</tt> -- The Study instance that this RTDose belongs to.
|
23
|
+
#
|
24
|
+
def self.load(dcm, study)
|
25
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
26
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
27
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
|
28
|
+
# Required attributes:
|
29
|
+
series_uid = dcm.value(SERIES_UID)
|
30
|
+
# Optional attributes:
|
31
|
+
class_uid = dcm.value(SOP_CLASS)
|
32
|
+
date = dcm.value(SERIES_DATE)
|
33
|
+
time = dcm.value(SERIES_TIME)
|
34
|
+
description = dcm.value(SERIES_DESCR)
|
35
|
+
series_uid = dcm.value(SERIES_UID)
|
36
|
+
# Get the corresponding Plan:
|
37
|
+
plan = self.plan(dcm, study)
|
38
|
+
# Create the RTDose instance:
|
39
|
+
dose = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
|
40
|
+
dose.add(dcm)
|
41
|
+
return dose
|
42
|
+
end
|
43
|
+
|
44
|
+
# Identifies the Plan that the RTDose object belongs to.
|
45
|
+
# If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
|
46
|
+
#
|
47
|
+
def self.plan(dcm, study)
|
48
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
49
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
50
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
|
51
|
+
# Extract the Frame of Reference UID:
|
52
|
+
begin
|
53
|
+
frame_of_ref = dcm.value(FRAME_OF_REF)
|
54
|
+
rescue
|
55
|
+
frame_of_ref = nil
|
56
|
+
end
|
57
|
+
# Extract referenced Plan SOP Instance UID:
|
58
|
+
begin
|
59
|
+
ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
|
60
|
+
rescue
|
61
|
+
ref_plan_uid = nil
|
62
|
+
end
|
63
|
+
# Create the Frame if it doesn't exist:
|
64
|
+
f = study.patient.dataset.frame(frame_of_ref)
|
65
|
+
f = Frame.new(frame_of_ref, study.patient) unless f
|
66
|
+
# Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
|
67
|
+
plan = study.fseries(ref_plan_uid)
|
68
|
+
unless plan
|
69
|
+
# Create ImageSeries (assuming modality CT):
|
70
|
+
is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
|
71
|
+
study.add_series(is)
|
72
|
+
# Create StructureSet:
|
73
|
+
struct = StructureSet.new(RTKIT.sop_uid, is)
|
74
|
+
study.add_series(struct)
|
75
|
+
# Create Plan:
|
76
|
+
plan = Plan.new(ref_plan_uid, struct)
|
77
|
+
study.add_series(plan)
|
78
|
+
end
|
79
|
+
return plan
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates a new RTDose instance.
|
83
|
+
#
|
84
|
+
# === Parameters
|
85
|
+
#
|
86
|
+
# * <tt>series_uid</tt> -- The Series Instance UID string.
|
87
|
+
# * <tt>plan</tt> -- The Plan instance that this RTDose series belongs to.
|
88
|
+
# * <tt>options</tt> -- A hash of parameters.
|
89
|
+
#
|
90
|
+
# === Options
|
91
|
+
#
|
92
|
+
# * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
|
93
|
+
# * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
|
94
|
+
# * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
|
95
|
+
#
|
96
|
+
def initialize(series_uid, plan, options={})
|
97
|
+
raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
|
98
|
+
raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
|
99
|
+
# Pass attributes to Series initialization:
|
100
|
+
options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.2' # RT Dose Storage
|
101
|
+
super(series_uid, 'RTDOSE', plan.study, options)
|
102
|
+
@plan = plan
|
103
|
+
# Default attributes:
|
104
|
+
@volumes = Array.new
|
105
|
+
@associated_volumes = Hash.new
|
106
|
+
# Register ourselves with the Plan:
|
107
|
+
@plan.add_rt_dose(self)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
111
|
+
#
|
112
|
+
def ==(other)
|
113
|
+
if other.respond_to?(:to_rt_dose)
|
114
|
+
other.send(:state) == state
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
alias_method :eql?, :==
|
119
|
+
|
120
|
+
# Registers a DICOM Object to the RTDose series, and processes it
|
121
|
+
# to create (and reference) a DoseVolume instance linked to this RTDose series.
|
122
|
+
#
|
123
|
+
def add(dcm)
|
124
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
125
|
+
DoseVolume.load(dcm, self)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Adds a DoseVolume instance to this RTDose series.
|
129
|
+
#
|
130
|
+
def add_volume(volume)
|
131
|
+
raise ArgumentError, "Invalid argument 'volume'. Expected DoseVolume, got #{volume.class}." unless volume.is_a?(DoseVolume)
|
132
|
+
@volumes << volume unless @associated_volumes[volume.uid]
|
133
|
+
@associated_volumes[volume.uid] = volume
|
134
|
+
end
|
135
|
+
|
136
|
+
# Generates a Fixnum hash value for this instance.
|
137
|
+
#
|
138
|
+
def hash
|
139
|
+
state.hash
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns a DoseVolume which is the sum of the volumes of this instance.
|
143
|
+
# With the individual DoseVolumes corresponding to the dose for a particular
|
144
|
+
# beam, the sum DoseVolume corresponds to the summed dose of the entire
|
145
|
+
# treatment plan.
|
146
|
+
#
|
147
|
+
def sum
|
148
|
+
if @sum
|
149
|
+
# If the sum volume has already been created, return it instead of recreating:
|
150
|
+
return @sum
|
151
|
+
else
|
152
|
+
if @volumes.length > 0
|
153
|
+
nr_frames = @volumes.first.images.length
|
154
|
+
# Create the sum DoseVolume instance:
|
155
|
+
sop_uid = @volumes.first.sop_uid + ".1"
|
156
|
+
@sum = DoseVolume.new(sop_uid, @volumes.first.frame, @volumes.first.dose_series, :sum => true)
|
157
|
+
# Sum the dose of the various DoseVolumes:
|
158
|
+
dose_sum = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
|
159
|
+
@volumes.each { |volume| dose_sum += volume.dose_arr }
|
160
|
+
# Convert dose float array to integer pixel values of a suitable range,
|
161
|
+
# along with a corresponding scaling factor:
|
162
|
+
sum_scaling_coeff = dose_sum.max / 65000.0
|
163
|
+
if sum_scaling_coeff == 0.0
|
164
|
+
pixel_values = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
|
165
|
+
else
|
166
|
+
pixel_values = dose_sum * (1 / sum_scaling_coeff)
|
167
|
+
end
|
168
|
+
# Set the scaling coeffecient:
|
169
|
+
@sum.scaling = sum_scaling_coeff
|
170
|
+
# Collect the rest of the image information needed to create new dose images:
|
171
|
+
sop_uids = RTKIT.sop_uids(nr_frames)
|
172
|
+
slice_positions = @volumes.first.images.collect {|img| img.pos_slice}
|
173
|
+
columns = @volumes.first.images.first.columns
|
174
|
+
rows = @volumes.first.images.first.rows
|
175
|
+
pos_x = @volumes.first.images.first.pos_x
|
176
|
+
pos_y = @volumes.first.images.first.pos_y
|
177
|
+
col_spacing = @volumes.first.images.first.col_spacing
|
178
|
+
row_spacing = @volumes.first.images.first.row_spacing
|
179
|
+
cosines = @volumes.first.images.first.cosines
|
180
|
+
# Create dose images for our sum dose volume:
|
181
|
+
nr_frames.times do |i|
|
182
|
+
img = Image.new(sop_uids[i], @sum)
|
183
|
+
# Fill in image information:
|
184
|
+
img.columns = columns
|
185
|
+
img.rows = rows
|
186
|
+
img.pos_x = pos_x
|
187
|
+
img.pos_y = pos_y
|
188
|
+
img.pos_slice = slice_positions[i]
|
189
|
+
img.col_spacing = col_spacing
|
190
|
+
img.row_spacing = row_spacing
|
191
|
+
img.cosines = cosines
|
192
|
+
# Fill in the pixel frame data:
|
193
|
+
img.narray = pixel_values[i, true, true]
|
194
|
+
end
|
195
|
+
return @sum
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns self.
|
201
|
+
#
|
202
|
+
def to_rt_dose
|
203
|
+
self
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns the Volume instance mathcing the specified SOP Instance UID (if an argument is used).
|
207
|
+
# If a specified UID doesn't match, nil is returned.
|
208
|
+
# If no argument is passed, the first Volume instance associated with the RTDose is returned.
|
209
|
+
#
|
210
|
+
# === Parameters
|
211
|
+
#
|
212
|
+
# * <tt>uid</tt> -- String. The value of the SOP Instance UID element.
|
213
|
+
#
|
214
|
+
def volume(*args)
|
215
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
216
|
+
if args.length == 1
|
217
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
218
|
+
return @associated_volumes[args.first]
|
219
|
+
else
|
220
|
+
# No argument used, therefore we return the first Volume instance:
|
221
|
+
return @volumes.first
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
private
|
227
|
+
|
228
|
+
|
229
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
230
|
+
#
|
231
|
+
def state
|
232
|
+
[@series_uid, @volumes]
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# The RTImage class contains methods that are specific for this modality (RTIMAGE).
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# * RTImage inherits all methods and attributes from the Series class.
|
8
|
+
#
|
9
|
+
class RTImage < Series
|
10
|
+
|
11
|
+
# An array of Plan Verification Images associated with this RTImage Series.
|
12
|
+
attr_reader :images
|
13
|
+
# The Plan which this RTImage Series belongs to.
|
14
|
+
attr_reader :plan
|
15
|
+
|
16
|
+
# Creates a new RTImage (series) instance by loading the relevant information from the specified DICOM object.
|
17
|
+
# The Series Instance UID string value is used to uniquely identify a RTImage (series) instance.
|
18
|
+
#
|
19
|
+
# === Parameters
|
20
|
+
#
|
21
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTIMAGE'.
|
22
|
+
# * <tt>study</tt> -- The Study instance that this RTImage belongs to.
|
23
|
+
#
|
24
|
+
def self.load(dcm, study)
|
25
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
26
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
27
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
|
28
|
+
# Required attributes:
|
29
|
+
series_uid = dcm.value(SERIES_UID)
|
30
|
+
# Optional attributes:
|
31
|
+
class_uid = dcm.value(SOP_CLASS)
|
32
|
+
date = dcm.value(SERIES_DATE)
|
33
|
+
time = dcm.value(SERIES_TIME)
|
34
|
+
description = dcm.value(SERIES_DESCR)
|
35
|
+
series_uid = dcm.value(SERIES_UID)
|
36
|
+
# Get the corresponding Plan:
|
37
|
+
plan = self.plan(dcm, study)
|
38
|
+
# Create the RTImage instance:
|
39
|
+
rtimage = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
|
40
|
+
rtimage.add(dcm)
|
41
|
+
return rtimage
|
42
|
+
end
|
43
|
+
|
44
|
+
# Identifies the Plan that the RTImage object belongs to.
|
45
|
+
# If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
|
46
|
+
#
|
47
|
+
def self.plan(dcm, study)
|
48
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
49
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
50
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
|
51
|
+
# Extract the Frame of Reference UID:
|
52
|
+
begin
|
53
|
+
frame_of_ref = dcm.value(FRAME_OF_REF)
|
54
|
+
rescue
|
55
|
+
frame_of_ref = nil
|
56
|
+
end
|
57
|
+
# Extract referenced Plan SOP Instance UID:
|
58
|
+
begin
|
59
|
+
ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
|
60
|
+
rescue
|
61
|
+
ref_plan_uid = nil
|
62
|
+
end
|
63
|
+
# Create the Frame if it doesn't exist:
|
64
|
+
f = study.patient.dataset.frame(frame_of_ref)
|
65
|
+
f = Frame.new(frame_of_ref, study.patient) unless f
|
66
|
+
# Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
|
67
|
+
plan = study.fseries(ref_plan_uid)
|
68
|
+
unless plan
|
69
|
+
# Create ImageSeries (assuming modality CT):
|
70
|
+
is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
|
71
|
+
study.add_series(is)
|
72
|
+
# Create StructureSet:
|
73
|
+
struct = StructureSet.new(RTKIT.sop_uid, is)
|
74
|
+
study.add_series(struct)
|
75
|
+
# Create Plan:
|
76
|
+
plan = Plan.new(ref_plan_uid, struct)
|
77
|
+
study.add_series(plan)
|
78
|
+
end
|
79
|
+
return plan
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates a new RTImage instance.
|
83
|
+
#
|
84
|
+
# === Parameters
|
85
|
+
#
|
86
|
+
# * <tt>series_uid</tt> -- The Series Instance UID string.
|
87
|
+
# * <tt>plan</tt> -- The Plan that this RTImage series belongs to.
|
88
|
+
# * <tt>options</tt> -- A hash of parameters.
|
89
|
+
#
|
90
|
+
# === Options
|
91
|
+
#
|
92
|
+
# * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
|
93
|
+
# * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
|
94
|
+
# * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
|
95
|
+
# * <tt>:series_uid</tt> -- String. The Series Instance UID (DICOM tag '0020,000E').
|
96
|
+
#
|
97
|
+
def initialize(series_uid, plan, options={})
|
98
|
+
raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
|
99
|
+
raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
|
100
|
+
# Pass attributes to Series initialization:
|
101
|
+
options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.1' # RT Image Storage
|
102
|
+
super(series_uid, 'RTIMAGE', plan.struct.study, options)
|
103
|
+
@plan = plan
|
104
|
+
# Default attributes:
|
105
|
+
@images = Array.new
|
106
|
+
@associated_images = Hash.new
|
107
|
+
# Register ourselves with the Plan:
|
108
|
+
@plan.add_rt_image(self)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
112
|
+
#
|
113
|
+
def ==(other)
|
114
|
+
if other.respond_to?(:to_rt_image)
|
115
|
+
other.send(:state) == state
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :eql?, :==
|
120
|
+
|
121
|
+
# Registers a DICOM Object to the RTImage series, and processes it
|
122
|
+
# to create (and reference) an (RT) Image instance linked to this RTImage series.
|
123
|
+
#
|
124
|
+
def add(dcm)
|
125
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
126
|
+
Image.load(dcm, self)
|
127
|
+
#load_patient_setup
|
128
|
+
#load_fields
|
129
|
+
end
|
130
|
+
|
131
|
+
# Adds an Image to this RTImage series.
|
132
|
+
#
|
133
|
+
def add_image(image)
|
134
|
+
raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
|
135
|
+
@images << image unless @associated_images[image.uid]
|
136
|
+
@associated_images[image.uid] = image
|
137
|
+
end
|
138
|
+
|
139
|
+
# Generates a Fixnum hash value for this instance.
|
140
|
+
#
|
141
|
+
def hash
|
142
|
+
state.hash
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns self.
|
146
|
+
#
|
147
|
+
def to_rt_image
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
|
155
|
+
=begin
|
156
|
+
# Registers this RTImage series instance with the Plan that it references.
|
157
|
+
#
|
158
|
+
def connect_to_plan(dcm)
|
159
|
+
# Find out which Plan is referenced:
|
160
|
+
dcm[REF_PLAN_SQ].each do |plan_item|
|
161
|
+
ref_sop_uid = plan_item.value(REF_SOP_UID)
|
162
|
+
matched_plan = @study.associated_instance_uids[ref_sop_uid]
|
163
|
+
if matched_plan
|
164
|
+
# The referenced series exists in our dataset. Proceed with setting up the references:
|
165
|
+
matched_plan.add_rtimage(self)
|
166
|
+
@plan = matched_plan
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
=end
|
171
|
+
|
172
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
173
|
+
#
|
174
|
+
def state
|
175
|
+
[@series_uid, @images]
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# Array extensions used by RTKIT.
|
2
|
+
#
|
3
|
+
class Array
|
4
|
+
|
5
|
+
# Rearranges the array (self) so that its elements appear in exactly
|
6
|
+
# the same sequence as another array (the argument).
|
7
|
+
# If the two arrays do not contain the same set of elements, an error is raised.
|
8
|
+
#
|
9
|
+
# === Examples
|
10
|
+
#
|
11
|
+
# a = [5, 2, 10, 1]
|
12
|
+
# b = [10, 2, 1, 5]
|
13
|
+
# b.assimilate!(a)
|
14
|
+
# b
|
15
|
+
# => [5, 2, 10, 1]
|
16
|
+
#
|
17
|
+
# NB! Not in use atm!
|
18
|
+
#
|
19
|
+
def assimilate!(other)
|
20
|
+
if self.length != other.length
|
21
|
+
raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare."
|
22
|
+
else
|
23
|
+
# Validate equality:
|
24
|
+
self.each_index do |i|
|
25
|
+
index = other.index(self[i])
|
26
|
+
if index
|
27
|
+
self[i] = other[index]
|
28
|
+
else
|
29
|
+
raise "An element (index #{i}) in self was not found in the other array. Unable to assimilate."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
return self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Compares an array (self) with a target array to determine an array of indices which can be
|
37
|
+
# used to extract the elements of self in order to create an array which shares the exact
|
38
|
+
# order of elements as the target array.
|
39
|
+
# Naturally, for this comparison to make sense, the target array and self must
|
40
|
+
# contain the same set of elements.
|
41
|
+
# Raises an error if self and other are not of equal length.
|
42
|
+
#
|
43
|
+
# === Restrictions
|
44
|
+
#
|
45
|
+
# * Behaviour may be incorrect if the array contains multiple identical objects.
|
46
|
+
#
|
47
|
+
# === Examples
|
48
|
+
#
|
49
|
+
# a = ['hi', 2, dcm]
|
50
|
+
# b = [2, dcm, 'hi']
|
51
|
+
# order = b.compare_with(a)
|
52
|
+
# => [2, 0, 1]
|
53
|
+
#
|
54
|
+
def compare_with(other)
|
55
|
+
raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare." if self.length != other.length
|
56
|
+
if self.length > 0
|
57
|
+
order = Array.new
|
58
|
+
other.each do |item|
|
59
|
+
index = self.index(item)
|
60
|
+
if index
|
61
|
+
order << index
|
62
|
+
else
|
63
|
+
raise "An element (#{item}) from the other array was not found in self. Unable to complete comparison."
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
return order
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the most common value in the array.
|
71
|
+
#
|
72
|
+
def most_common_value
|
73
|
+
self.group_by do |e|
|
74
|
+
e
|
75
|
+
end.values.max_by(&:size).first
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns an array where the elements of the original array are extracted
|
79
|
+
# according to the indices given in the argument array.
|
80
|
+
#
|
81
|
+
# === Examples
|
82
|
+
#
|
83
|
+
# a = [5, 2, 10, 1]
|
84
|
+
# i = a.sort_order
|
85
|
+
# a.sort_by_order(i)
|
86
|
+
# => [1, 2, 5, 10]
|
87
|
+
#
|
88
|
+
#
|
89
|
+
def sort_by_order(order=[])
|
90
|
+
if self.length != order.length
|
91
|
+
return nil
|
92
|
+
else
|
93
|
+
return self.values_at(*order)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Rearranges an array (self) so that it's original elements in the
|
98
|
+
# order specified by the indices given in the argument array.
|
99
|
+
#
|
100
|
+
# === Examples
|
101
|
+
#
|
102
|
+
# a = [5, 2, 10, 1]
|
103
|
+
# a.sort_by_order!([3, 1, 0, 2])
|
104
|
+
# a
|
105
|
+
# => [1, 2, 5, 10]
|
106
|
+
#
|
107
|
+
def sort_by_order!(order=[])
|
108
|
+
raise ArgumentError, "Invalid argument 'order'. Expected length equal to self.length (#{self.length}), got #{order.length}." if self.length != order.length
|
109
|
+
# It only makes sense to sort if length is 2 or more:
|
110
|
+
if self.length > 1
|
111
|
+
copy = self.dup
|
112
|
+
self.each_index do |i|
|
113
|
+
self[i] = copy[order[i]]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
return self
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns an ordered array of indices, where each element contains the index in the original array
|
120
|
+
# which needs to be extracted to produce a sorted array.
|
121
|
+
# This method is useful if you wish to sort multiple arrays depending on the sequence of elements in a specific array.
|
122
|
+
#
|
123
|
+
# === Examples
|
124
|
+
#
|
125
|
+
# a = [5, 2, 10, 1]
|
126
|
+
# a.sort_order
|
127
|
+
# => [3, 1, 0, 2]
|
128
|
+
#
|
129
|
+
def sort_order
|
130
|
+
d=[]
|
131
|
+
self.each_with_index{|x,i| d[i]=[x,i]}
|
132
|
+
if block_given?
|
133
|
+
return d.sort {|x,y| yield x[0],y[0]}.collect{|x| x[1]}
|
134
|
+
else
|
135
|
+
return d.sort.collect{|x| x[1]}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
class NArray
|
143
|
+
|
144
|
+
# Checks if an image array is segmented. Returns true if it is, and false if not.
|
145
|
+
# We define the image to be segmented if it contains at least positive 3 pixel values.
|
146
|
+
#
|
147
|
+
def segmented?
|
148
|
+
(self.gt 0).where.length > 2 ? true : false
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
class String
|
155
|
+
|
156
|
+
# Converts a string (containing a '\' separated x,y,z coordinate triplet)
|
157
|
+
# to a Coordinate instance.
|
158
|
+
#
|
159
|
+
def to_coordinate
|
160
|
+
values = self.split("\\").collect {|str| str.to_f}
|
161
|
+
raise ArgumentError, "Unable to create coordinate. Expected a string containing 3 values, got #{values.length}" unless values.length >= 3
|
162
|
+
return RTKIT::Coordinate.new(values[0], values[1], values[2])
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|