# HG changeset patch # User Sebastien Jodogne # Date 1496071711 -7200 # Node ID 5945e81734a3297379776e143887e28fa85209c3 # Parent 961ee171d933ba1c4ab46fe07960d736938cd4bd decoding of JPEG images diff -r 961ee171d933 -r 5945e81734a3 Framework/Enumerations.h --- a/Framework/Enumerations.h Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Enumerations.h Mon May 29 17:28:31 2017 +0200 @@ -71,4 +71,12 @@ KeyboardModifiers_Control = (1 << 1), KeyboardModifiers_Alt = (1 << 2) }; + + enum SliceImageQuality + { + SliceImageQuality_Full, + SliceImageQuality_Jpeg50, + SliceImageQuality_Jpeg90, + SliceImageQuality_Jpeg95 + }; } diff -r 961ee171d933 -r 5945e81734a3 Framework/Layers/OrthancFrameLayerSource.cpp --- a/Framework/Layers/OrthancFrameLayerSource.cpp Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Layers/OrthancFrameLayerSource.cpp Mon May 29 17:28:31 2017 +0200 @@ -29,29 +29,18 @@ #include - -// TODO REMOVE THIS -#include "../Widgets/LayerWidget.h" - namespace OrthancStone { void OrthancFrameLayerSource::NotifyGeometryReady(const OrthancSlicesLoader& loader) { if (loader.GetSliceCount() > 0) { - // Make sure all the slices are parallel. TODO Alleviate this constraint - for (size_t i = 1; i < loader.GetSliceCount(); i++) - { - if (!GeometryToolbox::IsParallel(loader.GetSlice(i).GetGeometry().GetNormal(), - loader.GetSlice(0).GetGeometry().GetNormal())) - { - LayerSourceBase::NotifyGeometryError(); - return; - } - } + LayerSourceBase::NotifyGeometryReady(); } - - LayerSourceBase::NotifyGeometryReady(); + else + { + LayerSourceBase::NotifyGeometryError(); + } } void OrthancFrameLayerSource::NotifyGeometryError(const OrthancSlicesLoader& loader) @@ -62,14 +51,17 @@ void OrthancFrameLayerSource::NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image) + Orthanc::ImageAccessor* image, + SliceImageQuality quality) { - LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(image, slice, true), slice); + bool isFull = (quality == SliceImageQuality_Full); + LayerSourceBase::NotifyLayerReady(FrameRenderer::CreateRenderer(image, slice, isFull), slice); } void OrthancFrameLayerSource::NotifySliceImageError(const OrthancSlicesLoader& loader, unsigned int sliceIndex, - const Slice& slice) + const Slice& slice, + SliceImageQuality quality) { LayerSourceBase::NotifyLayerError(slice.GetGeometry()); } @@ -133,7 +125,8 @@ { if (loader_.LookupSlice(index, viewportSlice)) { - loader_.ScheduleLoadSliceImage(index); + //loader_.ScheduleLoadSliceImage(index, SliceImageQuality_Full); + loader_.ScheduleLoadSliceImage(index, SliceImageQuality_Jpeg50); } else { diff -r 961ee171d933 -r 5945e81734a3 Framework/Layers/OrthancFrameLayerSource.h --- a/Framework/Layers/OrthancFrameLayerSource.h Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Layers/OrthancFrameLayerSource.h Mon May 29 17:28:31 2017 +0200 @@ -43,11 +43,13 @@ virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image); + Orthanc::ImageAccessor* image, + SliceImageQuality quality); virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, unsigned int sliceIndex, - const Slice& slice); + const Slice& slice, + SliceImageQuality quality); public: OrthancFrameLayerSource(IWebService& orthanc, diff -r 961ee171d933 -r 5945e81734a3 Framework/Toolbox/OrthancSlicesLoader.cpp --- a/Framework/Toolbox/OrthancSlicesLoader.cpp Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Toolbox/OrthancSlicesLoader.cpp Mon May 29 17:28:31 2017 +0200 @@ -23,9 +23,13 @@ #include "MessagingToolbox.h" +#include "../../Resources/Orthanc/Core/Images/Image.h" +#include "../../Resources/Orthanc/Core/Images/ImageProcessing.h" +#include "../../Resources/Orthanc/Core/Images/JpegReader.h" #include "../../Resources/Orthanc/Core/Images/PngReader.h" #include "../../Resources/Orthanc/Core/Logging.h" #include "../../Resources/Orthanc/Core/OrthancException.h" +#include "../../Resources/Orthanc/Core/Toolbox.h" #include "../../Resources/Orthanc/Plugins/Samples/Common/DicomDatasetReader.h" #include "../../Resources/Orthanc/Plugins/Samples/Common/FullOrthancDataset.h" @@ -36,11 +40,12 @@ class OrthancSlicesLoader::Operation : public Orthanc::IDynamicObject { private: - Mode mode_; - unsigned int frame_; - unsigned int sliceIndex_; - const Slice* slice_; - std::string instanceId_; + Mode mode_; + unsigned int frame_; + unsigned int sliceIndex_; + const Slice* slice_; + std::string instanceId_; + SliceImageQuality quality_; Operation(Mode mode) : mode_(mode) @@ -53,6 +58,12 @@ return mode_; } + SliceImageQuality GetQuality() const + { + assert(mode_ == Mode_LoadImage); + return quality_; + } + unsigned int GetSliceIndex() const { assert(mode_ == Mode_LoadImage); @@ -92,11 +103,13 @@ } static Operation* DownloadSliceImage(unsigned int sliceIndex, - const Slice& slice) + const Slice& slice, + SliceImageQuality quality) { std::auto_ptr tmp(new Operation(Mode_LoadImage)); tmp->sliceIndex_ = sliceIndex; tmp->slice_ = &slice; + tmp->quality_ = quality; return tmp.release(); } }; @@ -132,7 +145,22 @@ break; case Mode_LoadImage: - that_.ParseSliceImage(*operation, answer, answerSize); + switch (operation->GetQuality()) + { + case SliceImageQuality_Full: + that_.ParseSliceImagePng(*operation, answer, answerSize); + break; + + case SliceImageQuality_Jpeg50: + case SliceImageQuality_Jpeg90: + case SliceImageQuality_Jpeg95: + that_.ParseSliceImageJpeg(*operation, answer, answerSize); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError); + } + break; default: @@ -155,7 +183,8 @@ case Mode_LoadImage: that_.userCallback_.NotifySliceImageError(that_, operation->GetSliceIndex(), - operation->GetSlice()); + operation->GetSlice(), + operation->GetQuality()); break; default: @@ -164,6 +193,29 @@ } }; + + + void OrthancSlicesLoader::NotifySliceImageSuccess(const Operation& operation, + Orthanc::ImageAccessor* image) const + { + if (image == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer); + } + else + { + userCallback_.NotifySliceImageReady + (*this, operation.GetSliceIndex(), operation.GetSlice(), image, operation.GetQuality()); + } + } + + + void OrthancSlicesLoader::NotifySliceImageError(const Operation& operation) const + { + userCallback_.NotifySliceImageError + (*this, operation.GetSliceIndex(), operation.GetSlice(), operation.GetQuality()); + } + void OrthancSlicesLoader::ParseSeriesGeometry(const void* answer, size_t size) @@ -256,18 +308,32 @@ } - void OrthancSlicesLoader::ParseSliceImage(const Operation& operation, - const void* answer, - size_t size) + void OrthancSlicesLoader::ParseSliceImagePng(const Operation& operation, + const void* answer, + size_t size) { std::auto_ptr image(new Orthanc::PngReader); - image->ReadFromMemory(answer, size); - bool ok = (image->GetWidth() == operation.GetSlice().GetWidth() || - image->GetHeight() == operation.GetSlice().GetHeight()); + bool ok = false; + + try + { + image->ReadFromMemory(answer, size); + } + catch (Orthanc::OrthancException&) + { + NotifySliceImageError(operation); + return; + } + + if (image->GetWidth() != operation.GetSlice().GetWidth() || + image->GetHeight() != operation.GetSlice().GetHeight()) + { + NotifySliceImageError(operation); + return; + } - if (ok && - operation.GetSlice().GetConverter().GetExpectedPixelFormat() == + if (operation.GetSlice().GetConverter().GetExpectedPixelFormat() == Orthanc::PixelFormat_SignedGrayscale16) { if (image->GetFormat() == Orthanc::PixelFormat_Grayscale16) @@ -276,20 +342,149 @@ } else { - ok = false; + NotifySliceImageError(operation); + return; + } + } + + NotifySliceImageSuccess(operation, image.release()); + } + + + void OrthancSlicesLoader::ParseSliceImageJpeg(const Operation& operation, + const void* answer, + size_t size) + { + Json::Value encoded; + if (!MessagingToolbox::ParseJson(encoded, answer, size) || + encoded.type() != Json::objectValue || + !encoded.isMember("Orthanc") || + encoded["Orthanc"].type() != Json::objectValue) + { + NotifySliceImageError(operation); + return; + } + + Json::Value& info = encoded["Orthanc"]; + if (!info.isMember("PixelData") || + !info.isMember("Stretched") || + !info.isMember("Compression") || + info["Compression"].type() != Json::stringValue || + info["PixelData"].type() != Json::stringValue || + info["Stretched"].type() != Json::booleanValue || + info["Compression"].asString() != "Jpeg") + { + NotifySliceImageError(operation); + return; + } + + bool isSigned = false; + bool isStretched = info["Stretched"].asBool(); + + if (info.isMember("IsSigned")) + { + if (info["IsSigned"].type() != Json::booleanValue) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol); + } + else + { + isSigned = info["IsSigned"].asBool(); } } - if (ok) + std::string jpeg; + Orthanc::Toolbox::DecodeBase64(jpeg, info["PixelData"].asString()); + + std::auto_ptr reader(new Orthanc::JpegReader); + try + { + reader->ReadFromMemory(jpeg); + } + catch (Orthanc::OrthancException&) + { + NotifySliceImageError(operation); + return; + } + + Orthanc::PixelFormat expectedFormat = + operation.GetSlice().GetConverter().GetExpectedPixelFormat(); + + if (reader->GetFormat() == Orthanc::PixelFormat_RGB24) // This is a color image { - userCallback_.NotifySliceImageReady(*this, operation.GetSliceIndex(), - operation.GetSlice(), image.release()); + if (expectedFormat != Orthanc::PixelFormat_RGB24) + { + NotifySliceImageError(operation); + return; + } + + if (isSigned || isStretched) + { + NotifySliceImageError(operation); + return; + } + else + { + NotifySliceImageSuccess(operation, reader.release()); + return; + } + } + + if (reader->GetFormat() != Orthanc::PixelFormat_Grayscale8) + { + NotifySliceImageError(operation); + return; } - else + + if (!isStretched) + { + if (expectedFormat != reader->GetFormat()) + { + NotifySliceImageError(operation); + return; + } + else + { + NotifySliceImageSuccess(operation, reader.release()); + return; + } + } + + int32_t stretchLow = 0; + int32_t stretchHigh = 0; + + if (!info.isMember("StretchLow") || + !info.isMember("StretchHigh") || + info["StretchLow"].type() != Json::intValue || + info["StretchHigh"].type() != Json::intValue) { - userCallback_.NotifySliceImageError(*this, operation.GetSliceIndex(), - operation.GetSlice()); + NotifySliceImageError(operation); + return; } + + stretchLow = info["StretchLow"].asInt(); + stretchHigh = info["StretchHigh"].asInt(); + + if (stretchLow < -32768 || + stretchHigh > 65535 || + (stretchLow < 0 && stretchHigh > 32767)) + { + // This range cannot be represented with a uint16_t or an int16_t + NotifySliceImageError(operation); + return; + } + + // Decode a grayscale JPEG 8bpp image coming from the Web viewer + std::auto_ptr image + (new Orthanc::Image(expectedFormat, reader->GetWidth(), reader->GetHeight(), false)); + + float scaling = static_cast(stretchHigh - stretchLow) / 255.0f; + float offset = static_cast(stretchLow) / scaling; + + Orthanc::ImageProcessing::Convert(*image, *reader); + Orthanc::ImageProcessing::ShiftScale(*image, offset, scaling); + + NotifySliceImageSuccess(operation, image.release()); } @@ -375,38 +570,86 @@ } - void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index) + void OrthancSlicesLoader::ScheduleSliceImagePng(size_t index) + { + const Slice& slice = GetSlice(index); + + std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + + boost::lexical_cast(slice.GetFrame())); + + switch (slice.GetConverter().GetExpectedPixelFormat()) + { + case Orthanc::PixelFormat_RGB24: + uri += "/preview"; + break; + + case Orthanc::PixelFormat_Grayscale16: + uri += "/image-uint16"; + break; + + case Orthanc::PixelFormat_SignedGrayscale16: + uri += "/image-int16"; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + orthanc_.ScheduleGetRequest(*webCallback_, uri, + Operation::DownloadSliceImage(index, slice, SliceImageQuality_Full)); + } + + + void OrthancSlicesLoader::ScheduleSliceImageJpeg(size_t index, + SliceImageQuality quality) + { + unsigned int value; + + switch (quality) + { + case SliceImageQuality_Jpeg50: + value = 50; + break; + + case SliceImageQuality_Jpeg90: + value = 90; + break; + + case SliceImageQuality_Jpeg95: + value = 95; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + // This requires the official Web viewer plugin to be installed! + const Slice& slice = GetSlice(index); + std::string uri = ("web-viewer/instances/jpeg" + + boost::lexical_cast(value) + + "-" + slice.GetOrthancInstanceId() + "_" + + boost::lexical_cast(slice.GetFrame())); + + orthanc_.ScheduleGetRequest(*webCallback_, uri, + Operation::DownloadSliceImage(index, slice, quality)); + } + + + void OrthancSlicesLoader::ScheduleLoadSliceImage(size_t index, + SliceImageQuality quality) { if (state_ != State_GeometryReady) { throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); } + + if (quality == SliceImageQuality_Full) + { + ScheduleSliceImagePng(index); + } else { - const Slice& slice = GetSlice(index); - - std::string uri = ("/instances/" + slice.GetOrthancInstanceId() + "/frames/" + - boost::lexical_cast(slice.GetFrame())); - - switch (slice.GetConverter().GetExpectedPixelFormat()) - { - case Orthanc::PixelFormat_RGB24: - uri += "/preview"; - break; - - case Orthanc::PixelFormat_Grayscale16: - uri += "/image-uint16"; - break; - - case Orthanc::PixelFormat_SignedGrayscale16: - uri += "/image-int16"; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); - } - - orthanc_.ScheduleGetRequest(*webCallback_, uri, Operation::DownloadSliceImage(index, slice)); + ScheduleSliceImageJpeg(index, quality); } } } diff -r 961ee171d933 -r 5945e81734a3 Framework/Toolbox/OrthancSlicesLoader.h --- a/Framework/Toolbox/OrthancSlicesLoader.h Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Toolbox/OrthancSlicesLoader.h Mon May 29 17:28:31 2017 +0200 @@ -23,6 +23,7 @@ #include "IWebService.h" #include "SlicesSorter.h" +#include "../Enumerations.h" #include @@ -45,11 +46,13 @@ virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image) = 0; + Orthanc::ImageAccessor* image, + SliceImageQuality quality) = 0; virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, unsigned int sliceIndex, - const Slice& slice) = 0; + const Slice& slice, + SliceImageQuality quality) = 0; }; private: @@ -78,7 +81,11 @@ State state_; SlicesSorter slices_; - + void NotifySliceImageSuccess(const Operation& operation, + Orthanc::ImageAccessor* image) const; + + void NotifySliceImageError(const Operation& operation) const; + void ParseSeriesGeometry(const void* answer, size_t size); @@ -87,10 +94,18 @@ const void* answer, size_t size); - void ParseSliceImage(const Operation& operation, - const void* answer, - size_t size); + void ParseSliceImagePng(const Operation& operation, + const void* answer, + size_t size); + + void ParseSliceImageJpeg(const Operation& operation, + const void* answer, + size_t size); + + void ScheduleSliceImagePng(size_t index); + void ScheduleSliceImageJpeg(size_t index, + SliceImageQuality quality); public: OrthancSlicesLoader(ICallback& callback, @@ -110,6 +125,7 @@ bool LookupSlice(size_t& index, const SliceGeometry& plane) const; - void ScheduleLoadSliceImage(size_t index); + void ScheduleLoadSliceImage(size_t index, + SliceImageQuality quality); }; } diff -r 961ee171d933 -r 5945e81734a3 Framework/Widgets/LayerWidget.cpp --- a/Framework/Widgets/LayerWidget.cpp Mon May 29 11:42:53 2017 +0200 +++ b/Framework/Widgets/LayerWidget.cpp Mon May 29 17:28:31 2017 +0200 @@ -89,6 +89,11 @@ return slice_; } + bool HasRenderer(size_t index) + { + return renderers_[index] != NULL; + } + bool IsComplete() const { return countMissing_ == 0; @@ -208,7 +213,8 @@ assert(layers_[i] != NULL); if (GetAndFixExtent(ax, ay, bx, by, *layers_[i])) { - LOG(INFO) << "Extent of layer " << i << ": (" << ax << "," << ay << ")->(" << bx << "," << by << ")"; + LOG(INFO) << "Extent of layer " << i << ": (" << ax << "," << ay + << ")->(" << bx << "," << by << ")"; if (first) { @@ -379,6 +385,10 @@ void LayerWidget::SetSlice(const SliceGeometry& slice) { + LOG(INFO) << "Setting slice origin: (" << slice.GetOrigin()[0] + << "," << slice.GetOrigin()[1] + << "," << slice.GetOrigin()[2] << ")"; + Slice displayedSlice(slice_, THIN_SLICE_THICKNESS); if (!displayedSlice.ContainsPlane(slice)) @@ -490,6 +500,7 @@ double x1, y1, x2, y2; if (GetAndFixExtent(x1, y1, x2, y2, *layers_[index])) { + printf("**%d** %f %f %f %f\n", index, x1, y1, x2, y2); UpdateLayer(index, new MissingLayerRenderer(x1, y1, x2, y2), Slice(slice, THIN_SLICE_THICKNESS)); } } diff -r 961ee171d933 -r 5945e81734a3 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Mon May 29 11:42:53 2017 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Mon May 29 17:28:31 2017 +0200 @@ -48,7 +48,7 @@ for (size_t i = 0; i < loader.GetSliceCount(); i++) { - const_cast(loader).ScheduleLoadSliceImage(i); + const_cast(loader).ScheduleLoadSliceImage(i, SliceImageQuality_Full); } } @@ -60,7 +60,8 @@ virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image) + Orthanc::ImageAccessor* image, + SliceImageQuality quality) { std::auto_ptr tmp(image); printf("Slice OK %dx%d\n", tmp->GetWidth(), tmp->GetHeight()); @@ -68,14 +69,15 @@ virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, unsigned int sliceIndex, - const Slice& slice) + const Slice& slice, + SliceImageQuality quality) { printf("ERROR 2\n"); } }; - class OrthancVolumeImageLoader : + class OrthancVolumeImage : public SlicedVolumeBase, private OrthancSlicesLoader::ICallback { @@ -92,7 +94,7 @@ unsigned int slice; if (downloadStack_->Pop(slice)) { - loader_.ScheduleLoadSliceImage(slice); + loader_.ScheduleLoadSliceImage(slice, SliceImageQuality_Full); } } @@ -172,7 +174,11 @@ for (size_t i = 1; i < loader.GetSliceCount(); i++) { - if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)))) + printf("%d %s %f\n", i, loader.GetSlice(i).GetOrthancInstanceId().c_str(), + GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i))); + + if (!GeometryToolbox::IsNear(spacingZ, GetDistance(loader.GetSlice(i - 1), loader.GetSlice(i)), + 0.001 /* this is expressed in mm */)) { LOG(ERROR) << "The distance between successive slices is not constant in a volume image"; SlicedVolumeBase::NotifyGeometryError(); @@ -210,7 +216,8 @@ virtual void NotifySliceImageReady(const OrthancSlicesLoader& loader, unsigned int sliceIndex, const Slice& slice, - Orthanc::ImageAccessor* image) + Orthanc::ImageAccessor* image, + SliceImageQuality quality) { std::auto_ptr protection(image); @@ -226,14 +233,15 @@ virtual void NotifySliceImageError(const OrthancSlicesLoader& loader, unsigned int sliceIndex, - const Slice& slice) + const Slice& slice, + SliceImageQuality quality) { LOG(ERROR) << "Cannot download slice " << sliceIndex << " in a volume image"; ScheduleSliceDownload(); } public: - OrthancVolumeImageLoader(IWebService& orthanc) : + OrthancVolumeImage(IWebService& orthanc) : loader_(*this, orthanc) { } @@ -258,6 +266,18 @@ { return loader_.GetSlice(index); } + + ImageBuffer3D& GetImage() + { + if (image_.get() == NULL) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls); + } + else + { + return *image_; + } + } }; } @@ -296,11 +316,13 @@ Orthanc::WebServiceParameters web; OrthancStone::OracleWebService orthanc(oracle, web); - OrthancStone::OrthancVolumeImageLoader volume(orthanc); + OrthancStone::OrthancVolumeImage volume(orthanc); - volume.ScheduleLoadInstance("19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5", 0); + //volume.ScheduleLoadInstance("19816330-cb02e1cf-df3a8fe8-bf510623-ccefe9f5", 0); //volume.ScheduleLoadSeries("318603c5-03e8cffc-a82b6ee1-3ccd3c1e-18d7e3bb"); // COMUNIX PET + //volume.ScheduleLoadSeries("7124dba7-09803f33-98b73826-33f14632-ea842d29"); // COMUNIX CT //volume.ScheduleLoadSeries("5990e39c-51e5f201-fe87a54c-31a55943-e59ef80e"); // Delphine sagital + volume.ScheduleLoadSeries("6f1b492a-e181e200-44e51840-ef8db55e-af529ab6"); // Delphine ax 2.5 boost::this_thread::sleep(boost::posix_time::milliseconds(1000));