PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
sr_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// SR-specific DICOM Tags
22// =============================================================================
23
24namespace sr_tags {
25
26// SR Document Series Module
27[[maybe_unused]] constexpr dicom_tag modality{0x0008, 0x0060};
28[[maybe_unused]] constexpr dicom_tag series_instance_uid{0x0020, 0x000E};
29
30// SR Document General Module
31constexpr dicom_tag instance_number{0x0020, 0x0013};
32constexpr dicom_tag completion_flag{0x0040, 0xA491};
33constexpr dicom_tag verification_flag{0x0040, 0xA493};
34constexpr dicom_tag content_date{0x0008, 0x0023};
35constexpr dicom_tag content_time{0x0008, 0x0033};
36constexpr dicom_tag verifying_observer_sequence{0x0040, 0xA073};
37[[maybe_unused]] constexpr dicom_tag predecessor_documents_sequence{0x0040, 0xA360};
38[[maybe_unused]] constexpr dicom_tag identical_documents_sequence{0x0040, 0xA525};
39
40// SR Document Content Module
41constexpr dicom_tag value_type{0x0040, 0xA040};
42constexpr dicom_tag concept_name_code_sequence{0x0040, 0xA043};
43constexpr dicom_tag content_sequence{0x0040, 0xA730};
44[[maybe_unused]] constexpr dicom_tag continuity_of_content{0x0040, 0xA050};
45
46// Content Item attributes
47constexpr dicom_tag relationship_type{0x0040, 0xA010};
48constexpr dicom_tag text_value{0x0040, 0xA160};
49constexpr dicom_tag measured_value_sequence{0x0040, 0xA300};
50constexpr dicom_tag numeric_value{0x0040, 0xA30A};
52
53// Code Sequence attributes
54constexpr dicom_tag code_value{0x0008, 0x0100};
55constexpr dicom_tag coding_scheme_designator{0x0008, 0x0102};
56constexpr dicom_tag code_meaning{0x0008, 0x0104};
57
58// Reference attributes
59constexpr dicom_tag referenced_sop_class_uid{0x0008, 0x1150};
60constexpr dicom_tag referenced_sop_instance_uid{0x0008, 0x1155};
61constexpr dicom_tag referenced_sop_sequence{0x0008, 0x1199};
62
63// Evidence sequences
66
67// Spatial coordinates
68constexpr dicom_tag graphic_data{0x0070, 0x0022};
69constexpr dicom_tag graphic_type{0x0070, 0x0023};
71
72// Temporal coordinates
73[[maybe_unused]] constexpr dicom_tag temporal_range_type{0x0040, 0xA130};
74[[maybe_unused]] constexpr dicom_tag referenced_sample_positions{0x0040, 0xA132};
75
76// Template identification
77[[maybe_unused]] constexpr dicom_tag template_identifier{0x0040, 0xDB00};
78[[maybe_unused]] constexpr dicom_tag mapping_resource{0x0008, 0x0105};
79
80// Key Object Selection specific
81[[maybe_unused]] constexpr dicom_tag referenced_series_sequence{0x0008, 0x1115};
82
83} // namespace sr_tags
84
85// =============================================================================
86// sr_iod_validator Implementation
87// =============================================================================
88
90 : options_(options) {}
91
93 // Detect SR type and delegate to appropriate validation
94 if (!dataset.contains(tags::sop_class_uid)) {
95 validation_result result;
96 result.is_valid = false;
97 result.findings.push_back({
100 "SOPClassUID is required to determine SR type",
101 "SR-ERR-000"
102 });
103 return result;
104 }
105
106 auto sop_class = dataset.get_string(tags::sop_class_uid);
107 auto doc_type = sop_classes::get_sr_document_type(sop_class);
108
109 switch (doc_type) {
111 return validate_basic_text_sr(dataset);
113 return validate_enhanced_sr(dataset);
116 return validate_comprehensive_sr(dataset);
118 return validate_key_object_selection(dataset);
119 default:
120 // For other types, use comprehensive validation
121 return validate_comprehensive_sr(dataset);
122 }
123}
124
127 validation_result result;
128 result.is_valid = true;
129
130 // Common modules
131 validate_patient_module(dataset, result.findings);
137 validate_sop_common_module(dataset, result.findings);
138
139 // Basic Text SR does not support SCOORD, IMAGE, WAVEFORM, etc.
140 // Content validation will handle value type restrictions
141
142 for (const auto& finding : result.findings) {
143 if (finding.severity == validation_severity::error) {
144 result.is_valid = false;
145 break;
146 }
147 if (options_.strict_mode && finding.severity == validation_severity::warning) {
148 result.is_valid = false;
149 break;
150 }
151 }
152
153 return result;
154}
155
158 validation_result result;
159 result.is_valid = true;
160
161 // Common modules
162 validate_patient_module(dataset, result.findings);
168 validate_sop_common_module(dataset, result.findings);
169
170 // Enhanced SR specific - evidence sequences
172 validate_evidence_sequences(dataset, result.findings);
173 }
174
175 for (const auto& finding : result.findings) {
176 if (finding.severity == validation_severity::error) {
177 result.is_valid = false;
178 break;
179 }
180 if (options_.strict_mode && finding.severity == validation_severity::warning) {
181 result.is_valid = false;
182 break;
183 }
184 }
185
186 return result;
187}
188
191 validation_result result;
192 result.is_valid = true;
193
194 // Common modules
195 validate_patient_module(dataset, result.findings);
201 validate_sop_common_module(dataset, result.findings);
202
203 // Comprehensive SR specific - evidence sequences
205 validate_evidence_sequences(dataset, result.findings);
206 }
207
208 // Content tree validation (includes SCOORD support)
210 validate_content_sequence(dataset, result.findings);
211 }
212
213 for (const auto& finding : result.findings) {
214 if (finding.severity == validation_severity::error) {
215 result.is_valid = false;
216 break;
217 }
218 if (options_.strict_mode && finding.severity == validation_severity::warning) {
219 result.is_valid = false;
220 break;
221 }
222 }
223
224 return result;
225}
226
229 validation_result result;
230 result.is_valid = true;
231
233 return result;
234 }
235
236 // Common modules
237 validate_patient_module(dataset, result.findings);
243 validate_sop_common_module(dataset, result.findings);
244
245 // KOS must have Current Requested Procedure Evidence Sequence
246 if (options_.check_type1) {
248 result.findings.push_back({
251 "CurrentRequestedProcedureEvidenceSequence is required for Key Object Selection",
252 "SR-KOS-ERR-001"
253 });
254 }
255 }
256
257 for (const auto& finding : result.findings) {
258 if (finding.severity == validation_severity::error) {
259 result.is_valid = false;
260 break;
261 }
262 if (options_.strict_mode && finding.severity == validation_severity::warning) {
263 result.is_valid = false;
264 break;
265 }
266 }
267
268 return result;
269}
270
273 validation_result result;
274 result.is_valid = true;
275
276 validate_content_sequence(dataset, result.findings);
277
278 for (const auto& finding : result.findings) {
279 if (finding.severity == validation_severity::error) {
280 result.is_valid = false;
281 break;
282 }
283 }
284
285 return result;
286}
287
290 validation_result result;
291 result.is_valid = true;
292
293 validate_evidence_sequences(dataset, result.findings);
294
295 for (const auto& finding : result.findings) {
296 if (finding.severity == validation_severity::error) {
297 result.is_valid = false;
298 break;
299 }
300 }
301
302 return result;
303}
304
306 // Check essential Type 1 attributes
307
308 // General Study Module
309 if (!dataset.contains(tags::study_instance_uid)) return false;
310
311 // General Series Module
312 if (!dataset.contains(tags::modality)) return false;
313 if (!dataset.contains(tags::series_instance_uid)) return false;
314
315 // Check modality is SR
316 auto modality = dataset.get_string(tags::modality);
317 if (modality != "SR") return false;
318
319 // SR Document General Module
320 if (!dataset.contains(sr_tags::completion_flag)) return false;
321 if (!dataset.contains(sr_tags::verification_flag)) return false;
322
323 // SR Document Content Module - root content item
324 if (!dataset.contains(sr_tags::value_type)) return false;
325 if (!dataset.contains(sr_tags::concept_name_code_sequence)) return false;
326
327 // SOP Common Module
328 if (!dataset.contains(tags::sop_class_uid)) return false;
329 if (!dataset.contains(tags::sop_instance_uid)) return false;
330
331 // Verify SOP Class is SR
332 auto sop_class = dataset.get_string(tags::sop_class_uid);
333 if (!sop_classes::is_sr_storage_sop_class(sop_class)) return false;
334
335 return true;
336}
337
339 return options_;
340}
341
345
346// =============================================================================
347// Module Validation Methods
348// =============================================================================
349
351 const dicom_dataset& dataset,
352 std::vector<validation_finding>& findings) const {
353
354 if (options_.check_type2) {
355 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
356 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
357 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
358 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
359 }
360}
361
363 const dicom_dataset& dataset,
364 std::vector<validation_finding>& findings) const {
365
366 if (options_.check_type1) {
367 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
368 }
369
370 if (options_.check_type2) {
371 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
372 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
373 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
374 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
375 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
376 }
377}
378
380 const dicom_dataset& dataset,
381 std::vector<validation_finding>& findings) const {
382
383 if (options_.check_type1) {
384 check_type1_attribute(dataset, tags::modality, "Modality", findings);
385 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
386 check_modality(dataset, findings);
387 }
388
389 if (options_.check_type2) {
390 check_type2_attribute(dataset, tags::series_number, "SeriesNumber", findings);
391 }
392}
393
395 const dicom_dataset& dataset,
396 std::vector<validation_finding>& findings) const {
397
398 constexpr dicom_tag manufacturer{0x0008, 0x0070};
399 if (options_.check_type2) {
400 check_type2_attribute(dataset, manufacturer, "Manufacturer", findings);
401 }
402}
403
405 const dicom_dataset& dataset,
406 std::vector<validation_finding>& findings) const {
407
408 if (options_.check_type1) {
409 check_type1_attribute(dataset, sr_tags::instance_number, "InstanceNumber", findings);
410 check_type1_attribute(dataset, sr_tags::completion_flag, "CompletionFlag", findings);
411 check_type1_attribute(dataset, sr_tags::verification_flag, "VerificationFlag", findings);
412 }
413
414 if (options_.check_type2) {
415 check_type2_attribute(dataset, sr_tags::content_date, "ContentDate", findings);
416 check_type2_attribute(dataset, sr_tags::content_time, "ContentTime", findings);
417 }
418
419 // Validate flag values
421 check_completion_flag(dataset, findings);
422 check_verification_flag(dataset, findings);
423 }
424
425 // VerifyingObserverSequence is Type 1C - required if VerificationFlag is VERIFIED
427 auto flag = dataset.get_string(sr_tags::verification_flag);
428 if (flag == "VERIFIED" && !dataset.contains(sr_tags::verifying_observer_sequence)) {
429 findings.push_back({
432 "VerifyingObserverSequence is required when VerificationFlag is VERIFIED",
433 "SR-DOC-ERR-001"
434 });
435 }
436 }
437}
438
440 const dicom_dataset& dataset,
441 std::vector<validation_finding>& findings) const {
442
443 // Root content item attributes
444 if (options_.check_type1) {
445 check_type1_attribute(dataset, sr_tags::value_type, "ValueType", findings);
446 check_type1_attribute(dataset, sr_tags::concept_name_code_sequence, "ConceptNameCodeSequence", findings);
447 }
448
449 // Validate root value type
451 auto value_type = dataset.get_string(sr_tags::value_type);
452 if (value_type != "CONTAINER") {
453 findings.push_back({
456 "Root content item ValueType must be CONTAINER, found: " + value_type,
457 "SR-CONTENT-ERR-001"
458 });
459 }
460 }
461
462 // Validate Concept Name Code Sequence
464 const auto* concept_elem = dataset.get(sr_tags::concept_name_code_sequence);
465 if (concept_elem && concept_elem->is_sequence() && !concept_elem->sequence_items().empty()) {
466 validate_coded_entry(concept_elem->sequence_items()[0], "Root Concept Name", findings);
467 }
468 }
469
470 // Validate content sequence
472 validate_content_sequence(dataset, findings);
473 }
474}
475
477 const dicom_dataset& dataset,
478 std::vector<validation_finding>& findings) const {
479
480 if (options_.check_type1) {
481 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
482 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
483 }
484
485 // Validate SOP Class UID is an SR storage class
486 if (dataset.contains(tags::sop_class_uid)) {
487 auto sop_class = dataset.get_string(tags::sop_class_uid);
488 if (!sop_classes::is_sr_storage_sop_class(sop_class)) {
489 findings.push_back({
492 "SOPClassUID is not a recognized SR Storage SOP Class: " + sop_class,
493 "SR-ERR-001"
494 });
495 }
496 }
497}
498
500 const dicom_dataset& dataset,
501 std::vector<validation_finding>& findings) const {
502
503 // For Enhanced/Comprehensive SR, at least one evidence sequence should be present
506
507 if (!has_evidence) {
508 findings.push_back({
511 "No evidence sequences present - SR may not properly link to source images",
512 "SR-EVIDENCE-INFO-001"
513 });
514 }
515}
516
518 const dicom_dataset& dataset,
519 std::vector<validation_finding>& findings) const {
520
521 if (!dataset.contains(sr_tags::content_sequence)) {
522 findings.push_back({
525 "ContentSequence not present - SR document has no content items",
526 "SR-CONTENT-INFO-001"
527 });
528 return;
529 }
530
531 const auto* content_elem = dataset.get(sr_tags::content_sequence);
532 if (!content_elem || !content_elem->is_sequence() || content_elem->sequence_items().empty()) {
533 findings.push_back({
536 "ContentSequence is empty",
537 "SR-CONTENT-INFO-002"
538 });
539 return;
540 }
541
542 // Get root value type for context
543 auto root_value_type = dataset.get_string(sr_tags::value_type);
544
545 // Validate each content item
546 const auto& content_seq = content_elem->sequence_items();
547 for (size_t i = 0; i < content_seq.size(); ++i) {
548 validate_content_item(content_seq[i], 1, root_value_type, findings);
549 }
550}
551
553 const dicom_dataset& item,
554 size_t depth,
555 [[maybe_unused]] std::string_view parent_value_type,
556 std::vector<validation_finding>& findings) const {
557
558 // Prevent infinite recursion
559 if (depth > 100) {
560 findings.push_back({
563 "Content tree depth exceeds 100 - possible circular reference",
564 "SR-CONTENT-WARN-001"
565 });
566 return;
567 }
568
569 // Check required attributes
570 if (!item.contains(sr_tags::relationship_type)) {
571 findings.push_back({
574 "RelationshipType missing in content item at depth " + std::to_string(depth),
575 "SR-ITEM-ERR-001"
576 });
577 }
578
579 if (!item.contains(sr_tags::value_type)) {
580 findings.push_back({
583 "ValueType missing in content item at depth " + std::to_string(depth),
584 "SR-ITEM-ERR-002"
585 });
586 return; // Can't validate further without value type
587 }
588
589 auto value_type = item.get_string(sr_tags::value_type);
590 auto sr_vt = sop_classes::parse_sr_value_type(value_type);
591
593 findings.push_back({
596 "Invalid ValueType: " + value_type,
597 "SR-ITEM-ERR-003"
598 });
599 return;
600 }
601
602 // Value type specific validation
603 switch (sr_vt) {
605 validate_text_content_item(item, findings);
606 break;
608 validate_code_content_item(item, findings);
609 break;
611 validate_num_content_item(item, findings);
612 break;
614 validate_image_content_item(item, findings);
615 break;
617 validate_scoord_content_item(item, findings);
618 break;
620 validate_scoord3d_content_item(item, findings);
621 break;
622 default:
623 break;
624 }
625
626 // Validate nested content items
627 if (item.contains(sr_tags::content_sequence)) {
628 const auto* nested_elem = item.get(sr_tags::content_sequence);
629 if (nested_elem && nested_elem->is_sequence()) {
630 const auto& nested_seq = nested_elem->sequence_items();
631 for (size_t i = 0; i < nested_seq.size(); ++i) {
632 validate_content_item(nested_seq[i], depth + 1, value_type, findings);
633 }
634 }
635 }
636}
637
639 const dicom_dataset& coded_entry,
640 std::string_view context,
641 std::vector<validation_finding>& findings) const {
642
643 // Code Value
644 if (!coded_entry.contains(sr_tags::code_value)) {
645 findings.push_back({
648 std::string(context) + ": CodeValue is required",
649 "SR-CODE-ERR-001"
650 });
651 }
652
653 // Coding Scheme Designator
655 findings.push_back({
658 std::string(context) + ": CodingSchemeDesignator is required",
659 "SR-CODE-ERR-002"
660 });
661 }
662
663 // Code Meaning
664 if (!coded_entry.contains(sr_tags::code_meaning)) {
665 findings.push_back({
668 std::string(context) + ": CodeMeaning is required",
669 "SR-CODE-ERR-003"
670 });
671 }
672}
673
675 const dicom_dataset& item,
676 std::vector<validation_finding>& findings) const {
677
678 if (!item.contains(sr_tags::text_value)) {
679 findings.push_back({
682 "TextValue is required for TEXT content item",
683 "SR-TEXT-ERR-001"
684 });
685 }
686}
687
689 const dicom_dataset& item,
690 std::vector<validation_finding>& findings) const {
691
692 if (!item.contains(sr_tags::concept_name_code_sequence)) {
693 findings.push_back({
696 "ConceptNameCodeSequence is required for CODE content item",
697 "SR-CODE-ITEM-ERR-001"
698 });
699 }
700}
701
703 const dicom_dataset& item,
704 std::vector<validation_finding>& findings) const {
705
706 if (!item.contains(sr_tags::measured_value_sequence)) {
707 findings.push_back({
710 "MeasuredValueSequence is required for NUM content item",
711 "SR-NUM-ERR-001"
712 });
713 return;
714 }
715
716 const auto* measured_elem = item.get(sr_tags::measured_value_sequence);
717 if (measured_elem && measured_elem->is_sequence() && !measured_elem->sequence_items().empty()) {
718 const auto& mv_item = measured_elem->sequence_items()[0];
719
720 if (!mv_item.contains(sr_tags::numeric_value)) {
721 findings.push_back({
724 "NumericValue is required in MeasuredValueSequence",
725 "SR-NUM-ERR-002"
726 });
727 }
728
729 if (!mv_item.contains(sr_tags::measurement_units_code_sequence)) {
730 findings.push_back({
733 "MeasurementUnitsCodeSequence is required in MeasuredValueSequence",
734 "SR-NUM-ERR-003"
735 });
736 }
737 }
738}
739
741 const dicom_dataset& item,
742 std::vector<validation_finding>& findings) const {
743
744 if (!item.contains(sr_tags::referenced_sop_sequence)) {
745 findings.push_back({
748 "ReferencedSOPSequence is required for IMAGE content item",
749 "SR-IMAGE-ERR-001"
750 });
751 return;
752 }
753
754 const auto* ref_elem = item.get(sr_tags::referenced_sop_sequence);
755 if (ref_elem && ref_elem->is_sequence() && !ref_elem->sequence_items().empty()) {
756 const auto& ref_item = ref_elem->sequence_items()[0];
757
758 if (!ref_item.contains(sr_tags::referenced_sop_class_uid)) {
759 findings.push_back({
762 "ReferencedSOPClassUID is required in IMAGE reference",
763 "SR-IMAGE-ERR-002"
764 });
765 }
766
767 if (!ref_item.contains(sr_tags::referenced_sop_instance_uid)) {
768 findings.push_back({
771 "ReferencedSOPInstanceUID is required in IMAGE reference",
772 "SR-IMAGE-ERR-003"
773 });
774 }
775 }
776}
777
779 const dicom_dataset& item,
780 std::vector<validation_finding>& findings) const {
781
782 if (!item.contains(sr_tags::graphic_type)) {
783 findings.push_back({
786 "GraphicType is required for SCOORD content item",
787 "SR-SCOORD-ERR-001"
788 });
789 }
790
791 if (!item.contains(sr_tags::graphic_data)) {
792 findings.push_back({
795 "GraphicData is required for SCOORD content item",
796 "SR-SCOORD-ERR-002"
797 });
798 }
799}
800
802 const dicom_dataset& item,
803 std::vector<validation_finding>& findings) const {
804
805 if (!item.contains(sr_tags::graphic_type)) {
806 findings.push_back({
809 "GraphicType is required for SCOORD3D content item",
810 "SR-SCOORD3D-ERR-001"
811 });
812 }
813
814 if (!item.contains(sr_tags::graphic_data)) {
815 findings.push_back({
818 "GraphicData is required for SCOORD3D content item",
819 "SR-SCOORD3D-ERR-002"
820 });
821 }
822
823 if (!item.contains(sr_tags::referenced_frame_of_reference_uid)) {
824 findings.push_back({
827 "ReferencedFrameOfReferenceUID is required for SCOORD3D content item",
828 "SR-SCOORD3D-ERR-003"
829 });
830 }
831}
832
833// =============================================================================
834// Attribute Validation Helpers
835// =============================================================================
836
838 const dicom_dataset& dataset,
839 dicom_tag tag,
840 std::string_view name,
841 std::vector<validation_finding>& findings) const {
842
843 if (!dataset.contains(tag)) {
844 findings.push_back({
846 tag,
847 std::string("Type 1 attribute missing: ") + std::string(name) +
848 " (" + tag.to_string() + ")",
849 "SR-TYPE1-MISSING"
850 });
851 } else {
852 const auto* element = dataset.get(tag);
853 if (element != nullptr) {
854 // For sequences, check if the sequence has items
855 if (element->is_sequence()) {
856 if (element->sequence_items().empty()) {
857 findings.push_back({
859 tag,
860 std::string("Type 1 sequence has no items: ") +
861 std::string(name) + " (" + tag.to_string() + ")",
862 "SR-TYPE1-EMPTY"
863 });
864 }
865 } else {
866 // For non-sequence elements, check if the value is empty
867 auto value = dataset.get_string(tag);
868 if (value.empty()) {
869 findings.push_back({
871 tag,
872 std::string("Type 1 attribute has empty value: ") +
873 std::string(name) + " (" + tag.to_string() + ")",
874 "SR-TYPE1-EMPTY"
875 });
876 }
877 }
878 }
879 }
880}
881
883 const dicom_dataset& dataset,
884 dicom_tag tag,
885 std::string_view name,
886 std::vector<validation_finding>& findings) const {
887
888 if (!dataset.contains(tag)) {
889 findings.push_back({
891 tag,
892 std::string("Type 2 attribute missing: ") + std::string(name) +
893 " (" + tag.to_string() + ")",
894 "SR-TYPE2-MISSING"
895 });
896 }
897}
898
900 const dicom_dataset& dataset,
901 std::vector<validation_finding>& findings) const {
902
903 if (!dataset.contains(tags::modality)) {
904 return;
905 }
906
907 auto modality = dataset.get_string(tags::modality);
908 if (modality != "SR") {
909 findings.push_back({
912 "Modality must be 'SR' for Structured Report objects, found: " + modality,
913 "SR-ERR-002"
914 });
915 }
916}
917
919 const dicom_dataset& dataset,
920 std::vector<validation_finding>& findings) const {
921
922 if (!dataset.contains(sr_tags::completion_flag)) {
923 return;
924 }
925
926 auto flag = dataset.get_string(sr_tags::completion_flag);
927 if (flag != "PARTIAL" && flag != "COMPLETE") {
928 findings.push_back({
931 "Invalid CompletionFlag value: " + flag + " (must be PARTIAL or COMPLETE)",
932 "SR-FLAG-ERR-001"
933 });
934 }
935}
936
938 const dicom_dataset& dataset,
939 std::vector<validation_finding>& findings) const {
940
941 if (!dataset.contains(sr_tags::verification_flag)) {
942 return;
943 }
944
945 auto flag = dataset.get_string(sr_tags::verification_flag);
946 if (flag != "UNVERIFIED" && flag != "VERIFIED") {
947 findings.push_back({
950 "Invalid VerificationFlag value: " + flag + " (must be UNVERIFIED or VERIFIED)",
951 "SR-FLAG-ERR-002"
952 });
953 }
954}
955
956// =============================================================================
957// Convenience Functions
958// =============================================================================
959
961 sr_iod_validator validator;
962 return validator.validate(dataset);
963}
964
965bool is_valid_sr_dataset(const dicom_dataset& dataset) {
966 sr_iod_validator validator;
967 return validator.quick_check(dataset);
968}
969
970bool is_sr_complete(const dicom_dataset& dataset) {
971 constexpr dicom_tag completion_flag{0x0040, 0xA491};
972 if (!dataset.contains(completion_flag)) {
973 return false;
974 }
975 return dataset.get_string(completion_flag) == "COMPLETE";
976}
977
978bool is_sr_verified(const dicom_dataset& dataset) {
979 constexpr dicom_tag verification_flag{0x0040, 0xA493};
980 if (!dataset.contains(verification_flag)) {
981 return false;
982 }
983 return dataset.get_string(verification_flag) == "VERIFIED";
984}
985
986size_t get_content_item_count(const dicom_dataset& dataset) {
987 constexpr dicom_tag content_sequence{0x0040, 0xA730};
988 const auto* element = dataset.get(content_sequence);
989 if (element && element->is_sequence()) {
990 return element->sequence_items().size();
991 }
992 return 0;
993}
994
995std::string get_sr_document_title(const dicom_dataset& dataset) {
996 constexpr dicom_tag concept_name_code_sequence{0x0040, 0xA043};
997 constexpr dicom_tag code_meaning{0x0008, 0x0104};
998
999 const auto* element = dataset.get(concept_name_code_sequence);
1000 if (element && element->is_sequence() && !element->sequence_items().empty()) {
1001 const auto& item = element->sequence_items()[0];
1002 if (item.contains(code_meaning)) {
1003 return item.get_string(code_meaning);
1004 }
1005 }
1006 return "";
1007}
1008
1009} // namespace kcenon::pacs::services::validation
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
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 validate_image_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
sr_iod_validator()=default
Construct validator with default options.
const sr_validation_options & options() const noexcept
Get the validation options.
void check_verification_flag(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_num_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against SR IOD.
void validate_coded_entry(const core::dicom_dataset &coded_entry, std::string_view context, std::vector< validation_finding > &findings) const
validation_result validate_key_object_selection(const core::dicom_dataset &dataset) const
Validate a Key Object Selection document.
void validate_text_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
void set_options(const sr_validation_options &options)
Set validation options.
void validate_evidence_sequences(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_scoord_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required attributes.
void check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_content_tree(const core::dicom_dataset &dataset) const
Validate content tree structure.
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_content_item(const core::dicom_dataset &item, size_t depth, std::string_view parent_value_type, std::vector< validation_finding > &findings) const
void validate_code_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
void validate_scoord3d_content_item(const core::dicom_dataset &item, std::vector< validation_finding > &findings) const
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_basic_text_sr(const core::dicom_dataset &dataset) const
Validate a Basic Text SR dataset.
void validate_content_sequence(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_completion_flag(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_sr_document_general_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_comprehensive_sr(const core::dicom_dataset &dataset) const
Validate a Comprehensive SR dataset.
void validate_sr_document_content_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
validation_result validate_references(const core::dicom_dataset &dataset) const
Validate referenced instances.
void validate_sr_document_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_enhanced_sr(const core::dicom_dataset &dataset) const
Validate an Enhanced SR dataset.
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_equipment_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) 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 series_number
Series Number.
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.
sr_document_type get_sr_document_type(std::string_view uid) noexcept
Get SR document type for a SOP Class UID.
bool is_sr_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is an SR Storage SOP Class.
@ enhanced
Enhanced SR - References to images/waveforms.
@ comprehensive
Comprehensive SR - Complex with spatial coords.
@ comprehensive_3d
Comprehensive 3D SR - 3D spatial coordinates.
@ basic_text
Basic Text SR - Simple text reports.
@ key_object_selection
Key Object Selection - Image selection.
sr_value_type parse_sr_value_type(std::string_view value) noexcept
Parse SR value type from DICOM string.
@ scoord
SCOORD - Spatial coordinates (2D)
@ scoord3d
SCOORD3D - Spatial coordinates (3D)
constexpr dicom_tag current_requested_procedure_evidence_sequence
size_t get_content_item_count(const core::dicom_dataset &dataset)
Get content item count from dataset.
bool is_sr_complete(const core::dicom_dataset &dataset)
Check if SR document is complete.
validation_result validate_sr_iod(const core::dicom_dataset &dataset)
Validate an SR dataset with default options.
bool is_sr_verified(const core::dicom_dataset &dataset)
Check if SR document is verified.
std::string get_sr_document_title(const core::dicom_dataset &dataset)
Get SR document title from Concept Name Code Sequence.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool is_valid_sr_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid SR document.
Structured Report IOD Validator.
Structured Report (SR) Storage SOP Classes.
bool validate_key_object_selection
Allow Key Object Selection document specific validation.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_coded_entries
Validate coded entries (concept name codes, etc.)
bool strict_mode
Strict mode - treat warnings as errors.
bool validate_value_types
Validate content item value types.
bool check_type1
Check Type 1 (required) attributes.
bool validate_references
Validate referenced SOP instances.
bool validate_content_sequence
Validate Content Sequence structure.
bool validate_document_status
Validate completion and verification flags.
bool check_type2
Check Type 2 (required, can be empty) attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name