comparison Sources/Plugin.cpp @ 7:e3e59de705f6

reorganization
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 18 Jul 2023 15:10:21 +0200
parents c02d12eb34d4
children d1267c6c33e1
comparison
equal deleted inserted replaced
6:c02d12eb34d4 7:e3e59de705f6
1014 buffer.Flatten(target); 1014 buffer.Flatten(target);
1015 } 1015 }
1016 1016
1017 1017
1018 bool EncodeStructureSetMesh(std::string& stl, 1018 bool EncodeStructureSetMesh(std::string& stl,
1019 const StructureSet& structureSet, 1019 vtkImageData* volume,
1020 const std::set<std::string>& roiNames,
1021 unsigned int resolution, 1020 unsigned int resolution,
1022 bool smooth) 1021 bool smooth)
1023 { 1022 {
1024 if (!structureSet.HasGeometry()) 1023 if (volume == NULL)
1025 { 1024 {
1026 return false; 1025 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
1027 }
1028
1029 if (resolution < 1)
1030 {
1031 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
1032 }
1033
1034 if (!IsNear(1, structureSet.GetSlicesNormal().ComputeNorm()))
1035 {
1036 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1037 }
1038
1039 // TODO - Axes could be retrieved from the referenced CT volume
1040 Vector3D axisX(1, 0, 0);
1041 Vector3D axisY = Vector3D::CrossProduct(structureSet.GetSlicesNormal(), axisX);
1042
1043 if (!IsNear(1, axisX.ComputeNorm()) ||
1044 !IsNear(1, axisY.ComputeNorm()))
1045 {
1046 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1047 }
1048
1049 Extent2D extent;
1050 for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++)
1051 {
1052 structureSet.GetPolygon(i).Add(extent, axisX, axisY);
1053 }
1054
1055 const int depth = structureSet.GetSlicesCount();
1056
1057 vtkNew<vtkImageData> volume;
1058 volume->SetDimensions(resolution, resolution, depth);
1059 volume->AllocateScalars(VTK_UNSIGNED_CHAR, 1);
1060
1061 assert(sizeof(unsigned char) == 1);
1062 memset(volume->GetScalarPointer(), 0, resolution * resolution * depth);
1063
1064 for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++)
1065 {
1066 const StructurePolygon& polygon = structureSet.GetPolygon(i);
1067 if (roiNames.find(polygon.GetRoiName()) == roiNames.end())
1068 {
1069 // This polygon doesn't correspond to a ROI of interest
1070 continue;
1071 }
1072
1073 size_t j;
1074 if (!structureSet.LookupSliceIndex(j, polygon))
1075 {
1076 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1077 }
1078
1079 std::vector<Orthanc::ImageProcessing::ImagePoint> points;
1080 points.reserve(polygon.GetPointsCount());
1081 for (size_t j = 0; j < polygon.GetPointsCount(); j++)
1082 {
1083 const Vector3D& point = polygon.GetPoint(j);
1084 double x = (Vector3D::DotProduct(point, axisX) - extent.GetMinX()) / extent.GetWidth() * static_cast<double>(resolution);
1085 double y = (Vector3D::DotProduct(point, axisY) - extent.GetMinY()) / extent.GetHeight() * static_cast<double>(resolution);
1086 points.push_back(Orthanc::ImageProcessing::ImagePoint(static_cast<int32_t>(std::floor(x)),
1087 static_cast<int32_t>(std::floor(y))));
1088 }
1089
1090 Orthanc::ImageAccessor slice;
1091 slice.AssignWritable(Orthanc::PixelFormat_Grayscale8, resolution, resolution, resolution /* pitch */,
1092 reinterpret_cast<uint8_t*>(volume->GetScalarPointer()) + j * resolution * resolution);
1093
1094 XorFiller filler(slice);
1095 Orthanc::ImageProcessing::FillPolygon(filler, points);
1096 } 1026 }
1097 1027
1098 vtkNew<vtkImageResize> resize; 1028 vtkNew<vtkImageResize> resize;
1099 resize->SetOutputDimensions(resolution, resolution, resolution); 1029 resize->SetOutputDimensions(resolution, resolution, resolution);
1100 resize->SetInputData(volume.Get()); 1030 resize->SetInputData(volume);
1101 resize->Update(); 1031 resize->Update();
1102
1103 resize->GetOutput()->SetSpacing(
1104 extent.GetWidth() / static_cast<double>(resolution),
1105 extent.GetHeight() / static_cast<double>(resolution),
1106 (structureSet.GetMaxProjectionAlongNormal() - structureSet.GetMinProjectionAlongNormal()) / static_cast<double>(resolution));
1107
1108 // TODO
1109 // resize->GetOutput()->SetOrigin()
1110 1032
1111 vtkNew<vtkImageConstantPad> padding; 1033 vtkNew<vtkImageConstantPad> padding;
1112 padding->SetConstant(0); 1034 padding->SetConstant(0);
1113 padding->SetOutputNumberOfScalarComponents(1); 1035 padding->SetOutputNumberOfScalarComponents(1);
1114 padding->SetOutputWholeExtent(-1, resolution, -1, resolution, -1, resolution); 1036 padding->SetOutputWholeExtent(-1, resolution, -1, resolution, -1, resolution);
1153 1075
1154 return true; 1076 return true;
1155 } 1077 }
1156 1078
1157 1079
1080 bool EncodeStructureSetMesh(std::string& stl,
1081 const StructureSet& structureSet,
1082 const std::set<std::string>& roiNames,
1083 unsigned int resolution,
1084 bool smooth)
1085 {
1086 if (!structureSet.HasGeometry())
1087 {
1088 return false;
1089 }
1090
1091 if (resolution < 1)
1092 {
1093 throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
1094 }
1095
1096 if (!IsNear(1, structureSet.GetSlicesNormal().ComputeNorm()))
1097 {
1098 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1099 }
1100
1101 // TODO - Axes could be retrieved from the referenced CT volume
1102 Vector3D axisX(1, 0, 0);
1103 Vector3D axisY = Vector3D::CrossProduct(structureSet.GetSlicesNormal(), axisX);
1104
1105 if (!IsNear(1, axisX.ComputeNorm()) ||
1106 !IsNear(1, axisY.ComputeNorm()))
1107 {
1108 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1109 }
1110
1111 Extent2D extent;
1112 for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++)
1113 {
1114 structureSet.GetPolygon(i).Add(extent, axisX, axisY);
1115 }
1116
1117 const int depth = structureSet.GetSlicesCount();
1118
1119 vtkNew<vtkImageData> volume;
1120 volume->SetDimensions(resolution, resolution, depth);
1121 volume->AllocateScalars(VTK_UNSIGNED_CHAR, 1);
1122
1123 assert(sizeof(unsigned char) == 1);
1124 memset(volume->GetScalarPointer(), 0, resolution * resolution * depth);
1125
1126 for (size_t i = 0; i < structureSet.GetPolygonsCount(); i++)
1127 {
1128 const StructurePolygon& polygon = structureSet.GetPolygon(i);
1129 if (roiNames.find(polygon.GetRoiName()) == roiNames.end())
1130 {
1131 // This polygon doesn't correspond to a ROI of interest
1132 continue;
1133 }
1134
1135 size_t j;
1136 if (!structureSet.LookupSliceIndex(j, polygon))
1137 {
1138 throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
1139 }
1140
1141 std::vector<Orthanc::ImageProcessing::ImagePoint> points;
1142 points.reserve(polygon.GetPointsCount());
1143 for (size_t j = 0; j < polygon.GetPointsCount(); j++)
1144 {
1145 const Vector3D& point = polygon.GetPoint(j);
1146 double x = (Vector3D::DotProduct(point, axisX) - extent.GetMinX()) / extent.GetWidth() * static_cast<double>(resolution);
1147 double y = (Vector3D::DotProduct(point, axisY) - extent.GetMinY()) / extent.GetHeight() * static_cast<double>(resolution);
1148 points.push_back(Orthanc::ImageProcessing::ImagePoint(static_cast<int32_t>(std::floor(x)),
1149 static_cast<int32_t>(std::floor(y))));
1150 }
1151
1152 Orthanc::ImageAccessor slice;
1153 slice.AssignWritable(Orthanc::PixelFormat_Grayscale8, resolution, resolution, resolution /* pitch */,
1154 reinterpret_cast<uint8_t*>(volume->GetScalarPointer()) + j * resolution * resolution);
1155
1156 XorFiller filler(slice);
1157 Orthanc::ImageProcessing::FillPolygon(filler, points);
1158 }
1159
1160 volume->SetSpacing(
1161 extent.GetWidth() / static_cast<double>(resolution),
1162 extent.GetHeight() / static_cast<double>(resolution),
1163 (structureSet.GetMaxProjectionAlongNormal() - structureSet.GetMinProjectionAlongNormal()) / static_cast<double>(depth));
1164
1165 // TODO
1166 // volume->SetOrigin()
1167
1168 return EncodeStructureSetMesh(stl, volume.Get(), resolution, smooth);
1169 }
1170
1171
1158 static Orthanc::ParsedDicomFile* LoadInstance(const std::string& instanceId) 1172 static Orthanc::ParsedDicomFile* LoadInstance(const std::string& instanceId)
1159 { 1173 {
1160 std::string dicom; 1174 std::string dicom;
1161 1175
1162 if (!OrthancPlugins::RestApiGetString(dicom, "/instances/" + instanceId + "/file", false)) 1176 if (!OrthancPlugins::RestApiGetString(dicom, "/instances/" + instanceId + "/file", false))
1216 { 1230 {
1217 AddDefaultTagValue(target, Orthanc::DicomTag(tag.getGroup(), tag.getElement()), value); 1231 AddDefaultTagValue(target, Orthanc::DicomTag(tag.getGroup(), tag.getElement()), value);
1218 } 1232 }
1219 1233
1220 1234
1221 void Encode(OrthancPluginRestOutput* output, 1235 static void CallCreateDicom(Json::Value& answer,
1222 const char* url, 1236 const std::string& stl,
1223 const OrthancPluginHttpRequest* request) 1237 const Json::Value& body,
1238 const std::string& parentStudy,
1239 const std::string& defaultSeriesDescription,
1240 const std::string& defaultFrameOfReferenceUid,
1241 const std::string& defaultTitle)
1242 {
1243 static const char* const KEY_TAGS = "Tags";
1244
1245 Json::Value normalized = Json::objectValue;
1246
1247 if (body.isMember(KEY_TAGS))
1248 {
1249 const Json::Value& tags = body[KEY_TAGS];
1250
1251 if (tags.type() != Json::objectValue)
1252 {
1253 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Tags must be provided as a JSON object");
1254 }
1255
1256 std::vector<std::string> keys = tags.getMemberNames();
1257 for (size_t i = 0; i < keys.size(); i++)
1258 {
1259 const Orthanc::DicomTag tag = Orthanc::FromDcmtkBridge::ParseTag(keys[i]);
1260 normalized[tag.Format()] = tags[keys[i]];
1261 }
1262 }
1263
1264 if (!normalized.isMember(Orthanc::DICOM_TAG_SERIES_DESCRIPTION.Format()))
1265 {
1266 normalized[Orthanc::DICOM_TAG_SERIES_DESCRIPTION.Format()] = defaultSeriesDescription;
1267 }
1268
1269 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_SERIES_NUMBER, "1");
1270 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_FRAME_OF_REFERENCE_UID, defaultFrameOfReferenceUid);
1271 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1");
1272
1273 AddDefaultTagValue(normalized, DCM_BurnedInAnnotation, "NO");
1274 AddDefaultTagValue(normalized, DCM_DeviceSerialNumber, ORTHANC_STL_VERSION);
1275 AddDefaultTagValue(normalized, DCM_DocumentTitle, defaultTitle);
1276 AddDefaultTagValue(normalized, DCM_Manufacturer, "Orthanc STL plugin");
1277 AddDefaultTagValue(normalized, DCM_ManufacturerModelName, "Orthanc STL plugin");
1278 AddDefaultTagValue(normalized, DCM_PositionReferenceIndicator, "");
1279 AddDefaultTagValue(normalized, DCM_SoftwareVersions, ORTHANC_STL_VERSION);
1280 AddDefaultTagValue(normalized, DCM_ConceptNameCodeSequence, "");
1281
1282 std::string date, time;
1283 Orthanc::SystemToolbox::GetNowDicom(date, time, true /* use UTC time (not local time) */);
1284 AddDefaultTagValue(normalized, DCM_AcquisitionDateTime, date + time);
1285
1286 const Orthanc::DicomTag MEASUREMENT_UNITS_CODE_SEQUENCE(DCM_MeasurementUnitsCodeSequence.getGroup(),
1287 DCM_MeasurementUnitsCodeSequence.getElement());
1288
1289 if (!normalized.isMember(MEASUREMENT_UNITS_CODE_SEQUENCE.Format()))
1290 {
1291 Json::Value item;
1292 item["CodeValue"] = "mm";
1293 item["CodingSchemeDesignator"] = "UCUM";
1294 item["CodeMeaning"] = defaultTitle;
1295
1296 normalized[MEASUREMENT_UNITS_CODE_SEQUENCE.Format()].append(item);
1297 }
1298
1299 std::string content;
1300 Orthanc::Toolbox::EncodeDataUriScheme(content, Orthanc::MIME_STL, stl);
1301
1302 Json::Value create;
1303 create["Content"] = content;
1304 create["Parent"] = parentStudy;
1305 create["Tags"] = normalized;
1306
1307 if (!OrthancPlugins::RestApiPost(answer, "/tools/create-dicom", create.toStyledString(), false))
1308 {
1309 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Cannot create DICOM from STL");
1310 }
1311 }
1312
1313
1314 void EncodeStructureSet(OrthancPluginRestOutput* output,
1315 const char* url,
1316 const OrthancPluginHttpRequest* request)
1224 { 1317 {
1225 static const char* const KEY_INSTANCE = "Instance"; 1318 static const char* const KEY_INSTANCE = "Instance";
1226 static const char* const KEY_RESOLUTION = "Resolution"; 1319 static const char* const KEY_RESOLUTION = "Resolution";
1227 static const char* const KEY_ROI_NAMES = "RoiNames"; 1320 static const char* const KEY_ROI_NAMES = "RoiNames";
1228 static const char* const KEY_SMOOTH = "Smooth"; 1321 static const char* const KEY_SMOOTH = "Smooth";
1229 static const char* const KEY_TAGS = "Tags";
1230 1322
1231 if (request->method != OrthancPluginHttpMethod_Post) 1323 if (request->method != OrthancPluginHttpMethod_Post)
1232 { 1324 {
1233 OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST"); 1325 OrthancPluginSendMethodNotAllowed(OrthancPlugins::GetGlobalContext(), output, "POST");
1234 return; 1326 return;
1260 { 1352 {
1261 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot encode STL"); 1353 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot encode STL");
1262 } 1354 }
1263 else 1355 else
1264 { 1356 {
1265 std::string content; 1357 std::string seriesDescription;
1266 Orthanc::Toolbox::EncodeDataUriScheme(content, "model/stl", stl); 1358
1267 1359 if (dicom->GetTagValue(seriesDescription, Orthanc::DICOM_TAG_SERIES_DESCRIPTION))
1268 Json::Value normalized = Json::objectValue; 1360 {
1269 1361 seriesDescription += ": ";
1270 if (body.isMember(KEY_TAGS)) 1362 }
1271 { 1363 else
1272 const Json::Value& tags = body[KEY_TAGS]; 1364 {
1273 if (tags.type() != Json::objectValue) 1365 seriesDescription.clear();
1274 { 1366 }
1275 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Tags must be provided as a JSON object"); 1367
1276 } 1368 bool first = true;
1277 1369 for (std::set<std::string>::const_iterator it = roiNames.begin(); it != roiNames.end(); ++it)
1278 std::vector<std::string> keys = tags.getMemberNames(); 1370 {
1279 for (size_t i = 0; i < keys.size(); i++) 1371 if (first)
1280 { 1372 {
1281 const Orthanc::DicomTag tag = Orthanc::FromDcmtkBridge::ParseTag(keys[i]); 1373 first = false;
1282 normalized[tag.Format()] = tags[keys[i]];
1283 }
1284 }
1285
1286 if (!normalized.isMember(Orthanc::DICOM_TAG_SERIES_DESCRIPTION.Format()))
1287 {
1288 std::string description;
1289
1290 if (dicom->GetTagValue(description, Orthanc::DICOM_TAG_SERIES_DESCRIPTION))
1291 {
1292 description += ": ";
1293 } 1374 }
1294 else 1375 else
1295 { 1376 {
1296 description.clear(); 1377 seriesDescription += ", ";
1297 } 1378 }
1298 1379
1299 bool first = true; 1380 seriesDescription += *it;
1300 for (std::set<std::string>::const_iterator it = roiNames.begin(); it != roiNames.end(); ++it) 1381 }
1301 { 1382
1302 if (first) 1383 std::string frameOfReferenceUid;
1303 {
1304 first = false;
1305 }
1306 else
1307 {
1308 description += ", ";
1309 }
1310
1311 description += *it;
1312 }
1313
1314 normalized[Orthanc::DICOM_TAG_SERIES_DESCRIPTION.Format()] = description;
1315 }
1316
1317 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_SERIES_NUMBER, "1");
1318
1319 std::string s;
1320 if (structureSet.HasFrameOfReferenceUid()) 1384 if (structureSet.HasFrameOfReferenceUid())
1321 { 1385 {
1322 s = structureSet.GetFrameOfReferenceUid(); 1386 frameOfReferenceUid = structureSet.GetFrameOfReferenceUid();
1323 } 1387 }
1324 else 1388 else
1325 { 1389 {
1326 s = Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Instance); 1390 frameOfReferenceUid = Orthanc::FromDcmtkBridge::GenerateUniqueIdentifier(Orthanc::ResourceType_Instance);
1327 } 1391 }
1328
1329 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_FRAME_OF_REFERENCE_UID, s);
1330 AddDefaultTagValue(normalized, Orthanc::DICOM_TAG_INSTANCE_NUMBER, "1");
1331
1332 const std::string title = "STL model generated from DICOM RT-STRUCT";
1333
1334 AddDefaultTagValue(normalized, DCM_BurnedInAnnotation, "NO");
1335 AddDefaultTagValue(normalized, DCM_DeviceSerialNumber, ORTHANC_STL_VERSION);
1336 AddDefaultTagValue(normalized, DCM_DocumentTitle, title);
1337 AddDefaultTagValue(normalized, DCM_Manufacturer, "Orthanc STL plugin");
1338 AddDefaultTagValue(normalized, DCM_ManufacturerModelName, "Orthanc STL plugin");
1339 AddDefaultTagValue(normalized, DCM_PositionReferenceIndicator, "");
1340 AddDefaultTagValue(normalized, DCM_SoftwareVersions, ORTHANC_STL_VERSION);
1341 AddDefaultTagValue(normalized, DCM_ConceptNameCodeSequence, "");
1342
1343 std::string date, time;
1344 Orthanc::SystemToolbox::GetNowDicom(date, time, true /* use UTC time (not local time) */);
1345 AddDefaultTagValue(normalized, DCM_AcquisitionDateTime, date + time);
1346
1347 const Orthanc::DicomTag MEASUREMENT_UNITS_CODE_SEQUENCE(DCM_MeasurementUnitsCodeSequence.getGroup(),
1348 DCM_MeasurementUnitsCodeSequence.getElement());
1349
1350 if (!normalized.isMember(MEASUREMENT_UNITS_CODE_SEQUENCE.Format()))
1351 {
1352 Json::Value item;
1353 item["CodeValue"] = "mm";
1354 item["CodingSchemeDesignator"] = "UCUM";
1355 item["CodeMeaning"] = title;
1356
1357 normalized[MEASUREMENT_UNITS_CODE_SEQUENCE.Format()].append(item);
1358 }
1359
1360 Json::Value create;
1361 create["Content"] = content;
1362 create["Parent"] = structureSet.HashStudy();
1363 create["Tags"] = normalized;
1364 1392
1365 Json::Value answer; 1393 Json::Value answer;
1366 if (OrthancPlugins::RestApiPost(answer, "/tools/create-dicom", create.toStyledString(), false)) 1394 CallCreateDicom(answer, stl, body, structureSet.HashStudy(), seriesDescription,
1367 { 1395 frameOfReferenceUid, "STL model generated from DICOM RT-STRUCT");
1368 std::string s = answer.toStyledString(); 1396
1369 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::MIME_JSON); 1397 std::string s = answer.toStyledString();
1370 } 1398 OrthancPluginAnswerBuffer(OrthancPlugins::GetGlobalContext(), output, s.c_str(), s.size(), Orthanc::MIME_JSON);
1371 else
1372 {
1373 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Cannot create DICOM from STL");
1374 }
1375 } 1399 }
1376 } 1400 }
1377 1401
1378 1402
1379 void ExtractStl(OrthancPluginRestOutput* output, 1403 void ExtractStl(OrthancPluginRestOutput* output,
1536 OrthancPlugins::RegisterRestCallback<ExtractStl>("/instances/([0-9a-f-]+)/stl", true); 1560 OrthancPlugins::RegisterRestCallback<ExtractStl>("/instances/([0-9a-f-]+)/stl", true);
1537 OrthancPlugins::RegisterRestCallback<ListStructures>("/stl/rt-struct/([0-9a-f-]+)", true); 1561 OrthancPlugins::RegisterRestCallback<ListStructures>("/stl/rt-struct/([0-9a-f-]+)", true);
1538 1562
1539 if (hasCreateDicomStl_) 1563 if (hasCreateDicomStl_)
1540 { 1564 {
1541 OrthancPlugins::RegisterRestCallback<Encode>("/stl/encode", true); 1565 OrthancPlugins::RegisterRestCallback<EncodeStructureSet>("/stl/encode", true);
1542 } 1566 }
1543 1567
1544 // Extend the default Orthanc Explorer with custom JavaScript for STL 1568 // Extend the default Orthanc Explorer with custom JavaScript for STL
1545 std::string explorer; 1569 std::string explorer;
1546 1570