PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
mg_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;
20
21// =============================================================================
22// Mammography-Specific DICOM Tags
23// =============================================================================
24
25namespace mg_tags {
26
27// Laterality Tags
28inline constexpr dicom_tag laterality{0x0020, 0x0060};
29inline constexpr dicom_tag image_laterality{0x0020, 0x0062};
30
31// Mammography Series Module
32inline constexpr dicom_tag modality{0x0008, 0x0060};
33inline constexpr dicom_tag request_attributes_sequence{0x0040, 0x0275};
34
35// Mammography Image Module
36inline constexpr dicom_tag view_position{0x0018, 0x5101};
37inline constexpr dicom_tag view_code_sequence{0x0054, 0x0220};
38inline constexpr dicom_tag image_type{0x0008, 0x0008};
39inline constexpr dicom_tag presentation_intent_type{0x0008, 0x0068};
40inline constexpr dicom_tag partial_view{0x0028, 0x1350};
41inline constexpr dicom_tag partial_view_description{0x0028, 0x1351};
42inline constexpr dicom_tag partial_view_code_sequence{0x0028, 0x1352};
43
44// DX Anatomy Imaged Module
45inline constexpr dicom_tag body_part_examined{0x0018, 0x0015};
46inline constexpr dicom_tag anatomic_region_sequence{0x0008, 0x2218};
47
48// X-Ray Acquisition Dose Module
49inline constexpr dicom_tag compression_force{0x0018, 0x11A2};
50inline constexpr dicom_tag body_part_thickness{0x0018, 0x11A0};
51inline constexpr dicom_tag measured_ap_dimension{0x0010, 0x1023};
52inline constexpr dicom_tag entrance_dose{0x0040, 0x0302};
53inline constexpr dicom_tag entrance_dose_derivation{0x0040, 0x0303};
54inline constexpr dicom_tag organ_dose{0x0040, 0x0316};
55inline constexpr dicom_tag half_value_layer{0x0040, 0x0314};
56inline constexpr dicom_tag relative_x_ray_exposure{0x0018, 0x1405};
57
58// DX Detector Module
59inline constexpr dicom_tag detector_type{0x0018, 0x7004};
60inline constexpr dicom_tag detector_id{0x0018, 0x700A};
61inline constexpr dicom_tag imager_pixel_spacing{0x0018, 0x1164};
62
63// Exposure Module
64inline constexpr dicom_tag kvp{0x0018, 0x0060};
65inline constexpr dicom_tag exposure_time{0x0018, 0x1150};
66inline constexpr dicom_tag exposure{0x0018, 0x1152};
67inline constexpr dicom_tag exposure_in_uas{0x0018, 0x1153};
68inline constexpr dicom_tag tube_current{0x0018, 0x1151};
69inline constexpr dicom_tag anode_target_material{0x0018, 0x1191};
70inline constexpr dicom_tag filter_material{0x0018, 0x7050};
71inline constexpr dicom_tag filter_thickness{0x0018, 0x7052};
72inline constexpr dicom_tag focal_spot{0x0018, 0x1190};
73
74// Breast Implant Module
75inline constexpr dicom_tag breast_implant_present{0x0028, 0x1300};
76
77// Image Pixel Module
78inline constexpr dicom_tag pixel_intensity_relationship{0x0028, 0x1040};
79inline constexpr dicom_tag pixel_intensity_relationship_sign{0x0028, 0x1041};
80
81} // namespace mg_tags
82
83// =============================================================================
84// mg_iod_validator Implementation
85// =============================================================================
86
88 : options_(options) {}
89
91 validation_result result;
92 result.is_valid = true;
93
94 // Validate standard modules
96 validate_patient_module(dataset, result.findings);
99 validate_sop_common_module(dataset, result.findings);
100 }
101
102 // Validate mammography series module
104
106 validate_image_pixel_module(dataset, result.findings);
107 }
108
112 validate_dx_detector_module(dataset, result.findings);
113 }
114
117 }
118
121 }
122
123 // Check for errors
124 for (const auto& finding : result.findings) {
125 if (finding.severity == validation_severity::error) {
126 result.is_valid = false;
127 break;
128 }
129 if (options_.strict_mode && finding.severity == validation_severity::warning) {
130 result.is_valid = false;
131 break;
132 }
133 }
134
135 return result;
136}
137
140 auto result = validate(dataset);
141
142 // Additional For Presentation validation
144 validate_voi_lut_module(dataset, result.findings);
145
146 // Check Presentation Intent Type
148 auto intent = dataset.get_string(mg_tags::presentation_intent_type);
149 if (intent != "FOR PRESENTATION") {
150 result.findings.push_back({
153 "Presentation Intent Type should be 'FOR PRESENTATION' "
154 "for this SOP Class, found: " + intent,
155 "MG-ERR-010"
156 });
157 }
158 }
159 }
160
161 // Re-check validity
162 for (const auto& finding : result.findings) {
163 if (finding.severity == validation_severity::error) {
164 result.is_valid = false;
165 break;
166 }
167 }
168
169 return result;
170}
171
174 auto result = validate(dataset);
175
176 // Additional For Processing validation
178 // Check Presentation Intent Type
180 auto intent = dataset.get_string(mg_tags::presentation_intent_type);
181 if (intent != "FOR PROCESSING") {
182 result.findings.push_back({
185 "Presentation Intent Type should be 'FOR PROCESSING' "
186 "for this SOP Class, found: " + intent,
187 "MG-ERR-011"
188 });
189 }
190 }
191
192 // For Processing images should have linear pixel intensity relationship
194 auto relationship = dataset.get_string(mg_tags::pixel_intensity_relationship);
195 if (relationship != "LIN") {
196 result.findings.push_back({
199 "For Processing mammography images typically have linear (LIN) "
200 "pixel intensity relationship, found: " + relationship,
201 "MG-INFO-002"
202 });
203 }
204 }
205 }
206
207 // Re-check validity
208 for (const auto& finding : result.findings) {
209 if (finding.severity == validation_severity::error) {
210 result.is_valid = false;
211 break;
212 }
213 }
214
215 return result;
216}
217
220 validation_result result;
221 result.is_valid = true;
222
223 check_laterality_consistency(dataset, result.findings);
224
225 for (const auto& finding : result.findings) {
226 if (finding.severity == validation_severity::error) {
227 result.is_valid = false;
228 break;
229 }
230 }
231
232 return result;
233}
234
237 validation_result result;
238 result.is_valid = true;
239
240 check_view_position_validity(dataset, result.findings);
241
242 for (const auto& finding : result.findings) {
243 if (finding.severity == validation_severity::error) {
244 result.is_valid = false;
245 break;
246 }
247 }
248
249 return result;
250}
251
254 validation_result result;
255 result.is_valid = true;
256
258
259 for (const auto& finding : result.findings) {
260 if (finding.severity == validation_severity::error) {
261 result.is_valid = false;
262 break;
263 }
264 }
265
266 return result;
267}
268
270 // Check only critical Type 1 attributes for quick validation
271
272 // General Study Module Type 1
273 if (!dataset.contains(tags::study_instance_uid)) return false;
274
275 // General Series Module Type 1
276 if (!dataset.contains(tags::modality)) return false;
277 if (!dataset.contains(tags::series_instance_uid)) return false;
278
279 // Check modality is MG
280 auto modality = dataset.get_string(tags::modality);
281 if (modality != "MG") return false;
282
283 // Image Pixel Module Type 1
284 if (!dataset.contains(tags::samples_per_pixel)) return false;
285 if (!dataset.contains(tags::photometric_interpretation)) return false;
286 if (!dataset.contains(tags::rows)) return false;
287 if (!dataset.contains(tags::columns)) return false;
288 if (!dataset.contains(tags::bits_allocated)) return false;
289 if (!dataset.contains(tags::bits_stored)) return false;
290 if (!dataset.contains(tags::high_bit)) return false;
291 if (!dataset.contains(tags::pixel_representation)) return false;
292 if (!dataset.contains(tags::pixel_data)) return false;
293
294 // SOP Common Module Type 1
295 if (!dataset.contains(tags::sop_class_uid)) return false;
296 if (!dataset.contains(tags::sop_instance_uid)) return false;
297
298 // Mammography-specific: Laterality should be present
299 if (!dataset.contains(mg_tags::laterality) &&
301 return false;
302 }
303
304 return true;
305}
306
308 return options_;
309}
310
314
315// =============================================================================
316// Module Validation Methods
317// =============================================================================
318
320 const dicom_dataset& dataset,
321 std::vector<validation_finding>& findings) const {
322
323 // Patient Module - All attributes are Type 2
324 if (options_.check_type2) {
325 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
326 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
327 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
328 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
329 }
330}
331
333 const dicom_dataset& dataset,
334 std::vector<validation_finding>& findings) const {
335
336 // Type 1
337 if (options_.check_type1) {
338 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
339 }
340
341 // Type 2
342 if (options_.check_type2) {
343 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
344 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
345 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
346 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
347 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
348 }
349}
350
352 const dicom_dataset& dataset,
353 std::vector<validation_finding>& findings) const {
354
355 // Type 1
356 if (options_.check_type1) {
357 check_type1_attribute(dataset, tags::modality, "Modality", findings);
358 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
359
360 // Special check: Modality must be "MG"
361 check_modality(dataset, findings);
362 }
363
364 // Type 2
365 if (options_.check_type2) {
366 check_type2_attribute(dataset, tags::series_number, "SeriesNumber", findings);
367 }
368}
369
371 const dicom_dataset& dataset,
372 std::vector<validation_finding>& findings) const {
373
374 // Mammography Series Module includes Request Attributes Sequence (0040,0275)
375 // which is Type 3, so no validation required but informational
376
378 // The Request Attributes Sequence is useful for workflow integration
380 findings.push_back({
383 "Request Attributes Sequence not present - may limit workflow integration",
384 "MG-INFO-001"
385 });
386 }
387 }
388}
389
391 const dicom_dataset& dataset,
392 std::vector<validation_finding>& findings) const {
393
394 // Image Type (0008,0008) - Type 1
395 if (options_.check_type1) {
396 check_type1_attribute(dataset, mg_tags::image_type, "ImageType", findings);
397
398 if (dataset.contains(mg_tags::image_type)) {
399 auto image_type = dataset.get_string(mg_tags::image_type);
400 // First value should be ORIGINAL or DERIVED
401 if (image_type.find("ORIGINAL") == std::string::npos &&
402 image_type.find("DERIVED") == std::string::npos) {
403 findings.push_back({
406 "Image Type first value should be ORIGINAL or DERIVED",
407 "MG-WARN-002"
408 });
409 }
410 }
411 }
412
413 // Validate laterality if enabled
415 check_laterality_consistency(dataset, findings);
416 }
417
418 // Validate view position if enabled
420 check_view_position_validity(dataset, findings);
421 }
422
423 // Partial View (0028,1350) - Type 3
424 if (dataset.contains(mg_tags::partial_view)) {
426 if (partial == "YES" && !dataset.contains(mg_tags::partial_view_description) &&
428 findings.push_back({
431 "Partial View is YES but no description or code sequence provided",
432 "MG-INFO-003"
433 });
434 }
435 }
436}
437
439 const dicom_dataset& dataset,
440 std::vector<validation_finding>& findings) const {
441
442 // Body Part Examined (0018,0015) - Type 2
443 if (options_.check_type2) {
444 check_type2_attribute(dataset, mg_tags::body_part_examined, "BodyPartExamined", findings);
445
447 auto body_part = dataset.get_string(mg_tags::body_part_examined);
448 if (body_part != "BREAST") {
449 findings.push_back({
452 "Body Part Examined should be 'BREAST' for mammography, found: " + body_part,
453 "MG-WARN-003"
454 });
455 }
456 }
457 }
458
459 // Anatomic Region Sequence (0008,2218) - Type 1C
463 findings.push_back({
466 "Neither Body Part Examined nor Anatomic Region Sequence present - "
467 "anatomy information may be insufficient for clinical use",
468 "MG-WARN-004"
469 });
470 }
471 }
472}
473
475 const dicom_dataset& dataset,
476 std::vector<validation_finding>& findings) const {
477
478 // Detector Type (0018,7004) - Type 2
479 if (options_.check_type2) {
480 check_type2_attribute(dataset, mg_tags::detector_type, "DetectorType", findings);
481
482 if (dataset.contains(mg_tags::detector_type)) {
483 auto type = dataset.get_string(mg_tags::detector_type);
484 // Mammography typically uses DIRECT (a-Se) or SCINTILLATOR (CsI)
485 if (type != "DIRECT" && type != "SCINTILLATOR" && type != "STORAGE") {
486 findings.push_back({
489 "Unusual Detector Type for mammography: " + type +
490 " (typical: DIRECT, SCINTILLATOR)",
491 "MG-INFO-004"
492 });
493 }
494 }
495 }
496
497 // Imager Pixel Spacing (0018,1164) - Type 1 for DX/MG
498 if (options_.check_type1) {
500 "ImagerPixelSpacing", findings);
501
502 // Mammography typically has very fine pixel spacing (< 0.1 mm)
503 // This is informational only
504 }
505}
506
508 const dicom_dataset& dataset,
509 std::vector<validation_finding>& findings) const {
510
511 // Compression Force (0018,11A2) - Type 3 but important for mammography
513 check_compression_force_range(dataset, findings);
514 }
515
516 // Body Part Thickness (0018,11A0) - Type 3
519 auto thickness = dataset.get_numeric<double>(mg_tags::body_part_thickness);
520 if (thickness && !is_valid_compressed_breast_thickness(*thickness)) {
521 findings.push_back({
524 "Compressed breast thickness outside typical range (10-150mm): " +
525 std::to_string(*thickness) + " mm",
526 "MG-WARN-007"
527 });
528 }
529 } else {
530 // Recommend including thickness for quality assessment
531 findings.push_back({
534 "Compressed breast thickness not documented - "
535 "recommended for dose assessment and quality control",
536 "MG-INFO-005"
537 });
538 }
539
540 // KVP (0018,0060) - Type 3 but important for dose
541 if (!dataset.contains(mg_tags::kvp)) {
542 findings.push_back({
545 "KVP not documented - recommended for dose assessment",
546 "MG-INFO-006"
547 });
548 }
549
550 // Entrance Dose (0040,0302) - Type 3
551 // Organ Dose (0040,0316) - Type 3 but important for breast dose tracking
552 }
553}
554
556 const dicom_dataset& dataset,
557 std::vector<validation_finding>& findings) const {
558
559 // Type 1 attributes
560 if (options_.check_type1) {
561 check_type1_attribute(dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
562 check_type1_attribute(dataset, tags::photometric_interpretation, "PhotometricInterpretation", findings);
563 check_type1_attribute(dataset, tags::rows, "Rows", findings);
564 check_type1_attribute(dataset, tags::columns, "Columns", findings);
565 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated", findings);
566 check_type1_attribute(dataset, tags::bits_stored, "BitsStored", findings);
567 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
568 check_type1_attribute(dataset, tags::pixel_representation, "PixelRepresentation", findings);
569 check_type1_attribute(dataset, tags::pixel_data, "PixelData", findings);
570 }
571
572 // Validate pixel data consistency
574 check_pixel_data_consistency(dataset, findings);
575 check_photometric_interpretation(dataset, findings);
576 }
577}
578
580 const dicom_dataset& dataset,
581 std::vector<validation_finding>& findings) const {
582
583 // Window Center (0028,1050) and Window Width (0028,1051) - Type 1C for For Presentation
585 bool has_window = dataset.contains(tags::window_center) &&
587
588 constexpr dicom_tag voi_lut_sequence{0x0028, 0x3010};
589 bool has_voi_lut = dataset.contains(voi_lut_sequence);
590
591 if (!has_window && !has_voi_lut) {
592 findings.push_back({
595 "For Presentation mammography images should have Window Center/Width "
596 "or VOI LUT Sequence for proper display",
597 "MG-WARN-008"
598 });
599 }
600 }
601}
602
604 const dicom_dataset& dataset,
605 std::vector<validation_finding>& findings) const {
606
607 // Type 1
608 if (options_.check_type1) {
609 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
610 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
611 }
612
613 // Validate SOP Class UID is a mammography storage class
614 if (dataset.contains(tags::sop_class_uid)) {
615 auto sop_class = dataset.get_string(tags::sop_class_uid);
616 if (!is_mg_storage_sop_class(sop_class)) {
617 findings.push_back({
620 "SOPClassUID is not a recognized Mammography Storage SOP Class: " + sop_class,
621 "MG-ERR-001"
622 });
623 }
624 }
625}
626
628 const dicom_dataset& dataset,
629 std::vector<validation_finding>& findings) const {
630
631 // Breast Implant Present (0028,1300) - Type 3
634 if (implant != "YES" && implant != "NO") {
635 findings.push_back({
638 "Breast Implant Present should be 'YES' or 'NO', found: " + implant,
639 "MG-WARN-009"
640 });
641 }
642
643 // If implant is present, check for implant displaced views
644 if (implant == "YES") {
645 // Informational: implant displacement technique (Eklund) may be needed
646 if (dataset.contains(mg_tags::view_position)) {
647 auto view = dataset.get_string(mg_tags::view_position);
648 auto parsed_view = parse_mg_view_position(view);
649 if (parsed_view != mg_view_position::implant &&
650 parsed_view != mg_view_position::id) {
651 findings.push_back({
654 "Breast implant present but view is not implant-displaced (ID). "
655 "Implant displacement technique (Eklund) may improve visualization.",
656 "MG-INFO-007"
657 });
658 }
659 }
660 }
661 }
662}
663
664// =============================================================================
665// Attribute Validation Helpers
666// =============================================================================
667
669 const dicom_dataset& dataset,
670 dicom_tag tag,
671 std::string_view name,
672 std::vector<validation_finding>& findings) const {
673
674 if (!dataset.contains(tag)) {
675 findings.push_back({
677 tag,
678 std::string("Type 1 attribute missing: ") + std::string(name) +
679 " (" + tag.to_string() + ")",
680 "MG-TYPE1-MISSING"
681 });
682 } else {
683 // Type 1 must have a value (cannot be empty)
684 auto value = dataset.get_string(tag);
685 if (value.empty()) {
686 findings.push_back({
688 tag,
689 std::string("Type 1 attribute has empty value: ") +
690 std::string(name) + " (" + tag.to_string() + ")",
691 "MG-TYPE1-EMPTY"
692 });
693 }
694 }
695}
696
698 const dicom_dataset& dataset,
699 dicom_tag tag,
700 std::string_view name,
701 std::vector<validation_finding>& findings) const {
702
703 // Type 2 must be present but can be empty
704 if (!dataset.contains(tag)) {
705 findings.push_back({
707 tag,
708 std::string("Type 2 attribute missing: ") + std::string(name) +
709 " (" + tag.to_string() + ")",
710 "MG-TYPE2-MISSING"
711 });
712 }
713}
714
716 const dicom_dataset& dataset,
717 std::vector<validation_finding>& findings) const {
718
719 if (!dataset.contains(tags::modality)) {
720 return; // Already reported as Type 1 missing
721 }
722
723 auto modality = dataset.get_string(tags::modality);
724 if (modality != "MG") {
725 findings.push_back({
728 "Modality must be 'MG' for mammography images, found: " + modality,
729 "MG-ERR-002"
730 });
731 }
732}
733
735 const dicom_dataset& dataset,
736 std::vector<validation_finding>& findings) const {
737
738 // Check Laterality (0020,0060) - Series level
739 bool has_series_laterality = dataset.contains(mg_tags::laterality);
740 // Check Image Laterality (0020,0062) - Image level
741 bool has_image_laterality = dataset.contains(mg_tags::image_laterality);
742
743 // At least one should be present for mammography
744 if (!has_series_laterality && !has_image_laterality) {
745 findings.push_back({
748 "Neither Laterality (0020,0060) nor Image Laterality (0020,0062) is present. "
749 "Breast laterality must be specified for mammography images.",
750 "MG-ERR-003"
751 });
752 return;
753 }
754
755 // Validate Laterality value if present
756 if (has_series_laterality) {
757 auto lat = dataset.get_string(mg_tags::laterality);
758 if (!is_valid_breast_laterality(lat)) {
759 findings.push_back({
762 "Invalid Laterality value: '" + lat +
763 "'. Must be 'L' (left), 'R' (right), or 'B' (bilateral).",
764 "MG-ERR-004"
765 });
766 }
767 }
768
769 // Validate Image Laterality value if present
770 if (has_image_laterality) {
771 auto img_lat = dataset.get_string(mg_tags::image_laterality);
772 if (!is_valid_breast_laterality(img_lat)) {
773 findings.push_back({
776 "Invalid Image Laterality value: '" + img_lat +
777 "'. Must be 'L' (left), 'R' (right), or 'B' (bilateral).",
778 "MG-ERR-005"
779 });
780 }
781 }
782
783 // Check consistency between series and image laterality if both present
784 if (has_series_laterality && has_image_laterality) {
785 auto series_lat = dataset.get_string(mg_tags::laterality);
786 auto image_lat = dataset.get_string(mg_tags::image_laterality);
787
788 if (series_lat != image_lat) {
789 findings.push_back({
792 "Laterality mismatch: Series Laterality is '" + series_lat +
793 "' but Image Laterality is '" + image_lat + "'.",
794 "MG-WARN-001"
795 });
796 }
797 }
798}
799
801 const dicom_dataset& dataset,
802 std::vector<validation_finding>& findings) const {
803
804 // View Position (0018,5101) - Type 2 for mammography
805 if (!dataset.contains(mg_tags::view_position)) {
806 findings.push_back({
809 "View Position (0018,5101) is not present. "
810 "This attribute should be specified for mammography images.",
811 "MG-WARN-005"
812 });
813 return;
814 }
815
816 auto view_str = dataset.get_string(mg_tags::view_position);
817 if (view_str.empty()) {
818 findings.push_back({
821 "View Position is present but empty.",
822 "MG-WARN-006"
823 });
824 return;
825 }
826
827 // Parse and validate view position
828 auto view = parse_mg_view_position(view_str);
829 if (view == mg_view_position::other) {
830 // Check if it's a valid view we just don't recognize
831 auto valid_views = get_valid_mg_view_positions();
832 std::string valid_list;
833 for (size_t i = 0; i < valid_views.size(); ++i) {
834 if (i > 0) valid_list += ", ";
835 valid_list += std::string(valid_views[i]);
836 }
837
838 findings.push_back({
841 "Unrecognized View Position: '" + view_str +
842 "'. Common mammography views include: " + valid_list,
843 "MG-WARN-010"
844 });
845 }
846
847 // Additional validation: check laterality-view consistency
849 auto lat_str = dataset.contains(mg_tags::image_laterality)
852
853 auto laterality = parse_breast_laterality(lat_str);
854
855 if (!is_valid_laterality_view_combination(laterality, view)) {
856 findings.push_back({
859 "Unusual laterality-view combination: " + lat_str + " / " + view_str,
860 "MG-WARN-011"
861 });
862 }
863 }
864}
865
867 const dicom_dataset& dataset,
868 std::vector<validation_finding>& findings) const {
869
870 if (!dataset.contains(mg_tags::compression_force)) {
871 // Compression force is Type 3 but important for mammography QC
872 findings.push_back({
875 "Compression Force (0018,11A2) is not present. "
876 "This information is valuable for quality control and patient safety.",
877 "MG-INFO-008"
878 });
879 return;
880 }
881
882 auto force = dataset.get_numeric<double>(mg_tags::compression_force);
883 if (!force) {
884 findings.push_back({
887 "Compression Force is present but could not be parsed as a number.",
888 "MG-WARN-012"
889 });
890 return;
891 }
892
893 auto [min_typical, max_typical] = get_typical_compression_force_range();
894
895 if (!is_valid_compression_force(*force)) {
896 findings.push_back({
899 "Compression Force (" + std::to_string(*force) +
900 " N) is outside the typical range (20-300 N). "
901 "This may indicate a measurement error or non-standard technique.",
902 "MG-WARN-013"
903 });
904 } else if (*force < min_typical || *force > max_typical) {
905 findings.push_back({
908 "Compression Force (" + std::to_string(*force) +
909 " N) is outside the typical screening range (" +
910 std::to_string(min_typical) + "-" + std::to_string(max_typical) + " N).",
911 "MG-INFO-009"
912 });
913 }
914}
915
917 const dicom_dataset& dataset,
918 std::vector<validation_finding>& findings) const {
919
920 // Check BitsStored <= BitsAllocated
921 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
922 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
923 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
924
925 if (bits_allocated && bits_stored) {
926 if (*bits_stored > *bits_allocated) {
927 findings.push_back({
930 "BitsStored (" + std::to_string(*bits_stored) +
931 ") exceeds BitsAllocated (" + std::to_string(*bits_allocated) + ")",
932 "MG-ERR-006"
933 });
934 }
935
936 // Mammography typically uses 12-16 bits stored for high dynamic range
937 if (*bits_stored < 12 || *bits_stored > 16) {
938 findings.push_back({
941 "Mammography images typically use 12-16 bits, found: " +
942 std::to_string(*bits_stored),
943 "MG-INFO-010"
944 });
945 }
946 }
947
948 // Check HighBit == BitsStored - 1
949 if (bits_stored && high_bit) {
950 if (*high_bit != *bits_stored - 1) {
951 findings.push_back({
954 "HighBit (" + std::to_string(*high_bit) +
955 ") should typically be BitsStored - 1 (" +
956 std::to_string(*bits_stored - 1) + ")",
957 "MG-WARN-014"
958 });
959 }
960 }
961
962 // Mammography must be grayscale - check SamplesPerPixel
963 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
964 if (samples && *samples != 1) {
965 findings.push_back({
968 "Mammography images must be grayscale (SamplesPerPixel = 1), found: " +
969 std::to_string(*samples),
970 "MG-ERR-007"
971 });
972 }
973}
974
976 const dicom_dataset& dataset,
977 std::vector<validation_finding>& findings) const {
978
980 return; // Already reported as missing
981 }
982
983 auto photometric = dataset.get_string(tags::photometric_interpretation);
984 if (photometric != "MONOCHROME1" && photometric != "MONOCHROME2") {
985 findings.push_back({
988 "Mammography images must use MONOCHROME1 or MONOCHROME2, found: " + photometric,
989 "MG-ERR-008"
990 });
991 }
992}
993
994// =============================================================================
995// Convenience Functions
996// =============================================================================
997
999 mg_iod_validator validator;
1000 return validator.validate(dataset);
1001}
1002
1004 mg_iod_validator validator;
1005 return validator.quick_check(dataset);
1006}
1007
1009 if (!dataset.contains(tags::sop_class_uid)) {
1010 return false;
1011 }
1012 auto sop_class = dataset.get_string(tags::sop_class_uid);
1013 return is_mg_for_presentation_sop_class(sop_class);
1014}
1015
1017 if (!dataset.contains(tags::sop_class_uid)) {
1018 return false;
1019 }
1020 auto sop_class = dataset.get_string(tags::sop_class_uid);
1021 return is_mg_for_processing_sop_class(sop_class);
1022}
1023
1024bool has_breast_implant(const dicom_dataset& dataset) {
1025 constexpr dicom_tag breast_implant_present{0x0028, 0x1300};
1026 if (!dataset.contains(breast_implant_present)) {
1027 return false;
1028 }
1029 auto value = dataset.get_string(breast_implant_present);
1030 return value == "YES";
1031}
1032
1034 constexpr dicom_tag view_position{0x0018, 0x5101};
1035 if (!dataset.contains(view_position)) {
1036 return false;
1037 }
1038 auto view_str = dataset.get_string(view_position);
1039 auto view = parse_mg_view_position(view_str);
1040 return is_screening_view(view);
1041}
1042
1043} // 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 check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_laterality_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_compression_force_range(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_compression_force(const core::dicom_dataset &dataset) const
Validate compression force.
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_breast_implant_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_dx_anatomy_imaged_module(const core::dicom_dataset &dataset, 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_photometric_interpretation(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_dx_detector_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_mammography_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_pixel_data_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const mg_validation_options & options() const noexcept
Get the validation options.
void check_view_position_validity(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_view_position(const core::dicom_dataset &dataset) const
Validate mammography view position.
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against Mammography IOD.
validation_result validate_laterality(const core::dicom_dataset &dataset) const
Validate breast laterality attribute.
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_mammography_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const mg_validation_options &options)
Set validation options.
mg_iod_validator()=default
Construct validator with default options.
void validate_voi_lut_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_sop_common_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
validation_result validate_for_processing(const core::dicom_dataset &dataset) const
Validate a For Processing mammography dataset.
void validate_acquisition_dose_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_for_presentation(const core::dicom_dataset &dataset) const
Validate a For Presentation mammography dataset.
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
Compile-time constants for commonly used DICOM tags.
Digital Mammography X-Ray Image IOD Validator.
Digital Mammography X-Ray Image Storage SOP Classes.
constexpr dicom_tag high_bit
High Bit.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag window_width
Window Width.
constexpr dicom_tag window_center
Window Center.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag bits_allocated
Bits Allocated.
constexpr dicom_tag rows
Rows.
constexpr dicom_tag columns
Columns.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag bits_stored
Bits Stored.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag pixel_data
Pixel Data.
constexpr dicom_tag pixel_representation
Pixel Representation.
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 samples_per_pixel
Samples per Pixel.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag photometric_interpretation
Photometric Interpretation.
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.
std::pair< double, double > get_typical_compression_force_range() noexcept
Get typical compression force range.
bool is_valid_compression_force(double force_n) noexcept
Validate compression force value.
bool is_mg_for_presentation_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a For Presentation mammography SOP Class.
bool is_valid_compressed_breast_thickness(double thickness_mm) noexcept
Validate compressed breast thickness.
@ implant
Implant displaced view (Eklund technique)
mg_view_position parse_mg_view_position(std::string_view value) noexcept
Parse DICOM view position string to mammography view enum.
@ partial
PARTIAL - Document is not complete.
bool is_valid_laterality_view_combination(breast_laterality laterality, mg_view_position view) noexcept
Check if laterality and view position are consistent.
bool is_mg_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a Mammography Storage SOP Class.
breast_laterality parse_breast_laterality(std::string_view value) noexcept
Parse DICOM laterality string to enum.
bool is_valid_breast_laterality(std::string_view value) noexcept
Check if a laterality value is valid for mammography.
std::vector< std::string_view > get_valid_mg_view_positions() noexcept
Get all valid mammography view position strings.
bool is_screening_view(mg_view_position position) noexcept
Check if a view position is a standard screening view.
bool is_mg_for_processing_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a For Processing mammography SOP Class.
bool is_screening_mammogram(const core::dicom_dataset &dataset)
Check if dataset is a screening mammogram.
validation_result validate_mg_iod(const core::dicom_dataset &dataset)
Validate a mammography dataset with default options.
bool is_for_presentation_mg(const core::dicom_dataset &dataset)
Check if dataset is a For Presentation mammography image.
bool is_valid_mg_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid mammography image.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool has_breast_implant(const core::dicom_dataset &dataset)
Check if dataset indicates breast implant presence.
bool is_for_processing_mg(const core::dicom_dataset &dataset)
Check if dataset is a For Processing mammography image.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool validate_dose_parameters
Validate acquisition dose parameters.
bool validate_view_position
Validate mammography view position (0018,5101)
bool validate_mg_specific
Validate mammography-specific attributes.
bool check_type1
Check Type 1 (required) attributes.
bool strict_mode
Strict mode - treat warnings as errors.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_laterality
Validate breast laterality (0020,0060)
bool validate_processing_requirements
Validate For Processing specific requirements.
bool validate_compression
Validate compression force (0018,11A2)
bool validate_presentation_requirements
Validate For Presentation specific requirements.
bool validate_implant_attributes
Validate breast implant attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name