rtp-connect 1.4 → 1.5
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.
- data/CHANGELOG.rdoc +26 -0
- data/Gemfile.lock +6 -6
- data/README.rdoc +2 -2
- data/lib/rtp-connect.rb +1 -0
- data/lib/rtp-connect/methods.rb +33 -0
- data/lib/rtp-connect/plan.rb +9 -0
- data/lib/rtp-connect/plan_to_dcm.rb +314 -282
- data/lib/rtp-connect/prescription.rb +14 -3
- data/lib/rtp-connect/record.rb +1 -1
- data/lib/rtp-connect/ruby_extensions.rb +12 -9
- data/lib/rtp-connect/simulation_field.rb +702 -0
- data/lib/rtp-connect/version.rb +1 -1
- data/rtp-connect.gemspec +12 -12
- metadata +36 -18
- checksums.yaml +0 -7
data/CHANGELOG.rdoc
CHANGED
@@ -1,9 +1,35 @@
|
|
1
|
+
= 1.5
|
2
|
+
|
3
|
+
=== 24th October, 2013
|
4
|
+
|
5
|
+
* Added support for the Simulation Field record.
|
6
|
+
* Bumped required Ruby version to 1.9.3.
|
7
|
+
* More robust CSV implementation:
|
8
|
+
* Properly handle attributes containing a double-quote character.
|
9
|
+
* Improved handling of invalid CSV RTP files.
|
10
|
+
* Ensure that we don't produce invalid CSV with records containing attributes with the double-quote character.
|
11
|
+
* Plan#to_dcm improvements:
|
12
|
+
* Exclude CT & 2DkV fields on export.
|
13
|
+
* Properly handle the case of a missing Site Setup in the RTP file.
|
14
|
+
* Improve handling of logging in the DICOM module.
|
15
|
+
* Added logic for deciding whether the plan's machine actually has an X jaw.
|
16
|
+
* Ensure that a correct number of fields and control points are specified.
|
17
|
+
* Added support for more MLC types:
|
18
|
+
* Siemens 58 & 82 leaf
|
19
|
+
* Varian 120 leaf
|
20
|
+
* Added options for specifying undeterminable machine information such as:
|
21
|
+
* Manufacturer
|
22
|
+
* Manufacturer's Model Name
|
23
|
+
* Device Serial Number
|
24
|
+
|
25
|
+
|
1
26
|
= 1.4
|
2
27
|
|
3
28
|
=== 10th April, 2013
|
4
29
|
|
5
30
|
* Support an extended ascii character set (ISO8859-1 encoding) for record values and file read/write.
|
6
31
|
|
32
|
+
|
7
33
|
= 1.3
|
8
34
|
|
9
35
|
=== 12th October, 2012
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rtp-connect (1.
|
4
|
+
rtp-connect (1.5)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://www.rubygems.org/
|
@@ -26,10 +26,10 @@ PLATFORMS
|
|
26
26
|
x86-mingw32
|
27
27
|
|
28
28
|
DEPENDENCIES
|
29
|
-
bundler (~> 1.
|
29
|
+
bundler (~> 1.3)
|
30
30
|
dicom (~> 0.9.5)
|
31
|
-
mocha (~> 0.
|
32
|
-
rake (~> 0.9.
|
33
|
-
rspec (~> 2.
|
31
|
+
mocha (~> 0.13)
|
32
|
+
rake (~> 0.9.6)
|
33
|
+
rspec (~> 2.13)
|
34
34
|
rtp-connect!
|
35
|
-
yard (~> 0.8.
|
35
|
+
yard (~> 0.8.5)
|
data/README.rdoc
CHANGED
@@ -13,7 +13,7 @@ external dependencies.
|
|
13
13
|
|
14
14
|
== REQUIREMENTS
|
15
15
|
|
16
|
-
* Ruby 1.9.
|
16
|
+
* Ruby 1.9.3 (or higher)
|
17
17
|
|
18
18
|
|
19
19
|
== BASIC USAGE
|
@@ -92,6 +92,7 @@ Example:
|
|
92
92
|
* Plan definition [PLAN_DEF]
|
93
93
|
* Prescription site [RX_DEF]
|
94
94
|
* Site setup [SITE_SETUP_DEF]
|
95
|
+
* Simulation field [SIM_DEF]
|
95
96
|
* Treatment field [FIELD_DEF]
|
96
97
|
* Extended treatment field [EXTENDED_FIELD_DEF]
|
97
98
|
* Control point record [CONTROL_PT_DEF]
|
@@ -99,7 +100,6 @@ Example:
|
|
99
100
|
|
100
101
|
=== Unsupported records
|
101
102
|
|
102
|
-
* Simulation field [SIM_DEF]
|
103
103
|
* Extended plan definition [EXTENDED_PLAN_DEF]
|
104
104
|
* Document based treatment field [PDF_FIELD_DEF]
|
105
105
|
* Multileaf collimator [MLC_DEF]
|
data/lib/rtp-connect.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative 'rtp-connect/plan'
|
|
9
9
|
require_relative 'rtp-connect/plan_to_dcm'
|
10
10
|
require_relative 'rtp-connect/prescription'
|
11
11
|
require_relative 'rtp-connect/site_setup'
|
12
|
+
require_relative 'rtp-connect/simulation_field'
|
12
13
|
require_relative 'rtp-connect/field'
|
13
14
|
require_relative 'rtp-connect/extended_field'
|
14
15
|
require_relative 'rtp-connect/control_point'
|
data/lib/rtp-connect/methods.rb
CHANGED
@@ -6,6 +6,39 @@ module RTP
|
|
6
6
|
# Module methods:
|
7
7
|
#++
|
8
8
|
|
9
|
+
# Gives an array of MLC leaf position boundaries for a given type of MLC,
|
10
|
+
# specified by its number of leaves at one side.
|
11
|
+
#
|
12
|
+
# @param [Fixnum] nr_leaves the number of leaves (in one leaf bank)
|
13
|
+
# @return [Array<Fixnum>] the leaf boundary positions
|
14
|
+
# @raise [ArgumentError] if an unsupported MLC (nr of leaves) is given
|
15
|
+
#
|
16
|
+
def leaf_boundaries(nr_leaves)
|
17
|
+
case nr_leaves
|
18
|
+
when 29
|
19
|
+
[-200, -135, -125, -115, -105, -95, -85, -75, -65, -55, -45, -35, -25,
|
20
|
+
-15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
|
21
|
+
]
|
22
|
+
when 40
|
23
|
+
Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
24
|
+
when 41
|
25
|
+
[-200, -195, -185, -175, -165, -155, -145, -135, -125, -115,
|
26
|
+
-105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
|
27
|
+
55, 65, 75, 85, 95, 105, 115, 125, 135, 145, 155, 165, 175, 185, 195, 200
|
28
|
+
]
|
29
|
+
when 60
|
30
|
+
[-200, -190, -180, -170, -160, -150, -140, -130, -120, -110,
|
31
|
+
-100, -95, -90, -85, -80, -75, -70, -65, -60, -55, -50, -45, -40, -35, -30,
|
32
|
+
-25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65,
|
33
|
+
70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
|
34
|
+
]
|
35
|
+
when 80
|
36
|
+
Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
9
42
|
# Computes the CRC checksum of the given line and verifies that
|
10
43
|
# this value corresponds with the checksum given at the end of the line.
|
11
44
|
#
|
data/lib/rtp-connect/plan.rb
CHANGED
@@ -623,6 +623,15 @@ module RTP
|
|
623
623
|
@current_parent = f
|
624
624
|
end
|
625
625
|
|
626
|
+
# Creates a simulation field record from the given string.
|
627
|
+
#
|
628
|
+
# @param [String] string a string line containing a simulation field definition
|
629
|
+
#
|
630
|
+
def simulation_field(string)
|
631
|
+
sf = SimulationField.load(string, @current_parent)
|
632
|
+
@current_parent = sf
|
633
|
+
end
|
634
|
+
|
626
635
|
end
|
627
636
|
|
628
637
|
end
|
@@ -9,14 +9,19 @@ module RTP
|
|
9
9
|
# Electron beams or dynamic photon beams may give an invalid DICOM file.
|
10
10
|
# Also note that, due to limitations in the RTP file format, some original
|
11
11
|
# values can not be recreated, like e.g. Study UID or Series UID.
|
12
|
+
# @param [Hash] options the options to use for creating the DICOM object
|
13
|
+
# @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
|
14
|
+
# @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
|
15
|
+
# @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
|
12
16
|
# @return [DICOM::DObject] the converted DICOM object
|
13
17
|
#
|
14
|
-
def to_dcm
|
18
|
+
def to_dcm(options={})
|
15
19
|
#
|
16
20
|
# FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
|
17
21
|
# Refactoring and simplifying it at some stage might be a good idea.
|
18
22
|
#
|
19
23
|
require 'dicom'
|
24
|
+
original_level = DICOM.logger.level
|
20
25
|
DICOM.logger.level = Logger::FATAL
|
21
26
|
p = @prescriptions.first
|
22
27
|
# If no prescription is present, we are not going to be able to make a valid DICOM object:
|
@@ -162,11 +167,8 @@ module RTP
|
|
162
167
|
num_frac = '0'
|
163
168
|
end
|
164
169
|
DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
|
165
|
-
# Number of Beams:
|
166
|
-
num_beams = p ? p.fields.length : 0
|
167
|
-
DICOM::Element.new('300A,0080', "#{num_beams}", :parent => fg_item)
|
168
170
|
# Number of Brachy Application Setups:
|
169
|
-
DICOM::Element.new('300A,00A0',
|
171
|
+
DICOM::Element.new('300A,00A0', '0', :parent => fg_item)
|
170
172
|
# Referenced Beam Sequence (items created for each beam below):
|
171
173
|
rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
|
172
174
|
#
|
@@ -177,330 +179,360 @@ module RTP
|
|
177
179
|
# If no fields are present, we are not going to be able to make a valid DICOM object:
|
178
180
|
logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
|
179
181
|
p.fields.each_with_index do |field, i|
|
180
|
-
#
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
# MLCX item is only created if leaves are defined:
|
252
|
-
# (NB: The RTP file doesn't specify leaf position boundaries, so for now we estimate these positions
|
253
|
-
# based on the (even) number of leaves and the assumptions of a 200 mm position of the outer leaf)
|
254
|
-
# FIXME: In the future, the MLCX leaf position boundary should be configurable - i.e. an option argument of to_dcm().
|
255
|
-
if field.control_points.length > 0
|
256
|
-
bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
|
257
|
-
DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
|
258
|
-
num_leaves = field.control_points.first.mlc_leaves.to_i
|
259
|
-
logger.warn("Support for odd number of leaves (#{num_leaves}) is not implemented yet. Leaf Position Boundaries tag will be incorrect.") if num_leaves.odd?
|
260
|
-
logger.warn("Untested number of leaves encountered: #{num_leaves} Leaf Position Boundaries tag may be incorrect.") if num_leaves.even? && ![40, 80].include?(num_leaves)
|
261
|
-
DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
|
262
|
-
pos_boundaries = Array.new(num_leaves) {|i| i * 400 / num_leaves.to_f - 200}
|
263
|
-
DICOM::Element.new('300A,00BE', "#{pos_boundaries.join("\\")}", :parent => bl_item_mlcx)
|
264
|
-
end
|
265
|
-
#
|
266
|
-
# Block Sequence (if any):
|
267
|
-
# FIXME: It seems that the Block Sequence (300A,00F4) may be
|
268
|
-
# difficult (impossible?) to reconstruct based on the RTP file's
|
269
|
-
# information, and thus it is skipped altogether.
|
270
|
-
#
|
271
|
-
#
|
272
|
-
# Applicator Sequence (if any):
|
273
|
-
#
|
274
|
-
unless field.e_applicator.empty?
|
275
|
-
app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
|
276
|
-
app_item = DICOM::Item.new(:parent => app_seq)
|
277
|
-
# Applicator ID:
|
278
|
-
DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
|
279
|
-
# Applicator Type:
|
280
|
-
DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
|
281
|
-
# Applicator Description:
|
282
|
-
DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
|
283
|
-
end
|
284
|
-
#
|
285
|
-
# Control Point Sequence:
|
286
|
-
#
|
287
|
-
# A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
|
288
|
-
# The DICOM file shall always contain 2n control points (minimum 2).
|
289
|
-
#
|
290
|
-
cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
|
291
|
-
if field.control_points.length < 2
|
292
|
-
# When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
|
293
|
-
# First CP:
|
294
|
-
cp_item = DICOM::Item.new(:parent => cp_seq)
|
295
|
-
# Control Point Index:
|
296
|
-
DICOM::Element.new('300A,0112', "0", :parent => cp_item)
|
297
|
-
# Nominal Beam Energy:
|
298
|
-
DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
|
299
|
-
# Dose Rate Set:
|
300
|
-
DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
|
301
|
-
# Gantry Angle:
|
302
|
-
DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
|
303
|
-
# Gantry Rotation Direction:
|
304
|
-
DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
|
305
|
-
# Beam Limiting Device Angle:
|
306
|
-
DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
|
307
|
-
# Beam Limiting Device Rotation Direction:
|
308
|
-
DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
|
309
|
-
# Patient Support Angle:
|
310
|
-
DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
|
311
|
-
# Patient Support Rotation Direction:
|
312
|
-
DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
|
313
|
-
# Table Top Eccentric Angle:
|
314
|
-
DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
|
315
|
-
# Table Top Eccentric Rotation Direction:
|
316
|
-
DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
|
317
|
-
# Table Top Vertical Position:
|
318
|
-
couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
|
319
|
-
DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
|
320
|
-
# Table Top Longitudinal Position:
|
321
|
-
couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
|
322
|
-
DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
|
323
|
-
# Table Top Lateral Position:
|
324
|
-
couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
|
325
|
-
DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
|
326
|
-
# Isocenter Position (x\y\z):
|
327
|
-
DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item)
|
328
|
-
# Source to Surface Distance:
|
329
|
-
DICOM::Element.new('300A,0130', "#{field.ssd.to_f * 10}", :parent => cp_item)
|
330
|
-
# Cumulative Meterset Weight:
|
331
|
-
DICOM::Element.new('300A,0134', "0.0", :parent => cp_item)
|
332
|
-
# Beam Limiting Device Position Sequence:
|
333
|
-
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
334
|
-
# Always create one ASYMX and one ASYMY item:
|
335
|
-
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
336
|
-
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
182
|
+
# Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped:
|
183
|
+
unless field.modality == 'Unspecified'
|
184
|
+
# If this is an electron beam, a warning should be printed, as these are less reliably converted:
|
185
|
+
logger.warn("This is not a photon beam (#{field.modality}). Beware that DICOM conversion of Electron beams are experimental, and other modalities are unsupported.") if field.modality != 'Xrays'
|
186
|
+
# Beam number and name:
|
187
|
+
beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
|
188
|
+
beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
|
189
|
+
# Ref Beam Item:
|
190
|
+
rb_item = DICOM::Item.new(:parent => rb_seq)
|
191
|
+
# Beam Dose (convert from cGy to Gy):
|
192
|
+
field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
|
193
|
+
DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
|
194
|
+
# Beam Meterset:
|
195
|
+
DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
|
196
|
+
# Referenced Beam Number:
|
197
|
+
DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
|
198
|
+
# Beam Item:
|
199
|
+
b_item = DICOM::Item.new(:parent => b_seq)
|
200
|
+
# Optional method values:
|
201
|
+
# Manufacturer:
|
202
|
+
DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer]
|
203
|
+
# Manufacturer's Model Name:
|
204
|
+
DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model]
|
205
|
+
# Device Serial Number:
|
206
|
+
DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number]
|
207
|
+
# Treatment Machine Name (max 16 characters):
|
208
|
+
DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
|
209
|
+
# Primary Dosimeter Unit:
|
210
|
+
DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
|
211
|
+
# Source-Axis Distance (convert to mm):
|
212
|
+
DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
|
213
|
+
# Beam Number:
|
214
|
+
DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
|
215
|
+
# Beam Name:
|
216
|
+
DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
|
217
|
+
# Beam Description:
|
218
|
+
DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
|
219
|
+
# Beam Type:
|
220
|
+
beam_type = case field.treatment_type
|
221
|
+
when 'Static' then 'STATIC'
|
222
|
+
when 'StepNShoot' then 'STATIC'
|
223
|
+
else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
|
224
|
+
end
|
225
|
+
DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
|
226
|
+
# Radiation Type:
|
227
|
+
rad_type = case field.modality
|
228
|
+
when 'Elect' then 'ELECTRON'
|
229
|
+
when 'Xrays' then 'PHOTON'
|
230
|
+
else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
|
231
|
+
end
|
232
|
+
DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
|
233
|
+
# Treatment Delivery Type:
|
234
|
+
DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
|
235
|
+
# Number of Wedges:
|
236
|
+
DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
|
237
|
+
# Number of Compensators:
|
238
|
+
DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
|
239
|
+
# Number of Boli:
|
240
|
+
DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
|
241
|
+
# Number of Blocks:
|
242
|
+
DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
|
243
|
+
# Final Cumulative Meterset Weight:
|
244
|
+
DICOM::Element.new('300A,010E', field.field_monitor_units, :parent => b_item)
|
245
|
+
# Referenced Patient Setup Number:
|
246
|
+
DICOM::Element.new('300C,006A', '1', :parent => b_item)
|
247
|
+
#
|
248
|
+
# Beam Limiting Device Sequence:
|
249
|
+
#
|
250
|
+
bl_seq = DICOM::Sequence.new('300A,00B6', :parent => b_item)
|
251
|
+
# Always create one ASYMY item:
|
252
|
+
bl_item_y = DICOM::Item.new(:parent => bl_seq)
|
337
253
|
# RT Beam Limiting Device Type:
|
338
|
-
DICOM::Element.new('300A,00B8', "
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
254
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
|
255
|
+
# Number of Leaf/Jaw Pairs:
|
256
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
|
257
|
+
# The ASYMX item ('backup jaws') only exsists on some models:
|
258
|
+
if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
|
259
|
+
bl_item_x = DICOM::Item.new(:parent => bl_seq)
|
260
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
|
261
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
|
262
|
+
end
|
263
|
+
# MLCX item is only created if leaves are defined:
|
264
|
+
# (NB: The RTP file doesn't specify leaf position boundaries, so we
|
265
|
+
# have to set these based on a set of known MLC types, their number
|
266
|
+
# of leaves, and their leaf boundary positions.)
|
344
267
|
if field.control_points.length > 0
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
268
|
+
bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
|
269
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
|
270
|
+
num_leaves = field.control_points.first.mlc_leaves.to_i
|
271
|
+
DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
|
272
|
+
DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
|
273
|
+
end
|
274
|
+
#
|
275
|
+
# Block Sequence (if any):
|
276
|
+
# FIXME: It seems that the Block Sequence (300A,00F4) may be
|
277
|
+
# difficult (impossible?) to reconstruct based on the RTP file's
|
278
|
+
# information, and thus it is skipped altogether.
|
279
|
+
#
|
280
|
+
#
|
281
|
+
# Applicator Sequence (if any):
|
282
|
+
#
|
283
|
+
unless field.e_applicator.empty?
|
284
|
+
app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
|
285
|
+
app_item = DICOM::Item.new(:parent => app_seq)
|
286
|
+
# Applicator ID:
|
287
|
+
DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
|
288
|
+
# Applicator Type:
|
289
|
+
DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
|
290
|
+
# Applicator Description:
|
291
|
+
DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
|
353
292
|
end
|
354
|
-
#
|
355
|
-
|
356
|
-
|
357
|
-
#
|
358
|
-
DICOM
|
359
|
-
#
|
360
|
-
DICOM::
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
# Cumulative Meterset Weight:
|
366
|
-
DICOM::Element.new('300A,0134', field.field_monitor_units, :parent => cp_item)
|
367
|
-
else
|
368
|
-
# When we have multiple (2n) control points, iterate and pick settings from the CPs:
|
369
|
-
field.control_points.each_slice(2) do |cp1, cp2|
|
370
|
-
cp_item1 = DICOM::Item.new(:parent => cp_seq)
|
371
|
-
cp_item2 = DICOM::Item.new(:parent => cp_seq)
|
372
|
-
# First control point:
|
293
|
+
#
|
294
|
+
# Control Point Sequence:
|
295
|
+
#
|
296
|
+
# A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
|
297
|
+
# The DICOM file shall always contain 2n control points (minimum 2).
|
298
|
+
#
|
299
|
+
cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
|
300
|
+
if field.control_points.length < 2
|
301
|
+
# When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
|
302
|
+
# First CP:
|
303
|
+
cp_item = DICOM::Item.new(:parent => cp_seq)
|
373
304
|
# Control Point Index:
|
374
|
-
DICOM::Element.new('300A,0112', "
|
305
|
+
DICOM::Element.new('300A,0112', "0", :parent => cp_item)
|
375
306
|
# Nominal Beam Energy:
|
376
|
-
DICOM::Element.new('300A,0114', "#{
|
307
|
+
DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
|
377
308
|
# Dose Rate Set:
|
378
|
-
DICOM::Element.new('300A,0115',
|
309
|
+
DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
|
379
310
|
# Gantry Angle:
|
380
|
-
DICOM::Element.new('300A,011E',
|
311
|
+
DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
|
381
312
|
# Gantry Rotation Direction:
|
382
|
-
DICOM::Element.new('300A,011F', (
|
313
|
+
DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
|
383
314
|
# Beam Limiting Device Angle:
|
384
|
-
DICOM::Element.new('300A,0120',
|
315
|
+
DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
|
385
316
|
# Beam Limiting Device Rotation Direction:
|
386
|
-
DICOM::Element.new('300A,0121',
|
317
|
+
DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
|
387
318
|
# Patient Support Angle:
|
388
|
-
DICOM::Element.new('300A,0122',
|
319
|
+
DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
|
389
320
|
# Patient Support Rotation Direction:
|
390
|
-
DICOM::Element.new('300A,0123',
|
321
|
+
DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
|
391
322
|
# Table Top Eccentric Angle:
|
392
|
-
DICOM::Element.new('300A,0125',
|
323
|
+
DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
|
393
324
|
# Table Top Eccentric Rotation Direction:
|
394
|
-
DICOM::Element.new('300A,0126',
|
325
|
+
DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
|
395
326
|
# Table Top Vertical Position:
|
396
|
-
couch_vert =
|
397
|
-
DICOM::Element.new('300A,0128', couch_vert, :parent =>
|
327
|
+
couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
|
328
|
+
DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
|
398
329
|
# Table Top Longitudinal Position:
|
399
|
-
couch_long =
|
400
|
-
DICOM::Element.new('300A,0129', couch_long, :parent =>
|
330
|
+
couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
|
331
|
+
DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
|
401
332
|
# Table Top Lateral Position:
|
402
|
-
couch_lat =
|
403
|
-
DICOM::Element.new('300A,012A', couch_lat, :parent =>
|
333
|
+
couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
|
334
|
+
DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
|
404
335
|
# Isocenter Position (x\y\z):
|
405
|
-
|
336
|
+
if p.site_setup
|
337
|
+
DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item)
|
338
|
+
else
|
339
|
+
logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
|
340
|
+
DICOM::Element.new('300A,012C', '', :parent => cp_item)
|
341
|
+
end
|
406
342
|
# Source to Surface Distance:
|
407
|
-
DICOM::Element.new('300A,0130', "#{
|
343
|
+
DICOM::Element.new('300A,0130', "#{field.ssd.to_f * 10}", :parent => cp_item)
|
408
344
|
# Cumulative Meterset Weight:
|
409
|
-
|
410
|
-
DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item1)
|
345
|
+
DICOM::Element.new('300A,0134', "0.0", :parent => cp_item)
|
411
346
|
# Beam Limiting Device Position Sequence:
|
412
|
-
dp_seq = DICOM::Sequence.new('300A,011A', :parent =>
|
413
|
-
# Always create one
|
414
|
-
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
347
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
348
|
+
# Always create one ASYMY item:
|
415
349
|
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
416
350
|
# RT Beam Limiting Device Type:
|
417
|
-
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
418
351
|
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
419
352
|
# Leaf/Jaw Positions:
|
420
|
-
DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
|
421
353
|
DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
|
354
|
+
# The ASYMX item ('backup jaws') only exsists on some models:
|
355
|
+
if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
|
356
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
357
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
358
|
+
DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
|
359
|
+
end
|
422
360
|
# MLCX:
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
361
|
+
if field.control_points.length > 0
|
362
|
+
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
363
|
+
# RT Beam Limiting Device Type:
|
364
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
365
|
+
# Leaf/Jaw Positions:
|
366
|
+
pos_a = field.control_points.first.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
367
|
+
pos_b = field.control_points.first.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
368
|
+
leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
|
369
|
+
DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
|
370
|
+
end
|
431
371
|
# Referenced Dose Reference Sequence:
|
432
|
-
rd_seq = DICOM::Sequence.new('300C,0050', :parent =>
|
372
|
+
rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
|
433
373
|
rd_item = DICOM::Item.new(:parent => rd_seq)
|
434
374
|
# Cumulative Dose Reference Coeffecient:
|
435
375
|
DICOM::Element.new('300A,010C', '', :parent => rd_item)
|
436
376
|
# Referenced Dose Reference Number:
|
437
377
|
DICOM::Element.new('300C,0051', '1', :parent => rd_item)
|
438
|
-
# Second
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
DICOM::Element.new('300A,
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
378
|
+
# Second CP:
|
379
|
+
cp_item = DICOM::Item.new(:parent => cp_seq)
|
380
|
+
# Control Point Index:
|
381
|
+
DICOM::Element.new('300A,0112', "1", :parent => cp_item)
|
382
|
+
# Cumulative Meterset Weight:
|
383
|
+
DICOM::Element.new('300A,0134', field.field_monitor_units, :parent => cp_item)
|
384
|
+
else
|
385
|
+
# When we have multiple (2n) control points, iterate and pick settings from the CPs:
|
386
|
+
field.control_points.each_slice(2) do |cp1, cp2|
|
387
|
+
cp_item1 = DICOM::Item.new(:parent => cp_seq)
|
388
|
+
cp_item2 = DICOM::Item.new(:parent => cp_seq)
|
389
|
+
# First control point:
|
390
|
+
# Control Point Index:
|
391
|
+
DICOM::Element.new('300A,0112', "#{cp1.index}", :parent => cp_item1)
|
392
|
+
# Nominal Beam Energy:
|
393
|
+
DICOM::Element.new('300A,0114', "#{cp1.energy.to_f}", :parent => cp_item1)
|
394
|
+
# Dose Rate Set:
|
395
|
+
DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item1)
|
396
|
+
# Gantry Angle:
|
397
|
+
DICOM::Element.new('300A,011E', cp1.gantry_angle, :parent => cp_item1)
|
398
|
+
# Gantry Rotation Direction:
|
399
|
+
DICOM::Element.new('300A,011F', (cp1.gantry_dir.empty? ? 'NONE' : cp1.gantry_dir), :parent => cp_item1)
|
400
|
+
# Beam Limiting Device Angle:
|
401
|
+
DICOM::Element.new('300A,0120', cp1.collimator_angle, :parent => cp_item1)
|
402
|
+
# Beam Limiting Device Rotation Direction:
|
403
|
+
DICOM::Element.new('300A,0121', (cp1.collimator_dir.empty? ? 'NONE' : cp1.collimator_dir), :parent => cp_item1)
|
404
|
+
# Patient Support Angle:
|
405
|
+
DICOM::Element.new('300A,0122', cp1.couch_pedestal, :parent => cp_item1)
|
406
|
+
# Patient Support Rotation Direction:
|
407
|
+
DICOM::Element.new('300A,0123', (cp1.couch_ped_dir.empty? ? 'NONE' : cp1.couch_ped_dir), :parent => cp_item1)
|
408
|
+
# Table Top Eccentric Angle:
|
409
|
+
DICOM::Element.new('300A,0125', cp1.couch_angle, :parent => cp_item1)
|
410
|
+
# Table Top Eccentric Rotation Direction:
|
411
|
+
DICOM::Element.new('300A,0126', (cp1.couch_dir.empty? ? 'NONE' : cp1.couch_dir), :parent => cp_item1)
|
412
|
+
# Table Top Vertical Position:
|
413
|
+
couch_vert = cp1.couch_vertical.empty? ? '' : (cp1.couch_vertical.to_f * 10).to_s
|
414
|
+
DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item1)
|
415
|
+
# Table Top Longitudinal Position:
|
416
|
+
couch_long = cp1.couch_longitudinal.empty? ? '' : (cp1.couch_longitudinal.to_f * 10).to_s
|
417
|
+
DICOM::Element.new('300A,0129', couch_long, :parent => cp_item1)
|
418
|
+
# Table Top Lateral Position:
|
419
|
+
couch_lat = cp1.couch_lateral.empty? ? '' : (cp1.couch_lateral.to_f * 10).to_s
|
420
|
+
DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item1)
|
421
|
+
# Isocenter Position (x\y\z):
|
422
|
+
if p.site_setup
|
423
|
+
DICOM::Element.new('300A,012C', "#{(p.site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(p.site_setup.iso_pos_z.to_f * 10).round(2)}", :parent => cp_item1)
|
424
|
+
else
|
425
|
+
logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
|
426
|
+
DICOM::Element.new('300A,012C', '', :parent => cp_item1)
|
427
|
+
end
|
428
|
+
# Source to Surface Distance:
|
429
|
+
DICOM::Element.new('300A,0130', "#{cp1.ssd.to_f * 10}", :parent => cp_item1)
|
430
|
+
# Cumulative Meterset Weight:
|
431
|
+
mu_weight = (cp1.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
|
432
|
+
DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item1)
|
433
|
+
# Beam Limiting Device Position Sequence:
|
434
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item1)
|
435
|
+
# Always create one ASYMY item:
|
485
436
|
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
437
|
+
# RT Beam Limiting Device Type:
|
486
438
|
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
439
|
+
# Leaf/Jaw Positions:
|
487
440
|
DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
|
488
|
-
|
489
|
-
|
490
|
-
|
441
|
+
# The ASYMX item ('backup jaws') only exsists on some models:
|
442
|
+
if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
|
443
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
444
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
445
|
+
DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
|
446
|
+
end
|
447
|
+
# MLCX:
|
491
448
|
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
492
449
|
# RT Beam Limiting Device Type:
|
493
450
|
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
494
451
|
# Leaf/Jaw Positions:
|
495
|
-
pos_a =
|
496
|
-
pos_b =
|
452
|
+
pos_a = cp1.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
453
|
+
pos_b = cp1.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
497
454
|
leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
|
498
455
|
DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
|
456
|
+
# Referenced Dose Reference Sequence:
|
457
|
+
rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item1)
|
458
|
+
rd_item = DICOM::Item.new(:parent => rd_seq)
|
459
|
+
# Cumulative Dose Reference Coeffecient:
|
460
|
+
DICOM::Element.new('300A,010C', '', :parent => rd_item)
|
461
|
+
# Referenced Dose Reference Number:
|
462
|
+
DICOM::Element.new('300C,0051', '1', :parent => rd_item)
|
463
|
+
# Second control point:
|
464
|
+
# Always include index and cumulative weight:
|
465
|
+
DICOM::Element.new('300A,0112', "#{cp2.index}", :parent => cp_item2)
|
466
|
+
mu_weight = (cp2.monitor_units.to_f * field.field_monitor_units.to_f).round(4)
|
467
|
+
DICOM::Element.new('300A,0134', "#{mu_weight}", :parent => cp_item2)
|
468
|
+
# The other parameters are only included if they have changed from the previous control point:
|
469
|
+
# Nominal Beam Energy:
|
470
|
+
DICOM::Element.new('300A,0114', "#{cp2.energy.to_f}", :parent => cp_item2) if cp2.energy != cp1.energy
|
471
|
+
# Dose Rate Set:
|
472
|
+
DICOM::Element.new('300A,0115', cp1.doserate, :parent => cp_item2) if cp2.doserate != cp1.doserate
|
473
|
+
# Gantry Angle:
|
474
|
+
DICOM::Element.new('300A,011E', cp2.gantry_angle, :parent => cp_item2) if cp2.gantry_angle != cp1.gantry_angle
|
475
|
+
# Gantry Rotation Direction:
|
476
|
+
DICOM::Element.new('300A,011F', (cp2.gantry_dir.empty? ? 'NONE' : cp2.gantry_dir), :parent => cp_item2) if cp2.gantry_dir != cp1.gantry_dir
|
477
|
+
# Beam Limiting Device Angle:
|
478
|
+
DICOM::Element.new('300A,0120', cp2.collimator_angle, :parent => cp_item2) if cp2.collimator_angle != cp1.collimator_angle
|
479
|
+
# Beam Limiting Device Rotation Direction:
|
480
|
+
DICOM::Element.new('300A,0121', (cp2.collimator_dir.empty? ? 'NONE' : cp2.collimator_dir), :parent => cp_item2) if cp2.collimator_dir != cp1.collimator_dir
|
481
|
+
# Patient Support Angle:
|
482
|
+
DICOM::Element.new('300A,0122', cp2.couch_pedestal, :parent => cp_item2) if cp2.couch_pedestal != cp1.couch_pedestal
|
483
|
+
# Patient Support Rotation Direction:
|
484
|
+
DICOM::Element.new('300A,0123', (cp2.couch_ped_dir.empty? ? 'NONE' : cp2.couch_ped_dir), :parent => cp_item2) if cp2.couch_ped_dir != cp1.couch_ped_dir
|
485
|
+
# Table Top Eccentric Angle:
|
486
|
+
DICOM::Element.new('300A,0125', cp2.couch_angle, :parent => cp_item2) if cp2.couch_angle != cp1.couch_angle
|
487
|
+
# Table Top Eccentric Rotation Direction:
|
488
|
+
DICOM::Element.new('300A,0126', (cp2.couch_dir.empty? ? 'NONE' : cp2.couch_dir), :parent => cp_item2) if cp2.couch_dir != cp1.couch_dir
|
489
|
+
# Table Top Vertical Position:
|
490
|
+
couch_vert = cp2.couch_vertical.empty? ? '' : (cp2.couch_vertical.to_f * 10).to_s
|
491
|
+
DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item2) if cp2.couch_vertical != cp1.couch_vertical
|
492
|
+
# Table Top Longitudinal Position:
|
493
|
+
couch_long = cp2.couch_longitudinal.empty? ? '' : (cp2.couch_longitudinal.to_f * 10).to_s
|
494
|
+
DICOM::Element.new('300A,0129', couch_long, :parent => cp_item2) if cp2.couch_longitudinal != cp1.couch_longitudinal
|
495
|
+
# Table Top Lateral Position:
|
496
|
+
couch_lat = cp2.couch_lateral.empty? ? '' : (cp2.couch_lateral.to_f * 10).to_s
|
497
|
+
DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item2) if cp2.couch_lateral != cp1.couch_lateral
|
498
|
+
# Source to Surface Distance:
|
499
|
+
DICOM::Element.new('300A,0130', "#{cp2.ssd.to_f * 10}", :parent => cp_item2) if cp2.ssd != cp1.ssd
|
500
|
+
# Beam Limiting Device Position Sequence:
|
501
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item2)
|
502
|
+
# ASYMX:
|
503
|
+
if cp2.collimator_x1 != cp1.collimator_x1
|
504
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
505
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
506
|
+
DICOM::Element.new('300A,011C', "#{field.collimator_x1.to_f * 10}\\#{field.collimator_x2.to_f * 10}", :parent => dp_item_x)
|
507
|
+
end
|
508
|
+
# ASYMY:
|
509
|
+
if cp2.collimator_y1 != cp1.collimator_y1
|
510
|
+
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
511
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
512
|
+
DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
|
513
|
+
end
|
514
|
+
# MLCX:
|
515
|
+
if cp2.mlc_lp_a != cp1.mlc_lp_a or cp2.mlc_lp_b != cp1.mlc_lp_b
|
516
|
+
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
517
|
+
# RT Beam Limiting Device Type:
|
518
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
519
|
+
# Leaf/Jaw Positions:
|
520
|
+
pos_a = cp2.mlc_lp_a.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
521
|
+
pos_b = cp2.mlc_lp_b.collect{|p| (p.to_f * 10).round(2) unless p.empty?}.compact
|
522
|
+
leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
|
523
|
+
DICOM::Element.new('300A,011C', leaf_pos, :parent => dp_item_mlcx)
|
524
|
+
end
|
499
525
|
end
|
500
526
|
end
|
527
|
+
# Number of Control Points:
|
528
|
+
DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
|
501
529
|
end
|
502
530
|
end
|
531
|
+
# Number of Beams:
|
532
|
+
DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item)
|
503
533
|
end
|
534
|
+
# Restore the DICOM logger:
|
535
|
+
DICOM.logger.level = original_level
|
504
536
|
return dcm
|
505
537
|
end
|
506
538
|
|