rtp-connect 1.6 → 1.11
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.
- checksums.yaml +7 -0
- data/{CHANGELOG.rdoc → CHANGELOG.md} +137 -90
- data/COPYING +674 -674
- data/Gemfile +2 -2
- data/Gemfile.lock +31 -21
- data/README.md +161 -0
- data/lib/rtp-connect.rb +1 -0
- data/lib/rtp-connect/constants.rb +58 -57
- data/lib/rtp-connect/control_point.rb +158 -118
- data/lib/rtp-connect/dose_tracking.rb +37 -54
- data/lib/rtp-connect/extended_field.rb +36 -69
- data/lib/rtp-connect/extended_plan.rb +127 -0
- data/lib/rtp-connect/field.rb +158 -143
- data/lib/rtp-connect/methods.rb +85 -62
- data/lib/rtp-connect/plan.rb +645 -636
- data/lib/rtp-connect/plan_to_dcm.rb +668 -694
- data/lib/rtp-connect/prescription.rb +57 -74
- data/lib/rtp-connect/record.rb +225 -57
- data/lib/rtp-connect/ruby_extensions.rb +34 -3
- data/lib/rtp-connect/simulation_field.rb +606 -701
- data/lib/rtp-connect/site_setup.rb +112 -80
- data/lib/rtp-connect/version.rb +5 -5
- data/rakefile.rb +0 -1
- data/rtp-connect.gemspec +27 -27
- metadata +67 -58
- data/README.rdoc +0 -136
@@ -1,695 +1,669 @@
|
|
1
|
-
module RTP
|
2
|
-
|
3
|
-
class Plan < Record
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
# @
|
17
|
-
#
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
#
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
DICOM
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
DICOM::
|
40
|
-
#
|
41
|
-
|
42
|
-
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
DICOM::Element.new('0008,
|
49
|
-
#
|
50
|
-
DICOM::Element.new('0008,
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
|
71
|
-
#
|
72
|
-
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
DICOM::Element.new('0020,
|
90
|
-
#
|
91
|
-
DICOM::Element.new('0020,
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
DICOM::Element.new('
|
101
|
-
# RT Plan
|
102
|
-
|
103
|
-
DICOM::Element.new('300A,
|
104
|
-
# RT Plan
|
105
|
-
|
106
|
-
DICOM::Element.new('300A,
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
#
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
#
|
133
|
-
DICOM::
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
DICOM::
|
149
|
-
#
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
#
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
#
|
161
|
-
|
162
|
-
#
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
DICOM::
|
169
|
-
#
|
170
|
-
DICOM::Element.new('300A,
|
171
|
-
#
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
#
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
# Beam Item:
|
200
|
-
|
201
|
-
#
|
202
|
-
|
203
|
-
DICOM::Element.new('
|
204
|
-
#
|
205
|
-
DICOM::Element.new('
|
206
|
-
#
|
207
|
-
DICOM::Element.new('
|
208
|
-
#
|
209
|
-
DICOM::
|
210
|
-
#
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
when '
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
DICOM::Element.new('300A,
|
237
|
-
#
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
#
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
#
|
255
|
-
|
256
|
-
#
|
257
|
-
|
258
|
-
#
|
259
|
-
#
|
260
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
#
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
#
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
DICOM::Element.new('300A,
|
308
|
-
#
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
DICOM::Element.new('300A,
|
314
|
-
#
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
#
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
#
|
375
|
-
#
|
376
|
-
#
|
377
|
-
#
|
378
|
-
# @param [String
|
379
|
-
# @param [String, NilClass]
|
380
|
-
# @param [
|
381
|
-
#
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
DICOM::Element.new(
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
#
|
392
|
-
#
|
393
|
-
#
|
394
|
-
#
|
395
|
-
# @param [DICOM::Item] item the DICOM control point item in which to create
|
396
|
-
#
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
#
|
408
|
-
#
|
409
|
-
#
|
410
|
-
#
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
#
|
422
|
-
#
|
423
|
-
#
|
424
|
-
#
|
425
|
-
#
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
#
|
436
|
-
#
|
437
|
-
#
|
438
|
-
#
|
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
|
-
# @param [
|
467
|
-
#
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
#
|
477
|
-
#
|
478
|
-
#
|
479
|
-
#
|
480
|
-
#
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
#
|
520
|
-
#
|
521
|
-
#
|
522
|
-
# @param [
|
523
|
-
# @
|
524
|
-
#
|
525
|
-
def
|
526
|
-
DICOM::
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
#
|
541
|
-
#
|
542
|
-
#
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
#
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
#
|
566
|
-
|
567
|
-
#
|
568
|
-
|
569
|
-
#
|
570
|
-
|
571
|
-
#
|
572
|
-
|
573
|
-
|
574
|
-
end
|
575
|
-
|
576
|
-
# Creates
|
577
|
-
#
|
578
|
-
# @param [
|
579
|
-
# @param [
|
580
|
-
# @
|
581
|
-
#
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
#
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
#
|
614
|
-
#
|
615
|
-
#
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
#
|
625
|
-
|
626
|
-
#
|
627
|
-
DICOM::Element.new('300A,
|
628
|
-
#
|
629
|
-
DICOM::Element.new('300A,
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
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
|
-
|
693
|
-
end
|
694
|
-
|
1
|
+
module RTP
|
2
|
+
|
3
|
+
class Plan < Record
|
4
|
+
|
5
|
+
attr_accessor :current_gantry
|
6
|
+
attr_accessor :current_collimator
|
7
|
+
attr_accessor :current_couch_angle
|
8
|
+
attr_accessor :current_couch_pedestal
|
9
|
+
attr_accessor :current_couch_lateral
|
10
|
+
attr_accessor :current_couch_longitudinal
|
11
|
+
attr_accessor :current_couch_vertical
|
12
|
+
|
13
|
+
# Converts the Plan (and child) records to a
|
14
|
+
# DICOM::DObject of modality RTPLAN.
|
15
|
+
#
|
16
|
+
# @note Only photon plans have been tested.
|
17
|
+
# Electron beams beams may give an invalid DICOM file.
|
18
|
+
# Also note that, due to limitations in the RTP file format, some original
|
19
|
+
# values can not be recreated, like e.g. Study UID or Series UID.
|
20
|
+
# @param [Hash] options the options to use for creating the DICOM object
|
21
|
+
# @option options [Boolean] :dose_ref if set, Dose Reference & Referenced Dose Reference sequences will be included in the generated DICOM file
|
22
|
+
# @option options [String] :manufacturer the value used for the manufacturer tag (0008,0070) in the beam sequence
|
23
|
+
# @option options [String] :model the value used for the manufacturer's model name tag (0008,1090) in the beam sequence
|
24
|
+
# @option options [Symbol] :scale if set, relevant device parameters are converted from native readout format to IEC1217 (supported values are :elekta & :varian)
|
25
|
+
# @option options [String] :serial_number the value used for the device serial number tag (0018,1000) in the beam sequence
|
26
|
+
# @return [DICOM::DObject] the converted DICOM object
|
27
|
+
#
|
28
|
+
def to_dcm(options={})
|
29
|
+
#
|
30
|
+
# FIXME: This method is rather big, with a few sections of somewhat similar, repeating code.
|
31
|
+
# Refactoring and simplifying it at some stage might be a good idea.
|
32
|
+
#
|
33
|
+
require 'dicom'
|
34
|
+
original_level = DICOM.logger.level
|
35
|
+
DICOM.logger.level = Logger::FATAL
|
36
|
+
p = @prescriptions.first
|
37
|
+
# If no prescription is present, we are not going to be able to make a valid DICOM object:
|
38
|
+
logger.error("No Prescription Record present. Unable to build a valid RTPLAN DICOM object.") unless p
|
39
|
+
dcm = DICOM::DObject.new
|
40
|
+
#
|
41
|
+
# TOP LEVEL TAGS:
|
42
|
+
#
|
43
|
+
# Specific Character Set:
|
44
|
+
DICOM::Element.new('0008,0005', 'ISO_IR 100', :parent => dcm)
|
45
|
+
# Instance Creation Date
|
46
|
+
DICOM::Element.new('0008,0012', Time.now.strftime("%Y%m%d"), :parent => dcm)
|
47
|
+
# Instance Creation Time:
|
48
|
+
DICOM::Element.new('0008,0013', Time.now.strftime("%H%M%S"), :parent => dcm)
|
49
|
+
# SOP Class UID:
|
50
|
+
DICOM::Element.new('0008,0016', '1.2.840.10008.5.1.4.1.1.481.5', :parent => dcm)
|
51
|
+
# SOP Instance UID (if an original UID is not present, we make up a UID):
|
52
|
+
begin
|
53
|
+
sop_uid = p.fields.first.extended_field.original_plan_uid.empty? ? DICOM.generate_uid : p.fields.first.extended_field.original_plan_uid
|
54
|
+
rescue
|
55
|
+
sop_uid = DICOM.generate_uid
|
56
|
+
end
|
57
|
+
DICOM::Element.new('0008,0018', sop_uid, :parent => dcm)
|
58
|
+
# Study Date
|
59
|
+
DICOM::Element.new('0008,0020', Time.now.strftime("%Y%m%d"), :parent => dcm)
|
60
|
+
# Study Time:
|
61
|
+
DICOM::Element.new('0008,0030', Time.now.strftime("%H%M%S"), :parent => dcm)
|
62
|
+
# Accession Number:
|
63
|
+
DICOM::Element.new('0008,0050', '', :parent => dcm)
|
64
|
+
# Modality:
|
65
|
+
DICOM::Element.new('0008,0060', 'RTPLAN', :parent => dcm)
|
66
|
+
# Manufacturer:
|
67
|
+
DICOM::Element.new('0008,0070', 'rtp-connect', :parent => dcm)
|
68
|
+
# Referring Physician's Name:
|
69
|
+
DICOM::Element.new('0008,0090', "#{@md_last_name}^#{@md_first_name}^#{@md_middle_name}^^", :parent => dcm)
|
70
|
+
# Operator's Name:
|
71
|
+
DICOM::Element.new('0008,1070', "#{@author_last_name}^#{@author_first_name}^#{@author_middle_name}^^", :parent => dcm)
|
72
|
+
# Patient's Name:
|
73
|
+
DICOM::Element.new('0010,0010', "#{@patient_last_name}^#{@patient_first_name}^#{@patient_middle_name}^^", :parent => dcm)
|
74
|
+
# Patient ID:
|
75
|
+
DICOM::Element.new('0010,0020', @patient_id, :parent => dcm)
|
76
|
+
# Patient's Birth Date:
|
77
|
+
DICOM::Element.new('0010,0030', '', :parent => dcm)
|
78
|
+
# Patient's Sex:
|
79
|
+
DICOM::Element.new('0010,0040', '', :parent => dcm)
|
80
|
+
# Manufacturer's Model Name:
|
81
|
+
DICOM::Element.new('0008,1090', 'RTP-to-DICOM', :parent => dcm)
|
82
|
+
# Software Version(s):
|
83
|
+
DICOM::Element.new('0018,1020', "RubyRTP#{VERSION}", :parent => dcm)
|
84
|
+
# Study Instance UID:
|
85
|
+
DICOM::Element.new('0020,000D', DICOM.generate_uid, :parent => dcm)
|
86
|
+
# Series Instance UID:
|
87
|
+
DICOM::Element.new('0020,000E', DICOM.generate_uid, :parent => dcm)
|
88
|
+
# Study ID:
|
89
|
+
DICOM::Element.new('0020,0010', '1', :parent => dcm)
|
90
|
+
# Series Number:
|
91
|
+
DICOM::Element.new('0020,0011', '1', :parent => dcm)
|
92
|
+
# Frame of Reference UID (if an original UID is not present, we make up a UID):
|
93
|
+
begin
|
94
|
+
for_uid = p.site_setup.frame_of_ref_uid.empty? ? DICOM.generate_uid : p.site_setup.frame_of_ref_uid
|
95
|
+
rescue
|
96
|
+
for_uid = DICOM.generate_uid
|
97
|
+
end
|
98
|
+
DICOM::Element.new('0020,0052', for_uid, :parent => dcm)
|
99
|
+
# Position Reference Indicator:
|
100
|
+
DICOM::Element.new('0020,1040', '', :parent => dcm)
|
101
|
+
# RT Plan Label (max 16 characters):
|
102
|
+
plan_label = p ? p.rx_site_name[0..15] : @course_id
|
103
|
+
DICOM::Element.new('300A,0002', plan_label, :parent => dcm)
|
104
|
+
# RT Plan Name:
|
105
|
+
plan_name = p ? p.rx_site_name : @course_id
|
106
|
+
DICOM::Element.new('300A,0003', plan_name, :parent => dcm)
|
107
|
+
# RT Plan Description:
|
108
|
+
plan_desc = p ? p.technique : @diagnosis
|
109
|
+
DICOM::Element.new('300A,0004', plan_desc, :parent => dcm)
|
110
|
+
# RT Plan Date:
|
111
|
+
plan_date = @plan_date.empty? ? Time.now.strftime("%Y%m%d") : @plan_date
|
112
|
+
DICOM::Element.new('300A,0006', plan_date, :parent => dcm)
|
113
|
+
# RT Plan Time:
|
114
|
+
plan_time = @plan_time.empty? ? Time.now.strftime("%H%M%S") : @plan_time
|
115
|
+
DICOM::Element.new('300A,0007', plan_time, :parent => dcm)
|
116
|
+
# Approval Status:
|
117
|
+
DICOM::Element.new('300E,0002', 'UNAPPROVED', :parent => dcm)
|
118
|
+
#
|
119
|
+
# SEQUENCES:
|
120
|
+
#
|
121
|
+
# Tolerance Table Sequence:
|
122
|
+
if p && p.fields.first && !p.fields.first.tolerance_table.empty?
|
123
|
+
tt_seq = DICOM::Sequence.new('300A,0040', :parent => dcm)
|
124
|
+
tt_item = DICOM::Item.new(:parent => tt_seq)
|
125
|
+
# Tolerance Table Number:
|
126
|
+
DICOM::Element.new('300A,0042', p.fields.first.tolerance_table, :parent => tt_item)
|
127
|
+
end
|
128
|
+
# Structure set information:
|
129
|
+
if p && p.site_setup && !p.site_setup.structure_set_uid.empty?
|
130
|
+
#
|
131
|
+
# Referenced Structure Set Sequence:
|
132
|
+
#
|
133
|
+
ss_seq = DICOM::Sequence.new('300C,0060', :parent => dcm)
|
134
|
+
ss_item = DICOM::Item.new(:parent => ss_seq)
|
135
|
+
# Referenced SOP Class UID:
|
136
|
+
DICOM::Element.new('0008,1150', '1.2.840.10008.5.1.4.1.1.481.3', :parent => ss_item)
|
137
|
+
DICOM::Element.new('0008,1155', p.site_setup.structure_set_uid, :parent => ss_item)
|
138
|
+
# RT Plan Geometry:
|
139
|
+
DICOM::Element.new('300A,000C', 'PATIENT', :parent => dcm)
|
140
|
+
else
|
141
|
+
# RT Plan Geometry:
|
142
|
+
DICOM::Element.new('300A,000C', 'TREATMENT_DEVICE', :parent => dcm)
|
143
|
+
end
|
144
|
+
#
|
145
|
+
# Patient Setup Sequence:
|
146
|
+
#
|
147
|
+
ps_seq = DICOM::Sequence.new('300A,0180', :parent => dcm)
|
148
|
+
ps_item = DICOM::Item.new(:parent => ps_seq)
|
149
|
+
# Patient Position:
|
150
|
+
begin
|
151
|
+
pat_pos = p.site_setup.patient_orientation.empty? ? 'HFS' : p.site_setup.patient_orientation
|
152
|
+
rescue
|
153
|
+
pat_pos = 'HFS'
|
154
|
+
end
|
155
|
+
DICOM::Element.new('0018,5100', pat_pos, :parent => ps_item)
|
156
|
+
# Patient Setup Number:
|
157
|
+
DICOM::Element.new('300A,0182', '1', :parent => ps_item)
|
158
|
+
# Setup Technique (assume Isocentric):
|
159
|
+
DICOM::Element.new('300A,01B0', 'ISOCENTRIC', :parent => ps_item)
|
160
|
+
#
|
161
|
+
# Dose Reference Sequence:
|
162
|
+
#
|
163
|
+
create_dose_reference(dcm, plan_name) if options[:dose_ref]
|
164
|
+
#
|
165
|
+
# Fraction Group Sequence:
|
166
|
+
#
|
167
|
+
fg_seq = DICOM::Sequence.new('300A,0070', :parent => dcm)
|
168
|
+
fg_item = DICOM::Item.new(:parent => fg_seq)
|
169
|
+
# Fraction Group Number:
|
170
|
+
DICOM::Element.new('300A,0071', '1', :parent => fg_item)
|
171
|
+
# Number of Fractions Planned (try to derive from total dose/fraction dose, or use 1 as default):
|
172
|
+
begin
|
173
|
+
num_frac = p.dose_ttl.empty? || p.dose_tx.empty? ? '1' : (p.dose_ttl.to_i / p.dose_tx.to_f).round.to_s
|
174
|
+
rescue
|
175
|
+
num_frac = '0'
|
176
|
+
end
|
177
|
+
DICOM::Element.new('300A,0078', num_frac, :parent => fg_item)
|
178
|
+
# Number of Brachy Application Setups:
|
179
|
+
DICOM::Element.new('300A,00A0', '0', :parent => fg_item)
|
180
|
+
# Referenced Beam Sequence (items created for each beam below):
|
181
|
+
rb_seq = DICOM::Sequence.new('300C,0004', :parent => fg_item)
|
182
|
+
#
|
183
|
+
# Beam Sequence:
|
184
|
+
#
|
185
|
+
b_seq = DICOM::Sequence.new('300A,00B0', :parent => dcm)
|
186
|
+
if p
|
187
|
+
# If no fields are present, we are not going to be able to make a valid DICOM object:
|
188
|
+
logger.error("No Field Record present. Unable to build a valid RTPLAN DICOM object.") unless p.fields.length > 0
|
189
|
+
p.fields.each_with_index do |field, i|
|
190
|
+
# Fields with modality 'Unspecified' (e.g. CT or 2dkV) must be skipped:
|
191
|
+
unless field.modality == 'Unspecified'
|
192
|
+
# If this is an electron beam, a warning should be printed, as these are less reliably converted:
|
193
|
+
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'
|
194
|
+
# Reset control point 'current value' attributes:
|
195
|
+
reset_cp_current_attributes
|
196
|
+
# Beam number and name:
|
197
|
+
beam_number = field.extended_field ? field.extended_field.original_beam_number : (i + 1).to_s
|
198
|
+
beam_name = field.extended_field ? field.extended_field.original_beam_name : field.field_name
|
199
|
+
# Ref Beam Item:
|
200
|
+
rb_item = DICOM::Item.new(:parent => rb_seq)
|
201
|
+
# Beam Dose (convert from cGy to Gy):
|
202
|
+
field_dose = field.field_dose.empty? ? '' : (field.field_dose.to_f * 0.01).round(4).to_s
|
203
|
+
DICOM::Element.new('300A,0084', field_dose, :parent => rb_item)
|
204
|
+
# Beam Meterset:
|
205
|
+
DICOM::Element.new('300A,0086', field.field_monitor_units, :parent => rb_item)
|
206
|
+
# Referenced Beam Number:
|
207
|
+
DICOM::Element.new('300C,0006', beam_number, :parent => rb_item)
|
208
|
+
# Beam Item:
|
209
|
+
b_item = DICOM::Item.new(:parent => b_seq)
|
210
|
+
# Optional method values:
|
211
|
+
# Manufacturer:
|
212
|
+
DICOM::Element.new('0008,0070', options[:manufacturer], :parent => b_item) if options[:manufacturer]
|
213
|
+
# Manufacturer's Model Name:
|
214
|
+
DICOM::Element.new('0008,1090', options[:model], :parent => b_item) if options[:model]
|
215
|
+
# Device Serial Number:
|
216
|
+
DICOM::Element.new('0018,1000', options[:serial_number], :parent => b_item) if options[:serial_number]
|
217
|
+
# Treatment Machine Name (max 16 characters):
|
218
|
+
DICOM::Element.new('300A,00B2', field.treatment_machine[0..15], :parent => b_item)
|
219
|
+
# Primary Dosimeter Unit:
|
220
|
+
DICOM::Element.new('300A,00B3', 'MU', :parent => b_item)
|
221
|
+
# Source-Axis Distance (convert to mm):
|
222
|
+
DICOM::Element.new('300A,00B4', "#{field.sad.to_f * 10}", :parent => b_item)
|
223
|
+
# Beam Number:
|
224
|
+
DICOM::Element.new('300A,00C0', beam_number, :parent => b_item)
|
225
|
+
# Beam Name:
|
226
|
+
DICOM::Element.new('300A,00C2', beam_name, :parent => b_item)
|
227
|
+
# Beam Description:
|
228
|
+
DICOM::Element.new('300A,00C3', field.field_note, :parent => b_item)
|
229
|
+
# Beam Type:
|
230
|
+
beam_type = case field.treatment_type
|
231
|
+
when 'Static' then 'STATIC'
|
232
|
+
when 'StepNShoot' then 'STATIC'
|
233
|
+
when 'VMAT' then 'DYNAMIC'
|
234
|
+
else logger.error("The beam type (treatment type) #{field.treatment_type} is not yet supported.")
|
235
|
+
end
|
236
|
+
DICOM::Element.new('300A,00C4', beam_type, :parent => b_item)
|
237
|
+
# Radiation Type:
|
238
|
+
rad_type = case field.modality
|
239
|
+
when 'Elect' then 'ELECTRON'
|
240
|
+
when 'Xrays' then 'PHOTON'
|
241
|
+
else logger.error("The radiation type (modality) #{field.modality} is not yet supported.")
|
242
|
+
end
|
243
|
+
DICOM::Element.new('300A,00C6', rad_type, :parent => b_item)
|
244
|
+
# Treatment Delivery Type:
|
245
|
+
DICOM::Element.new('300A,00CE', 'TREATMENT', :parent => b_item)
|
246
|
+
# Number of Wedges:
|
247
|
+
DICOM::Element.new('300A,00D0', (field.wedge.empty? ? '0' : '1'), :parent => b_item)
|
248
|
+
# Number of Compensators:
|
249
|
+
DICOM::Element.new('300A,00E0', (field.compensator.empty? ? '0' : '1'), :parent => b_item)
|
250
|
+
# Number of Boli:
|
251
|
+
DICOM::Element.new('300A,00ED', (field.bolus.empty? ? '0' : '1'), :parent => b_item)
|
252
|
+
# Number of Blocks:
|
253
|
+
DICOM::Element.new('300A,00F0', (field.block.empty? ? '0' : '1'), :parent => b_item)
|
254
|
+
# Final Cumulative Meterset Weight:
|
255
|
+
DICOM::Element.new('300A,010E', 1, :parent => b_item)
|
256
|
+
# Referenced Patient Setup Number:
|
257
|
+
DICOM::Element.new('300C,006A', '1', :parent => b_item)
|
258
|
+
#
|
259
|
+
# Beam Limiting Device Sequence:
|
260
|
+
#
|
261
|
+
create_beam_limiting_devices(b_item, field)
|
262
|
+
#
|
263
|
+
# Block Sequence (if any):
|
264
|
+
# FIXME: It seems that the Block Sequence (300A,00F4) may be
|
265
|
+
# difficult (impossible?) to reconstruct based on the RTP file's
|
266
|
+
# information, and thus it is skipped altogether.
|
267
|
+
#
|
268
|
+
#
|
269
|
+
# Applicator Sequence (if any):
|
270
|
+
#
|
271
|
+
unless field.e_applicator.empty?
|
272
|
+
app_seq = DICOM::Sequence.new('300A,0107', :parent => b_item)
|
273
|
+
app_item = DICOM::Item.new(:parent => app_seq)
|
274
|
+
# Applicator ID:
|
275
|
+
DICOM::Element.new('300A,0108', field.e_field_def_aperture, :parent => app_item)
|
276
|
+
# Applicator Type:
|
277
|
+
DICOM::Element.new('300A,0109', "ELECTRON_#{field.e_applicator.upcase}", :parent => app_item)
|
278
|
+
# Applicator Description:
|
279
|
+
DICOM::Element.new('300A,010A', "Appl. #{field.e_field_def_aperture}", :parent => app_item)
|
280
|
+
end
|
281
|
+
#
|
282
|
+
# Control Point Sequence:
|
283
|
+
#
|
284
|
+
# A field may have 0 (no MLC), 1 (conventional beam with MLC) or 2n (IMRT) control points.
|
285
|
+
# The DICOM file shall always contain 2n control points (minimum 2).
|
286
|
+
#
|
287
|
+
cp_seq = DICOM::Sequence.new('300A,0111', :parent => b_item)
|
288
|
+
if field.control_points.length < 2
|
289
|
+
# When we have 0 or 1 control point, use settings from field, and insert MLC settings if present:
|
290
|
+
# First CP:
|
291
|
+
cp_item = DICOM::Item.new(:parent => cp_seq)
|
292
|
+
# Control Point Index:
|
293
|
+
DICOM::Element.new('300A,0112', "0", :parent => cp_item)
|
294
|
+
# Nominal Beam Energy:
|
295
|
+
DICOM::Element.new('300A,0114', "#{field.energy.to_f}", :parent => cp_item)
|
296
|
+
# Dose Rate Set:
|
297
|
+
DICOM::Element.new('300A,0115', field.doserate, :parent => cp_item)
|
298
|
+
# Gantry Angle:
|
299
|
+
DICOM::Element.new('300A,011E', field.gantry_angle, :parent => cp_item)
|
300
|
+
# Gantry Rotation Direction:
|
301
|
+
DICOM::Element.new('300A,011F', (field.arc_direction.empty? ? 'NONE' : field.arc_direction), :parent => cp_item)
|
302
|
+
# Beam Limiting Device Angle:
|
303
|
+
DICOM::Element.new('300A,0120', field.collimator_angle, :parent => cp_item)
|
304
|
+
# Beam Limiting Device Rotation Direction:
|
305
|
+
DICOM::Element.new('300A,0121', 'NONE', :parent => cp_item)
|
306
|
+
# Patient Support Angle:
|
307
|
+
DICOM::Element.new('300A,0122', field.couch_pedestal, :parent => cp_item)
|
308
|
+
# Patient Support Rotation Direction:
|
309
|
+
DICOM::Element.new('300A,0123', 'NONE', :parent => cp_item)
|
310
|
+
# Table Top Eccentric Angle:
|
311
|
+
DICOM::Element.new('300A,0125', field.couch_angle, :parent => cp_item)
|
312
|
+
# Table Top Eccentric Rotation Direction:
|
313
|
+
DICOM::Element.new('300A,0126', 'NONE', :parent => cp_item)
|
314
|
+
# Table Top Vertical Position:
|
315
|
+
couch_vert = field.couch_vertical.empty? ? '' : (field.couch_vertical.to_f * 10).to_s
|
316
|
+
DICOM::Element.new('300A,0128', couch_vert, :parent => cp_item)
|
317
|
+
# Table Top Longitudinal Position:
|
318
|
+
couch_long = field.couch_longitudinal.empty? ? '' : (field.couch_longitudinal.to_f * 10).to_s
|
319
|
+
DICOM::Element.new('300A,0129', couch_long, :parent => cp_item)
|
320
|
+
# Table Top Lateral Position:
|
321
|
+
couch_lat = field.couch_lateral.empty? ? '' : (field.couch_lateral.to_f * 10).to_s
|
322
|
+
DICOM::Element.new('300A,012A', couch_lat, :parent => cp_item)
|
323
|
+
# Isocenter Position (x\y\z):
|
324
|
+
if p.site_setup
|
325
|
+
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)
|
326
|
+
else
|
327
|
+
logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
|
328
|
+
DICOM::Element.new('300A,012C', '', :parent => cp_item)
|
329
|
+
end
|
330
|
+
# Source to Surface Distance:
|
331
|
+
add_ssd(field.ssd, cp_item)
|
332
|
+
# Cumulative Meterset Weight:
|
333
|
+
DICOM::Element.new('300A,0134', '0', :parent => cp_item)
|
334
|
+
# Beam Limiting Device Position Sequence:
|
335
|
+
if field.control_points.length > 0
|
336
|
+
create_beam_limiting_device_positions(cp_item, field.control_points.first, options)
|
337
|
+
else
|
338
|
+
create_beam_limiting_device_positions_from_field(cp_item, field, options)
|
339
|
+
end
|
340
|
+
# Referenced Dose Reference Sequence:
|
341
|
+
create_referenced_dose_reference(cp_item) if options[:dose_ref]
|
342
|
+
# Second CP:
|
343
|
+
cp_item = DICOM::Item.new(:parent => cp_seq)
|
344
|
+
# Control Point Index:
|
345
|
+
DICOM::Element.new('300A,0112', "1", :parent => cp_item)
|
346
|
+
# Cumulative Meterset Weight:
|
347
|
+
DICOM::Element.new('300A,0134', '1', :parent => cp_item)
|
348
|
+
else
|
349
|
+
# When we have multiple (2 or more) control points, iterate each control point:
|
350
|
+
field.control_points.each { |cp| create_control_point(cp, cp_seq, options) }
|
351
|
+
# Make sure that hte cumulative meterset weight of the last control
|
352
|
+
# point is '1' (exactly equal to final cumulative meterset weight):
|
353
|
+
cp_seq.items.last['300A,0134'].value = '1'
|
354
|
+
end
|
355
|
+
# Number of Control Points:
|
356
|
+
DICOM::Element.new('300A,0110', b_item['300A,0111'].items.length, :parent => b_item)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
# Number of Beams:
|
360
|
+
DICOM::Element.new('300A,0080', fg_item['300C,0004'].items.length, :parent => fg_item)
|
361
|
+
end
|
362
|
+
# Restore the DICOM logger:
|
363
|
+
DICOM.logger.level = original_level
|
364
|
+
return dcm
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
private
|
369
|
+
|
370
|
+
|
371
|
+
# Adds an angular type value to a Control Point Item, by creating the
|
372
|
+
# necessary DICOM elements.
|
373
|
+
# Note that the element is only added if there is no 'current' attribute
|
374
|
+
# defined, or the given value is different form the current attribute.
|
375
|
+
#
|
376
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create the elements
|
377
|
+
# @param [String] angle_tag the DICOM tag of the angle element
|
378
|
+
# @param [String] direction_tag the DICOM tag of the direction element
|
379
|
+
# @param [String, NilClass] angle the collimator angle attribute
|
380
|
+
# @param [String, NilClass] direction the collimator rotation direction attribute
|
381
|
+
# @param [Symbol] current_angle the instance variable that keeps track of the current value of this attribute
|
382
|
+
#
|
383
|
+
def add_angle(item, angle_tag, direction_tag, angle, direction, current_angle)
|
384
|
+
if !self.send(current_angle) || angle != self.send(current_angle)
|
385
|
+
self.send("#{current_angle}=", angle)
|
386
|
+
DICOM::Element.new(angle_tag, angle, :parent => item)
|
387
|
+
DICOM::Element.new(direction_tag, (direction.empty? ? 'NONE' : direction), :parent => item)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Adds a Table Top Position element to a Control Point Item.
|
392
|
+
# Note that the element is only added if there is no 'current' attribute
|
393
|
+
# defined, or the given value is different form the current attribute.
|
394
|
+
#
|
395
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create the element
|
396
|
+
# @param [String] tag the DICOM tag of the couch position element
|
397
|
+
# @param [String, NilClass] value the couch position
|
398
|
+
# @param [Symbol] current the instance variable that keeps track of the current value of this attribute
|
399
|
+
#
|
400
|
+
def add_couch_position(item, tag, value, current)
|
401
|
+
if !self.send(current) || value != self.send(current)
|
402
|
+
self.send("#{current}=", value)
|
403
|
+
DICOM::Element.new(tag, (value.empty? ? '' : value.to_f * 10), :parent => item)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Adds a Dose Rate Set element to a Control Point Item.
|
408
|
+
# Note that the element is only added if there is no 'current' attribute
|
409
|
+
# defined, or the given value is different form the current attribute.
|
410
|
+
#
|
411
|
+
# @param [String, NilClass] value the doserate attribute
|
412
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
413
|
+
#
|
414
|
+
def add_doserate(value, item)
|
415
|
+
if !@current_doserate || value != @current_doserate
|
416
|
+
@current_doserate = value
|
417
|
+
DICOM::Element.new('300A,0115', value, :parent => item)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Adds a Nominal Beam Energy element to a Control Point Item.
|
422
|
+
# Note that the element is only added if there is no 'current' attribute
|
423
|
+
# defined, or the given value is different form the current attribute.
|
424
|
+
#
|
425
|
+
# @param [String, NilClass] value the energy attribute
|
426
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
427
|
+
#
|
428
|
+
def add_energy(value, item)
|
429
|
+
if !@current_energy || value != @current_energy
|
430
|
+
@current_energy = value
|
431
|
+
DICOM::Element.new('300A,0114', "#{value.to_f}", :parent => item)
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# Adds an Isosenter element to a Control Point Item.
|
436
|
+
# Note that the element is only added if there is a Site Setup record present,
|
437
|
+
# and it contains a real (non-empty) value. Also, the element is only added if there
|
438
|
+
# is no 'current' attribute defined, or the given value is different form the current attribute.
|
439
|
+
#
|
440
|
+
# @param [SiteSetup, NilClass] site_setup the associated site setup record
|
441
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
442
|
+
#
|
443
|
+
def add_isosenter(site_setup, item)
|
444
|
+
if site_setup
|
445
|
+
# Create an element if the value is new or unique:
|
446
|
+
if !@current_isosenter
|
447
|
+
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)}"
|
448
|
+
if iso != @current_isosenter
|
449
|
+
@current_isosenter = iso
|
450
|
+
DICOM::Element.new('300A,012C', iso, :parent => item)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
else
|
454
|
+
# Log a warning if this is the first control point:
|
455
|
+
unless @current_isosenter
|
456
|
+
logger.warn("No Site Setup record exists for this plan. Unable to provide an isosenter position.")
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Adds a Source to Surface Distance element to a Control Point Item.
|
462
|
+
# Note that the element is only added if the SSD attribute contains
|
463
|
+
# real (non-empty) value.
|
464
|
+
#
|
465
|
+
# @param [String, NilClass] value the SSD attribute
|
466
|
+
# @param [DICOM::Item] item the DICOM control point item in which to create an element
|
467
|
+
#
|
468
|
+
def add_ssd(value, item)
|
469
|
+
DICOM::Element.new('300A,0130', "#{value.to_f * 10}", :parent => item) if value && !value.empty?
|
470
|
+
end
|
471
|
+
|
472
|
+
# Creates a control point item in the given control point sequence, based
|
473
|
+
# on an RTP control point record.
|
474
|
+
#
|
475
|
+
# @param [ControlPoint] cp the RTP ControlPoint record to convert
|
476
|
+
# @param [DICOM::Sequence] sequence the DICOM parent sequence of the item to be created
|
477
|
+
# @param [Hash] options the options to use for creating the control point
|
478
|
+
# @option options [Boolean] :dose_ref if set, a Referenced Dose Reference sequence will be included in the generated control point item
|
479
|
+
# @return [DICOM::Item] the constructed control point DICOM item
|
480
|
+
#
|
481
|
+
def create_control_point(cp, sequence, options={})
|
482
|
+
cp_item = DICOM::Item.new(:parent => sequence)
|
483
|
+
# Some CP attributes will always be written (CP index, BLD positions & Cumulative meterset weight).
|
484
|
+
# The other attributes are only written if they are different from the previous control point.
|
485
|
+
# Control Point Index:
|
486
|
+
DICOM::Element.new('300A,0112', "#{cp.index}", :parent => cp_item)
|
487
|
+
# Beam Limiting Device Position Sequence:
|
488
|
+
create_beam_limiting_device_positions(cp_item, cp, options)
|
489
|
+
# Source to Surface Distance:
|
490
|
+
add_ssd(cp.ssd, cp_item)
|
491
|
+
# Cumulative Meterset Weight:
|
492
|
+
DICOM::Element.new('300A,0134', cp.monitor_units.to_f, :parent => cp_item)
|
493
|
+
# Referenced Dose Reference Sequence:
|
494
|
+
create_referenced_dose_reference(cp_item) if options[:dose_ref]
|
495
|
+
# Attributes that are only added if they carry an updated value:
|
496
|
+
# Nominal Beam Energy:
|
497
|
+
add_energy(cp.energy, cp_item)
|
498
|
+
# Dose Rate Set:
|
499
|
+
add_doserate(cp.doserate, cp_item)
|
500
|
+
# Gantry Angle & Rotation Direction:
|
501
|
+
add_angle(cp_item, '300A,011E', '300A,011F', cp.gantry_angle, cp.gantry_dir, :current_gantry)
|
502
|
+
# Beam Limiting Device Angle & Rotation Direction:
|
503
|
+
add_angle(cp_item, '300A,0120', '300A,0121', cp.collimator_angle, cp.collimator_dir, :current_collimator)
|
504
|
+
# Patient Support Angle & Rotation Direction:
|
505
|
+
add_angle(cp_item, '300A,0122', '300A,0123', cp.couch_pedestal, cp.couch_ped_dir, :current_couch_pedestal)
|
506
|
+
# Table Top Eccentric Angle & Rotation Direction:
|
507
|
+
add_angle(cp_item, '300A,0125', '300A,0126', cp.couch_angle, cp.couch_dir, :current_couch_angle)
|
508
|
+
# Table Top Vertical Position:
|
509
|
+
add_couch_position(cp_item, '300A,0128', cp.couch_vertical, :current_couch_vertical)
|
510
|
+
# Table Top Longitudinal Position:
|
511
|
+
add_couch_position(cp_item, '300A,0129', cp.couch_longitudinal, :current_couch_longitudinal)
|
512
|
+
# Table Top Lateral Position:
|
513
|
+
add_couch_position(cp_item, '300A,012A', cp.couch_lateral, :current_couch_lateral)
|
514
|
+
# Isocenter Position (x\y\z):
|
515
|
+
add_isosenter(cp.parent.parent.site_setup, cp_item)
|
516
|
+
cp_item
|
517
|
+
end
|
518
|
+
|
519
|
+
# Creates a beam limiting device sequence in the given DICOM object.
|
520
|
+
#
|
521
|
+
# @param [DICOM::Item] beam_item the DICOM beam item in which to insert the sequence
|
522
|
+
# @param [Field] field the RTP field to fetch device parameters from
|
523
|
+
# @return [DICOM::Sequence] the constructed beam limiting device sequence
|
524
|
+
#
|
525
|
+
def create_beam_limiting_devices(beam_item, field)
|
526
|
+
bl_seq = DICOM::Sequence.new('300A,00B6', :parent => beam_item)
|
527
|
+
# The ASYMX item ('backup jaws') doesn't exist on all models:
|
528
|
+
if ['SYM', 'ASY'].include?(field.field_x_mode.upcase)
|
529
|
+
bl_item_x = DICOM::Item.new(:parent => bl_seq)
|
530
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => bl_item_x)
|
531
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_x)
|
532
|
+
end
|
533
|
+
# The ASYMY item is always created:
|
534
|
+
bl_item_y = DICOM::Item.new(:parent => bl_seq)
|
535
|
+
# RT Beam Limiting Device Type:
|
536
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => bl_item_y)
|
537
|
+
# Number of Leaf/Jaw Pairs:
|
538
|
+
DICOM::Element.new('300A,00BC', "1", :parent => bl_item_y)
|
539
|
+
# MLCX item is only created if leaves are defined:
|
540
|
+
# (NB: The RTP file doesn't specify leaf position boundaries, so we
|
541
|
+
# have to set these based on a set of known MLC types, their number
|
542
|
+
# of leaves, and their leaf boundary positions.)
|
543
|
+
if field.control_points.length > 0
|
544
|
+
bl_item_mlcx = DICOM::Item.new(:parent => bl_seq)
|
545
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => bl_item_mlcx)
|
546
|
+
num_leaves = field.control_points.first.mlc_leaves.to_i
|
547
|
+
DICOM::Element.new('300A,00BC', num_leaves.to_s, :parent => bl_item_mlcx)
|
548
|
+
DICOM::Element.new('300A,00BE', "#{RTP.leaf_boundaries(num_leaves).join("\\")}", :parent => bl_item_mlcx)
|
549
|
+
end
|
550
|
+
bl_seq
|
551
|
+
end
|
552
|
+
|
553
|
+
# Creates a beam limiting device positions sequence in the given DICOM object.
|
554
|
+
#
|
555
|
+
# @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
|
556
|
+
# @param [ControlPoint] cp the RTP control point to fetch device parameters from
|
557
|
+
# @return [DICOM::Sequence] the constructed beam limiting device positions sequence
|
558
|
+
#
|
559
|
+
def create_beam_limiting_device_positions(cp_item, cp, options={})
|
560
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
561
|
+
# The ASYMX item ('backup jaws') doesn't exist on all models:
|
562
|
+
if ['SYM', 'ASY'].include?(cp.parent.field_x_mode.upcase)
|
563
|
+
dp_item_x = create_asym_item(cp, dp_seq, axis=:x, options)
|
564
|
+
end
|
565
|
+
# Always create one ASYMY item:
|
566
|
+
dp_item_y = create_asym_item(cp, dp_seq, axis=:y, options)
|
567
|
+
# MLCX:
|
568
|
+
dp_item_mlcx = DICOM::Item.new(:parent => dp_seq)
|
569
|
+
# RT Beam Limiting Device Type:
|
570
|
+
DICOM::Element.new('300A,00B8', "MLCX", :parent => dp_item_mlcx)
|
571
|
+
# Leaf/Jaw Positions:
|
572
|
+
DICOM::Element.new('300A,011C', cp.dcm_mlc_positions(options[:scale]), :parent => dp_item_mlcx)
|
573
|
+
dp_seq
|
574
|
+
end
|
575
|
+
|
576
|
+
# Creates an ASYMX or ASYMY item.
|
577
|
+
#
|
578
|
+
# @param [ControlPoint] cp the RTP control point to fetch device parameters from
|
579
|
+
# @param [DICOM::Sequence] dcm_parent the DICOM sequence in which to insert the item
|
580
|
+
# @param [Symbol] axis the axis for the item (:x or :y)
|
581
|
+
# @return [DICOM::Item] the constructed ASYMX or ASYMY item
|
582
|
+
#
|
583
|
+
def create_asym_item(cp, dcm_parent, axis, options={})
|
584
|
+
val1 = cp.send("dcm_collimator_#{axis.to_s}1", options[:scale])
|
585
|
+
val2 = cp.send("dcm_collimator_#{axis.to_s}2", options[:scale])
|
586
|
+
item = DICOM::Item.new(:parent => dcm_parent)
|
587
|
+
# RT Beam Limiting Device Type:
|
588
|
+
DICOM::Element.new('300A,00B8', "ASYM#{axis.to_s.upcase}", :parent => item)
|
589
|
+
# Leaf/Jaw Positions:
|
590
|
+
DICOM::Element.new('300A,011C', "#{val1}\\#{val2}", :parent => item)
|
591
|
+
item
|
592
|
+
end
|
593
|
+
|
594
|
+
# Creates a beam limiting device positions sequence in the given DICOM object.
|
595
|
+
#
|
596
|
+
# @param [DICOM::Item] cp_item the DICOM control point item in which to insert the sequence
|
597
|
+
# @param [Field] field the RTP treatment field to fetch device parameters from
|
598
|
+
# @return [DICOM::Sequence] the constructed beam limiting device positions sequence
|
599
|
+
#
|
600
|
+
def create_beam_limiting_device_positions_from_field(cp_item, field, options={})
|
601
|
+
dp_seq = DICOM::Sequence.new('300A,011A', :parent => cp_item)
|
602
|
+
# ASYMX:
|
603
|
+
dp_item_x = DICOM::Item.new(:parent => dp_seq)
|
604
|
+
DICOM::Element.new('300A,00B8', "ASYMX", :parent => dp_item_x)
|
605
|
+
DICOM::Element.new('300A,011C', "#{field.dcm_collimator_x1}\\#{field.dcm_collimator_x2}", :parent => dp_item_x)
|
606
|
+
# ASYMY:
|
607
|
+
dp_item_y = DICOM::Item.new(:parent => dp_seq)
|
608
|
+
DICOM::Element.new('300A,00B8', "ASYMY", :parent => dp_item_y)
|
609
|
+
DICOM::Element.new('300A,011C', "#{field.dcm_collimator_y1}\\#{field.dcm_collimator_y2}", :parent => dp_item_y)
|
610
|
+
dp_seq
|
611
|
+
end
|
612
|
+
|
613
|
+
# Creates a dose reference sequence in the given DICOM object.
|
614
|
+
#
|
615
|
+
# @param [DICOM::DObject] dcm the DICOM object in which to insert the sequence
|
616
|
+
# @param [String] description the value to use for Dose Reference Description
|
617
|
+
# @return [DICOM::Sequence] the constructed dose reference sequence
|
618
|
+
#
|
619
|
+
def create_dose_reference(dcm, description)
|
620
|
+
dr_seq = DICOM::Sequence.new('300A,0010', :parent => dcm)
|
621
|
+
dr_item = DICOM::Item.new(:parent => dr_seq)
|
622
|
+
# Dose Reference Number:
|
623
|
+
DICOM::Element.new('300A,0012', '1', :parent => dr_item)
|
624
|
+
# Dose Reference Structure Type:
|
625
|
+
DICOM::Element.new('300A,0014', 'SITE', :parent => dr_item)
|
626
|
+
# Dose Reference Description:
|
627
|
+
DICOM::Element.new('300A,0016', description, :parent => dr_item)
|
628
|
+
# Dose Reference Type:
|
629
|
+
DICOM::Element.new('300A,0020', 'TARGET', :parent => dr_item)
|
630
|
+
dr_seq
|
631
|
+
end
|
632
|
+
|
633
|
+
# Creates a referenced dose reference sequence in the given DICOM object.
|
634
|
+
#
|
635
|
+
# @param [DICOM::Item] cp_item the DICOM item in which to insert the sequence
|
636
|
+
# @return [DICOM::Sequence] the constructed referenced dose reference sequence
|
637
|
+
#
|
638
|
+
def create_referenced_dose_reference(cp_item)
|
639
|
+
# Referenced Dose Reference Sequence:
|
640
|
+
rd_seq = DICOM::Sequence.new('300C,0050', :parent => cp_item)
|
641
|
+
rd_item = DICOM::Item.new(:parent => rd_seq)
|
642
|
+
# Cumulative Dose Reference Coeffecient:
|
643
|
+
DICOM::Element.new('300A,010C', '', :parent => rd_item)
|
644
|
+
# Referenced Dose Reference Number:
|
645
|
+
DICOM::Element.new('300C,0051', '1', :parent => rd_item)
|
646
|
+
rd_seq
|
647
|
+
end
|
648
|
+
|
649
|
+
# Resets the types of control point attributes that are only written to the
|
650
|
+
# first control point item, and for following control point items only when
|
651
|
+
# they are different from the 'current' value. When a new field is reached,
|
652
|
+
# it is essential to reset these attributes, or else we could risk to start
|
653
|
+
# the field with a control point with missing attributes, if one of its first
|
654
|
+
# attributes is equal to the last attribute of the previous field.
|
655
|
+
#
|
656
|
+
def reset_cp_current_attributes
|
657
|
+
@current_gantry = nil
|
658
|
+
@current_collimator = nil
|
659
|
+
@current_couch_pedestal = nil
|
660
|
+
@current_couch_angle = nil
|
661
|
+
@current_couch_vertical = nil
|
662
|
+
@current_couch_longitudinal = nil
|
663
|
+
@current_couch_lateral = nil
|
664
|
+
@current_isosenter = nil
|
665
|
+
end
|
666
|
+
|
667
|
+
end
|
668
|
+
|
695
669
|
end
|