comparison OrthancStone/Sources/Toolbox/DicomStructuredReport.cpp @ 2085:554bc96e7508

added DicomStructuredReport
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 07 Nov 2023 17:03:38 +0100
parents
children 51c8b21b81e4
comparison
equal deleted inserted replaced
2083:0c0c228a3a73 2085:554bc96e7508
1 /**
2 * Stone of Orthanc
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
4 * Department, University Hospital of Liege, Belgium
5 * Copyright (C) 2017-2023 Osimis S.A., Belgium
6 * Copyright (C) 2021-2023 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
7 *
8 * This program is free software: you can redistribute it and/or
9 * modify it under the terms of the GNU Lesser General Public License
10 * as published by the Free Software Foundation, either version 3 of
11 * the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
17 *
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with this program. If not, see
20 * <http://www.gnu.org/licenses/>.
21 **/
22
23
24 #include "DicomStructuredReport.h"
25
26 #include "../Scene2D/ScenePoint2D.h"
27
28 #include <OrthancException.h>
29 #include <SerializationToolbox.h>
30
31 #include <dcmtk/dcmdata/dcdeftag.h>
32 #include <dcmtk/dcmdata/dcsequen.h>
33 #include <dcmtk/dcmdata/dcfilefo.h>
34
35
36 static std::string GetStringValue(DcmItem& dataset,
37 const DcmTagKey& key)
38 {
39 const char* value = NULL;
40 if (dataset.findAndGetString(key, value).good() &&
41 value != NULL)
42 {
43 return value;
44 }
45 else
46 {
47 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
48 "Missing tag in DICOM-SR: " + key.toString());
49 }
50 }
51
52
53 static DcmSequenceOfItems& GetSequenceValue(DcmItem& dataset,
54 const DcmTagKey& key)
55 {
56 DcmSequenceOfItems* sequence = NULL;
57 if (dataset.findAndGetSequence(key, sequence).good() &&
58 sequence != NULL)
59 {
60 return *sequence;
61 }
62 else
63 {
64 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
65 "Missing sequence in DICOM-SR: " + key.toString());
66 }
67 }
68
69
70 static void CheckStringValue(DcmItem& dataset,
71 const DcmTagKey& key,
72 const std::string& expected)
73 {
74 if (GetStringValue(dataset, key) != expected)
75 {
76 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
77 }
78 }
79
80
81 static bool IsDicomTemplate(DcmItem& dataset,
82 const std::string& tid)
83 {
84 DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_ContentTemplateSequence);
85
86 return (sequence.card() == 1 &&
87 GetStringValue(*sequence.getItem(0), DCM_MappingResource) == "DCMR" &&
88 GetStringValue(*sequence.getItem(0), DCM_TemplateIdentifier) == tid);
89 }
90
91
92 static bool IsValidConcept(DcmItem& dataset,
93 const DcmTagKey& key,
94 const std::string& scheme,
95 const std::string& concept)
96 {
97 DcmSequenceOfItems& sequence = GetSequenceValue(dataset, key);
98
99 return (sequence.card() == 1 &&
100 GetStringValue(*sequence.getItem(0), DCM_CodingSchemeDesignator) == scheme &&
101 GetStringValue(*sequence.getItem(0), DCM_CodeValue) == concept);
102 }
103
104
105 static bool IsDicomConcept(DcmItem& dataset,
106 const std::string& concept)
107 {
108 return IsValidConcept(dataset, DCM_ConceptNameCodeSequence, "DCM", concept);
109 }
110
111
112 namespace OrthancStone
113 {
114 class DicomStructuredReport::Structure : public boost::noncopyable
115 {
116 private:
117 std::string sopInstanceUid_;
118 bool hasFrameNumber_;
119 unsigned int frameNumber_;
120 bool hasProbabilityOfCancer_;
121 float probabilityOfCancer_;
122
123 public:
124 Structure(const std::string& sopInstanceUid) :
125 sopInstanceUid_(sopInstanceUid),
126 hasFrameNumber_(false),
127 hasProbabilityOfCancer_(false)
128 {
129 }
130
131 virtual ~Structure()
132 {
133 }
134
135 void SetFrameNumber(unsigned int frame)
136 {
137 if (frame <= 0)
138 {
139 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
140 }
141 else
142 {
143 hasFrameNumber_ = true;
144 frameNumber_ = frame;
145 }
146 }
147
148 void SetProbabilityOfCancer(float probability)
149 {
150 if (probability < 0 ||
151 probability > 100)
152 {
153 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
154 }
155 else
156 {
157 hasProbabilityOfCancer_ = true;
158 probabilityOfCancer_ = probability;
159 }
160 }
161
162 bool HasFrameNumber() const
163 {
164 return hasFrameNumber_;
165 }
166
167 bool HasProbabilityOfCancer() const
168 {
169 return hasProbabilityOfCancer_;
170 }
171
172 unsigned int GetFrameNumber() const
173 {
174 if (hasFrameNumber_)
175 {
176 return frameNumber_;
177 }
178 else
179 {
180 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
181 }
182 }
183
184 float GetProbabilityOfCancer() const
185 {
186 if (hasProbabilityOfCancer_)
187 {
188 return probabilityOfCancer_;
189 }
190 else
191 {
192 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
193 }
194 }
195 };
196
197
198 class DicomStructuredReport::Point : public Structure
199 {
200 private:
201 ScenePoint2D point_;
202
203 public:
204 Point(const std::string& sopInstanceUid,
205 double x,
206 double y) :
207 Structure(sopInstanceUid),
208 point_(x, y)
209 {
210 }
211
212 const ScenePoint2D& GetPoint() const
213 {
214 return point_;
215 }
216 };
217
218
219 class DicomStructuredReport::Polyline : public Structure
220 {
221 private:
222 std::vector<ScenePoint2D> points_;
223
224 public:
225 Polyline(const std::string& sopInstanceUid,
226 const float* points,
227 unsigned long pointsCount) :
228 Structure(sopInstanceUid)
229 {
230 if (pointsCount % 2 != 0)
231 {
232 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
233 }
234
235 points_.reserve(pointsCount / 2);
236
237 for (unsigned long i = 0; i < pointsCount; i += 2)
238 {
239 points_.push_back(ScenePoint2D(points[i], points[i + 1]));
240 }
241 }
242
243 size_t GetSize() const
244 {
245 return points_.size();
246 }
247
248 const ScenePoint2D& GetPoint(size_t i) const
249 {
250 if (i >= points_.size())
251 {
252 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
253 }
254 else
255 {
256 return points_[i];
257 }
258 }
259 };
260
261
262 void DicomStructuredReport::AddStructure(const std::string& sopInstanceUid,
263 DcmItem& group,
264 bool hasFrameNumber,
265 unsigned int frameNumber,
266 bool hasProbabilityOfCancer,
267 float probabilityOfCancer)
268 {
269 const std::string graphicType = GetStringValue(group, DCM_GraphicType);
270
271 const Float32* coords = NULL;
272 unsigned long coordsCount = 0;
273 if (!group.findAndGetFloat32Array(DCM_GraphicData, coords, &coordsCount).good() ||
274 (coordsCount != 0 && coords == NULL))
275 {
276 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
277 "Cannot read coordinates for region in DICOM-SR");
278 }
279
280 std::unique_ptr<Structure> structure;
281
282 if (graphicType == "POINT")
283 {
284 if (coordsCount != 2)
285 {
286 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
287 }
288 else
289 {
290 structure.reset(new Point(sopInstanceUid, coords[0], coords[1]));
291 }
292 }
293 else if (graphicType == "POLYLINE")
294 {
295 structure.reset(new Polyline(sopInstanceUid, coords, coordsCount));
296 }
297 else
298 {
299 return; // Unsupported graphic type
300 }
301
302 assert(structure.get() != NULL);
303
304 if (hasFrameNumber)
305 {
306 structure->SetFrameNumber(frameNumber);
307 }
308
309 if (hasProbabilityOfCancer)
310 {
311 structure->SetProbabilityOfCancer(probabilityOfCancer);
312 }
313
314 structures_.push_back(structure.release());
315 }
316
317
318 DicomStructuredReport::DicomStructuredReport(Orthanc::ParsedDicomFile& dicom)
319 {
320 DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset();
321
322 CheckStringValue(dataset, DCM_Modality, "SR");
323 CheckStringValue(dataset, DCM_SOPClassUID, "1.2.840.10008.5.1.4.1.1.88.33"); // Comprehensive SR IOD
324 CheckStringValue(dataset, DCM_ValueType, "CONTAINER");
325
326 if (!IsDicomConcept(dataset, "126000") /* Imaging measurement report */ ||
327 !IsDicomTemplate(dataset, "1500"))
328 {
329 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
330 }
331
332 DcmSequenceOfItems& sequence = GetSequenceValue(dataset, DCM_CurrentRequestedProcedureEvidenceSequence);
333
334 std::list<std::string> tmp;
335
336 for (unsigned long i = 0; i < sequence.card(); i++)
337 {
338 std::string studyInstanceUid = GetStringValue(*sequence.getItem(i), DCM_StudyInstanceUID);
339
340 DcmSequenceOfItems* referencedSeries = NULL;
341 if (!sequence.getItem(i)->findAndGetSequence(DCM_ReferencedSeriesSequence, referencedSeries).good() ||
342 referencedSeries == NULL)
343 {
344 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
345 }
346
347 for (unsigned long j = 0; j < referencedSeries->card(); j++)
348 {
349 std::string seriesInstanceUid = GetStringValue(*referencedSeries->getItem(j), DCM_SeriesInstanceUID);
350
351 DcmSequenceOfItems* referencedInstances = NULL;
352 if (!referencedSeries->getItem(j)->findAndGetSequence(DCM_ReferencedSOPSequence, referencedInstances).good() ||
353 referencedInstances == NULL)
354 {
355 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
356 }
357
358 for (unsigned int k = 0; k < referencedInstances->card(); k++)
359 {
360 std::string sopClassUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPClassUID);
361 std::string sopInstanceUid = GetStringValue(*referencedInstances->getItem(k), DCM_ReferencedSOPInstanceUID);
362
363 if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end())
364 {
365 instancesInformation_[sopInstanceUid] = ReferencedInstance(studyInstanceUid, seriesInstanceUid, sopClassUid);
366 }
367 else
368 {
369 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
370 "Multiple occurrences of the same instance in DICOM-SR: " + sopInstanceUid);
371 }
372
373 tmp.push_back(sopInstanceUid);
374 }
375 }
376 }
377
378 orderedInstances_.reserve(tmp.size());
379
380 for (std::list<std::string>::const_iterator it = tmp.begin(); it != tmp.end(); ++it)
381 {
382 orderedInstances_.push_back(*it);
383 }
384
385 sequence = GetSequenceValue(dataset, DCM_ContentSequence);
386
387 for (unsigned long i = 0; i < sequence.card(); i++)
388 {
389 DcmItem& item = *sequence.getItem(i);
390
391 if (GetStringValue(item, DCM_RelationshipType) == "CONTAINS" &&
392 GetStringValue(item, DCM_ValueType) == "CONTAINER" &&
393 IsDicomConcept(item, "126010" /* Imaging measurements */))
394 {
395 DcmSequenceOfItems& measurements = GetSequenceValue(item, DCM_ContentSequence);
396
397 for (unsigned long j = 0; j < measurements.card(); j++)
398 {
399 DcmItem& measurement = *measurements.getItem(j);
400
401 if (GetStringValue(measurement, DCM_RelationshipType) == "CONTAINS" &&
402 GetStringValue(measurement, DCM_ValueType) == "CONTAINER" &&
403 IsDicomConcept(measurement, "125007" /* Measurement group */) &&
404 IsDicomTemplate(measurement, "1410"))
405 {
406 DcmSequenceOfItems& groups = GetSequenceValue(measurement, DCM_ContentSequence);
407
408 bool hasProbabilityOfCancer = false;
409 float probabilityOfCancer = 0;
410
411 for (unsigned int k = 0; k < groups.card(); k++)
412 {
413 DcmItem& group = *groups.getItem(k);
414
415 if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
416 GetStringValue(group, DCM_ValueType) == "NUM" &&
417 IsDicomConcept(group, "111047" /* Probability of cancer */))
418 {
419 DcmSequenceOfItems& values = GetSequenceValue(group, DCM_MeasuredValueSequence);
420
421 if (values.card() == 1 &&
422 IsValidConcept(*values.getItem(0), DCM_MeasurementUnitsCodeSequence, "UCUM", "%"))
423 {
424 std::string value = GetStringValue(*values.getItem(0), DCM_NumericValue);
425 if (Orthanc::SerializationToolbox::ParseFloat(probabilityOfCancer, value))
426 {
427 hasProbabilityOfCancer = true;
428 }
429 else
430 {
431 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
432 "Cannot parse float in DICOM-SR: " + value);
433 }
434 }
435 }
436 }
437
438 for (unsigned int k = 0; k < groups.card(); k++)
439 {
440 DcmItem& group = *groups.getItem(k);
441
442 if (GetStringValue(group, DCM_RelationshipType) == "CONTAINS" &&
443 GetStringValue(group, DCM_ValueType) == "SCOORD" &&
444 IsDicomConcept(group, "111030" /* Image region */))
445 {
446 DcmSequenceOfItems& regions = GetSequenceValue(group, DCM_ContentSequence);
447
448 for (unsigned int l = 0; l < regions.card(); l++)
449 {
450 DcmItem& region = *regions.getItem(l);
451
452 if (GetStringValue(region, DCM_RelationshipType) == "SELECTED FROM" &&
453 GetStringValue(region, DCM_ValueType) == "IMAGE" &&
454 IsDicomConcept(region, "111040") /* Original source */)
455 {
456 DcmSequenceOfItems& instances = GetSequenceValue(region, DCM_ReferencedSOPSequence);
457 if (instances.card() != 1)
458 {
459 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
460 "Region cannot reference multiple instances in DICOM-SR");
461 }
462
463 std::string sopInstanceUid = GetStringValue(*instances.getItem(0), DCM_ReferencedSOPInstanceUID);
464 if (instancesInformation_.find(sopInstanceUid) == instancesInformation_.end())
465 {
466 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
467 "Referencing unknown instance in DICOM-SR: " + sopInstanceUid);
468 }
469
470 if (instances.getItem(0)->tagExists(DCM_ReferencedFrameNumber))
471 {
472 std::string frames = GetStringValue(*instances.getItem(0), DCM_ReferencedFrameNumber);
473 std::vector<std::string> tokens;
474 Orthanc::Toolbox::SplitString(tokens, frames, '\\');
475
476 for (size_t m = 0; m < tokens.size(); m++)
477 {
478 uint32_t frame;
479 if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(frame, tokens[m]))
480 {
481 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
482 }
483 else
484 {
485 AddStructure(sopInstanceUid, group, true, frame, hasProbabilityOfCancer, probabilityOfCancer);
486 }
487 }
488 }
489 else
490 {
491 AddStructure(sopInstanceUid, group, false, 0, hasProbabilityOfCancer, probabilityOfCancer);
492 }
493 }
494 }
495 }
496 }
497 }
498 }
499 }
500 }
501 }
502
503
504 DicomStructuredReport::~DicomStructuredReport()
505 {
506 for (std::list<Structure*>::iterator it = structures_.begin(); it != structures_.end(); ++it)
507 {
508 assert(*it != NULL);
509 delete *it;
510 }
511 }
512 }