PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ct_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_iod_validator Implementation
23// =============================================================================
24
26 : options_(options) {}
27
29 const dicom_dataset& dataset) const {
30 validation_result result;
31 result.is_valid = true;
32
33 // Validate mandatory modules (PS3.3 Table A.3-1)
35 validate_patient_module(dataset, result.findings);
40 validate_sop_common_module(dataset, result.findings);
41 }
42
44 validate_ct_image_module(dataset, result.findings);
45 }
46
49 }
50
51 // Determine overall validity
52 for (const auto& finding : result.findings) {
53 if (finding.severity == validation_severity::error) {
54 result.is_valid = false;
55 break;
56 }
58 finding.severity == validation_severity::warning) {
59 result.is_valid = false;
60 break;
61 }
62 }
63
64 return result;
65}
66
67bool ct_iod_validator::quick_check(const dicom_dataset& dataset) const {
68 // Check only Type 1 required attributes for quick validation
69
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 return true;
100}
101
103 return options_;
104}
105
109
110// =============================================================================
111// Module Validation Methods
112// =============================================================================
113
115 const dicom_dataset& dataset,
116 std::vector<validation_finding>& findings) const {
117
118 // Patient Module - All attributes are Type 2
119 if (options_.check_type2) {
120 check_type2_attribute(dataset, tags::patient_name, "PatientName",
121 findings);
122 check_type2_attribute(dataset, tags::patient_id, "PatientID",
123 findings);
125 "PatientBirthDate", findings);
126 check_type2_attribute(dataset, tags::patient_sex, "PatientSex",
127 findings);
128 }
129}
130
132 const dicom_dataset& dataset,
133 std::vector<validation_finding>& findings) const {
134
135 // Type 1
136 if (options_.check_type1) {
138 "StudyInstanceUID", findings);
139 }
140
141 // Type 2
142 if (options_.check_type2) {
143 check_type2_attribute(dataset, tags::study_date, "StudyDate",
144 findings);
145 check_type2_attribute(dataset, tags::study_time, "StudyTime",
146 findings);
148 "ReferringPhysicianName", findings);
149 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
151 "AccessionNumber", findings);
152 }
153}
154
156 const dicom_dataset& dataset,
157 std::vector<validation_finding>& findings) const {
158
159 // Type 1
160 if (options_.check_type1) {
161 check_type1_attribute(dataset, tags::modality, "Modality", findings);
163 "SeriesInstanceUID", findings);
164
165 // Modality must be "CT"
166 check_modality(dataset, findings);
167 }
168
169 // Type 2
170 if (options_.check_type2) {
171 check_type2_attribute(dataset, tags::series_number, "SeriesNumber",
172 findings);
173 }
174}
175
177 const dicom_dataset& dataset,
178 std::vector<validation_finding>& findings) const {
179
180 // Frame of Reference UID is Type 1 for CT
181 if (options_.check_type1) {
183 "FrameOfReferenceUID", findings);
184 }
185
186 // Position Reference Indicator is Type 2
187 if (options_.check_type2) {
188 constexpr dicom_tag position_reference_indicator{0x0020, 0x1040};
189 check_type2_attribute(dataset, position_reference_indicator,
190 "PositionReferenceIndicator", findings);
191 }
192}
193
195 const dicom_dataset& dataset,
196 std::vector<validation_finding>& findings) const {
197
198 // Manufacturer is Type 2
199 if (options_.check_type2) {
200 check_type2_attribute(dataset, tags::manufacturer, "Manufacturer",
201 findings);
202 }
203}
204
206 const dicom_dataset& dataset,
207 std::vector<validation_finding>& findings) const {
208
209 // CT Image Module (PS3.3 Section C.8.2.1)
210
211 // ImageType is Type 1
212 if (options_.check_type1) {
213 check_type1_attribute(dataset, tags::image_type, "ImageType",
214 findings);
215 }
216
217 // Type 2 attributes specific to CT Image Module
218 if (options_.check_type2) {
219 check_type2_attribute(dataset, ct_tags::kvp, "KVP", findings);
221 "RescaleIntercept", findings);
222 check_type2_attribute(dataset, tags::rescale_slope, "RescaleSlope",
223 findings);
224 }
225
226 // Type 2C/3 - informational checks for common CT attributes
228 if (!dataset.contains(ct_tags::slice_thickness)) {
229 findings.push_back(
231 "SliceThickness (0018,0050) not present - slice geometry "
232 "information unavailable",
233 "CT-INFO-001"});
234 }
235
237 findings.push_back(
239 "ConvolutionKernel (0018,1210) not present - reconstruction "
240 "algorithm unknown",
241 "CT-INFO-002"});
242 }
243
244 // Image Position (Patient) is Type 1C - required if
245 // Frame of Reference Module is present
248 findings.push_back(
251 "ImagePositionPatient (0020,0032) missing - required when "
252 "Frame of Reference is present",
253 "CT-WARN-001"});
254 }
255
256 // Image Orientation (Patient) is Type 1C
259 findings.push_back(
262 "ImageOrientationPatient (0020,0037) missing - required when "
263 "Frame of Reference is present",
264 "CT-WARN-002"});
265 }
266 }
267}
268
270 const dicom_dataset& dataset,
271 std::vector<validation_finding>& findings) const {
272
273 // Type 1 attributes
274 if (options_.check_type1) {
276 "SamplesPerPixel", findings);
278 "PhotometricInterpretation", findings);
279 check_type1_attribute(dataset, tags::rows, "Rows", findings);
280 check_type1_attribute(dataset, tags::columns, "Columns", findings);
281 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated",
282 findings);
283 check_type1_attribute(dataset, tags::bits_stored, "BitsStored",
284 findings);
285 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
287 "PixelRepresentation", findings);
288 check_type1_attribute(dataset, tags::pixel_data, "PixelData",
289 findings);
290 }
291
292 // Validate pixel data consistency
294 check_pixel_data_consistency(dataset, findings);
295 }
296}
297
299 const dicom_dataset& dataset,
300 std::vector<validation_finding>& findings) const {
301
302 // Type 1
303 if (options_.check_type1) {
304 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID",
305 findings);
307 "SOPInstanceUID", findings);
308 }
309
310 // Validate SOP Class UID is a CT storage class
311 if (dataset.contains(tags::sop_class_uid)) {
312 auto sop_class = dataset.get_string(tags::sop_class_uid);
313 if (!sop_classes::is_ct_storage_sop_class(sop_class)) {
314 findings.push_back(
316 "SOPClassUID is not a recognized CT Storage SOP Class: " +
317 sop_class,
318 "CT-ERR-001"});
319 }
320 }
321}
322
323// =============================================================================
324// Attribute Validation Helpers
325// =============================================================================
326
328 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
329 std::vector<validation_finding>& findings) const {
330
331 if (!dataset.contains(tag)) {
332 findings.push_back(
334 std::string("Type 1 attribute missing: ") + std::string(name) +
335 " (" + tag.to_string() + ")",
336 "CT-TYPE1-MISSING"});
337 } else {
338 // Type 1 must have a value (cannot be empty)
339 auto value = dataset.get_string(tag);
340 if (value.empty()) {
341 findings.push_back(
343 std::string("Type 1 attribute has empty value: ") +
344 std::string(name) + " (" + tag.to_string() + ")",
345 "CT-TYPE1-EMPTY"});
346 }
347 }
348}
349
351 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
352 std::vector<validation_finding>& findings) const {
353
354 // Type 2 must be present but can be empty
355 if (!dataset.contains(tag)) {
356 findings.push_back(
358 std::string("Type 2 attribute missing: ") + std::string(name) +
359 " (" + tag.to_string() + ")",
360 "CT-TYPE2-MISSING"});
361 }
362}
363
365 const dicom_dataset& dataset,
366 std::vector<validation_finding>& findings) const {
367
368 if (!dataset.contains(tags::modality)) {
369 return; // Already reported as Type 1 missing
370 }
371
372 auto modality = dataset.get_string(tags::modality);
373 if (modality != "CT") {
374 findings.push_back(
376 "Modality must be 'CT' for CT images, found: " + modality,
377 "CT-ERR-002"});
378 }
379}
380
382 const dicom_dataset& dataset,
383 std::vector<validation_finding>& findings) const {
384
385 // Check BitsStored <= BitsAllocated
386 auto bits_allocated =
387 dataset.get_numeric<uint16_t>(tags::bits_allocated);
388 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
389 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
390
391 if (bits_allocated && bits_stored) {
392 if (*bits_stored > *bits_allocated) {
393 findings.push_back(
395 "BitsStored (" + std::to_string(*bits_stored) +
396 ") exceeds BitsAllocated (" +
397 std::to_string(*bits_allocated) + ")",
398 "CT-ERR-003"});
399 }
400 }
401
402 // Check HighBit == BitsStored - 1
403 if (bits_stored && high_bit) {
404 if (*high_bit != *bits_stored - 1) {
405 findings.push_back(
407 "HighBit (" + std::to_string(*high_bit) +
408 ") should typically be BitsStored - 1 (" +
409 std::to_string(*bits_stored - 1) + ")",
410 "CT-WARN-003"});
411 }
412 }
413
414 // CT images must be grayscale (MONOCHROME1 or MONOCHROME2)
416 auto photometric =
418 if (!sop_classes::is_valid_ct_photometric(photometric)) {
419 findings.push_back(
422 "CT images must use MONOCHROME1 or MONOCHROME2, found: " +
423 photometric,
424 "CT-ERR-004"});
425 }
426 }
427
428 // CT images must have SamplesPerPixel = 1
429 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
430 if (samples && *samples != 1) {
431 findings.push_back(
433 "CT images require SamplesPerPixel = 1, found: " +
434 std::to_string(*samples),
435 "CT-ERR-005"});
436 }
437
438 // CT images typically use signed pixel representation (for HU values)
439 auto pixel_rep =
440 dataset.get_numeric<uint16_t>(tags::pixel_representation);
441 if (pixel_rep && *pixel_rep != 1) {
442 findings.push_back(
444 "CT images typically use signed pixel representation "
445 "(PixelRepresentation = 1) for Hounsfield Unit values",
446 "CT-INFO-003"});
447 }
448}
449
450// =============================================================================
451// Convenience Functions
452// =============================================================================
453
455 ct_iod_validator validator;
456 return validator.validate(dataset);
457}
458
459bool is_valid_ct_dataset(const dicom_dataset& dataset) {
460 ct_iod_validator validator;
461 return validator.quick_check(dataset);
462}
463
464} // 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_equipment_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const ct_validation_options & options() const noexcept
Get the validation options.
void validate_ct_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
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against CT IOD.
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
ct_iod_validator()=default
Construct validator with default options.
void check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const ct_validation_options &options)
Set validation options.
void validate_general_series_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
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 check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required CT attributes.
void validate_sop_common_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
CT 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.
bool is_ct_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a CT Storage SOP Class.
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 convolution_kernel
Convolution Kernel (0018,1210) - Reconstruction algorithm.
constexpr core::dicom_tag slice_thickness
Slice Thickness (0018,0050) - Nominal slice thickness in mm.
constexpr core::dicom_tag kvp
KVP (0018,0060) - Peak kilo voltage output of the X-Ray generator.
constexpr core::dicom_tag image_orientation_patient
Image Orientation (Patient) (0020,0037)
validation_result validate_ct_iod(const core::dicom_dataset &dataset)
Validate a CT dataset with default options.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool is_valid_ct_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid CT image.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool check_type1
Check Type 1 (required) attributes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_ct_params
Validate CT-specific acquisition parameters.
bool strict_mode
Strict mode - treat warnings as errors.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name