Mercurial > hg > orthanc-stone
annotate Framework/Toolbox/DicomStructureSet.cpp @ 40:7207a407bcd8
shared copyright with osimis
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 04 Jan 2017 16:37:42 +0100 |
parents | 6465fbd23bce |
children | 28956ed68280 |
rev | line source |
---|---|
0 | 1 /** |
2 * Stone of Orthanc | |
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics | |
4 * Department, University Hospital of Liege, Belgium | |
40
7207a407bcd8
shared copyright with osimis
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
35
diff
changeset
|
5 * Copyright (C) 2017 Osimis, Belgium |
0 | 6 * |
7 * This program is free software: you can redistribute it and/or | |
8 * modify it under the terms of the GNU General Public License as | |
9 * published by the Free Software Foundation, either version 3 of the | |
10 * License, or (at your option) any later version. | |
11 * | |
12 * In addition, as a special exception, the copyright holders of this | |
13 * program give permission to link the code of its release with the | |
14 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
15 * that use the same license as the "OpenSSL" library), and distribute | |
16 * the linked executables. You must obey the GNU General Public License | |
17 * in all respects for all of the code used other than "OpenSSL". If you | |
18 * modify file(s) with this exception, you may extend this exception to | |
19 * your version of the file(s), but you are not obligated to do so. If | |
20 * you do not wish to do so, delete this exception statement from your | |
21 * version. If you delete this exception statement from all source files | |
22 * in the program, then also delete it here. | |
23 * | |
24 * This program is distributed in the hope that it will be useful, but | |
25 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
26 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
27 * General Public License for more details. | |
28 * | |
29 * You should have received a copy of the GNU General Public License | |
30 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
31 **/ | |
32 | |
33 | |
34 #include "DicomStructureSet.h" | |
35 | |
16 | 36 #include "../../Resources/Orthanc/Core/Logging.h" |
37 #include "../../Resources/Orthanc/Core/OrthancException.h" | |
32 | 38 #include "../../Resources/Orthanc/Plugins/Samples/Common/FullOrthancDataset.h" |
35 | 39 #include "../Toolbox/MessagingToolbox.h" |
0 | 40 |
41 #include <stdio.h> | |
42 #include <boost/lexical_cast.hpp> | |
43 | |
44 namespace OrthancStone | |
45 { | |
32 | 46 static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_GEOMETRIC_TYPE(0x3006, 0x0042); |
47 static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_IMAGE_SEQUENCE(0x3006, 0x0016); | |
48 static const OrthancPlugins::DicomTag DICOM_TAG_CONTOUR_SEQUENCE(0x3006, 0x0040); | |
49 static const OrthancPlugins::DicomTag DICOM_TAG_NUMBER_OF_CONTOUR_POINTS(0x3006, 0x0046); | |
50 static const OrthancPlugins::DicomTag DICOM_TAG_REFERENCED_SOP_INSTANCE_UID(0x0008, 0x1155); | |
51 static const OrthancPlugins::DicomTag DICOM_TAG_ROI_CONTOUR_SEQUENCE(0x3006, 0x0039); | |
52 static const OrthancPlugins::DicomTag DICOM_TAG_ROI_DISPLAY_COLOR(0x3006, 0x002a); | |
53 static const OrthancPlugins::DicomTag DICOM_TAG_ROI_NAME(0x3006, 0x0026); | |
54 static const OrthancPlugins::DicomTag DICOM_TAG_RT_ROI_INTERPRETED_TYPE(0x3006, 0x00a4); | |
55 static const OrthancPlugins::DicomTag DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE(0x3006, 0x0080); | |
56 static const OrthancPlugins::DicomTag DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE(0x3006, 0x0020); | |
0 | 57 |
58 | |
59 static uint8_t ConvertColor(double v) | |
60 { | |
61 if (v < 0) | |
62 { | |
63 return 0; | |
64 } | |
65 else if (v >= 255) | |
66 { | |
67 return 255; | |
68 } | |
69 else | |
70 { | |
71 return static_cast<uint8_t>(v); | |
72 } | |
73 } | |
74 | |
75 | |
76 SliceGeometry DicomStructureSet::ExtractSliceGeometry(double& sliceThickness, | |
31
9aace933cb64
sharing code with the Orthanc core
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
16
diff
changeset
|
77 OrthancPlugins::IOrthancConnection& orthanc, |
32 | 78 const OrthancPlugins::IDicomDataset& tags, |
79 size_t contourIndex, | |
80 size_t sliceIndex) | |
0 | 81 { |
32 | 82 using namespace OrthancPlugins; |
0 | 83 |
32 | 84 size_t size; |
85 if (!tags.GetSequenceSize(size, DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, contourIndex, | |
86 DICOM_TAG_CONTOUR_SEQUENCE, sliceIndex, | |
87 DICOM_TAG_CONTOUR_IMAGE_SEQUENCE)) || | |
88 size != 1) | |
0 | 89 { |
90 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
91 } | |
92 | |
32 | 93 DicomDatasetReader reader(tags); |
94 std::string parentUid = reader.GetMandatoryStringValue(DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, contourIndex, | |
95 DICOM_TAG_CONTOUR_SEQUENCE, sliceIndex, | |
96 DICOM_TAG_CONTOUR_IMAGE_SEQUENCE, 0, | |
97 DICOM_TAG_REFERENCED_SOP_INSTANCE_UID)); | |
0 | 98 |
34 | 99 Json::Value parentLookup; |
100 MessagingToolbox::RestApiPost(parentLookup, orthanc, "/tools/lookup", parentUid); | |
0 | 101 |
34 | 102 if (parentLookup.type() != Json::arrayValue || |
103 parentLookup.size() != 1 || | |
104 !parentLookup[0].isMember("Type") || | |
105 !parentLookup[0].isMember("Path") || | |
106 parentLookup[0]["Type"].type() != Json::stringValue || | |
107 parentLookup[0]["ID"].type() != Json::stringValue || | |
108 parentLookup[0]["Type"].asString() != "Instance") | |
0 | 109 { |
110 throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource); | |
111 } | |
112 | |
113 Json::Value parentInstance; | |
34 | 114 MessagingToolbox::RestApiGet(parentInstance, orthanc, "/instances/" + parentLookup[0]["ID"].asString()); |
0 | 115 |
116 if (parentInstance.type() != Json::objectValue || | |
117 !parentInstance.isMember("ParentSeries") || | |
118 parentInstance["ParentSeries"].type() != Json::stringValue) | |
119 { | |
120 throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); | |
121 } | |
122 | |
123 std::string parentSeriesId = parentInstance["ParentSeries"].asString(); | |
124 bool isFirst = parentSeriesId_.empty(); | |
125 | |
126 if (isFirst) | |
127 { | |
128 parentSeriesId_ = parentSeriesId; | |
129 } | |
130 else if (parentSeriesId_ != parentSeriesId) | |
131 { | |
132 LOG(ERROR) << "This RT-STRUCT refers to several different series"; | |
133 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
134 } | |
135 | |
34 | 136 FullOrthancDataset parentTags(orthanc, "/instances/" + parentLookup[0]["ID"].asString() + "/tags"); |
32 | 137 SliceGeometry slice(parentTags); |
0 | 138 |
32 | 139 Vector v; |
140 if (GeometryToolbox::ParseVector(v, parentTags, DICOM_TAG_SLICE_THICKNESS) && | |
141 v.size() > 0) | |
0 | 142 { |
32 | 143 sliceThickness = v[0]; |
144 } | |
145 else | |
146 { | |
147 sliceThickness = 1; // 1 mm by default | |
0 | 148 } |
149 | |
150 if (isFirst) | |
151 { | |
152 normal_ = slice.GetNormal(); | |
153 } | |
154 else if (!GeometryToolbox::IsParallel(normal_, slice.GetNormal())) | |
155 { | |
156 LOG(ERROR) << "Incompatible orientation of slices in this RT-STRUCT"; | |
157 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
158 } | |
159 | |
160 return slice; | |
161 } | |
162 | |
163 | |
164 const DicomStructureSet::Structure& DicomStructureSet::GetStructure(size_t index) const | |
165 { | |
166 if (index >= structures_.size()) | |
167 { | |
168 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); | |
169 } | |
170 | |
171 return structures_[index]; | |
172 } | |
173 | |
174 | |
175 bool DicomStructureSet::IsPolygonOnSlice(const Polygon& polygon, | |
176 const SliceGeometry& geometry) const | |
177 { | |
178 double d = boost::numeric::ublas::inner_prod(geometry.GetOrigin(), normal_); | |
179 | |
180 return (GeometryToolbox::IsNear(d, polygon.projectionAlongNormal_, polygon.sliceThickness_ / 2.0) && | |
181 !polygon.points_.empty()); | |
182 } | |
183 | |
184 | |
31
9aace933cb64
sharing code with the Orthanc core
Sebastien Jodogne <s.jodogne@gmail.com>
parents:
16
diff
changeset
|
185 DicomStructureSet::DicomStructureSet(OrthancPlugins::IOrthancConnection& orthanc, |
0 | 186 const std::string& instanceId) |
187 { | |
32 | 188 using namespace OrthancPlugins; |
0 | 189 |
32 | 190 FullOrthancDataset tags(orthanc, "/instances/" + instanceId + "/tags"); |
191 DicomDatasetReader reader(tags); | |
192 | |
193 size_t count, tmp; | |
194 if (!tags.GetSequenceSize(count, DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE) || | |
195 !tags.GetSequenceSize(tmp, DICOM_TAG_ROI_CONTOUR_SEQUENCE) || | |
196 tmp != count || | |
197 !tags.GetSequenceSize(tmp, DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE) || | |
198 tmp != count) | |
0 | 199 { |
200 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
201 } | |
202 | |
32 | 203 structures_.resize(count); |
204 for (size_t i = 0; i < count; i++) | |
205 { | |
206 structures_[i].interpretation_ = reader.GetStringValue(DicomPath(DICOM_TAG_RT_ROI_OBSERVATIONS_SEQUENCE, i, | |
207 DICOM_TAG_RT_ROI_INTERPRETED_TYPE), | |
208 "No interpretation"); | |
0 | 209 |
32 | 210 structures_[i].name_ = reader.GetStringValue(DicomPath(DICOM_TAG_STRUCTURE_SET_ROI_SEQUENCE, i, |
211 DICOM_TAG_ROI_NAME), | |
212 "No interpretation"); | |
0 | 213 |
214 Vector color; | |
32 | 215 if (GeometryToolbox::ParseVector(color, tags, DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, |
216 DICOM_TAG_ROI_DISPLAY_COLOR)) && | |
217 color.size() == 3) | |
0 | 218 { |
32 | 219 structures_[i].red_ = ConvertColor(color[0]); |
220 structures_[i].green_ = ConvertColor(color[1]); | |
221 structures_[i].blue_ = ConvertColor(color[2]); | |
222 } | |
223 else | |
224 { | |
225 structures_[i].red_ = 255; | |
226 structures_[i].green_ = 0; | |
227 structures_[i].blue_ = 0; | |
0 | 228 } |
229 | |
32 | 230 size_t countSlices; |
231 if (!tags.GetSequenceSize(countSlices, DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, | |
232 DICOM_TAG_CONTOUR_SEQUENCE))) | |
233 { | |
234 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
235 } | |
0 | 236 |
237 LOG(WARNING) << "New RT structure: \"" << structures_[i].name_ | |
238 << "\" with interpretation \"" << structures_[i].interpretation_ | |
32 | 239 << "\" containing " << countSlices << " slices (color: " |
0 | 240 << static_cast<int>(structures_[i].red_) << "," |
241 << static_cast<int>(structures_[i].green_) << "," | |
242 << static_cast<int>(structures_[i].blue_) << ")"; | |
243 | |
32 | 244 for (size_t j = 0; j < countSlices; j++) |
0 | 245 { |
32 | 246 unsigned int countPoints; |
0 | 247 |
32 | 248 if (!reader.GetUnsignedIntegerValue(countPoints, DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, |
249 DICOM_TAG_CONTOUR_SEQUENCE, j, | |
250 DICOM_TAG_NUMBER_OF_CONTOUR_POINTS))) | |
251 { | |
252 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
253 } | |
254 | |
255 LOG(INFO) << "Parsing slice containing " << countPoints << " vertices"; | |
0 | 256 |
32 | 257 std::string type = reader.GetMandatoryStringValue(DicomPath(DICOM_TAG_ROI_CONTOUR_SEQUENCE, i, |
258 DICOM_TAG_CONTOUR_SEQUENCE, j, | |
259 DICOM_TAG_CONTOUR_GEOMETRIC_TYPE)); | |
260 if (type != "CLOSED_PLANAR") | |
0 | 261 { |
32 | 262 LOG(ERROR) << "Cannot handle contour with geometry type: " << type; |
0 | 263 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); |
264 } | |
265 | |
32 | 266 // The "CountourData" tag (3006,0050) is too large to be |
267 // returned by the "/instances/{id}/tags" URI: Access it using | |
268 // the raw "/instances/{id}/content/{...}" endpoint | |
0 | 269 std::string slicesData; |
270 orthanc.RestApiGet(slicesData, "/instances/" + instanceId + "/content/3006-0039/" + | |
271 boost::lexical_cast<std::string>(i) + "/3006-0040/" + | |
272 boost::lexical_cast<std::string>(j) + "/3006-0050"); | |
273 | |
274 Vector points; | |
275 if (!GeometryToolbox::ParseVector(points, slicesData) || | |
32 | 276 points.size() != 3 * countPoints) |
0 | 277 { |
278 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
279 } | |
280 | |
281 Polygon polygon; | |
32 | 282 SliceGeometry geometry = ExtractSliceGeometry(polygon.sliceThickness_, orthanc, tags, i, j); |
0 | 283 polygon.projectionAlongNormal_ = geometry.ProjectAlongNormal(geometry.GetOrigin()); |
284 | |
32 | 285 for (size_t k = 0; k < countPoints; k++) |
0 | 286 { |
287 Vector v(3); | |
288 v[0] = points[3 * k]; | |
289 v[1] = points[3 * k + 1]; | |
290 v[2] = points[3 * k + 2]; | |
291 | |
292 if (!GeometryToolbox::IsNear(geometry.ProjectAlongNormal(v), | |
293 polygon.projectionAlongNormal_, | |
294 polygon.sliceThickness_ / 2.0 /* in mm */)) | |
295 { | |
296 LOG(ERROR) << "This RT-STRUCT contains a point that is off the slice of its instance"; | |
297 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
298 } | |
299 | |
300 polygon.points_.push_back(v); | |
301 } | |
302 | |
303 structures_[i].polygons_.push_back(polygon); | |
304 } | |
305 } | |
306 | |
307 if (parentSeriesId_.empty() || | |
308 normal_.size() != 3) | |
309 { | |
310 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
311 } | |
312 } | |
313 | |
314 | |
315 Vector DicomStructureSet::GetStructureCenter(size_t index) const | |
316 { | |
317 const Structure& structure = GetStructure(index); | |
318 | |
319 Vector center; | |
320 GeometryToolbox::AssignVector(center, 0, 0, 0); | |
321 if (structure.polygons_.empty()) | |
322 { | |
323 return center; | |
324 } | |
325 | |
326 double n = static_cast<double>(structure.polygons_.size()); | |
327 | |
328 for (Polygons::const_iterator polygon = structure.polygons_.begin(); | |
329 polygon != structure.polygons_.end(); ++polygon) | |
330 { | |
331 if (!polygon->points_.empty()) | |
332 { | |
333 center += polygon->points_.front() / n; | |
334 } | |
335 } | |
336 | |
337 return center; | |
338 } | |
339 | |
340 | |
341 const std::string& DicomStructureSet::GetStructureName(size_t index) const | |
342 { | |
343 return GetStructure(index).name_; | |
344 } | |
345 | |
346 | |
347 const std::string& DicomStructureSet::GetStructureInterpretation(size_t index) const | |
348 { | |
349 return GetStructure(index).interpretation_; | |
350 } | |
351 | |
352 | |
353 void DicomStructureSet::GetStructureColor(uint8_t& red, | |
354 uint8_t& green, | |
355 uint8_t& blue, | |
356 size_t index) const | |
357 { | |
358 const Structure& s = GetStructure(index); | |
359 red = s.red_; | |
360 green = s.green_; | |
361 blue = s.blue_; | |
362 } | |
363 | |
364 | |
365 void DicomStructureSet::Render(CairoContext& context, | |
366 const SliceGeometry& slice) const | |
367 { | |
368 cairo_t* cr = context.GetObject(); | |
369 | |
370 for (Structures::const_iterator structure = structures_.begin(); | |
371 structure != structures_.end(); ++structure) | |
372 { | |
373 for (Polygons::const_iterator polygon = structure->polygons_.begin(); | |
374 polygon != structure->polygons_.end(); ++polygon) | |
375 { | |
376 if (IsPolygonOnSlice(*polygon, slice)) | |
377 { | |
378 context.SetSourceColor(structure->red_, structure->green_, structure->blue_); | |
379 | |
380 Points::const_iterator p = polygon->points_.begin(); | |
381 | |
382 double x, y; | |
383 slice.ProjectPoint(x, y, *p); | |
384 cairo_move_to(cr, x, y); | |
385 ++p; | |
386 | |
387 while (p != polygon->points_.end()) | |
388 { | |
389 slice.ProjectPoint(x, y, *p); | |
390 cairo_line_to(cr, x, y); | |
391 ++p; | |
392 } | |
393 | |
394 slice.ProjectPoint(x, y, *polygon->points_.begin()); | |
395 cairo_line_to(cr, x, y); | |
396 } | |
397 } | |
398 } | |
399 | |
400 cairo_stroke(cr); | |
401 } | |
402 } |