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.
@@ -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