comparison OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp @ 4834:bec432ee1094

download of numpy arrays from the REST API
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 26 Nov 2021 19:03:32 +0100
parents 7053502fbf97
children ec1e9571b645
comparison
equal deleted inserted replaced
4833:970092a67897 4834:bec432ee1094
41 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h" 41 #include "../../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
42 #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h" 42 #include "../../../OrthancFramework/Sources/DicomParsing/Internals/DicomImageDecoder.h"
43 #include "../../../OrthancFramework/Sources/HttpServer/HttpContentNegociation.h" 43 #include "../../../OrthancFramework/Sources/HttpServer/HttpContentNegociation.h"
44 #include "../../../OrthancFramework/Sources/Images/Image.h" 44 #include "../../../OrthancFramework/Sources/Images/Image.h"
45 #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h" 45 #include "../../../OrthancFramework/Sources/Images/ImageProcessing.h"
46 #include "../../../OrthancFramework/Sources/Images/NumpyWriter.h"
46 #include "../../../OrthancFramework/Sources/Logging.h" 47 #include "../../../OrthancFramework/Sources/Logging.h"
47 #include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h" 48 #include "../../../OrthancFramework/Sources/MultiThreading/Semaphore.h"
48 #include "../../../OrthancFramework/Sources/SerializationToolbox.h" 49 #include "../../../OrthancFramework/Sources/SerializationToolbox.h"
49 50
50 #include "../OrthancConfiguration.h" 51 #include "../OrthancConfiguration.h"
1079 if (windowWidth <= 1.0f) 1080 if (windowWidth <= 1.0f)
1080 { 1081 {
1081 windowWidth = 1; 1082 windowWidth = 1;
1082 } 1083 }
1083 1084
1084 if (std::abs(rescaleSlope) <= 0.1) 1085 if (std::abs(rescaleSlope) <= 0.0001)
1085 { 1086 {
1086 rescaleSlope = 0.1; 1087 rescaleSlope = 0.0001;
1087 } 1088 }
1088 1089
1089 const double scaling = 255.0 * rescaleSlope / windowWidth; 1090 const double scaling = 255.0 * rescaleSlope / windowWidth;
1090 const double offset = (rescaleIntercept - windowCenter + windowWidth / 2.0) / rescaleSlope; 1091 const double offset = (rescaleIntercept - windowCenter + windowWidth / 2.0) / rescaleSlope;
1091 1092
1137 { 1138 {
1138 Semaphore::Locker locker(throttlingSemaphore_); 1139 Semaphore::Locker locker(throttlingSemaphore_);
1139 1140
1140 RenderedFrameHandler handler; 1141 RenderedFrameHandler handler;
1141 IDecodedFrameHandler::Apply(call, handler, ImageExtractionMode_Preview /* arbitrary value */, true); 1142 IDecodedFrameHandler::Apply(call, handler, ImageExtractionMode_Preview /* arbitrary value */, true);
1143 }
1144
1145
1146 static void DocumentSharedNumpy(RestApiGetCall& call)
1147 {
1148 call.GetDocumentation()
1149 .SetUriArgument("id", "Orthanc identifier of the DICOM resource of interest")
1150 .SetHttpGetArgument("compress", RestApiCallDocumentation::Type_Boolean, "Compress the file as `.npz`", false)
1151 .SetHttpGetArgument("rescale", RestApiCallDocumentation::Type_Boolean,
1152 "On grayscale images, apply the rescaling and return floating-point values", false)
1153 .AddAnswerType(MimeType_PlainText, "Numpy file: https://numpy.org/devdocs/reference/generated/numpy.lib.format.html");
1154 }
1155
1156
1157 namespace
1158 {
1159 class NumpyVisitor : public boost::noncopyable
1160 {
1161 private:
1162 bool rescale_;
1163 unsigned int depth_;
1164 unsigned int currentDepth_;
1165 unsigned int height_;
1166 unsigned int width_;
1167 PixelFormat format_;
1168 ChunkedBuffer buffer_;
1169
1170 public:
1171 NumpyVisitor(unsigned int depth /* can be zero if 2D frame */,
1172 bool rescale) :
1173 rescale_(rescale),
1174 depth_(depth),
1175 currentDepth_(0)
1176 {
1177 }
1178
1179 void WriteFrame(ParsedDicomFile& dicom,
1180 unsigned int frame)
1181 {
1182 std::unique_ptr<ImageAccessor> decoded(dicom.DecodeFrame(frame));
1183
1184 if (decoded.get() == NULL)
1185 {
1186 throw OrthancException(ErrorCode_NotImplemented, "Cannot decode DICOM instance");
1187 }
1188
1189 if (currentDepth_ == 0)
1190 {
1191 width_ = decoded->GetWidth();
1192 height_ = decoded->GetHeight();
1193 format_ = decoded->GetFormat();
1194 }
1195 else if (width_ != decoded->GetWidth() ||
1196 height_ != decoded->GetHeight())
1197 {
1198 throw OrthancException(ErrorCode_IncompatibleImageSize, "The size of the frames varies across the instance(s)");
1199 }
1200 else if (format_ != decoded->GetFormat())
1201 {
1202 throw OrthancException(ErrorCode_IncompatibleImageFormat, "The pixel format of the frames varies across the instance(s)");
1203 }
1204
1205 if (rescale_ &&
1206 decoded->GetFormat() != PixelFormat_RGB24)
1207 {
1208 if (currentDepth_ == 0)
1209 {
1210 NumpyWriter::WriteHeader(buffer_, depth_, width_, height_, PixelFormat_Float32);
1211 }
1212
1213 double rescaleIntercept, rescaleSlope;
1214 dicom.GetRescale(rescaleIntercept, rescaleSlope, frame);
1215
1216 Image converted(PixelFormat_Float32, decoded->GetWidth(), decoded->GetHeight(), false);
1217 ImageProcessing::Convert(converted, *decoded);
1218 ImageProcessing::ShiftScale2(converted, static_cast<float>(rescaleIntercept), static_cast<float>(rescaleSlope), false);
1219
1220 NumpyWriter::WritePixels(buffer_, converted);
1221 }
1222 else
1223 {
1224 if (currentDepth_ == 0)
1225 {
1226 NumpyWriter::WriteHeader(buffer_, depth_, width_, height_, format_);
1227 }
1228
1229 NumpyWriter::WritePixels(buffer_, *decoded);
1230 }
1231
1232 currentDepth_ ++;
1233 }
1234
1235 void Answer(RestApiOutput& output,
1236 bool compress)
1237 {
1238 if ((depth_ == 0 && currentDepth_ != 1) ||
1239 (depth_ != 0 && currentDepth_ != depth_))
1240 {
1241 throw OrthancException(ErrorCode_BadSequenceOfCalls);
1242 }
1243 else
1244 {
1245 std::string answer;
1246 NumpyWriter::Finalize(answer, buffer_, compress);
1247 output.AnswerBuffer(answer, MimeType_Binary);
1248 }
1249 }
1250 };
1251 }
1252
1253
1254 static void GetNumpyFrame(RestApiGetCall& call)
1255 {
1256 if (call.IsDocumentation())
1257 {
1258 DocumentSharedNumpy(call);
1259 call.GetDocumentation()
1260 .SetTag("Instances")
1261 .SetSummary("Decode frame for numpy")
1262 .SetDescription("Decode one frame of interest from the given DICOM instance, for use with numpy in Python")
1263 .SetUriArgument("frame", RestApiCallDocumentation::Type_Number, "Index of the frame (starts at `0`)");
1264 }
1265 else
1266 {
1267 const std::string instanceId = call.GetUriComponent("id", "");
1268 const bool compress = call.GetBooleanArgument("compress", false);
1269 const bool rescale = call.GetBooleanArgument("rescale", true);
1270
1271 uint32_t frame;
1272 if (!SerializationToolbox::ParseUnsignedInteger32(frame, call.GetUriComponent("frame", "0")))
1273 {
1274 throw OrthancException(ErrorCode_ParameterOutOfRange, "Expected an unsigned integer for the \"frame\" argument");
1275 }
1276
1277 NumpyVisitor visitor(0 /* no depth, 2D frame */, rescale);
1278
1279 {
1280 Semaphore::Locker throttling(throttlingSemaphore_);
1281 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId);
1282
1283 visitor.WriteFrame(locker.GetDicom(), frame);
1284 }
1285
1286 visitor.Answer(call.GetOutput(), compress);
1287 }
1288 }
1289
1290
1291 static void GetNumpyInstance(RestApiGetCall& call)
1292 {
1293 if (call.IsDocumentation())
1294 {
1295 DocumentSharedNumpy(call);
1296 call.GetDocumentation()
1297 .SetTag("Instances")
1298 .SetSummary("Decode instance for numpy")
1299 .SetDescription("Decode the given DICOM instance, for use with numpy in Python");
1300 }
1301 else
1302 {
1303 const std::string instanceId = call.GetUriComponent("id", "");
1304 const bool compress = call.GetBooleanArgument("compress", false);
1305 const bool rescale = call.GetBooleanArgument("rescale", true);
1306
1307 {
1308 Semaphore::Locker throttling(throttlingSemaphore_);
1309 ServerContext::DicomCacheLocker locker(OrthancRestApi::GetContext(call), instanceId);
1310
1311 const unsigned int depth = locker.GetDicom().GetFramesCount();
1312 if (depth == 0)
1313 {
1314 throw OrthancException(ErrorCode_BadFileFormat, "Empty DICOM instance");
1315 }
1316
1317 NumpyVisitor visitor(depth, rescale);
1318
1319 for (unsigned int frame = 0; frame < depth; frame++)
1320 {
1321 visitor.WriteFrame(locker.GetDicom(), frame);
1322 }
1323
1324 visitor.Answer(call.GetOutput(), compress);
1325 }
1326 }
1327 }
1328
1329
1330 static void GetNumpySeries(RestApiGetCall& call)
1331 {
1332 if (call.IsDocumentation())
1333 {
1334 DocumentSharedNumpy(call);
1335 call.GetDocumentation()
1336 .SetTag("Series")
1337 .SetSummary("Decode series for numpy")
1338 .SetDescription("Decode the given DICOM series, for use with numpy in Python");
1339 }
1340 else
1341 {
1342 const std::string seriesId = call.GetUriComponent("id", "");
1343 const bool compress = call.GetBooleanArgument("compress", false);
1344 const bool rescale = call.GetBooleanArgument("rescale", true);
1345
1346 Semaphore::Locker throttling(throttlingSemaphore_);
1347
1348 ServerIndex& index = OrthancRestApi::GetIndex(call);
1349 SliceOrdering ordering(index, seriesId);
1350
1351 unsigned int depth = 0;
1352 for (size_t i = 0; i < ordering.GetInstancesCount(); i++)
1353 {
1354 depth += ordering.GetFramesCount(i);
1355 }
1356
1357 ServerContext& context = OrthancRestApi::GetContext(call);
1358
1359 NumpyVisitor visitor(depth, rescale);
1360
1361 for (size_t i = 0; i < ordering.GetInstancesCount(); i++)
1362 {
1363 const std::string& instanceId = ordering.GetInstanceId(i);
1364 unsigned int framesCount = ordering.GetFramesCount(i);
1365
1366 {
1367 ServerContext::DicomCacheLocker locker(context, instanceId);
1368
1369 for (unsigned int frame = 0; frame < framesCount; frame++)
1370 {
1371 visitor.WriteFrame(locker.GetDicom(), frame);
1372 }
1373 }
1374 }
1375
1376 visitor.Answer(call.GetOutput(), compress);
1377 }
1142 } 1378 }
1143 1379
1144 1380
1145 static void GetMatlabImage(RestApiGetCall& call) 1381 static void GetMatlabImage(RestApiGetCall& call)
1146 { 1382 {
3394 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>); 3630 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
3395 Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>); 3631 Register("/instances/{id}/frames/{frame}/image-int16", GetImage<ImageExtractionMode_Int16>);
3396 Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage); 3632 Register("/instances/{id}/frames/{frame}/matlab", GetMatlabImage);
3397 Register("/instances/{id}/frames/{frame}/raw", GetRawFrame<false>); 3633 Register("/instances/{id}/frames/{frame}/raw", GetRawFrame<false>);
3398 Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>); 3634 Register("/instances/{id}/frames/{frame}/raw.gz", GetRawFrame<true>);
3635 Register("/instances/{id}/frames/{frame}/numpy", GetNumpyFrame); // New in Orthanc 1.9.8
3399 Register("/instances/{id}/pdf", ExtractPdf); 3636 Register("/instances/{id}/pdf", ExtractPdf);
3400 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>); 3637 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>);
3401 Register("/instances/{id}/rendered", GetRenderedFrame); 3638 Register("/instances/{id}/rendered", GetRenderedFrame);
3402 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>); 3639 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>);
3403 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); 3640 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>);
3404 Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>); 3641 Register("/instances/{id}/image-int16", GetImage<ImageExtractionMode_Int16>);
3405 Register("/instances/{id}/matlab", GetMatlabImage); 3642 Register("/instances/{id}/matlab", GetMatlabImage);
3406 Register("/instances/{id}/header", GetInstanceHeader); 3643 Register("/instances/{id}/header", GetInstanceHeader);
3644 Register("/instances/{id}/numpy", GetNumpyInstance); // New in Orthanc 1.9.8
3407 3645
3408 Register("/patients/{id}/protected", IsProtectedPatient); 3646 Register("/patients/{id}/protected", IsProtectedPatient);
3409 Register("/patients/{id}/protected", SetPatientProtection); 3647 Register("/patients/{id}/protected", SetPatientProtection);
3410 3648
3411 std::vector<std::string> resourceTypes; 3649 std::vector<std::string> resourceTypes;
3460 Register("/series/{id}/instances-tags", GetChildInstancesTags); 3698 Register("/series/{id}/instances-tags", GetChildInstancesTags);
3461 3699
3462 Register("/instances/{id}/content/*", GetRawContent); 3700 Register("/instances/{id}/content/*", GetRawContent);
3463 3701
3464 Register("/series/{id}/ordered-slices", OrderSlices); 3702 Register("/series/{id}/ordered-slices", OrderSlices);
3703 Register("/series/{id}/numpy", GetNumpySeries); // New in Orthanc 1.9.8
3465 3704
3466 Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>); 3705 Register("/patients/{id}/reconstruct", ReconstructResource<ResourceType_Patient>);
3467 Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>); 3706 Register("/studies/{id}/reconstruct", ReconstructResource<ResourceType_Study>);
3468 Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>); 3707 Register("/series/{id}/reconstruct", ReconstructResource<ResourceType_Series>);
3469 Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>); 3708 Register("/instances/{id}/reconstruct", ReconstructResource<ResourceType_Instance>);