rtp-connect 1.5 → 1.6
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +19 -0
- data/Gemfile.lock +1 -1
- data/lib/rtp-connect/control_point.rb +78 -0
- data/lib/rtp-connect/methods.rb +2 -2
- data/lib/rtp-connect/plan_to_dcm.rb +380 -226
- data/lib/rtp-connect/version.rb +1 -1
- metadata +3 -3
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,22 @@
|
|
1
|
+
= 1.6
|
2
|
+
|
3
|
+
=== 12th December, 2013
|
4
|
+
|
5
|
+
* Plan#to_dcm improvements:
|
6
|
+
* Added support for VMAT by improving the handling of control point conversion.
|
7
|
+
* Made dose reference sequences optional.
|
8
|
+
* Order beam limiting device items alphabetically.
|
9
|
+
* Added rudimentary support for scale conversion (scale convention = 1 in control point records).
|
10
|
+
* More robust extraction of jaw position.
|
11
|
+
* More robust handling of cases with missing structure set in the RTP file.
|
12
|
+
* Add support for tolerance table sequence.
|
13
|
+
* Fixed a bug with missing leaf boundary value for 80 and 160 leaf MLCs.
|
14
|
+
* Switched to using fractional cumulative meterset weight, which seems to be more commonly used in commercial systems.
|
15
|
+
* Don't create an SSD DICOM element if the SSD attribute in the RTP file is undefined.
|
16
|
+
* Only write control point attributes which have changed since the previous/initial control point of each beam.
|
17
|
+
* Make sure that the last cumulative meterset weight exactly equals the final cumulative meterset weight.
|
18
|
+
|
19
|
+
|
1
20
|
= 1.5
|
2
21
|
|
3
22
|
=== 24th October, 2013
|
data/Gemfile.lock
CHANGED
@@ -143,6 +143,57 @@ module RTP
|
|
143
143
|
return Array.new
|
144
144
|
end
|
145
145
|
|
146
|
+
# Converts the collimator_x1 attribute to proper DICOM format.
|
147
|
+
#
|
148
|
+
# @return [Float] the DICOM-formatted collimator_x1 attribute
|
149
|
+
#
|
150
|
+
def dcm_collimator_x1
|
151
|
+
attribute = (scale_convertion? ? :collimator_y1 : :collimator_x1)
|
152
|
+
target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
|
153
|
+
target.send(attribute).to_f * 10 * scale_factor
|
154
|
+
end
|
155
|
+
|
156
|
+
# Converts the collimator_x2 attribute to proper DICOM format.
|
157
|
+
#
|
158
|
+
# @return [Float] the DICOM-formatted collimator_x2 attribute
|
159
|
+
#
|
160
|
+
def dcm_collimator_x2
|
161
|
+
attribute = (scale_convertion? ? :collimator_y2 : :collimator_x2)
|
162
|
+
target = (@field_x_mode && !@field_x_mode.empty? ? self : @parent)
|
163
|
+
target.send(attribute).to_f * 10
|
164
|
+
end
|
165
|
+
|
166
|
+
# Converts the collimator_y1 attribute to proper DICOM format.
|
167
|
+
#
|
168
|
+
# @return [Float] the DICOM-formatted collimator_y1 attribute
|
169
|
+
#
|
170
|
+
def dcm_collimator_y1
|
171
|
+
attribute = (scale_convertion? ? :collimator_x1 : :collimator_y1)
|
172
|
+
target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
|
173
|
+
target.send(attribute).to_f * 10 * scale_factor
|
174
|
+
end
|
175
|
+
|
176
|
+
# Converts the collimator_y2 attribute to proper DICOM format.
|
177
|
+
#
|
178
|
+
# @return [Float] the DICOM-formatted collimator_y2 attribute
|
179
|
+
#
|
180
|
+
def dcm_collimator_y2
|
181
|
+
attribute = (scale_convertion? ? :collimator_x2 : :collimator_y2)
|
182
|
+
target = (@field_y_mode && !@field_y_mode.empty? ? self : @parent)
|
183
|
+
target.send(attribute).to_f * 10
|
184
|
+
end
|
185
|
+
|
186
|
+
# Converts the mlc_lp_a & mlc_lp_b attributes to a proper DICOM formatted string.
|
187
|
+
#
|
188
|
+
# @return [String] the DICOM-formatted leaf pair positions
|
189
|
+
#
|
190
|
+
def dcm_mlc_positions
|
191
|
+
# As with the collimators, the first side (1/a) may need scale invertion:
|
192
|
+
pos_a = @mlc_lp_a.collect{|p| (p.to_f * 10 * scale_factor).round(1) unless p.empty?}.compact
|
193
|
+
pos_b = @mlc_lp_b.collect{|p| (p.to_f * 10).round(1) unless p.empty?}.compact
|
194
|
+
(pos_a + pos_b).join("\\")
|
195
|
+
end
|
196
|
+
|
146
197
|
# Computes a hash code for this object.
|
147
198
|
#
|
148
199
|
# @note Two objects with the same attributes will have the same hash code.
|
@@ -526,6 +577,33 @@ module RTP
|
|
526
577
|
#
|
527
578
|
alias_method :state, :values
|
528
579
|
|
580
|
+
# Checks whether the contents of the this record indicates that scale
|
581
|
+
# convertion is to be applied. This convertion entails converting a value
|
582
|
+
# from IEC1217 format to the target machine's native readout format.
|
583
|
+
# Note that the scope of this scale conversion is not precisely known (the
|
584
|
+
# current implementation is based on a few observations made from a single
|
585
|
+
# RTP file).
|
586
|
+
#
|
587
|
+
# @return [Boolean] true if the scale convention attribute indicates scale convertion
|
588
|
+
#
|
589
|
+
def scale_convertion?
|
590
|
+
# A scale convention of 1 means that geometric parameters are represented
|
591
|
+
# in the target machine's native readout format, as opposed to the IEC 1217
|
592
|
+
# convention. The consequences of this is not totally clear, but at least for
|
593
|
+
# an Elekta device, there are a number of convertions which seems to be indicated.
|
594
|
+
@scale_convention.to_i == 1 ? true : false
|
595
|
+
end
|
596
|
+
|
597
|
+
# Gives a factor used for scale convertion, which depends on the
|
598
|
+
# 'scale_convention' attribute.
|
599
|
+
#
|
600
|
+
# @param [Numerical] value the value to process
|
601
|
+
# @return [Numerical] the scale converted value
|
602
|
+
#
|
603
|
+
def scale_factor
|
604
|
+
scale_convertion? ? -1 : 1
|
605
|
+
end
|
606
|
+
|
529
607
|
end
|
530
608
|
|
531
609
|
end
|
data/lib/rtp-connect/methods.rb
CHANGED
@@ -20,7 +20,7 @@ module RTP
|
|
20
20
|
-15, -5, 5, 15, 25, 35, 45, 55, 65, 75, 85, 95, 105, 115, 125, 135, 200
|
21
21
|
]
|
22
22
|
when 40
|
23
|
-
Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
23
|
+
Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
24
24
|
when 41
|
25
25
|
[-200, -195, -185, -175, -165, -155, -145, -135, -125, -115,
|
26
26
|
-105, -95, -85, -75, -65, -55, -45, -35, -25, -15, -5, 5, 15, 25, 35, 45,
|
@@ -33,7 +33,7 @@ module RTP
|
|
33
33
|
70, 75, 80, 85, 90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200
|
34
34
|
]
|
35
35
|
when 80
|
36
|
-
Array.new(nr_leaves) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
36
|
+
Array.new(nr_leaves+1) {|i| (i * 400 / nr_leaves.to_f - 200).to_i}
|
37
37
|
else
|
38
38
|
raise ArgumentError, "Unsupported number of leaves: #{nr_leaves}"
|
39
39
|
end
|
@@ -5,11 +5,12 @@ module RTP
|
|
5
5
|
# Converts the Plan (and child) records to a
|
6
6
|
# DICOM::DObject of modality RTPLAN.
|
7
7
|
#
|
8
|
-
# @note Only
|
9
|
-
# Electron beams
|
8
|
+
# @note Only photon plans have been tested.
|
9
|
+
# Electron beams 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
12
|
# @param [Hash] options the options to use for creating the DICOM object
|
13
|
+
# @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
|
13
14
|
# @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
|
14
15
|
# @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
|
15
16
|
# @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
|
@@ -103,27 +104,34 @@ module RTP
|
|
103
104
|
# RT Plan Time:
|
104
105
|
plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
|
105
106
|
DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
|
106
|
-
# RT Plan Geometry:
|
107
|
-
DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
|
108
107
|
# Approval Status:
|
109
108
|
DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
|
110
109
|
#
|
111
110
|
# SEQUENCES:
|
112
111
|
#
|
113
|
-
#
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
#
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
112
|
+
# Tolerance Table Sequence:
|
113
|
+
if p && p.fields.first && !p.fields.first.tolerance_table.empty?
|
114
|
+
tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm)
|
115
|
+
tt_item = DICOM::Item.new(:parent => tt_seq)
|
116
|
+
# Tolerance Table Number:
|
117
|
+
DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item)
|
118
|
+
end
|
119
|
+
# Structure set information:
|
120
|
+
if p && p.site_setup && !p.site_setup.structure_set_uid.empty?
|
121
|
+
#
|
122
|
+
# Referenced Structure Set Sequence:
|
123
|
+
#
|
124
|
+
ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
|
125
|
+
ss_item = DICOM::Item.new(:parent => ss_seq)
|
126
|
+
# Referenced SOP Class UID:
|
127
|
+
DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
|
128
|
+
DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item)
|
129
|
+
# RT Plan Geometry:
|
130
|
+
DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
|
131
|
+
else
|
132
|
+
# RT Plan Geometry:
|
133
|
+
DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm)
|
125
134
|
end
|
126
|
-
DICOM::Element.new('0008,1155', ref_ss_uid, :parent => ss_item)
|
127
135
|
#
|
128
136
|
# Patient Setup Sequence:
|
129
137
|
#
|
@@ -143,16 +151,7 @@ module RTP
|
|
143
151
|
#
|
144
152
|
# Dose Reference Sequence:
|
145
153
|
#
|
146
|
-
|
147
|
-
dr_item = DICOM::Item.new(:parent => dr_seq)
|
148
|
-
# Dose Reference Number:
|
149
|
-
DICOM::Element.new('300A,0012', '1', :parent => dr_item)
|
150
|
-
# Dose Reference Structure Type:
|
151
|
-
DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
|
152
|
-
# Dose Reference Description:
|
153
|
-
DICOM::Element.new('300A,0016', plan_name, :parent => dr_item)
|
154
|
-
# Dose Reference Type:
|
155
|
-
DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
|
154
|
+
create_dose_reference(dcm, plan_name) if options[:dose_ref]
|
156
155
|
#
|
157
156
|
# Fraction Group Sequence:
|
158
157
|
#
|
@@ -183,6 +182,8 @@ module RTP
|
|
183
182
|
unless field.modality == 'Unspecified'
|
184
183
|
# If this is an electron beam, a warning should be printed, as these are less reliably converted:
|
185
184
|
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'
|
185
|
+
# Reset control point 'current value' attributes:
|
186
|
+
reset_cp_current_attributes
|
186
187
|
# Beam number and name:
|
187
188
|
beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
|
188
189
|
beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
|
@@ -220,6 +221,7 @@ module RTP
|
|
220
221
|
beam_type = case field.treatment_type
|
221
222
|
when 'Static' then 'STATIC'
|
222
223
|
when 'StepNShoot' then 'STATIC'
|
224
|
+
when 'VMAT' then 'DYNAMIC'
|
223
225
|
else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
|
224
226
|
end
|
225
227
|
DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
|
@@ -241,36 +243,13 @@ module RTP
|
|
241
243
|
# Number of Blocks:
|
242
244
|
DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
|
243
245
|
# Final Cumulative Meterset Weight:
|
244
|
-
DICOM::Element.new('300A,010E',
|
246
|
+
DICOM::Element.new('300A,010E', 1, :parent => b_item)
|
245
247
|
# Referenced Patient Setup Number:
|
246
248
|
DICOM::Element.new('300C,006A', '1', :parent => b_item)
|
247
249
|
#
|
248
250
|
# Beam Limiting Device Sequence:
|
249
251
|
#
|
250
|
-
|
251
|
-
# Always create one ASYMY item:
|
252
|
-
bl_item_y = DICOM::Item.new(:parent => bl_seq)
|
253
|
-
# RT Beam Limiting Device Type:
|
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.)
|
267
|
-
if field.control_points.length > 0
|
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
|
252
|
+
create_beam_limiting_devices(b_item, field)
|
274
253
|
#
|
275
254
|
# Block Sequence (if any):
|
276
255
|
# FIXME: It seems that the Block Sequence (300A,00F4) may be
|
@@ -340,189 +319,25 @@ module RTP
|
|
340
319
|
DICOM::Element.new('300A,012C', '', :parent => cp_item)
|
341
320
|
end
|
342
321
|
# Source to Surface Distance:
|
343
|
-
|
322
|
+
add_ssd(field.ssd, cp_item)
|
344
323
|
# Cumulative Meterset Weight:
|
345
|
-
DICOM::Element.new('300A,0134',
|
324
|
+
DICOM::Element.new('300A,0134', '0', :parent => cp_item)
|
346
325
|
# Beam Limiting Device Position Sequence:
|
347
|
-
|
348
|
-
# Always create one ASYMY item:
|
349
|
-
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
350
|
-
# RT Beam Limiting Device Type:
|
351
|
-
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
352
|
-
# Leaf/Jaw Positions:
|
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
|
360
|
-
# MLCX:
|
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
|
326
|
+
create_beam_limiting_device_positions(cp_item, field.control_points.first)
|
371
327
|
# Referenced Dose Reference Sequence:
|
372
|
-
|
373
|
-
rd_item = DICOM::Item.new(:parent => rd_seq)
|
374
|
-
# Cumulative Dose Reference Coeffecient:
|
375
|
-
DICOM::Element.new('300A,010C', '', :parent => rd_item)
|
376
|
-
# Referenced Dose Reference Number:
|
377
|
-
DICOM::Element.new('300C,0051', '1', :parent => rd_item)
|
328
|
+
create_referenced_dose_reference(cp_item) if options[:dose_ref]
|
378
329
|
# Second CP:
|
379
330
|
cp_item = DICOM::Item.new(:parent => cp_seq)
|
380
331
|
# Control Point Index:
|
381
332
|
DICOM::Element.new('300A,0112', "1", :parent => cp_item)
|
382
333
|
# Cumulative Meterset Weight:
|
383
|
-
DICOM::Element.new('300A,0134',
|
334
|
+
DICOM::Element.new('300A,0134', '1', :parent => cp_item)
|
384
335
|
else
|
385
|
-
# When we have multiple (
|
386
|
-
field.control_points.
|
387
|
-
|
388
|
-
|
389
|
-
|
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:
|
436
|
-
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
437
|
-
# RT Beam Limiting Device Type:
|
438
|
-
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
439
|
-
# Leaf/Jaw Positions:
|
440
|
-
DICOM::Element.new('300A,011C', "#{field.collimator_y1.to_f * 10}\\#{field.collimator_y2.to_f * 10}", :parent => dp_item_y)
|
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:
|
448
|
-
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
449
|
-
# RT Beam Limiting Device Type:
|
450
|
-
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
451
|
-
# Leaf/Jaw Positions:
|
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
|
454
|
-
leaf_pos = "#{pos_a.join("\\")}\\#{pos_b.join("\\")}"
|
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
|
525
|
-
end
|
336
|
+
# When we have multiple (2 or more) control points, iterate each control point:
|
337
|
+
field.control_points.each { |cp| create_control_point(cp, cp_seq, options) }
|
338
|
+
# Make sure that hte cumulative meterset weight of the last control
|
339
|
+
# point is '1' (exactly equal to final cumulative meterset weight):
|
340
|
+
cp_seq.items.last['300A,0134'].value = '1'
|
526
341
|
end
|
527
342
|
# Number of Control Points:
|
528
343
|
DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
|
@@ -536,6 +351,345 @@ module RTP
|
|
536
351
|
return dcm
|
537
352
|
end
|
538
353
|
|
354
|
+
|
355
|
+
private
|
356
|
+
|
357
|
+
|
358
|
+
# Adds Collimator Angle elements to a Control Point Item.
|
359
|
+
# Note that the element is only added if there is no 'current' attribute
|
360
|
+
# defined, or the given value is different form the current attribute.
|
361
|
+
#
|
362
|
+
# @param [String, NilClass] value1 the collimator angle attribute
|
363
|
+
# @param [String, NilClass] value2 the collimator rotation direction attribute
|
364
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
365
|
+
#
|
366
|
+
def add_collimator(value1, value2, item)
|
367
|
+
if !@current_collimator || value1 != @current_collimator
|
368
|
+
@current_collimator = value1
|
369
|
+
DICOM::Element.new('300A,0120', value1, :parent => item)
|
370
|
+
DICOM::Element.new('300A,0121', (value2.empty? ? 'NONE' : value2), :parent => item)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Adds Table Top Eccentric Angle elements to a Control Point Item.
|
375
|
+
# Note that the element is only added if there is no 'current' attribute
|
376
|
+
# defined, or the given value is different form the current attribute.
|
377
|
+
#
|
378
|
+
# @param [String, NilClass] value1 the table top eccentric angle attribute
|
379
|
+
# @param [String, NilClass] value2 the table top eccentric rotation direction attribute
|
380
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
381
|
+
#
|
382
|
+
def add_couch_angle(value1, value2, item)
|
383
|
+
if !@current_couch_angle || value1 != @current_couch_angle
|
384
|
+
@current_couch_angle = value1
|
385
|
+
DICOM::Element.new('300A,0125', value1, :parent => item)
|
386
|
+
DICOM::Element.new('300A,0126', (value2.empty? ? 'NONE' : value2), :parent => item)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Adds a Table Top Lateral Position element to a Control Point Item.
|
391
|
+
# Note that the element is only added if there is no 'current' attribute
|
392
|
+
# defined, or the given value is different form the current attribute.
|
393
|
+
#
|
394
|
+
# @param [String, NilClass] value the couch lateral attribute
|
395
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
396
|
+
#
|
397
|
+
def add_couch_lateral(value, item)
|
398
|
+
if !@current_couch_lateral || value != @current_couch_lateral
|
399
|
+
@current_couch_lateral = value
|
400
|
+
DICOM::Element.new('300A,012A', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Adds a Table Top Longitudinal Position element to a Control Point Item.
|
405
|
+
# Note that the element is only added if there is no 'current' attribute
|
406
|
+
# defined, or the given value is different form the current attribute.
|
407
|
+
#
|
408
|
+
# @param [String, NilClass] value the couch longitudinal attribute
|
409
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
410
|
+
#
|
411
|
+
def add_couch_longitudinal(value, item)
|
412
|
+
if !@current_couch_longitudinal || value != @current_couch_longitudinal
|
413
|
+
@current_couch_longitudinal = value
|
414
|
+
DICOM::Element.new('300A,0129', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Adds Patient Support Angle elements to a Control Point Item.
|
419
|
+
# Note that the element is only added if there is no 'current' attribute
|
420
|
+
# defined, or the given value is different form the current attribute.
|
421
|
+
#
|
422
|
+
# @param [String, NilClass] value1 the patient support angle attribute
|
423
|
+
# @param [String, NilClass] value2 the patient support rotation direction attribute
|
424
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
425
|
+
#
|
426
|
+
def add_couch_pedestal(value1, value2, item)
|
427
|
+
if !@current_couch_pedestal || value1 != @current_couch_pedestal
|
428
|
+
@current_couch_pedestal = value1
|
429
|
+
DICOM::Element.new('300A,0122', value1, :parent => item)
|
430
|
+
DICOM::Element.new('300A,0123', (value2.empty? ? 'NONE' : value2), :parent => item)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Adds a Table Top Vertical Position element to a Control Point Item.
|
435
|
+
# Note that the element is only added if there is no 'current' attribute
|
436
|
+
# defined, or the given value is different form the current attribute.
|
437
|
+
#
|
438
|
+
# @param [String, NilClass] value the couch vertical attribute
|
439
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
440
|
+
#
|
441
|
+
def add_couch_vertical(value, item)
|
442
|
+
if !@current_couch_vertical || value != @current_couch_vertical
|
443
|
+
@current_couch_vertical = value
|
444
|
+
DICOM::Element.new('300A,0128', (value.empty? ? '' : value.to_f * 10), :parent => item)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
# Adds a Dose Rate Set element to a Control Point Item.
|
449
|
+
# Note that the element is only added if there is no 'current' attribute
|
450
|
+
# defined, or the given value is different form the current attribute.
|
451
|
+
#
|
452
|
+
# @param [String, NilClass] value the doserate attribute
|
453
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
454
|
+
#
|
455
|
+
def add_doserate(value, item)
|
456
|
+
if !@current_doserate || value != @current_doserate
|
457
|
+
@current_doserate = value
|
458
|
+
DICOM::Element.new('300A,0115', value, :parent => item)
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
# Adds a Nominal Beam Energy element to a Control Point Item.
|
463
|
+
# Note that the element is only added if there is no 'current' attribute
|
464
|
+
# defined, or the given value is different form the current attribute.
|
465
|
+
#
|
466
|
+
# @param [String, NilClass] value the energy attribute
|
467
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
468
|
+
#
|
469
|
+
def add_energy(value, item)
|
470
|
+
if !@current_energy || value != @current_energy
|
471
|
+
@current_energy = value
|
472
|
+
DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item)
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
# Adds Gantry Angle elements to a Control Point Item.
|
477
|
+
# Note that the element is only added if there is no 'current' attribute
|
478
|
+
# defined, or the given value is different form the current attribute.
|
479
|
+
#
|
480
|
+
# @param [String, NilClass] value1 the gantry angle attribute
|
481
|
+
# @param [String, NilClass] value2 the gantry rotation direction attribute
|
482
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
483
|
+
#
|
484
|
+
def add_gantry(value1, value2, item)
|
485
|
+
if !@current_gantry || value1 != @current_gantry
|
486
|
+
@current_gantry = value1
|
487
|
+
DICOM::Element.new('300A,011E', value1, :parent => item)
|
488
|
+
DICOM::Element.new('300A,011F', (value2.empty? ? 'NONE' : value2), :parent => item)
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# Adds an Isosenter element to a Control Point Item.
|
493
|
+
# Note that the element is only added if there is a Site Setup record present,
|
494
|
+
# and it contains a real (non-empty) value. Also, the element is only added if there
|
495
|
+
# is no 'current' attribute defined, or the given value is different form the current attribute.
|
496
|
+
#
|
497
|
+
# @param [SiteSetup, NilClass] site_setup the associated site setup record
|
498
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
499
|
+
#
|
500
|
+
def add_isosenter(site_setup, item)
|
501
|
+
if site_setup
|
502
|
+
# Create an element if the value is new or unique:
|
503
|
+
if !@current_isosenter
|
504
|
+
iso = "#{(site_setup.iso_pos_x.to_f * 10).round(2)}\\#{(site_setup.iso_pos_y.to_f * 10).round(2)}\\#{(site_setup.iso_pos_z.to_f * 10).round(2)}"
|
505
|
+
if iso != @current_isosenter
|
506
|
+
@current_isosenter = iso
|
507
|
+
DICOM::Element.new('300A,012C', iso, :parent => item)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
else
|
511
|
+
# Log a warning if this is the first control point:
|
512
|
+
unless @current_isosenter
|
513
|
+
logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Adds a Source to Surface Distance element to a Control Point Item.
|
519
|
+
# Note that the element is only added if the SSD attribute contains
|
520
|
+
# real (non-empty) value.
|
521
|
+
#
|
522
|
+
# @param [String, NilClass] value the SSD attribute
|
523
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
524
|
+
#
|
525
|
+
def add_ssd(value, item)
|
526
|
+
DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty?
|
527
|
+
end
|
528
|
+
|
529
|
+
# Creates a control point item in the given control point sequence, based
|
530
|
+
# on an RTP control point record.
|
531
|
+
#
|
532
|
+
# @param [ControlPoint] cp the RTP ControlPoint record to convert
|
533
|
+
# @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created
|
534
|
+
# @param [Hash] options the options to use for creating the control point
|
535
|
+
# @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item
|
536
|
+
# @return [DICOM::Item] the constructed control point DICOM item
|
537
|
+
#
|
538
|
+
def create_control_point(cp, sequence, options={})
|
539
|
+
cp_item = DICOM::Item.new(:parent => sequence)
|
540
|
+
# Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight).
|
541
|
+
# The other attributes are only written if they are different from the previous control point.
|
542
|
+
# Control Point Index:
|
543
|
+
DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
|
544
|
+
# Beam Limiting Device Position Sequence:
|
545
|
+
create_beam_limiting_device_positions(cp_item, cp)
|
546
|
+
# Source to Surface Distance:
|
547
|
+
add_ssd(cp.ssd, cp_item)
|
548
|
+
# Cumulative Meterset Weight:
|
549
|
+
DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item)
|
550
|
+
# Referenced Dose Reference Sequence:
|
551
|
+
create_referenced_dose_reference(cp_item) if options[:dose_ref]
|
552
|
+
# Attributes that are only added if they carry an updated value:
|
553
|
+
# Nominal Beam Energy:
|
554
|
+
add_energy(cp.energy, cp_item)
|
555
|
+
# Dose Rate Set:
|
556
|
+
add_doserate(cp.doserate, cp_item)
|
557
|
+
# Gantry Angle & Rotation Direction:
|
558
|
+
add_gantry(cp.gantry_angle, cp.gantry_dir, cp_item)
|
559
|
+
# Beam Limiting Device Angle & Rotation Direction:
|
560
|
+
add_collimator(cp.collimator_angle, cp.collimator_dir, cp_item)
|
561
|
+
# Patient Support Angle & Rotation Direction:
|
562
|
+
add_couch_pedestal(cp.couch_pedestal, cp.couch_ped_dir, cp_item)
|
563
|
+
# Table Top Eccentric Angle & Rotation Direction:
|
564
|
+
add_couch_angle(cp.couch_angle, cp.couch_dir, cp_item)
|
565
|
+
# Table Top Vertical Position:
|
566
|
+
add_couch_vertical(cp.couch_vertical, cp_item)
|
567
|
+
# Table Top Longitudinal Position:
|
568
|
+
add_couch_longitudinal(cp.couch_vertical, cp_item)
|
569
|
+
# Table Top Lateral Position:
|
570
|
+
add_couch_lateral(cp.couch_vertical, cp_item)
|
571
|
+
# Isocenter Position (x\y\z):
|
572
|
+
add_isosenter(cp.parent.parent.site_setup, cp_item)
|
573
|
+
cp_item
|
574
|
+
end
|
575
|
+
|
576
|
+
# Creates a beam limiting device sequence in the given DICOM object.
|
577
|
+
#
|
578
|
+
# @param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence
|
579
|
+
# @param [Field] field the RTP field to fetch device parameters from
|
580
|
+
# @return [DICOM::Sequence] the constructed beam limiting device sequence
|
581
|
+
#
|
582
|
+
def create_beam_limiting_devices(beam_item, field)
|
583
|
+
bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item)
|
584
|
+
# The ASYMX item ('backup jaws') doesn't exist on all models:
|
585
|
+
if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
|
586
|
+
bl_item_x = DICOM::Item.new(:parent => bl_seq)
|
587
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
|
588
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
|
589
|
+
end
|
590
|
+
# The ASYMY item is always created:
|
591
|
+
bl_item_y = DICOM::Item.new(:parent => bl_seq)
|
592
|
+
# RT Beam Limiting Device Type:
|
593
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
|
594
|
+
# Number of Leaf/Jaw Pairs:
|
595
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
|
596
|
+
# MLCX item is only created if leaves are defined:
|
597
|
+
# (NB: The RTP file doesn't specify leaf position boundaries, so we
|
598
|
+
# have to set these based on a set of known MLC types, their number
|
599
|
+
# of leaves, and their leaf boundary positions.)
|
600
|
+
if field.control_points.length > 0
|
601
|
+
bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
|
602
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
|
603
|
+
num_leaves = field.control_points.first.mlc_leaves.to_i
|
604
|
+
DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
|
605
|
+
DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
|
606
|
+
end
|
607
|
+
bl_seq
|
608
|
+
end
|
609
|
+
|
610
|
+
# Creates a beam limiting device positions sequence in the given DICOM object.
|
611
|
+
#
|
612
|
+
# @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
|
613
|
+
# @param [ControlPoint] cp the RTP control point to fetch device parameters from
|
614
|
+
# @return [DICOM::Sequence] the constructed beam limiting device positions sequence
|
615
|
+
#
|
616
|
+
def create_beam_limiting_device_positions(cp_item, cp)
|
617
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
618
|
+
# The ASYMX item ('backup jaws') doesn't exist on all models:
|
619
|
+
if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
|
620
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
621
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
622
|
+
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_x1}\\#{cp.dcm_collimator_x2}", :parent => dp_item_x)
|
623
|
+
end
|
624
|
+
# Always create one ASYMY item:
|
625
|
+
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
626
|
+
# RT Beam Limiting Device Type:
|
627
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
628
|
+
# Leaf/Jaw Positions:
|
629
|
+
DICOM::Element.new('300A,011C', "#{cp.dcm_collimator_y1}\\#{cp.dcm_collimator_y2}", :parent => dp_item_y)
|
630
|
+
# MLCX:
|
631
|
+
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
632
|
+
# RT Beam Limiting Device Type:
|
633
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
634
|
+
# Leaf/Jaw Positions:
|
635
|
+
DICOM::Element.new('300A,011C', cp.dcm_mlc_positions, :parent => dp_item_mlcx)
|
636
|
+
dp_seq
|
637
|
+
end
|
638
|
+
|
639
|
+
# Creates a dose reference sequence in the given DICOM object.
|
640
|
+
#
|
641
|
+
# @param [DICOM::DObject] dcm the DICOM object in which to insert the sequence
|
642
|
+
# @param [String] description the value to use for Dose Reference Description
|
643
|
+
# @return [DICOM::Sequence] the constructed dose reference sequence
|
644
|
+
#
|
645
|
+
def create_dose_reference(dcm, description)
|
646
|
+
dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
|
647
|
+
dr_item = DICOM::Item.new(:parent => dr_seq)
|
648
|
+
# Dose Reference Number:
|
649
|
+
DICOM::Element.new('300A,0012', '1', :parent => dr_item)
|
650
|
+
# Dose Reference Structure Type:
|
651
|
+
DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
|
652
|
+
# Dose Reference Description:
|
653
|
+
DICOM::Element.new('300A,0016', description, :parent => dr_item)
|
654
|
+
# Dose Reference Type:
|
655
|
+
DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
|
656
|
+
dr_seq
|
657
|
+
end
|
658
|
+
|
659
|
+
# Creates a referenced dose reference sequence in the given DICOM object.
|
660
|
+
#
|
661
|
+
# @param [DICOM::Item] cp_item the DICOM item in which to insert the sequence
|
662
|
+
# @return [DICOM::Sequence] the constructed referenced dose reference sequence
|
663
|
+
#
|
664
|
+
def create_referenced_dose_reference(cp_item)
|
665
|
+
# Referenced Dose Reference Sequence:
|
666
|
+
rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
|
667
|
+
rd_item = DICOM::Item.new(:parent => rd_seq)
|
668
|
+
# Cumulative Dose Reference Coeffecient:
|
669
|
+
DICOM::Element.new('300A,010C', '', :parent => rd_item)
|
670
|
+
# Referenced Dose Reference Number:
|
671
|
+
DICOM::Element.new('300C,0051', '1', :parent => rd_item)
|
672
|
+
rd_seq
|
673
|
+
end
|
674
|
+
|
675
|
+
# Resets the types of control point attributes that are only written to the
|
676
|
+
# first control point item, and for following control point items only when
|
677
|
+
# they are different from the 'current' value. When a new field is reached,
|
678
|
+
# it is essential to reset these attributes, or else we could risk to start
|
679
|
+
# the field with a control point with missing attributes, if one of its first
|
680
|
+
# attributes is equal to the last attribute of the previous field.
|
681
|
+
#
|
682
|
+
def reset_cp_current_attributes
|
683
|
+
@current_gantry = nil
|
684
|
+
@current_collimator = nil
|
685
|
+
@current_couch_pedestal = nil
|
686
|
+
@current_couch_angle = nil
|
687
|
+
@current_couch_vertical = nil
|
688
|
+
@current_couch_longitudinal = nil
|
689
|
+
@current_couch_lateral = nil
|
690
|
+
@current_isosenter = nil
|
691
|
+
end
|
692
|
+
|
539
693
|
end
|
540
694
|
|
541
695
|
end
|
data/lib/rtp-connect/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rtp-connect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.6'
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -159,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
159
|
version: '0'
|
160
160
|
segments:
|
161
161
|
- 0
|
162
|
-
hash:
|
162
|
+
hash: -111446803
|
163
163
|
requirements: []
|
164
164
|
rubyforge_project: rtp-connect
|
165
165
|
rubygems_version: 1.8.24
|