From 1db982780fa7789b9437f39f523f22f358af694a Mon Sep 17 00:00:00 2001 From: cDc Date: Tue, 31 Dec 2024 10:56:35 +0200 Subject: [PATCH] Merge pull request #26379 from cdcseacave:jxl_codec Add jxl (JPEG XL) codec support #26379 ### Pull Request Readiness Checklist Related CI and Docker changes: - https://github.com/opencv/ci-gha-workflow/pull/190 - https://github.com/opencv-infrastructure/opencv-gha-dockerfile/pull/44 See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work https://github.com/opencv/opencv/issues/20178 - [ ] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake --- CMakeLists.txt | 7 + cmake/OpenCVFindJPEGXL.cmake | 148 ++++++ cmake/OpenCVFindLibsGrfmt.cmake | 11 + cmake/templates/cvconfig.h.in | 3 + .../config_reference.markdown | 3 +- modules/highgui/src/window_w32.cpp | 3 + modules/imgcodecs/CMakeLists.txt | 13 +- .../imgcodecs/include/opencv2/imgcodecs.hpp | 10 +- modules/imgcodecs/src/grfmt_jpegxl.cpp | 420 ++++++++++++++++++ modules/imgcodecs/src/grfmt_jpegxl.hpp | 68 +++ modules/imgcodecs/src/grfmts.hpp | 1 + modules/imgcodecs/src/loadsave.cpp | 4 + modules/imgcodecs/test/test_jpegxl.cpp | 186 ++++++++ modules/imgcodecs/test/test_read_write.cpp | 7 + 14 files changed, 881 insertions(+), 3 deletions(-) create mode 100644 cmake/OpenCVFindJPEGXL.cmake create mode 100644 modules/imgcodecs/src/grfmt_jpegxl.cpp create mode 100644 modules/imgcodecs/src/grfmt_jpegxl.hpp create mode 100644 modules/imgcodecs/test/test_jpegxl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7af059f8dd..e60406fbe2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -306,6 +306,9 @@ OCV_OPTION(WITH_OPENJPEG "Include JPEG2K support (OpenJPEG)" ON OCV_OPTION(WITH_JPEG "Include JPEG support" ON VISIBLE_IF TRUE VERIFY HAVE_JPEG) +OCV_OPTION(WITH_JPEGXL "Include JPEG XL support" OFF + VISIBLE_IF TRUE + VERIFY HAVE_JPEGXL) OCV_OPTION(WITH_WEBP "Include WebP support" ON VISIBLE_IF NOT WINRT VERIFY HAVE_WEBP) @@ -1533,6 +1536,10 @@ 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_JPEGXL OR HAVE_JPEGXL) + status(" JPEG XL:" JPEGXL_FOUND THEN "${JPEGXL_LIBRARY} (ver ${JPEGXL_VERSION})" ELSE "NO") +endif() + if(HAVE_OPENJPEG) status(" JPEG 2000:" OpenJPEG_FOUND THEN "OpenJPEG (ver ${OPENJPEG_VERSION})" diff --git a/cmake/OpenCVFindJPEGXL.cmake b/cmake/OpenCVFindJPEGXL.cmake new file mode 100644 index 0000000000..6eb5abb9c4 --- /dev/null +++ b/cmake/OpenCVFindJPEGXL.cmake @@ -0,0 +1,148 @@ +# The script is taken from https://webkit.googlesource.com/WebKit/+/master/Source/cmake/FindJPEGXL.cmake + +# Copyright (C) 2021 Sony Interactive Entertainment Inc. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +# THE POSSIBILITY OF SUCH DAMAGE. +#[=======================================================================[.rst: +FindJPEGXL +--------- +Find JPEGXL headers and libraries. +Imported Targets +^^^^^^^^^^^^^^^^ +``JPEGXL::jxl`` + The JPEGXL library, if found. +Result Variables +^^^^^^^^^^^^^^^^ +This will define the following variables in your project: +``JPEGXL_FOUND`` + true if (the requested version of) JPEGXL is available. +``JPEGXL_VERSION`` + the version of JPEGXL. +``JPEGXL_LIBRARIES`` + the libraries to link against to use JPEGXL. +``JPEGXL_INCLUDE_DIRS`` + where to find the JPEGXL headers. +``JPEGXL_COMPILE_OPTIONS`` + this should be passed to target_compile_options(), if the + target is not used for linking +#]=======================================================================] + +if(NOT OPENCV_SKIP_JPEGXL_FIND_PACKAGE) +find_package(PkgConfig QUIET) +if (PkgConfig_FOUND) + pkg_check_modules(PC_JPEGXL QUIET jxl) + set(JPEGXL_COMPILE_OPTIONS ${PC_JPEGXL_CFLAGS_OTHER}) + set(JPEGXL_VERSION ${PC_JPEGXL_VERSION}) +endif () +find_path(JPEGXL_INCLUDE_DIR + NAMES jxl/decode.h + HINTS ${PC_JPEGXL_INCLUDEDIR} ${PC_JPEGXL_INCLUDE_DIRS} ${JPEGXL_INCLUDE_DIR} +) +find_library(JPEGXL_LIBRARY + NAMES ${JPEGXL_NAMES} jxl + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +find_library(JPEGXL_CMS_LIBRARY + NAMES ${JPEGXL_NAMES} jxl_cms + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_CMS_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_CMS_LIBRARY}) +endif () +find_library(JPEGXL_CMS2_LIBRARY + NAMES ${JPEGXL_NAMES} lcms2 + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_CMS2_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_CMS2_LIBRARY}) +endif () +find_library(JPEGXL_THREADS_LIBRARY + NAMES ${JPEGXL_NAMES} jxl_threads + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_THREADS_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_THREADS_LIBRARY}) +endif () +find_library(JPEGXL_BROTLICOMMON_LIBRARY + NAMES ${JPEGXL_NAMES} brotlicommon + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +find_library(JPEGXL_BROTLIDEC_LIBRARY + NAMES ${JPEGXL_NAMES} brotlidec + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_BROTLIDEC_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_BROTLIDEC_LIBRARY}) +endif () +find_library(JPEGXL_BROTLIENC_LIBRARY + NAMES ${JPEGXL_NAMES} brotlienc + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_BROTLIENC_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_BROTLIENC_LIBRARY}) +endif () +if (JPEGXL_LIBRARY AND JPEGXL_BROTLICOMMON_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_BROTLICOMMON_LIBRARY}) +endif () +find_library(JPEGXL_HWY_LIBRARY + NAMES ${JPEGXL_NAMES} hwy + HINTS ${PC_JPEGXL_LIBDIR} ${PC_JPEGXL_LIBRARY_DIRS} +) +if (JPEGXL_LIBRARY AND JPEGXL_HWY_LIBRARY) + set(JPEGXL_LIBRARY ${JPEGXL_LIBRARY} ${JPEGXL_HWY_LIBRARY}) +endif () +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(JPEGXL + FOUND_VAR JPEGXL_FOUND + REQUIRED_VARS JPEGXL_LIBRARY JPEGXL_INCLUDE_DIR + VERSION_VAR JPEGXL_VERSION +) +if (NOT EXISTS "${JPEGXL_INCLUDE_DIR}/jxl/version.h") + # the library version older 0.6 is not supported (no version.h file there) + set(JPEGXL_FOUND FALSE CACHE BOOL "libjxl found" FORCE) + message(STATUS "Ignored incompatible version of libjxl") +endif () + +if (JPEGXL_LIBRARY AND NOT TARGET JPEGXL::jxl) + add_library(JPEGXL::jxl UNKNOWN IMPORTED GLOBAL) + set_target_properties(JPEGXL::jxl PROPERTIES + IMPORTED_LOCATION "${JPEGXL_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${JPEGXL_COMPILE_OPTIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${JPEGXL_INCLUDE_DIR}" + ) +endif () +mark_as_advanced(JPEGXL_INCLUDE_DIR JPEGXL_LIBRARY) +if (JPEGXL_FOUND) + set(JPEGXL_LIBRARIES ${JPEGXL_LIBRARY}) + set(JPEGXL_INCLUDE_DIRS ${JPEGXL_INCLUDE_DIR}) + if (NOT JPEGXL_VERSION) + file(READ "${JPEGXL_INCLUDE_DIR}/jxl/version.h" VERSION_HEADER_CONTENTS) + string(REGEX MATCH "#define JPEGXL_MAJOR_VERSION ([0-9]+)" _ ${VERSION_HEADER_CONTENTS}) + set(JXL_VERSION_MAJOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define JPEGXL_MINOR_VERSION ([0-9]+)" _ ${VERSION_HEADER_CONTENTS}) + set(JXL_VERSION_MINOR ${CMAKE_MATCH_1}) + string(REGEX MATCH "#define JPEGXL_PATCH_VERSION ([0-9]+)" _ ${VERSION_HEADER_CONTENTS}) + set(JXL_VERSION_PATCH ${CMAKE_MATCH_1}) + set(JPEGXL_VERSION "${JXL_VERSION_MAJOR}.${JXL_VERSION_MINOR}.${JXL_VERSION_PATCH}") + endif() +endif () +endif() diff --git a/cmake/OpenCVFindLibsGrfmt.cmake b/cmake/OpenCVFindLibsGrfmt.cmake index a034a37431..6e53c45ba6 100644 --- a/cmake/OpenCVFindLibsGrfmt.cmake +++ b/cmake/OpenCVFindLibsGrfmt.cmake @@ -226,6 +226,17 @@ if(NOT WEBP_VERSION AND WEBP_INCLUDE_DIR) set(WEBP_VERSION "decoder: ${WEBP_DECODER_ABI_VERSION}, encoder: ${WEBP_ENCODER_ABI_VERSION}, demux: ${WEBP_DEMUX_ABI_VERSION}") endif() +# --- libjxl (optional) --- +if(WITH_JPEGXL) + ocv_clear_vars(HAVE_JPEGXL) + ocv_clear_internal_cache_vars(JPEGXL_INCLUDE_PATHS JPEGXL_LIBRARIES JPEGXL_VERSION) + include("${OpenCV_SOURCE_DIR}/cmake/OpenCVFindJPEGXL.cmake") + if(JPEGXL_FOUND) + set(HAVE_JPEGXL YES) + message(STATUS "Found system JPEG-XL: ver ${JPEGXL_VERSION}") + endif() +endif() + # --- libopenjp2 (optional, check before libjasper) --- if(WITH_OPENJPEG) if(BUILD_OPENJPEG) diff --git a/cmake/templates/cvconfig.h.in b/cmake/templates/cvconfig.h.in index ed53f3bf44..91efb9ab3d 100644 --- a/cmake/templates/cvconfig.h.in +++ b/cmake/templates/cvconfig.h.in @@ -78,6 +78,9 @@ /* IJG JPEG codec */ #cmakedefine HAVE_JPEG +/* JPEG XL codec */ +#cmakedefine HAVE_JPEGXL + /* GDCM DICOM codec */ #cmakedefine HAVE_GDCM diff --git a/doc/tutorials/introduction/config_reference/config_reference.markdown b/doc/tutorials/introduction/config_reference/config_reference.markdown index ab8bdee229..3c3c1d5c99 100644 --- a/doc/tutorials/introduction/config_reference/config_reference.markdown +++ b/doc/tutorials/introduction/config_reference/config_reference.markdown @@ -310,11 +310,12 @@ Following formats can be read by OpenCV without help of any third-party library: | [JPEG2000 with OpenJPEG](https://en.wikipedia.org/wiki/OpenJPEG) | `WITH_OPENJPEG` | _ON_ | `BUILD_OPENJPEG` | | [JPEG2000 with JasPer](https://en.wikipedia.org/wiki/JasPer) | `WITH_JASPER` | _ON_ (see note) | `BUILD_JASPER` | | [EXR](https://en.wikipedia.org/wiki/OpenEXR) | `WITH_OPENEXR` | _ON_ | `BUILD_OPENEXR` | +| [JPEG XL](https://en.wikipedia.org/wiki/JPEG_XL) | `WITH_JPEGXL` | _ON_ | Not supported. (see note) | All libraries required to read images in these formats are included into OpenCV and will be built automatically if not found at the configuration stage. Corresponding `BUILD_*` options will force building and using own libraries, they are enabled by default on some platforms, e.g. Windows. @note OpenJPEG have higher priority than JasPer which is deprecated. In order to use JasPer, OpenJPEG must be disabled. - +@note (JPEG XL) OpenCV doesn't contain libjxl source code, so `BUILD_JPEGXL` is not supported. ### GDAL integration diff --git a/modules/highgui/src/window_w32.cpp b/modules/highgui/src/window_w32.cpp index fa9e35bd89..2543c81c6a 100644 --- a/modules/highgui/src/window_w32.cpp +++ b/modules/highgui/src/window_w32.cpp @@ -2158,6 +2158,9 @@ static void showSaveDialog(CvWindow& window) #ifdef HAVE_JPEG "JPEG files (*.jpeg;*.jpg;*.jpe)\0*.jpeg;*.jpg;*.jpe\0" #endif +#ifdef HAVE_JPEGXL + "JPEG XL files (*.jxl)\0*.jxl\0" +#endif #ifdef HAVE_TIFF "TIFF Files (*.tiff;*.tif)\0*.tiff;*.tif\0" #endif diff --git a/modules/imgcodecs/CMakeLists.txt b/modules/imgcodecs/CMakeLists.txt index 0c52ce7cfb..7f97dde391 100644 --- a/modules/imgcodecs/CMakeLists.txt +++ b/modules/imgcodecs/CMakeLists.txt @@ -23,6 +23,11 @@ if(HAVE_JPEG) list(APPEND GRFMT_LIBS ${JPEG_LIBRARIES}) endif() +if(HAVE_JPEGXL) + ocv_include_directories(${OPENJPEG_INCLUDE_DIRS}) + list(APPEND GRFMT_LIBS ${OPENJPEG_LIBRARIES}) +endif() + if(HAVE_WEBP) add_definitions(-DHAVE_WEBP) ocv_include_directories(${WEBP_INCLUDE_DIR}) @@ -51,6 +56,12 @@ if(HAVE_TIFF) list(APPEND GRFMT_LIBS ${TIFF_LIBRARIES}) endif() +if(HAVE_JPEGXL) + ocv_include_directories(${JPEGXL_INCLUDE_DIRS}) + message(STATUS "JPEGXL_INCLUDE_DIRS: ${JPEGXL_INCLUDE_DIRS}") + list(APPEND GRFMT_LIBS ${JPEGXL_LIBRARIES}) +endif() + if(HAVE_OPENJPEG) ocv_include_directories(${OPENJPEG_INCLUDE_DIRS}) list(APPEND GRFMT_LIBS ${OPENJPEG_LIBRARIES}) @@ -78,7 +89,7 @@ if(HAVE_OPENEXR) endif() endif() -if(HAVE_PNG OR HAVE_TIFF OR HAVE_OPENEXR OR HAVE_SPNG) +if(HAVE_PNG OR HAVE_TIFF OR HAVE_OPENEXR OR HAVE_SPNG OR HAVE_JPEGXL) ocv_include_directories(${ZLIB_INCLUDE_DIRS}) list(APPEND GRFMT_LIBS ${ZLIB_LIBRARIES}) endif() diff --git a/modules/imgcodecs/include/opencv2/imgcodecs.hpp b/modules/imgcodecs/include/opencv2/imgcodecs.hpp index 25273342d2..cd648c2c6e 100644 --- a/modules/imgcodecs/include/opencv2/imgcodecs.hpp +++ b/modules/imgcodecs/include/opencv2/imgcodecs.hpp @@ -112,13 +112,17 @@ enum ImwriteFlags { IMWRITE_AVIF_QUALITY = 512,//!< For AVIF, it can be a quality between 0 and 100 (the higher the better). Default is 95. IMWRITE_AVIF_DEPTH = 513,//!< For AVIF, it can be 8, 10 or 12. If >8, it is stored/read as CV_32F. Default is 8. IMWRITE_AVIF_SPEED = 514,//!< For AVIF, it is between 0 (slowest) and (fastest). Default is 9. + IMWRITE_JPEGXL_QUALITY = 640,//!< For JPEG XL, it can be a quality from 0 to 100 (the higher is the better). Default value is 95. If set, distance parameter is re-calicurated from quality level automatically. This parameter request libjxl v0.10 or later. + IMWRITE_JPEGXL_EFFORT = 641,//!< For JPEG XL, encoder effort/speed level without affecting decoding speed; it is between 1 (fastest) and 10 (slowest). Default is 7. + IMWRITE_JPEGXL_DISTANCE = 642,//!< For JPEG XL, distance level for lossy compression: target max butteraugli distance, lower = higher quality, 0 = lossless; range: 0 .. 25. Default is 1. + IMWRITE_JPEGXL_DECODING_SPEED = 643,//!< For JPEG XL, decoding speed tier for the provided options; minimum is 0 (slowest to decode, best quality/density), and maximum is 4 (fastest to decode, at the cost of some quality/density). Default is 0. IMWRITE_GIF_LOOP = 1024,//!< For GIF, it can be a loop flag from 0 to 65535. Default is 0 - loop forever. IMWRITE_GIF_SPEED = 1025,//!< For GIF, it is between 1 (slowest) and 100 (fastest). Default is 96. IMWRITE_GIF_QUALITY = 1026, //!< For GIF, it can be a quality from 1 to 8. Default is 2. See cv::ImwriteGifCompressionFlags. IMWRITE_GIF_DITHER = 1027, //!< For GIF, it can be a quality from -1(most dither) to 3(no dither). Default is 0. IMWRITE_GIF_TRANSPARENCY = 1028, //!< For GIF, the alpha channel lower than this will be set to transparent. Default is 1. IMWRITE_GIF_COLORTABLE = 1029 //!< For GIF, 0 means global color table is used, 1 means local color table is used. Default is 0. - }; +}; enum ImwriteJPEGSamplingFactorParams { IMWRITE_JPEG_SAMPLING_FACTOR_411 = 0x411111, //!< 4x1,1x1,1x1 @@ -407,6 +411,10 @@ can be saved using this function, with these exceptions: - With Radiance HDR encoder, non 64-bit float (CV_64F) images can be saved. - All images will be converted to 32-bit float (CV_32F). - With JPEG 2000 encoder, 8-bit unsigned (CV_8U) and 16-bit unsigned (CV_16U) images can be saved. +- With JPEG XL encoder, 8-bit unsigned (CV_8U), 16-bit unsigned (CV_16U) and 32-bit float(CV_32F) images can be saved. + - JPEG XL images with an alpha channel can be saved using this function. + To do this, create 8-bit (or 16-bit, 32-bit float) 4-channel image BGRA, where the alpha channel goes last. + Fully transparent pixels should have alpha set to 0, fully opaque pixels should have alpha set to 255/65535/1.0. - With PAM encoder, 8-bit unsigned (CV_8U) and 16-bit unsigned (CV_16U) images can be saved. - With PNG encoder, 8-bit unsigned (CV_8U) and 16-bit unsigned (CV_16U) images can be saved. - PNG images with an alpha channel can be saved using this function. To do this, create diff --git a/modules/imgcodecs/src/grfmt_jpegxl.cpp b/modules/imgcodecs/src/grfmt_jpegxl.cpp new file mode 100644 index 0000000000..fc89dd2c6e --- /dev/null +++ b/modules/imgcodecs/src/grfmt_jpegxl.cpp @@ -0,0 +1,420 @@ +// 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. +#include "precomp.hpp" +#include "grfmt_jpegxl.hpp" + +#ifdef HAVE_JPEGXL + +#include +#include +#include + +namespace cv +{ + +/////////////////////// JpegXLDecoder /////////////////// + +JpegXLDecoder::JpegXLDecoder() : m_f(nullptr, &fclose) +{ + m_signature = "\xFF\x0A"; + m_decoder = nullptr; + m_buf_supported = false; + m_type = m_convert = -1; + m_status = JXL_DEC_NEED_MORE_INPUT; +} + +JpegXLDecoder::~JpegXLDecoder() +{ + close(); +} + +void JpegXLDecoder::close() +{ + if (m_decoder) + m_decoder.release(); + if (m_f) + m_f.release(); + m_read_buffer = {}; + m_width = m_height = 0; + m_type = m_convert = -1; + m_status = JXL_DEC_NEED_MORE_INPUT; +} + +// see https://github.com/libjxl/libjxl/blob/v0.10.0/doc/format_overview.md +size_t JpegXLDecoder::signatureLength() const +{ + return 12; // For an ISOBMFF-based container +} + +bool JpegXLDecoder::checkSignature( const String& signature ) const +{ + // A "naked" codestream. + if ( + ( signature.size() >= 2 ) && + ( memcmp( signature.c_str(), "\xFF\x0A", 2 ) == 0 ) + ) + { + return true; + } + + // An ISOBMFF-based container. + // 0x0000_000C_4A58_4C20_0D0A_870A. + if ( + ( signature.size() >= 12 ) && + ( memcmp( signature.c_str(), "\x00\x00\x00\x0C\x4A\x58\x4C\x20\x0D\x0A\x87\x0A", 12 ) == 0 ) + ) + { + return true; + } + + return false; +} + +ImageDecoder JpegXLDecoder::newDecoder() const +{ + return makePtr(); +} + +bool JpegXLDecoder::read(Mat* pimg) +{ + // Open file + if (!m_f) { + m_f.reset(fopen(m_filename.c_str(), "rb")); + if (!m_f) + return false; + } + + // Initialize decoder + if (!m_decoder) { + m_decoder = JxlDecoderMake(nullptr); + if (!m_decoder) + return false; + // Subscribe to the basic info event + JxlDecoderStatus status = JxlDecoderSubscribeEvents(m_decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE); + if (status != JXL_DEC_SUCCESS) + return false; + } + + // Set up parallel m_parallel_runner + if (!m_parallel_runner) { + m_parallel_runner = JxlThreadParallelRunnerMake(nullptr, cv::getNumThreads()); + if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(m_decoder.get(), + JxlThreadParallelRunner, + m_parallel_runner.get())) { + return false; + } + } + + // Create buffer for reading + const size_t read_buffer_size = 16384; // 16KB chunks + if (m_read_buffer.capacity() < read_buffer_size) + m_read_buffer.resize(read_buffer_size); + + // Create image if needed + if (m_type != -1 && pimg) { + pimg->create(m_height, m_width, m_type); + if (!pimg->isContinuous()) + return false; + if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(m_decoder.get(), + &m_format, + pimg->ptr(), + pimg->total() * pimg->elemSize())) { + return false; + } + } + + // Start decoding loop + do { + // Check if we need more input + if (m_status == JXL_DEC_NEED_MORE_INPUT) { + size_t remaining = JxlDecoderReleaseInput(m_decoder.get()); + // Move any remaining bytes to the beginning + if (remaining > 0) + memmove(m_read_buffer.data(), m_read_buffer.data() + m_read_buffer.size() - remaining, remaining); + // Read more data from file + size_t bytes_read = fread(m_read_buffer.data() + remaining, + 1, m_read_buffer.size() - remaining, m_f.get()); + if (bytes_read == 0) { + if (ferror(m_f.get())) { + CV_LOG_WARNING(NULL, "Error reading input file"); + return false; + } + // If we reached EOF but decoder needs more input, file is truncated + if (m_status == JXL_DEC_NEED_MORE_INPUT) { + CV_LOG_WARNING(NULL, "Truncated JXL file"); + return false; + } + } + + // Set input buffer + if (JXL_DEC_SUCCESS != JxlDecoderSetInput(m_decoder.get(), + m_read_buffer.data(), + bytes_read + remaining)) { + return false; + } + } + + // Get the next decoder status + m_status = JxlDecoderProcessInput(m_decoder.get()); + + // Handle different decoder states + switch (m_status) { + case JXL_DEC_BASIC_INFO: { + if (m_type != -1) + return false; + JxlBasicInfo info; + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(m_decoder.get(), &info)) + return false; + + // total channels (Color + Alpha) + const uint32_t ncn = info.num_color_channels + info.num_extra_channels; + + m_width = info.xsize; + m_height = info.ysize; + m_format = { + ncn, + JXL_TYPE_UINT8, // (temporary) + JXL_LITTLE_ENDIAN, // endianness + 0 // align stride to bytes + }; + if (!m_use_rgb) { + switch (ncn) { + case 3: + m_convert = cv::COLOR_RGB2BGR; + break; + case 4: + m_convert = cv::COLOR_RGBA2BGRA; + break; + default: + m_convert = -1; + } + } + if (info.exponent_bits_per_sample > 0) { + m_format.data_type = JXL_TYPE_FLOAT; + m_type = CV_MAKETYPE( CV_32F, ncn ); + } else { + switch (info.bits_per_sample) { + case 8: + m_format.data_type = JXL_TYPE_UINT8; + m_type = CV_MAKETYPE( CV_8U, ncn ); + break; + case 16: + m_format.data_type = JXL_TYPE_UINT16; + m_type = CV_MAKETYPE( CV_16U, ncn ); + break; + default: + return false; + } + } + if (!pimg) + return true; + break; + } + case JXL_DEC_FULL_IMAGE: { + // Image is ready + if (m_convert != -1) + cv::cvtColor(*pimg, *pimg, m_convert); + break; + } + case JXL_DEC_ERROR: { + close(); + return false; + } + default: + break; + } + } while (m_status != JXL_DEC_SUCCESS); + + return true; +} + +bool JpegXLDecoder::readHeader() +{ + close(); + return read(nullptr); +} + +bool JpegXLDecoder::readData(Mat& img) +{ + if (!m_decoder || m_width == 0 || m_height == 0) + return false; + return read(&img); +} + +/////////////////////// JpegXLEncoder /////////////////// + +JpegXLEncoder::JpegXLEncoder() +{ + m_description = "JPEG XL files (*.jxl)"; + m_buf_supported = true; +} + +JpegXLEncoder::~JpegXLEncoder() +{ +} + +ImageEncoder JpegXLEncoder::newEncoder() const +{ + return makePtr(); +} + +bool JpegXLEncoder::isFormatSupported( int depth ) const +{ + return depth == CV_8U || depth == CV_16U || depth == CV_32F; +} + +bool JpegXLEncoder::write(const Mat& img, const std::vector& params) +{ + m_last_error.clear(); + + JxlEncoderPtr encoder = JxlEncoderMake(nullptr); + if (!encoder) + return false; + + JxlThreadParallelRunnerPtr runner = JxlThreadParallelRunnerMake( + /*memory_manager=*/nullptr, cv::getNumThreads()); + if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(encoder.get(), JxlThreadParallelRunner, runner.get())) + return false; + + CV_CheckDepth(img.depth(), + ( img.depth() == CV_8U || img.depth() == CV_16U || img.depth() == CV_32F ), + "JPEG XL encoder only supports CV_8U, CV_16U, CV_32F"); + CV_CheckChannels(img.channels(), + ( img.channels() == 1 || img.channels() == 3 || img.channels() == 4) , + "JPEG XL encoder only supports 1, 3, 4 channels"); + + WLByteStream strm; + if( m_buf ) { + if( !strm.open( *m_buf ) ) + return false; + } + else if( !strm.open( m_filename )) { + return false; + } + + JxlBasicInfo info; + JxlEncoderInitBasicInfo(&info); + info.xsize = img.cols; + info.ysize = img.rows; + info.uses_original_profile = JXL_FALSE; + + if( img.channels() == 4 ) + { + info.num_color_channels = 3; + info.num_extra_channels = 1; + + info.bits_per_sample = + info.alpha_bits = 8 * static_cast(img.elemSize1()); + + info.exponent_bits_per_sample = + info.alpha_exponent_bits = img.depth() == CV_32F ? 8 : 0; + }else{ + info.num_color_channels = img.channels(); + info.bits_per_sample = 8 * static_cast(img.elemSize1()); + info.exponent_bits_per_sample = img.depth() == CV_32F ? 8 : 0; + } + + if (JxlEncoderSetBasicInfo(encoder.get(), &info) != JXL_ENC_SUCCESS) + return false; + + JxlDataType type = JXL_TYPE_UINT8; + if (img.depth() == CV_32F) + type = JXL_TYPE_FLOAT; + else if (img.depth() == CV_16U) + type = JXL_TYPE_UINT16; + JxlPixelFormat format = {(uint32_t)img.channels(), type, JXL_NATIVE_ENDIAN, 0}; + JxlColorEncoding color_encoding = {}; + JXL_BOOL is_gray(format.num_channels < 3 ? JXL_TRUE : JXL_FALSE); + JxlColorEncodingSetToSRGB(&color_encoding, is_gray); + if (JXL_ENC_SUCCESS != JxlEncoderSetColorEncoding(encoder.get(), &color_encoding)) + return false; + + Mat image; + switch ( img.channels() ) { + case 3: + cv::cvtColor(img, image, cv::COLOR_BGR2RGB); + break; + case 4: + cv::cvtColor(img, image, cv::COLOR_BGRA2RGBA); + break; + case 1: + default: + if(img.isContinuous()) { + image = img; + } else { + image = img.clone(); // reconstruction as continuous image. + } + break; + } + if (!image.isContinuous()) + return false; + + JxlEncoderFrameSettings* frame_settings = JxlEncoderFrameSettingsCreate(encoder.get(), nullptr); + // set frame settings from params if available + for( size_t i = 0; i < params.size(); i += 2 ) + { + if( params[i] == IMWRITE_JPEGXL_QUALITY ) + { +#if JPEGXL_MAJOR_VERSION > 0 || JPEGXL_MINOR_VERSION >= 10 + int quality = params[i+1]; + quality = MIN(MAX(quality, 0), 100); + const float distance = JxlEncoderDistanceFromQuality(static_cast(quality)); + JxlEncoderSetFrameDistance(frame_settings, distance); + if (distance == 0) + JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE); +#else + CV_LOG_ONCE_WARNING(NULL, "Quality parameter is supported with libjxl v0.10.0 or later"); +#endif + } + if( params[i] == IMWRITE_JPEGXL_DISTANCE ) + { + int distance = params[i+1]; + distance = MIN(MAX(distance, 0), 25); + JxlEncoderSetFrameDistance(frame_settings, distance); + if (distance == 0) + JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE); + } + if( params[i] == IMWRITE_JPEGXL_EFFORT ) + { + int effort = params[i+1]; + effort = MIN(MAX(effort, 1), 10); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, effort); + } + if( params[i] == IMWRITE_JPEGXL_DECODING_SPEED ) + { + int speed = params[i+1]; + speed = MIN(MAX(speed, 0), 4); + JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_DECODING_SPEED, speed); + } + } + if (JXL_ENC_SUCCESS != + JxlEncoderAddImageFrame(frame_settings, &format, + static_cast(image.ptr()), + image.total() * image.elemSize())) { + return false; + } + JxlEncoderCloseInput(encoder.get()); + + const size_t buffer_size = 16384; // 16KB chunks + + std::vector compressed(buffer_size); + JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; + while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { + uint8_t* next_out = compressed.data(); + size_t avail_out = buffer_size; + process_result = JxlEncoderProcessOutput(encoder.get(), &next_out, &avail_out); + if (JXL_ENC_ERROR == process_result) + return false; + const size_t write_size = buffer_size - avail_out; + if ( strm.putBytes(compressed.data(), write_size) == false ) + return false; + } + return true; +} + +} + +#endif + +/* End of file. */ diff --git a/modules/imgcodecs/src/grfmt_jpegxl.hpp b/modules/imgcodecs/src/grfmt_jpegxl.hpp new file mode 100644 index 0000000000..87e4bfcba5 --- /dev/null +++ b/modules/imgcodecs/src/grfmt_jpegxl.hpp @@ -0,0 +1,68 @@ +// 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. +#ifndef _GRFMT_JPEGXL_H_ +#define _GRFMT_JPEGXL_H_ + +#ifdef HAVE_JPEGXL + +#include "grfmt_base.hpp" +#include +#include +#include +#include + +// Jpeg XL codec + +namespace cv +{ + +/** +* @brief JpegXL codec using libjxl library +*/ + +class JpegXLDecoder CV_FINAL : public BaseImageDecoder +{ +public: + + JpegXLDecoder(); + virtual ~JpegXLDecoder(); + + bool readData( Mat& img ) CV_OVERRIDE; + bool readHeader() CV_OVERRIDE; + void close(); + size_t signatureLength() const CV_OVERRIDE; + bool checkSignature( const String& signature ) const CV_OVERRIDE; + + ImageDecoder newDecoder() const CV_OVERRIDE; + +protected: + std::unique_ptr m_f; + JxlDecoderPtr m_decoder; + JxlThreadParallelRunnerPtr m_parallel_runner; + JxlPixelFormat m_format; + int m_convert; + std::vector m_read_buffer; + JxlDecoderStatus m_status; + +private: + bool read(Mat* pimg); +}; + + +class JpegXLEncoder CV_FINAL : public BaseImageEncoder +{ +public: + JpegXLEncoder(); + virtual ~JpegXLEncoder(); + + bool isFormatSupported( int depth ) const CV_OVERRIDE; + bool write( const Mat& img, const std::vector& params ) CV_OVERRIDE; + ImageEncoder newEncoder() const CV_OVERRIDE; +}; + +} + +#endif + +#endif/*_GRFMT_JPEGXL_H_*/ diff --git a/modules/imgcodecs/src/grfmts.hpp b/modules/imgcodecs/src/grfmts.hpp index 198588630c..9fcc27740c 100644 --- a/modules/imgcodecs/src/grfmts.hpp +++ b/modules/imgcodecs/src/grfmts.hpp @@ -48,6 +48,7 @@ #include "grfmt_gif.hpp" #include "grfmt_sunras.hpp" #include "grfmt_jpeg.hpp" +#include "grfmt_jpegxl.hpp" #include "grfmt_pxm.hpp" #include "grfmt_pfm.hpp" #include "grfmt_tiff.hpp" diff --git a/modules/imgcodecs/src/loadsave.cpp b/modules/imgcodecs/src/loadsave.cpp index e044554350..ec25f8c610 100644 --- a/modules/imgcodecs/src/loadsave.cpp +++ b/modules/imgcodecs/src/loadsave.cpp @@ -210,6 +210,10 @@ struct ImageCodecInitializer decoders.push_back( makePtr() ); encoders.push_back( makePtr() ); #endif + #ifdef HAVE_JPEGXL + decoders.push_back( makePtr() ); + encoders.push_back( makePtr() ); + #endif #ifdef HAVE_OPENJPEG decoders.push_back( makePtr() ); decoders.push_back( makePtr() ); diff --git a/modules/imgcodecs/test/test_jpegxl.cpp b/modules/imgcodecs/test/test_jpegxl.cpp new file mode 100644 index 0000000000..6c5aad181f --- /dev/null +++ b/modules/imgcodecs/test/test_jpegxl.cpp @@ -0,0 +1,186 @@ +// 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 +#include "test_precomp.hpp" + +namespace opencv_test { namespace { + +#ifdef HAVE_JPEGXL + +typedef tuple MatType_and_Distance; +typedef testing::TestWithParam Imgcodecs_JpegXL_MatType; + +TEST_P(Imgcodecs_JpegXL_MatType, write_read) +{ + const int matType = get<0>(GetParam()); + const int distanceParam = get<1>(GetParam()); + + cv::Scalar col; + // Jpeg XL is lossy compression. + // There may be small differences in decoding results by environments. + double th; + + switch( CV_MAT_DEPTH(matType) ) + { + case CV_16U: + col = cv::Scalar(124 * 256, 76 * 256, 42 * 256, 192 * 256 ); + th = 656; // = 65535 / 100; + break; + case CV_32F: + col = cv::Scalar(0.486, 0.298, 0.165, 0.75); + th = 1.0 / 100.0; + break; + default: + case CV_8U: + col = cv::Scalar(124, 76, 42, 192); + th = 3; // = 255 / 100 (1%); + break; + } + + // If increasing distanceParam, threshold should be increased. + th *= (distanceParam >= 25) ? 5 : ( distanceParam > 2 ) ? 3 : (distanceParam == 2) ? 2: 1; + + bool ret = false; + string tmp_fname = cv::tempfile(".jxl"); + Mat img_org(320, 480, matType, col); + vector param; + param.push_back(IMWRITE_JPEGXL_DISTANCE); + param.push_back(distanceParam); + EXPECT_NO_THROW(ret = imwrite(tmp_fname, img_org, param)); + EXPECT_TRUE(ret); + Mat img_decoded; + EXPECT_NO_THROW(img_decoded = imread(tmp_fname, IMREAD_UNCHANGED)); + EXPECT_FALSE(img_decoded.empty()); + + EXPECT_LE(cvtest::norm(img_org, img_decoded, NORM_INF), th); + + EXPECT_EQ(0, remove(tmp_fname.c_str())); +} + +TEST_P(Imgcodecs_JpegXL_MatType, encode_decode) +{ + const int matType = get<0>(GetParam()); + const int distanceParam = get<1>(GetParam()); + + cv::Scalar col; + // Jpeg XL is lossy compression. + // There may be small differences in decoding results by environments. + double th; + + // If alpha=0, libjxl modify color channels(BGR). So do not set it. + switch( CV_MAT_DEPTH(matType) ) + { + case CV_16U: + col = cv::Scalar(124 * 256, 76 * 256, 42 * 256, 192 * 256 ); + th = 656; // = 65535 / 100; + break; + case CV_32F: + col = cv::Scalar(0.486, 0.298, 0.165, 0.75); + th = 1.0 / 100.0; + break; + default: + case CV_8U: + col = cv::Scalar(124, 76, 42, 192); + th = 3; // = 255 / 100 (1%); + break; + } + + // If increasing distanceParam, threshold should be increased. + th *= (distanceParam >= 25) ? 5 : ( distanceParam > 2 ) ? 3 : (distanceParam == 2) ? 2: 1; + + bool ret = false; + vector buff; + Mat img_org(320, 480, matType, col); + vector param; + param.push_back(IMWRITE_JPEGXL_DISTANCE); + param.push_back(distanceParam); + EXPECT_NO_THROW(ret = imencode(".jxl", img_org, buff, param)); + EXPECT_TRUE(ret); + Mat img_decoded; + EXPECT_NO_THROW(img_decoded = imdecode(buff, IMREAD_UNCHANGED)); + EXPECT_FALSE(img_decoded.empty()); + + EXPECT_LE(cvtest::norm(img_org, img_decoded, NORM_INF), th); +} + +INSTANTIATE_TEST_CASE_P( + /**/, + Imgcodecs_JpegXL_MatType, + testing::Combine( + testing::Values( + CV_8UC1, CV_8UC3, CV_8UC4, + CV_16UC1, CV_16UC3, CV_16UC4, + CV_32FC1, CV_32FC3, CV_32FC4 + ), + testing::Values( // Distance + 0, // Lossless + 1, // Default + 3, // Recomended Lossy Max + 25 // Specification Max + ) +) ); + + +typedef tuple Effort_and_Decoding_speed; +typedef testing::TestWithParam Imgcodecs_JpegXL_Effort_DecodingSpeed; + +TEST_P(Imgcodecs_JpegXL_Effort_DecodingSpeed, encode_decode) +{ + const int effort = get<0>(GetParam()); + const int speed = get<1>(GetParam()); + + cv::Scalar col = cv::Scalar(124,76,42); + // Jpeg XL is lossy compression. + // There may be small differences in decoding results by environments. + double th = 3; // = 255 / 100 (1%); + + bool ret = false; + vector buff; + Mat img_org(320, 480, CV_8UC3, col); + vector param; + param.push_back(IMWRITE_JPEGXL_EFFORT); + param.push_back(effort); + param.push_back(IMWRITE_JPEGXL_DECODING_SPEED); + param.push_back(speed); + EXPECT_NO_THROW(ret = imencode(".jxl", img_org, buff, param)); + EXPECT_TRUE(ret); + Mat img_decoded; + EXPECT_NO_THROW(img_decoded = imdecode(buff, IMREAD_UNCHANGED)); + EXPECT_FALSE(img_decoded.empty()); + + EXPECT_LE(cvtest::norm(img_org, img_decoded, NORM_INF), th); +} + +INSTANTIATE_TEST_CASE_P( + /**/, + Imgcodecs_JpegXL_Effort_DecodingSpeed, + testing::Combine( + testing::Values( // Effort + 1, // fastest + 7, // default + 9 // slowest + ), + testing::Values( // Decoding Speed + 0, // default, slowest, and best quality/density + 2, + 4 // fastest, at the cost of some qulity/density + ) +) ); + +TEST(Imgcodecs_JpegXL, encode_from_uncontinued_image) +{ + cv::Mat src(100, 100, CV_8UC1, Scalar(40,50,10)); + cv::Mat roi = src(cv::Rect(10,20,30,50)); + EXPECT_FALSE(roi.isContinuous()); // uncontinued image + + vector buff; + vector param; + bool ret = false; + EXPECT_NO_THROW(ret = cv::imencode(".jxl", roi, buff, param)); + EXPECT_TRUE(ret); +} + +#endif // HAVE_JPEGXL + +} // namespace +} // namespace opencv_test diff --git a/modules/imgcodecs/test/test_read_write.cpp b/modules/imgcodecs/test/test_read_write.cpp index 7dfd02c67c..1e171d9e8d 100644 --- a/modules/imgcodecs/test/test_read_write.cpp +++ b/modules/imgcodecs/test/test_read_write.cpp @@ -157,6 +157,9 @@ const string exts[] = { #ifdef HAVE_JPEG "jpg", #endif +#ifdef HAVE_JPEGXL + "jxl", +#endif #if (defined(HAVE_JASPER) && defined(OPENCV_IMGCODECS_ENABLE_JASPER_TESTS)) \ || defined(HAVE_OPENJPEG) "jp2", @@ -238,6 +241,8 @@ TEST_P(Imgcodecs_Image, read_write_BGR) double psnrThreshold = 100; if (ext == "jpg") psnrThreshold = 32; + if (ext == "jxl") + psnrThreshold = 30; #if defined(HAVE_JASPER) if (ext == "jp2") psnrThreshold = 95; @@ -268,6 +273,8 @@ TEST_P(Imgcodecs_Image, read_write_GRAYSCALE) double psnrThreshold = 100; if (ext == "jpg") psnrThreshold = 40; + if (ext == "jxl") + psnrThreshold = 40; #if defined(HAVE_JASPER) if (ext == "jp2") psnrThreshold = 70;