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,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
|