PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
aira_assessment.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 <chrono>
15#include <random>
16
17namespace kcenon::pacs::ai {
18
19using namespace kcenon::pacs::core;
20using namespace kcenon::pacs::encoding;
21
22// =============================================================================
23// DICOM Tags for SR Assessment
24// =============================================================================
25
26namespace aira_tags {
27
28constexpr dicom_tag value_type{0x0040, 0xA040};
29constexpr dicom_tag concept_name_code_sequence{0x0040, 0xA043};
30constexpr dicom_tag text_value{0x0040, 0xA160};
31constexpr dicom_tag code_value{0x0008, 0x0100};
32constexpr dicom_tag coding_scheme_designator{0x0008, 0x0102};
33constexpr dicom_tag code_meaning{0x0008, 0x0104};
34constexpr dicom_tag completion_flag{0x0040, 0xA491};
35constexpr dicom_tag verification_flag{0x0040, 0xA493};
36constexpr dicom_tag content_sequence{0x0040, 0xA730};
37constexpr dicom_tag content_template_sequence{0x0040, 0xA504};
38constexpr dicom_tag template_identifier{0x0040, 0xDB00};
39constexpr dicom_tag mapping_resource{0x0008, 0x0105};
40constexpr dicom_tag referenced_sop_sequence{0x0008, 0x1199};
41constexpr dicom_tag referenced_sop_class_uid{0x0008, 0x1150};
42constexpr dicom_tag referenced_sop_instance_uid{0x0008, 0x1155};
43constexpr dicom_tag relationship_type{0x0040, 0xA010};
44
45} // namespace aira_tags
46
47// =============================================================================
48// Helper Functions
49// =============================================================================
50
51namespace {
52
53void insert_sequence(dicom_dataset& ds, dicom_tag tag,
54 std::vector<dicom_dataset> items) {
55 dicom_element seq_elem(tag, vr_type::SQ);
56 seq_elem.sequence_items() = std::move(items);
57 ds.insert(std::move(seq_elem));
58}
59
60std::string current_datetime() {
61 auto now = std::chrono::system_clock::now();
62 auto time_t_now = std::chrono::system_clock::to_time_t(now);
63 std::tm tm_now{};
64#if defined(_WIN32)
65 gmtime_s(&tm_now, &time_t_now);
66#else
67 gmtime_r(&time_t_now, &tm_now);
68#endif
69 char buf[16];
70 std::strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", &tm_now);
71 return buf;
72}
73
74std::string current_date() {
75 return current_datetime().substr(0, 8);
76}
77
78std::string current_time() {
79 return current_datetime().substr(8, 6);
80}
81
82dicom_dataset make_code_item(const std::string& code_value,
83 const std::string& scheme,
84 const std::string& meaning) {
85 dicom_dataset item;
86 item.set_string(aira_tags::code_value, vr_type::SH, code_value);
87 item.set_string(aira_tags::coding_scheme_designator, vr_type::SH, scheme);
88 item.set_string(aira_tags::code_meaning, vr_type::LO, meaning);
89 return item;
90}
91
92} // namespace
93
94// =============================================================================
95// assessment_creator Implementation
96// =============================================================================
97
99 const ai_assessment& assessment) const {
100
102
103 // Validate required fields
104 if (assessment.ai_result.sop_instance_uid.empty()) {
105 result.error_message = "AI result SOP Instance UID is required";
106 return result;
107 }
108
109 if (assessment.assessor_name.empty()) {
110 result.error_message = "Assessor name is required";
111 return result;
112 }
113
115
116 // --- SOP Common Module ---
117 sr.set_string(tags::sop_class_uid, vr_type::UI,
118 "1.2.840.10008.5.1.4.1.1.88.22"); // Enhanced SR
119 result.assessment_uid = generate_uid();
120 sr.set_string(tags::sop_instance_uid, vr_type::UI, result.assessment_uid);
121
122 // --- Patient Module (Type 2 - empty if not provided) ---
123 build_patient_module(sr, assessment);
124
125 // --- General Study Module ---
126 sr.set_string(tags::study_instance_uid, vr_type::UI,
127 assessment.ai_result.study_instance_uid);
128 sr.set_string(tags::study_date, vr_type::DA, current_date());
129 sr.set_string(tags::study_time, vr_type::TM, current_time());
130 sr.set_string(tags::referring_physician_name, vr_type::PN, "");
131 sr.set_string(tags::study_id, vr_type::SH, "");
132 sr.set_string(tags::accession_number, vr_type::SH, "");
133
134 // --- General Series Module ---
135 sr.set_string(tags::modality, vr_type::CS, "SR");
136 sr.set_string(tags::series_instance_uid, vr_type::UI, generate_uid());
137 sr.set_string(tags::series_number, vr_type::IS, "1");
138
139 // --- General Equipment Module ---
140 sr.set_string(dicom_tag{0x0008, 0x0070}, vr_type::LO, ""); // Manufacturer
141
142 // --- SR Document General Module ---
143 sr.set_string(tags::instance_number, vr_type::IS, "1");
144 sr.set_string(tags::content_date, vr_type::DA, current_date());
145 sr.set_string(tags::content_time, vr_type::TM, current_time());
146 sr.set_string(aira_tags::completion_flag, vr_type::CS,
148 sr.set_string(aira_tags::verification_flag, vr_type::CS, "UNVERIFIED");
149
150 // --- SR Document Content Module ---
151 build_sr_content(sr, assessment);
152
153 // --- Referenced SOP Sequence ---
154 build_referenced_sop_sequence(sr, assessment);
155
156 result.success = true;
157 result.sr_dataset = std::move(sr);
158
159 return result;
160}
161
164 [[maybe_unused]] const ai_assessment& assessment) const {
165
166 sr.set_string(tags::patient_name, vr_type::PN, "");
167 sr.set_string(tags::patient_id, vr_type::LO, "");
168 sr.set_string(tags::patient_birth_date, vr_type::DA, "");
169 sr.set_string(tags::patient_sex, vr_type::CS, "");
170}
171
174 const ai_assessment& assessment) const {
175
176 // Root content: CONTAINER
177 sr.set_string(aira_tags::value_type, vr_type::CS, "CONTAINER");
178
179 // Concept Name: AI Result Assessment (custom code)
180 auto concept_name = make_code_item(
181 "AIRA-001", "99PACS", "AI Result Assessment");
182 insert_sequence(sr, aira_tags::concept_name_code_sequence, {concept_name});
183
184 // Content Template Sequence (IHE AIRA assessment template)
185 dicom_dataset template_item;
186 template_item.set_string(aira_tags::template_identifier, vr_type::CS,
187 "AIRA");
188 template_item.set_string(aira_tags::mapping_resource, vr_type::CS, "99PACS");
189 insert_sequence(sr, aira_tags::content_template_sequence, {template_item});
190
191 // Content Sequence items
192 std::vector<dicom_dataset> content_items;
193
194 // Item 1: Assessment Type (CODE)
195 {
196 dicom_dataset item;
197 item.set_string(aira_tags::relationship_type, vr_type::CS, "CONTAINS");
198 item.set_string(aira_tags::value_type, vr_type::CS, "CODE");
199
200 auto name = make_code_item("AIRA-010", "99PACS", "Assessment Decision");
201 insert_sequence(item, aira_tags::concept_name_code_sequence, {name});
202
203 auto value = make_code_item(
204 assessment_type_to_code(assessment.type),
205 "99PACS",
206 assessment_type_to_meaning(assessment.type));
207 // Concept Code Sequence (0040,A168) for CODE value type
208 constexpr dicom_tag concept_code_sequence{0x0040, 0xA168};
209 insert_sequence(item, concept_code_sequence, {value});
210
211 content_items.push_back(std::move(item));
212 }
213
214 // Item 2: Assessor (PNAME)
215 {
216 dicom_dataset item;
217 item.set_string(aira_tags::relationship_type, vr_type::CS, "HAS OBS CONTEXT");
218 item.set_string(aira_tags::value_type, vr_type::CS, "PNAME");
219
220 auto name = make_code_item("AIRA-020", "99PACS", "Assessor");
221 insert_sequence(item, aira_tags::concept_name_code_sequence, {name});
222
223 constexpr dicom_tag person_name{0x0040, 0xA123};
224 item.set_string(person_name, vr_type::PN, assessment.assessor_name);
225
226 content_items.push_back(std::move(item));
227 }
228
229 // Item 3: Comment (TEXT, optional)
230 if (assessment.comment && !assessment.comment->empty()) {
231 dicom_dataset item;
232 item.set_string(aira_tags::relationship_type, vr_type::CS, "CONTAINS");
233 item.set_string(aira_tags::value_type, vr_type::CS, "TEXT");
234
235 auto name = make_code_item("AIRA-030", "99PACS", "Assessment Comment");
236 insert_sequence(item, aira_tags::concept_name_code_sequence, {name});
237
238 item.set_string(aira_tags::text_value, vr_type::UT,
239 assessment.comment.value());
240
241 content_items.push_back(std::move(item));
242 }
243
244 // Item 4: Modification description (TEXT, only for modify type)
245 if (assessment.type == assessment_type::modify && assessment.modification) {
246 dicom_dataset item;
247 item.set_string(aira_tags::relationship_type, vr_type::CS, "CONTAINS");
248 item.set_string(aira_tags::value_type, vr_type::CS, "TEXT");
249
250 auto name = make_code_item("AIRA-040", "99PACS",
251 "Modification Description");
252 insert_sequence(item, aira_tags::concept_name_code_sequence, {name});
253
254 item.set_string(aira_tags::text_value, vr_type::UT,
255 assessment.modification->description);
256
257 content_items.push_back(std::move(item));
258 }
259
260 // Item 5: Rejection reason (CODE, only for reject type)
261 if (assessment.type == assessment_type::reject && assessment.rejection) {
262 dicom_dataset item;
263 item.set_string(aira_tags::relationship_type, vr_type::CS, "CONTAINS");
264 item.set_string(aira_tags::value_type, vr_type::CS, "CODE");
265
266 auto name = make_code_item("AIRA-050", "99PACS", "Rejection Reason");
267 insert_sequence(item, aira_tags::concept_name_code_sequence, {name});
268
269 auto value = make_code_item(
270 assessment.rejection->reason_code,
271 assessment.rejection->reason_scheme,
272 assessment.rejection->reason_description);
273 constexpr dicom_tag concept_code_sequence{0x0040, 0xA168};
274 insert_sequence(item, concept_code_sequence, {value});
275
276 content_items.push_back(std::move(item));
277 }
278
279 insert_sequence(sr, aira_tags::content_sequence, std::move(content_items));
280}
281
284 const ai_assessment& assessment) const {
285
286 std::vector<dicom_dataset> ref_items;
287
288 // Reference to the assessed AI result
289 dicom_dataset ref_item;
291 assessment.ai_result.sop_class_uid);
293 assessment.ai_result.sop_instance_uid);
294 ref_items.push_back(std::move(ref_item));
295
296 // Reference to modified result (if applicable)
297 if (assessment.type == assessment_type::modify &&
298 assessment.modification &&
299 assessment.modification->modified_result_uid) {
300 dicom_dataset mod_ref;
302 assessment.modification->modified_result_class_uid
303 .value_or(assessment.ai_result.sop_class_uid));
305 assessment.modification->modified_result_uid.value());
306 ref_items.push_back(std::move(mod_ref));
307 }
308
309 insert_sequence(sr, aira_tags::referenced_sop_sequence,
310 std::move(ref_items));
311}
312
314 static constexpr const char* uid_root = "1.2.826.0.1.3680043.2.1545.1";
315 static std::mt19937_64 gen{std::random_device{}()};
316 static std::uniform_int_distribution<uint64_t> dist;
317
318 auto now = std::chrono::system_clock::now();
319 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
320 now.time_since_epoch()).count();
321
322 return std::string(uid_root) + "." + std::to_string(timestamp) +
323 "." + std::to_string(dist(gen) % 100000);
324}
325
326// =============================================================================
327// Static Methods
328// =============================================================================
329
331 switch (type) {
332 case assessment_type::accept: return "AIRA-ACCEPT";
333 case assessment_type::modify: return "AIRA-MODIFY";
334 case assessment_type::reject: return "AIRA-REJECT";
335 }
336 return "AIRA-ACCEPT";
337}
338
340 switch (type) {
341 case assessment_type::accept: return "AI Result Accepted";
342 case assessment_type::modify: return "AI Result Modified";
343 case assessment_type::reject: return "AI Result Rejected";
344 }
345 return "AI Result Accepted";
346}
347
349 assessment_status status) {
350 switch (status) {
351 case assessment_status::draft: return "PARTIAL";
352 case assessment_status::final_: return "COMPLETE";
353 case assessment_status::amended: return "COMPLETE";
354 }
355 return "PARTIAL";
356}
357
358// =============================================================================
359// Free Functions
360// =============================================================================
361
362std::string to_string(assessment_type type) {
363 switch (type) {
364 case assessment_type::accept: return "accept";
365 case assessment_type::modify: return "modify";
366 case assessment_type::reject: return "reject";
367 }
368 return "accept";
369}
370
371std::string to_string(assessment_status status) {
372 switch (status) {
373 case assessment_status::draft: return "draft";
374 case assessment_status::final_: return "final";
375 case assessment_status::amended: return "amended";
376 }
377 return "draft";
378}
379
380} // namespace kcenon::pacs::ai
IHE AIRA (AI Result Assessment) - Assessment Creator Actor.
assessment_creation_result create_assessment(const ai_assessment &assessment) const
Create an assessment SR document for an AI result.
static std::string assessment_type_to_meaning(assessment_type type)
Convert assessment_type to human-readable meaning.
static std::string status_to_completion_flag(assessment_status status)
Convert assessment_status to DICOM completion flag value.
void build_referenced_sop_sequence(core::dicom_dataset &sr, const ai_assessment &assessment) const
void build_patient_module(core::dicom_dataset &sr, const ai_assessment &assessment) const
static std::string assessment_type_to_code(assessment_type type)
Convert assessment_type to DICOM coded value.
void build_sr_content(core::dicom_dataset &sr, const ai_assessment &assessment) const
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag template_identifier
constexpr dicom_tag coding_scheme_designator
constexpr dicom_tag content_sequence
constexpr dicom_tag concept_name_code_sequence
constexpr dicom_tag mapping_resource
constexpr dicom_tag value_type
constexpr dicom_tag content_template_sequence
constexpr dicom_tag referenced_sop_class_uid
constexpr dicom_tag verification_flag
constexpr dicom_tag completion_flag
constexpr dicom_tag referenced_sop_sequence
constexpr dicom_tag text_value
constexpr dicom_tag referenced_sop_instance_uid
constexpr dicom_tag code_value
constexpr dicom_tag code_meaning
constexpr dicom_tag relationship_type
assessment_type
Assessment decision made by a clinician on an AI result.
@ accept
Clinician accepts AI result as-is.
@ reject
Clinician rejects AI result with reason.
@ modify
Clinician modifies AI result (e.g., edits segmentation)
auto to_string(inference_status_code status) -> std::string
Convert inference status code to string.
assessment_status
Assessment status tracking.
@ amended
Previously finalized assessment has been amended.
@ draft
Assessment in progress, not yet finalized.
@ final_
Assessment finalized and signed off.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag content_time
Content Time.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
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 study_instance_uid
Study Instance UID.
constexpr dicom_tag item
Item.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag content_date
Content Date.
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 dicom_tag instance_number
Instance Number.
std::string_view meaning
Complete assessment of an AI result.
std::optional< assessment_rejection > rejection
Rejection details (only for assessment_type::reject)
assessment_status status
Current status of the assessment.
assessed_result_reference ai_result
Reference to the AI result being assessed.
assessment_type type
Assessment type (accept/modify/reject)
std::optional< std::string > comment
Free-text comment about the assessment.
std::string assessor_name
Clinician who performed the assessment.
std::optional< assessment_modification > modification
Modification details (only for assessment_type::modify)
std::string study_instance_uid
Study Instance UID containing the AI result.
std::string sop_class_uid
SOP Class UID of the AI result.
std::string sop_instance_uid
SOP Instance UID of the AI result.
Result of creating an assessment SR document.
std::string assessment_uid
SOP Instance UID of the created assessment.
std::optional< core::dicom_dataset > sr_dataset
The created assessment SR dataset.
std::string error_message
Error message (if failed)
bool success
Whether creation succeeded.
std::string_view name