PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
pet_iod_validator.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <sstream>
15
17
18using namespace kcenon::pacs::core;
19
20// PET-specific DICOM tags
21namespace pet_tags {
22 // PET Series Module
23 constexpr dicom_tag series_type{0x0054, 0x1000};
24 constexpr dicom_tag units{0x0054, 0x1001};
25 constexpr dicom_tag counts_source{0x0054, 0x1002};
26
27 // PET Image Module
28 constexpr dicom_tag image_type{0x0008, 0x0008};
29 constexpr dicom_tag frame_reference_time{0x0054, 0x1300};
30 constexpr dicom_tag decay_correction{0x0054, 0x1102};
31 constexpr dicom_tag decay_factor{0x0054, 0x1321};
32 constexpr dicom_tag rescale_slope{0x0028, 0x1053};
33
34 // Radiopharmaceutical Information
36 constexpr dicom_tag radionuclide_total_dose{0x0018, 0x1074};
37 constexpr dicom_tag radionuclide_half_life{0x0018, 0x1075};
38
39 // Frame of Reference
40 constexpr dicom_tag frame_of_reference_uid{0x0020, 0x0052};
41 constexpr dicom_tag position_reference_indicator{0x0020, 0x1040};
42
43 // Image Plane
44 constexpr dicom_tag pixel_spacing{0x0028, 0x0030};
45 constexpr dicom_tag image_orientation_patient{0x0020, 0x0037};
46 constexpr dicom_tag image_position_patient{0x0020, 0x0032};
47 constexpr dicom_tag slice_thickness{0x0018, 0x0050};
48
49 // Reconstruction
50 constexpr dicom_tag reconstruction_method{0x0054, 0x1103};
51
52 // Corrections
53 constexpr dicom_tag attenuation_correction_method{0x0054, 0x1101};
54 constexpr dicom_tag randoms_correction_method{0x0054, 0x1100};
55}
56
57// =============================================================================
58// pet_iod_validator Implementation
59// =============================================================================
60
62 : options_(options) {}
63
65 validation_result result;
66 result.is_valid = true;
67
68 // Validate mandatory modules
70 validate_patient_module(dataset, result.findings);
73 validate_pet_series_module(dataset, result.findings);
75 validate_pet_image_module(dataset, result.findings);
76 validate_sop_common_module(dataset, result.findings);
77 }
78
82 }
83
84 // PET-specific validation
88 }
89 validate_suv_parameters(dataset, result.findings);
92 validate_corrections(dataset, result.findings);
93 }
94 }
95
96 // Check for errors
97 for (const auto& finding : result.findings) {
98 if (finding.severity == validation_severity::error) {
99 result.is_valid = false;
100 break;
101 }
102 if (options_.strict_mode && finding.severity == validation_severity::warning) {
103 result.is_valid = false;
104 break;
105 }
106 }
107
108 return result;
109}
110
112 // Check only Type 1 required attributes for quick validation
113
114 // General Study Module Type 1
115 if (!dataset.contains(tags::study_instance_uid)) return false;
116
117 // General Series Module Type 1
118 if (!dataset.contains(tags::modality)) return false;
119 if (!dataset.contains(tags::series_instance_uid)) return false;
120
121 // Check modality is PT
122 auto modality = dataset.get_string(tags::modality);
123 if (modality != "PT") return false;
124
125 // Frame of Reference Module Type 1
126 if (!dataset.contains(pet_tags::frame_of_reference_uid)) return false;
127
128 // Image Pixel Module Type 1
129 if (!dataset.contains(tags::samples_per_pixel)) return false;
130 if (!dataset.contains(tags::photometric_interpretation)) return false;
131 if (!dataset.contains(tags::rows)) return false;
132 if (!dataset.contains(tags::columns)) return false;
133 if (!dataset.contains(tags::bits_allocated)) return false;
134 if (!dataset.contains(tags::bits_stored)) return false;
135 if (!dataset.contains(tags::high_bit)) return false;
136 if (!dataset.contains(tags::pixel_representation)) return false;
137 if (!dataset.contains(tags::pixel_data)) return false;
138
139 // SOP Common Module Type 1
140 if (!dataset.contains(tags::sop_class_uid)) return false;
141 if (!dataset.contains(tags::sop_instance_uid)) return false;
142
143 return true;
144}
145
147 return options_;
148}
149
153
154// =============================================================================
155// Module Validation Methods
156// =============================================================================
157
159 const dicom_dataset& dataset,
160 std::vector<validation_finding>& findings) const {
161
162 // Patient Module - All attributes are Type 2
163 if (options_.check_type2) {
164 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
165 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
166 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
167 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
168 }
169}
170
172 const dicom_dataset& dataset,
173 std::vector<validation_finding>& findings) const {
174
175 // Type 1
176 if (options_.check_type1) {
177 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
178 }
179
180 // Type 2
181 if (options_.check_type2) {
182 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
183 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
184 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
185 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
186 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
187 }
188}
189
191 const dicom_dataset& dataset,
192 std::vector<validation_finding>& findings) const {
193
194 // Type 1
195 if (options_.check_type1) {
196 check_type1_attribute(dataset, tags::modality, "Modality", findings);
197 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
198
199 // Special check: Modality must be "PT"
200 check_modality(dataset, findings);
201 }
202
203 // Type 2
204 if (options_.check_type2) {
205 check_type2_attribute(dataset, tags::series_number, "SeriesNumber", findings);
206 }
207}
208
210 const dicom_dataset& dataset,
211 std::vector<validation_finding>& findings) const {
212
213 // PET Series Module Type 1 attributes
214 if (options_.check_type1) {
215 check_type1_attribute(dataset, pet_tags::series_type, "SeriesType", findings);
216 check_type1_attribute(dataset, pet_tags::units, "Units", findings);
217 check_type1_attribute(dataset, pet_tags::counts_source, "CountsSource", findings);
218 }
219
220 // Validate Units value
221 if (dataset.contains(pet_tags::units)) {
222 auto units = dataset.get_string(pet_tags::units);
223 if (units != "CNTS" && units != "BQML" && units != "GML" &&
224 units != "PROPCNTS" && units != "NONE") {
225 findings.push_back({
228 "Unusual Units value for PET: " + units +
229 " (expected CNTS, BQML, or GML)",
230 "PT-WARN-001"
231 });
232 }
233 }
234
235 // Validate SeriesType
236 if (dataset.contains(pet_tags::series_type)) {
237 auto series_type = dataset.get_string(pet_tags::series_type);
238 // SeriesType should be like "STATIC\IMAGE" or "WHOLE BODY\IMAGE"
239 if (series_type.find('\\') == std::string::npos) {
240 findings.push_back({
243 "SeriesType should contain backslash separator (e.g., STATIC\\IMAGE)",
244 "PT-INFO-001"
245 });
246 }
247 }
248}
249
251 const dicom_dataset& dataset,
252 std::vector<validation_finding>& findings) const {
253
254 // Type 1
255 if (options_.check_type1) {
257 "FrameOfReferenceUID", findings);
258 }
259
260 // Type 2
261 if (options_.check_type2) {
263 "PositionReferenceIndicator", findings);
264 }
265}
266
268 const dicom_dataset& dataset,
269 std::vector<validation_finding>& findings) const {
270
271 // Type 1
272 if (options_.check_type1) {
273 check_type1_attribute(dataset, pet_tags::image_type, "ImageType", findings);
275 "FrameReferenceTime", findings);
276 }
277
278 // Type 1C - Decay Correction (required if Units = BQML)
280 if (dataset.contains(pet_tags::units)) {
281 auto units = dataset.get_string(pet_tags::units);
282 if (units == "BQML") {
283 if (!dataset.contains(pet_tags::decay_correction)) {
284 findings.push_back({
287 "DecayCorrection is required when Units = BQML",
288 "PT-ERR-001"
289 });
290 }
291 }
292 }
293 }
294
295 // Validate DecayCorrection value
297 auto decay = dataset.get_string(pet_tags::decay_correction);
298 if (decay != "NONE" && decay != "START" && decay != "ADMIN") {
299 findings.push_back({
302 "Invalid DecayCorrection value: " + decay +
303 " (expected NONE, START, or ADMIN)",
304 "PT-WARN-002"
305 });
306 }
307 }
308}
309
311 const dicom_dataset& dataset,
312 std::vector<validation_finding>& findings) const {
313
314 // Type 1 attributes
315 if (options_.check_type1) {
316 check_type1_attribute(dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
318 "PhotometricInterpretation", findings);
319 check_type1_attribute(dataset, tags::rows, "Rows", findings);
320 check_type1_attribute(dataset, tags::columns, "Columns", findings);
321 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated", findings);
322 check_type1_attribute(dataset, tags::bits_stored, "BitsStored", findings);
323 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
324 check_type1_attribute(dataset, tags::pixel_representation, "PixelRepresentation", findings);
325 check_type1_attribute(dataset, tags::pixel_data, "PixelData", findings);
326 }
327
328 // Validate pixel data consistency
330 check_pixel_data_consistency(dataset, findings);
331 }
332}
333
335 const dicom_dataset& dataset,
336 std::vector<validation_finding>& findings) const {
337
338 // Type 1
339 if (options_.check_type1) {
340 check_type1_attribute(dataset, pet_tags::pixel_spacing, "PixelSpacing", findings);
342 "ImageOrientationPatient", findings);
344 "ImagePositionPatient", findings);
345 }
346
347 // Type 2
348 if (options_.check_type2) {
349 check_type2_attribute(dataset, pet_tags::slice_thickness, "SliceThickness", findings);
350 }
351}
352
354 const dicom_dataset& dataset,
355 std::vector<validation_finding>& findings) const {
356
357 // Type 1
358 if (options_.check_type1) {
359 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
360 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
361 }
362
363 // Validate SOP Class UID is a PET storage class
364 if (dataset.contains(tags::sop_class_uid)) {
365 auto sop_class = dataset.get_string(tags::sop_class_uid);
367 findings.push_back({
370 "SOPClassUID is not a recognized PET Storage SOP Class: " + sop_class,
371 "PT-ERR-002"
372 });
373 }
374 }
375}
376
377// =============================================================================
378// PET-Specific Validation
379// =============================================================================
380
382 const dicom_dataset& dataset,
383 std::vector<validation_finding>& findings) const {
384
385 // Radiopharmaceutical Information Sequence is Type 2
386 if (options_.check_type2) {
388 "RadiopharmaceuticalInformationSequence", findings);
389 }
390
391 // If sequence is present, validate its contents
393 // For quantitative PET (BQML), dose and half-life are critical
394 if (dataset.contains(pet_tags::units)) {
395 auto units = dataset.get_string(pet_tags::units);
396 if (units == "BQML" || units == "GML") {
397 // Check for required dose information
399 findings.push_back({
402 "RadionuclideTotalDose recommended for quantitative PET",
403 "PT-WARN-003"
404 });
405 }
407 findings.push_back({
410 "RadionuclideHalfLife recommended for quantitative PET",
411 "PT-WARN-004"
412 });
413 }
414 }
415 }
416 }
417}
418
420 const dicom_dataset& dataset,
421 std::vector<validation_finding>& findings) const {
422
423 // SUV calculation requires specific parameters
424 if (dataset.contains(pet_tags::units)) {
425 auto units = dataset.get_string(pet_tags::units);
426
427 // For SUV calculation, rescale parameters are important
428 if (units == "BQML" || units == "GML") {
429 if (!dataset.contains(pet_tags::rescale_slope)) {
430 findings.push_back({
433 "RescaleSlope recommended for accurate SUV calculation",
434 "PT-INFO-002"
435 });
436 }
437 }
438
439 // Decay factor for quantitative analysis
440 if (units == "BQML") {
441 if (!dataset.contains(pet_tags::decay_factor)) {
442 findings.push_back({
445 "DecayFactor recommended for quantitative analysis",
446 "PT-INFO-003"
447 });
448 }
449 }
450 }
451}
452
454 const dicom_dataset& dataset,
455 std::vector<validation_finding>& findings) const {
456
457 // Reconstruction Method is informational but important for interpretation
459 findings.push_back({
462 "ReconstructionMethod not specified - helps with interpretation",
463 "PT-INFO-004"
464 });
465 }
466}
467
469 const dicom_dataset& dataset,
470 std::vector<validation_finding>& findings) const {
471
472 // Attenuation Correction Method is Type 3 but important
474 findings.push_back({
477 "AttenuationCorrectionMethod not specified",
478 "PT-INFO-005"
479 });
480 }
481
482 // For quantitative PET, corrections are critical
483 if (dataset.contains(pet_tags::units)) {
484 auto units = dataset.get_string(pet_tags::units);
485 if (units == "BQML" || units == "GML") {
486 // Randoms correction
488 findings.push_back({
491 "RandomsCorrectionMethod recommended for quantitative PET",
492 "PT-INFO-006"
493 });
494 }
495 }
496 }
497}
498
499// =============================================================================
500// Attribute Validation Helpers
501// =============================================================================
502
504 const dicom_dataset& dataset,
505 dicom_tag tag,
506 std::string_view name,
507 std::vector<validation_finding>& findings) const {
508
509 if (!dataset.contains(tag)) {
510 findings.push_back({
512 tag,
513 std::string("Type 1 attribute missing: ") + std::string(name) +
514 " (" + tag.to_string() + ")",
515 "PT-TYPE1-MISSING"
516 });
517 } else {
518 // Type 1 must have a value (cannot be empty)
519 auto value = dataset.get_string(tag);
520 if (value.empty()) {
521 findings.push_back({
523 tag,
524 std::string("Type 1 attribute has empty value: ") +
525 std::string(name) + " (" + tag.to_string() + ")",
526 "PT-TYPE1-EMPTY"
527 });
528 }
529 }
530}
531
533 const dicom_dataset& dataset,
534 dicom_tag tag,
535 std::string_view name,
536 std::vector<validation_finding>& findings) const {
537
538 // Type 2 must be present but can be empty
539 if (!dataset.contains(tag)) {
540 findings.push_back({
542 tag,
543 std::string("Type 2 attribute missing: ") + std::string(name) +
544 " (" + tag.to_string() + ")",
545 "PT-TYPE2-MISSING"
546 });
547 }
548}
549
551 const dicom_dataset& dataset,
552 std::vector<validation_finding>& findings) const {
553
554 if (!dataset.contains(tags::modality)) {
555 return; // Already reported as Type 1 missing
556 }
557
558 auto modality = dataset.get_string(tags::modality);
559 if (modality != "PT") {
560 findings.push_back({
563 "Modality must be 'PT' for PET images, found: " + modality,
564 "PT-ERR-003"
565 });
566 }
567}
568
570 const dicom_dataset& dataset,
571 std::vector<validation_finding>& findings) const {
572
573 // Check BitsStored <= BitsAllocated
574 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
575 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
576 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
577
578 if (bits_allocated && bits_stored) {
579 if (*bits_stored > *bits_allocated) {
580 findings.push_back({
583 "BitsStored (" + std::to_string(*bits_stored) +
584 ") exceeds BitsAllocated (" + std::to_string(*bits_allocated) + ")",
585 "PT-ERR-004"
586 });
587 }
588 }
589
590 // Check HighBit == BitsStored - 1
591 if (bits_stored && high_bit) {
592 if (*high_bit != *bits_stored - 1) {
593 findings.push_back({
596 "HighBit (" + std::to_string(*high_bit) +
597 ") should typically be BitsStored - 1 (" +
598 std::to_string(*bits_stored - 1) + ")",
599 "PT-WARN-005"
600 });
601 }
602 }
603
604 // Check photometric interpretation is valid for PET
606 auto photometric = dataset.get_string(tags::photometric_interpretation);
607 if (!sop_classes::is_valid_pet_photometric(photometric)) {
608 findings.push_back({
611 "Unusual photometric interpretation for PET: " + photometric +
612 " (expected MONOCHROME2)",
613 "PT-WARN-006"
614 });
615 }
616 }
617
618 // PET images should be grayscale (SamplesPerPixel = 1)
619 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
620 if (samples && *samples != 1) {
621 findings.push_back({
624 "PET images should have SamplesPerPixel = 1, found: " +
625 std::to_string(*samples),
626 "PT-WARN-007"
627 });
628 }
629}
630
631// =============================================================================
632// Convenience Functions
633// =============================================================================
634
636 pet_iod_validator validator;
637 return validator.validate(dataset);
638}
639
641 pet_iod_validator validator;
642 return validator.quick_check(dataset);
643}
644
645} // 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 validate_general_series_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
void validate_image_plane_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_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_pet_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
void set_options(const pet_validation_options &options)
Set validation options.
void validate_corrections(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 PET IOD.
void validate_frame_of_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.
const pet_validation_options & options() const noexcept
Get the validation options.
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_reconstruction_info(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 check_pixel_data_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_radiopharmaceutical_info(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_suv_parameters(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_pet_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
pet_iod_validator()=default
Construct validator with default options.
void validate_patient_module(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_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.
bool is_pet_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a PET Storage SOP Class.
bool is_valid_pet_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for PET.
bool is_valid_pet_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid PET image.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
validation_result validate_pet_iod(const core::dicom_dataset &dataset)
Validate a PET dataset with default options.
PET (Positron Emission Tomography) Image IOD Validator.
Positron Emission Tomography (PET) Image Storage SOP Classes.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_corrections
Validate attenuation and scatter correction.
bool check_type1
Check Type 1 (required) attributes.
bool validate_pet_specific
Validate PET-specific attributes (SUV, reconstruction, etc.)
bool strict_mode
Strict mode - treat warnings as errors.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool validate_radiopharmaceutical
Validate radiopharmaceutical information.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name