PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
us_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// =============================================================================
21// validation_result Implementation
22// =============================================================================
23
24bool validation_result::has_errors() const noexcept {
25 for (const auto& f : findings) {
26 if (f.severity == validation_severity::error) {
27 return true;
28 }
29 }
30 return false;
31}
32
33bool validation_result::has_warnings() const noexcept {
34 for (const auto& f : findings) {
35 if (f.severity == validation_severity::warning) {
36 return true;
37 }
38 }
39 return false;
40}
41
42size_t validation_result::error_count() const noexcept {
43 size_t count = 0;
44 for (const auto& f : findings) {
45 if (f.severity == validation_severity::error) {
46 ++count;
47 }
48 }
49 return count;
50}
51
52size_t validation_result::warning_count() const noexcept {
53 size_t count = 0;
54 for (const auto& f : findings) {
55 if (f.severity == validation_severity::warning) {
56 ++count;
57 }
58 }
59 return count;
60}
61
62std::string validation_result::summary() const {
63 std::ostringstream oss;
64 oss << "Validation " << (is_valid ? "PASSED" : "FAILED");
65 oss << " - " << error_count() << " error(s), "
66 << warning_count() << " warning(s)";
67 return oss.str();
68}
69
70// =============================================================================
71// us_iod_validator Implementation
72// =============================================================================
73
75 : options_(options) {}
76
78 validation_result result;
79 result.is_valid = true;
80
81 // Validate mandatory modules
83 validate_patient_module(dataset, result.findings);
86 validate_us_image_module(dataset, result.findings);
87 validate_sop_common_module(dataset, result.findings);
88 }
89
92 }
93
94 // Check for errors
95 for (const auto& finding : result.findings) {
96 if (finding.severity == validation_severity::error) {
97 result.is_valid = false;
98 break;
99 }
100 if (options_.strict_mode && finding.severity == validation_severity::warning) {
101 result.is_valid = false;
102 break;
103 }
104 }
105
106 return result;
107}
108
111 // First do standard validation
112 auto result = validate(dataset);
113
114 // Then add multi-frame specific validation
115 validate_multiframe_module(dataset, result.findings);
116
117 // Re-check validity after multi-frame validation
118 for (const auto& finding : result.findings) {
119 if (finding.severity == validation_severity::error) {
120 result.is_valid = false;
121 break;
122 }
123 }
124
125 return result;
126}
127
129 // Check only Type 1 required attributes for quick validation
130
131 // Patient Module Type 1
132 // (Patient module has Type 2 attributes, no Type 1)
133
134 // General Study Module Type 1
135 if (!dataset.contains(tags::study_instance_uid)) return false;
136
137 // General Series Module Type 1
138 if (!dataset.contains(tags::modality)) return false;
139 if (!dataset.contains(tags::series_instance_uid)) return false;
140
141 // Check modality is US
142 auto modality = dataset.get_string(tags::modality);
143 if (modality != "US") return false;
144
145 // Image Pixel Module Type 1
146 if (!dataset.contains(tags::samples_per_pixel)) return false;
147 if (!dataset.contains(tags::photometric_interpretation)) return false;
148 if (!dataset.contains(tags::rows)) return false;
149 if (!dataset.contains(tags::columns)) return false;
150 if (!dataset.contains(tags::bits_allocated)) return false;
151 if (!dataset.contains(tags::bits_stored)) return false;
152 if (!dataset.contains(tags::high_bit)) return false;
153 if (!dataset.contains(tags::pixel_representation)) return false;
154 if (!dataset.contains(tags::pixel_data)) return false;
155
156 // SOP Common Module Type 1
157 if (!dataset.contains(tags::sop_class_uid)) return false;
158 if (!dataset.contains(tags::sop_instance_uid)) return false;
159
160 return true;
161}
162
164 return options_;
165}
166
170
171// =============================================================================
172// Module Validation Methods
173// =============================================================================
174
176 const dicom_dataset& dataset,
177 std::vector<validation_finding>& findings) const {
178
179 // Patient Module - All attributes are Type 2
180 if (options_.check_type2) {
181 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
182 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
183 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
184 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
185 }
186}
187
189 const dicom_dataset& dataset,
190 std::vector<validation_finding>& findings) const {
191
192 // Type 1
193 if (options_.check_type1) {
194 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
195 }
196
197 // Type 2
198 if (options_.check_type2) {
199 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
200 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
201 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
202 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
203 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
204 }
205}
206
208 const dicom_dataset& dataset,
209 std::vector<validation_finding>& findings) const {
210
211 // Type 1
212 if (options_.check_type1) {
213 check_type1_attribute(dataset, tags::modality, "Modality", findings);
214 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
215
216 // Special check: Modality must be "US"
217 check_modality(dataset, findings);
218 }
219
220 // Type 2
221 if (options_.check_type2) {
222 check_type2_attribute(dataset, tags::series_number, "SeriesNumber", findings);
223 }
224}
225
227 const dicom_dataset& dataset,
228 std::vector<validation_finding>& findings) const {
229
230 // US Image Module validation
231 // Sequence of Ultrasound Regions (0018,6011) is Type 1C - required if
232 // the Ultrasound image contains calibrated regions
233
234 // Note: The exact conditional logic depends on the presence of
235 // calibration data. For now, we add an info finding if it's missing.
237 constexpr dicom_tag sequence_of_ultrasound_regions{0x0018, 0x6011};
238 if (!dataset.contains(sequence_of_ultrasound_regions)) {
239 findings.push_back({
241 sequence_of_ultrasound_regions,
242 "SequenceOfUltrasoundRegions (0018,6011) not present - "
243 "no calibration information available",
244 "US-INFO-001"
245 });
246 }
247 }
248}
249
251 const dicom_dataset& dataset,
252 std::vector<validation_finding>& findings) const {
253
254 // Type 1 attributes
255 if (options_.check_type1) {
256 check_type1_attribute(dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
257 check_type1_attribute(dataset, tags::photometric_interpretation, "PhotometricInterpretation", findings);
258 check_type1_attribute(dataset, tags::rows, "Rows", findings);
259 check_type1_attribute(dataset, tags::columns, "Columns", findings);
260 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated", findings);
261 check_type1_attribute(dataset, tags::bits_stored, "BitsStored", findings);
262 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
263 check_type1_attribute(dataset, tags::pixel_representation, "PixelRepresentation", findings);
264 check_type1_attribute(dataset, tags::pixel_data, "PixelData", findings);
265 }
266
267 // Validate pixel data consistency
269 check_pixel_data_consistency(dataset, findings);
270 }
271}
272
274 const dicom_dataset& dataset,
275 std::vector<validation_finding>& findings) const {
276
277 constexpr dicom_tag number_of_frames{0x0028, 0x0008};
278 constexpr dicom_tag frame_time{0x0018, 0x1063};
279 constexpr dicom_tag frame_time_vector{0x0018, 0x1065};
280
281 // NumberOfFrames is Type 1 for multi-frame
282 if (options_.check_type1) {
283 check_type1_attribute(dataset, number_of_frames, "NumberOfFrames", findings);
284 }
285
286 // FrameTime or FrameTimeVector should be present (Type 1C)
288 if (!dataset.contains(frame_time) && !dataset.contains(frame_time_vector)) {
289 findings.push_back({
291 frame_time,
292 "Neither FrameTime nor FrameTimeVector present - "
293 "frame timing information missing",
294 "US-WARN-001"
295 });
296 }
297 }
298}
299
301 const dicom_dataset& dataset,
302 std::vector<validation_finding>& findings) const {
303
304 // Type 1
305 if (options_.check_type1) {
306 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
307 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
308 }
309
310 // Validate SOP Class UID is a US 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_us_storage_sop_class(sop_class)) {
314 findings.push_back({
317 "SOPClassUID is not a recognized US Storage SOP Class: " + sop_class,
318 "US-ERR-001"
319 });
320 }
321 }
322}
323
324// =============================================================================
325// Attribute Validation Helpers
326// =============================================================================
327
329 const dicom_dataset& dataset,
330 dicom_tag tag,
331 std::string_view name,
332 std::vector<validation_finding>& findings) const {
333
334 if (!dataset.contains(tag)) {
335 findings.push_back({
337 tag,
338 std::string("Type 1 attribute missing: ") + std::string(name) +
339 " (" + tag.to_string() + ")",
340 "US-TYPE1-MISSING"
341 });
342 } else {
343 // Type 1 must have a value (cannot be empty)
344 auto value = dataset.get_string(tag);
345 if (value.empty()) {
346 findings.push_back({
348 tag,
349 std::string("Type 1 attribute has empty value: ") +
350 std::string(name) + " (" + tag.to_string() + ")",
351 "US-TYPE1-EMPTY"
352 });
353 }
354 }
355}
356
358 const dicom_dataset& dataset,
359 dicom_tag tag,
360 std::string_view name,
361 std::vector<validation_finding>& findings) const {
362
363 // Type 2 must be present but can be empty
364 if (!dataset.contains(tag)) {
365 findings.push_back({
367 tag,
368 std::string("Type 2 attribute missing: ") + std::string(name) +
369 " (" + tag.to_string() + ")",
370 "US-TYPE2-MISSING"
371 });
372 }
373}
374
376 const dicom_dataset& dataset,
377 std::vector<validation_finding>& findings) const {
378
379 if (!dataset.contains(tags::modality)) {
380 return; // Already reported as Type 1 missing
381 }
382
383 auto modality = dataset.get_string(tags::modality);
384 if (modality != "US") {
385 findings.push_back({
388 "Modality must be 'US' for ultrasound images, found: " + modality,
389 "US-ERR-002"
390 });
391 }
392}
393
395 const dicom_dataset& dataset,
396 std::vector<validation_finding>& findings) const {
397
398 // Check BitsStored <= BitsAllocated
399 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
400 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
401 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
402
403 if (bits_allocated && bits_stored) {
404 if (*bits_stored > *bits_allocated) {
405 findings.push_back({
408 "BitsStored (" + std::to_string(*bits_stored) +
409 ") exceeds BitsAllocated (" + std::to_string(*bits_allocated) + ")",
410 "US-ERR-003"
411 });
412 }
413 }
414
415 // Check HighBit == BitsStored - 1
416 if (bits_stored && high_bit) {
417 if (*high_bit != *bits_stored - 1) {
418 findings.push_back({
421 "HighBit (" + std::to_string(*high_bit) +
422 ") should typically be BitsStored - 1 (" +
423 std::to_string(*bits_stored - 1) + ")",
424 "US-WARN-002"
425 });
426 }
427 }
428
429 // Check photometric interpretation is valid for US
431 auto photometric = dataset.get_string(tags::photometric_interpretation);
432 if (!sop_classes::is_valid_us_photometric(photometric)) {
433 findings.push_back({
436 "Unusual photometric interpretation for US: " + photometric,
437 "US-WARN-003"
438 });
439 }
440 }
441
442 // Check SamplesPerPixel consistency with photometric
443 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
444 if (samples && dataset.contains(tags::photometric_interpretation)) {
445 auto photometric = dataset.get_string(tags::photometric_interpretation);
446 bool is_color = (photometric == "RGB" || photometric == "YBR_FULL" ||
447 photometric == "YBR_FULL_422" || photometric == "PALETTE COLOR");
448
449 if (is_color && *samples != 3) {
450 findings.push_back({
453 "Color photometric interpretation requires SamplesPerPixel = 3",
454 "US-ERR-004"
455 });
456 }
457
458 if (!is_color && *samples != 1) {
459 findings.push_back({
462 "Grayscale photometric interpretation requires SamplesPerPixel = 1",
463 "US-ERR-005"
464 });
465 }
466 }
467}
468
469// =============================================================================
470// Convenience Functions
471// =============================================================================
472
474 us_iod_validator validator;
475 return validator.validate(dataset);
476}
477
478bool is_valid_us_dataset(const dicom_dataset& dataset) {
479 us_iod_validator validator;
480 return validator.quick_check(dataset);
481}
482
483} // 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_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
us_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_modality(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_us_image_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
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required attributes.
validation_result validate_multiframe(const core::dicom_dataset &dataset) const
Validate a multi-frame US dataset.
void validate_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const us_validation_options & options() const noexcept
Get the validation options.
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against US IOD.
void check_pixel_data_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const us_validation_options &options)
Set validation options.
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_multiframe_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_us_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a US Storage SOP Class.
bool is_valid_us_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for US.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool is_valid_us_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid US image.
validation_result validate_us_iod(const core::dicom_dataset &dataset)
Validate a US dataset with default options.
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool check_type1
Check Type 1 (required) attributes.
bool strict_mode
Strict mode - treat warnings as errors.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string summary() const
Get a formatted summary string.
size_t error_count() const noexcept
Get count of errors.
bool has_errors() const noexcept
Check if there are any errors.
size_t warning_count() const noexcept
Get count of warnings.
bool has_warnings() const noexcept
Check if there are any warnings.
constexpr dicom_tag number_of_frames
std::string_view name
Ultrasound Image IOD Validator.
Ultrasound Image Storage SOP Classes.