rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
1
+ module RTKIT
2
+
3
+ # The RTDose class contains methods that are specific for this modality (RTDOSE).
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # * RTDose inherits all methods and attributes from the Series class.
8
+ #
9
+ class RTDose < Series
10
+
11
+ # The Plan which this RTDose series belongs to.
12
+ attr_reader :plan
13
+ # An array of dose Volume instances associated with this RTDose series.
14
+ attr_accessor :volumes
15
+
16
+ # Creates a new RTDose instance by loading the relevant information from the specified DICOM object.
17
+ # The Series Instance UID string value is used to uniquely identify a RTDose instance.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTDOSE'.
22
+ # * <tt>study</tt> -- The Study instance that this RTDose belongs to.
23
+ #
24
+ def self.load(dcm, study)
25
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
26
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
27
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
28
+ # Required attributes:
29
+ series_uid = dcm.value(SERIES_UID)
30
+ # Optional attributes:
31
+ class_uid = dcm.value(SOP_CLASS)
32
+ date = dcm.value(SERIES_DATE)
33
+ time = dcm.value(SERIES_TIME)
34
+ description = dcm.value(SERIES_DESCR)
35
+ series_uid = dcm.value(SERIES_UID)
36
+ # Get the corresponding Plan:
37
+ plan = self.plan(dcm, study)
38
+ # Create the RTDose instance:
39
+ dose = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
40
+ dose.add(dcm)
41
+ return dose
42
+ end
43
+
44
+ # Identifies the Plan that the RTDose object belongs to.
45
+ # If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
46
+ #
47
+ def self.plan(dcm, study)
48
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
49
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
50
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTDOSE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTDOSE'
51
+ # Extract the Frame of Reference UID:
52
+ begin
53
+ frame_of_ref = dcm.value(FRAME_OF_REF)
54
+ rescue
55
+ frame_of_ref = nil
56
+ end
57
+ # Extract referenced Plan SOP Instance UID:
58
+ begin
59
+ ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
60
+ rescue
61
+ ref_plan_uid = nil
62
+ end
63
+ # Create the Frame if it doesn't exist:
64
+ f = study.patient.dataset.frame(frame_of_ref)
65
+ f = Frame.new(frame_of_ref, study.patient) unless f
66
+ # Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
67
+ plan = study.fseries(ref_plan_uid)
68
+ unless plan
69
+ # Create ImageSeries (assuming modality CT):
70
+ is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
71
+ study.add_series(is)
72
+ # Create StructureSet:
73
+ struct = StructureSet.new(RTKIT.sop_uid, is)
74
+ study.add_series(struct)
75
+ # Create Plan:
76
+ plan = Plan.new(ref_plan_uid, struct)
77
+ study.add_series(plan)
78
+ end
79
+ return plan
80
+ end
81
+
82
+ # Creates a new RTDose instance.
83
+ #
84
+ # === Parameters
85
+ #
86
+ # * <tt>series_uid</tt> -- The Series Instance UID string.
87
+ # * <tt>plan</tt> -- The Plan instance that this RTDose series belongs to.
88
+ # * <tt>options</tt> -- A hash of parameters.
89
+ #
90
+ # === Options
91
+ #
92
+ # * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
93
+ # * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
94
+ # * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
95
+ #
96
+ def initialize(series_uid, plan, options={})
97
+ raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
98
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
99
+ # Pass attributes to Series initialization:
100
+ options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.2' # RT Dose Storage
101
+ super(series_uid, 'RTDOSE', plan.study, options)
102
+ @plan = plan
103
+ # Default attributes:
104
+ @volumes = Array.new
105
+ @associated_volumes = Hash.new
106
+ # Register ourselves with the Plan:
107
+ @plan.add_rt_dose(self)
108
+ end
109
+
110
+ # Returns true if the argument is an instance with attributes equal to self.
111
+ #
112
+ def ==(other)
113
+ if other.respond_to?(:to_rt_dose)
114
+ other.send(:state) == state
115
+ end
116
+ end
117
+
118
+ alias_method :eql?, :==
119
+
120
+ # Registers a DICOM Object to the RTDose series, and processes it
121
+ # to create (and reference) a DoseVolume instance linked to this RTDose series.
122
+ #
123
+ def add(dcm)
124
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
125
+ DoseVolume.load(dcm, self)
126
+ end
127
+
128
+ # Adds a DoseVolume instance to this RTDose series.
129
+ #
130
+ def add_volume(volume)
131
+ raise ArgumentError, "Invalid argument 'volume'. Expected DoseVolume, got #{volume.class}." unless volume.is_a?(DoseVolume)
132
+ @volumes << volume unless @associated_volumes[volume.uid]
133
+ @associated_volumes[volume.uid] = volume
134
+ end
135
+
136
+ # Generates a Fixnum hash value for this instance.
137
+ #
138
+ def hash
139
+ state.hash
140
+ end
141
+
142
+ # Returns a DoseVolume which is the sum of the volumes of this instance.
143
+ # With the individual DoseVolumes corresponding to the dose for a particular
144
+ # beam, the sum DoseVolume corresponds to the summed dose of the entire
145
+ # treatment plan.
146
+ #
147
+ def sum
148
+ if @sum
149
+ # If the sum volume has already been created, return it instead of recreating:
150
+ return @sum
151
+ else
152
+ if @volumes.length > 0
153
+ nr_frames = @volumes.first.images.length
154
+ # Create the sum DoseVolume instance:
155
+ sop_uid = @volumes.first.sop_uid + ".1"
156
+ @sum = DoseVolume.new(sop_uid, @volumes.first.frame, @volumes.first.dose_series, :sum => true)
157
+ # Sum the dose of the various DoseVolumes:
158
+ dose_sum = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
159
+ @volumes.each { |volume| dose_sum += volume.dose_arr }
160
+ # Convert dose float array to integer pixel values of a suitable range,
161
+ # along with a corresponding scaling factor:
162
+ sum_scaling_coeff = dose_sum.max / 65000.0
163
+ if sum_scaling_coeff == 0.0
164
+ pixel_values = NArray.int(nr_frames, @volumes.first.images.first.columns, @volumes.first.images.first.rows)
165
+ else
166
+ pixel_values = dose_sum * (1 / sum_scaling_coeff)
167
+ end
168
+ # Set the scaling coeffecient:
169
+ @sum.scaling = sum_scaling_coeff
170
+ # Collect the rest of the image information needed to create new dose images:
171
+ sop_uids = RTKIT.sop_uids(nr_frames)
172
+ slice_positions = @volumes.first.images.collect {|img| img.pos_slice}
173
+ columns = @volumes.first.images.first.columns
174
+ rows = @volumes.first.images.first.rows
175
+ pos_x = @volumes.first.images.first.pos_x
176
+ pos_y = @volumes.first.images.first.pos_y
177
+ col_spacing = @volumes.first.images.first.col_spacing
178
+ row_spacing = @volumes.first.images.first.row_spacing
179
+ cosines = @volumes.first.images.first.cosines
180
+ # Create dose images for our sum dose volume:
181
+ nr_frames.times do |i|
182
+ img = Image.new(sop_uids[i], @sum)
183
+ # Fill in image information:
184
+ img.columns = columns
185
+ img.rows = rows
186
+ img.pos_x = pos_x
187
+ img.pos_y = pos_y
188
+ img.pos_slice = slice_positions[i]
189
+ img.col_spacing = col_spacing
190
+ img.row_spacing = row_spacing
191
+ img.cosines = cosines
192
+ # Fill in the pixel frame data:
193
+ img.narray = pixel_values[i, true, true]
194
+ end
195
+ return @sum
196
+ end
197
+ end
198
+ end
199
+
200
+ # Returns self.
201
+ #
202
+ def to_rt_dose
203
+ self
204
+ end
205
+
206
+ # Returns the Volume instance mathcing the specified SOP Instance UID (if an argument is used).
207
+ # If a specified UID doesn't match, nil is returned.
208
+ # If no argument is passed, the first Volume instance associated with the RTDose is returned.
209
+ #
210
+ # === Parameters
211
+ #
212
+ # * <tt>uid</tt> -- String. The value of the SOP Instance UID element.
213
+ #
214
+ def volume(*args)
215
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
216
+ if args.length == 1
217
+ raise ArgumentError, "Expected String (or nil), got #{args.first.class}." unless [String, NilClass].include?(args.first.class)
218
+ return @associated_volumes[args.first]
219
+ else
220
+ # No argument used, therefore we return the first Volume instance:
221
+ return @volumes.first
222
+ end
223
+ end
224
+
225
+
226
+ private
227
+
228
+
229
+ # Returns the attributes of this instance in an array (for comparison purposes).
230
+ #
231
+ def state
232
+ [@series_uid, @volumes]
233
+ end
234
+
235
+ end
236
+
237
+ end
@@ -0,0 +1,179 @@
1
+ module RTKIT
2
+
3
+ # The RTImage class contains methods that are specific for this modality (RTIMAGE).
4
+ #
5
+ # === Inheritance
6
+ #
7
+ # * RTImage inherits all methods and attributes from the Series class.
8
+ #
9
+ class RTImage < Series
10
+
11
+ # An array of Plan Verification Images associated with this RTImage Series.
12
+ attr_reader :images
13
+ # The Plan which this RTImage Series belongs to.
14
+ attr_reader :plan
15
+
16
+ # Creates a new RTImage (series) instance by loading the relevant information from the specified DICOM object.
17
+ # The Series Instance UID string value is used to uniquely identify a RTImage (series) instance.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>dcm</tt> -- An instance of a DICOM object (DICOM::DObject) with modality 'RTIMAGE'.
22
+ # * <tt>study</tt> -- The Study instance that this RTImage belongs to.
23
+ #
24
+ def self.load(dcm, study)
25
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
26
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
27
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
28
+ # Required attributes:
29
+ series_uid = dcm.value(SERIES_UID)
30
+ # Optional attributes:
31
+ class_uid = dcm.value(SOP_CLASS)
32
+ date = dcm.value(SERIES_DATE)
33
+ time = dcm.value(SERIES_TIME)
34
+ description = dcm.value(SERIES_DESCR)
35
+ series_uid = dcm.value(SERIES_UID)
36
+ # Get the corresponding Plan:
37
+ plan = self.plan(dcm, study)
38
+ # Create the RTImage instance:
39
+ rtimage = self.new(series_uid, plan, :class_uid => class_uid, :date => date, :time => time, :description => description)
40
+ rtimage.add(dcm)
41
+ return rtimage
42
+ end
43
+
44
+ # Identifies the Plan that the RTImage object belongs to.
45
+ # If the referenced instances (Plan, StructureSet, ImageSeries & Frame) does not exist, they are created by this method.
46
+ #
47
+ def self.plan(dcm, study)
48
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
49
+ raise ArgumentError, "Invalid argument 'study'. Expected Study, got #{study.class}." unless study.is_a?(Study)
50
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject with modality 'RTIMAGE', got #{dcm.value(MODALITY)}." unless dcm.value(MODALITY) == 'RTIMAGE'
51
+ # Extract the Frame of Reference UID:
52
+ begin
53
+ frame_of_ref = dcm.value(FRAME_OF_REF)
54
+ rescue
55
+ frame_of_ref = nil
56
+ end
57
+ # Extract referenced Plan SOP Instance UID:
58
+ begin
59
+ ref_plan_uid = dcm[REF_PLAN_SQ][0].value(REF_SOP_UID)
60
+ rescue
61
+ ref_plan_uid = nil
62
+ end
63
+ # Create the Frame if it doesn't exist:
64
+ f = study.patient.dataset.frame(frame_of_ref)
65
+ f = Frame.new(frame_of_ref, study.patient) unless f
66
+ # Create the Plan, StructureSet & ImageSeries if the referenced Plan doesn't exist:
67
+ plan = study.fseries(ref_plan_uid)
68
+ unless plan
69
+ # Create ImageSeries (assuming modality CT):
70
+ is = ImageSeries.new(RTKIT.series_uid, 'CT', f, study)
71
+ study.add_series(is)
72
+ # Create StructureSet:
73
+ struct = StructureSet.new(RTKIT.sop_uid, is)
74
+ study.add_series(struct)
75
+ # Create Plan:
76
+ plan = Plan.new(ref_plan_uid, struct)
77
+ study.add_series(plan)
78
+ end
79
+ return plan
80
+ end
81
+
82
+ # Creates a new RTImage instance.
83
+ #
84
+ # === Parameters
85
+ #
86
+ # * <tt>series_uid</tt> -- The Series Instance UID string.
87
+ # * <tt>plan</tt> -- The Plan that this RTImage series belongs to.
88
+ # * <tt>options</tt> -- A hash of parameters.
89
+ #
90
+ # === Options
91
+ #
92
+ # * <tt>:date</tt> -- String. The Series Date (DICOM tag '0008,0021').
93
+ # * <tt>:time</tt> -- String. The Series Time (DICOM tag '0008,0031').
94
+ # * <tt>:description</tt> -- String. The Series Description (DICOM tag '0008,103E').
95
+ # * <tt>:series_uid</tt> -- String. The Series Instance UID (DICOM tag '0020,000E').
96
+ #
97
+ def initialize(series_uid, plan, options={})
98
+ raise ArgumentError, "Invalid argument 'series_uid'. Expected String, got #{series_uid.class}." unless series_uid.is_a?(String)
99
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
100
+ # Pass attributes to Series initialization:
101
+ options[:class_uid] = '1.2.840.10008.5.1.4.1.1.481.1' # RT Image Storage
102
+ super(series_uid, 'RTIMAGE', plan.struct.study, options)
103
+ @plan = plan
104
+ # Default attributes:
105
+ @images = Array.new
106
+ @associated_images = Hash.new
107
+ # Register ourselves with the Plan:
108
+ @plan.add_rt_image(self)
109
+ end
110
+
111
+ # Returns true if the argument is an instance with attributes equal to self.
112
+ #
113
+ def ==(other)
114
+ if other.respond_to?(:to_rt_image)
115
+ other.send(:state) == state
116
+ end
117
+ end
118
+
119
+ alias_method :eql?, :==
120
+
121
+ # Registers a DICOM Object to the RTImage series, and processes it
122
+ # to create (and reference) an (RT) Image instance linked to this RTImage series.
123
+ #
124
+ def add(dcm)
125
+ raise ArgumentError, "Invalid argument 'dcm'. Expected DObject, got #{dcm.class}." unless dcm.is_a?(DICOM::DObject)
126
+ Image.load(dcm, self)
127
+ #load_patient_setup
128
+ #load_fields
129
+ end
130
+
131
+ # Adds an Image to this RTImage series.
132
+ #
133
+ def add_image(image)
134
+ raise ArgumentError, "Invalid argument 'image'. Expected Image, got #{image.class}." unless image.is_a?(Image)
135
+ @images << image unless @associated_images[image.uid]
136
+ @associated_images[image.uid] = image
137
+ end
138
+
139
+ # Generates a Fixnum hash value for this instance.
140
+ #
141
+ def hash
142
+ state.hash
143
+ end
144
+
145
+ # Returns self.
146
+ #
147
+ def to_rt_image
148
+ self
149
+ end
150
+
151
+
152
+ private
153
+
154
+
155
+ =begin
156
+ # Registers this RTImage series instance with the Plan that it references.
157
+ #
158
+ def connect_to_plan(dcm)
159
+ # Find out which Plan is referenced:
160
+ dcm[REF_PLAN_SQ].each do |plan_item|
161
+ ref_sop_uid = plan_item.value(REF_SOP_UID)
162
+ matched_plan = @study.associated_instance_uids[ref_sop_uid]
163
+ if matched_plan
164
+ # The referenced series exists in our dataset. Proceed with setting up the references:
165
+ matched_plan.add_rtimage(self)
166
+ @plan = matched_plan
167
+ end
168
+ end
169
+ end
170
+ =end
171
+
172
+ # Returns the attributes of this instance in an array (for comparison purposes).
173
+ #
174
+ def state
175
+ [@series_uid, @images]
176
+ end
177
+
178
+ end
179
+ end
@@ -0,0 +1,165 @@
1
+ # Array extensions used by RTKIT.
2
+ #
3
+ class Array
4
+
5
+ # Rearranges the array (self) so that its elements appear in exactly
6
+ # the same sequence as another array (the argument).
7
+ # If the two arrays do not contain the same set of elements, an error is raised.
8
+ #
9
+ # === Examples
10
+ #
11
+ # a = [5, 2, 10, 1]
12
+ # b = [10, 2, 1, 5]
13
+ # b.assimilate!(a)
14
+ # b
15
+ # => [5, 2, 10, 1]
16
+ #
17
+ # NB! Not in use atm!
18
+ #
19
+ def assimilate!(other)
20
+ if self.length != other.length
21
+ raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare."
22
+ else
23
+ # Validate equality:
24
+ self.each_index do |i|
25
+ index = other.index(self[i])
26
+ if index
27
+ self[i] = other[index]
28
+ else
29
+ raise "An element (index #{i}) in self was not found in the other array. Unable to assimilate."
30
+ end
31
+ end
32
+ end
33
+ return self
34
+ end
35
+
36
+ # Compares an array (self) with a target array to determine an array of indices which can be
37
+ # used to extract the elements of self in order to create an array which shares the exact
38
+ # order of elements as the target array.
39
+ # Naturally, for this comparison to make sense, the target array and self must
40
+ # contain the same set of elements.
41
+ # Raises an error if self and other are not of equal length.
42
+ #
43
+ # === Restrictions
44
+ #
45
+ # * Behaviour may be incorrect if the array contains multiple identical objects.
46
+ #
47
+ # === Examples
48
+ #
49
+ # a = ['hi', 2, dcm]
50
+ # b = [2, dcm, 'hi']
51
+ # order = b.compare_with(a)
52
+ # => [2, 0, 1]
53
+ #
54
+ def compare_with(other)
55
+ raise ArgumentError, "Arrays 'self' and 'other' are of unequal length. Unable to compare." if self.length != other.length
56
+ if self.length > 0
57
+ order = Array.new
58
+ other.each do |item|
59
+ index = self.index(item)
60
+ if index
61
+ order << index
62
+ else
63
+ raise "An element (#{item}) from the other array was not found in self. Unable to complete comparison."
64
+ end
65
+ end
66
+ end
67
+ return order
68
+ end
69
+
70
+ # Returns the most common value in the array.
71
+ #
72
+ def most_common_value
73
+ self.group_by do |e|
74
+ e
75
+ end.values.max_by(&:size).first
76
+ end
77
+
78
+ # Returns an array where the elements of the original array are extracted
79
+ # according to the indices given in the argument array.
80
+ #
81
+ # === Examples
82
+ #
83
+ # a = [5, 2, 10, 1]
84
+ # i = a.sort_order
85
+ # a.sort_by_order(i)
86
+ # => [1, 2, 5, 10]
87
+ #
88
+ #
89
+ def sort_by_order(order=[])
90
+ if self.length != order.length
91
+ return nil
92
+ else
93
+ return self.values_at(*order)
94
+ end
95
+ end
96
+
97
+ # Rearranges an array (self) so that it's original elements in the
98
+ # order specified by the indices given in the argument array.
99
+ #
100
+ # === Examples
101
+ #
102
+ # a = [5, 2, 10, 1]
103
+ # a.sort_by_order!([3, 1, 0, 2])
104
+ # a
105
+ # => [1, 2, 5, 10]
106
+ #
107
+ def sort_by_order!(order=[])
108
+ raise ArgumentError, "Invalid argument 'order'. Expected length equal to self.length (#{self.length}), got #{order.length}." if self.length != order.length
109
+ # It only makes sense to sort if length is 2 or more:
110
+ if self.length > 1
111
+ copy = self.dup
112
+ self.each_index do |i|
113
+ self[i] = copy[order[i]]
114
+ end
115
+ end
116
+ return self
117
+ end
118
+
119
+ # Returns an ordered array of indices, where each element contains the index in the original array
120
+ # which needs to be extracted to produce a sorted array.
121
+ # This method is useful if you wish to sort multiple arrays depending on the sequence of elements in a specific array.
122
+ #
123
+ # === Examples
124
+ #
125
+ # a = [5, 2, 10, 1]
126
+ # a.sort_order
127
+ # => [3, 1, 0, 2]
128
+ #
129
+ def sort_order
130
+ d=[]
131
+ self.each_with_index{|x,i| d[i]=[x,i]}
132
+ if block_given?
133
+ return d.sort {|x,y| yield x[0],y[0]}.collect{|x| x[1]}
134
+ else
135
+ return d.sort.collect{|x| x[1]}
136
+ end
137
+ end
138
+
139
+ end
140
+
141
+
142
+ class NArray
143
+
144
+ # Checks if an image array is segmented. Returns true if it is, and false if not.
145
+ # We define the image to be segmented if it contains at least positive 3 pixel values.
146
+ #
147
+ def segmented?
148
+ (self.gt 0).where.length > 2 ? true : false
149
+ end
150
+
151
+ end
152
+
153
+
154
+ class String
155
+
156
+ # Converts a string (containing a '\' separated x,y,z coordinate triplet)
157
+ # to a Coordinate instance.
158
+ #
159
+ def to_coordinate
160
+ values = self.split("\\").collect {|str| str.to_f}
161
+ raise ArgumentError, "Unable to create coordinate. Expected a string containing 3 values, got #{values.length}" unless values.length >= 3
162
+ return RTKIT::Coordinate.new(values[0], values[1], values[2])
163
+ end
164
+
165
+ end