Skip to content

Commit

Permalink
Allow for larger excursions of min, max content boost
Browse files Browse the repository at this point in the history
Current implementation clips min/max content boost to a smaller range.
Allow for larger excursions for better hdr intent representation.

Also, fixed round factor before encoding gainmap coefficient

Test: ./ultrahdr_unit_test
  • Loading branch information
ram-mohan committed Jul 20, 2024
1 parent e899aec commit a131f09
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 149 deletions.
21 changes: 4 additions & 17 deletions lib/include/ultrahdr/gainmapmath.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ struct GainLUT {

GainLUT(uhdr_gainmap_metadata_ext_t* metadata, float displayBoost) {
this->mGammaInv = 1.0f / metadata->gamma;
float boostFactor = displayBoost > 0 ? displayBoost / metadata->max_content_boost : 1.0f;
float boostFactor = displayBoost > 0 ? displayBoost / metadata->hdr_capacity_max : 1.0f;
for (int32_t idx = 0; idx < kGainFactorNumEntries; idx++) {
float value = static_cast<float>(idx) / static_cast<float>(kGainFactorNumEntries - 1);
float logBoost = log2(metadata->min_content_boost) * (1.0f - value) +
Expand Down Expand Up @@ -540,23 +540,14 @@ void transformYuv444(uhdr_raw_image_t* image, const std::array<float, 9>& coeffs

/*
* Calculate the 8-bit unsigned integer gain value for the given SDR and HDR
* luminances in linear space, and the hdr ratio to encode against.
*
* Note: since this library always uses gamma of 1.0, offsetSdr of 0.0, and
* offsetHdr of 0.0, this function doesn't handle different metadata values for
* these fields.
* luminances in linear space and gainmap metadata fields.
*/
uint8_t encodeGain(float y_sdr, float y_hdr, uhdr_gainmap_metadata_ext_t* metadata);
uint8_t encodeGain(float y_sdr, float y_hdr, uhdr_gainmap_metadata_ext_t* metadata,
float log2MinContentBoost, float log2MaxContentBoost);
float computeGain(float sdr, float hdr);
uint8_t affineMapGain(float gainlog2, float mingainlog2, float maxgainlog2, float gamma);

/*
* Calculates the linear luminance in nits after applying the given gain
* value, with the given hdr ratio, to the given sdr input in the range [0, 1].
*
* Note: similar to encodeGain(), this function only supports gamma 1.0,
* offsetSdr 0.0, offsetHdr 0.0, hdrCapacityMin 1.0, and hdrCapacityMax equal to
* gainMapMax, as this library encodes.
*/
Color applyGain(Color e, float gain, uhdr_gainmap_metadata_ext_t* metadata);
Color applyGain(Color e, float gain, uhdr_gainmap_metadata_ext_t* metadata, float displayBoost);
Expand All @@ -565,10 +556,6 @@ Color applyGainLUT(Color e, float gain, GainLUT& gainLUT);
/*
* Apply gain in R, G and B channels, with the given hdr ratio, to the given sdr input
* in the range [0, 1].
*
* Note: similar to encodeGain(), this function only supports gamma 1.0,
* offsetSdr 0.0, offsetHdr 0.0, hdrCapacityMin 1.0, and hdrCapacityMax equal to
* gainMapMax, as this library encodes.
*/
Color applyGain(Color e, Color gain, uhdr_gainmap_metadata_ext_t* metadata);
Color applyGain(Color e, Color gain, uhdr_gainmap_metadata_ext_t* metadata, float displayBoost);
Expand Down
36 changes: 16 additions & 20 deletions lib/src/gainmapmath.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -636,24 +636,20 @@ void transformYuv444(uhdr_raw_image_t* image, const std::array<float, 9>& coeffs

////////////////////////////////////////////////////////////////////////////////
// Gain map calculations
uint8_t encodeGain(float y_sdr, float y_hdr, uhdr_gainmap_metadata_ext_t* metadata) {
return encodeGain(y_sdr, y_hdr, metadata, log2(metadata->min_content_boost),
log2(metadata->max_content_boost));
}

uint8_t encodeGain(float y_sdr, float y_hdr, uhdr_gainmap_metadata_ext_t* metadata,
float log2MinContentBoost, float log2MaxContentBoost) {
float gain = 1.0f;
if (y_sdr > 0.0f) {
gain = y_hdr / y_sdr;
float computeGain(float sdr, float hdr) {
if (sdr == 0.0f) return 0.0f; // for sdr black return no gain
if (hdr == 0.0f) { // for hdr black, return a gain large enough to attenuate the sdr pel
float offset = (1.0f / 64);
return log2(offset / (offset + sdr));
}
return log2(hdr / sdr);
}

if (gain < metadata->min_content_boost) gain = metadata->min_content_boost;
if (gain > metadata->max_content_boost) gain = metadata->max_content_boost;
float gain_normalized =
(log2(gain) - log2MinContentBoost) / (log2MaxContentBoost - log2MinContentBoost);
float gain_normalized_gamma = powf(gain_normalized, metadata->gamma);
return static_cast<uint8_t>(gain_normalized_gamma * 255.0f);
uint8_t affineMapGain(float gainlog2, float mingainlog2, float maxgainlog2, float gamma) {
float mappedVal = (gainlog2 - mingainlog2) / (maxgainlog2 - mingainlog2);
if (gamma != 1.0f) mappedVal = pow(mappedVal, gamma);
mappedVal *= 255;
return CLIP3(mappedVal + 0.5f, 0, 255);
}

Color applyGain(Color e, float gain, uhdr_gainmap_metadata_ext_t* metadata) {
Expand All @@ -668,7 +664,7 @@ Color applyGain(Color e, float gain, uhdr_gainmap_metadata_ext_t* metadata, floa
gain = pow(gain, 1.0f / metadata->gamma);
float logBoost =
log2(metadata->min_content_boost) * (1.0f - gain) + log2(metadata->max_content_boost) * gain;
float gainFactor = exp2(logBoost * displayBoost / metadata->max_content_boost);
float gainFactor = exp2(logBoost * displayBoost / metadata->hdr_capacity_max);
return e * gainFactor;
}

Expand Down Expand Up @@ -697,9 +693,9 @@ Color applyGain(Color e, Color gain, uhdr_gainmap_metadata_ext_t* metadata, floa
log2(metadata->max_content_boost) * gain.g;
float logBoostB = log2(metadata->min_content_boost) * (1.0f - gain.b) +
log2(metadata->max_content_boost) * gain.b;
float gainFactorR = exp2(logBoostR * displayBoost / metadata->max_content_boost);
float gainFactorG = exp2(logBoostG * displayBoost / metadata->max_content_boost);
float gainFactorB = exp2(logBoostB * displayBoost / metadata->max_content_boost);
float gainFactorR = exp2(logBoostR * displayBoost / metadata->hdr_capacity_max);
float gainFactorG = exp2(logBoostG * displayBoost / metadata->hdr_capacity_max);
float gainFactorB = exp2(logBoostB * displayBoost / metadata->hdr_capacity_max);
return {{{e.r * gainFactorR, e.g * gainFactorG, e.b * gainFactorB}}};
}

Expand Down
166 changes: 112 additions & 54 deletions lib/src/jpegr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -514,17 +514,6 @@ uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_
return status;
}

gainmap_metadata->max_content_boost = hdr_white_nits / kSdrWhiteNits;
gainmap_metadata->min_content_boost = 1.0f;
gainmap_metadata->gamma = mGamma;
gainmap_metadata->offset_sdr = 0.0f;
gainmap_metadata->offset_hdr = 0.0f;
gainmap_metadata->hdr_capacity_min = 1.0f;
gainmap_metadata->hdr_capacity_max = gainmap_metadata->max_content_boost;

float log2MinBoost = log2(gainmap_metadata->min_content_boost);
float log2MaxBoost = log2(gainmap_metadata->max_content_boost);

ColorTransformFn hdrGamutConversionFn = getGamutConversionFn(sdr_intent->cg, hdr_intent->cg);
if (hdrGamutConversionFn == nullptr) {
status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE;
Expand Down Expand Up @@ -608,18 +597,29 @@ uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_
UHDR_CG_UNSPECIFIED, UHDR_CT_UNSPECIFIED, UHDR_CR_UNSPECIFIED, map_width, map_height, 64);
uhdr_raw_image_ext_t* dest = gainmap_img.get();

uhdr_memory_block_t gainmap_mem(map_width * map_height * sizeof(float) *
(mUseMultiChannelGainMap ? 3 : 1));
float* gainmap_data = reinterpret_cast<float*>(gainmap_mem.m_buffer.get());
float gainmap_min[3] = {127.0f, 127.0f, 127.0f};
float gainmap_max[3] = {-128.0f, -128.0f, -128.0f};
std::mutex gainmap_minmax;

const int threads = (std::min)(GetCPUCoreCount(), 4);
const int jobSizeInRows = 1;
size_t rowStep = threads == 1 ? map_height : jobSizeInRows;
JobQueue jobQueue;
std::function<void()> generateMap =
[this, sdr_intent, hdr_intent, gainmap_metadata, dest, hdrInvOetf, hdrGamutConversionFn,
luminanceFn, sdrYuvToRgbFn, hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn,
hdr_white_nits, log2MinBoost, log2MaxBoost, use_luminance, &jobQueue]() -> void {
std::function<void()> generateMap = [this, sdr_intent, hdr_intent, gainmap_data, map_width,
hdrInvOetf, hdrGamutConversionFn, luminanceFn, sdrYuvToRgbFn,
hdrYuvToRgbFn, sdr_sample_pixel_fn, hdr_sample_pixel_fn,
hdr_white_nits, use_luminance, &gainmap_min, &gainmap_max,
&gainmap_minmax, &jobQueue]() -> void {
size_t rowStart, rowEnd;
float gainmap_min_th[3] = {127.0f, 127.0f, 127.0f};
float gainmap_max_th[3] = {-128.0f, -128.0f, -128.0f};

while (jobQueue.dequeueJob(rowStart, rowEnd)) {
for (size_t y = rowStart; y < rowEnd; ++y) {
for (size_t x = 0; x < dest->w; ++x) {
for (size_t x = 0; x < map_width; ++x) {
Color sdr_yuv_gamma = sdr_sample_pixel_fn(sdr_intent, mMapDimensionScaleFactor, x, y);
Color sdr_rgb_gamma = sdrYuvToRgbFn(sdr_yuv_gamma);
// We are assuming the SDR input is always sRGB transfer.
Expand All @@ -637,17 +637,19 @@ uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_
if (mUseMultiChannelGainMap) {
Color sdr_rgb_nits = sdr_rgb * kSdrWhiteNits;
Color hdr_rgb_nits = hdr_rgb * hdr_white_nits;
size_t pixel_idx = (x + y * dest->stride[UHDR_PLANE_PACKED]) * 3;

reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_PACKED])[pixel_idx] = encodeGain(
sdr_rgb_nits.r, hdr_rgb_nits.r, gainmap_metadata, log2MinBoost, log2MaxBoost);
reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_PACKED])[pixel_idx + 1] = encodeGain(
sdr_rgb_nits.g, hdr_rgb_nits.g, gainmap_metadata, log2MinBoost, log2MaxBoost);
reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_PACKED])[pixel_idx + 2] = encodeGain(
sdr_rgb_nits.b, hdr_rgb_nits.b, gainmap_metadata, log2MinBoost, log2MaxBoost);
size_t pixel_idx = (x + y * map_width) * 3;

gainmap_data[pixel_idx] = computeGain(sdr_rgb_nits.r, hdr_rgb_nits.r);
gainmap_data[pixel_idx + 1] = computeGain(sdr_rgb_nits.g, hdr_rgb_nits.g);
gainmap_data[pixel_idx + 2] = computeGain(sdr_rgb_nits.b, hdr_rgb_nits.b);
for (int i = 0; i < 3; i++) {
gainmap_min_th[i] = (std::min)(gainmap_data[pixel_idx + i], gainmap_min_th[i]);
gainmap_max_th[i] = (std::max)(gainmap_data[pixel_idx + i], gainmap_max_th[i]);
}
} else {
float sdr_y_nits;
float hdr_y_nits;

if (use_luminance) {
sdr_y_nits = luminanceFn(sdr_rgb) * kSdrWhiteNits;
hdr_y_nits = luminanceFn(hdr_rgb) * hdr_white_nits;
Expand All @@ -656,14 +658,21 @@ uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_
hdr_y_nits = fmax(hdr_rgb.r, fmax(hdr_rgb.g, hdr_rgb.b)) * hdr_white_nits;
}

size_t pixel_idx = x + y * dest->stride[UHDR_PLANE_Y];

reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_Y])[pixel_idx] =
encodeGain(sdr_y_nits, hdr_y_nits, gainmap_metadata, log2MinBoost, log2MaxBoost);
size_t pixel_idx = x + y * map_width;
gainmap_data[pixel_idx] = computeGain(sdr_y_nits, hdr_y_nits);
gainmap_min_th[0] = (std::min)(gainmap_data[pixel_idx], gainmap_min_th[0]);
gainmap_max_th[0] = (std::max)(gainmap_data[pixel_idx], gainmap_max_th[0]);
}
}
}
}
{
std::unique_lock<std::mutex> lock{gainmap_minmax};
for (int index = 0; index < (mUseMultiChannelGainMap ? 3 : 1); index++) {
gainmap_min[index] = (std::min)(gainmap_min[index], gainmap_min_th[index]);
gainmap_max[index] = (std::max)(gainmap_max[index], gainmap_max_th[index]);
}
}
};

// generate map
Expand All @@ -681,6 +690,71 @@ uhdr_error_info_t JpegR::generateGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_
generateMap();
std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });

float min_content_boost_log2 = gainmap_min[0];
float max_content_boost_log2 = gainmap_max[0];
for (int index = 1; index < (mUseMultiChannelGainMap ? 3 : 1); index++) {
min_content_boost_log2 = (std::min)(gainmap_min[index], min_content_boost_log2);
max_content_boost_log2 = (std::max)(gainmap_max[index], max_content_boost_log2);
}
// -13.0 emphirically is a small enough gain factor that is capable of representing hdr
// black from any sdr luminance. Allowing further excursion might not offer any benefit and on the
// downside can cause bigger error during affine map and inverse map.
min_content_boost_log2 = (std::max)(-13.0f, min_content_boost_log2);
if (fabs(max_content_boost_log2 - min_content_boost_log2) < FLT_EPSILON) {
max_content_boost_log2 += 0.1; // to avoid div by zero during affine transform
}

std::function<void()> encodeMap = [this, gainmap_data, map_width, dest, min_content_boost_log2,
max_content_boost_log2, &jobQueue]() -> void {
size_t rowStart, rowEnd;

while (jobQueue.dequeueJob(rowStart, rowEnd)) {
if (mUseMultiChannelGainMap) {
for (size_t j = rowStart; j < rowEnd; j++) {
size_t dst_pixel_idx = j * dest->stride[UHDR_PLANE_PACKED] * 3;
size_t src_pixel_idx = j * map_width * 3;
for (size_t i = 0; i < map_width * 3; i++) {
reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_PACKED])[dst_pixel_idx + i] =
affineMapGain(gainmap_data[src_pixel_idx + i], min_content_boost_log2,
max_content_boost_log2, this->mGamma);
}
}
} else {
for (size_t j = rowStart; j < rowEnd; j++) {
size_t dst_pixel_idx = j * dest->stride[UHDR_PLANE_Y];
size_t src_pixel_idx = j * map_width;
for (size_t i = 0; i < map_width; i++) {
reinterpret_cast<uint8_t*>(dest->planes[UHDR_PLANE_Y])[dst_pixel_idx + i] =
affineMapGain(gainmap_data[src_pixel_idx + i], min_content_boost_log2,
max_content_boost_log2, this->mGamma);
}
}
}
}
};
workers.clear();
jobQueue.reset();
rowStep = threads == 1 ? map_height : 1;
for (int th = 0; th < threads - 1; th++) {
workers.push_back(std::thread(encodeMap));
}
for (size_t rowStart = 0; rowStart < map_height;) {
size_t rowEnd = (std::min)(rowStart + rowStep, map_height);
jobQueue.enqueueJob(rowStart, rowEnd);
rowStart = rowEnd;
}
jobQueue.markQueueForEnd();
encodeMap();
std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });

gainmap_metadata->max_content_boost = exp2(max_content_boost_log2);
gainmap_metadata->min_content_boost = exp2(min_content_boost_log2);
gainmap_metadata->gamma = this->mGamma;
gainmap_metadata->offset_sdr = 0.0f;
gainmap_metadata->offset_hdr = 0.0f;
gainmap_metadata->hdr_capacity_min = 1.0f;
gainmap_metadata->hdr_capacity_max = hdr_white_nits / kSdrWhiteNits;

return status;
}

Expand Down Expand Up @@ -1076,26 +1150,6 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima
gainmap_metadata->offset_hdr);
return status;
}
if (gainmap_metadata->hdr_capacity_min != gainmap_metadata->min_content_boost) {
uhdr_error_info_t status;
status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE;
status.has_detail = 1;
snprintf(status.detail, sizeof status.detail,
"Unsupported gainmap metadata, min_content_boost. Min content boost is expected to be "
"same as hdr capacity min. Min content boost %f, Hdr Capacity min %f",
gainmap_metadata->min_content_boost, gainmap_metadata->hdr_capacity_min);
return status;
}
if (gainmap_metadata->hdr_capacity_max != gainmap_metadata->max_content_boost) {
uhdr_error_info_t status;
status.error_code = UHDR_CODEC_UNSUPPORTED_FEATURE;
status.has_detail = 1;
snprintf(status.detail, sizeof status.detail,
"Unsupported gainmap metadata, max_content_boost. Max content boost is expected to be "
"same as hdr capacity max. Max content boost %f, Hdr Capacity max %f",
gainmap_metadata->max_content_boost, gainmap_metadata->hdr_capacity_max);
return status;
}
if (sdr_intent->fmt != UHDR_IMG_FMT_24bppYCbCr444 &&
sdr_intent->fmt != UHDR_IMG_FMT_16bppYCbCr422 &&
sdr_intent->fmt != UHDR_IMG_FMT_12bppYCbCr420) {
Expand Down Expand Up @@ -1147,7 +1201,7 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima
dest->cg = sdr_intent->cg;
// Table will only be used when map scale factor is integer.
ShepardsIDW idwTable(static_cast<int>(map_scale_factor));
float display_boost = (std::min)(max_display_boost, gainmap_metadata->max_content_boost);
float display_boost = (std::min)(max_display_boost, gainmap_metadata->hdr_capacity_max);
GainLUT gainLUT(gainmap_metadata, display_boost);

GetPixelFn get_pixel_fn = getPixelFn(sdr_intent->fmt);
Expand All @@ -1162,11 +1216,14 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima

JobQueue jobQueue;
std::function<void()> applyRecMap = [sdr_intent, gainmap_img, dest, &jobQueue, &idwTable,
output_ct, &gainLUT, display_boost, map_scale_factor,
get_pixel_fn]() -> void {
output_ct, &gainLUT, display_boost,
#if !USE_APPLY_GAIN_LUT
gainmap_metadata,
#endif
map_scale_factor, get_pixel_fn]() -> void {
size_t width = sdr_intent->w;

size_t rowStart, rowEnd;

while (jobQueue.dequeueJob(rowStart, rowEnd)) {
for (size_t y = rowStart; y < rowEnd; ++y) {
for (size_t x = 0; x < width; ++x) {
Expand All @@ -1192,7 +1249,7 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima
#if USE_APPLY_GAIN_LUT
rgb_hdr = applyGainLUT(rgb_sdr, gain, gainLUT);
#else
rgb_hdr = applyGain(rgb_sdr, gain, metadata, display_boost);
rgb_hdr = applyGain(rgb_sdr, gain, gainmap_metadata, display_boost);
#endif
} else {
Color gain;
Expand All @@ -1208,7 +1265,7 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima
#if USE_APPLY_GAIN_LUT
rgb_hdr = applyGainLUT(rgb_sdr, gain, gainLUT);
#else
rgb_hdr = applyGain(rgb_sdr, gain, metadata, display_boost);
rgb_hdr = applyGain(rgb_sdr, gain, gainmap_metadata, display_boost);
#endif
}

Expand Down Expand Up @@ -1268,6 +1325,7 @@ uhdr_error_info_t JpegR::applyGainMap(uhdr_raw_image_t* sdr_intent, uhdr_raw_ima
jobQueue.markQueueForEnd();
applyRecMap();
std::for_each(workers.begin(), workers.end(), [](std::thread& t) { t.join(); });

return g_no_error;
}

Expand Down
Loading

0 comments on commit a131f09

Please sign in to comment.