libheif/color-conversion/colorconversion.cc (433 lines of code) (raw):
/*
* HEIF codec.
* Copyright (c) 2017 Dirk Farin <dirk.farin@gmail.com>
*
* This file is part of libheif.
*
* libheif is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* libheif is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libheif. If not, see <http://www.gnu.org/licenses/>.
*/
#include "colorconversion.h"
#include "common_utils.h"
#include "nclx.h"
#include <typeinfo>
#include <algorithm>
#include <cstring>
#include <cassert>
#include <iostream>
#include <set>
#include <cmath>
#include <limits>
#include <string>
#include "rgb2yuv.h"
#include "rgb2yuv_sharp.h"
#include "yuv2rgb.h"
#include "rgb2rgb.h"
#include "monochrome.h"
#include "alpha.h"
#include "hdr_sdr.h"
#include "chroma_sampling.h"
#if ENABLE_MULTITHREADING_SUPPORT
#include <mutex>
#endif
#define DEBUG_ME 0
#define DEBUG_PIPELINE_CREATION 0
#define USE_CENTER_CHROMA_422 0
std::ostream& operator<<(std::ostream& ostr, heif_colorspace c)
{
switch (c) {
case heif_colorspace_RGB:
ostr << "RGB";
break;
case heif_colorspace_YCbCr:
ostr << "YCbCr";
break;
case heif_colorspace_monochrome:
ostr << "mono";
break;
case heif_colorspace_undefined:
ostr << "undefined";
break;
default:
assert(false);
}
return ostr;
}
std::ostream& operator<<(std::ostream& ostr, heif_chroma c)
{
switch (c) {
case heif_chroma_420:
ostr << "420";
break;
case heif_chroma_422:
ostr << "422";
break;
case heif_chroma_444:
ostr << "444";
break;
case heif_chroma_monochrome:
ostr << "mono";
break;
case heif_chroma_interleaved_RGB:
ostr << "RGB";
break;
case heif_chroma_interleaved_RGBA:
ostr << "RGBA";
break;
case heif_chroma_interleaved_RRGGBB_BE:
ostr << "RRGGBB_BE";
break;
case heif_chroma_interleaved_RRGGBB_LE:
ostr << "RRGGBBB_LE";
break;
case heif_chroma_interleaved_RRGGBBAA_BE:
ostr << "RRGGBBAA_BE";
break;
case heif_chroma_interleaved_RRGGBBAA_LE:
ostr << "RRGGBBBAA_LE";
break;
case heif_chroma_undefined:
ostr << "undefined";
break;
default:
assert(false);
}
return ostr;
}
#if DEBUG_ME
static void __attribute__ ((unused)) print_spec(std::ostream& ostr, const std::shared_ptr<HeifPixelImage>& img)
{
ostr << "colorspace=" << img->get_colorspace()
<< " chroma=" << img->get_chroma_format();
if (img->get_colorspace() == heif_colorspace_RGB) {
if (img->get_chroma_format() == heif_chroma_444) {
ostr << " bpp(R)=" << ((int) img->get_bits_per_pixel(heif_channel_R));
}
else {
ostr << " bpp(interleaved)=" << ((int) img->get_bits_per_pixel(heif_channel_interleaved));
}
}
else if (img->get_colorspace() == heif_colorspace_YCbCr ||
img->get_colorspace() == heif_colorspace_monochrome) {
ostr << " bpp(Y)=" << ((int) img->get_bits_per_pixel(heif_channel_Y));
}
ostr << "\n";
}
#endif
bool ColorState::operator==(const ColorState& b) const
{
bool mainParamsMatch = (colorspace == b.colorspace &&
chroma == b.chroma &&
has_alpha == b.has_alpha &&
bits_per_pixel == b.bits_per_pixel);
if (!mainParamsMatch) {
return false;
}
if (colorspace == heif_colorspace_YCbCr) {
bool ycbcr_parameters_match = (nclx_profile.get_full_range_flag() == b.nclx_profile.get_full_range_flag() &&
nclx_profile.get_matrix_coefficients() == b.nclx_profile.get_matrix_coefficients() &&
nclx_profile.get_colour_primaries() == b.nclx_profile.get_colour_primaries());
return ycbcr_parameters_match;
}
else {
return true;
}
}
struct Node
{
Node() = default;
Node(int prev,
const std::shared_ptr<ColorConversionOperation>& _op,
//const ColorState& _input_state,
const ColorState& _output_state,
int _speed_cost)
{
prev_processed_idx = prev;
op = _op;
//input_state = _input_state;
output_state = _output_state;
speed_costs = _speed_cost;
}
int prev_processed_idx = -1;
std::shared_ptr<ColorConversionOperation> op;
//ColorState input_state;
ColorState output_state;
int speed_costs;
};
std::ostream& operator<<(std::ostream& ostr, const ColorState& state)
{
ostr << "colorspace=" << state.colorspace << " chroma=" << state.chroma
<< " bpp(R)=" << state.bits_per_pixel
<< " alpha=" << (state.has_alpha ? "yes" : "no");
if (state.colorspace == heif_colorspace_YCbCr) {
ostr << " matrix-coefficients=" << state.nclx_profile.get_matrix_coefficients()
<< " colour-primaries=" << state.nclx_profile.get_colour_primaries()
<< " transfer-characteristics=" << state.nclx_profile.get_transfer_characteristics()
<< " full-range=" << (state.nclx_profile.get_full_range_flag() ? "yes" : "no");
}
return ostr;
}
std::vector<std::shared_ptr<ColorConversionOperation>> ColorConversionPipeline::m_operation_pool;
void ColorConversionPipeline::init_ops()
{
#if ENABLE_MULTITHREADING_SUPPORT
static std::mutex init_ops_mutex;
std::lock_guard<std::mutex> lock(init_ops_mutex);
#endif
if (!m_operation_pool.empty()) {
return;
}
std::vector<std::shared_ptr<ColorConversionOperation>>& ops = m_operation_pool;
ops.push_back(std::make_shared<Op_RGB_to_RGB24_32>());
ops.push_back(std::make_shared<Op_RGB24_32_to_RGB>());
ops.push_back(std::make_shared<Op_YCbCr_to_RGB<uint16_t>>());
ops.push_back(std::make_shared<Op_YCbCr_to_RGB<uint8_t>>());
ops.push_back(std::make_shared<Op_YCbCr420_to_RGB24>());
ops.push_back(std::make_shared<Op_YCbCr420_to_RGB32>());
ops.push_back(std::make_shared<Op_YCbCr420_to_RRGGBBaa>());
ops.push_back(std::make_shared<Op_RGB_HDR_to_RRGGBBaa_BE>());
ops.push_back(std::make_shared<Op_RGB_to_RRGGBBaa_BE>());
ops.push_back(std::make_shared<Op_mono_to_YCbCr420>());
ops.push_back(std::make_shared<Op_mono_to_RGB24_32>());
ops.push_back(std::make_shared<Op_RRGGBBaa_swap_endianness>());
ops.push_back(std::make_shared<Op_RRGGBBaa_BE_to_RGB_HDR>());
ops.push_back(std::make_shared<Op_RGB24_32_to_YCbCr>());
ops.push_back(std::make_shared<Op_RGB_to_YCbCr<uint8_t>>());
ops.push_back(std::make_shared<Op_RGB_to_YCbCr<uint16_t>>());
ops.push_back(std::make_shared<Op_RRGGBBxx_HDR_to_YCbCr420>());
ops.push_back(std::make_shared<Op_RGB24_32_to_YCbCr444_GBR>());
ops.push_back(std::make_shared<Op_drop_alpha_plane>());
ops.push_back(std::make_shared<Op_to_hdr_planes>());
ops.push_back(std::make_shared<Op_to_sdr_planes>());
ops.push_back(std::make_shared<Op_YCbCr420_bilinear_to_YCbCr444<uint8_t>>());
ops.push_back(std::make_shared<Op_YCbCr420_bilinear_to_YCbCr444<uint16_t>>());
ops.push_back(std::make_shared<Op_YCbCr422_bilinear_to_YCbCr444<uint8_t>>());
ops.push_back(std::make_shared<Op_YCbCr422_bilinear_to_YCbCr444<uint16_t>>());
ops.push_back(std::make_shared<Op_YCbCr444_to_YCbCr420_average<uint8_t>>());
ops.push_back(std::make_shared<Op_YCbCr444_to_YCbCr420_average<uint16_t>>());
ops.push_back(std::make_shared<Op_YCbCr444_to_YCbCr422_average<uint8_t>>());
ops.push_back(std::make_shared<Op_YCbCr444_to_YCbCr422_average<uint16_t>>());
ops.push_back(std::make_shared<Op_Any_RGB_to_YCbCr_420_Sharp>());
#if HAVE_YUV
ops.push_back(std::make_shared<Op_YCbCr_to_RGBA_GENERAL>());
#endif
ops.push_back(std::make_shared<Op_RGBA_GENERAL_to_RGB_GENTRAL<uint8_t>>());
ops.push_back(std::make_shared<Op_RGBA_GENERAL_to_RGB_GENTRAL<uint16_t>>());
}
void ColorConversionPipeline::release_ops()
{
m_operation_pool.clear();
}
bool ColorConversionPipeline::construct_pipeline(const ColorState& input_state,
const ColorState& target_state,
const heif_color_conversion_options& options)
{
m_conversion_steps.clear();
m_options = options;
if (input_state == target_state) {
return true;
}
#if DEBUG_ME
std::cerr << "--- construct_pipeline\n";
std::cerr << "from: " << input_state << "\nto: " << target_state << "\n";
#endif
init_ops(); // to be sure these are initialized even without heif_init()
std::vector<std::shared_ptr<ColorConversionOperation>>& ops = m_operation_pool;
// --- Dijkstra search for the minimum-cost conversion pipeline
std::vector<Node> processed_states;
std::vector<Node> border_states;
border_states.push_back({-1, nullptr, input_state, 0});
while (!border_states.empty()) {
int minIdx = -1;
int minCost = std::numeric_limits<int>::max();
for (int i = 0; i < (int) border_states.size(); i++) {
int cost = border_states[i].speed_costs;
if (cost < minCost) {
minIdx = i;
minCost = cost;
}
}
assert(minIdx >= 0);
// move minimum-cost border_state into processed_states
processed_states.push_back(border_states[minIdx]);
border_states[minIdx] = border_states.back();
border_states.pop_back();
#if DEBUG_PIPELINE_CREATION
std::cerr << "- expand node: " << processed_states.back().output_state
<< " with cost " << processed_states.back().speed_costs << " \n";
#endif
if (processed_states.back().output_state == target_state) {
// end-state found, backtrack path to find conversion pipeline
size_t idx = processed_states.size() - 1;
int len = 0;
while (idx > 0) {
idx = processed_states[idx].prev_processed_idx;
len++;
}
m_conversion_steps.resize(len);
idx = processed_states.size() - 1;
int step = 0;
while (idx > 0) {
m_conversion_steps[len - 1 - step].operation = processed_states[idx].op;
m_conversion_steps[len - 1 - step].output_state = processed_states[idx].output_state;
if (step > 0) {
m_conversion_steps[len - step].input_state = m_conversion_steps[len - 1 - step].output_state;
}
//printf("cost: %f\n",processed_states[idx].color_state.costs.total(options.criterion));
idx = processed_states[idx].prev_processed_idx;
step++;
}
m_conversion_steps[0].input_state = input_state;
assert(m_conversion_steps.back().output_state == target_state);
#if DEBUG_ME
std::cerr << debug_dump_pipeline();
#endif
return true;
}
// expand the node with minimum cost
for (const auto& op_ptr : ops) {
#if DEBUG_PIPELINE_CREATION
auto& op = *op_ptr;
std::cerr << "-- apply op: " << typeid(op).name() << "\n";
#endif
auto out_states = op_ptr->state_after_conversion(processed_states.back().output_state,
target_state,
options);
for (const auto& out_state : out_states) {
int new_op_costs = out_state.speed_costs + processed_states.back().speed_costs;
#if DEBUG_PIPELINE_CREATION
std::cerr << "--- " << out_state.color_state << " with cost " << new_op_costs << "\n";
#endif
bool state_exists = false;
for (const auto& s : processed_states) {
if (s.output_state == out_state.color_state) {
state_exists = true;
break;
}
}
if (!state_exists) {
for (auto& s : border_states) {
if (s.output_state == out_state.color_state) {
state_exists = true;
// if we reached the same border node with a lower cost, replace the border node
if (s.speed_costs > new_op_costs) {
s = {(int) (processed_states.size() - 1),
op_ptr,
out_state.color_state,
out_state.speed_costs};
s.speed_costs = new_op_costs;
}
break;
}
}
}
// enter the new output state into the list of border states
if (!state_exists) {
ColorStateWithCost s = out_state;
s.speed_costs = s.speed_costs + processed_states.back().speed_costs;
border_states.push_back({(int) (processed_states.size() - 1),
op_ptr,
s.color_state,
s.speed_costs});
}
}
}
}
return false;
}
std::string ColorConversionPipeline::debug_dump_pipeline() const
{
std::ostringstream ostr;
ostr << "final pipeline has " << m_conversion_steps.size() << " steps:\n";
for (const auto& step : m_conversion_steps) {
auto& op = *step.operation;
ostr << "> " << typeid(op).name() << "\n";
}
return ostr.str();
}
std::shared_ptr<HeifPixelImage> ColorConversionPipeline::convert_image(const std::shared_ptr<HeifPixelImage>& input)
{
std::shared_ptr<HeifPixelImage> in = input;
std::shared_ptr<HeifPixelImage> out = in;
for (const auto& step : m_conversion_steps) {
#if DEBUG_ME
std::cerr << "input spec: ";
print_spec(std::cerr, in);
#endif
out = step.operation->convert_colorspace(in, step.input_state, step.output_state, m_options);
if (!out) {
return nullptr; // TODO: we should return a proper error
}
// --- pass the color profiles to the new image
auto output_nclx = std::make_shared<color_profile_nclx>(step.output_state.nclx_profile);
out->set_color_profile_nclx(output_nclx);
out->set_color_profile_icc(in->get_color_profile_icc());
out->set_premultiplied_alpha(in->is_premultiplied_alpha());
// pass through HDR information
if (in->has_clli()) {
out->set_clli(in->get_clli());
}
if (in->has_mdcv()) {
out->set_mdcv(in->get_mdcv());
}
if (in->has_nonsquare_pixel_ratio()) {
uint32_t h, v;
in->get_pixel_ratio(&h, &v);
out->set_pixel_ratio(h, v);
}
const auto& warnings = in->get_warnings();
for (const auto& warning : warnings) {
out->add_warning(warning);
}
in = out;
}
return out;
}
std::shared_ptr<HeifPixelImage> convert_colorspace(const std::shared_ptr<HeifPixelImage>& input,
heif_colorspace target_colorspace,
heif_chroma target_chroma,
const std::shared_ptr<const color_profile_nclx>& target_profile,
int output_bpp,
const heif_color_conversion_options& options)
{
// --- check that input image is valid
int width = input->get_width();
int height = input->get_height();
// alpha image should have full image resolution
// if (input->has_channel(heif_channel_Alpha)) {
// if (input->get_width(heif_channel_Alpha) != width ||
// input->get_height(heif_channel_Alpha) != height) {
// return nullptr;
// }
// }
// check for valid target YCbCr chroma formats
if (target_colorspace == heif_colorspace_YCbCr) {
if (target_chroma != heif_chroma_420 &&
target_chroma != heif_chroma_422 &&
target_chroma != heif_chroma_444) {
return nullptr;
}
}
// --- prepare conversion
ColorState input_state;
input_state.colorspace = input->get_colorspace();
input_state.chroma = input->get_chroma_format();
input_state.has_alpha = input->has_channel(heif_channel_Alpha) || is_chroma_with_alpha(input->get_chroma_format());
if (input->get_color_profile_nclx()) {
input_state.nclx_profile = *input->get_color_profile_nclx();
}
input_state.nclx_profile.replace_undefined_values_with_sRGB_defaults();
std::set<enum heif_channel> channels = input->get_channel_set();
assert(!channels.empty());
input_state.bits_per_pixel = input->get_bits_per_pixel(*(channels.begin()));
ColorState output_state = input_state;
output_state.colorspace = target_colorspace;
output_state.chroma = target_chroma;
if (target_profile) {
output_state.nclx_profile = *target_profile;
}
// If some output nclx values are unspecified, set them to the same as the input.
if (output_state.nclx_profile.get_matrix_coefficients() == heif_matrix_coefficients_unspecified) {
output_state.nclx_profile.set_matrix_coefficients(input_state.nclx_profile.get_matrix_coefficients());
}
if (output_state.nclx_profile.get_colour_primaries() == heif_color_primaries_unspecified) {
output_state.nclx_profile.set_colour_primaries(input_state.nclx_profile.get_colour_primaries());
}
if (output_state.nclx_profile.get_transfer_characteristics() == heif_transfer_characteristic_unspecified) {
output_state.nclx_profile.set_transfer_characteristics(input_state.nclx_profile.get_transfer_characteristics());
}
// If we convert to an interleaved format, we want alpha only if present in the
// interleaved output format.
// For planar formats, we include an alpha plane when included in the input.
if (num_interleaved_pixels_per_plane(target_chroma) > 1) {
output_state.has_alpha = is_chroma_with_alpha(target_chroma);
}
else {
output_state.has_alpha = input_state.has_alpha;
}
if (output_bpp) {
output_state.bits_per_pixel = output_bpp;
}
// interleaved RGB formats always have to be 8-bit
if (target_chroma == heif_chroma_interleaved_RGB ||
target_chroma == heif_chroma_interleaved_RGBA) {
output_state.bits_per_pixel = 8;
}
// interleaved RRGGBB formats have to be >8-bit.
// If we don't know a target bit-depth, use 10 bit.
if ((target_chroma == heif_chroma_interleaved_RRGGBB_LE ||
target_chroma == heif_chroma_interleaved_RRGGBB_BE ||
target_chroma == heif_chroma_interleaved_RRGGBBAA_LE ||
target_chroma == heif_chroma_interleaved_RRGGBBAA_BE) &&
output_state.bits_per_pixel <= 8) {
output_state.bits_per_pixel = 10;
}
ColorConversionPipeline pipeline;
bool success = pipeline.construct_pipeline(input_state, output_state, options);
if (!success) {
return nullptr;
}
return pipeline.convert_image(input);
}