rtkit 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,107 @@
1
+ = RTKIT
2
+
3
+ == <em>The Radiotherapy DICOM toolkit</em>
4
+
5
+ RTKIT is a toolkit for processing information from the various DICOM modalities
6
+ encountered in radiotherapy. It contains a number of classes and convenience methods
7
+ designed to make it easy to extract and manipulate radiotherapy information of
8
+ interest, like e.g. segmentation, plan, image and dose data.
9
+
10
+ Note that the toolkit is in an early state of release, and as such, may
11
+ be a bit rough in the edges. If you are interested in using RTKIT, please feel free
12
+ to send me an email with a brief explanation of what you would like to do, and I'll
13
+ be happy to assist you in getting started with Ruby and RTKIT.
14
+
15
+ === Supported DICOM Modalities
16
+
17
+ * CT
18
+ * MR
19
+ * RTSTRUCT
20
+ * RTPLAN
21
+ * RTDOSE
22
+ * RTIMAGE
23
+
24
+
25
+ == INSTALLATION
26
+
27
+ gem install rtkit
28
+
29
+
30
+ == REQUIREMENTS
31
+
32
+ * Ruby 1.9.2 (or higher)
33
+ * ruby-dicom
34
+ * NArray
35
+
36
+
37
+ == BASIC USAGE
38
+
39
+ === Load & Include
40
+
41
+ require 'rtkit'
42
+ include RTKIT
43
+
44
+ === Load a set of DICOM files from a folder
45
+
46
+ # Load files:
47
+ ds = RTKIT::DataSet.read("C:/phantom_study/")
48
+
49
+ === Example: Merge two structure sets
50
+
51
+ # Locate the structure set objects:
52
+ structs = ds.patient.study.iseries.structs
53
+ # Extract the two structure sets:
54
+ s1 = structs.first
55
+ s2 = structs.last
56
+ # Transfer all ROIs except the external contour:
57
+ s2.rois.each do |roi|
58
+ s1.add_roi(roi) unless roi.name.downcase.include?('external')
59
+ end
60
+ # Ensure unique ROI Numbers:
61
+ s1.set_numbers
62
+ # Write the composed structure set object to file:
63
+ s1.write("fusion_struct.dcm")
64
+
65
+ === IRB Tip
66
+
67
+ When working with RTKIT in irb, you may be annoyed with all the
68
+ information that is printed to screen. This is because in irb every
69
+ variable loaded in the program is automatically printed to the screen.
70
+ A useful hack to avoid this effect is to append ";0" after a command.
71
+ Example:
72
+ ds = RTKIT::DataSet.read(folder) ;0
73
+
74
+
75
+ == RESOURCES
76
+
77
+ * {Source code repository}[https://github.com/dicom/rtkit]
78
+ * {RTKIT gem}[http://rubygems.org/gems/rtkit]
79
+ * {DICOM in Ruby}[http://dicom.rubyforge.org/]
80
+ * {ruby-dicom forum}[http://groups.google.com/group/ruby-dicom]
81
+
82
+
83
+ == COPYRIGHT
84
+
85
+ Copyright 2012 Christoffer Lervåg
86
+
87
+ This program is free software: you can redistribute it and/or modify
88
+ it under the terms of the GNU General Public License as published by
89
+ the Free Software Foundation, either version 3 of the License, or
90
+ (at your option) any later version.
91
+
92
+ This program is distributed in the hope that it will be useful,
93
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
94
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
95
+ GNU General Public License for more details.
96
+
97
+ You should have received a copy of the GNU General Public License
98
+ along with this program. If not, see http://www.gnu.org/licenses/ .
99
+
100
+
101
+ == ABOUT THE AUTHOR
102
+
103
+ * Name: Christoffer Lervåg
104
+ * Location: Norway
105
+ * Email: chris.lervag [@nospam.com] @gmail.com
106
+
107
+ Please don't hesitate to email me if you have any feedback related to this project!
@@ -0,0 +1,68 @@
1
+ # Loads files and libraries that are used by RTKIT.
2
+ # Configures some DICOM and UID settings.
3
+ #
4
+
5
+ # Logging:
6
+ require_relative 'rtkit/logging'
7
+ # Super classes/modules:
8
+ require_relative 'rtkit/series'
9
+ require_relative 'rtkit/pixel_data'
10
+ require_relative 'rtkit/mixins/image_parent'
11
+ # Subclasses and independent classes:
12
+ # Collection classes:
13
+ require_relative 'rtkit/data_set'
14
+ require_relative 'rtkit/frame'
15
+ require_relative 'rtkit/patient'
16
+ require_relative 'rtkit/study'
17
+ require_relative 'rtkit/image_series'
18
+ require_relative 'rtkit/structure_set'
19
+ require_relative 'rtkit/plan'
20
+ require_relative 'rtkit/rt_dose'
21
+ require_relative 'rtkit/rt_image'
22
+ # Image related:
23
+ require_relative 'rtkit/dose_volume'
24
+ require_relative 'rtkit/image'
25
+ require_relative 'rtkit/plane'
26
+ # Dose related:
27
+ require_relative 'rtkit/dose_distribution'
28
+ require_relative 'rtkit/dose'
29
+ # Segmentation related:
30
+ require_relative 'rtkit/roi'
31
+ require_relative 'rtkit/slice'
32
+ require_relative 'rtkit/contour'
33
+ require_relative 'rtkit/coordinate'
34
+ require_relative 'rtkit/bin_matcher'
35
+ require_relative 'rtkit/bin_volume'
36
+ require_relative 'rtkit/bin_image'
37
+ require_relative 'rtkit/staple'
38
+ require_relative 'rtkit/selection'
39
+ # Plan related:
40
+ require_relative 'rtkit/setup'
41
+ require_relative 'rtkit/beam'
42
+ require_relative 'rtkit/control_point'
43
+ require_relative 'rtkit/collimator'
44
+ require_relative 'rtkit/collimator_setup'
45
+ # Module settings:
46
+ require_relative 'rtkit/version'
47
+ require_relative 'rtkit/constants'
48
+ require_relative 'rtkit/variables'
49
+ require_relative 'rtkit/methods'
50
+ # Extensions to the Ruby library:
51
+ require_relative 'rtkit/ruby_extensions'
52
+
53
+ # Ruby Standard Library dependencies:
54
+ require 'find'
55
+ require 'matrix'
56
+ require 'set'
57
+
58
+ # External dependencies:
59
+ require 'dicom'
60
+ require 'narray'
61
+
62
+ # Modify the source application entity title of the DICOM module:
63
+ DICOM.source_app_title = "RTKIT"
64
+ # Set a high threshold for the log level of the DICOM module's logger:
65
+ DICOM.logger.level = Logger::FATAL
66
+
67
+ # Use ruby-dicom's UID as our DICOM Root UID:
68
+ RTKIT.dicom_root = DICOM::UID
@@ -0,0 +1,346 @@
1
+ module RTKIT
2
+
3
+ # Contains DICOM data and methods related to a Beam, defined in a Plan.
4
+ #
5
+ # === Relations
6
+ #
7
+ # * A Plan has many Beams.
8
+ # * A Beam has many ControlPoints.
9
+ #
10
+ class Beam
11
+
12
+ # An array containing the beam's ControlPoints.
13
+ attr_reader :control_points
14
+ # Treatment delivery type.
15
+ attr_reader :delivery_type
16
+ # Beam description.
17
+ attr_reader :description
18
+ # Treatment machine name.
19
+ attr_reader :machine
20
+ # Beam meterset.
21
+ attr_reader :meterset
22
+ # Beam Name.
23
+ attr_reader :name
24
+ # Beam Number (Integer).
25
+ attr_reader :number
26
+ # The Plan that the Beam is defined in.
27
+ attr_reader :plan
28
+ # Radiation type.
29
+ attr_reader :rad_type
30
+ # Source-axis distance.
31
+ attr_reader :sad
32
+ # Beam type.
33
+ attr_reader :type
34
+ # Primary dosimeter unit.
35
+ attr_reader :unit
36
+
37
+ # Creates a new beam instance from the beam item of the RTPlan file
38
+ # which contains the information related to a particular beam.
39
+ # This method also creates and connects any child structures as indicated in the items (e.g. ControlPoints).
40
+ # Returns the Beam instance.
41
+ #
42
+ # === Parameters
43
+ #
44
+ # * <tt>beam_item</tt> -- The Beam's Item from the Beam Sequence in the DObject of a RTPlan file.
45
+ # * <tt>meterset</tt> -- The Beam's meterset (e.g. monitor units) value.
46
+ # * <tt>plan</tt> -- The Plan instance that this Beam belongs to.
47
+ #
48
+ def self.create_from_item(beam_item, meterset, plan)
49
+ raise ArgumentError, "Invalid argument 'beam_item'. Expected DICOM::Item, got #{beam_item.class}." unless beam_item.is_a?(DICOM::Item)
50
+ raise ArgumentError, "Invalid argument 'meterset'. Expected Float, got #{meterset.class}." unless meterset.is_a?(Float)
51
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
52
+ options = Hash.new
53
+ # Values from the Structure Set ROI Sequence Item:
54
+ name = beam_item.value(BEAM_NAME) || ''
55
+ number = beam_item.value(BEAM_NUMBER).to_i
56
+ machine = beam_item.value(MACHINE_NAME) || ''
57
+ options[:type] = beam_item.value(BEAM_TYPE)
58
+ options[:delivery_type] = beam_item.value(DELIVERY_TYPE)
59
+ options[:description] = beam_item.value(BEAM_DESCR)
60
+ options[:rad_type] = beam_item.value(RAD_TYPE)
61
+ options[:sad] = beam_item.value(SAD).to_f
62
+ options[:unit] = beam_item.value(DOSIMETER_UNIT)
63
+ # Create the Beam instance:
64
+ beam = self.new(name, number, machine, meterset, plan, options)
65
+ # Iterate the RT Beam Limiting Device items and create Collimator instances:
66
+ if beam_item.exists?(COLL_SQ)
67
+ beam_item[COLL_SQ].each do |coll_item|
68
+ Collimator.create_from_item(coll_item, beam)
69
+ end
70
+ end
71
+ # Iterate the control point items and create ControlPoint instances:
72
+ beam_item[CONTROL_POINT_SQ].each do |cp_item|
73
+ ControlPoint.create_from_item(cp_item, beam)
74
+ end
75
+ return beam
76
+ end
77
+
78
+ # Creates a new Beam instance.
79
+ #
80
+ # === Parameters
81
+ #
82
+ # * <tt>name</tt> -- String. The Beam name.
83
+ # * <tt>number</tt> -- Integer. The Beam number.
84
+ # * <tt>machine</tt> -- The name of the treatment machine.
85
+ # * <tt>meterset</tt> -- The Beam's meterset (e.g. monitor units) value.
86
+ # * <tt>plan</tt> -- The Plan instance that this Beam belongs to.
87
+ # * <tt>options</tt> -- A hash of parameters.
88
+ #
89
+ # === Options
90
+ #
91
+ # * <tt>:type</tt> -- String. Beam type. Defaults to 'STATIC'.
92
+ # * <tt>:delivery_type</tt> -- String. Treatment delivery type. Defaults to 'TREATMENT'.
93
+ # * <tt>:description</tt> -- String. Beam description. Defaults to the 'name' attribute.
94
+ # * <tt>:rad_type</tt> -- String. Radiation type. Defaults to 'PHOTON'.
95
+ # * <tt>:sad</tt> -- Float. Source-axis distance. Defaults to 1000.0.
96
+ # * <tt>:unit</tt> -- String. The primary dosimeter unit. Defaults to 'MU'.
97
+ #
98
+ def initialize(name, number, machine, meterset, plan, options={})
99
+ raise ArgumentError, "Invalid argument 'plan'. Expected Plan, got #{plan.class}." unless plan.is_a?(Plan)
100
+ @control_points = Array.new
101
+ @collimators = Array.new
102
+ @associated_control_points = Hash.new
103
+ @associated_collimators = Hash.new
104
+ # Set values:
105
+ self.name = name
106
+ self.number = number
107
+ self.machine = machine
108
+ self.meterset = meterset
109
+ # Set options/defaults:
110
+ self.type = options[:type] || 'STATIC'
111
+ self.delivery_type = options[:delivery_type] || 'TREATMENT'
112
+ self.description = options[:description] || @name
113
+ self.rad_type = options[:rad_type] || 'PHOTON'
114
+ self.sad = options[:sad] ? options[:sad] : 1000.0
115
+ self.unit = options[:unit] || 'MU'
116
+ # Set references:
117
+ @plan = plan
118
+ # Register ourselves with the Plan:
119
+ @plan.add_beam(self)
120
+ end
121
+
122
+ # Returns true if the argument is an instance with attributes equal to self.
123
+ #
124
+ def ==(other)
125
+ if other.respond_to?(:to_beam)
126
+ other.send(:state) == state
127
+ end
128
+ end
129
+
130
+ alias_method :eql?, :==
131
+
132
+ # Adds a Collimator instance to this Beam.
133
+ #
134
+ def add_collimator(coll)
135
+ raise ArgumentError, "Invalid argument 'coll'. Expected Collimator, got #{coll.class}." unless coll.is_a?(Collimator)
136
+ @collimators << coll unless @associated_collimators[coll.type]
137
+ @associated_collimators[coll.type] = coll
138
+ end
139
+
140
+ # Adds a ControlPoint instance to this Beam.
141
+ #
142
+ def add_control_point(cp)
143
+ raise ArgumentError, "Invalid argument 'cp'. Expected ControlPoint, got #{cp.class}." unless cp.is_a?(ControlPoint)
144
+ @control_points << cp unless @associated_control_points[cp]
145
+ @associated_control_points[cp] = true
146
+ end
147
+
148
+ # Creates and returns a Beam Sequence Item from the attributes of the Beam.
149
+ #
150
+ def beam_item
151
+ item = DICOM::Item.new
152
+ item.add(DICOM::Element.new(ROI_COLOR, @color))
153
+ s = DICOM::Sequence.new(CONTOUR_SQ)
154
+ item.add(s)
155
+ item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
156
+ # Add Contour items to the Contour Sequence (one or several items per Slice):
157
+ @slices.each do |slice|
158
+ slice.contours.each do |contour|
159
+ s.add_item(contour.to_item)
160
+ end
161
+ end
162
+ return item
163
+ end
164
+
165
+ # Returns the Collimator instance mathcing the specified type (if an argument is used).
166
+ # If a specified type doesn't match, nil is returned.
167
+ # If no argument is passed, the first Collimator instance associated with the Beam is returned.
168
+ #
169
+ # === Parameters
170
+ #
171
+ # * <tt>type</tt> -- Integer. The Collimator's type.
172
+ #
173
+ def collimator(*args)
174
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
175
+ if args.length == 1
176
+ return @associated_collimators[args.first && args.first.to_s]
177
+ else
178
+ # No argument used, therefore we return the first instance:
179
+ return @collimators.first
180
+ end
181
+ end
182
+
183
+ # Returns the ControlPoint instance mathcing the specified index (if an argument is used).
184
+ # If a specified index doesn't match, nil is returned.
185
+ # If no argument is passed, the first ControlPoint instance associated with the Beam is returned.
186
+ #
187
+ # === Parameters
188
+ #
189
+ # * <tt>index</tt> -- Integer. The ControlPoint's index.
190
+ #
191
+ def control_point(*args)
192
+ raise ArgumentError, "Expected one or none arguments, got #{args.length}." unless [0, 1].include?(args.length)
193
+ if args.length == 1
194
+ raise ArgumentError, "Invalid argument 'index'. Expected Integer (or nil), got #{args.first.class}." unless [Integer, NilClass].include?(args.first.class)
195
+ return @associated_control_points[args.first]
196
+ else
197
+ # No argument used, therefore we return the first instance:
198
+ return @control_points.first
199
+ end
200
+ end
201
+
202
+ # Sets a new treatment delivery type for this Beam.
203
+ #
204
+ # === Parameters
205
+ #
206
+ # * <tt>value</tt> -- String. The treatment delivery type.
207
+ #
208
+ def delivery_type=(value)
209
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
210
+ @delivery_type = value
211
+ end
212
+
213
+ # Sets a new description for this Beam.
214
+ #
215
+ # === Parameters
216
+ #
217
+ # * <tt>value</tt> -- String. The beam description.
218
+ #
219
+ def description=(value)
220
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
221
+ @description = value
222
+ end
223
+
224
+ # Generates a Fixnum hash value for this instance.
225
+ #
226
+ def hash
227
+ state.hash
228
+ end
229
+
230
+ # Sets a new machine for this Beam.
231
+ #
232
+ # === Parameters
233
+ #
234
+ # * <tt>value</tt> -- String. The machine of the beam.
235
+ #
236
+ def machine=(value)
237
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
238
+ @machine = value
239
+ end
240
+
241
+ # Sets a new meterset for this Beam.
242
+ #
243
+ # === Parameters
244
+ #
245
+ # * <tt>value</tt> -- Float. The beam meterset.
246
+ #
247
+ def meterset=(value)
248
+ raise ArgumentError, "Invalid argument 'value'. Expected Float, got #{value.class}." unless value.is_a?(Float)
249
+ @meterset = value
250
+ end
251
+
252
+ # Sets a new name for this Beam.
253
+ #
254
+ # === Parameters
255
+ #
256
+ # * <tt>value</tt> -- String. The beam name.
257
+ #
258
+ def name=(value)
259
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
260
+ @name = value
261
+ end
262
+
263
+ # Sets a new number for this Beam.
264
+ #
265
+ # === Parameters
266
+ #
267
+ # * <tt>value</tt> -- Integer. The beam number.
268
+ #
269
+ def number=(value)
270
+ raise ArgumentError, "Invalid argument 'value'. Expected Integer, got #{value.class}." unless value.is_a?(Integer)
271
+ @number = value
272
+ end
273
+
274
+ # Sets a new radiation type for this Beam.
275
+ #
276
+ # === Parameters
277
+ #
278
+ # * <tt>value</tt> -- String. The radiation type.
279
+ #
280
+ def rad_type=(value)
281
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
282
+ @rad_type = value
283
+ end
284
+
285
+ # Creates and returns a Referenced Beam Sequence Item from the attributes of the Beam.
286
+ #
287
+ def ref_beam_item
288
+ item = DICOM::Item.new
289
+ item.add(DICOM::Element.new(OBS_NUMBER, @number.to_s))
290
+ item.add(DICOM::Element.new(REF_ROI_NUMBER, @number.to_s))
291
+ item.add(DICOM::Element.new(ROI_TYPE, @type))
292
+ item.add(DICOM::Element.new(ROI_INTERPRETER, @interpreter))
293
+ return item
294
+ end
295
+
296
+ # Sets a new source-axis distance for this Beam.
297
+ #
298
+ # === Parameters
299
+ #
300
+ # * <tt>value</tt> -- Float. The source-axis distance.
301
+ #
302
+ def sad=(value)
303
+ raise ArgumentError, "Invalid argument 'value'. Expected Float, got #{value.class}." unless value.is_a?(Float)
304
+ @sad = value
305
+ end
306
+
307
+ # Returns self.
308
+ #
309
+ def to_beam
310
+ self
311
+ end
312
+
313
+ # Sets a new beam type for this Beam.
314
+ #
315
+ # === Parameters
316
+ #
317
+ # * <tt>value</tt> -- String. The beam type.
318
+ #
319
+ def type=(value)
320
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
321
+ @type = value
322
+ end
323
+
324
+ # Sets a new primary dosimeter unit for this Beam.
325
+ #
326
+ # === Parameters
327
+ #
328
+ # * <tt>value</tt> -- String. The primary dosimeter unit.
329
+ #
330
+ def unit=(value)
331
+ raise ArgumentError, "Invalid argument 'value'. Expected String, got #{value.class}." unless value.is_a?(String)
332
+ @unit = value
333
+ end
334
+
335
+
336
+ private
337
+
338
+
339
+ # Returns the attributes of this instance in an array (for comparison purposes).
340
+ #
341
+ def state
342
+ [@delivery_type, @description, @machine, @meterset, @name, @number, @rad_type, @sad, @type, @unit, @control_points]
343
+ end
344
+
345
+ end
346
+ end