rtkit 0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|