PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
heightmap_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// Heightmap Segmentation Storage SOP Class UID (Supplement 240)
20static constexpr std::string_view heightmap_segmentation_storage_uid =
21 "1.2.840.10008.5.1.4.1.1.66.7";
22
23// =============================================================================
24// heightmap_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);
45 validate_sop_common_module(dataset, result.findings);
46 }
47
50 }
51
53 validate_segment_sequence(dataset, result.findings);
54 }
55
58 }
59
62 }
63
64 // Multi-frame validation
67
68 // Check for errors
69 for (const auto& finding : result.findings) {
70 if (finding.severity == validation_severity::error) {
71 result.is_valid = false;
72 break;
73 }
75 && finding.severity == validation_severity::warning) {
76 result.is_valid = false;
77 break;
78 }
79 }
80
81 return result;
82}
83
85 const dicom_dataset& dataset) const {
86 validation_result result;
87 result.is_valid = true;
88
89 validate_segment_sequence(dataset, result.findings);
90
91 for (const auto& finding : result.findings) {
92 if (finding.severity == validation_severity::error) {
93 result.is_valid = false;
94 break;
95 }
96 }
97
98 return result;
99}
100
102 const dicom_dataset& dataset) const {
103 // Check essential Type 1 attributes
104
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 // Heightmap Segmentation Image Module
117 if (!dataset.contains(heightmap_seg_tags::segmentation_type)) return false;
118 auto seg_type = dataset.get_string(heightmap_seg_tags::segmentation_type);
119 if (seg_type != "HEIGHTMAP") return false;
120
121 if (!dataset.contains(heightmap_seg_tags::segment_sequence)) return false;
122
123 // SOP Common Module
124 if (!dataset.contains(tags::sop_class_uid)) return false;
125 if (!dataset.contains(tags::sop_instance_uid)) return false;
126
127 // Verify SOP Class is Heightmap Segmentation
128 auto sop_class = dataset.get_string(tags::sop_class_uid);
129 if (sop_class != heightmap_segmentation_storage_uid) return false;
130
131 return true;
132}
133
136 return options_;
137}
138
143
144// =============================================================================
145// Module Validation Methods
146// =============================================================================
147
149 const dicom_dataset& dataset,
150 std::vector<validation_finding>& findings) const {
151
152 if (options_.check_type2) {
154 dataset, tags::patient_name, "PatientName", findings);
156 dataset, tags::patient_id, "PatientID", findings);
158 dataset, tags::patient_birth_date, "PatientBirthDate", findings);
160 dataset, tags::patient_sex, "PatientSex", findings);
161 }
162}
163
165 const dicom_dataset& dataset,
166 std::vector<validation_finding>& findings) const {
167
168 if (options_.check_type1) {
170 dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
171 }
172
173 if (options_.check_type2) {
175 dataset, tags::study_date, "StudyDate", findings);
177 dataset, tags::study_time, "StudyTime", findings);
180 "ReferringPhysicianName", findings);
182 dataset, tags::study_id, "StudyID", findings);
184 dataset, tags::accession_number, "AccessionNumber", findings);
185 }
186}
187
189 const dicom_dataset& dataset,
190 std::vector<validation_finding>& findings) const {
191
192 if (options_.check_type1) {
194 dataset, tags::modality, "Modality", findings);
196 dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
197 check_modality(dataset, findings);
198
199 // Frame of Reference Module - Type 1 for segmentation
202 "FrameOfReferenceUID", findings);
203 }
204
205 if (options_.check_type2) {
207 dataset, tags::series_number, "SeriesNumber", findings);
208 }
209}
210
212 [[maybe_unused]] const dicom_dataset& dataset,
213 [[maybe_unused]] std::vector<validation_finding>& findings) const {
214
215 // Modality must be "SEG" - already checked in general series validation
216 // This module primarily constrains Modality to SEG for heightmap segmentation
217}
218
220 const dicom_dataset& dataset,
221 std::vector<validation_finding>& findings) const {
222
223 if (options_.check_type2) {
225 dataset, heightmap_seg_tags::manufacturer, "Manufacturer", findings);
226 }
227}
228
230 const dicom_dataset& dataset,
231 std::vector<validation_finding>& findings) const {
232
233 if (options_.check_type1) {
236 "Manufacturer", findings);
239 "ManufacturerModelName", findings);
242 "DeviceSerialNumber", findings);
245 "SoftwareVersions", findings);
246 }
247}
248
250 const dicom_dataset& dataset,
251 std::vector<validation_finding>& findings) const {
252
253 if (options_.check_type2) {
254 constexpr dicom_tag instance_number{0x0020, 0x0013};
256 dataset, instance_number, "InstanceNumber", findings);
257 }
258}
259
261 const dicom_dataset& dataset,
262 std::vector<validation_finding>& findings) const {
263
264 if (options_.check_type1) {
266 dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
269 "PhotometricInterpretation", findings);
271 dataset, tags::rows, "Rows", findings);
273 dataset, tags::columns, "Columns", findings);
275 dataset, tags::bits_allocated, "BitsAllocated", findings);
277 dataset, tags::bits_stored, "BitsStored", findings);
279 dataset, tags::high_bit, "HighBit", findings);
282 "PixelRepresentation", findings);
283 }
284
285 // Heightmap-specific pixel data constraints
286 check_pixel_data_consistency(dataset, findings);
287}
288
290 const dicom_dataset& dataset,
291 std::vector<validation_finding>& findings) const {
292
293 if (options_.check_type1) {
296 "SegmentationType", findings);
299 "SegmentSequence", findings);
300 }
301
302 // Validate segmentation type is HEIGHTMAP
303 check_segmentation_type(dataset, findings);
304}
305
307 const dicom_dataset& dataset,
308 std::vector<validation_finding>& findings) const {
309
311 // Already reported as Type 1 missing
312 return;
313 }
314
315 const auto* element = dataset.get(heightmap_seg_tags::segment_sequence);
316 if (!element || !element->is_sequence()
317 || element->sequence_items().empty()) {
318 findings.push_back({
321 "SegmentSequence must contain at least one segment",
322 "HMSEG-ERR-004"
323 });
324 return;
325 }
326
327 const auto& sequence = element->sequence_items();
328 for (size_t i = 0; i < sequence.size(); ++i) {
329 const auto& segment_item = sequence[i];
330 validate_single_segment(segment_item, i, findings);
331 }
332}
333
335 const dicom_dataset& segment_item,
336 size_t segment_index,
337 std::vector<validation_finding>& findings) const {
338
339 std::string prefix = "Segment[" + std::to_string(segment_index) + "]: ";
340
341 // Type 1 attributes within segment
342 if (!segment_item.contains(heightmap_seg_tags::segment_number)) {
343 findings.push_back({
346 prefix + "SegmentNumber (0062,0004) is required",
347 "HMSEG-SEQ-ERR-001"
348 });
349 }
350
351 if (!segment_item.contains(heightmap_seg_tags::segment_label)) {
352 findings.push_back({
355 prefix + "SegmentLabel (0062,0005) is required",
356 "HMSEG-SEQ-ERR-002"
357 });
358 }
359
361 findings.push_back({
364 prefix + "SegmentAlgorithmType (0062,0008) is required",
365 "HMSEG-SEQ-ERR-003"
366 });
368 auto algo_type =
370 if (algo_type != "AUTOMATIC" && algo_type != "SEMIAUTOMATIC"
371 && algo_type != "MANUAL") {
372 findings.push_back({
375 prefix + "Invalid SegmentAlgorithmType value: " + algo_type,
376 "HMSEG-SEQ-WARN-001"
377 });
378 }
379 }
380
381 // Segmented Property Category Code Sequence is Type 1
382 if (!segment_item.contains(
384 findings.push_back({
387 prefix + "SegmentedPropertyCategoryCodeSequence (0062,0003) "
388 "is required",
389 "HMSEG-SEQ-ERR-004"
390 });
391 }
392
393 // Segmented Property Type Code Sequence is Type 1
394 if (!segment_item.contains(
396 findings.push_back({
399 prefix + "SegmentedPropertyTypeCodeSequence (0062,000F) "
400 "is required",
401 "HMSEG-SEQ-ERR-005"
402 });
403 }
404
405 // Validate segment label is not empty
407 auto label =
409 if (label.empty()) {
410 findings.push_back({
413 prefix + "SegmentLabel should not be empty",
414 "HMSEG-SEQ-WARN-002"
415 });
416 }
417 }
418}
419
421 const dicom_dataset& dataset,
422 std::vector<validation_finding>& findings) const {
423
424 if (options_.check_type1) {
427 "NumberOfFrames", findings);
430 "SharedFunctionalGroupsSequence", findings);
433 "PerFrameFunctionalGroupsSequence", findings);
434 }
435}
436
438 const dicom_dataset& dataset,
439 std::vector<validation_finding>& findings) const {
440
441 if (options_.check_type1) {
444 "DimensionOrganizationSequence", findings);
447 "DimensionIndexSequence", findings);
448 }
449}
450
452 const dicom_dataset& dataset,
453 std::vector<validation_finding>& findings) const {
454
457 findings.push_back({
460 "ReferencedSeriesSequence (0008,1115) should be present "
461 "for source image references",
462 "HMSEG-REF-WARN-001"
463 });
464 }
465 }
466}
467
469 const dicom_dataset& dataset,
470 std::vector<validation_finding>& findings) const {
471
472 if (options_.check_type1) {
474 dataset, tags::sop_class_uid, "SOPClassUID", findings);
476 dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
477 }
478
479 // Validate SOP Class UID is Heightmap Segmentation
480 if (dataset.contains(tags::sop_class_uid)) {
481 auto sop_class = dataset.get_string(tags::sop_class_uid);
482 if (sop_class != heightmap_segmentation_storage_uid) {
483 findings.push_back({
486 "SOPClassUID is not Heightmap Segmentation Storage: "
487 + sop_class,
488 "HMSEG-ERR-005"
489 });
490 }
491 }
492}
493
494// =============================================================================
495// Attribute Validation Helpers
496// =============================================================================
497
499 const dicom_dataset& dataset,
500 dicom_tag tag,
501 std::string_view name,
502 std::vector<validation_finding>& findings) const {
503
504 if (!dataset.contains(tag)) {
505 findings.push_back({
507 tag,
508 std::string("Type 1 attribute missing: ") + std::string(name)
509 + " (" + tag.to_string() + ")",
510 "HMSEG-TYPE1-MISSING"
511 });
512 } else {
513 const auto* element = dataset.get(tag);
514 if (element != nullptr) {
515 if (element->is_sequence()) {
516 if (element->sequence_items().empty()) {
517 findings.push_back({
519 tag,
520 std::string("Type 1 sequence has no items: ")
521 + std::string(name) + " (" + tag.to_string() + ")",
522 "HMSEG-TYPE1-EMPTY"
523 });
524 }
525 } else {
526 auto value = dataset.get_string(tag);
527 if (value.empty()) {
528 findings.push_back({
530 tag,
531 std::string("Type 1 attribute has empty value: ")
532 + std::string(name) + " (" + tag.to_string() + ")",
533 "HMSEG-TYPE1-EMPTY"
534 });
535 }
536 }
537 }
538 }
539}
540
542 const dicom_dataset& dataset,
543 dicom_tag tag,
544 std::string_view name,
545 std::vector<validation_finding>& findings) const {
546
547 if (!dataset.contains(tag)) {
548 findings.push_back({
550 tag,
551 std::string("Type 2 attribute missing: ") + std::string(name)
552 + " (" + tag.to_string() + ")",
553 "HMSEG-TYPE2-MISSING"
554 });
555 }
556}
557
559 const dicom_dataset& dataset,
560 std::vector<validation_finding>& findings) const {
561
562 if (!dataset.contains(tags::modality)) {
563 return; // Already reported
564 }
565
566 auto modality = dataset.get_string(tags::modality);
567 if (modality != "SEG") {
568 findings.push_back({
571 "Modality must be 'SEG' for Heightmap Segmentation objects, "
572 "found: " + modality,
573 "HMSEG-ERR-001"
574 });
575 }
576}
577
579 const dicom_dataset& dataset,
580 std::vector<validation_finding>& findings) const {
581
583 return; // Already reported
584 }
585
586 auto seg_type =
588 if (seg_type != "HEIGHTMAP") {
589 findings.push_back({
592 "SegmentationType must be 'HEIGHTMAP' for Heightmap Segmentation "
593 "objects, found: " + seg_type,
594 "HMSEG-ERR-006"
595 });
596 }
597}
598
600 const dicom_dataset& dataset,
601 std::vector<validation_finding>& findings) const {
602
603 // For Heightmap SEG, SamplesPerPixel must be 1
604 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
605 if (samples && *samples != 1) {
606 findings.push_back({
609 "SamplesPerPixel must be 1 for Heightmap Segmentation objects",
610 "HMSEG-PXL-ERR-001"
611 });
612 }
613
614 // PhotometricInterpretation must be MONOCHROME2
616 auto photometric =
618 if (photometric != "MONOCHROME2") {
619 findings.push_back({
622 "PhotometricInterpretation must be MONOCHROME2 for "
623 "Heightmap Segmentation",
624 "HMSEG-PXL-ERR-002"
625 });
626 }
627 }
628
629 // Heightmap segmentation uses 32-bit float or 16-bit unsigned
630 // for height values (sub-pixel precision)
631 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
632 if (bits_allocated) {
633 if (*bits_allocated != 16 && *bits_allocated != 32) {
634 findings.push_back({
637 "BitsAllocated is typically 16 or 32 for Heightmap "
638 "Segmentation, found: "
639 + std::to_string(*bits_allocated),
640 "HMSEG-PXL-WARN-001"
641 });
642 }
643 }
644}
645
646// =============================================================================
647// Convenience Functions
648// =============================================================================
649
652 return validator.validate(dataset);
653}
654
657 return validator.quick_check(dataset);
658}
659
661 constexpr dicom_tag segmentation_type{0x0062, 0x0001};
662 if (!dataset.contains(segmentation_type)) {
663 return false;
664 }
665 return dataset.get_string(segmentation_type) == "HEIGHTMAP";
666}
667
668} // 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.
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against Heightmap Segmentation IOD.
void check_segmentation_type(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 validate_enhanced_general_equipment_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
validation_result validate_segments(const core::dicom_dataset &dataset) const
Validate segment sequence completeness.
void validate_multiframe_functional_groups_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_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const heightmap_seg_validation_options & options() const noexcept
Get the validation options.
void validate_heightmap_segmentation_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_equipment_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
heightmap_seg_iod_validator()=default
Construct validator with default options.
void validate_single_segment(const core::dicom_dataset &segment_item, size_t segment_index, std::vector< validation_finding > &findings) const
void validate_heightmap_segmentation_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const heightmap_seg_validation_options &options)
Set validation options.
void validate_common_instance_reference_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_general_image_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 check_modality(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
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_image_pixel_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
Compile-time constants for commonly used DICOM tags.
Heightmap 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 heightmap_segmentation_storage_uid
Heightmap Segmentation Storage SOP Class UID (Supplement 240)
Definition seg_storage.h:46
segmentation_type
Segmentation type (0062,0001)
Definition seg_storage.h:79
constexpr core::dicom_tag segment_label
Segment Label (0062,0005)
constexpr core::dicom_tag segment_sequence
Segment Sequence (0062,0002)
constexpr core::dicom_tag dimension_organization_sequence
Dimension Organization Sequence (0020,9221)
constexpr core::dicom_tag shared_functional_groups_sequence
Shared Functional Groups Sequence (5200,9229)
constexpr core::dicom_tag number_of_frames
Number of Frames (0028,0008)
constexpr core::dicom_tag segmented_property_category_code_sequence
Segmented Property Category Code Sequence (0062,0003)
constexpr core::dicom_tag segment_number
Segment Number (0062,0004)
constexpr core::dicom_tag segmented_property_type_code_sequence
Segmented Property Type Code Sequence (0062,000F)
constexpr core::dicom_tag manufacturer
Enhanced General Equipment Module tags.
constexpr core::dicom_tag dimension_index_sequence
Dimension Index Sequence (0020,9222)
constexpr core::dicom_tag referenced_series_sequence
Referenced Series Sequence (0008,1115) - Common Instance Reference Module.
constexpr core::dicom_tag segmentation_type
Segmentation Type (0062,0001) - must be "HEIGHTMAP".
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)
@ warning
Non-critical - IOD may have issues.
validation_result validate_heightmap_seg_iod(const core::dicom_dataset &dataset)
Validate a Heightmap Segmentation dataset with default options.
bool is_heightmap_segmentation(const core::dicom_dataset &dataset)
Check if dataset is a heightmap segmentation.
bool is_valid_heightmap_seg_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid Heightmap Segmentation object.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_heightmap_instance
Validate heightmap-specific instance attributes.
bool check_type2
Check Type 2 (required, can be empty) attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name