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,442 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# The StructureSet class contains methods that are specific for this modality (RTSTRUCT).
|
4
|
+
#
|
5
|
+
# === Inheritance
|
6
|
+
#
|
7
|
+
# * StructureSet inherits all methods and attributes from the Series class.
|
8
|
+
#
|
9
|
+
class StructureSet < Series
|
10
|
+
|
11
|
+
# The original DObject instance of the StructureSet.
|
12
|
+
attr_reader :dcm
|
13
|
+
# An array of ImageSeries that this Structure Set Series references has ROIs defined for.
|
14
|
+
attr_reader :image_series
|
15
|
+
# An array of RTPlans associated with this Structure Set Series.
|
16
|
+
attr_reader :plans
|
17
|
+
# An array of ROIs belonging to this structure set.
|
18
|
+
attr_reader :rois
|
19
|
+
# The SOP Instance UID.
|
20
|
+
attr_reader :sop_uid
|
21
|
+
|
22
|
+
# Creates a new StructureSet instance by loading the relevant information from the specified DICOM object.
|
23
|
+
# The SOP Instance UID string value is used to uniquely identify a StructureSet instance.
|
24
|
+
#
|
25
|
+
# === Parameters
|
26
|
+
#
|
27
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTSTRUCT'.
|
28
|
+
# * <tt>study</tt> -- The Study instance that this StructureSet belongs to.
|
29
|
+
#
|
30
|
+
def self.load(dcm, study)
|
31
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
32
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
33
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTSTUCT', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTSTRUCT'
|
34
|
+
# Required attributes:
|
35
|
+
sop_uid = dcm.value(SOP_UID)
|
36
|
+
# Optional attributes:
|
37
|
+
class_uid = dcm.value(SOP_CLASS)
|
38
|
+
date = dcm.value(SERIES_DATE)
|
39
|
+
time = dcm.value(SERIES_TIME)
|
40
|
+
description = dcm.value(SERIES_DESCR)
|
41
|
+
series_uid = dcm.value(SERIES_UID)
|
42
|
+
# Get the corresponding image series:
|
43
|
+
image_series = self.image_series(dcm, study)
|
44
|
+
# Create the StructureSet instance:
|
45
|
+
ss = self.new(sop_uid, image_series, :class_uid => class_uid, :date => date, :time => time, :description => description, :series_uid => series_uid)
|
46
|
+
ss.add(dcm)
|
47
|
+
return ss
|
48
|
+
end
|
49
|
+
|
50
|
+
# Identifies the ImageSeries that the StructureSet object belongs to.
|
51
|
+
# If the referenced instances (ImageSeries & Frame) does not exist, they are created by this method.
|
52
|
+
#
|
53
|
+
def self.image_series(dcm, study)
|
54
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
55
|
+
raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
|
56
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTSTUCT', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTSTRUCT'
|
57
|
+
# Extract the Referenced Frame UID:
|
58
|
+
begin
|
59
|
+
ref_frame_of_ref = dcm[REF_FRAME_OF_REF_SQ][0].value(FRAME_OF_REF)
|
60
|
+
rescue
|
61
|
+
ref_frame_of_ref = nil
|
62
|
+
end
|
63
|
+
# Extract referenced Image Series UID:
|
64
|
+
begin
|
65
|
+
ref_series_uid = dcm[REF_FRAME_OF_REF_SQ][0][RT_REF_STUDY_SQ][0][RT_REF_SERIES_SQ][0].value(SERIES_UID)
|
66
|
+
rescue
|
67
|
+
ref_series_uid = nil
|
68
|
+
end
|
69
|
+
# Create the Frame if it doesn't exist:
|
70
|
+
f = study.patient.dataset.frame(ref_frame_of_ref)
|
71
|
+
f = Frame.new(ref_frame_of_ref, study.patient) unless f
|
72
|
+
# Create the ImageSeries if it doesnt exist:
|
73
|
+
is = f.series(ref_series_uid)
|
74
|
+
is = ImageSeries.new(ref_series_uid, 'CT', f, study) unless is
|
75
|
+
study.add_series(is)
|
76
|
+
return is
|
77
|
+
end
|
78
|
+
|
79
|
+
# Creates a new StructureSet instance.
|
80
|
+
#
|
81
|
+
# === Parameters
|
82
|
+
#
|
83
|
+
# * <tt>sop_uid</tt> -- The SOP Instance UID string.
|
84
|
+
# * <tt>image_series</tt> -- An Image Series that this StructureSet belongs to.
|
85
|
+
# * <tt>options</tt> -- A hash of parameters.
|
86
|
+
#
|
87
|
+
# === Options
|
88
|
+
#
|
89
|
+
# * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
|
90
|
+
# * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
|
91
|
+
# * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
|
92
|
+
# * <tt>:series_uid</tt> -- String. The Series Instance UID (DICOM tag '0020,000E').
|
93
|
+
#
|
94
|
+
def initialize(sop_uid, image_series, options={})
|
95
|
+
raise ArgumentError, "Invalid argument 'sop_uid'. Expected String, got #{sop_uid.class}." unless sop_uid.is_a?(String)
|
96
|
+
raise ArgumentError, "Invalid argument 'image_series'. Expected ImageSeries, got #{image_series.class}." unless image_series.is_a?(ImageSeries)
|
97
|
+
# Pass attributes to Series initialization:
|
98
|
+
options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.3' # RT Structure Set Storage
|
99
|
+
# Get a randomized Series UID unless it has been defined in the options hash:
|
100
|
+
series_uid = options[:series_uid] || RTKIT.series_uid
|
101
|
+
super(series_uid, 'RTSTRUCT', image_series.study, options)
|
102
|
+
@sop_uid = sop_uid
|
103
|
+
# Default attributes:
|
104
|
+
@image_series = Array.new
|
105
|
+
@rois = Array.new
|
106
|
+
@plans = Array.new
|
107
|
+
@associated_plans = Hash.new
|
108
|
+
# Register ourselves with the ImageSeries:
|
109
|
+
image_series.add_struct(self)
|
110
|
+
@image_series << image_series
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
114
|
+
#
|
115
|
+
def ==(other)
|
116
|
+
if other.respond_to?(:to_structure_set)
|
117
|
+
other.send(:state) == state
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
alias_method :eql?, :==
|
122
|
+
|
123
|
+
# Registers a DICOM Object to the StructureSet, and processes it
|
124
|
+
# to create (and reference) the ROIs contained in the object.
|
125
|
+
#
|
126
|
+
def add(dcm)
|
127
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
128
|
+
@dcm = dcm
|
129
|
+
load_rois
|
130
|
+
end
|
131
|
+
|
132
|
+
# Adds a Plan Series to this StructureSet.
|
133
|
+
# Note: Intended for internal use in the library only.
|
134
|
+
#
|
135
|
+
def add_plan(plan)
|
136
|
+
raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
|
137
|
+
@plans << plan unless @associated_plans[plan.uid]
|
138
|
+
@associated_plans[plan.uid] = plan
|
139
|
+
end
|
140
|
+
|
141
|
+
# Adds a ROI instance to this StructureSet.
|
142
|
+
#
|
143
|
+
def add_roi(roi)
|
144
|
+
raise ArgumentError, "Invalid argument 'roi'. Expected ROI, got #{roi.class}." unless roi.is_a?(ROI)
|
145
|
+
@rois << roi unless @rois.include?(roi)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Creates a ROI belonging to this StructureSet.
|
149
|
+
# Returns the created ROI.
|
150
|
+
#
|
151
|
+
# === Notes
|
152
|
+
#
|
153
|
+
# * The ROI is created without Slices, and these must be added after the ROI creation.
|
154
|
+
#
|
155
|
+
# === Parameters
|
156
|
+
#
|
157
|
+
# * <tt>frame</tt> -- The Frame instance which the ROI will belong to.
|
158
|
+
# * <tt>options</tt> -- A hash of parameters.
|
159
|
+
#
|
160
|
+
# === Options
|
161
|
+
#
|
162
|
+
# * <tt>:algorithm</tt> -- String. The ROI Generation Algorithm. Defaults to 'Automatic'.
|
163
|
+
# * <tt>:name</tt> -- String. The ROI Name. Defaults to 'RTKIT-VOLUME'.
|
164
|
+
# * <tt>:number</tt> -- Integer. The ROI Number. Defaults to the first available ROI Number in the StructureSet.
|
165
|
+
# * <tt>:interpreter</tt> -- String. The ROI Interpreter. Defaults to 'RTKIT'.
|
166
|
+
# * <tt>:type</tt> -- String. The ROI Interpreted Type. Defaults to 'CONTROL'.
|
167
|
+
#
|
168
|
+
def create_roi(frame, options={})
|
169
|
+
raise ArgumentError, "Expected Frame, got #{frame.class}." unless frame.is_a?(Frame)
|
170
|
+
# Set values:
|
171
|
+
algorithm = options[:algorithm] || 'Automatic'
|
172
|
+
name = options[:name] || 'RTKIT-VOLUME'
|
173
|
+
interpreter = options[:interpreter] || 'RTKIT'
|
174
|
+
type = options[:type] || 'CONTROL'
|
175
|
+
if options[:number]
|
176
|
+
raise ArgumentError, "Expected Integer, got #{options[:number].class} for the option :number." unless options[:number].is_a?(Integer)
|
177
|
+
raise ArgumentError, "The specified ROI Number (#{options[:roi_number]}) is already used by one of the existing ROIs (#{roi_numbers})." if roi_numbers.include?(options[:number])
|
178
|
+
number = options[:number]
|
179
|
+
else
|
180
|
+
number = (roi_numbers.max ? roi_numbers.max + 1 : 1)
|
181
|
+
end
|
182
|
+
# Create ROI:
|
183
|
+
roi = ROI.new(name, number, frame, self, :algorithm => algorithm, :name => name, :number => number, :interpreter => interpreter, :type => type)
|
184
|
+
return roi
|
185
|
+
end
|
186
|
+
|
187
|
+
# Generates a Fixnum hash value for this instance.
|
188
|
+
#
|
189
|
+
def hash
|
190
|
+
state.hash
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns the Plan instance mathcing the specified SOP Instance UID (if an argument is used).
|
194
|
+
# If a specified UID doesn't match, nil is returned.
|
195
|
+
# If no argument is passed, the first Plan instance associated with the StructureSet is returned.
|
196
|
+
#
|
197
|
+
# === Parameters
|
198
|
+
#
|
199
|
+
# * <tt>uid</tt> -- String. The value of the SOP Instance UID element.
|
200
|
+
#
|
201
|
+
def plan(*args)
|
202
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
203
|
+
if args.length == 1
|
204
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
205
|
+
return @associated_plans[args.first]
|
206
|
+
else
|
207
|
+
# No argument used, therefore we return the first Plan instance:
|
208
|
+
return @plans.first
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Removes the ROI (specified by a ROI Number) from the Structure Set.
|
213
|
+
#
|
214
|
+
# === Parameters
|
215
|
+
#
|
216
|
+
# * <tt>instance_or_number</tt> -- The ROI Instance (or ROI Number of the instance) to be removed.
|
217
|
+
#
|
218
|
+
def remove_roi(instance_or_number)
|
219
|
+
raise ArgumentError, "Invalid argument 'instance_or_number'. Expected a ROI Instance or an Integer (ROI Number). Got #{instance_or_number.class}." unless [ROI, Integer].include?(instance_or_number.class)
|
220
|
+
roi_instance = instance_or_number
|
221
|
+
if instance_or_number.is_a?(Integer)
|
222
|
+
roi_instance = roi(instance_or_number)
|
223
|
+
end
|
224
|
+
index = @rois.index(roi_instance)
|
225
|
+
if index
|
226
|
+
@rois.delete_at(index)
|
227
|
+
roi_instance.remove_references
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Removes all ROIs from the Structure Set.
|
232
|
+
#
|
233
|
+
def remove_rois
|
234
|
+
@rois.each do |roi|
|
235
|
+
roi.remove_references
|
236
|
+
end
|
237
|
+
@rois = Array.new
|
238
|
+
end
|
239
|
+
|
240
|
+
# Returns a ROI that matches the specified number or name.
|
241
|
+
# Returns nil if no match is found.
|
242
|
+
#
|
243
|
+
def roi(name_or_number)
|
244
|
+
raise ArgumentError, "Invalid argument 'name_or_number'. Expected String or Integer, got #{name_or_number.class}." unless [String, Integer, Fixnum].include?(name_or_number.class)
|
245
|
+
if name_or_number.is_a?(String)
|
246
|
+
@rois.each do |r|
|
247
|
+
return r if r.name == name_or_number
|
248
|
+
end
|
249
|
+
else
|
250
|
+
@rois.each do |r|
|
251
|
+
return r if r.number == name_or_number
|
252
|
+
end
|
253
|
+
end
|
254
|
+
return nil
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns the ROI Names assigned to the various structures present in the structure set.
|
258
|
+
# The names are returned in an array.
|
259
|
+
#
|
260
|
+
def roi_names
|
261
|
+
names = Array.new
|
262
|
+
@rois.each do |roi|
|
263
|
+
names << roi.name
|
264
|
+
end
|
265
|
+
return names
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns the ROI Numbers assigned to the various structures present in the structure set.
|
269
|
+
# The numbers are returned in an array.
|
270
|
+
#
|
271
|
+
def roi_numbers
|
272
|
+
numbers = Array.new
|
273
|
+
@rois.each do |roi|
|
274
|
+
numbers << roi.number
|
275
|
+
end
|
276
|
+
return numbers
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns all ROIs defined in this structure set that belongs to the specified Frame of Reference UID.
|
280
|
+
# Returns an empty array if no matching ROIs are found.
|
281
|
+
#
|
282
|
+
def rois_in_frame(uid)
|
283
|
+
raise ArgumentError, "Expected String, got #{uid.class}." unless uid.is_a?(String)
|
284
|
+
frame_rois = Array.new
|
285
|
+
@rois.each do |roi|
|
286
|
+
frame_rois << roi if roi.frame.uid == uid
|
287
|
+
end
|
288
|
+
return frame_rois
|
289
|
+
end
|
290
|
+
|
291
|
+
# Sets new color values for all ROIs belonging to the StructureSet.
|
292
|
+
# Color values will be selected in a way which attempts to make the ROI colors maximally different.
|
293
|
+
# The method uses a predefined list containing 54 colors, which means for the rare case of more
|
294
|
+
# than 24 ROIs, some will not be assigned a color.
|
295
|
+
# Obviously, the more ROIs to assign colors to, the more similar the color values will be.
|
296
|
+
#
|
297
|
+
def set_colors
|
298
|
+
if @rois.length > 0
|
299
|
+
# Determine colors:
|
300
|
+
initialize_colors
|
301
|
+
# Set colors:
|
302
|
+
@rois.each_index do |i|
|
303
|
+
@rois[i].color = @colors[i] if i < 24
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Sets new ROI Numbers to all ROIs belonging to the StructureSet.
|
309
|
+
# Numbers increase sequentially, starting at 1 for the first ROI.
|
310
|
+
#
|
311
|
+
def set_numbers
|
312
|
+
@rois.each_with_index do |roi, i|
|
313
|
+
roi.number = i + 1
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# Dumps the StructureSet instance to a DObject.
|
318
|
+
# This overwrites the dcm instance attribute.
|
319
|
+
# Returns the DObject instance.
|
320
|
+
#
|
321
|
+
def to_dcm
|
322
|
+
# Use the original DICOM object as a starting point (keeping all non-sequence elements):
|
323
|
+
#@dcm[REF_FRAME_OF_REF_SQ].delete_children
|
324
|
+
@dcm[STRUCTURE_SET_ROI_SQ].delete_children
|
325
|
+
@dcm[ROI_CONTOUR_SQ].delete_children
|
326
|
+
@dcm[RT_ROI_OBS_SQ].delete_children
|
327
|
+
# Create DICOM
|
328
|
+
@rois.each do |roi|
|
329
|
+
@dcm[STRUCTURE_SET_ROI_SQ].add_item(roi.ss_item)
|
330
|
+
@dcm[ROI_CONTOUR_SQ].add_item(roi.contour_item)
|
331
|
+
@dcm[RT_ROI_OBS_SQ].add_item(roi.obs_item)
|
332
|
+
end
|
333
|
+
return @dcm
|
334
|
+
end
|
335
|
+
|
336
|
+
# Returns self.
|
337
|
+
#
|
338
|
+
def to_structure_set
|
339
|
+
self
|
340
|
+
end
|
341
|
+
|
342
|
+
# Writes the StructureSet to a DICOM file given by the specified file string.
|
343
|
+
#
|
344
|
+
def write(path)
|
345
|
+
to_dcm
|
346
|
+
@dcm.write(path)
|
347
|
+
end
|
348
|
+
|
349
|
+
|
350
|
+
private
|
351
|
+
|
352
|
+
|
353
|
+
=begin
|
354
|
+
# Registers this Structure Set instance with the ImageSeries that it references.
|
355
|
+
#
|
356
|
+
def connect_to_image_series
|
357
|
+
# Find out which Image Series is referenced:
|
358
|
+
@dcm[REF_FRAME_OF_REF_SQ].each do |frame_item|
|
359
|
+
ref_frame_of_ref = frame_item.value(FRAME_OF_REF)
|
360
|
+
# Continue if the Referenced Frame of Ref matches one of the Frames registered to our DataSet.
|
361
|
+
matched_frame = @study.patient.dataset.frame(ref_frame_of_ref)
|
362
|
+
if matched_frame
|
363
|
+
frame_item[RT_REF_STUDY_SQ].each do |study_item|
|
364
|
+
# Skip testing against the Study UID.
|
365
|
+
#ref_study_uid = study_item.value(REF_SOP_UID)
|
366
|
+
study_item[RT_REF_SERIES_SQ].each do |series_item|
|
367
|
+
ref_series_uid = series_item.value(SERIES_UID)
|
368
|
+
matched_series = matched_frame.series(ref_series_uid)
|
369
|
+
if matched_series
|
370
|
+
# The referenced series exists in our dataset. Proceed with setting up the references:
|
371
|
+
matched_series.add_struct(self)
|
372
|
+
@image_series << matched_series
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
=end
|
380
|
+
|
381
|
+
# Loads the ROI Items contained in the structure set and creates ROI instances.
|
382
|
+
#
|
383
|
+
def load_rois
|
384
|
+
# Load the information in a nested hash:
|
385
|
+
item_group = Hash.new
|
386
|
+
@dcm[STRUCTURE_SET_ROI_SQ].each do |roi_item|
|
387
|
+
item_group[roi_item.value(ROI_NUMBER)] = {:roi => roi_item}
|
388
|
+
end
|
389
|
+
@dcm[ROI_CONTOUR_SQ].each do |contour_item|
|
390
|
+
item_group[contour_item.value(REF_ROI_NUMBER)][:contour] = contour_item
|
391
|
+
end
|
392
|
+
@dcm[RT_ROI_OBS_SQ].each do |rt_item|
|
393
|
+
item_group[rt_item.value(REF_ROI_NUMBER)][:rt] = rt_item
|
394
|
+
end
|
395
|
+
# Create a ROI instance for each set of items:
|
396
|
+
item_group.each_value do |roi_items|
|
397
|
+
ROI.create_from_items(roi_items[:roi], roi_items[:contour], roi_items[:rt], self)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Initializes the color instance array.
|
402
|
+
#
|
403
|
+
def initialize_colors
|
404
|
+
@colors = [
|
405
|
+
# 6 colors with only 255:
|
406
|
+
"255\\0\\0",
|
407
|
+
"0\\255\\0",
|
408
|
+
"0\\0\\255",
|
409
|
+
"255\\255\\0",
|
410
|
+
"0\\255\\255",
|
411
|
+
"255\\0\\255",
|
412
|
+
# 12 colors with a mix of 128 and 255:
|
413
|
+
"255\\128\\0",
|
414
|
+
"128\\255\\0",
|
415
|
+
"0\\128\\255",
|
416
|
+
"255\\0\\128",
|
417
|
+
"0\\255\\128",
|
418
|
+
"128\\0\\255",
|
419
|
+
"255\\255\\128",
|
420
|
+
"128\\255\\255",
|
421
|
+
"255\\128\\255",
|
422
|
+
"255\\128\\128",
|
423
|
+
"128\\128\\255",
|
424
|
+
"128\\255\\128",
|
425
|
+
# 6 colors with only 128:
|
426
|
+
"128\\0\\0",
|
427
|
+
"0\\128\\0",
|
428
|
+
"0\\0\\128",
|
429
|
+
"128\\128\\0",
|
430
|
+
"0\\128\\128",
|
431
|
+
"128\\0\\128",
|
432
|
+
]
|
433
|
+
end
|
434
|
+
|
435
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
436
|
+
#
|
437
|
+
def state
|
438
|
+
[@plans, @rois, @sop_uid]
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
end
|
data/lib/rtkit/study.rb
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
module RTKIT
|
2
|
+
|
3
|
+
# Contains the DICOM data and methods related to a study.
|
4
|
+
#
|
5
|
+
# === Relations
|
6
|
+
#
|
7
|
+
# * A Study belongs to a Patient.
|
8
|
+
# * A Study has many Series instances.
|
9
|
+
#
|
10
|
+
class Study
|
11
|
+
|
12
|
+
# The Study Date.
|
13
|
+
attr_reader :date
|
14
|
+
# The Study Description.
|
15
|
+
attr_reader :description
|
16
|
+
# The Study ID.
|
17
|
+
attr_reader :id
|
18
|
+
# An array of ImageSeries references.
|
19
|
+
attr_reader :image_series
|
20
|
+
# The Study's Patient reference.
|
21
|
+
attr_reader :patient
|
22
|
+
# An array of Series references.
|
23
|
+
attr_reader :series
|
24
|
+
# The Study Instance UID.
|
25
|
+
attr_reader :study_uid
|
26
|
+
# The Study Time.
|
27
|
+
attr_reader :time
|
28
|
+
|
29
|
+
# Creates a new Study instance by loading study information from the specified DICOM object.
|
30
|
+
# The Study's UID string value is used to uniquely identify a study.
|
31
|
+
#
|
32
|
+
# === Parameters
|
33
|
+
#
|
34
|
+
# * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject).
|
35
|
+
# * <tt>patient</tt> -- The Patient instance that this Study belongs to.
|
36
|
+
#
|
37
|
+
def self.load(dcm, patient)
|
38
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
39
|
+
raise ArgumentError, "Invalid argument 'patient'. Expected Patient, got #{patient.class}." unless patient.is_a?(Patient)
|
40
|
+
uid = dcm.value(STUDY_UID)
|
41
|
+
date = dcm.value(STUDY_DATE)
|
42
|
+
time = dcm.value(STUDY_TIME)
|
43
|
+
description = dcm.value(STUDY_DESCR)
|
44
|
+
id = dcm.value(STUDY_ID)
|
45
|
+
study = self.new(uid, patient, :date => date, :time => time, :description => description, :id => id)
|
46
|
+
study.add(dcm)
|
47
|
+
return study
|
48
|
+
end
|
49
|
+
|
50
|
+
# Creates a new Study instance. The Study Instance UID string is used to uniquely identify a Study.
|
51
|
+
#
|
52
|
+
# === Parameters
|
53
|
+
#
|
54
|
+
# * <tt>study_uid</tt> -- The Study Instance UID string.
|
55
|
+
# * <tt>patient</tt> -- The Patient instance that this Study belongs to.
|
56
|
+
# * <tt>options</tt> -- A hash of parameters.
|
57
|
+
#
|
58
|
+
# === Options
|
59
|
+
#
|
60
|
+
# * <tt>:date</tt> -- String. The Study Date (DICOM tag '0008,0020').
|
61
|
+
# * <tt>:time</tt> -- String. The Study Time (DICOM tag '0008,0030').
|
62
|
+
# * <tt>:description</tt> -- String. The Study Description (DICOM tag '0008,1030').
|
63
|
+
# * <tt>:id</tt> -- String. The Study ID (DICOM tag '0020,0010').
|
64
|
+
#
|
65
|
+
def initialize(study_uid, patient, options={})
|
66
|
+
raise ArgumentError, "Invalid argument 'study_uid'. Expected String, got #{study_uid.class}." unless study_uid.is_a?(String)
|
67
|
+
raise ArgumentError, "Invalid argument 'patient'. Expected Patient, got #{patient.class}." unless patient.is_a?(Patient)
|
68
|
+
raise ArgumentError, "Invalid option ':date'. Expected String, got #{options[:date].class}." if options[:date] && !options[:date].is_a?(String)
|
69
|
+
raise ArgumentError, "Invalid option ':time'. Expected String, got #{options[:time].class}." if options[:time] && !options[:time].is_a?(String)
|
70
|
+
raise ArgumentError, "Invalid option ':description'. Expected String, got #{options[:description].class}." if options[:description] && !options[:description].is_a?(String)
|
71
|
+
raise ArgumentError, "Invalid option ':id'. Expected String, got #{options[:id].class}." if options[:id] && !options[:id].is_a?(String)
|
72
|
+
# Key attributes:
|
73
|
+
@study_uid = study_uid
|
74
|
+
@patient = patient
|
75
|
+
# Default attributes:
|
76
|
+
@image_series = Array.new
|
77
|
+
@series = Array.new
|
78
|
+
# A hash with the associated Series' UID as key and the instance of the Series that belongs to this Study as value:
|
79
|
+
@associated_series = Hash.new
|
80
|
+
@associated_iseries = Hash.new
|
81
|
+
# Optional attributes:
|
82
|
+
@date = options[:date]
|
83
|
+
@time = options[:time]
|
84
|
+
@description = options[:description]
|
85
|
+
@id = options[:id]
|
86
|
+
# Register ourselves with the patient:
|
87
|
+
@patient.add_study(self)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Adds a DICOM object to the study, by adding it
|
91
|
+
# to an existing Series, or creating a new Series.
|
92
|
+
#
|
93
|
+
def add(dcm)
|
94
|
+
raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
|
95
|
+
existing_series = @associated_series[dcm.value(SERIES_UID)]
|
96
|
+
if existing_series
|
97
|
+
existing_series.add(dcm)
|
98
|
+
else
|
99
|
+
# New series (series subclass depends on modality):
|
100
|
+
case dcm.value(MODALITY)
|
101
|
+
when *IMAGE_SERIES
|
102
|
+
# Create the ImageSeries:
|
103
|
+
s = ImageSeries.load(dcm, self)
|
104
|
+
@image_series << s
|
105
|
+
when 'RTSTRUCT'
|
106
|
+
s = StructureSet.load(dcm, self)
|
107
|
+
when 'RTPLAN'
|
108
|
+
s = Plan.load(dcm, self)
|
109
|
+
when 'RTDOSE'
|
110
|
+
s = RTDose.load(dcm, self)
|
111
|
+
when 'RTIMAGE'
|
112
|
+
s = RTImage.load(dcm, self)
|
113
|
+
else
|
114
|
+
raise ArgumentError, "Unexpected (unsupported) modality (#{dcm.value(MODALITY)})in Study#add()"
|
115
|
+
end
|
116
|
+
# Add the newly created series to this study:
|
117
|
+
add_series(s)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns true if the argument is an instance with attributes equal to self.
|
122
|
+
#
|
123
|
+
def ==(other)
|
124
|
+
if other.respond_to?(:to_study)
|
125
|
+
other.send(:state) == state
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
alias_method :eql?, :==
|
130
|
+
|
131
|
+
# Adds a Series to this Study.
|
132
|
+
#
|
133
|
+
#--
|
134
|
+
# Note: At some time we may decide to allow only ImageSeries
|
135
|
+
# (i.e. excluding other kinds of series) to be attached to a study.
|
136
|
+
#
|
137
|
+
def add_series(series)
|
138
|
+
raise ArgumentError, "Invalid argument 'series'. Expected Series, got #{series.class}." unless series.is_a?(Series)
|
139
|
+
# Do not add it again if the series already belongs to this instance:
|
140
|
+
@series << series unless @associated_series[series.uid]
|
141
|
+
@image_series << series if series.is_a?(ImageSeries) && !@associated_series[series.uid]
|
142
|
+
@associated_series[series.uid] = series
|
143
|
+
@associated_iseries[series.uid] = series if series.is_a?(ImageSeries)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Generates a Fixnum hash value for this instance.
|
147
|
+
#
|
148
|
+
def hash
|
149
|
+
state.hash
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns the ImageSeries instance mathcing the specified Series Instance UID (if an argument is used).
|
153
|
+
# If a specified UID doesn't match, nil is returned.
|
154
|
+
# If no argument is passed, the first ImageSeries instance associated with the Study is returned.
|
155
|
+
#
|
156
|
+
# === Parameters
|
157
|
+
#
|
158
|
+
# * <tt>uid</tt> -- String. The value of the Series Instance UID element.
|
159
|
+
#
|
160
|
+
def iseries(*args)
|
161
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
162
|
+
if args.length == 1
|
163
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
164
|
+
return @associated_iseries[args.first]
|
165
|
+
else
|
166
|
+
# No argument used, therefore we return the first ImageSeries instance:
|
167
|
+
return @image_series.first
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns the Series instance mathcing the specified unique identifier (if an argument is used).
|
172
|
+
# The unique identifier is either a Series Instance UID (for ImageSeries) or a SOP Instance UID (for other kinds).
|
173
|
+
# If a specified UID doesn't match, nil is returned.
|
174
|
+
# If no argument is passed, the first Series instance associated with the Study is returned.
|
175
|
+
#
|
176
|
+
# === Parameters
|
177
|
+
#
|
178
|
+
# * <tt>uid</tt> -- The Series' unique identifier string.
|
179
|
+
#
|
180
|
+
def fseries(*args)
|
181
|
+
raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
|
182
|
+
if args.length == 1
|
183
|
+
raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
|
184
|
+
return @associated_series[args.first]
|
185
|
+
else
|
186
|
+
# No argument used, therefore we return the first Series instance:
|
187
|
+
return @series.first
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns self.
|
192
|
+
#
|
193
|
+
def to_study
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
# Returns the unique identifier string, which for this class is the Study Instance UID.
|
198
|
+
#
|
199
|
+
def uid
|
200
|
+
return @study_uid
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
|
207
|
+
# Returns the attributes of this instance in an array (for comparison purposes).
|
208
|
+
#
|
209
|
+
def state
|
210
|
+
[@date, @description, @id, @image_series, @time, @study_uid]
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|