PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
label_map_seg_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
12
13#include <sstream>
14
16
17using namespace kcenon::pacs::core;
18
19// Label Map Segmentation Storage SOP Class UID (Supplement 243)
20static constexpr std::string_view label_map_segmentation_storage_uid =
21 "1.2.840.10008.5.1.4.1.1.66.8";
22
23// =============================================================================
24// label_map_seg_iod_validator Implementation
25// =============================================================================
26
30
32 const dicom_dataset& dataset) const {
33 validation_result result;
34 result.is_valid = true;
35
36 // Validate mandatory modules
38 validate_patient_module(dataset, result.findings);
42 dataset, result.findings);
46 validate_sop_common_module(dataset, result.findings);
47 }
48
51 dataset, result.findings);
52 }
53
55 validate_segment_sequence(dataset, result.findings);
56 }
57
60 }
61
64 }
65
66 // Multi-frame validation
69
70 // Check for errors
71 for (const auto& finding : result.findings) {
72 if (finding.severity == validation_severity::error) {
73 result.is_valid = false;
74 break;
75 }
77 && finding.severity == validation_severity::warning) {
78 result.is_valid = false;
79 break;
80 }
81 }
82
83 return result;
84}
85
87 const dicom_dataset& dataset) const {
88 validation_result result;
89 result.is_valid = true;
90
91 validate_segment_sequence(dataset, result.findings);
92
93 for (const auto& finding : result.findings) {
94 if (finding.severity == validation_severity::error) {
95 result.is_valid = false;
96 break;
97 }
98 }
99
100 return result;
101}
102
104 const dicom_dataset& dataset) const {
105 // General Study Module
106 if (!dataset.contains(tags::study_instance_uid)) return false;
107
108 // General Series Module
109 if (!dataset.contains(tags::modality)) return false;
110 if (!dataset.contains(tags::series_instance_uid)) return false;
111
112 // Check modality is SEG
113 auto modality = dataset.get_string(tags::modality);
114 if (modality != "SEG") return false;
115
116 // Label Map Segmentation Image Module
117 if (!dataset.contains(label_map_seg_tags::segmentation_type)) return false;
118 auto seg_type =
120 if (seg_type != "LABELMAP") return false;
121
122 if (!dataset.contains(label_map_seg_tags::segment_sequence)) return false;
123
124 // SOP Common Module
125 if (!dataset.contains(tags::sop_class_uid)) return false;
126 if (!dataset.contains(tags::sop_instance_uid)) return false;
127
128 // Verify SOP Class is Label Map Segmentation
129 auto sop_class = dataset.get_string(tags::sop_class_uid);
130 if (sop_class != label_map_segmentation_storage_uid) return false;
131
132 return true;
133}
134
137 return options_;
138}
139
144
145// =============================================================================
146// Module Validation Methods
147// =============================================================================
148
150 const dicom_dataset& dataset,
151 std::vector<validation_finding>& findings) const {
152
153 if (options_.check_type2) {
155 dataset, tags::patient_name, "PatientName", findings);
157 dataset, tags::patient_id, "PatientID", findings);
159 dataset, tags::patient_birth_date, "PatientBirthDate", findings);
161 dataset, tags::patient_sex, "PatientSex", findings);
162 }
163}
164
166 const dicom_dataset& dataset,
167 std::vector<validation_finding>& findings) const {
168
169 if (options_.check_type1) {
171 dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
172 }
173
174 if (options_.check_type2) {
176 dataset, tags::study_date, "StudyDate", findings);
178 dataset, tags::study_time, "StudyTime", findings);
181 "ReferringPhysicianName", findings);
183 dataset, tags::study_id, "StudyID", findings);
185 dataset, tags::accession_number, "AccessionNumber", findings);
186 }
187}
188
190 const dicom_dataset& dataset,
191 std::vector<validation_finding>& findings) const {
192
193 if (options_.check_type1) {
195 dataset, tags::modality, "Modality", findings);
197 dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
198 check_modality(dataset, findings);
199
200 // Frame of Reference Module - Type 1 for segmentation
203 "FrameOfReferenceUID", findings);
204 }
205
206 if (options_.check_type2) {
208 dataset, tags::series_number, "SeriesNumber", findings);
209 }
210}
211
214 [[maybe_unused]] const dicom_dataset& dataset,
215 [[maybe_unused]] std::vector<validation_finding>& findings) const {
216
217 // Modality must be "SEG" - already checked in general series validation
218}
219
221 const dicom_dataset& dataset,
222 std::vector<validation_finding>& findings) const {
223
224 if (options_.check_type2) {
227 "Manufacturer", findings);
228 }
229}
230
232 const dicom_dataset& dataset,
233 std::vector<validation_finding>& findings) const {
234
235 if (options_.check_type1) {
238 "Manufacturer", findings);
241 "ManufacturerModelName", findings);
244 "DeviceSerialNumber", findings);
247 "SoftwareVersions", findings);
248 }
249}
250
252 const dicom_dataset& dataset,
253 std::vector<validation_finding>& findings) const {
254
255 if (options_.check_type2) {
256 constexpr dicom_tag instance_number{0x0020, 0x0013};
258 dataset, instance_number, "InstanceNumber", findings);
259 }
260}
261
263 const dicom_dataset& dataset,
264 std::vector<validation_finding>& findings) const {
265
266 if (options_.check_type1) {
268 dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
271 "PhotometricInterpretation", findings);
273 dataset, tags::rows, "Rows", findings);
275 dataset, tags::columns, "Columns", findings);
277 dataset, tags::bits_allocated, "BitsAllocated", findings);
279 dataset, tags::bits_stored, "BitsStored", findings);
281 dataset, tags::high_bit, "HighBit", findings);
284 "PixelRepresentation", findings);
285 }
286
287 // Label map-specific pixel data constraints
288 check_pixel_data_consistency(dataset, findings);
289}
290
293 const dicom_dataset& dataset,
294 std::vector<validation_finding>& findings) const {
295
296 if (options_.check_type1) {
299 "SegmentationType", findings);
302 "SegmentSequence", findings);
303 }
304
305 // Validate segmentation type is LABELMAP
306 check_segmentation_type(dataset, findings);
307}
308
310 const dicom_dataset& dataset,
311 std::vector<validation_finding>& findings) const {
312
314 return; // Already reported as Type 1 missing
315 }
316
317 const auto* element = dataset.get(label_map_seg_tags::segment_sequence);
318 if (!element || !element->is_sequence()
319 || element->sequence_items().empty()) {
320 findings.push_back({
323 "SegmentSequence must contain at least one segment",
324 "LMSEG-ERR-004"
325 });
326 return;
327 }
328
329 const auto& sequence = element->sequence_items();
330 for (size_t i = 0; i < sequence.size(); ++i) {
331 const auto& segment_item = sequence[i];
332 validate_single_segment(segment_item, i, findings);
333 }
334
335 // Validate segment numbers are contiguous starting from 1
336 // (Label Map constraint: label values map directly to segment numbers)
337 for (size_t i = 0; i < sequence.size(); ++i) {
339 auto seg_num = sequence[i].get_numeric<uint16_t>(
341 if (seg_num && *seg_num != static_cast<uint16_t>(i + 1)) {
342 findings.push_back({
345 "Segment[" + std::to_string(i) + "]: "
346 "SegmentNumber should be " + std::to_string(i + 1)
347 + " for Label Map (contiguous from 1), found: "
348 + std::to_string(*seg_num),
349 "LMSEG-SEQ-WARN-003"
350 });
351 }
352 }
353 }
354}
355
357 const dicom_dataset& segment_item,
358 size_t segment_index,
359 std::vector<validation_finding>& findings) const {
360
361 std::string prefix = "Segment[" + std::to_string(segment_index) + "]: ";
362
363 if (!segment_item.contains(label_map_seg_tags::segment_number)) {
364 findings.push_back({
367 prefix + "SegmentNumber (0062,0004) is required",
368 "LMSEG-SEQ-ERR-001"
369 });
370 }
371
372 if (!segment_item.contains(label_map_seg_tags::segment_label)) {
373 findings.push_back({
376 prefix + "SegmentLabel (0062,0005) is required",
377 "LMSEG-SEQ-ERR-002"
378 });
379 }
380
382 findings.push_back({
385 prefix + "SegmentAlgorithmType (0062,0008) is required",
386 "LMSEG-SEQ-ERR-003"
387 });
389 auto algo_type =
390 segment_item.get_string(
392 if (algo_type != "AUTOMATIC" && algo_type != "SEMIAUTOMATIC"
393 && algo_type != "MANUAL") {
394 findings.push_back({
397 prefix + "Invalid SegmentAlgorithmType value: " + algo_type,
398 "LMSEG-SEQ-WARN-001"
399 });
400 }
401 }
402
403 if (!segment_item.contains(
405 findings.push_back({
408 prefix + "SegmentedPropertyCategoryCodeSequence (0062,0003) "
409 "is required",
410 "LMSEG-SEQ-ERR-004"
411 });
412 }
413
414 if (!segment_item.contains(
416 findings.push_back({
419 prefix + "SegmentedPropertyTypeCodeSequence (0062,000F) "
420 "is required",
421 "LMSEG-SEQ-ERR-005"
422 });
423 }
424
426 auto label =
428 if (label.empty()) {
429 findings.push_back({
432 prefix + "SegmentLabel should not be empty",
433 "LMSEG-SEQ-WARN-002"
434 });
435 }
436 }
437}
438
441 const dicom_dataset& dataset,
442 std::vector<validation_finding>& findings) const {
443
444 if (options_.check_type1) {
447 "NumberOfFrames", findings);
450 "SharedFunctionalGroupsSequence", findings);
452 dataset,
454 "PerFrameFunctionalGroupsSequence", findings);
455 }
456}
457
459 const dicom_dataset& dataset,
460 std::vector<validation_finding>& findings) const {
461
462 if (options_.check_type1) {
465 "DimensionOrganizationSequence", findings);
468 "DimensionIndexSequence", findings);
469 }
470}
471
473 const dicom_dataset& dataset,
474 std::vector<validation_finding>& findings) const {
475
477 if (!dataset.contains(
479 findings.push_back({
482 "ReferencedSeriesSequence (0008,1115) should be present "
483 "for source image references",
484 "LMSEG-REF-WARN-001"
485 });
486 }
487 }
488}
489
491 const dicom_dataset& dataset,
492 std::vector<validation_finding>& findings) const {
493
494 if (options_.check_type1) {
496 dataset, tags::sop_class_uid, "SOPClassUID", findings);
498 dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
499 }
500
501 if (dataset.contains(tags::sop_class_uid)) {
502 auto sop_class = dataset.get_string(tags::sop_class_uid);
503 if (sop_class != label_map_segmentation_storage_uid) {
504 findings.push_back({
507 "SOPClassUID is not Label Map Segmentation Storage: "
508 + sop_class,
509 "LMSEG-ERR-005"
510 });
511 }
512 }
513}
514
515// =============================================================================
516// Attribute Validation Helpers
517// =============================================================================
518
520 const dicom_dataset& dataset,
521 dicom_tag tag,
522 std::string_view name,
523 std::vector<validation_finding>& findings) const {
524
525 if (!dataset.contains(tag)) {
526 findings.push_back({
528 tag,
529 std::string("Type 1 attribute missing: ") + std::string(name)
530 + " (" + tag.to_string() + ")",
531 "LMSEG-TYPE1-MISSING"
532 });
533 } else {
534 const auto* element = dataset.get(tag);
535 if (element != nullptr) {
536 if (element->is_sequence()) {
537 if (element->sequence_items().empty()) {
538 findings.push_back({
540 tag,
541 std::string("Type 1 sequence has no items: ")
542 + std::string(name) + " (" + tag.to_string()
543 + ")",
544 "LMSEG-TYPE1-EMPTY"
545 });
546 }
547 } else {
548 auto value = dataset.get_string(tag);
549 if (value.empty()) {
550 findings.push_back({
552 tag,
553 std::string("Type 1 attribute has empty value: ")
554 + std::string(name) + " (" + tag.to_string()
555 + ")",
556 "LMSEG-TYPE1-EMPTY"
557 });
558 }
559 }
560 }
561 }
562}
563
565 const dicom_dataset& dataset,
566 dicom_tag tag,
567 std::string_view name,
568 std::vector<validation_finding>& findings) const {
569
570 if (!dataset.contains(tag)) {
571 findings.push_back({
573 tag,
574 std::string("Type 2 attribute missing: ") + std::string(name)
575 + " (" + tag.to_string() + ")",
576 "LMSEG-TYPE2-MISSING"
577 });
578 }
579}
580
582 const dicom_dataset& dataset,
583 std::vector<validation_finding>& findings) const {
584
585 if (!dataset.contains(tags::modality)) {
586 return;
587 }
588
589 auto modality = dataset.get_string(tags::modality);
590 if (modality != "SEG") {
591 findings.push_back({
594 "Modality must be 'SEG' for Label Map Segmentation objects, "
595 "found: " + modality,
596 "LMSEG-ERR-001"
597 });
598 }
599}
600
602 const dicom_dataset& dataset,
603 std::vector<validation_finding>& findings) const {
604
606 return;
607 }
608
609 auto seg_type =
611 if (seg_type != "LABELMAP") {
612 findings.push_back({
615 "SegmentationType must be 'LABELMAP' for Label Map Segmentation "
616 "objects, found: " + seg_type,
617 "LMSEG-ERR-006"
618 });
619 }
620}
621
623 const dicom_dataset& dataset,
624 std::vector<validation_finding>& findings) const {
625
626 // SamplesPerPixel must be 1
627 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
628 if (samples && *samples != 1) {
629 findings.push_back({
632 "SamplesPerPixel must be 1 for Label Map Segmentation objects",
633 "LMSEG-PXL-ERR-001"
634 });
635 }
636
637 // PhotometricInterpretation must be MONOCHROME2
639 auto photometric =
641 if (photometric != "MONOCHROME2") {
642 findings.push_back({
645 "PhotometricInterpretation must be MONOCHROME2 for "
646 "Label Map Segmentation",
647 "LMSEG-PXL-ERR-002"
648 });
649 }
650 }
651
652 // Label Map uses unsigned 8-bit or 16-bit integer labels
653 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
654 if (bits_allocated) {
655 if (*bits_allocated != 8 && *bits_allocated != 16) {
656 findings.push_back({
659 "BitsAllocated should be 8 or 16 for Label Map "
660 "Segmentation (unsigned integer labels), found: "
661 + std::to_string(*bits_allocated),
662 "LMSEG-PXL-WARN-001"
663 });
664 }
665 }
666
667 // PixelRepresentation must be 0 (unsigned) for label values
668 auto pixel_rep =
669 dataset.get_numeric<uint16_t>(tags::pixel_representation);
670 if (pixel_rep && *pixel_rep != 0) {
671 findings.push_back({
674 "PixelRepresentation must be 0 (unsigned) for Label Map "
675 "Segmentation -- label values must be unsigned integers",
676 "LMSEG-PXL-ERR-003"
677 });
678 }
679}
680
681// =============================================================================
682// Convenience Functions
683// =============================================================================
684
687 return validator.validate(dataset);
688}
689
692 return validator.quick_check(dataset);
693}
694
696 constexpr dicom_tag segmentation_type{0x0062, 0x0001};
697 if (!dataset.contains(segmentation_type)) {
698 return false;
699 }
700 return dataset.get_string(segmentation_type) == "LABELMAP";
701}
702
703} // namespace kcenon::pacs::services::validation
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
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 validate_label_map_segmentation_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_segments(const core::dicom_dataset &dataset) const
Validate segment sequence completeness.
void validate_general_equipment_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
label_map_seg_iod_validator()=default
Construct validator with default options.
void validate_image_pixel_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
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required attributes.
void validate_label_map_segmentation_series_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
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against Label Map Segmentation IOD.
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_multiframe_dimension_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_multiframe_functional_groups_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const label_map_seg_validation_options & options() const noexcept
Get the validation options.
void check_segmentation_type(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_single_segment(const core::dicom_dataset &segment_item, size_t segment_index, std::vector< validation_finding > &findings) const
void validate_common_instance_reference_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
void validate_segment_sequence(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 set_options(const label_map_seg_validation_options &options)
Set validation options.
void validate_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_enhanced_general_equipment_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
Compile-time constants for commonly used DICOM tags.
Label Map Segmentation IOD Validator.
constexpr dicom_tag high_bit
High Bit.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
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 frame_of_reference_uid
Frame of Reference UID.
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_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.
constexpr std::string_view label_map_segmentation_storage_uid
Label Map Segmentation Storage SOP Class UID (Supplement 243)
Definition seg_storage.h:50
segmentation_type
Segmentation type (0062,0001)
Definition seg_storage.h:79
constexpr core::dicom_tag manufacturer
Enhanced General Equipment Module tags.
constexpr core::dicom_tag shared_functional_groups_sequence
Shared Functional Groups Sequence (5200,9229)
constexpr core::dicom_tag segmented_property_category_code_sequence
Segmented Property Category Code Sequence (0062,0003)
constexpr core::dicom_tag segmented_property_type_code_sequence
Segmented Property Type Code Sequence (0062,000F)
constexpr core::dicom_tag segmentation_type
Segmentation Type (0062,0001) - must be "LABELMAP".
constexpr core::dicom_tag number_of_frames
Number of Frames (0028,0008)
constexpr core::dicom_tag segment_sequence
Segment Sequence (0062,0002)
constexpr core::dicom_tag referenced_series_sequence
Referenced Series Sequence (0008,1115)
constexpr core::dicom_tag per_frame_functional_groups_sequence
Per-Frame Functional Groups Sequence (5200,9230)
constexpr core::dicom_tag segment_algorithm_type
Segment Algorithm Type (0062,0008)
constexpr core::dicom_tag segment_number
Segment Number (0062,0004)
constexpr core::dicom_tag dimension_index_sequence
Dimension Index Sequence (0020,9222)
constexpr core::dicom_tag segment_label
Segment Label (0062,0005)
constexpr core::dicom_tag dimension_organization_sequence
Dimension Organization Sequence (0020,9221)
bool is_valid_label_map_seg_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid Label Map Segmentation object.
validation_result validate_label_map_seg_iod(const core::dicom_dataset &dataset)
Validate a Label Map Segmentation dataset with default options.
@ warning
Non-critical - IOD may have issues.
bool is_label_map_segmentation(const core::dicom_dataset &dataset)
Check if dataset is a label map segmentation.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_label_map_instance
Validate label map-specific instance attributes.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name