Mercurial > hg > orthanc-stl
comparison Sources/Plugin.cpp @ 33:2460b376d3f7
reorganization
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 04 Apr 2024 18:50:11 +0200 |
parents | 976da5476810 |
children | bee2017f3088 |
comparison
equal
deleted
inserted
replaced
32:976da5476810 | 33:2460b376d3f7 |
---|---|
20 * You should have received a copy of the GNU General Public License | 20 * You should have received a copy of the GNU General Public License |
21 * along with this program. If not, see <http://www.gnu.org/licenses/>. | 21 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
22 **/ | 22 **/ |
23 | 23 |
24 | 24 |
25 #include "StructurePolygon.h" | |
25 #include "VTKToolbox.h" | 26 #include "VTKToolbox.h" |
26 #include "Vector3D.h" | 27 #include "Vector3D.h" |
27 #include "Toolbox.h" | 28 #include "Toolbox.h" |
28 #include "Extent2D.h" | 29 #include "Extent2D.h" |
29 | 30 |
38 #include <Logging.h> | 39 #include <Logging.h> |
39 #include <OrthancFramework.h> | 40 #include <OrthancFramework.h> |
40 #include <SerializationToolbox.h> | 41 #include <SerializationToolbox.h> |
41 #include <SystemToolbox.h> | 42 #include <SystemToolbox.h> |
42 | 43 |
44 #include <vtkNew.h> | |
45 | |
43 #include <boost/thread/shared_mutex.hpp> | 46 #include <boost/thread/shared_mutex.hpp> |
44 | 47 |
45 #define ORTHANC_PLUGIN_NAME "stl" | 48 #define ORTHANC_PLUGIN_NAME "stl" |
46 | 49 |
47 | 50 |
145 } | 148 } |
146 | 149 |
147 | 150 |
148 | 151 |
149 | 152 |
150 #include <dcmtk/dcmdata/dcdeftag.h> | |
151 #include <dcmtk/dcmdata/dcfilefo.h> | 153 #include <dcmtk/dcmdata/dcfilefo.h> |
152 #include <dcmtk/dcmdata/dcitem.h> | |
153 #include <dcmtk/dcmdata/dcsequen.h> | 154 #include <dcmtk/dcmdata/dcsequen.h> |
154 #include <dcmtk/dcmdata/dcuid.h> | 155 #include <dcmtk/dcmdata/dcuid.h> |
155 | |
156 | |
157 static std::string GetStringValue(DcmItem& item, | |
158 const DcmTagKey& key) | |
159 { | |
160 const char* s = NULL; | |
161 if (!item.findAndGetString(key, s).good() || | |
162 s == NULL) | |
163 { | |
164 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
165 } | |
166 else | |
167 { | |
168 return Orthanc::Toolbox::StripSpaces(s); | |
169 } | |
170 } | |
171 | |
172 | |
173 static void ListStructuresNames(std::set<std::string>& target, | |
174 Orthanc::ParsedDicomFile& source) | |
175 { | |
176 target.clear(); | |
177 | |
178 DcmSequenceOfItems* sequence = NULL; | |
179 if (!source.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_StructureSetROISequence, sequence).good() || | |
180 sequence == NULL) | |
181 { | |
182 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
183 } | |
184 | |
185 for (unsigned long i = 0; i < sequence->card(); i++) | |
186 { | |
187 DcmItem* item = sequence->getItem(i); | |
188 if (item == NULL) | |
189 { | |
190 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
191 } | |
192 else | |
193 { | |
194 target.insert(GetStringValue(*item, DCM_ROIName)); | |
195 } | |
196 } | |
197 } | |
198 | |
199 | |
200 class StructurePolygon : public boost::noncopyable | |
201 { | |
202 private: | |
203 std::string roiName_; | |
204 std::string referencedSopInstanceUid_; | |
205 uint8_t red_; | |
206 uint8_t green_; | |
207 uint8_t blue_; | |
208 std::vector<Vector3D> points_; | |
209 | |
210 public: | |
211 StructurePolygon(Orthanc::ParsedDicomFile& dicom, | |
212 unsigned long roiIndex, | |
213 unsigned long contourIndex) | |
214 { | |
215 DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); | |
216 | |
217 DcmItem* structure = NULL; | |
218 DcmItem* roi = NULL; | |
219 DcmItem* contour = NULL; | |
220 DcmSequenceOfItems* referenced = NULL; | |
221 | |
222 if (!dataset.findAndGetSequenceItem(DCM_StructureSetROISequence, structure, roiIndex).good() || | |
223 structure == NULL || | |
224 !dataset.findAndGetSequenceItem(DCM_ROIContourSequence, roi, roiIndex).good() || | |
225 roi == NULL || | |
226 !roi->findAndGetSequenceItem(DCM_ContourSequence, contour, contourIndex).good() || | |
227 contour == NULL || | |
228 !contour->findAndGetSequence(DCM_ContourImageSequence, referenced).good() || | |
229 referenced == NULL || | |
230 referenced->card() != 1) | |
231 { | |
232 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
233 } | |
234 | |
235 roiName_ = GetStringValue(*structure, DCM_ROIName); | |
236 referencedSopInstanceUid_ = GetStringValue(*referenced->getItem(0), DCM_ReferencedSOPInstanceUID); | |
237 | |
238 if (GetStringValue(*contour, DCM_ContourGeometricType) != "CLOSED_PLANAR") | |
239 { | |
240 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
241 } | |
242 | |
243 { | |
244 std::vector<std::string> tokens; | |
245 Orthanc::Toolbox::TokenizeString(tokens, GetStringValue(*roi, DCM_ROIDisplayColor), '\\'); | |
246 | |
247 uint32_t r, g, b; | |
248 if (tokens.size() != 3 || | |
249 !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(r, tokens[0]) || | |
250 !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(g, tokens[1]) || | |
251 !Orthanc::SerializationToolbox::ParseFirstUnsignedInteger32(b, tokens[2]) || | |
252 r > 255 || | |
253 g > 255 || | |
254 b > 255) | |
255 { | |
256 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
257 } | |
258 | |
259 red_ = r; | |
260 green_ = g; | |
261 blue_ = b; | |
262 } | |
263 | |
264 { | |
265 std::vector<std::string> tokens; | |
266 Orthanc::Toolbox::TokenizeString(tokens, GetStringValue(*contour, DCM_ContourData), '\\'); | |
267 | |
268 const std::string s = GetStringValue(*contour, DCM_NumberOfContourPoints); | |
269 | |
270 uint32_t countPoints; | |
271 if (!Orthanc::SerializationToolbox::ParseUnsignedInteger32(countPoints, s) || | |
272 tokens.size() != 3 * countPoints) | |
273 { | |
274 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
275 } | |
276 | |
277 points_.reserve(countPoints); | |
278 | |
279 for (size_t i = 0; i < tokens.size(); i += 3) | |
280 { | |
281 double x, y, z; | |
282 if (!Toolbox::MyParseDouble(x, tokens[i]) || | |
283 !Toolbox::MyParseDouble(y, tokens[i + 1]) || | |
284 !Toolbox::MyParseDouble(z, tokens[i + 2])) | |
285 { | |
286 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
287 } | |
288 | |
289 points_.push_back(Vector3D(x, y, z)); | |
290 } | |
291 | |
292 assert(points_.size() == countPoints); | |
293 } | |
294 } | |
295 | |
296 const std::string& GetRoiName() const | |
297 { | |
298 return roiName_; | |
299 } | |
300 | |
301 const std::string& GetReferencedSopInstanceUid() const | |
302 { | |
303 return referencedSopInstanceUid_; | |
304 } | |
305 | |
306 size_t GetPointsCount() const | |
307 { | |
308 return points_.size(); | |
309 } | |
310 | |
311 const Vector3D& GetPoint(size_t i) const | |
312 { | |
313 if (i >= points_.size()) | |
314 { | |
315 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | |
316 } | |
317 else | |
318 { | |
319 return points_[i]; | |
320 } | |
321 } | |
322 | |
323 bool IsCoplanar(Vector3D& normal) const | |
324 { | |
325 if (points_.size() < 3) | |
326 { | |
327 return false; | |
328 } | |
329 | |
330 bool hasNormal = false; | |
331 | |
332 for (size_t i = 0; i < points_.size(); i++) | |
333 { | |
334 normal = Vector3D::CrossProduct(Vector3D(points_[1], points_[0]), | |
335 Vector3D(points_[2], points_[0])); | |
336 if (!Toolbox::IsNear(normal.ComputeNorm(), 0)) | |
337 { | |
338 normal.Normalize(); | |
339 hasNormal = true; | |
340 } | |
341 } | |
342 | |
343 if (!hasNormal) | |
344 { | |
345 return false; | |
346 } | |
347 | |
348 double a = Vector3D::DotProduct(points_[0], normal); | |
349 | |
350 for (size_t i = 1; i < points_.size(); i++) | |
351 { | |
352 double b = Vector3D::DotProduct(points_[i], normal); | |
353 if (!Toolbox::IsNear(a, b)) | |
354 { | |
355 return false; | |
356 } | |
357 } | |
358 | |
359 return true; | |
360 } | |
361 | |
362 void Add(Extent2D& extent, | |
363 const Vector3D& axisX, | |
364 const Vector3D& axisY) const | |
365 { | |
366 assert(Toolbox::IsNear(1, axisX.ComputeNorm())); | |
367 assert(Toolbox::IsNear(1, axisY.ComputeNorm())); | |
368 | |
369 for (size_t i = 0; i < points_.size(); i++) | |
370 { | |
371 extent.Add(Vector3D::DotProduct(axisX, points_[i]), | |
372 Vector3D::DotProduct(axisY, points_[i])); | |
373 } | |
374 } | |
375 }; | |
376 | |
377 | 156 |
378 | 157 |
379 class StructureSet : public boost::noncopyable | 158 class StructureSet : public boost::noncopyable |
380 { | 159 { |
381 private: | 160 private: |
390 public: | 169 public: |
391 explicit StructureSet(Orthanc::ParsedDicomFile& dicom) : | 170 explicit StructureSet(Orthanc::ParsedDicomFile& dicom) : |
392 hasFrameOfReferenceUid_(false) | 171 hasFrameOfReferenceUid_(false) |
393 { | 172 { |
394 DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); | 173 DcmDataset& dataset = *dicom.GetDcmtkObject().getDataset(); |
395 patientId_ = GetStringValue(dataset, DCM_PatientID); | 174 patientId_ = STLToolbox::GetStringValue(dataset, DCM_PatientID); |
396 studyInstanceUid_ = GetStringValue(dataset, DCM_StudyInstanceUID); | 175 studyInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_StudyInstanceUID); |
397 seriesInstanceUid_ = GetStringValue(dataset, DCM_SeriesInstanceUID); | 176 seriesInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_SeriesInstanceUID); |
398 sopInstanceUid_ = GetStringValue(dataset, DCM_SOPInstanceUID); | 177 sopInstanceUid_ = STLToolbox::GetStringValue(dataset, DCM_SOPInstanceUID); |
399 | 178 |
400 DcmSequenceOfItems* frame = NULL; | 179 DcmSequenceOfItems* frame = NULL; |
401 if (!dataset.findAndGetSequence(DCM_ReferencedFrameOfReferenceSequence, frame).good() || | 180 if (!dataset.findAndGetSequence(DCM_ReferencedFrameOfReferenceSequence, frame).good() || |
402 frame == NULL) | 181 frame == NULL) |
403 { | 182 { |
519 return frameOfReferenceUid_; | 298 return frameOfReferenceUid_; |
520 } | 299 } |
521 else | 300 else |
522 { | 301 { |
523 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); | 302 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); |
303 } | |
304 } | |
305 | |
306 // This static method is faster than constructing the full "StructureSet" object | |
307 static void ListStructuresNames(std::set<std::string>& target, | |
308 Orthanc::ParsedDicomFile& source) | |
309 { | |
310 target.clear(); | |
311 | |
312 DcmSequenceOfItems* sequence = NULL; | |
313 if (!source.GetDcmtkObject().getDataset()->findAndGetSequence(DCM_StructureSetROISequence, sequence).good() || | |
314 sequence == NULL) | |
315 { | |
316 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
317 } | |
318 | |
319 for (unsigned long i = 0; i < sequence->card(); i++) | |
320 { | |
321 DcmItem* item = sequence->getItem(i); | |
322 if (item == NULL) | |
323 { | |
324 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat); | |
325 } | |
326 else | |
327 { | |
328 target.insert(STLToolbox::GetStringValue(*item, DCM_ROIName)); | |
329 } | |
524 } | 330 } |
525 } | 331 } |
526 }; | 332 }; |
527 | 333 |
528 | 334 |
553 assert(slicesSpacing_ > 0 && | 359 assert(slicesSpacing_ > 0 && |
554 minProjectionAlongNormal_ < maxProjectionAlongNormal_); | 360 minProjectionAlongNormal_ < maxProjectionAlongNormal_); |
555 | 361 |
556 double d = (z - minProjectionAlongNormal_) / slicesSpacing_; | 362 double d = (z - minProjectionAlongNormal_) / slicesSpacing_; |
557 | 363 |
558 if (Toolbox::IsNear(d, round(d))) | 364 if (STLToolbox::IsNear(d, round(d))) |
559 { | 365 { |
560 if (d < 0.0 || | 366 if (d < 0.0 || |
561 d > static_cast<double>(slicesCount_) - 1.0) | 367 d > static_cast<double>(slicesCount_) - 1.0) |
562 { | 368 { |
563 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 369 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
621 } | 427 } |
622 | 428 |
623 // Only keep unique projections | 429 // Only keep unique projections |
624 | 430 |
625 std::sort(projections.begin(), projections.end()); | 431 std::sort(projections.begin(), projections.end()); |
626 Toolbox::RemoveDuplicateValues(projections); | 432 STLToolbox::RemoveDuplicateValues(projections); |
627 assert(!projections.empty()); | 433 assert(!projections.empty()); |
628 | 434 |
629 if (projections.size() == 1) | 435 if (projections.size() == 1) |
630 { | 436 { |
631 // Volume with one single slice | 437 // Volume with one single slice |
648 spacings[i] = projections[i + 1] - projections[i]; | 454 spacings[i] = projections[i + 1] - projections[i]; |
649 assert(spacings[i] > 0); | 455 assert(spacings[i] > 0); |
650 } | 456 } |
651 | 457 |
652 std::sort(spacings.begin(), spacings.end()); | 458 std::sort(spacings.begin(), spacings.end()); |
653 Toolbox::RemoveDuplicateValues(spacings); | 459 STLToolbox::RemoveDuplicateValues(spacings); |
654 | 460 |
655 if (spacings.empty()) | 461 if (spacings.empty()) |
656 { | 462 { |
657 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 463 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
658 } | 464 } |
685 it++; | 491 it++; |
686 | 492 |
687 while (it != candidates.end()) | 493 while (it != candidates.end()) |
688 { | 494 { |
689 double d = (projections[*it] - projections[reference]) / slicesSpacing_; | 495 double d = (projections[*it] - projections[reference]) / slicesSpacing_; |
690 if (Toolbox::IsNear(d, round(d))) | 496 if (STLToolbox::IsNear(d, round(d))) |
691 { | 497 { |
692 countSupport ++; | 498 countSupport ++; |
693 } | 499 } |
694 else | 500 else |
695 { | 501 { |
724 maxProjectionAlongNormal_ = bestProjection; | 530 maxProjectionAlongNormal_ = bestProjection; |
725 | 531 |
726 for (size_t i = 0; i < projections.size(); i++) | 532 for (size_t i = 0; i < projections.size(); i++) |
727 { | 533 { |
728 double d = (projections[i] - bestProjection) / slicesSpacing_; | 534 double d = (projections[i] - bestProjection) / slicesSpacing_; |
729 if (Toolbox::IsNear(d, round(d))) | 535 if (STLToolbox::IsNear(d, round(d))) |
730 { | 536 { |
731 minProjectionAlongNormal_ = std::min(minProjectionAlongNormal_, projections[i]); | 537 minProjectionAlongNormal_ = std::min(minProjectionAlongNormal_, projections[i]); |
732 maxProjectionAlongNormal_ = std::max(maxProjectionAlongNormal_, projections[i]); | 538 maxProjectionAlongNormal_ = std::max(maxProjectionAlongNormal_, projections[i]); |
733 } | 539 } |
734 } | 540 } |
735 | 541 |
736 double d = (maxProjectionAlongNormal_ - minProjectionAlongNormal_) / slicesSpacing_; | 542 double d = (maxProjectionAlongNormal_ - minProjectionAlongNormal_) / slicesSpacing_; |
737 if (Toolbox::IsNear(d, round(d))) | 543 if (STLToolbox::IsNear(d, round(d))) |
738 { | 544 { |
739 slicesCount_ = static_cast<size_t>(round(d)) + 1; | 545 slicesCount_ = static_cast<size_t>(round(d)) + 1; |
740 } | 546 } |
741 else | 547 else |
742 { | 548 { |
900 Orthanc::Toolbox::TokenizeString(items, imageOrientation, '\\'); | 706 Orthanc::Toolbox::TokenizeString(items, imageOrientation, '\\'); |
901 | 707 |
902 double x1, x2, x3, y1, y2, y3; | 708 double x1, x2, x3, y1, y2, y3; |
903 | 709 |
904 if (items.size() == 6 && | 710 if (items.size() == 6 && |
905 Toolbox::MyParseDouble(x1, items[0]) && | 711 STLToolbox::MyParseDouble(x1, items[0]) && |
906 Toolbox::MyParseDouble(x2, items[1]) && | 712 STLToolbox::MyParseDouble(x2, items[1]) && |
907 Toolbox::MyParseDouble(x3, items[2]) && | 713 STLToolbox::MyParseDouble(x3, items[2]) && |
908 Toolbox::MyParseDouble(y1, items[3]) && | 714 STLToolbox::MyParseDouble(y1, items[3]) && |
909 Toolbox::MyParseDouble(y2, items[4]) && | 715 STLToolbox::MyParseDouble(y2, items[4]) && |
910 Toolbox::MyParseDouble(y3, items[5])) | 716 STLToolbox::MyParseDouble(y3, items[5])) |
911 { | 717 { |
912 axisX = Vector3D(x1, x2, x3); | 718 axisX = Vector3D(x1, x2, x3); |
913 axisY = Vector3D(y1, y2, y3); | 719 axisY = Vector3D(y1, y2, y3); |
914 } | 720 } |
915 } | 721 } |
936 structureSet.GetPolygonsCount() < 1) | 742 structureSet.GetPolygonsCount() < 1) |
937 { | 743 { |
938 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); | 744 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); |
939 } | 745 } |
940 | 746 |
941 if (!Toolbox::IsNear(1, geometry.GetSlicesNormal().ComputeNorm())) | 747 if (!STLToolbox::IsNear(1, geometry.GetSlicesNormal().ComputeNorm())) |
942 { | 748 { |
943 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 749 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
944 } | 750 } |
945 | 751 |
946 Vector3D axisX, axisY; | 752 Vector3D axisX, axisY; |
947 GetReferencedVolumeAxes(axisX, axisY, structureSet); | 753 GetReferencedVolumeAxes(axisX, axisY, structureSet); |
948 | 754 |
949 Vector3D axisZ = Vector3D::CrossProduct(axisX, axisY); | 755 Vector3D axisZ = Vector3D::CrossProduct(axisX, axisY); |
950 | 756 |
951 if (!Toolbox::IsNear(1, axisX.ComputeNorm()) || | 757 if (!STLToolbox::IsNear(1, axisX.ComputeNorm()) || |
952 !Toolbox::IsNear(1, axisY.ComputeNorm()) || | 758 !STLToolbox::IsNear(1, axisY.ComputeNorm()) || |
953 !Vector3D::AreParallel(axisZ, geometry.GetSlicesNormal())) | 759 !Vector3D::AreParallel(axisZ, geometry.GetSlicesNormal())) |
954 { | 760 { |
955 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); | 761 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); |
956 } | 762 } |
957 | 763 |
1035 const std::string instanceId(request->groups[0]); | 841 const std::string instanceId(request->groups[0]); |
1036 | 842 |
1037 std::unique_ptr<Orthanc::ParsedDicomFile> dicom(LoadInstance(instanceId)); | 843 std::unique_ptr<Orthanc::ParsedDicomFile> dicom(LoadInstance(instanceId)); |
1038 | 844 |
1039 std::set<std::string> names; | 845 std::set<std::string> names; |
1040 ListStructuresNames(names, *dicom); | 846 StructureSet::ListStructuresNames(names, *dicom); |
1041 | 847 |
1042 Json::Value answer = Json::arrayValue; | 848 Json::Value answer = Json::arrayValue; |
1043 | 849 |
1044 for (std::set<std::string>::const_iterator it = names.begin(); it != names.end(); ++it) | 850 for (std::set<std::string>::const_iterator it = names.begin(); it != names.end(); ++it) |
1045 { | 851 { |
1258 | 1064 |
1259 std::unique_ptr<Orthanc::ParsedDicomFile> dicom(LoadInstance(instanceId)); | 1065 std::unique_ptr<Orthanc::ParsedDicomFile> dicom(LoadInstance(instanceId)); |
1260 DcmDataset& dataset = *dicom->GetDcmtkObject().getDataset(); | 1066 DcmDataset& dataset = *dicom->GetDcmtkObject().getDataset(); |
1261 | 1067 |
1262 std::string stl; | 1068 std::string stl; |
1263 if (GetStringValue(dataset, DCM_MIMETypeOfEncapsulatedDocument) != Orthanc::MIME_STL || | 1069 if (STLToolbox::GetStringValue(dataset, DCM_MIMETypeOfEncapsulatedDocument) != Orthanc::MIME_STL || |
1264 GetStringValue(dataset, DCM_SOPClassUID) != UID_EncapsulatedSTLStorage || | 1070 STLToolbox::GetStringValue(dataset, DCM_SOPClassUID) != UID_EncapsulatedSTLStorage || |
1265 !dicom->GetTagValue(stl, Orthanc::DICOM_TAG_ENCAPSULATED_DOCUMENT)) | 1071 !dicom->GetTagValue(stl, Orthanc::DICOM_TAG_ENCAPSULATED_DOCUMENT)) |
1266 { | 1072 { |
1267 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "DICOM instance not encapsulating a STL model: " + instanceId); | 1073 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "DICOM instance not encapsulating a STL model: " + instanceId); |
1268 } | 1074 } |
1269 else | 1075 else |