PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ct_processing_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
16
18
19using namespace kcenon::pacs::core;
20
21// =============================================================================
22// ct_processing_iod_validator Implementation
23// =============================================================================
24
28
30 const dicom_dataset& dataset) const {
31 validation_result result;
32 result.is_valid = true;
33
34 // Validate mandatory modules
36 validate_patient_module(dataset, result.findings);
41 validate_sop_common_module(dataset, result.findings);
42 }
43
45 validate_ct_image_module(dataset, result.findings);
46 }
47
50 }
51
52 // Determine overall validity
53 for (const auto& finding : result.findings) {
54 if (finding.severity == validation_severity::error) {
55 result.is_valid = false;
56 break;
57 }
59 finding.severity == validation_severity::warning) {
60 result.is_valid = false;
61 break;
62 }
63 }
64
65 return result;
66}
67
69 const dicom_dataset& dataset) const {
70 // General Study Module Type 1
71 if (!dataset.contains(tags::study_instance_uid)) return false;
72
73 // General Series Module Type 1
74 if (!dataset.contains(tags::modality)) return false;
75 if (!dataset.contains(tags::series_instance_uid)) return false;
76
77 // Check modality is CT
78 auto modality = dataset.get_string(tags::modality);
79 if (modality != "CT") return false;
80
81 // Frame of Reference Module Type 1
82 if (!dataset.contains(tags::frame_of_reference_uid)) return false;
83
84 // Image Pixel Module Type 1
85 if (!dataset.contains(tags::samples_per_pixel)) return false;
86 if (!dataset.contains(tags::photometric_interpretation)) return false;
87 if (!dataset.contains(tags::rows)) return false;
88 if (!dataset.contains(tags::columns)) return false;
89 if (!dataset.contains(tags::bits_allocated)) return false;
90 if (!dataset.contains(tags::bits_stored)) return false;
91 if (!dataset.contains(tags::high_bit)) return false;
92 if (!dataset.contains(tags::pixel_representation)) return false;
93 if (!dataset.contains(tags::pixel_data)) return false;
94
95 // SOP Common Module Type 1
96 if (!dataset.contains(tags::sop_class_uid)) return false;
97 if (!dataset.contains(tags::sop_instance_uid)) return false;
98
99 // Verify SOP Class UID is CT For Processing
100 auto sop_class = dataset.get_string(tags::sop_class_uid);
102 return false;
103 }
104
105 return true;
106}
107
110 return options_;
111}
112
117
118// =============================================================================
119// Module Validation Methods
120// =============================================================================
121
123 const dicom_dataset& dataset,
124 std::vector<validation_finding>& findings) const {
125
126 // Patient Module - All attributes are Type 2
127 if (options_.check_type2) {
128 check_type2_attribute(dataset, tags::patient_name, "PatientName",
129 findings);
130 check_type2_attribute(dataset, tags::patient_id, "PatientID",
131 findings);
133 "PatientBirthDate", findings);
134 check_type2_attribute(dataset, tags::patient_sex, "PatientSex",
135 findings);
136 }
137}
138
140 const dicom_dataset& dataset,
141 std::vector<validation_finding>& findings) const {
142
143 // Type 1
144 if (options_.check_type1) {
146 "StudyInstanceUID", findings);
147 }
148
149 // Type 2
150 if (options_.check_type2) {
151 check_type2_attribute(dataset, tags::study_date, "StudyDate",
152 findings);
153 check_type2_attribute(dataset, tags::study_time, "StudyTime",
154 findings);
156 "ReferringPhysicianName", findings);
157 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
159 "AccessionNumber", findings);
160 }
161}
162
164 const dicom_dataset& dataset,
165 std::vector<validation_finding>& findings) const {
166
167 // Type 1
168 if (options_.check_type1) {
169 check_type1_attribute(dataset, tags::modality, "Modality", findings);
171 "SeriesInstanceUID", findings);
172
173 // Modality must be "CT"
174 check_modality(dataset, findings);
175 }
176
177 // Type 2
178 if (options_.check_type2) {
179 check_type2_attribute(dataset, tags::series_number, "SeriesNumber",
180 findings);
181 }
182}
183
185 const dicom_dataset& dataset,
186 std::vector<validation_finding>& findings) const {
187
188 // Frame of Reference UID is Type 1 for CT
189 if (options_.check_type1) {
191 "FrameOfReferenceUID", findings);
192 }
193
194 // Position Reference Indicator is Type 2
195 if (options_.check_type2) {
196 constexpr dicom_tag position_reference_indicator{0x0020, 0x1040};
197 check_type2_attribute(dataset, position_reference_indicator,
198 "PositionReferenceIndicator", findings);
199 }
200}
201
203 const dicom_dataset& dataset,
204 std::vector<validation_finding>& findings) const {
205
206 // Manufacturer is Type 2
207 if (options_.check_type2) {
208 check_type2_attribute(dataset, tags::manufacturer, "Manufacturer",
209 findings);
210 }
211}
212
214 const dicom_dataset& dataset,
215 std::vector<validation_finding>& findings) const {
216
217 // CT Image Module
218
219 // ImageType is Type 1
220 if (options_.check_type1) {
221 check_type1_attribute(dataset, tags::image_type, "ImageType",
222 findings);
223 }
224
225 // Type 2 attributes
226 if (options_.check_type2) {
228 findings);
230 "RescaleIntercept", findings);
231 check_type2_attribute(dataset, tags::rescale_slope, "RescaleSlope",
232 findings);
233 }
234
235 // CT For Processing-specific informational checks
238 findings.push_back(
240 "SliceThickness (0018,0050) not present - slice geometry "
241 "information unavailable",
242 "CTP-INFO-001"});
243 }
244
246 findings.push_back(
249 "ConvolutionKernel (0018,1210) not present - reconstruction "
250 "algorithm unknown",
251 "CTP-INFO-002"});
252 }
253
254 // Image Position (Patient) is Type 1C
257 findings.push_back(
260 "ImagePositionPatient (0020,0032) missing - required when "
261 "Frame of Reference is present",
262 "CTP-WARN-001"});
263 }
264
265 // Image Orientation (Patient) is Type 1C
268 findings.push_back(
271 "ImageOrientationPatient (0020,0037) missing - required when "
272 "Frame of Reference is present",
273 "CTP-WARN-002"});
274 }
275
276 // DecompositionDescription is informational for processing images
278 findings.push_back(
281 "DecompositionDescription (0018,9380) not present - material "
282 "decomposition type unavailable",
283 "CTP-INFO-003"});
284 }
285 }
286}
287
289 const dicom_dataset& dataset,
290 std::vector<validation_finding>& findings) const {
291
292 // Type 1 attributes
293 if (options_.check_type1) {
295 "SamplesPerPixel", findings);
297 "PhotometricInterpretation", findings);
298 check_type1_attribute(dataset, tags::rows, "Rows", findings);
299 check_type1_attribute(dataset, tags::columns, "Columns", findings);
300 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated",
301 findings);
302 check_type1_attribute(dataset, tags::bits_stored, "BitsStored",
303 findings);
304 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
306 "PixelRepresentation", findings);
307 check_type1_attribute(dataset, tags::pixel_data, "PixelData",
308 findings);
309 }
310
311 // Validate pixel data consistency
313 check_pixel_data_consistency(dataset, findings);
314 }
315}
316
318 const dicom_dataset& dataset,
319 std::vector<validation_finding>& findings) const {
320
321 // Type 1
322 if (options_.check_type1) {
323 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID",
324 findings);
326 "SOPInstanceUID", findings);
327 }
328
329 // Validate SOP Class UID is CT For Processing
330 if (dataset.contains(tags::sop_class_uid)) {
331 auto sop_class = dataset.get_string(tags::sop_class_uid);
333 findings.push_back(
335 "SOPClassUID is not CT For Processing Image Storage: " +
336 sop_class,
337 "CTP-ERR-001"});
338 }
339 }
340}
341
342// =============================================================================
343// Attribute Validation Helpers
344// =============================================================================
345
347 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
348 std::vector<validation_finding>& findings) const {
349
350 if (!dataset.contains(tag)) {
351 findings.push_back(
353 std::string("Type 1 attribute missing: ") + std::string(name) +
354 " (" + tag.to_string() + ")",
355 "CTP-TYPE1-MISSING"});
356 } else {
357 // Type 1 must have a value (cannot be empty)
358 auto value = dataset.get_string(tag);
359 if (value.empty()) {
360 findings.push_back(
362 std::string("Type 1 attribute has empty value: ") +
363 std::string(name) + " (" + tag.to_string() + ")",
364 "CTP-TYPE1-EMPTY"});
365 }
366 }
367}
368
370 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
371 std::vector<validation_finding>& findings) const {
372
373 // Type 2 must be present but can be empty
374 if (!dataset.contains(tag)) {
375 findings.push_back(
377 std::string("Type 2 attribute missing: ") + std::string(name) +
378 " (" + tag.to_string() + ")",
379 "CTP-TYPE2-MISSING"});
380 }
381}
382
384 const dicom_dataset& dataset,
385 std::vector<validation_finding>& findings) const {
386
387 if (!dataset.contains(tags::modality)) {
388 return; // Already reported as Type 1 missing
389 }
390
391 auto modality = dataset.get_string(tags::modality);
392 if (modality != "CT") {
393 findings.push_back(
395 "Modality must be 'CT' for CT For Processing images, found: " +
396 modality,
397 "CTP-ERR-002"});
398 }
399}
400
402 const dicom_dataset& dataset,
403 std::vector<validation_finding>& findings) const {
404
405 // Check BitsStored <= BitsAllocated
406 auto bits_allocated =
407 dataset.get_numeric<uint16_t>(tags::bits_allocated);
408 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
409 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
410
411 if (bits_allocated && bits_stored) {
412 if (*bits_stored > *bits_allocated) {
413 findings.push_back(
415 "BitsStored (" + std::to_string(*bits_stored) +
416 ") exceeds BitsAllocated (" +
417 std::to_string(*bits_allocated) + ")",
418 "CTP-ERR-003"});
419 }
420 }
421
422 // Check HighBit == BitsStored - 1
423 if (bits_stored && high_bit) {
424 if (*high_bit != *bits_stored - 1) {
425 findings.push_back(
427 "HighBit (" + std::to_string(*high_bit) +
428 ") should typically be BitsStored - 1 (" +
429 std::to_string(*bits_stored - 1) + ")",
430 "CTP-WARN-003"});
431 }
432 }
433
434 // CT For Processing images must be grayscale
436 auto photometric =
438 if (!sop_classes::is_valid_ct_photometric(photometric)) {
439 findings.push_back(
442 "CT For Processing images must use MONOCHROME1 or "
443 "MONOCHROME2, found: " +
444 photometric,
445 "CTP-ERR-004"});
446 }
447 }
448
449 // CT For Processing images must have SamplesPerPixel = 1
450 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
451 if (samples && *samples != 1) {
452 findings.push_back(
454 "CT For Processing images require SamplesPerPixel = 1, found: " +
455 std::to_string(*samples),
456 "CTP-ERR-005"});
457 }
458
459 // Processing images may use various pixel representations
460 auto pixel_rep =
461 dataset.get_numeric<uint16_t>(tags::pixel_representation);
462 if (pixel_rep && *pixel_rep != 1) {
463 findings.push_back(
465 "CT For Processing images typically use signed pixel "
466 "representation (PixelRepresentation = 1) for quantitative values",
467 "CTP-INFO-004"});
468 }
469}
470
471// =============================================================================
472// Convenience Functions
473// =============================================================================
474
477 return validator.validate(dataset);
478}
479
482 return validator.quick_check(dataset);
483}
484
485} // 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.
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required attributes.
void validate_general_series_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 CT For Processing IOD.
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void check_pixel_data_consistency(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_frame_of_reference_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
ct_processing_iod_validator()=default
Construct validator with default options.
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
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_image_pixel_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 set_options(const ct_processing_validation_options &options)
Set validation options.
void validate_ct_image_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
const ct_processing_validation_options & options() const noexcept
Get the validation options.
CT For Processing Image IOD Validator.
CT Image Storage SOP Classes.
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 rescale_intercept
Rescale Intercept.
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 image_type
Image Type.
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 rescale_slope
Rescale Slope.
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 manufacturer
Manufacturer.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr std::string_view ct_for_processing_image_storage_uid
CT For Processing Image Storage SOP Class UID Multi-energy/spectral CT basis material decomposition i...
Definition ct_storage.h:44
bool is_valid_ct_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for CT.
constexpr core::dicom_tag image_position_patient
Image Position (Patient) (0020,0032)
constexpr core::dicom_tag image_orientation_patient
Image Orientation (Patient) (0020,0037)
constexpr core::dicom_tag slice_thickness
Slice Thickness (0018,0050)
constexpr core::dicom_tag convolution_kernel
Convolution Kernel (0018,1210)
constexpr core::dicom_tag decomposition_description
Decomposition Description (0018,9380) - describes the material decomposition.
bool is_valid_ct_processing_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid CT For Processing image.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
validation_result validate_ct_processing_iod(const core::dicom_dataset &dataset)
Validate a CT For Processing dataset with default options.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_ct_processing_params
Validate CT processing-specific attributes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
std::vector< validation_finding > findings
All findings during validation.
std::string_view name