mirror of
https://github.com/opencv/opencv.git
synced 2026-07-30 15:53:03 +04:00
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
This commit is contained in:
@@ -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 <jxl/encode_cxx.h>
|
||||
#include <jxl/version.h>
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
|
||||
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<JpegXLDecoder>();
|
||||
}
|
||||
|
||||
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<uint8_t>(),
|
||||
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<JpegXLEncoder>();
|
||||
}
|
||||
|
||||
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<int>& 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<int>(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<int>(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<float>(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<const void*>(image.ptr<uint8_t>()),
|
||||
image.total() * image.elemSize())) {
|
||||
return false;
|
||||
}
|
||||
JxlEncoderCloseInput(encoder.get());
|
||||
|
||||
const size_t buffer_size = 16384; // 16KB chunks
|
||||
|
||||
std::vector<uint8_t> 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. */
|
||||
@@ -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 <jxl/decode_cxx.h>
|
||||
#include <jxl/thread_parallel_runner_cxx.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
// 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<FILE, int (*)(FILE*)> m_f;
|
||||
JxlDecoderPtr m_decoder;
|
||||
JxlThreadParallelRunnerPtr m_parallel_runner;
|
||||
JxlPixelFormat m_format;
|
||||
int m_convert;
|
||||
std::vector<uint8_t> 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<int>& params ) CV_OVERRIDE;
|
||||
ImageEncoder newEncoder() const CV_OVERRIDE;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif/*_GRFMT_JPEGXL_H_*/
|
||||
@@ -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"
|
||||
|
||||
@@ -210,6 +210,10 @@ struct ImageCodecInitializer
|
||||
decoders.push_back( makePtr<Jpeg2KDecoder>() );
|
||||
encoders.push_back( makePtr<Jpeg2KEncoder>() );
|
||||
#endif
|
||||
#ifdef HAVE_JPEGXL
|
||||
decoders.push_back( makePtr<JpegXLDecoder>() );
|
||||
encoders.push_back( makePtr<JpegXLEncoder>() );
|
||||
#endif
|
||||
#ifdef HAVE_OPENJPEG
|
||||
decoders.push_back( makePtr<Jpeg2KJP2OpjDecoder>() );
|
||||
decoders.push_back( makePtr<Jpeg2KJ2KOpjDecoder>() );
|
||||
|
||||
Reference in New Issue
Block a user