PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
wsi_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// wsi_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.32.8-1)
35 validate_patient_module(dataset, result.findings);
38 validate_sop_common_module(dataset, result.findings);
39 }
40
42 validate_wsi_image_module(dataset, result.findings);
44 }
45
48 }
49
52 }
53
54 // Specimen Module is User Optional — info-level checks
56 validate_specimen_module(dataset, result.findings);
57 }
58
59 // Determine overall validity
60 for (const auto& finding : result.findings) {
61 if (finding.severity == validation_severity::error) {
62 result.is_valid = false;
63 break;
64 }
66 finding.severity == validation_severity::warning) {
67 result.is_valid = false;
68 break;
69 }
70 }
71
72 return result;
73}
74
76 // General Study Module Type 1
77 if (!dataset.contains(tags::study_instance_uid)) return false;
78
79 // General Series Module Type 1
80 if (!dataset.contains(tags::modality)) return false;
81 if (!dataset.contains(tags::series_instance_uid)) return false;
82
83 // Check modality is SM
84 auto modality = dataset.get_string(tags::modality);
85 if (modality != "SM") return false;
86
87 // WSI Image Module Type 1
88 if (!dataset.contains(tags::image_type)) return false;
90 return false;
91 if (!dataset.contains(wsi_iod_tags::total_pixel_matrix_rows)) return false;
92
93 // Image Pixel Module Type 1
94 if (!dataset.contains(tags::samples_per_pixel)) return false;
95 if (!dataset.contains(tags::photometric_interpretation)) return false;
96 if (!dataset.contains(tags::rows)) return false;
97 if (!dataset.contains(tags::columns)) return false;
98 if (!dataset.contains(tags::bits_allocated)) return false;
99 if (!dataset.contains(tags::bits_stored)) return false;
100 if (!dataset.contains(tags::high_bit)) return false;
101 if (!dataset.contains(tags::pixel_representation)) return false;
102
103 // SOP Common Module Type 1
104 if (!dataset.contains(tags::sop_class_uid)) return false;
105 if (!dataset.contains(tags::sop_instance_uid)) return false;
106
107 return true;
108}
109
111 return options_;
112}
113
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 "SM"
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 // WSI Image Module (PS3.3 Section C.8.12.4)
189
190 // ImageType is Type 1
191 if (options_.check_type1) {
192 check_type1_attribute(dataset, tags::image_type, "ImageType",
193 findings);
194 }
195
196 // Total Pixel Matrix Columns/Rows are Type 1
198 "TotalPixelMatrixColumns", findings);
200 "TotalPixelMatrixRows", findings);
201
202 // Number of Frames is Type 1 for multi-frame WSI
203 if (options_.check_type1) {
205 "NumberOfFrames", findings);
206 }
207
208 // Informational checks for WSI-specific attributes
210 // Imaged Volume Width/Height — important for physical measurements
212 findings.push_back(
214 "ImagedVolumeWidth (0048,0001) not present - physical "
215 "dimensions unavailable",
216 "WSI-INFO-001"});
217 }
218
220 findings.push_back(
222 "ImagedVolumeHeight (0048,0002) not present - physical "
223 "dimensions unavailable",
224 "WSI-INFO-002"});
225 }
226
227 // Image Orientation (Slide) is Type 1C
230 findings.push_back(
233 "ImageOrientationSlide (0048,0102) missing - slide "
234 "orientation unknown",
235 "WSI-WARN-001"});
236 }
237 }
238}
239
241 const dicom_dataset& dataset,
242 std::vector<validation_finding>& findings) const {
243
244 // Optical Path Module — Optical Path Identifier is Type 1
245 if (options_.check_type1) {
247 "OpticalPathIdentifier", findings);
248 }
249
250 // Optical Path Description is Type 3 (optional), info if missing
253 findings.push_back(
256 "OpticalPathDescription (0048,0106) not present - optical "
257 "path details unavailable",
258 "WSI-INFO-003"});
259 }
260 }
261}
262
264 const dicom_dataset& dataset,
265 std::vector<validation_finding>& findings) const {
266
267 // Dimension Organization Type is Type 1
269 "DimensionOrganizationType", findings);
270
271 // Validate the Dimension Organization Type value
273 auto org_type =
275 if (org_type != "TILED_FULL" && org_type != "TILED_SPARSE") {
276 findings.push_back(
279 "DimensionOrganizationType must be 'TILED_FULL' or "
280 "'TILED_SPARSE' for WSI, found: " + org_type,
281 "WSI-ERR-001"});
282 }
283 }
284}
285
287 const dicom_dataset& dataset,
288 std::vector<validation_finding>& findings) const {
289
290 // Type 1 attributes
291 if (options_.check_type1) {
293 "SamplesPerPixel", findings);
295 "PhotometricInterpretation", findings);
296 check_type1_attribute(dataset, tags::rows, "Rows", findings);
297 check_type1_attribute(dataset, tags::columns, "Columns", findings);
298 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated",
299 findings);
300 check_type1_attribute(dataset, tags::bits_stored, "BitsStored",
301 findings);
302 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
304 "PixelRepresentation", findings);
305 }
306
307 // Validate pixel data consistency
309 check_pixel_data_consistency(dataset, findings);
310 }
311}
312
314 const dicom_dataset& dataset,
315 std::vector<validation_finding>& findings) const {
316
317 // Specimen Module is User Optional (U) for WSI
318 // Provide info-level findings when specimen data is absent
319
321 findings.push_back(
323 "SpecimenIdentifier (0040,0551) not present - specimen "
324 "tracking unavailable",
325 "WSI-INFO-004"});
326 }
327
329 findings.push_back(
331 "ContainerIdentifier (0040,0512) not present - container "
332 "tracking unavailable",
333 "WSI-INFO-005"});
334 }
335}
336
338 const dicom_dataset& dataset,
339 std::vector<validation_finding>& findings) const {
340
341 // Type 1
342 if (options_.check_type1) {
343 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID",
344 findings);
346 "SOPInstanceUID", findings);
347 }
348
349 // Validate SOP Class UID is a WSI storage class
350 if (dataset.contains(tags::sop_class_uid)) {
351 auto sop_class = dataset.get_string(tags::sop_class_uid);
353 findings.push_back(
355 "SOPClassUID is not a recognized WSI Storage SOP Class: " +
356 sop_class,
357 "WSI-ERR-002"});
358 }
359 }
360}
361
362// =============================================================================
363// Attribute Validation Helpers
364// =============================================================================
365
367 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
368 std::vector<validation_finding>& findings) const {
369
370 if (!dataset.contains(tag)) {
371 findings.push_back(
373 std::string("Type 1 attribute missing: ") + std::string(name) +
374 " (" + tag.to_string() + ")",
375 "WSI-TYPE1-MISSING"});
376 } else {
377 // Type 1 must have a value (cannot be empty)
378 auto value = dataset.get_string(tag);
379 if (value.empty()) {
380 findings.push_back(
382 std::string("Type 1 attribute has empty value: ") +
383 std::string(name) + " (" + tag.to_string() + ")",
384 "WSI-TYPE1-EMPTY"});
385 }
386 }
387}
388
390 const dicom_dataset& dataset, dicom_tag tag, std::string_view name,
391 std::vector<validation_finding>& findings) const {
392
393 // Type 2 must be present but can be empty
394 if (!dataset.contains(tag)) {
395 findings.push_back(
397 std::string("Type 2 attribute missing: ") + std::string(name) +
398 " (" + tag.to_string() + ")",
399 "WSI-TYPE2-MISSING"});
400 }
401}
402
404 const dicom_dataset& dataset,
405 std::vector<validation_finding>& findings) const {
406
407 if (!dataset.contains(tags::modality)) {
408 return; // Already reported as Type 1 missing
409 }
410
411 auto modality = dataset.get_string(tags::modality);
412 if (modality != "SM") {
413 findings.push_back(
415 "Modality must be 'SM' for WSI images, found: " + modality,
416 "WSI-ERR-003"});
417 }
418}
419
421 const dicom_dataset& dataset,
422 std::vector<validation_finding>& findings) const {
423
424 // Check BitsStored <= BitsAllocated
425 auto bits_allocated =
426 dataset.get_numeric<uint16_t>(tags::bits_allocated);
427 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
428 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
429
430 if (bits_allocated && bits_stored) {
431 if (*bits_stored > *bits_allocated) {
432 findings.push_back(
434 "BitsStored (" + std::to_string(*bits_stored) +
435 ") exceeds BitsAllocated (" +
436 std::to_string(*bits_allocated) + ")",
437 "WSI-ERR-004"});
438 }
439 }
440
441 // Check HighBit == BitsStored - 1
442 if (bits_stored && high_bit) {
443 if (*high_bit != *bits_stored - 1) {
444 findings.push_back(
446 "HighBit (" + std::to_string(*high_bit) +
447 ") should typically be BitsStored - 1 (" +
448 std::to_string(*bits_stored - 1) + ")",
449 "WSI-WARN-002"});
450 }
451 }
452
453 // Validate PhotometricInterpretation for WSI
455 auto photometric =
457 if (!sop_classes::is_valid_wsi_photometric(photometric)) {
458 findings.push_back(
461 "WSI images must use RGB, YBR_FULL_422, YBR_ICT, YBR_RCT, "
462 "or MONOCHROME2, found: " + photometric,
463 "WSI-ERR-005"});
464 }
465 }
466
467 // WSI SamplesPerPixel must be 1 (grayscale/fluorescence) or 3 (color)
468 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
469 if (samples && *samples != 1 && *samples != 3) {
470 findings.push_back(
472 "WSI images require SamplesPerPixel of 1 or 3, found: " +
473 std::to_string(*samples),
474 "WSI-ERR-006"});
475 }
476
477 // WSI images must use unsigned pixel representation
478 auto pixel_rep =
479 dataset.get_numeric<uint16_t>(tags::pixel_representation);
480 if (pixel_rep && *pixel_rep != 0) {
481 findings.push_back(
483 "WSI images typically use unsigned pixel representation "
484 "(PixelRepresentation = 0)",
485 "WSI-WARN-003"});
486 }
487}
488
489// =============================================================================
490// Convenience Functions
491// =============================================================================
492
494 wsi_iod_validator validator;
495 return validator.validate(dataset);
496}
497
499 wsi_iod_validator validator;
500 return validator.quick_check(dataset);
501}
502
503} // 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_type2_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_optical_path_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_specimen_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_multiframe_dimension_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 WSI attributes.
wsi_iod_validator()=default
Construct validator with default 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 check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const wsi_validation_options &options)
Set validation options.
void validate_wsi_image_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
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against WSI IOD.
void validate_general_study_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
const wsi_validation_options & options() const noexcept
Get the validation 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.
bool is_wsi_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a WSI Storage SOP Class.
bool is_valid_wsi_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for WSI.
constexpr core::dicom_tag imaged_volume_width
Imaged Volume Width (0048,0001) — physical width in mm.
constexpr core::dicom_tag total_pixel_matrix_columns
Total Pixel Matrix Columns (0048,0006)
constexpr core::dicom_tag image_orientation_slide
Image Orientation (Slide) (0048,0102)
constexpr core::dicom_tag optical_path_description
Optical Path Description (0048,0106)
constexpr core::dicom_tag number_of_frames
Number of Frames (0028,0008)
constexpr core::dicom_tag dimension_organization_type
Dimension Organization Type (0020,9311)
constexpr core::dicom_tag total_pixel_matrix_rows
Total Pixel Matrix Rows (0048,0007)
constexpr core::dicom_tag container_identifier
Container Identifier (0040,0512)
constexpr core::dicom_tag optical_path_identifier
Optical Path Identifier (0048,0105)
constexpr core::dicom_tag specimen_identifier
Specimen Identifier (0040,0551)
constexpr core::dicom_tag imaged_volume_height
Imaged Volume Height (0048,0002) — physical height in mm.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
bool is_valid_wsi_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid WSI image.
validation_result validate_wsi_iod(const core::dicom_dataset &dataset)
Validate a WSI dataset with default options.
std::vector< validation_finding > findings
All findings during validation.
bool strict_mode
Strict mode - treat warnings as errors.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool check_type1
Check Type 1 (required) attributes.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool validate_optical_path
Validate optical path information.
bool validate_wsi_params
Validate WSI-specific image parameters.
std::string_view name
VL Whole Slide Microscopy Image IOD Validator.
VL Whole Slide Microscopy Image Storage SOP Class.