Skip to content

Added offset to gain computation in low-luminance regions #358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

hi-hori
Copy link

@hi-hori hi-hori commented May 16, 2025

Added an offset kComputeGainOffset to the computeGain function to prevent excessive gain in low-luminance regions for both SDR and HDR calculations. This adjustment helps stabilize gain computation.

Simulation

gain_offset.xlsx

The kComputeGainOffset prevents gain spikes in low-luminance regions, but as a side effect, it reduces HDR luminance. This effect is simulated in the attached XLSX file.

kSdrOffset kHdrOffset kComputeGainOffset
1.00E-07 1.00E-07 1.0
SDR (nits) HDR (nits)   gain (without offset) gain (with offset) restored HDR (without offset) restored HDR (with offset)
0 0.003   30001.0000000 1.0030000 0.0030000 0.0000000
0 0.1   1000001.0000000 1.1000000 0.1000000 0.0000000
0 1   10000001.0000000 1.9999999 1.0000000 0.0000001
0 50   500000001.0000000 50.9999950 50.0000000 0.0000050
0 100   1000000001.0000000 100.9999900 100.0000000 0.0000100
1 1   1.0000000 1.0000000 1.0000000 1.0000000
1 10   9.9999991 5.4999998 10.0000000 5.5000002
1 100   99.9999901 50.4999975 100.0000000 50.5000025
10 1   0.1000000 0.1818182 1.0000000 1.8181818
10 50   5.0000000 4.6363636 50.0000000 46.3636364
10 100   9.9999999 9.1818181 100.0000000 91.8181819
100 50   0.5000000 0.5049505 50.0000000 50.4950495
100 100   1.0000000 1.0000000 100.0000000 100.0000000
100 500   5.0000000 4.9603960 500.0000000 496.0396040
100 1000   10.0000000 9.9108911 1000.0000000 991.0891089
200 100   0.5000000 0.5024876 100.0000000 100.4975124
200 200   1.0000000 1.0000000 200.0000000 200.0000000
200 500   2.5000000 2.4925373 500.0000000 498.5074627
200 1000   5.0000000 4.9800995 1000.0000000 996.0199005

Test

ultrahdr_app.exe -m 0 -p hlg.raw -y sdr.rgba -b 3 -h 5760 -w 3840 -a 0 -C 2 -c 0 -t 1 -R 0 -s 1 -q 80 -Q 80 -G 1.0 -M 1 -e 1 -D 1 -z uhdr.jpg

The hlg.raw file is attached as hlg.zip.
The sdr.rgba file is attached as sdr.zip.

Result

a Without offset With offset
UltraHDR JPEG uhdr_without_offset uhdr_with_offset
Gain map JPEG gainmap_without_offset gainmap_with_offset

Copy link

google-cla bot commented May 16, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Added an offset kComputeGainOffset to the computeGain function to prevent excessive gain in low-luminance regions for both SDR and HDR calculations. This adjustment helps stabilize gain computation.
@hi-hori hi-hori force-pushed the low-brightness-noise-reduction branch from f8f7a18 to 1eb503f Compare May 16, 2025 01:00
@ram-mohan
Copy link
Contributor

There is a change that restricts gain for low-luminance regions. 720dbed. What is the problem this change is trying to resolve?

@hi-hori
Copy link
Author

hi-hori commented May 16, 2025

In the current version, there is an issue where even a slight luminance difference between SDR and HDR—especially in low-luminance regions (<1 nit)—can result in excessively large gain values. For example, if SDR is 0 nits and HDR is 0.003 nits, the gain becomes 30,001×.

Such extreme gain values broaden the range of GainMapMin and GainMapMax, which in turn leads to a degradation in Ultra HDR image quality.

This pull request suppresses excessive gain in low-luminance regions and improves the overall quality of Ultra HDR images.

Below is the Gain Map metadata of the Ultra HDR image initially attached.
Without the offset, the values are GainMapMin = -14.3 and GainMapMax = 5.29289.
With the offset applied, they are GainMapMin = -2.36361 and GainMapMax = 1.81413.

Gainmap metadata (without offset)

<x:xmpmeta
  xmlns:x="adobe:ns:meta/"
  x:xmptk="Adobe XMP Core 5.1.2">
  <rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
      hdrgm:Version="1.0"
      hdrgm:GainMapMin="-14.3"
      hdrgm:GainMapMax="5.29289"
      hdrgm:Gamma="1"
      hdrgm:OffsetSDR="1e-07"
      hdrgm:OffsetHDR="1e-07"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="2.30045"
      hdrgm:BaseRenditionIsHDR="False"/>
  </rdf:RDF>
</x:xmpmeta>

Gainmap metadata (with offset)

<x:xmpmeta
  xmlns:x="adobe:ns:meta/"
  x:xmptk="Adobe XMP Core 5.1.2">
  <rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
      hdrgm:Version="1.0"
      hdrgm:GainMapMin="-2.36361"
      hdrgm:GainMapMax="1.81413"
      hdrgm:Gamma="1"
      hdrgm:OffsetSDR="1e-07"
      hdrgm:OffsetHDR="1e-07"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="2.30045"
      hdrgm:BaseRenditionIsHDR="False"/>
  </rdf:RDF>
</x:xmpmeta>

@ram-mohan
Copy link
Contributor

if (sdr < 2.f / 255.0f) {
    // If sdr is zero and hdr is non zero, it can result in very large gain values. In compression -
    // decompression process, if the same sdr pixel increases to 1, the hdr recovered pixel will
    // blow out. Dont allow dark pixels to signal large gains.
    gain = (std::min)(gain, 2.3f);
  }

Those low sdr pixel should trigger this and gain must get clamped?

@hi-hori
Copy link
Author

hi-hori commented May 16, 2025

This image was generated by setting both kSdrOffset and kHdrOffset to 1.0 in the current library, but the quality significantly degraded. This is likely because kSdrOffset and kHdrOffset are also referenced during rendering.

<x:xmpmeta
  xmlns:x="adobe:ns:meta/"
  x:xmptk="Adobe XMP Core 5.1.2">
  <rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
      hdrgm:Version="1.0"
      hdrgm:GainMapMin="-1.85716"
      hdrgm:GainMapMax="2.29692"
      hdrgm:Gamma="1"
      hdrgm:OffsetSDR="1"
      hdrgm:OffsetHDR="1"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="2.30045"
      hdrgm:BaseRenditionIsHDR="False"/>
  </rdf:RDF>
</x:xmpmeta>

uhdr3

@hi-hori
Copy link
Author

hi-hori commented May 16, 2025

if (sdr < 2.f / 255.0f) {
    // If sdr is zero and hdr is non zero, it can result in very large gain values. In compression -
    // decompression process, if the same sdr pixel increases to 1, the hdr recovered pixel will
    // blow out. Dont allow dark pixels to signal large gains.
    gain = (std::min)(gain, 2.3f);
  }

Those low sdr pixel should trigger this and gain must get clamped?

float computeGain(float sdr, float hdr) {
  float gain = log2((hdr + kHdrOffset) / (sdr + kSdrOffset));
  if (sdr < 2.f / 255.0f) {
    // If sdr is zero and hdr is non zero, it can result in very large gain values. In compression -
    // decompression process, if the same sdr pixel increases to 1, the hdr recovered pixel will
    // blow out. Dont allow dark pixels to signal large gains.
    gain = (std::min)(gain, 2.3f);
  }
  if (gain < -4.0)
  {
    // dump log...
  }
  return gain;
}

Below is a dump of the sdr and hdr values when gain < -4 or gain > 4 occurred at the end of the computeGain function.
It seems that the clamp implemented in computeGain is insufficient for the image sources I tested.

gain < -4

sdr = 0.122870572, hdr = 0.00158074184, gain = -6.28030348
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.122870572, hdr = 0.00143404759, gain = -6.42080307
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.368611723, hdr = 0.00000000, gain = -21.8136711
sdr = 0.368611723, hdr = 0.0131801786, gain = -4.80564928
sdr = 0.184305862, hdr = 0.00884508342, gain = -4.38106680
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.122870572, hdr = 0.00479658041, gain = -4.67896032
sdr = 0.552917540, hdr = 0.0176169127, gain = -4.97202349
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.307176441, hdr = 0.0184366368, gain = -4.05841255
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.307176441, hdr = 0.00000000, gain = -21.5506363
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.307176441, hdr = 0.0122444034, gain = -4.64886189
sdr = 0.122870572, hdr = 0.00576052116, gain = -4.41477251
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.184305862, hdr = 0.00000000, gain = -20.8136711
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.184305862, hdr = 0.00000000, gain = -20.8136711
sdr = 0.0614352860, hdr = 0.000247491698, gain = -7.95496321
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.0614352860, hdr = 0.00151939457, gain = -5.33740664
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.430046976, hdr = 0.0132918507, gain = -5.01586962
sdr = 0.491482288, hdr = 0.0297473446, gain = -4.04630184
sdr = 0.307176441, hdr = 0.0103353737, gain = -4.89339161
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.245741144, hdr = 0.00000000, gain = -21.2287083
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.0614352860, hdr = 0.00000000, gain = -19.2287102
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083
sdr = 0.122870572, hdr = 0.00000000, gain = -20.2287083

gain > 4

sdr = 0.0614352860, hdr = 1.14984202, gain = 4.22622204
sdr = 0.0614352860, hdr = 0.992413700, gain = 4.01380014
sdr = 0.0614352860, hdr = 2.31389260, gain = 5.23510838
sdr = 0.0614352860, hdr = 1.54565060, gain = 4.65300083
sdr = 0.0614352860, hdr = 1.09679770, gain = 4.15808392
sdr = 0.0614352860, hdr = 1.17445242, gain = 4.25677490
sdr = 0.122870572, hdr = 2.08334184, gain = 4.08368731
sdr = 0.0614352860, hdr = 1.54644513, gain = 4.65374231
sdr = 0.0614352860, hdr = 1.18139493, gain = 4.26527786
sdr = 0.0614352860, hdr = 1.05953228, gain = 4.10821390
sdr = 0.0614352860, hdr = 1.28923678, gain = 4.39130354
sdr = 0.0614352860, hdr = 1.08547997, gain = 4.14311934
sdr = 0.0614352860, hdr = 1.15167391, gain = 4.22851849
sdr = 0.0614352860, hdr = 1.10320449, gain = 4.16648674
sdr = 0.0614352860, hdr = 1.06229985, gain = 4.11197758
sdr = 0.0614352860, hdr = 2.03277254, gain = 5.04823494
sdr = 0.0614352860, hdr = 1.40370023, gain = 4.51402140
sdr = 0.184305862, hdr = 3.69691610, gain = 4.32614756
sdr = 0.0614352860, hdr = 1.33562195, gain = 4.44229794
sdr = 0.0614352860, hdr = 1.09224856, gain = 4.15208769
sdr = 0.0614352860, hdr = 1.44992661, gain = 4.56076622
sdr = 0.0614352860, hdr = 1.25478208, gain = 4.35222340
sdr = 0.0614352860, hdr = 2.13258672, gain = 5.11739063
sdr = 0.0614352860, hdr = 1.25168264, gain = 4.34865522
sdr = 0.0614352860, hdr = 1.47584939, gain = 4.58633184
sdr = 0.0614352860, hdr = 2.18808389, gain = 5.15445423
sdr = 0.0614352860, hdr = 1.27939928, gain = 4.38025284
sdr = 0.0614352860, hdr = 1.56964719, gain = 4.67522669
sdr = 0.122870572, hdr = 2.35665274, gain = 4.26152658
sdr = 0.0614352860, hdr = 1.03341043, gain = 4.07219982
sdr = 0.0614352860, hdr = 2.63268709, gain = 5.42132235
sdr = 0.0614352860, hdr = 1.01830482, gain = 4.05095577
sdr = 0.0614352860, hdr = 2.18828964, gain = 5.15459013
sdr = 0.0614352860, hdr = 1.32633436, gain = 4.43223095
sdr = 0.0614352860, hdr = 1.34074056, gain = 4.44781637
sdr = 0.0614352860, hdr = 0.986475945, gain = 4.00514221
sdr = 0.0614352860, hdr = 2.37479663, gain = 5.27259016
sdr = 0.0614352860, hdr = 1.25883424, gain = 4.35687494
sdr = 0.0614352860, hdr = 1.22507489, gain = 4.31765652
sdr = 0.0614352860, hdr = 1.36097026, gain = 4.46942186

@ram-mohan
Copy link
Contributor

This was my thinking while computing gainmap coefficients,

  if (y_sdr > 0.0f) {
    gain = y_hdr / y_sdr;
  }

  float gain = log2(y_hdr / y_sdr)

If y_hdr is 0 nits, this expression evaluates to -inf. This will restrict an accurate computation of min content boost. To overcome its better to modify the expression as,

  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);

The computation if (sdr == 0.0f) return 0.0f; is only partially correct. This handles the most trivial case. Consider the scenario where the input given for encoding is yuv. During gainmap computation this yuv input is converted to rgb using yuv to rgb color space conversion equations. These rgb values are not yet rounded to the nearest integer between 0 - 255. Their values are float values between 0 - 255. Consider this value, 1/1024 as the response after doing color space conversion. This value would later be represented as 0 during sdr encoding. Corresponding to this pixel, lets say we have a hdr source whose value is 0.1. Now gainmap is computed as log2(0.1 * 1024) = 22.19. The same sdr source say after compression and decompression process, the pixel that was 1/1024 which maps to 0 at the source say got flipped to 1 (possible due to dct - quantization and dequantization - idct process, a 1024 fold increase), then after the applyGainMap, this pixel will blow up. In other words, smaller the sdr / darker the sdr pixel, higher the effects of gainmap coefficient to quantization errors. It's best to limit the gainmap coefficient for dark pixels. How dark? Our concern arises if 0 value is becoming 1. Values less than 1/255 are at risk. For safety I would like to restrict the gain for pixels with values < 2/255.

float computeGain(float sdr, float hdr) {
  float gain = log2((hdr + kHdrOffset) / (sdr + kSdrOffset));
  if (sdr < 2.f / 255.0f) {
    // If sdr is zero and hdr is non zero, it can result in very large gain values. In compression -
    // decompression process, if the same sdr pixel increases to 1, the hdr recovered pixel will
    // blow out. Dont allow dark pixels to signal large gains.
    gain = (std::min)(gain, 2.3f);
  }
  return gain;
}

You can always tune these. The kOffsets that are used in the gainmap computation should not be significant in comparison with the domain of sdr/hdr. Larger values like 1 can reduce the spread of gainmap min and max, but it will seriously undermine the ability to recover the original signal.

@ram-mohan
Copy link
Contributor

These computations are further clamped here,

      // gain coefficient range [-14.3, 15.6] is capable of representing hdr pels from sdr pels.
      // Allowing further excursion might not offer any benefit and on the downside can cause bigger
      // error during affine map and inverse affine map.
      gainmap_min[index] = (std::clamp)(gainmap_min[index], -14.3f, 15.6f);
      gainmap_max[index] = (std::clamp)(gainmap_max[index], -14.3f, 15.6f);

As I mentioned, these can always be tuned, but we thought this excursion should allow a user to represent black from white and viceversa. Although such color/tone shifts wont occur between hdr and sdr renditions of a image, as part of mathematical operations we allowed it. If your requirements dont need it, then you can pass capacity max and min via command line arguments.

@hi-hori
Copy link
Author

hi-hori commented May 16, 2025

These computations are further clamped here,

      // gain coefficient range [-14.3, 15.6] is capable of representing hdr pels from sdr pels.
      // Allowing further excursion might not offer any benefit and on the downside can cause bigger
      // error during affine map and inverse affine map.
      gainmap_min[index] = (std::clamp)(gainmap_min[index], -14.3f, 15.6f);
      gainmap_max[index] = (std::clamp)(gainmap_max[index], -14.3f, 15.6f);

As I mentioned, these can always be tuned, but we thought this excursion should allow a user to represent black from white and viceversa. Although such color/tone shifts wont occur between hdr and sdr renditions of a image, as part of mathematical operations we allowed it. If your requirements dont need it, then you can pass capacity max and min via command line arguments.

At first, I tried using the -k and -K command line options, but they did not resolve the issue.

For example, with -k 0.25 -K 4, the gain values are clamped to the range of -2 to 2.
In low-luminance areas of SDR or HDR, gain values can become extreme—such as -19 or 5—essentially in a random manner. Even if these are clamped to -2 to 2, the gain map in low-luminance regions still ends up randomly fluctuating within that range.
In contrast, the method used in my pull request results in smoother variations in the gain map within low-luminance regions.

ultrahdr_app.exe -m 0 -p hlg.raw -y sdr.rgba -b 3 -h 5760 -w 3840 -a 0 -C 2 -c 0 -t 1 -R 0 -s 1 -q 80 -Q 80 -G 1.0 -M 1 -e 1 -D 1 -k 0.25 -K 4 -z uhdr5.jpg
Ultra HDR Gain map
uhdr5 uhdr5_gainmap
<x:xmpmeta
  xmlns:x="adobe:ns:meta/"
  x:xmptk="Adobe XMP Core 5.1.2">
  <rdf:RDF
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
    <rdf:Description
      xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
      hdrgm:Version="1.0"
      hdrgm:GainMapMin="-2"
      hdrgm:GainMapMax="2"
      hdrgm:Gamma="1"
      hdrgm:OffsetSDR="1e-07"
      hdrgm:OffsetHDR="1e-07"
      hdrgm:HDRCapacityMin="0"
      hdrgm:HDRCapacityMax="2.30045"
      hdrgm:BaseRenditionIsHDR="False"/>
  </rdf:RDF>
</x:xmpmeta>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants