From 8d78400052c9e6b60374364163f234790251b8fb Mon Sep 17 00:00:00 2001 From: StefanBruens Date: Fri, 27 Mar 2020 08:18:58 +0100 Subject: [PATCH] Merge pull request #16494 from StefanBruens:jpeg2000_openjpeg_port Jpeg2000 OpenJPEG port * OpenJPEG based JPEG2000 decoder implementation Currently, the following input color spaces and depth conversions are supported: - 8 bit -> 8 bit - 16 bit -> 16 bit (IMREAD_UNCHANGED, IMREAD_ANYDEPTH) - RGB(a) -> BGR - RGBA -> BGRA (IMREAD_UNCHANGED) - Y(a) -> Y(a) (IMREAD_ANYCOLOR, IMREAD_GRAY, IMREAD_UNCHANGED)) - YCC -> Y (IMREAD_GRAY) * Check for OpenJPEG availability This enables OpenJPEG based JPEG2000 imread support by default, which can be disabled by -DWITH_OPENJPEG=OFF. In case OpenJPEG is enabled and found, any checks for Jasper are skipped. * Implement precision downscaling for precision > 8 without IMREAD_UNCHANGED With IMREAD_UNCHANGED, values are kept from the input image, without it components are downscaled to CV_8U range. * Enable Jpeg2K tests when OpenJPEG is available * Add support for some more color conversions Support IMREAD_GRAY when input color space is RGB or unspecified. Support YUV input color space for BGR output. * fix: problems with unmanaged memory * fix: CMake warning - HAVE_OPENJPEG is undefined Removed trailing whitespaces * fix: CMake find_package OpenJPEG add minimal version * Basic JPEG2K encoder Images with depth CV_8U and CV_16U are supported, with 1 to 4 channels. * feature: Improved code for OpenJPEG2000 encoder/decoder - Removed code duplication - Added error handlers - Extracted functions * feature: Update conversion openjpeg array from/to Mat * feature: Extend ChannelsIterator to fulfill RandomAccessIterator named requirements - Removed channels split in copyFromMatImpl. With ChannelsIterator no allocations are performed. - Split whole loop into 2 parts in copyToMat -> where std::copy and std::transforms are called. * fix: Applied review comments. - Changed `nullptr` in CV_LOG* functions to `NULL` - Added `falls through` comment in decoder color space `switch` - Added warning about unsupported parameters for the encoder * feature: Added decode from in-memory buffers. Co-authored-by: Vadim Levin --- CMakeLists.txt | 13 +- cmake/OpenCVFindLibsGrfmt.cmake | 19 +- cmake/templates/cvconfig.h.in | 1 + modules/imgcodecs/CMakeLists.txt | 5 + .../imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp | 762 ++++++++++++++++++ .../imgcodecs/src/grfmt_jpeg2000_openjpeg.hpp | 99 +++ modules/imgcodecs/src/grfmts.hpp | 1 + modules/imgcodecs/src/loadsave.cpp | 4 + modules/imgcodecs/test/test_grfmt.cpp | 3 +- modules/imgcodecs/test/test_read_write.cpp | 3 +- 10 files changed, 903 insertions(+), 7 deletions(-) create mode 100644 modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp create mode 100644 modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 56b4fa16e6..d5c486415f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -285,9 +285,12 @@ OCV_OPTION(WITH_INF_ENGINE "Include Intel Inference Engine support" OFF OCV_OPTION(WITH_NGRAPH "Include nGraph support" WITH_INF_ENGINE VISIBLE_IF TRUE VERIFY TARGET ngraph::ngraph) -OCV_OPTION(WITH_JASPER "Include JPEG2K support" ON +OCV_OPTION(WITH_JASPER "Include JPEG2K support (Jasper)" ON VISIBLE_IF NOT IOS VERIFY HAVE_JASPER) +OCV_OPTION(WITH_OPENJPEG "Include JPEG2K support (OpenJPEG)" ON + VISIBLE_IF NOT IOS + VERIFY HAVE_OPENJPEG) OCV_OPTION(WITH_JPEG "Include JPEG support" ON VISIBLE_IF TRUE VERIFY HAVE_JPEG) @@ -1249,8 +1252,12 @@ if(WITH_TIFF OR HAVE_TIFF) status(" TIFF:" TIFF_FOUND THEN "${TIFF_LIBRARY} (ver ${TIFF_VERSION} / ${TIFF_VERSION_STRING})" ELSE "build (ver ${TIFF_VERSION} - ${TIFF_VERSION_STRING})") endif() -if(WITH_JASPER OR HAVE_JASPER) - status(" JPEG 2000:" JASPER_FOUND THEN "${JASPER_LIBRARY} (ver ${JASPER_VERSION_STRING})" ELSE "build (ver ${JASPER_VERSION_STRING})") +if(HAVE_OPENJPEG) + status(" JPEG 2000:" "OpenJPEG (ver ${OPENJPEG_MAJOR_VERSION}.${OPENJPEG_MINOR_VERSION}.${OPENJPEG_BUILD_VERSION})") +elseif(HAVE_JASPER) + status(" JPEG 2000:" JASPER_FOUND THEN "${JASPER_LIBRARY} (ver ${JASPER_VERSION_STRING})" ELSE "build Jasper (ver ${JASPER_VERSION_STRING})") +elseif(WITH_OPENJPEG OR WITH_JASPER) + status(" JPEG 2000:" "NO") endif() if(WITH_OPENEXR OR HAVE_OPENEXR) diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index 227cfb256b..0beaf19317 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -153,8 +153,23 @@ if(NOT WEBP_VERSION AND WEBP_INCLUDE_DIR) endif() endif() +# --- libopenjp2 (optional, check before libjasper) --- +if(WITH_OPENJPEG) + find_package(OpenJPEG QUIET) + + if(NOT OpenJPEG_FOUND OR OPENJPEG_MAJOR_VERSION LESS 2) + set(HAVE_OPENJPEG NO) + ocv_clear_vars(OPENJPEG_MAJOR_VERSION OPENJPEG_MINOR_VERSION OPENJPEG_BUILD_VERSION OPENJPEG_LIBRARIES OPENJPEG_INCLUDE_DIRS) + message(STATUS "Could NOT find OpenJPEG (minimal suitable version: 2.0, recommended version >= 2.3.1)") + else() + set(HAVE_OPENJPEG YES) + message(STATUS "Found OpenJPEG: ${OPENJPEG_LIBRARIES} " + "(found version \"${OPENJPEG_MAJOR_VERSION}.${OPENJPEG_MINOR_VERSION}.${OPENJPEG_BUILD_VERSION}\")") + endif() +endif() + # --- libjasper (optional, should be searched after libjpeg) --- -if(WITH_JASPER) +if(WITH_JASPER AND NOT HAVE_OPENJPEG) if(BUILD_JASPER) ocv_clear_vars(JASPER_FOUND) else() @@ -273,4 +288,4 @@ if(WITH_IMGCODEC_PFM) set(HAVE_IMGCODEC_PFM ON) elseif(DEFINED WITH_IMGCODEC_PFM) set(HAVE_IMGCODEC_PFM OFF) -endif() \ No newline at end of file +endif() diff --git a/cmake/templates/cvconfig.h.in b/cmake/templates/cvconfig.h.in index 0bc4a4db66..f2b171a096 100644 --- a/cmake/templates/cvconfig.h.in +++ b/cmake/templates/cvconfig.h.in @@ -81,6 +81,7 @@ #cmakedefine HAVE_IPP_IW_LL /* JPEG-2000 codec */ +#cmakedefine HAVE_OPENJPEG #cmakedefine HAVE_JASPER /* IJG JPEG codec */ diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 30e8a9d3d0..929956ef32 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -45,6 +45,11 @@ if(HAVE_TIFF) list(APPEND GRFMT_LIBS ${TIFF_LIBRARIES}) endif() +if(HAVE_OPENJPEG) + ocv_include_directories(${OPENJPEG_INCLUDE_DIRS}) + list(APPEND GRFMT_LIBS ${OPENJPEG_LIBRARIES}) +endif() + if(HAVE_JASPER) ocv_include_directories(${JASPER_INCLUDE_DIR}) list(APPEND GRFMT_LIBS ${JASPER_LIBRARIES}) diff --git a/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp b/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp new file mode 100644 index 0000000000..b1ede93d3f --- /dev/null +++ b/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.cpp @@ -0,0 +1,762 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// Copyright (C) 2020, Stefan Brüns + +#include "precomp.hpp" + +#ifdef HAVE_OPENJPEG +#include "grfmt_jpeg2000_openjpeg.hpp" + +#include "opencv2/core/utils/logger.hpp" + +namespace cv { + +namespace { + +String colorspaceName(COLOR_SPACE colorspace) +{ + switch (colorspace) + { + case OPJ_CLRSPC_CMYK: + return "CMYK"; + case OPJ_CLRSPC_SRGB: + return "sRGB"; + case OPJ_CLRSPC_EYCC: + return "e-YCC"; + case OPJ_CLRSPC_GRAY: + return "grayscale"; + case OPJ_CLRSPC_SYCC: + return "YUV"; + case OPJ_CLRSPC_UNKNOWN: + return "unknown"; + case OPJ_CLRSPC_UNSPECIFIED: + return "unspecified"; + default: + CV_Assert(!"Invalid colorspace"); + } +} + +template +struct ConstItTraits { + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = const T*; + using reference = const T&; +}; + +template +struct NonConstItTraits { + using value_type = T; + using difference_type = std::ptrdiff_t; + using pointer = T*; + using reference = T&; +}; + +/** + * Iterator over the channel in continuous chunk of the memory e.g. in the one row of a Mat + * No bounds checks are preformed due to keeping this class as simple as possible while + * fulfilling RandomAccessIterator naming requirements. + * + * @tparam Traits holds information about value type and constness of the defined types + */ +template +class ChannelsIterator +{ +public: + using difference_type = typename Traits::difference_type; + using value_type = typename Traits::value_type; + using pointer = typename Traits::pointer; + using reference = typename Traits::reference; + using iterator_category = std::random_access_iterator_tag; + + ChannelsIterator(pointer ptr, std::size_t channel, std::size_t channels_count) + : ptr_ { ptr + channel }, step_ { channels_count } + { + } + + /* Element Access */ + reference operator*() const + { + return *ptr_; + } + + pointer operator->() const + { + return &(operator*()); + } + + reference operator[](difference_type n) const + { + return *(*this + n); + } + + /* Iterator movement */ + ChannelsIterator& operator++() + { + ptr_ += step_; + return *this; + } + + ChannelsIterator operator++(int) + { + ChannelsIterator ret(*this); + ++(*this); + return ret; + } + + ChannelsIterator& operator--() + { + ptr_ -= step_; + return *this; + } + + ChannelsIterator operator--(int) + { + ChannelsIterator ret(*this); + --(*this); + return ret; + } + + ChannelsIterator& operator-=(difference_type n) + { + ptr_ -= n * step_; + return *this; + } + + ChannelsIterator& operator+=(difference_type n) + { + ptr_ += n * step_; + return *this; + } + + ChannelsIterator operator-(difference_type n) const + { + return ChannelsIterator(*this) -= n; + } + + ChannelsIterator operator+(difference_type n) const + { + return ChannelsIterator(*this) += n; + } + + difference_type operator-(const ChannelsIterator& other) + { + return (ptr_ - other.ptr_) / step_; + } + + /* Comparision */ + bool operator==(const ChannelsIterator& other) const CV_NOEXCEPT + { + return ptr_ == other.ptr_; + } + + bool operator!=(const ChannelsIterator& other) const CV_NOEXCEPT + { + return !(*this == other); + } + + bool operator<(const ChannelsIterator& other) const CV_NOEXCEPT + { + return ptr_ < other.ptr_; + } + + bool operator>(const ChannelsIterator& other) const CV_NOEXCEPT + { + return other < *this; + } + + bool operator>=(const ChannelsIterator& other) const CV_NOEXCEPT + { + return !(*this < other); + } + + bool operator<=(const ChannelsIterator& other) const CV_NOEXCEPT + { + return !(other < *this); + } + +private: + pointer ptr_{nullptr}; + std::size_t step_{1}; +}; + +template +inline ChannelsIterator operator+(typename Traits::difference_type n, const ChannelsIterator& it) +{ + return it + n; +} + +template +void copyToMatImpl(std::vector&& in, Mat& out, uint8_t shift) +{ + using ChannelsIt = ChannelsIterator>; + + Size size = out.size(); + if (out.isContinuous()) + { + size.width *= size.height; + size.height = 1; + } + + const bool isShiftRequired = shift != 0; + + const std::size_t channelsCount = in.size(); + + if (isShiftRequired) + { + for (int i = 0; i < size.height; ++i) + { + auto rowPtr = out.ptr(i); + for (std::size_t c = 0; c < channelsCount; ++c) + { + const auto first = in[c]; + const auto last = first + size.width; + auto dOut = ChannelsIt(rowPtr, c, channelsCount); + std::transform(first, last, dOut, [shift](InT val) -> OutT { return val >> shift; }); + in[c] += size.width; + } + } + } + else + { + for (int i = 0; i < size.height; ++i) + { + auto rowPtr = out.ptr(i); + for (std::size_t c = 0; c < channelsCount; ++c) + { + const auto first = in[c]; + const auto last = first + size.width; + auto dOut = ChannelsIt(rowPtr, c, channelsCount); + std::copy(first, last, dOut); + in[c] += size.width; + } + } + } +} + +template +void copyToMat(std::vector&& in, Mat& out, uint8_t shift) +{ + switch (out.depth()) + { + case CV_8U: + copyToMatImpl(std::move(in), out, shift); + break; + case CV_16U: + copyToMatImpl(std::move(in), out, shift); + break; + default: + CV_Error(Error::StsNotImplemented, "only depth CV_8U and CV16_U are supported"); + } +} + +template +void copyFromMatImpl(const Mat& in, std::vector&& out) +{ + using ChannelsIt = ChannelsIterator>; + + Size size = in.size(); + if (in.isContinuous()) + { + size.width *= size.height; + size.height = 1; + } + + const std::size_t outChannelsCount = out.size(); + + for (int i = 0; i < size.height; ++i) + { + const InT* row = in.ptr(i); + for (std::size_t c = 0; c < outChannelsCount; ++c) + { + auto first = ChannelsIt(row, c, outChannelsCount); + auto last = first + size.width; + out[c] = std::copy(first, last, out[c]); + } + } +} + +template +void copyFromMat(const Mat& in, std::vector&& out) +{ + switch (in.depth()) + { + case CV_8U: + copyFromMatImpl(in, std::move(out)); + break; + case CV_16U: + copyFromMatImpl(in, std::move(out)); + break; + default: + CV_Error(Error::StsNotImplemented, "only depth CV_8U and CV16_U are supported"); + } +} + +void errorLogCallback(const char* msg, void* /* userData */) +{ + CV_LOG_ERROR(NULL, cv::format("OpenJPEG2000: %s", msg)); +} + +void warningLogCallback(const char* msg, void* /* userData */) +{ + CV_LOG_WARNING(NULL, cv::format("OpenJPEG2000: %s", msg)); +} + +void setupLogCallbacks(opj_codec_t* codec) +{ + if (!opj_set_error_handler(codec, errorLogCallback, nullptr)) + { + CV_LOG_WARNING(NULL, "OpenJPEG2000: can not set error log handler"); + } + if (!opj_set_warning_handler(codec, warningLogCallback, nullptr)) + { + CV_LOG_WARNING(NULL, "OpenJPEG2000: can not set warning log handler"); + } +} + +opj_dparameters setupDecoderParameters() +{ + opj_dparameters parameters; + opj_set_default_decoder_parameters(¶meters); + return parameters; +} + +opj_cparameters setupEncoderParameters(const std::vector& params) +{ + opj_cparameters parameters; + opj_set_default_encoder_parameters(¶meters); + for (size_t i = 0; i < params.size(); i += 2) + { + switch (params[i]) + { + case cv::IMWRITE_JPEG2000_COMPRESSION_X1000: + parameters.tcp_rates[0] = 1000.f / std::min(std::max(params[i + 1], 1), 1000); + break; + default: + CV_LOG_WARNING(NULL, "OpenJPEG2000(encoder): skip unsupported parameter: " << params[i]); + break; + } + } + parameters.tcp_numlayers = 1; + parameters.cp_disto_alloc = 1; + return parameters; +} + +bool decodeSRGBData(const opj_image_t& inImg, cv::Mat& outImg, uint8_t shift) +{ + using ImageComponents = std::vector; + + const int inChannels = inImg.numcomps; + const int outChannels = outImg.channels(); + + if (outChannels == 1) + { + // Assume gray (+ alpha) for 1 channels -> gray + if (inChannels <= 2) + { + copyToMat(ImageComponents { inImg.comps[0].data }, outImg, shift); + } + // Assume RGB for >= 3 channels -> gray + else + { + Mat tmp(outImg.size(), CV_MAKETYPE(outImg.depth(), 3)); + copyToMat(ImageComponents { inImg.comps[2].data, inImg.comps[1].data, inImg.comps[0].data }, + tmp, shift); + cvtColor(tmp, outImg, COLOR_BGR2GRAY); + } + return true; + } + + if (inChannels >= 3) + { + // Assume RGB (+ alpha) for 3 channels -> BGR + ImageComponents incomps { inImg.comps[2].data, inImg.comps[1].data, inImg.comps[0].data }; + // Assume RGBA for 4 channels -> BGRA + if (outChannels > 3) + { + incomps.push_back(inImg.comps[3].data); + } + copyToMat(std::move(incomps), outImg, shift); + return true; + } + CV_LOG_ERROR(NULL, + cv::format("OpenJPEG2000: unsupported conversion from %d components to %d for SRGB image decoding", + inChannels, outChannels)); + return false; +} + +bool decodeGrayscaleData(const opj_image_t& inImg, cv::Mat& outImg, uint8_t shift) +{ + using ImageComponents = std::vector; + + const int inChannels = inImg.numcomps; + const int outChannels = outImg.channels(); + + if (outChannels == 1 || outChannels == 3) + { + copyToMat(ImageComponents(outChannels, inImg.comps[0].data), outImg, shift); + return true; + } + CV_LOG_ERROR(NULL, + cv::format("OpenJPEG2000: unsupported conversion from %d components to %d for Grayscale image decoding", + inChannels, outChannels)); + return false; +} + +bool decodeSYCCData(const opj_image_t& inImg, cv::Mat& outImg, uint8_t shift) +{ + using ImageComponents = std::vector; + + const int inChannels = inImg.numcomps; + const int outChannels = outImg.channels(); + + if (outChannels == 1) { + copyToMat(ImageComponents { inImg.comps[0].data }, outImg, shift); + return true; + } + + if (outChannels == 3 && inChannels >= 3) { + copyToMat(ImageComponents { inImg.comps[0].data, inImg.comps[1].data, inImg.comps[2].data }, + outImg, shift); + cvtColor(outImg, outImg, COLOR_YUV2BGR); + return true; + } + + CV_LOG_ERROR(NULL, + cv::format("OpenJPEG2000: unsupported conversion from %d components to %d for YUV image decoding", + inChannels, outChannels)); + return false; +} + +OPJ_SIZE_T opjReadFromBuffer(void* dist, OPJ_SIZE_T count, detail::OpjMemoryBuffer* buffer) +{ + const OPJ_SIZE_T bytesToRead = std::min(buffer->availableBytes(), count); + if (bytesToRead > 0) + { + memcpy(dist, buffer->pos, bytesToRead); + buffer->pos += bytesToRead; + return bytesToRead; + } + else + { + return static_cast(-1); + } +} + +OPJ_SIZE_T opjSkipFromBuffer(OPJ_SIZE_T count, detail::OpjMemoryBuffer* buffer) { + const OPJ_SIZE_T bytesToSkip = std::min(buffer->availableBytes(), count); + if (bytesToSkip > 0) + { + buffer->pos += bytesToSkip; + return bytesToSkip; + } + else + { + return static_cast(-1); + } +} + +OPJ_BOOL opjSeekFromBuffer(OPJ_OFF_T count, detail::OpjMemoryBuffer* buffer) +{ + // Count should stay positive to prevent unsigned overflow + CV_DbgAssert(count > 0); + // To provide proper comparison between OPJ_OFF_T and OPJ_SIZE_T, both should be + // casted to uint64_t (On 32-bit systems sizeof(size_t) might be 4) + CV_DbgAssert(static_cast(count) < static_cast(std::numeric_limits::max())); + const OPJ_SIZE_T pos = std::min(buffer->length, static_cast(count)); + buffer->pos = buffer->begin + pos; + return OPJ_TRUE; +} + +detail::StreamPtr opjCreateBufferInputStream(detail::OpjMemoryBuffer* buf) +{ + detail::StreamPtr stream{ opj_stream_default_create(/* isInput */ true) }; + if (stream) + { + opj_stream_set_user_data(stream.get(), static_cast(buf), nullptr); + opj_stream_set_user_data_length(stream.get(), buf->length); + + opj_stream_set_read_function(stream.get(), (opj_stream_read_fn)(opjReadFromBuffer)); + opj_stream_set_skip_function(stream.get(), (opj_stream_skip_fn)(opjSkipFromBuffer)); + opj_stream_set_seek_function(stream.get(), (opj_stream_seek_fn)(opjSeekFromBuffer)); + } + return stream; +} + +} // namespace + +/////////////////////// Jpeg2KOpjDecoder /////////////////// + +Jpeg2KOpjDecoder::Jpeg2KOpjDecoder() +{ + static const unsigned char signature[] = { 0, 0, 0, 0x0c, 'j', 'P', ' ', ' ', 13, 10, 0x87, 10 }; + m_signature = String((const char*)(signature), sizeof(signature)); + m_buf_supported = true; +} + + +ImageDecoder Jpeg2KOpjDecoder::newDecoder() const +{ + return makePtr(); +} + +bool Jpeg2KOpjDecoder::readHeader() +{ + if (!m_buf.empty()) { + opjBuf_ = detail::OpjMemoryBuffer(m_buf); + stream_ = opjCreateBufferInputStream(&opjBuf_); + } + else + { + stream_.reset(opj_stream_create_default_file_stream(m_filename.c_str(), OPJ_STREAM_READ)); + } + if (!stream_) + return false; + + codec_.reset(opj_create_decompress(OPJ_CODEC_JP2)); + if (!codec_) + return false; + + // Callbacks are cleared, when opj_destroy_codec is called, + // They can provide some additional information for the user, about what goes wrong + setupLogCallbacks(codec_.get()); + + opj_dparameters parameters = setupDecoderParameters(); + if (!opj_setup_decoder(codec_.get(), ¶meters)) + return false; + + { + opj_image_t* rawImage; + if (!opj_read_header(stream_.get(), codec_.get(), &rawImage)) + return false; + + image_.reset(rawImage); + } + + m_width = image_->x1 - image_->x0; + m_height = image_->y1 - image_->y0; + + /* Different components may have different precision, + * so check all. + */ + bool hasAlpha = false; + const int numcomps = image_->numcomps; + CV_Assert(numcomps >= 1); + for (int i = 0; i < numcomps; i++) + { + const opj_image_comp_t& comp = image_->comps[i]; + + if (comp.sgnd) + { + CV_Error(Error::StsNotImplemented, cv::format("OpenJPEG2000: Component %d/%d is signed", i, numcomps)); + } + + if (hasAlpha && comp.alpha) + { + CV_Error(Error::StsNotImplemented, cv::format("OpenJPEG2000: Component %d/%d is duplicate alpha channel", i, numcomps)); + } + + hasAlpha |= comp.alpha; + + m_maxPrec = std::max(m_maxPrec, comp.prec); + } + + if (m_maxPrec < 8) { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Precision < 8 not supported"); + } else if (m_maxPrec == 8) { + m_type = CV_MAKETYPE(CV_8U, numcomps); + } else if (m_maxPrec <= 16) { + m_type = CV_MAKETYPE(CV_16U, numcomps); + } else if (m_maxPrec <= 23) { + m_type = CV_MAKETYPE(CV_32F, numcomps); + } else { + m_type = CV_MAKETYPE(CV_64F, numcomps); + } + return true; +} + +bool Jpeg2KOpjDecoder::readData( Mat& img ) +{ + using DecodeFunc = bool(*)(const opj_image_t&, cv::Mat&, uint8_t shift); + + if (!opj_decode(codec_.get(), stream_.get(), image_.get())) + { + CV_Error(Error::StsError, "OpenJPEG2000: Decoding is failed"); + } + + if (img.channels() == 2) + { + CV_Error(Error::StsNotImplemented, + cv::format("OpenJPEG2000: Unsupported number of output channels. IN: %d OUT: 2", image_->numcomps)); + } + + DecodeFunc decode = nullptr; + switch (image_->color_space) + { + case OPJ_CLRSPC_UNKNOWN: + CV_LOG_WARNING(NULL, "OpenJPEG2000: Image has unknown color space, SRGB is assumed"); + /* FALLTHRU */ + case OPJ_CLRSPC_SRGB: + decode = decodeSRGBData; + break; + case OPJ_CLRSPC_GRAY: + decode = decodeGrayscaleData; + break; + case OPJ_CLRSPC_SYCC: + decode = decodeSYCCData; + break; + case OPJ_CLRSPC_UNSPECIFIED: + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Image has unspecified color space"); + default: + CV_Error(Error::StsNotImplemented, + cv::format("OpenJPEG2000: Unsupported color space conversion: %s -> %s", + colorspaceName(image_->color_space).c_str(), + (img.channels() == 1) ? "gray" : "BGR")); + } + + const int depth = img.depth(); + const OPJ_UINT32 outPrec = [depth]() { + if (depth == CV_8U) return 8; + if (depth == CV_16U) return 16; + CV_Error(Error::StsNotImplemented, + cv::format("OpenJPEG2000: output precision > 16 not supported: target depth %d", depth)); + }(); + const uint8_t shift = outPrec > m_maxPrec ? 0 : m_maxPrec - outPrec; + return decode(*image_, img, shift); +} + + +/////////////////////// Jpeg2KOpjEncoder /////////////////// + +Jpeg2KOpjEncoder::Jpeg2KOpjEncoder() +{ + m_description = "JPEG-2000 files (*.jp2)"; +} + +ImageEncoder Jpeg2KOpjEncoder::newEncoder() const +{ + return makePtr(); +} + +bool Jpeg2KOpjEncoder::isFormatSupported(int depth) const +{ + return depth == CV_8U || depth == CV_16U; +} + +bool Jpeg2KOpjEncoder::write(const Mat& img, const std::vector& params) +{ + CV_Assert(params.size() % 2 == 0); + + const int channels = img.channels(); + CV_DbgAssert(channels > 0); // passed matrix is not empty + if (channels > 4) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: only BGR(a) and gray (+ alpha) images supported"); + } + + const int depth = img.depth(); + + const OPJ_UINT32 outPrec = [depth]() { + if (depth == CV_8U) return 8; + if (depth == CV_16U) return 16; + CV_Error(Error::StsNotImplemented, + cv::format("OpenJPEG2000: image precision > 16 not supported. Got: %d", depth)); + }(); + + opj_cparameters parameters = setupEncoderParameters(params); + + std::vector compparams(channels); + for (int i = 0; i < channels; i++) { + compparams[i].prec = outPrec; + compparams[i].bpp = outPrec; + compparams[i].sgnd = 0; // unsigned for now + compparams[i].dx = parameters.subsampling_dx; + compparams[i].dy = parameters.subsampling_dy; + compparams[i].w = img.size().width; + compparams[i].h = img.size().height; + } + + + auto colorspace = (channels > 2) ? OPJ_CLRSPC_SRGB : OPJ_CLRSPC_GRAY; + detail::ImagePtr image(opj_image_create(channels, compparams.data(), colorspace)); + if (!image) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: can not create image"); + } + + if (channels == 2 || channels == 4) + { + image->comps[channels - 1].alpha = 1; + } + // we want the full image + image->x0 = 0; + image->y0 = 0; + image->x1 = compparams[0].dx * compparams[0].w; + image->y1 = compparams[0].dy * compparams[0].h; + + // fill the component data arrays + std::vector outcomps(channels, nullptr); + if (channels == 1) + { + outcomps.assign({ image->comps[0].data }); + } + else if (channels == 2) + { + outcomps.assign({ image->comps[0].data, image->comps[1].data }); + } + // Reversed order for BGR -> RGB conversion + else if (channels == 3) + { + outcomps.assign({ image->comps[2].data, image->comps[1].data, image->comps[0].data }); + } + else if (channels == 4) + { + outcomps.assign({ image->comps[2].data, image->comps[1].data, image->comps[0].data, + image->comps[3].data }); + } + // outcomps holds pointers to the data, so the actual data will be modified but won't be freed + // The container is not needed after data was copied + copyFromMat(img, std::move(outcomps)); + + detail::CodecPtr codec(opj_create_compress(OPJ_CODEC_JP2)); + if (!codec) { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: can not create compression codec"); + } + + setupLogCallbacks(codec.get()); + + if (!opj_setup_encoder(codec.get(), ¶meters, image.get())) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Can not setup encoder"); + } + + detail::StreamPtr stream(opj_stream_create_default_file_stream(m_filename.c_str(), OPJ_STREAM_WRITE)); + if (!stream) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Can not create stream"); + } + + if (!opj_start_compress(codec.get(), image.get(), stream.get())) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Can not start compression"); + } + + if (!opj_encode(codec.get(), stream.get())) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Encoding failed"); + } + + if (!opj_end_compress(codec.get(), stream.get())) + { + CV_Error(Error::StsNotImplemented, "OpenJPEG2000: Can not end compression"); + } + + return true; +} + + +} // namespace cv + +#endif diff --git a/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.hpp b/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.hpp new file mode 100644 index 0000000000..14b888620b --- /dev/null +++ b/modules/imgcodecs/src/grfmt_jpeg2000_openjpeg.hpp @@ -0,0 +1,99 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. +// +// Copyright (C) 2020, Stefan Brüns + +#ifndef _GRFMT_OPENJPEG_H_ +#define _GRFMT_OPENJPEG_H_ + +#ifdef HAVE_OPENJPEG + +#include "grfmt_base.hpp" +#include + +namespace cv { +namespace detail { +struct OpjStreamDeleter +{ + void operator()(opj_stream_t* stream) const + { + opj_stream_destroy(stream); + } +}; + +struct OpjCodecDeleter +{ + void operator()(opj_codec_t* codec) const + { + opj_destroy_codec(codec); + } +}; + +struct OpjImageDeleter +{ + void operator()(opj_image_t* image) const + { + opj_image_destroy(image); + } +}; + +struct OpjMemoryBuffer { + OPJ_BYTE* pos{nullptr}; + OPJ_BYTE* begin{nullptr}; + OPJ_SIZE_T length{0}; + + OpjMemoryBuffer() = default; + + explicit OpjMemoryBuffer(cv::Mat& mat) + : pos{ mat.ptr() }, begin{ mat.ptr() }, length{ mat.rows * mat.cols * mat.elemSize() } + { + } + + OPJ_SIZE_T availableBytes() const CV_NOEXCEPT { + return begin + length - pos; + } +}; + +using StreamPtr = std::unique_ptr; +using CodecPtr = std::unique_ptr; +using ImagePtr = std::unique_ptr; + +} // namespace detail + +class Jpeg2KOpjDecoder CV_FINAL : public BaseImageDecoder +{ +public: + Jpeg2KOpjDecoder(); + ~Jpeg2KOpjDecoder() CV_OVERRIDE = default; + + ImageDecoder newDecoder() const CV_OVERRIDE; + bool readData( Mat& img ) CV_OVERRIDE; + bool readHeader() CV_OVERRIDE; + +private: + detail::StreamPtr stream_{nullptr}; + detail::CodecPtr codec_{nullptr}; + detail::ImagePtr image_{nullptr}; + + detail::OpjMemoryBuffer opjBuf_; + + OPJ_UINT32 m_maxPrec = 0; +}; + +class Jpeg2KOpjEncoder CV_FINAL : public BaseImageEncoder +{ +public: + Jpeg2KOpjEncoder(); + ~Jpeg2KOpjEncoder() CV_OVERRIDE = default; + + bool isFormatSupported( int depth ) const CV_OVERRIDE; + bool write( const Mat& img, const std::vector& params ) CV_OVERRIDE; + ImageEncoder newEncoder() const CV_OVERRIDE; +}; + +} //namespace cv + +#endif + +#endif/*_GRFMT_OPENJPEG_H_*/ diff --git a/modules/imgcodecs/src/grfmts.hpp b/modules/imgcodecs/src/grfmts.hpp index 4fd58d022d..496cb192d5 100644 --- a/modules/imgcodecs/src/grfmts.hpp +++ b/modules/imgcodecs/src/grfmts.hpp @@ -51,6 +51,7 @@ #include "grfmt_tiff.hpp" #include "grfmt_png.hpp" #include "grfmt_jpeg2000.hpp" +#include "grfmt_jpeg2000_openjpeg.hpp" #include "grfmt_exr.hpp" #include "grfmt_webp.hpp" #include "grfmt_hdr.hpp" diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index 5fe4c43e15..f4a0f0f4a3 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -178,6 +178,10 @@ struct ImageCodecInitializer decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); #endif + #ifdef HAVE_OPENJPEG + decoders.push_back( makePtr() ); + encoders.push_back( makePtr() ); + #endif #ifdef HAVE_OPENEXR decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); diff --git a/modules/imgcodecs/test/test_grfmt.cpp b/modules/imgcodecs/test/test_grfmt.cpp index b691891d2a..7d6930ef5d 100644 --- a/modules/imgcodecs/test/test_grfmt.cpp +++ b/modules/imgcodecs/test/test_grfmt.cpp @@ -71,7 +71,8 @@ TEST_P(Imgcodecs_FileMode, regression) const string all_images[] = { -#if defined(HAVE_JASPER) && defined(OPENCV_IMGCODECS_ENABLE_JASPER_TESTS) +#if (defined(HAVE_JASPER) && defined(OPENCV_IMGCODECS_ENABLE_JASPER_TESTS)) \ + || defined(HAVE_OPENJPEG) "readwrite/Rome.jp2", "readwrite/Bretagne2.jp2", "readwrite/Bretagne2.jp2", diff --git a/modules/imgcodecs/test/test_read_write.cpp b/modules/imgcodecs/test/test_read_write.cpp index 6396200ec9..aefc2d5eae 100644 --- a/modules/imgcodecs/test/test_read_write.cpp +++ b/modules/imgcodecs/test/test_read_write.cpp @@ -194,7 +194,8 @@ const string exts[] = { #ifdef HAVE_JPEG "jpg", #endif -#if defined(HAVE_JASPER) && defined(OPENCV_IMGCODECS_ENABLE_JASPER_TESTS) +#if (defined(HAVE_JASPER) && defined(OPENCV_IMGCODECS_ENABLE_JASPER_TESTS)) \ + || defined(HAVE_OPENJPEG) "jp2", #endif #if 0 /*defined HAVE_OPENEXR && !defined __APPLE__*/