PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
rt_iod_validator.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <sstream>
15
17
18using namespace kcenon::pacs::core;
19
20// =============================================================================
21// RT-specific DICOM tags
22// =============================================================================
23namespace rt_tags {
24 // RT Series Module
25 constexpr dicom_tag series_number{0x0020, 0x0011};
26
27 // Frame of Reference
28 constexpr dicom_tag frame_of_reference_uid{0x0020, 0x0052};
29 constexpr dicom_tag position_reference_indicator{0x0020, 0x1040};
30
31 // RT General Plan Module
32 constexpr dicom_tag rt_plan_label{0x300A, 0x0002};
33 constexpr dicom_tag rt_plan_date{0x300A, 0x0006};
34 constexpr dicom_tag rt_plan_time{0x300A, 0x0007};
35 constexpr dicom_tag rt_plan_geometry{0x300A, 0x000C};
36 constexpr dicom_tag plan_intent{0x300A, 0x000A};
38
39 // RT Fraction Scheme Module
40 constexpr dicom_tag fraction_group_sequence{0x300A, 0x0070};
41 constexpr dicom_tag number_of_fractions_planned{0x300A, 0x0078};
42 constexpr dicom_tag number_of_beams{0x300A, 0x0080};
43
44 // RT Beams Module
45 constexpr dicom_tag beam_sequence{0x300A, 0x00B0};
46 constexpr dicom_tag beam_type{0x300A, 0x00C4};
47 constexpr dicom_tag radiation_type{0x300A, 0x00C6};
48 constexpr dicom_tag treatment_machine_name{0x300A, 0x00B2};
49
50 // RT Dose Module
51 constexpr dicom_tag dose_units{0x3004, 0x0002};
52 constexpr dicom_tag dose_type{0x3004, 0x0004};
53 constexpr dicom_tag dose_summation_type{0x3004, 0x000A};
54 constexpr dicom_tag grid_frame_offset_vector{0x3004, 0x000C};
55 constexpr dicom_tag dose_grid_scaling{0x3004, 0x000E};
56 constexpr dicom_tag referenced_rt_plan_sequence{0x300C, 0x0002};
57
58 // Structure Set Module
59 constexpr dicom_tag structure_set_label{0x3006, 0x0002};
60 constexpr dicom_tag structure_set_date{0x3006, 0x0008};
61 constexpr dicom_tag structure_set_time{0x3006, 0x0009};
63 constexpr dicom_tag structure_set_roi_sequence{0x3006, 0x0020};
64
65 // ROI Contour Module
66 constexpr dicom_tag roi_contour_sequence{0x3006, 0x0039};
67 constexpr dicom_tag roi_display_color{0x3006, 0x002A};
68
69 // RT ROI Observations Module
70 constexpr dicom_tag rt_roi_observations_sequence{0x3006, 0x0080};
71 constexpr dicom_tag rt_roi_interpreted_type{0x3006, 0x00A4};
72
73 // Image Pixel Module (for RT Dose)
74 constexpr dicom_tag rows{0x0028, 0x0010};
75 constexpr dicom_tag columns{0x0028, 0x0011};
76 constexpr dicom_tag bits_allocated{0x0028, 0x0100};
77 constexpr dicom_tag bits_stored{0x0028, 0x0101};
78 constexpr dicom_tag high_bit{0x0028, 0x0102};
79 constexpr dicom_tag pixel_representation{0x0028, 0x0103};
80 constexpr dicom_tag pixel_data{0x7FE0, 0x0010};
81 constexpr dicom_tag number_of_frames{0x0028, 0x0008};
82}
83
84// =============================================================================
85// rt_plan_iod_validator Implementation
86// =============================================================================
87
90
92 validation_result result;
93 result.is_valid = true;
94
95 // Validate mandatory modules
97 validate_patient_module(dataset, result.findings);
99 validate_rt_series_module(dataset, result.findings);
102 validate_sop_common_module(dataset, result.findings);
103 }
104
105 // RT Plan specific validation
108 validate_rt_beams_module(dataset, result.findings);
109 }
110
111 // Check for errors
112 for (const auto& finding : result.findings) {
113 if (finding.severity == validation_severity::error) {
114 result.is_valid = false;
115 break;
116 }
117 if (options_.strict_mode && finding.severity == validation_severity::warning) {
118 result.is_valid = false;
119 break;
120 }
121 }
122
123 return result;
124}
125
127 // Check Type 1 required attributes
128 if (!dataset.contains(tags::study_instance_uid)) return false;
129 if (!dataset.contains(tags::series_instance_uid)) return false;
130 if (!dataset.contains(tags::modality)) return false;
131
132 auto modality = dataset.get_string(tags::modality);
133 if (modality != "RTPLAN") return false;
134
135 if (!dataset.contains(rt_tags::rt_plan_label)) return false;
136 if (!dataset.contains(rt_tags::rt_plan_geometry)) return false;
137 if (!dataset.contains(tags::sop_class_uid)) return false;
138 if (!dataset.contains(tags::sop_instance_uid)) return false;
139
140 return true;
141}
142
144 return options_;
145}
146
150
152 const dicom_dataset& dataset,
153 std::vector<validation_finding>& findings) const {
154
155 if (options_.check_type2) {
156 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
157 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
158 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
159 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
160 }
161}
162
164 const dicom_dataset& dataset,
165 std::vector<validation_finding>& findings) const {
166
167 if (options_.check_type1) {
168 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
169 }
170
171 if (options_.check_type2) {
172 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
173 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
174 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
175 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
176 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
177 }
178}
179
181 const dicom_dataset& dataset,
182 std::vector<validation_finding>& findings) const {
183
184 if (options_.check_type1) {
185 check_type1_attribute(dataset, tags::modality, "Modality", findings);
186 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
187
188 // Validate Modality is RTPLAN
189 if (dataset.contains(tags::modality)) {
190 auto modality = dataset.get_string(tags::modality);
191 if (modality != "RTPLAN") {
192 findings.push_back({
195 "Modality must be 'RTPLAN' for RT Plan, found: " + modality,
196 "RTPLAN-ERR-001"
197 });
198 }
199 }
200 }
201
202 if (options_.check_type2) {
203 check_type2_attribute(dataset, rt_tags::series_number, "SeriesNumber", findings);
204 }
205}
206
208 const dicom_dataset& dataset,
209 std::vector<validation_finding>& findings) const {
210
211 if (options_.check_type1) {
213 "FrameOfReferenceUID", findings);
214 }
215
216 if (options_.check_type2) {
218 "PositionReferenceIndicator", findings);
219 }
220}
221
223 const dicom_dataset& dataset,
224 std::vector<validation_finding>& findings) const {
225
226 // Type 1 attributes
227 if (options_.check_type1) {
228 check_type1_attribute(dataset, rt_tags::rt_plan_label, "RTPlanLabel", findings);
229 check_type1_attribute(dataset, rt_tags::rt_plan_geometry, "RTPlanGeometry", findings);
230 }
231
232 // Validate RTPlanGeometry value
233 if (dataset.contains(rt_tags::rt_plan_geometry)) {
234 auto geometry = dataset.get_string(rt_tags::rt_plan_geometry);
235 if (geometry != "PATIENT" && geometry != "TREATMENT_DEVICE") {
236 findings.push_back({
239 "Invalid RTPlanGeometry: " + geometry + " (expected PATIENT or TREATMENT_DEVICE)",
240 "RTPLAN-WARN-001"
241 });
242 }
243 }
244
245 // Type 2 attributes
246 if (options_.check_type2) {
247 check_type2_attribute(dataset, rt_tags::rt_plan_date, "RTPlanDate", findings);
248 check_type2_attribute(dataset, rt_tags::rt_plan_time, "RTPlanTime", findings);
249 }
250
251 // Type 3 but important: Plan Intent
252 if (dataset.contains(rt_tags::plan_intent)) {
253 auto intent = dataset.get_string(rt_tags::plan_intent);
254 if (intent != "CURATIVE" && intent != "PALLIATIVE" && intent != "PROPHYLACTIC" &&
255 intent != "VERIFICATION" && intent != "MACHINE_QA" && intent != "RESEARCH" &&
256 intent != "SERVICE") {
257 findings.push_back({
260 "Non-standard PlanIntent: " + intent,
261 "RTPLAN-INFO-001"
262 });
263 }
264 }
265
266 // Referenced Structure Set Sequence is important
269 findings.push_back({
272 "ReferencedStructureSetSequence not present - plan has no associated structures",
273 "RTPLAN-WARN-002"
274 });
275 }
276 }
277}
278
280 const dicom_dataset& dataset,
281 std::vector<validation_finding>& findings) const {
282
283 // Fraction Group Sequence is Type 1
284 if (options_.check_type1) {
286 "FractionGroupSequence", findings);
287 }
288
289 // Validate fraction scheme details if sequence exists
291 // NumberOfFractionsPlanned should be present
293 findings.push_back({
296 "NumberOfFractionsPlanned not specified in FractionGroupSequence",
297 "RTPLAN-WARN-003"
298 });
299 }
300 }
301}
302
304 const dicom_dataset& dataset,
305 std::vector<validation_finding>& findings) const {
306
307 // Beam Sequence is Type 1C (required if NumberOfBeams > 0)
308 if (dataset.contains(rt_tags::number_of_beams)) {
309 auto num_beams = dataset.get_numeric<int32_t>(rt_tags::number_of_beams);
310 if (num_beams && *num_beams > 0) {
311 if (!dataset.contains(rt_tags::beam_sequence)) {
312 findings.push_back({
315 "BeamSequence required when NumberOfBeams > 0",
316 "RTPLAN-ERR-002"
317 });
318 }
319 }
320 }
321
322 // Validate beam details if sequence exists
323 if (dataset.contains(rt_tags::beam_sequence)) {
324 // Check for treatment machine name
326 findings.push_back({
329 "TreatmentMachineName not specified",
330 "RTPLAN-INFO-002"
331 });
332 }
333
334 // Validate Beam Type if present
335 if (dataset.contains(rt_tags::beam_type)) {
336 auto beam_type = dataset.get_string(rt_tags::beam_type);
337 if (beam_type != "STATIC" && beam_type != "DYNAMIC") {
338 findings.push_back({
341 "Non-standard BeamType: " + beam_type + " (expected STATIC or DYNAMIC)",
342 "RTPLAN-WARN-004"
343 });
344 }
345 }
346
347 // Validate Radiation Type if present
348 if (dataset.contains(rt_tags::radiation_type)) {
349 auto radiation_type = dataset.get_string(rt_tags::radiation_type);
350 if (radiation_type != "PHOTON" && radiation_type != "ELECTRON" &&
351 radiation_type != "NEUTRON" && radiation_type != "PROTON" &&
352 radiation_type != "ION") {
353 findings.push_back({
356 "Non-standard RadiationType: " + radiation_type,
357 "RTPLAN-WARN-005"
358 });
359 }
360 }
361 }
362}
363
365 const dicom_dataset& dataset,
366 std::vector<validation_finding>& findings) const {
367
368 if (options_.check_type1) {
369 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
370 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
371 }
372
373 // Validate SOP Class UID is RT Plan
374 if (dataset.contains(tags::sop_class_uid)) {
375 auto sop_class = dataset.get_string(tags::sop_class_uid);
376 if (!sop_classes::is_rt_plan_sop_class(sop_class)) {
377 findings.push_back({
380 "SOPClassUID is not an RT Plan SOP Class: " + sop_class,
381 "RTPLAN-ERR-003"
382 });
383 }
384 }
385}
386
388 const dicom_dataset& dataset,
389 dicom_tag tag,
390 std::string_view name,
391 std::vector<validation_finding>& findings) const {
392
393 if (!dataset.contains(tag)) {
394 findings.push_back({
396 tag,
397 std::string("Type 1 attribute missing: ") + std::string(name) +
398 " (" + tag.to_string() + ")",
399 "RTPLAN-TYPE1-MISSING"
400 });
401 } else {
402 auto value = dataset.get_string(tag);
403 if (value.empty()) {
404 findings.push_back({
406 tag,
407 std::string("Type 1 attribute has empty value: ") +
408 std::string(name) + " (" + tag.to_string() + ")",
409 "RTPLAN-TYPE1-EMPTY"
410 });
411 }
412 }
413}
414
416 const dicom_dataset& dataset,
417 dicom_tag tag,
418 std::string_view name,
419 std::vector<validation_finding>& findings) const {
420
421 if (!dataset.contains(tag)) {
422 findings.push_back({
424 tag,
425 std::string("Type 2 attribute missing: ") + std::string(name) +
426 " (" + tag.to_string() + ")",
427 "RTPLAN-TYPE2-MISSING"
428 });
429 }
430}
431
432// =============================================================================
433// rt_dose_iod_validator Implementation
434// =============================================================================
435
438
440 validation_result result;
441 result.is_valid = true;
442
443 // Validate mandatory modules
445 validate_patient_module(dataset, result.findings);
447 validate_rt_series_module(dataset, result.findings);
449 validate_rt_dose_module(dataset, result.findings);
450 validate_sop_common_module(dataset, result.findings);
451 }
452
453 // Validate pixel data for dose grid
455 validate_image_pixel_module(dataset, result.findings);
456 }
457
458 // Check for errors
459 for (const auto& finding : result.findings) {
460 if (finding.severity == validation_severity::error) {
461 result.is_valid = false;
462 break;
463 }
464 if (options_.strict_mode && finding.severity == validation_severity::warning) {
465 result.is_valid = false;
466 break;
467 }
468 }
469
470 return result;
471}
472
474 if (!dataset.contains(tags::study_instance_uid)) return false;
475 if (!dataset.contains(tags::series_instance_uid)) return false;
476 if (!dataset.contains(tags::modality)) return false;
477
478 auto modality = dataset.get_string(tags::modality);
479 if (modality != "RTDOSE") return false;
480
481 if (!dataset.contains(rt_tags::dose_units)) return false;
482 if (!dataset.contains(rt_tags::dose_type)) return false;
483 if (!dataset.contains(rt_tags::dose_summation_type)) return false;
484 if (!dataset.contains(tags::sop_class_uid)) return false;
485 if (!dataset.contains(tags::sop_instance_uid)) return false;
486
487 return true;
488}
489
491 return options_;
492}
493
497
499 const dicom_dataset& dataset,
500 std::vector<validation_finding>& findings) const {
501
502 if (options_.check_type2) {
503 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
504 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
505 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
506 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
507 }
508}
509
511 const dicom_dataset& dataset,
512 std::vector<validation_finding>& findings) const {
513
514 if (options_.check_type1) {
515 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
516 }
517
518 if (options_.check_type2) {
519 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
520 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
521 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
522 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
523 }
524}
525
527 const dicom_dataset& dataset,
528 std::vector<validation_finding>& findings) const {
529
530 if (options_.check_type1) {
531 check_type1_attribute(dataset, tags::modality, "Modality", findings);
532 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
533
534 if (dataset.contains(tags::modality)) {
535 auto modality = dataset.get_string(tags::modality);
536 if (modality != "RTDOSE") {
537 findings.push_back({
540 "Modality must be 'RTDOSE' for RT Dose, found: " + modality,
541 "RTDOSE-ERR-001"
542 });
543 }
544 }
545 }
546}
547
549 const dicom_dataset& dataset,
550 std::vector<validation_finding>& findings) const {
551
552 if (options_.check_type1) {
554 "FrameOfReferenceUID", findings);
555 }
556}
557
559 const dicom_dataset& dataset,
560 std::vector<validation_finding>& findings) const {
561
562 // Type 1 attributes
563 if (options_.check_type1) {
564 check_type1_attribute(dataset, rt_tags::dose_units, "DoseUnits", findings);
565 check_type1_attribute(dataset, rt_tags::dose_type, "DoseType", findings);
566 check_type1_attribute(dataset, rt_tags::dose_summation_type, "DoseSummationType", findings);
567 }
568
569 // Validate DoseUnits
570 if (dataset.contains(rt_tags::dose_units)) {
571 auto units = dataset.get_string(rt_tags::dose_units);
572 if (units != "GY" && units != "RELATIVE") {
573 findings.push_back({
576 "Non-standard DoseUnits: " + units + " (expected GY or RELATIVE)",
577 "RTDOSE-WARN-001"
578 });
579 }
580 }
581
582 // Validate DoseType
583 if (dataset.contains(rt_tags::dose_type)) {
584 auto dose_type = dataset.get_string(rt_tags::dose_type);
585 if (dose_type != "PHYSICAL" && dose_type != "EFFECTIVE" && dose_type != "ERROR") {
586 findings.push_back({
589 "Non-standard DoseType: " + dose_type,
590 "RTDOSE-WARN-002"
591 });
592 }
593 }
594
595 // Validate DoseSummationType
597 auto summation = dataset.get_string(rt_tags::dose_summation_type);
598 if (summation != "PLAN" && summation != "MULTI_PLAN" && summation != "FRACTION" &&
599 summation != "BEAM" && summation != "BRACHY" && summation != "FRACTION_SESSION" &&
600 summation != "BEAM_SESSION" && summation != "BRACHY_SESSION" &&
601 summation != "CONTROL_POINT" && summation != "RECORD") {
602 findings.push_back({
605 "Non-standard DoseSummationType: " + summation,
606 "RTDOSE-WARN-003"
607 });
608 }
609 }
610
611 // DoseGridScaling is required for proper dose value interpretation
612 if (!dataset.contains(rt_tags::dose_grid_scaling)) {
613 findings.push_back({
616 "DoseGridScaling not present - dose values may not be properly scaled",
617 "RTDOSE-WARN-004"
618 });
619 }
620
621 // ReferencedRTPlanSequence for traceability
624 findings.push_back({
627 "ReferencedRTPlanSequence not present - dose has no associated plan reference",
628 "RTDOSE-INFO-001"
629 });
630 }
631 }
632
633 // Validate dose data consistency
635 check_dose_data_consistency(dataset, findings);
636 }
637}
638
640 const dicom_dataset& dataset,
641 std::vector<validation_finding>& findings) const {
642
643 // For dose grids, pixel data is required
644 if (options_.check_type1) {
645 check_type1_attribute(dataset, rt_tags::rows, "Rows", findings);
646 check_type1_attribute(dataset, rt_tags::columns, "Columns", findings);
647 check_type1_attribute(dataset, rt_tags::bits_allocated, "BitsAllocated", findings);
648 check_type1_attribute(dataset, rt_tags::bits_stored, "BitsStored", findings);
649 check_type1_attribute(dataset, rt_tags::high_bit, "HighBit", findings);
650 check_type1_attribute(dataset, rt_tags::pixel_representation, "PixelRepresentation", findings);
651 }
652
653 // Pixel Data may not be present for point doses
654 if (!dataset.contains(rt_tags::pixel_data)) {
655 findings.push_back({
658 "PixelData not present - this may be a point dose or DVH-only object",
659 "RTDOSE-INFO-002"
660 });
661 }
662}
663
665 const dicom_dataset& dataset,
666 std::vector<validation_finding>& findings) const {
667
668 if (options_.check_type1) {
669 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
670 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
671 }
672
673 if (dataset.contains(tags::sop_class_uid)) {
674 auto sop_class = dataset.get_string(tags::sop_class_uid);
675 if (sop_class != sop_classes::rt_dose_storage_uid) {
676 findings.push_back({
679 "SOPClassUID is not RT Dose Storage: " + sop_class,
680 "RTDOSE-ERR-002"
681 });
682 }
683 }
684}
685
687 const dicom_dataset& dataset,
688 dicom_tag tag,
689 std::string_view name,
690 std::vector<validation_finding>& findings) const {
691
692 if (!dataset.contains(tag)) {
693 findings.push_back({
695 tag,
696 std::string("Type 1 attribute missing: ") + std::string(name) +
697 " (" + tag.to_string() + ")",
698 "RTDOSE-TYPE1-MISSING"
699 });
700 } else {
701 auto value = dataset.get_string(tag);
702 if (value.empty()) {
703 findings.push_back({
705 tag,
706 std::string("Type 1 attribute has empty value: ") +
707 std::string(name) + " (" + tag.to_string() + ")",
708 "RTDOSE-TYPE1-EMPTY"
709 });
710 }
711 }
712}
713
715 const dicom_dataset& dataset,
716 dicom_tag tag,
717 std::string_view name,
718 std::vector<validation_finding>& findings) const {
719
720 if (!dataset.contains(tag)) {
721 findings.push_back({
723 tag,
724 std::string("Type 2 attribute missing: ") + std::string(name) +
725 " (" + tag.to_string() + ")",
726 "RTDOSE-TYPE2-MISSING"
727 });
728 }
729}
730
732 const dicom_dataset& dataset,
733 std::vector<validation_finding>& findings) const {
734
735 // Check bits consistency
736 auto bits_allocated = dataset.get_numeric<uint16_t>(rt_tags::bits_allocated);
737 auto bits_stored = dataset.get_numeric<uint16_t>(rt_tags::bits_stored);
738 auto high_bit = dataset.get_numeric<uint16_t>(rt_tags::high_bit);
739
740 if (bits_allocated && bits_stored) {
741 if (*bits_stored > *bits_allocated) {
742 findings.push_back({
745 "BitsStored exceeds BitsAllocated",
746 "RTDOSE-ERR-003"
747 });
748 }
749 }
750
751 if (bits_stored && high_bit) {
752 if (*high_bit != *bits_stored - 1) {
753 findings.push_back({
756 "HighBit should typically be BitsStored - 1",
757 "RTDOSE-WARN-005"
758 });
759 }
760 }
761
762 // Multi-frame dose grids should have GridFrameOffsetVector
763 auto num_frames = dataset.get_numeric<int32_t>(rt_tags::number_of_frames);
764 if (num_frames && *num_frames > 1) {
766 findings.push_back({
769 "GridFrameOffsetVector recommended for multi-frame dose grids",
770 "RTDOSE-WARN-006"
771 });
772 }
773 }
774}
775
776// =============================================================================
777// rt_structure_set_iod_validator Implementation
778// =============================================================================
779
782
784 validation_result result;
785 result.is_valid = true;
786
787 // Validate mandatory modules
789 validate_patient_module(dataset, result.findings);
791 validate_rt_series_module(dataset, result.findings);
793 validate_sop_common_module(dataset, result.findings);
794 }
795
796 // RT Structure Set specific validation
798 validate_roi_contour_module(dataset, result.findings);
800 }
801
802 // Check ROI consistency
803 check_roi_consistency(dataset, result.findings);
804
805 // Check for errors
806 for (const auto& finding : result.findings) {
807 if (finding.severity == validation_severity::error) {
808 result.is_valid = false;
809 break;
810 }
811 if (options_.strict_mode && finding.severity == validation_severity::warning) {
812 result.is_valid = false;
813 break;
814 }
815 }
816
817 return result;
818}
819
821 if (!dataset.contains(tags::study_instance_uid)) return false;
822 if (!dataset.contains(tags::series_instance_uid)) return false;
823 if (!dataset.contains(tags::modality)) return false;
824
825 auto modality = dataset.get_string(tags::modality);
826 if (modality != "RTSTRUCT") return false;
827
828 if (!dataset.contains(rt_tags::structure_set_label)) return false;
829 if (!dataset.contains(rt_tags::structure_set_roi_sequence)) return false;
830 if (!dataset.contains(tags::sop_class_uid)) return false;
831 if (!dataset.contains(tags::sop_instance_uid)) return false;
832
833 return true;
834}
835
839
843
845 const dicom_dataset& dataset,
846 std::vector<validation_finding>& findings) const {
847
848 if (options_.check_type2) {
849 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
850 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
851 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
852 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
853 }
854}
855
857 const dicom_dataset& dataset,
858 std::vector<validation_finding>& findings) const {
859
860 if (options_.check_type1) {
861 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
862 }
863
864 if (options_.check_type2) {
865 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
866 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
867 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
868 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
869 }
870}
871
873 const dicom_dataset& dataset,
874 std::vector<validation_finding>& findings) const {
875
876 if (options_.check_type1) {
877 check_type1_attribute(dataset, tags::modality, "Modality", findings);
878 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
879
880 if (dataset.contains(tags::modality)) {
881 auto modality = dataset.get_string(tags::modality);
882 if (modality != "RTSTRUCT") {
883 findings.push_back({
886 "Modality must be 'RTSTRUCT' for RT Structure Set, found: " + modality,
887 "RTSTRUCT-ERR-001"
888 });
889 }
890 }
891 }
892}
893
895 const dicom_dataset& dataset,
896 std::vector<validation_finding>& findings) const {
897
898 // Type 1 attributes
899 if (options_.check_type1) {
900 check_type1_attribute(dataset, rt_tags::structure_set_label, "StructureSetLabel", findings);
902 "StructureSetROISequence", findings);
903 }
904
905 // Type 2 attributes
906 if (options_.check_type2) {
907 check_type2_attribute(dataset, rt_tags::structure_set_date, "StructureSetDate", findings);
908 check_type2_attribute(dataset, rt_tags::structure_set_time, "StructureSetTime", findings);
909 }
910
911 // ReferencedFrameOfReferenceSequence is important for spatial registration
914 findings.push_back({
917 "ReferencedFrameOfReferenceSequence not present - structures have no spatial reference",
918 "RTSTRUCT-WARN-001"
919 });
920 }
921 }
922}
923
925 const dicom_dataset& dataset,
926 std::vector<validation_finding>& findings) const {
927
928 // ROI Contour Sequence is Type 1
929 if (options_.check_type1) {
930 check_type1_attribute(dataset, rt_tags::roi_contour_sequence, "ROIContourSequence", findings);
931 }
932
933 // ROI Display Color is Type 3 but useful
934 if (!dataset.contains(rt_tags::roi_display_color)) {
935 findings.push_back({
938 "ROIDisplayColor not present - ROIs will use default colors",
939 "RTSTRUCT-INFO-001"
940 });
941 }
942}
943
945 const dicom_dataset& dataset,
946 std::vector<validation_finding>& findings) const {
947
948 // RT ROI Observations Sequence is Type 1
949 if (options_.check_type1) {
951 "RTROIObservationsSequence", findings);
952 }
953
954 // Check for RT ROI Interpreted Type
957 findings.push_back({
960 "RTROIInterpretedType not specified - ROI semantic meaning not defined",
961 "RTSTRUCT-INFO-002"
962 });
963 }
964 }
965}
966
968 const dicom_dataset& dataset,
969 std::vector<validation_finding>& findings) const {
970
971 if (options_.check_type1) {
972 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
973 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
974 }
975
976 if (dataset.contains(tags::sop_class_uid)) {
977 auto sop_class = dataset.get_string(tags::sop_class_uid);
979 findings.push_back({
982 "SOPClassUID is not RT Structure Set Storage: " + sop_class,
983 "RTSTRUCT-ERR-002"
984 });
985 }
986 }
987}
988
990 const dicom_dataset& dataset,
991 dicom_tag tag,
992 std::string_view name,
993 std::vector<validation_finding>& findings) const {
994
995 if (!dataset.contains(tag)) {
996 findings.push_back({
998 tag,
999 std::string("Type 1 attribute missing: ") + std::string(name) +
1000 " (" + tag.to_string() + ")",
1001 "RTSTRUCT-TYPE1-MISSING"
1002 });
1003 } else {
1004 auto value = dataset.get_string(tag);
1005 if (value.empty()) {
1006 findings.push_back({
1008 tag,
1009 std::string("Type 1 attribute has empty value: ") +
1010 std::string(name) + " (" + tag.to_string() + ")",
1011 "RTSTRUCT-TYPE1-EMPTY"
1012 });
1013 }
1014 }
1015}
1016
1018 const dicom_dataset& dataset,
1019 dicom_tag tag,
1020 std::string_view name,
1021 std::vector<validation_finding>& findings) const {
1022
1023 if (!dataset.contains(tag)) {
1024 findings.push_back({
1026 tag,
1027 std::string("Type 2 attribute missing: ") + std::string(name) +
1028 " (" + tag.to_string() + ")",
1029 "RTSTRUCT-TYPE2-MISSING"
1030 });
1031 }
1032}
1033
1035 const dicom_dataset& dataset,
1036 std::vector<validation_finding>& findings) const {
1037
1038 // Check that ROI Contour Sequence items reference valid ROIs from Structure Set ROI Sequence
1039 // This is a simplified check - full implementation would iterate through sequences
1042 // Both sequences present - good
1043 } else if (dataset.contains(rt_tags::structure_set_roi_sequence) &&
1045 findings.push_back({
1048 "StructureSetROISequence present but ROIContourSequence missing - ROIs have no contours",
1049 "RTSTRUCT-WARN-002"
1050 });
1051 }
1052}
1053
1054// =============================================================================
1055// rt_iod_validator (Unified) Implementation
1056// =============================================================================
1057
1059 : options_(options) {}
1060
1062 // Detect RT object type from SOP Class UID or Modality
1063 std::string sop_class;
1064 if (dataset.contains(tags::sop_class_uid)) {
1065 sop_class = dataset.get_string(tags::sop_class_uid);
1066 }
1067
1068 // Route to appropriate validator
1069 if (sop_class == sop_classes::rt_plan_storage_uid ||
1072 return validator.validate(dataset);
1073 }
1074
1075 if (sop_class == sop_classes::rt_dose_storage_uid) {
1077 return validator.validate(dataset);
1078 }
1079
1082 return validator.validate(dataset);
1083 }
1084
1085 // Try to detect from Modality if SOP Class not recognized
1086 if (dataset.contains(tags::modality)) {
1087 auto modality = dataset.get_string(tags::modality);
1088
1089 if (modality == "RTPLAN") {
1091 return validator.validate(dataset);
1092 }
1093 if (modality == "RTDOSE") {
1095 return validator.validate(dataset);
1096 }
1097 if (modality == "RTSTRUCT") {
1099 return validator.validate(dataset);
1100 }
1101 }
1102
1103 // Unknown RT type
1104 validation_result result;
1105 result.is_valid = false;
1106 result.findings.push_back({
1109 "Unable to determine RT object type from SOPClassUID or Modality",
1110 "RT-ERR-001"
1111 });
1112
1113 return result;
1114}
1115
1117 // Try each RT type
1118 if (rt_plan_iod_validator(options_).quick_check(dataset)) return true;
1119 if (rt_dose_iod_validator(options_).quick_check(dataset)) return true;
1120 if (rt_structure_set_iod_validator(options_).quick_check(dataset)) return true;
1121
1122 return false;
1123}
1124
1126 return options_;
1127}
1128
1132
1133// =============================================================================
1134// Convenience Functions
1135// =============================================================================
1136
1138 rt_plan_iod_validator validator;
1139 return validator.validate(dataset);
1140}
1141
1143 rt_dose_iod_validator validator;
1144 return validator.validate(dataset);
1145}
1146
1149 return validator.validate(dataset);
1150}
1151
1153 rt_iod_validator validator;
1154 return validator.validate(dataset);
1155}
1156
1158 rt_plan_iod_validator validator;
1159 return validator.quick_check(dataset);
1160}
1161
1163 rt_dose_iod_validator validator;
1164 return validator.quick_check(dataset);
1165}
1166
1169 return validator.quick_check(dataset);
1170}
1171
1173 rt_iod_validator validator;
1174 return validator.quick_check(dataset);
1175}
1176
1177} // namespace kcenon::pacs::services::validation
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
auto contains(dicom_tag tag) const noexcept -> bool
Check if the dataset contains an element with the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
auto to_string() const -> std::string
Convert to string representation.
void set_options(const rt_validation_options &options)
void validate_rt_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type2_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
bool quick_check(const core::dicom_dataset &dataset) const
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate(const core::dicom_dataset &dataset) const
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const rt_validation_options & options() const noexcept
void validate_frame_of_reference_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_image_pixel_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_rt_dose_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_dose_data_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate(const core::dicom_dataset &dataset) const
Validate an RT dataset (auto-detects type)
const rt_validation_options & options() const noexcept
void set_options(const rt_validation_options &options)
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required RT attributes.
void validate_rt_general_plan_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type2_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_frame_of_reference_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const rt_validation_options & options() const noexcept
void validate_rt_fraction_scheme_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_rt_beams_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate(const core::dicom_dataset &dataset) const
bool quick_check(const core::dicom_dataset &dataset) const
void validate_rt_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const rt_validation_options &options)
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void check_type2_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_structure_set_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_rt_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_roi_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
bool quick_check(const core::dicom_dataset &dataset) const
void validate_roi_contour_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_rt_roi_observations_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate(const core::dicom_dataset &dataset) const
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr std::string_view rt_structure_set_storage_uid
RT Structure Set Storage SOP Class UID.
Definition rt_storage.h:46
constexpr std::string_view rt_ion_plan_storage_uid
RT Ion Plan Storage SOP Class UID.
Definition rt_storage.h:66
constexpr std::string_view rt_dose_storage_uid
RT Dose Storage SOP Class UID.
Definition rt_storage.h:42
bool is_rt_plan_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is an RT Plan type.
rt_roi_interpreted_type
RT ROI Interpreted Type.
Definition rt_storage.h:290
constexpr std::string_view rt_plan_storage_uid
RT Plan Storage SOP Class UID.
Definition rt_storage.h:38
validation_result validate_rt_iod(const core::dicom_dataset &dataset)
Validate any RT dataset (auto-detects type) with default options.
bool is_valid_rt_dose_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid RT Dose.
bool is_valid_rt_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid RT object (any type)
validation_result validate_rt_plan_iod(const core::dicom_dataset &dataset)
Validate an RT Plan dataset with default options.
bool is_valid_rt_structure_set_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid RT Structure Set.
bool is_valid_rt_plan_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid RT Plan.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
validation_result validate_rt_structure_set_iod(const core::dicom_dataset &dataset)
Validate an RT Structure Set dataset with default options.
validation_result validate_rt_dose_iod(const core::dicom_dataset &dataset)
Validate an RT Dose dataset with default options.
Radiation Therapy (RT) IOD Validators.
Radiation Therapy (RT) Storage SOP Classes.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_rt_plan
Validate RT Plan specific attributes (beams, fractions)
bool validate_pixel_data
Validate pixel data consistency (for RT Dose and RT Image)
bool check_type1
Check Type 1 (required) attributes.
bool validate_references
Validate referenced objects (plans, images)
bool strict_mode
Strict mode - treat warnings as errors.
bool validate_rt_structure_set
Validate RT Structure Set specific attributes (ROIs, contours)
std::vector< validation_finding > findings
All findings during validation.
std::string_view name