Mercurial > hg > orthanc
changeset 800:ecedd89055db
generation of DICOM images from PNG files
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 06 May 2014 16:33:40 +0200 |
parents | 777b6b694da6 |
children | 262feb14f92f |
files | CMakeLists.txt Core/DicomFormat/DicomIntegerPixelAccessor.cpp Core/DicomFormat/DicomTag.h Core/Enumerations.cpp Core/Enumerations.h Core/ImageFormats/ImageAccessor.cpp Core/ImageFormats/ImageAccessor.h Core/ImageFormats/PngReader.cpp Core/ImageFormats/PngWriter.cpp Core/Toolbox.cpp Core/Toolbox.h OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/ParsedDicomFile.cpp OrthancServer/ParsedDicomFile.h UnitTestsSources/FromDcmtk.cpp |
diffstat | 15 files changed, 339 insertions(+), 27 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Tue May 06 12:55:41 2014 +0200 +++ b/CMakeLists.txt Tue May 06 16:33:40 2014 +0200 @@ -173,6 +173,7 @@ Core/MultiThreading/SharedMessageQueue.cpp Core/MultiThreading/ThreadedCommandProcessor.cpp Core/ImageFormats/ImageAccessor.cpp + Core/ImageFormats/ImageBuffer.cpp Core/ImageFormats/PngReader.cpp Core/ImageFormats/PngWriter.cpp Core/SQLite/Connection.cpp @@ -333,6 +334,7 @@ ${ORTHANC_ROOT}/Core/MultiThreading/ThreadedCommandProcessor.cpp ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp ${ORTHANC_ROOT}/Core/ImageFormats/ImageAccessor.cpp + ${ORTHANC_ROOT}/Core/ImageFormats/ImageBuffer.cpp ${ORTHANC_ROOT}/Core/ImageFormats/PngReader.cpp ${ORTHANC_ROOT}/OrthancCppClient/OrthancConnection.cpp ${ORTHANC_ROOT}/OrthancCppClient/Series.cpp
--- a/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/DicomFormat/DicomIntegerPixelAccessor.cpp Tue May 06 16:33:40 2014 +0200 @@ -44,15 +44,6 @@ namespace Orthanc { - static const DicomTag COLUMNS(0x0028, 0x0011); - static const DicomTag ROWS(0x0028, 0x0010); - static const DicomTag SAMPLES_PER_PIXEL(0x0028, 0x0002); - static const DicomTag BITS_ALLOCATED(0x0028, 0x0100); - static const DicomTag BITS_STORED(0x0028, 0x0101); - static const DicomTag HIGH_BIT(0x0028, 0x0102); - static const DicomTag PIXEL_REPRESENTATION(0x0028, 0x0103); - static const DicomTag PLANAR_CONFIGURATION(0x0028, 0x0006); - DicomIntegerPixelAccessor::DicomIntegerPixelAccessor(const DicomMap& values, const void* pixelData, size_t size) : @@ -67,19 +58,19 @@ try { - width_ = boost::lexical_cast<unsigned int>(values.GetValue(COLUMNS).AsString()); - height_ = boost::lexical_cast<unsigned int>(values.GetValue(ROWS).AsString()); - samplesPerPixel_ = boost::lexical_cast<unsigned int>(values.GetValue(SAMPLES_PER_PIXEL).AsString()); - bitsAllocated = boost::lexical_cast<unsigned int>(values.GetValue(BITS_ALLOCATED).AsString()); - bitsStored = boost::lexical_cast<unsigned int>(values.GetValue(BITS_STORED).AsString()); - highBit = boost::lexical_cast<unsigned int>(values.GetValue(HIGH_BIT).AsString()); - pixelRepresentation = boost::lexical_cast<unsigned int>(values.GetValue(PIXEL_REPRESENTATION).AsString()); + width_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_COLUMNS).AsString()); + height_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_ROWS).AsString()); + samplesPerPixel_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_SAMPLES_PER_PIXEL).AsString()); + bitsAllocated = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_ALLOCATED).AsString()); + bitsStored = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_BITS_STORED).AsString()); + highBit = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_HIGH_BIT).AsString()); + pixelRepresentation = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_PIXEL_REPRESENTATION).AsString()); if (samplesPerPixel_ > 1) { // The "Planar Configuration" is only set when "Samples per Pixels" is greater than 1 // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.3/ - planarConfiguration_ = boost::lexical_cast<unsigned int>(values.GetValue(PLANAR_CONFIGURATION).AsString()); + planarConfiguration_ = boost::lexical_cast<unsigned int>(values.GetValue(DICOM_TAG_PLANAR_CONFIGURATION).AsString()); } } catch (boost::bad_lexical_cast)
--- a/Core/DicomFormat/DicomTag.h Tue May 06 12:55:41 2014 +0200 +++ b/Core/DicomFormat/DicomTag.h Tue May 06 16:33:40 2014 +0200 @@ -115,4 +115,15 @@ static const DicomTag DICOM_TAG_SPECIFIC_CHARACTER_SET(0x0008, 0x0005); static const DicomTag DICOM_TAG_QUERY_RETRIEVE_LEVEL(0x0008, 0x0052); static const DicomTag DICOM_TAG_MODALITIES_IN_STUDY(0x0008, 0x0061); + + // Tags for images + static const DicomTag DICOM_TAG_COLUMNS(0x0028, 0x0011); + static const DicomTag DICOM_TAG_ROWS(0x0028, 0x0010); + static const DicomTag DICOM_TAG_SAMPLES_PER_PIXEL(0x0028, 0x0002); + static const DicomTag DICOM_TAG_BITS_ALLOCATED(0x0028, 0x0100); + static const DicomTag DICOM_TAG_BITS_STORED(0x0028, 0x0101); + static const DicomTag DICOM_TAG_HIGH_BIT(0x0028, 0x0102); + static const DicomTag DICOM_TAG_PIXEL_REPRESENTATION(0x0028, 0x0103); + static const DicomTag DICOM_TAG_PLANAR_CONFIGURATION(0x0028, 0x0006); + static const DicomTag DICOM_TAG_PHOTOMETRIC_INTERPRETATION(0x0028, 0x0004); }
--- a/Core/Enumerations.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/Enumerations.cpp Tue May 06 16:33:40 2014 +0200 @@ -247,6 +247,19 @@ } + const char* EnumerationToString(ImageFormat format) + { + switch (format) + { + case ImageFormat_Png: + return "Png"; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + } + + ResourceType StringToResourceType(const char* type) { std::string s(type); @@ -269,9 +282,44 @@ { return ResourceType_Instance; } - else + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + ImageFormat StringToImageFormat(const char* format) + { + std::string s(format); + Toolbox::ToUpperCase(s); + + if (s == "PNG") { - throw OrthancException(ErrorCode_ParameterOutOfRange); + return ImageFormat_Png; + } + + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + + unsigned int GetBytesPerPixel(PixelFormat format) + { + switch (format) + { + case PixelFormat_Grayscale8: + return 1; + + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + return 2; + + case PixelFormat_RGB24: + return 3; + + case PixelFormat_RGBA32: + return 4; + + default: + throw OrthancException(ErrorCode_ParameterOutOfRange); } } }
--- a/Core/Enumerations.h Tue May 06 12:55:41 2014 +0200 +++ b/Core/Enumerations.h Tue May 06 16:33:40 2014 +0200 @@ -85,6 +85,13 @@ PixelFormat_RGB24, /** + * {summary}{Color image in RGBA32 format.} + * {description}{This format describes a color image. The pixels are stored in 4 + * consecutive bytes. The memory layout is RGBA. + **/ + PixelFormat_RGBA32, + + /** * {summary}{Graylevel 8bpp image.} * {description}{The image is graylevel. Each pixel is unsigned and stored in one byte.} **/ @@ -213,6 +220,12 @@ }; + enum ImageFormat + { + ImageFormat_Png = 1 + }; + + /** * WARNING: Do not change the explicit values in the enumerations * below this point. This would result in incompatible databases @@ -250,5 +263,11 @@ const char* EnumerationToString(ResourceType type); + const char* EnumerationToString(ImageFormat format); + ResourceType StringToResourceType(const char* type); + + ImageFormat StringToImageFormat(const char* format); + + unsigned int GetBytesPerPixel(PixelFormat format); }
--- a/Core/ImageFormats/ImageAccessor.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/ImageFormats/ImageAccessor.cpp Tue May 06 16:33:40 2014 +0200 @@ -38,7 +38,7 @@ namespace Orthanc { - void* ImageAccessor::GetBuffer() + void* ImageAccessor::GetBuffer() const { if (readOnly_) { @@ -62,7 +62,7 @@ } - void* ImageAccessor::GetRow(unsigned int y) + void* ImageAccessor::GetRow(unsigned int y) const { if (readOnly_) {
--- a/Core/ImageFormats/ImageAccessor.h Tue May 06 12:55:41 2014 +0200 +++ b/Core/ImageFormats/ImageAccessor.h Tue May 06 16:33:40 2014 +0200 @@ -82,11 +82,11 @@ return buffer_; } - void* GetBuffer(); + void* GetBuffer() const; const void* GetConstRow(unsigned int y) const; - void* GetRow(unsigned int y); + void* GetRow(unsigned int y) const; void AssignEmpty(PixelFormat format);
--- a/Core/ImageFormats/PngReader.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/ImageFormats/PngReader.cpp Tue May 06 16:33:40 2014 +0200 @@ -171,6 +171,11 @@ format = PixelFormat_RGB24; pitch = 3 * width; } + else if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 8) + { + format = PixelFormat_RGBA32; + pitch = 4 * width; + } else { throw OrthancException(ErrorCode_NotImplemented);
--- a/Core/ImageFormats/PngWriter.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/ImageFormats/PngWriter.cpp Tue May 06 16:33:40 2014 +0200 @@ -140,6 +140,11 @@ pimpl_->colorType_ = PNG_COLOR_TYPE_RGB; break; + case PixelFormat_RGBA32: + pimpl_->bitDepth_ = 8; + pimpl_->colorType_ = PNG_COLOR_TYPE_RGBA; + break; + case PixelFormat_Grayscale8: pimpl_->bitDepth_ = 8; pimpl_->colorType_ = PNG_COLOR_TYPE_GRAY;
--- a/Core/Toolbox.cpp Tue May 06 12:55:41 2014 +0200 +++ b/Core/Toolbox.cpp Tue May 06 16:33:40 2014 +0200 @@ -803,5 +803,26 @@ result.push_back(currentItem); } + + + void Toolbox::DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source) + { + boost::regex pattern("data:([^;]+);base64,([a-zA-Z0-9=+/]*)", + boost::regex::icase /* case insensitive search */); + + boost::cmatch what; + if (regex_match(source.c_str(), what, pattern)) + { + mime = what[1]; + content = what[2]; + } + else + { + throw OrthancException(ErrorCode_BadFileFormat); + } + } + }
--- a/Core/Toolbox.h Tue May 06 12:55:41 2014 +0200 +++ b/Core/Toolbox.h Tue May 06 16:33:40 2014 +0200 @@ -122,5 +122,9 @@ void TokenizeString(std::vector<std::string>& result, const std::string& source, char separator); + + void DecodeDataUriScheme(std::string& mime, + std::string& content, + const std::string& source); } }
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue May 06 12:55:41 2014 +0200 +++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp Tue May 06 16:33:40 2014 +0200 @@ -96,10 +96,7 @@ std::string value = replacements[name].asString(); DicomTag tag = FromDcmtkBridge::ParseTag(name); - if (tag != DICOM_TAG_PIXEL_DATA) - { - target.Replace(tag, value); - } + target.Replace(tag, value); VLOG(1) << "Replace: " << name << " " << tag << " == " << value << std::endl; } @@ -406,6 +403,7 @@ static void Create(RestApi::PostCall& call) { // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World"}' + // curl http://localhost:8042/tools/create-dicom -X POST -d '{"PatientName":"Hello^World","PixelData":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="}' Json::Value request; if (call.ParseJsonRequest(request) && request.isObject()) @@ -414,6 +412,13 @@ ParseReplacements(modification, request); ParsedDicomFile dicom; + + if (modification.IsReplaced(DICOM_TAG_PIXEL_DATA)) + { + dicom.EmbedImage(modification.GetReplacement(DICOM_TAG_PIXEL_DATA)); + modification.Keep(DICOM_TAG_PIXEL_DATA); + } + modification.Apply(dicom); std::string id;
--- a/OrthancServer/ParsedDicomFile.cpp Tue May 06 12:55:41 2014 +0200 +++ b/OrthancServer/ParsedDicomFile.cpp Tue May 06 16:33:40 2014 +0200 @@ -87,6 +87,7 @@ #include "../Core/DicomFormat/DicomString.h" #include "../Core/DicomFormat/DicomNullValue.h" #include "../Core/DicomFormat/DicomIntegerPixelAccessor.h" +#include "../Core/ImageFormats/PngReader.h" #include <list> #include <limits> @@ -1087,4 +1088,132 @@ { return new ParsedDicomFile(*this); } + + + void ParsedDicomFile::EmbedImage(const std::string& dataUriScheme) + { + std::string mime, content; + Toolbox::DecodeDataUriScheme(mime, content, dataUriScheme); + + std::string decoded = Toolbox::DecodeBase64(content); + + if (mime == "image/png") + { + PngReader reader; + reader.ReadFromMemory(decoded); + EmbedImage(reader); + } + else + { + throw OrthancException(ErrorCode_NotImplemented); + } + } + + + void ParsedDicomFile::EmbedImage(const ImageAccessor& accessor) + { + if (accessor.GetFormat() != PixelFormat_Grayscale8 && + accessor.GetFormat() != PixelFormat_Grayscale16 && + accessor.GetFormat() != PixelFormat_RGB24 && + accessor.GetFormat() != PixelFormat_RGBA32) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + if (accessor.GetFormat() == PixelFormat_RGBA32) + { + LOG(WARNING) << "Getting rid of the alpha channel when embedding a RGBA image inside DICOM"; + } + + // http://dicomiseasy.blogspot.be/2012/08/chapter-12-pixel-data.html + + Remove(DICOM_TAG_PIXEL_DATA); + Replace(DICOM_TAG_COLUMNS, boost::lexical_cast<std::string>(accessor.GetWidth())); + Replace(DICOM_TAG_ROWS, boost::lexical_cast<std::string>(accessor.GetHeight())); + Replace(DICOM_TAG_SAMPLES_PER_PIXEL, "1"); + Replace(DICOM_TAG_NUMBER_OF_FRAMES, "1"); + Replace(DICOM_TAG_PIXEL_REPRESENTATION, "0"); // Unsigned pixels + Replace(DICOM_TAG_PLANAR_CONFIGURATION, "0"); // Color channels are interleaved + Replace(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "MONOCHROME2"); + Replace(DICOM_TAG_BITS_ALLOCATED, "8"); + Replace(DICOM_TAG_BITS_STORED, "8"); + Replace(DICOM_TAG_HIGH_BIT, "7"); + + unsigned int bytesPerPixel = 1; + + switch (accessor.GetFormat()) + { + case PixelFormat_RGB24: + case PixelFormat_RGBA32: + Replace(DICOM_TAG_PHOTOMETRIC_INTERPRETATION, "RGB"); + Replace(DICOM_TAG_SAMPLES_PER_PIXEL, "3"); + bytesPerPixel = 3; + break; + + case PixelFormat_Grayscale8: + break; + + case PixelFormat_Grayscale16: + Replace(DICOM_TAG_BITS_ALLOCATED, "16"); + Replace(DICOM_TAG_BITS_STORED, "16"); + Replace(DICOM_TAG_HIGH_BIT, "15"); + bytesPerPixel = 2; + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + + DcmTag key(DICOM_TAG_PIXEL_DATA.GetGroup(), + DICOM_TAG_PIXEL_DATA.GetElement()); + + std::auto_ptr<DcmPixelData> pixels(new DcmPixelData(key)); + + unsigned int pitch = accessor.GetWidth() * bytesPerPixel; + Uint8* target = NULL; + pixels->createUint8Array(accessor.GetHeight() * pitch, target); + + for (unsigned int y = 0; y < accessor.GetHeight(); y++) + { + switch (accessor.GetFormat()) + { + case PixelFormat_RGB24: + case PixelFormat_Grayscale8: + case PixelFormat_Grayscale16: + case PixelFormat_SignedGrayscale16: + { + if (Toolbox::DetectEndianness() != Endianness_Little) + { + throw OrthancException(ErrorCode_NotImplemented); + } + + memcpy(target, reinterpret_cast<const Uint8*>(accessor.GetConstRow(y)), pitch); + target += pitch; + break; + } + + case PixelFormat_RGBA32: + { + // The alpha channel is not supported by the DICOM standard + const Uint8* source = reinterpret_cast<const Uint8*>(accessor.GetConstRow(y)); + for (unsigned int x = 0; x < accessor.GetWidth(); x++, target += 3, source += 4) + { + target[0] = source[0]; + target[1] = source[1]; + target[2] = source[2]; + } + + break; + } + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + if (!pimpl_->file_->getDataset()->insert(pixels.release(), false, false).good()) + { + throw OrthancException(ErrorCode_InternalError); + } + } }
--- a/OrthancServer/ParsedDicomFile.h Tue May 06 12:55:41 2014 +0200 +++ b/OrthancServer/ParsedDicomFile.h Tue May 06 16:33:40 2014 +0200 @@ -35,6 +35,7 @@ #include "../Core/DicomFormat/DicomInstanceHasher.h" #include "../Core/RestApi/RestApiOutput.h" #include "ServerEnumerations.h" +#include "../Core/ImageFormats/ImageAccessor.h" namespace Orthanc { @@ -87,6 +88,10 @@ void SaveToMemoryBuffer(std::string& buffer); void SaveToFile(const std::string& path); + + void EmbedImage(const ImageAccessor& accessor); + + void EmbedImage(const std::string& dataUriScheme); }; }
--- a/UnitTestsSources/FromDcmtk.cpp Tue May 06 12:55:41 2014 +0200 +++ b/UnitTestsSources/FromDcmtk.cpp Tue May 06 16:33:40 2014 +0200 @@ -4,6 +4,9 @@ #include "../OrthancServer/OrthancInitialization.h" #include "../OrthancServer/DicomModification.h" #include "../Core/OrthancException.h" +#include "../Core/ImageFormats/ImageBuffer.h" +#include "../Core/ImageFormats/PngReader.h" +#include "../Core/ImageFormats/PngWriter.h" using namespace Orthanc; @@ -47,3 +50,67 @@ f->SaveToFile(b); } } + + +#include <dcmdata/dcuid.h> + +TEST(DicomModification, Png) +{ + // Red dot in http://en.wikipedia.org/wiki/Data_URI_scheme (RGBA image) + std::string s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; + + std::string m, c; + Toolbox::DecodeDataUriScheme(m, c, s); + + ASSERT_EQ("image/png", m); + ASSERT_EQ(116, c.size()); + + std::string cc = Toolbox::DecodeBase64(c); + + Toolbox::WriteFile(cc, "/tmp/tata.png"); + + PngReader reader; + reader.ReadFromMemory(cc); + + ASSERT_EQ(5, reader.GetHeight()); + ASSERT_EQ(5, reader.GetWidth()); + ASSERT_EQ(PixelFormat_RGBA32, reader.GetFormat()); + + ParsedDicomFile o; + o.EmbedImage(s); + o.SaveToFile("png1.dcm"); + + // Red dot, without alpha channel + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDTcIn2+8BgAAACJJREFUCNdj/P//PwMjIwME/P/P+J8BBTAxEOL/R9Lx/z8AynoKAXOeiV8AAAAASUVORK5CYII="; + o.EmbedImage(s); + o.SaveToFile("png2.dcm"); + + // Check box in Graylevel8 + s = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gUGDDcB53FulQAAAElJREFUGNNtj0sSAEEEQ1+U+185s1CtmRkblQ9CZldsKHJDk6DLGLJa6chjh0ooQmpjXMM86zPwydGEj6Ed/UGykkEM8X+p3u8/8LcOJIWLGeMAAAAASUVORK5CYII="; + o.EmbedImage(s); + //o.Replace(DICOM_TAG_SOP_CLASS_UID, UID_DigitalXRayImageStorageForProcessing); + o.SaveToFile("png3.dcm"); + + + { + // Gradient in Graylevel16 + + ImageBuffer img; + img.SetWidth(256); + img.SetHeight(256); + img.SetFormat(PixelFormat_Grayscale16); + + int v = 0; + for (unsigned int y = 0; y < img.GetHeight(); y++) + { + uint16_t *p = reinterpret_cast<uint16_t*>(img.GetAccessor().GetRow(y)); + for (unsigned int x = 0; x < img.GetWidth(); x++, p++, v++) + { + *p = v; + } + } + + o.EmbedImage(img.GetAccessor()); + o.SaveToFile("png4.dcm"); + } +}