PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
parametric_map_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
15
17
18using namespace kcenon::pacs::core;
19
20// =============================================================================
21// parametric_map_iod_validator Implementation
22// =============================================================================
23
27
30 validation_result result;
31 result.is_valid = true;
32
33 // Validate mandatory modules
35 validate_patient_module(dataset, result.findings);
40 validate_sop_common_module(dataset, result.findings);
41 }
42
43 // Parametric Map Image Module (Content Label, RWVM)
45
48 }
49
53 }
54
57 }
58
59 // Check for errors
60 for (const auto& finding : result.findings) {
61 if (finding.severity == validation_severity::error) {
62 result.is_valid = false;
63 break;
64 }
66 && finding.severity == validation_severity::warning) {
67 result.is_valid = false;
68 break;
69 }
70 }
71
72 return result;
73}
74
76 const dicom_dataset& dataset) const {
77
78 // General Study Module
79 if (!dataset.contains(tags::study_instance_uid)) return false;
80
81 // General Series Module
82 if (!dataset.contains(tags::modality)) return false;
83 if (!dataset.contains(tags::series_instance_uid)) return false;
84
85 // Check modality
86 auto modality = dataset.get_string(tags::modality);
87 if (modality != "RWV" && modality != "PMAP") return false;
88
89 // Parametric Map Image Module
90 if (!dataset.contains(pmap_iod_tags::content_label)) return false;
92 return false;
93
94 // Multi-frame
95 if (!dataset.contains(pmap_iod_tags::number_of_frames)) return false;
96
97 // Image Pixel Module
98 if (!dataset.contains(tags::rows)) return false;
99 if (!dataset.contains(tags::columns)) return false;
100
101 // SOP Common Module
102 if (!dataset.contains(tags::sop_class_uid)) return false;
103 if (!dataset.contains(tags::sop_instance_uid)) return false;
104
105 // Verify SOP Class is Parametric Map
106 auto sop_class = dataset.get_string(tags::sop_class_uid);
108 return false;
109
110 return true;
111}
112
115 return options_;
116}
117
122
123// =============================================================================
124// Module Validation Methods
125// =============================================================================
126
128 const dicom_dataset& dataset,
129 std::vector<validation_finding>& findings) const {
130
131 if (options_.check_type2) {
133 "PatientName", findings);
135 "PatientID", findings);
137 "PatientBirthDate", findings);
139 "PatientSex", findings);
140 }
141}
142
144 const dicom_dataset& dataset,
145 std::vector<validation_finding>& findings) const {
146
147 if (options_.check_type1) {
149 "StudyInstanceUID", findings);
150 }
151
152 if (options_.check_type2) {
154 "StudyDate", findings);
156 "StudyTime", findings);
158 "ReferringPhysicianName", findings);
160 "StudyID", findings);
162 "AccessionNumber", findings);
163 }
164}
165
167 const dicom_dataset& dataset,
168 std::vector<validation_finding>& findings) const {
169
170 if (options_.check_type1) {
172 "Modality", findings);
174 "SeriesInstanceUID", findings);
175 check_modality(dataset, findings);
176 }
177
178 if (options_.check_type2) {
180 "SeriesNumber", findings);
181 }
182}
183
185 const dicom_dataset& dataset,
186 std::vector<validation_finding>& findings) const {
187
188 if (options_.check_type2) {
190 "Manufacturer", findings);
191 }
192}
193
195 const dicom_dataset& dataset,
196 std::vector<validation_finding>& findings) const {
197
198 if (options_.check_type1) {
200 "Manufacturer", findings);
202 "ManufacturerModelName", findings);
204 "DeviceSerialNumber", findings);
206 "SoftwareVersions", findings);
207 }
208}
209
211 const dicom_dataset& dataset,
212 std::vector<validation_finding>& findings) const {
213
214 if (options_.check_type1) {
216 "SamplesPerPixel", findings);
218 "PhotometricInterpretation", findings);
219 check_type1_attribute(dataset, tags::rows, "Rows", findings);
220 check_type1_attribute(dataset, tags::columns, "Columns", findings);
222 "BitsAllocated", findings);
224 "BitsStored", findings);
226 "HighBit", findings);
228 "PixelRepresentation", findings);
229 }
230
231 // Parametric Map-specific pixel data constraints
232 check_pixel_data_consistency(dataset, findings);
233}
234
236 const dicom_dataset& dataset,
237 std::vector<validation_finding>& findings) const {
238
239 // Content Label is Type 1
240 if (options_.check_type1) {
242 "ContentLabel", findings);
243 }
244
245 // Content Description is Type 2
246 if (options_.check_type2) {
248 "ContentDescription", findings);
250 "ContentCreatorName", findings);
251 }
252
253 // Real World Value Mapping Sequence is Type 1
256 findings.push_back({
259 "RealWorldValueMappingSequence (0040,9096) is required for "
260 "Parametric Map objects",
261 "PMAP-ERR-004"
262 });
263 }
264 }
265}
266
268 const dicom_dataset& dataset,
269 std::vector<validation_finding>& findings) const {
270
271 // NumberOfFrames is Type 1 (Parametric Maps are always multi-frame)
273 findings.push_back({
276 "NumberOfFrames (0028,0008) is required for Parametric Map objects "
277 "(always multi-frame)",
278 "PMAP-ERR-001"
279 });
280 }
281
282 if (options_.check_type1) {
283 if (!dataset.contains(
285 findings.push_back({
288 "SharedFunctionalGroupsSequence (5200,9229) should be present "
289 "for multi-frame Parametric Map objects",
290 "PMAP-WARN-001"
291 });
292 }
293
294 if (!dataset.contains(
296 findings.push_back({
299 "PerFrameFunctionalGroupsSequence (5200,9230) should be present "
300 "for multi-frame Parametric Map objects",
301 "PMAP-WARN-004"
302 });
303 }
304 }
305}
306
308 const dicom_dataset& dataset,
309 std::vector<validation_finding>& findings) const {
310
311 if (options_.check_type1) {
312 if (!dataset.contains(
314 findings.push_back({
317 "Type 1 attribute missing: DimensionOrganizationSequence "
318 "(0020,9221)",
319 "PMAP-TYPE1-MISSING"
320 });
321 }
323 findings.push_back({
326 "Type 1 attribute missing: DimensionIndexSequence "
327 "(0020,9222)",
328 "PMAP-TYPE1-MISSING"
329 });
330 }
331 }
332}
333
335 const dicom_dataset& dataset,
336 std::vector<validation_finding>& findings) const {
337
340 findings.push_back({
343 "ReferencedSeriesSequence (0008,1115) should be present for "
344 "source image references",
345 "PMAP-WARN-003"
346 });
347 }
348 }
349}
350
352 const dicom_dataset& dataset,
353 std::vector<validation_finding>& findings) const {
354
355 if (options_.check_type1) {
357 "SOPClassUID", findings);
359 "SOPInstanceUID", findings);
360 }
361
362 // Validate SOP Class UID is a Parametric Map storage class
363 if (dataset.contains(tags::sop_class_uid)) {
364 auto sop_class = dataset.get_string(tags::sop_class_uid);
366 findings.push_back({
369 "SOPClassUID is not a recognized Parametric Map Storage SOP "
370 "Class: " + sop_class,
371 "PMAP-ERR-002"
372 });
373 }
374 }
375}
376
377// =============================================================================
378// Attribute Validation Helpers
379// =============================================================================
380
382 const dicom_dataset& dataset,
383 dicom_tag tag,
384 std::string_view name,
385 std::vector<validation_finding>& findings) const {
386
387 if (!dataset.contains(tag)) {
388 findings.push_back({
390 tag,
391 std::string("Type 1 attribute missing: ") + std::string(name) +
392 " (" + tag.to_string() + ")",
393 "PMAP-TYPE1-MISSING"
394 });
395 } else {
396 const auto* element = dataset.get(tag);
397 if (element != nullptr) {
398 if (element->is_sequence()) {
399 if (element->sequence_items().empty()) {
400 findings.push_back({
402 tag,
403 std::string("Type 1 sequence has no items: ") +
404 std::string(name) + " (" + tag.to_string() + ")",
405 "PMAP-TYPE1-EMPTY"
406 });
407 }
408 } else {
409 auto value = dataset.get_string(tag);
410 if (value.empty()) {
411 findings.push_back({
413 tag,
414 std::string("Type 1 attribute has empty value: ") +
415 std::string(name) + " (" + tag.to_string() + ")",
416 "PMAP-TYPE1-EMPTY"
417 });
418 }
419 }
420 }
421 }
422}
423
425 const dicom_dataset& dataset,
426 dicom_tag tag,
427 std::string_view name,
428 std::vector<validation_finding>& findings) const {
429
430 if (!dataset.contains(tag)) {
431 findings.push_back({
433 tag,
434 std::string("Type 2 attribute missing: ") + std::string(name) +
435 " (" + tag.to_string() + ")",
436 "PMAP-TYPE2-MISSING"
437 });
438 }
439}
440
442 const dicom_dataset& dataset,
443 std::vector<validation_finding>& findings) const {
444
445 if (!dataset.contains(tags::modality)) {
446 return; // Already reported
447 }
448
449 auto modality = dataset.get_string(tags::modality);
450 if (modality != "RWV" && modality != "PMAP") {
451 findings.push_back({
454 "Modality must be 'RWV' or 'PMAP' for Parametric Map objects, "
455 "found: " + modality,
456 "PMAP-ERR-003"
457 });
458 }
459}
460
462 const dicom_dataset& dataset,
463 std::vector<validation_finding>& findings) const {
464
465 // SamplesPerPixel must be 1
466 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
467 if (samples && *samples != 1) {
468 findings.push_back({
471 "SamplesPerPixel must be 1 for Parametric Map objects",
472 "PMAP-ERR-007"
473 });
474 }
475
476 // PhotometricInterpretation must be MONOCHROME2
478 auto photometric = dataset.get_string(
481 findings.push_back({
484 "PhotometricInterpretation must be MONOCHROME2 for Parametric "
485 "Map objects, found: " + photometric,
486 "PMAP-ERR-006"
487 });
488 }
489 }
490
491 // BitsAllocated must be 32 or 64 (float or double)
492 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
493 if (bits_allocated
495 *bits_allocated)) {
496 findings.push_back({
499 "BitsAllocated must be 32 or 64 for Parametric Map objects "
500 "(float or double), found: " +
501 std::to_string(*bits_allocated),
502 "PMAP-ERR-008"
503 });
504 }
505
506 // BitsStored must not exceed BitsAllocated
507 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
508 if (bits_allocated && bits_stored && *bits_stored > *bits_allocated) {
509 findings.push_back({
512 "BitsStored (" + std::to_string(*bits_stored) +
513 ") exceeds BitsAllocated (" +
514 std::to_string(*bits_allocated) + ")",
515 "PMAP-ERR-005"
516 });
517 }
518
519 // HighBit should be BitsStored - 1
520 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
521 if (bits_stored && high_bit && *high_bit != (*bits_stored - 1)) {
522 findings.push_back({
525 "HighBit (" + std::to_string(*high_bit) +
526 ") does not match BitsStored-1 (" +
527 std::to_string(*bits_stored - 1) + ")",
528 "PMAP-WARN-002"
529 });
530 }
531}
532
533// =============================================================================
534// Convenience Functions
535// =============================================================================
536
539 return validator.validate(dataset);
540}
541
544 return validator.quick_check(dataset);
545}
546
547} // 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_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const pmap_validation_options &options)
Set validation options.
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_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
parametric_map_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 check_pixel_data_consistency(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
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_enhanced_general_equipment_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
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against Parametric Map IOD.
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required parametric map attributes.
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_common_instance_reference_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_parametric_map_image_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
const pmap_validation_options & options() const noexcept
Get the validation options.
void check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
Compile-time constants for commonly used DICOM tags.
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 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.
bool is_parametric_map_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is Parametric Map Storage.
bool is_valid_parametric_map_bits_allocated(uint16_t bits_allocated) noexcept
Check if a BitsAllocated value is valid for parametric maps.
bool is_valid_parametric_map_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for parametric maps.
constexpr core::dicom_tag referenced_series_sequence
Referenced Series Sequence (0008,1115)
constexpr core::dicom_tag dimension_index_sequence
Dimension Index Sequence (0020,9222)
constexpr core::dicom_tag content_creator_name
Content Creator's Name (0070,0084) — Type 2.
constexpr core::dicom_tag manufacturer_model_name
Manufacturer's Model Name (0008,1090)
constexpr core::dicom_tag device_serial_number
Device Serial Number (0018,1000)
constexpr core::dicom_tag software_versions
Software Versions (0018,1020)
constexpr core::dicom_tag number_of_frames
Number of Frames (0028,0008)
constexpr core::dicom_tag per_frame_functional_groups_sequence
Per-Frame Functional Groups Sequence (5200,9230)
constexpr core::dicom_tag shared_functional_groups_sequence
Shared Functional Groups Sequence (5200,9229)
constexpr core::dicom_tag content_description
Content Description (0070,0081) — Type 2.
constexpr core::dicom_tag manufacturer
Manufacturer (0008,0070)
constexpr core::dicom_tag real_world_value_mapping_sequence
Real World Value Mapping Sequence (0040,9096)
constexpr core::dicom_tag content_label
Content Label (0070,0080) — Type 1.
constexpr core::dicom_tag dimension_organization_sequence
Dimension Organization Sequence (0020,9221)
@ warning
Non-critical - IOD may have issues.
bool is_valid_parametric_map_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid Parametric Map object.
validation_result validate_parametric_map_iod(const core::dicom_dataset &dataset)
Validate a Parametric Map dataset with default options.
Parametric Map IOD Validator.
Parametric Map Storage SOP Class.
bool validate_rwvm
Validate Real World Value Mapping Sequence.
bool validate_pixel_data
Validate pixel data consistency (bits, photometric, float constraints)
bool check_conditional
Check Type 1C/2C (conditionally required) 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