rtp-connect 1.4 → 1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|