rtp-connect 1.5 → 1.6
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 +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
|