PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
ophthalmic_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
17
19
20using namespace kcenon::pacs::core;
21
22// =============================================================================
23// ophthalmic_iod_validator Implementation
24// =============================================================================
25
29
31 const dicom_dataset& dataset) const {
32 validation_result result;
33 result.is_valid = true;
34
35 // Validate mandatory modules (PS3.3 Tables A.39-1 / A.40-1)
37 validate_patient_module(dataset, result.findings);
40 validate_sop_common_module(dataset, result.findings);
41 }
42
45 }
46
49 }
50
51 // Multi-frame Module is conditional — applies to OCT images
53 validate_multiframe_module(dataset, result.findings);
55 }
56
57 // Determine overall validity
58 for (const auto& finding : result.findings) {
59 if (finding.severity == validation_severity::error) {
60 result.is_valid = false;
61 break;
62 }
64 finding.severity == validation_severity::warning) {
65 result.is_valid = false;
66 break;
67 }
68 }
69
70 return result;
71}
72
74 // General Study Module Type 1
75 if (!dataset.contains(tags::study_instance_uid)) return false;
76
77 // General Series Module Type 1
78 if (!dataset.contains(tags::modality)) return false;
79 if (!dataset.contains(tags::series_instance_uid)) return false;
80
81 // Check modality is OP or OPT
82 auto modality = dataset.get_string(tags::modality);
83 if (modality != "OP" && modality != "OPT") return false;
84
85 // Ophthalmic Image Module Type 1
86 if (!dataset.contains(tags::image_type)) return false;
87 if (!dataset.contains(ophthalmic_iod_tags::image_laterality)) return false;
88
89 // Image Pixel Module Type 1
90 if (!dataset.contains(tags::samples_per_pixel)) return false;
91 if (!dataset.contains(tags::photometric_interpretation)) return false;
92 if (!dataset.contains(tags::rows)) return false;
93 if (!dataset.contains(tags::columns)) return false;
94 if (!dataset.contains(tags::bits_allocated)) return false;
95 if (!dataset.contains(tags::bits_stored)) return false;
96 if (!dataset.contains(tags::high_bit)) return false;
97 if (!dataset.contains(tags::pixel_representation)) return false;
98
99 // SOP Common Module Type 1
100 if (!dataset.contains(tags::sop_class_uid)) return false;
101 if (!dataset.contains(tags::sop_instance_uid)) return false;
102
103 return true;
104}
105
108 return options_;
109}
110
115
116// =============================================================================
117// Module Validation Methods
118// =============================================================================
119
121 const dicom_dataset& dataset,
122 std::vector<validation_finding>& findings) const {
123
124 // Patient Module - All attributes are Type 2
125 if (options_.check_type2) {
126 check_type2_attribute(dataset, tags::patient_name, "PatientName",
127 findings);
128 check_type2_attribute(dataset, tags::patient_id, "PatientID",
129 findings);
131 "PatientBirthDate", findings);
132 check_type2_attribute(dataset, tags::patient_sex, "PatientSex",
133 findings);
134 }
135}
136
138 const dicom_dataset& dataset,
139 std::vector<validation_finding>& findings) const {
140
141 // Type 1
142 if (options_.check_type1) {
144 "StudyInstanceUID", findings);
145 }
146
147 // Type 2
148 if (options_.check_type2) {
149 check_type2_attribute(dataset, tags::study_date, "StudyDate",
150 findings);
151 check_type2_attribute(dataset, tags::study_time, "StudyTime",
152 findings);
154 "ReferringPhysicianName", findings);
155 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
157 "AccessionNumber", findings);
158 }
159}
160
162 const dicom_dataset& dataset,
163 std::vector<validation_finding>& findings) const {
164
165 // Type 1
166 if (options_.check_type1) {
167 check_type1_attribute(dataset, tags::modality, "Modality", findings);
169 "SeriesInstanceUID", findings);
170
171 // Modality must be "OP" or "OPT"
172 check_modality(dataset, findings);
173 }
174
175 // Type 2
176 if (options_.check_type2) {
177 check_type2_attribute(dataset, tags::series_number, "SeriesNumber",
178 findings);
179 }
180}
181
183 const dicom_dataset& dataset,
184 std::vector<validation_finding>& findings) const {
185
186 // ImageType is Type 1
187 if (options_.check_type1) {
188 check_type1_attribute(dataset, tags::image_type, "ImageType",
189 findings);
190 }
191
192 // Image Laterality is Type 1 — must be R, L, or B
195 "ImageLaterality", findings);
196 check_laterality(dataset, findings);
197 }
198
199 // Informational checks for ophthalmic-specific attributes
201 // Anatomic Region Sequence — Type 1 for ophthalmic
202 if (!dataset.contains(
204 findings.push_back(
207 "AnatomicRegionSequence (0008,2218) missing - anatomic "
208 "region unspecified",
209 "OPHTH-WARN-001"});
210 }
211
212 // Acquisition Device Type Code Sequence — Type 2
213 if (!dataset.contains(
215 findings.push_back(
218 "AcquisitionDeviceTypeCodeSequence (0022,0015) not present "
219 "- device type unavailable",
220 "OPHTH-INFO-001"});
221 }
222
223 // Horizontal Field of View — Type 2C
224 if (!dataset.contains(
226 findings.push_back(
229 "HorizontalFieldOfView (0022,000C) not present - field of "
230 "view unavailable",
231 "OPHTH-INFO-002"});
232 }
233
234 // Pupil Dilated — Type 2C
236 findings.push_back(
239 "PupilDilated (0022,000D) not present - pupil dilation "
240 "status unknown",
241 "OPHTH-INFO-003"});
242 }
243 }
244}
245
247 const dicom_dataset& dataset,
248 std::vector<validation_finding>& findings) const {
249
250 // Type 1 attributes
251 if (options_.check_type1) {
253 "SamplesPerPixel", findings);
255 "PhotometricInterpretation", findings);
256 check_type1_attribute(dataset, tags::rows, "Rows", findings);
257 check_type1_attribute(dataset, tags::columns, "Columns", findings);
258 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated",
259 findings);
260 check_type1_attribute(dataset, tags::bits_stored, "BitsStored",
261 findings);
262 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
264 "PixelRepresentation", 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 // Multi-frame Module is conditional — required for OCT images
278 if (!dataset.contains(tags::sop_class_uid)) return;
279
280 auto sop_class = dataset.get_string(tags::sop_class_uid);
281 bool is_oct =
284
285 if (is_oct) {
286 // NumberOfFrames is Type 1 for OCT multi-frame
288 findings.push_back(
291 "NumberOfFrames (0028,0008) required for OCT multi-frame "
292 "images",
293 "OPHTH-ERR-001"});
294 }
295 }
296}
297
299 const dicom_dataset& dataset,
300 std::vector<validation_finding>& findings) const {
301
302 // Acquisition Context Module is mandatory
304 findings.push_back(
307 "AcquisitionContextSequence (0040,0555) not present - "
308 "acquisition context unavailable",
309 "OPHTH-INFO-004"});
310 }
311}
312
314 const dicom_dataset& dataset,
315 std::vector<validation_finding>& findings) const {
316
317 // Type 1
318 if (options_.check_type1) {
319 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID",
320 findings);
322 "SOPInstanceUID", findings);
323 }
324
325 // Validate SOP Class UID is an ophthalmic storage class
326 if (dataset.contains(tags::sop_class_uid)) {
327 auto sop_class = dataset.get_string(tags::sop_class_uid);
329 findings.push_back(
331 "SOPClassUID is not a recognized Ophthalmic Storage SOP "
332 "Class: " + sop_class,
333 "OPHTH-ERR-002"});
334 }
335 }
336}
337
338// =============================================================================
339// Attribute Validation Helpers
340// =============================================================================
341
343 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
344 std::vector<validation_finding>& findings) const {
345
346 if (!dataset.contains(tag)) {
347 findings.push_back(
349 std::string("Type 1 attribute missing: ") + std::string(name) +
350 " (" + tag.to_string() + ")",
351 "OPHTH-TYPE1-MISSING"});
352 } else {
353 // Type 1 must have a value (cannot be empty)
354 auto value = dataset.get_string(tag);
355 if (value.empty()) {
356 findings.push_back(
358 std::string("Type 1 attribute has empty value: ") +
359 std::string(name) + " (" + tag.to_string() + ")",
360 "OPHTH-TYPE1-EMPTY"});
361 }
362 }
363}
364
366 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
367 std::vector<validation_finding>& findings) const {
368
369 // Type 2 must be present but can be empty
370 if (!dataset.contains(tag)) {
371 findings.push_back(
373 std::string("Type 2 attribute missing: ") + std::string(name) +
374 " (" + tag.to_string() + ")",
375 "OPHTH-TYPE2-MISSING"});
376 }
377}
378
380 const dicom_dataset& dataset,
381 std::vector<validation_finding>& findings) const {
382
383 if (!dataset.contains(tags::modality)) {
384 return; // Already reported as Type 1 missing
385 }
386
387 auto modality = dataset.get_string(tags::modality);
388 if (modality != "OP" && modality != "OPT") {
389 findings.push_back(
391 "Modality must be 'OP' or 'OPT' for ophthalmic images, "
392 "found: " + modality,
393 "OPHTH-ERR-003"});
394 }
395}
396
398 const dicom_dataset& dataset,
399 std::vector<validation_finding>& findings) const {
400
402 return; // Already reported as Type 1 missing
403 }
404
405 auto laterality =
407 if (laterality != "R" && laterality != "L" && laterality != "B") {
408 findings.push_back(
411 "ImageLaterality must be 'R' (Right), 'L' (Left), or "
412 "'B' (Both), found: " + laterality,
413 "OPHTH-ERR-004"});
414 }
415}
416
418 const dicom_dataset& dataset,
419 std::vector<validation_finding>& findings) const {
420
421 // Check BitsStored <= BitsAllocated
422 auto bits_allocated =
423 dataset.get_numeric<uint16_t>(tags::bits_allocated);
424 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
425 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
426
427 if (bits_allocated && bits_stored) {
428 if (*bits_stored > *bits_allocated) {
429 findings.push_back(
431 "BitsStored (" + std::to_string(*bits_stored) +
432 ") exceeds BitsAllocated (" +
433 std::to_string(*bits_allocated) + ")",
434 "OPHTH-ERR-005"});
435 }
436 }
437
438 // Check HighBit == BitsStored - 1
439 if (bits_stored && high_bit) {
440 if (*high_bit != *bits_stored - 1) {
441 findings.push_back(
443 "HighBit (" + std::to_string(*high_bit) +
444 ") should typically be BitsStored - 1 (" +
445 std::to_string(*bits_stored - 1) + ")",
446 "OPHTH-WARN-002"});
447 }
448 }
449
450 // Validate PhotometricInterpretation for ophthalmic
452 auto photometric =
455 findings.push_back(
458 "Ophthalmic images must use MONOCHROME1, MONOCHROME2, "
459 "RGB, YBR_FULL_422, or PALETTE COLOR, found: " + photometric,
460 "OPHTH-ERR-006"});
461 }
462 }
463
464 // Ophthalmic SamplesPerPixel must be 1 (grayscale) or 3 (color)
465 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
466 if (samples && *samples != 1 && *samples != 3) {
467 findings.push_back(
469 "Ophthalmic images require SamplesPerPixel of 1 or 3, "
470 "found: " + std::to_string(*samples),
471 "OPHTH-ERR-007"});
472 }
473
474 // Ophthalmic images use unsigned pixel representation
475 auto pixel_rep =
476 dataset.get_numeric<uint16_t>(tags::pixel_representation);
477 if (pixel_rep && *pixel_rep != 0) {
478 findings.push_back(
480 "Ophthalmic images typically use unsigned pixel representation "
481 "(PixelRepresentation = 0)",
482 "OPHTH-WARN-003"});
483 }
484}
485
486// =============================================================================
487// Convenience Functions
488// =============================================================================
489
491 ophthalmic_iod_validator validator;
492 return validator.validate(dataset);
493}
494
496 ophthalmic_iod_validator validator;
497 return validator.quick_check(dataset);
498}
499
500} // 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_acquisition_context_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_ophthalmic_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
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
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required ophthalmic attributes.
void set_options(const ophthalmic_validation_options &options)
Set validation options.
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
const ophthalmic_validation_options & options() const noexcept
Get the validation options.
void validate_multiframe_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_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void check_laterality(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 Ophthalmic IOD.
void validate_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
ophthalmic_iod_validator()=default
Construct validator with default options.
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 image_type
Image Type.
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.
constexpr std::string_view ophthalmic_oct_bscan_analysis_storage_uid
Ophthalmic Optical Coherence Tomography B-scan Volume Analysis Storage.
bool is_ophthalmic_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is an Ophthalmic Storage SOP Class.
constexpr std::string_view ophthalmic_tomography_storage_uid
Ophthalmic Tomography Image Storage (OCT)
bool is_valid_ophthalmic_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for ophthalmic imaging.
constexpr core::dicom_tag anatomic_region_sequence
Anatomic Region Sequence (0008,2218)
constexpr core::dicom_tag image_laterality
Image Laterality (0020,0062) — R, L, or B.
constexpr core::dicom_tag number_of_frames
Number of Frames (0028,0008)
constexpr core::dicom_tag pupil_dilated
Pupil Dilated (0022,000D)
constexpr core::dicom_tag acquisition_device_type_code_sequence
Acquisition Device Type Code Sequence (0022,0015)
constexpr core::dicom_tag acquisition_context_sequence
Acquisition Context Sequence (0040,0555)
constexpr core::dicom_tag horizontal_field_of_view
Horizontal Field of View (0022,000C)
validation_result validate_ophthalmic_iod(const core::dicom_dataset &dataset)
Validate an ophthalmic dataset with default options.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool is_valid_ophthalmic_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid ophthalmic image.
Ophthalmic Photography and Tomography Image IOD Validator.
Ophthalmic Photography and Tomography Storage SOP Classes.
bool validate_ophthalmic_params
Validate ophthalmic-specific image parameters.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool check_type2
Check Type 2 (required, can be empty) attributes.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name