rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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