mirror of
https://github.com/samsonjs/media.git
synced 2026-03-28 09:55:48 +00:00
HDR: Implement PQ to SDR tone-mapping.
Tested manually on the Pixel 7 and Samsung S10. PiperOrigin-RevId: 501626354
This commit is contained in:
parent
91d43dc915
commit
4ade37c48d
9 changed files with 138 additions and 35 deletions
|
|
@ -88,13 +88,20 @@ public final class GlEffectsFrameProcessorPixelTest {
|
|||
"media/bitmap/sample_mp4_first_frame/electrical_colors/grayscale_then_increase_red_channel.png";
|
||||
// This file is generated on a Pixel 7, because the emulator isn't able to decode HLG to generate
|
||||
// this file.
|
||||
public static final String TONE_MAP_HDR_TO_SDR_PNG_ASSET_PATH =
|
||||
"media/bitmap/sample_mp4_first_frame/electrical_colors/tone_map_hdr_to_sdr.png";
|
||||
public static final String TONE_MAP_HLG_TO_SDR_PNG_ASSET_PATH =
|
||||
"media/bitmap/sample_mp4_first_frame/electrical_colors/tone_map_hlg_to_sdr.png";
|
||||
// This file is generated on a Pixel 7, because the emulator isn't able to decode PQ to generate
|
||||
// this file.
|
||||
public static final String TONE_MAP_PQ_TO_SDR_PNG_ASSET_PATH =
|
||||
"media/bitmap/sample_mp4_first_frame/electrical_colors/tone_map_pq_to_sdr.png";
|
||||
|
||||
/** Input video of which we only use the first frame. */
|
||||
private static final String INPUT_SDR_MP4_ASSET_STRING = "media/mp4/sample.mp4";
|
||||
/** Input HLG video of which we only use the first frame. */
|
||||
private static final String INPUT_HLG_MP4_ASSET_STRING = "media/mp4/hlg-1080p.mp4";
|
||||
/** Input PQ video of which we only use the first frame. */
|
||||
private static final String INPUT_PQ_MP4_ASSET_STRING = "media/mp4/hdr10-1080p.mp4";
|
||||
|
||||
/**
|
||||
* Time to wait for the decoded frame to populate the {@link GlEffectsFrameProcessor} instance's
|
||||
* input surface and the {@link GlEffectsFrameProcessor} to finish processing the frame, in
|
||||
|
|
@ -399,24 +406,58 @@ public final class GlEffectsFrameProcessorPixelTest {
|
|||
// TODO(b/239735341): Move this test to mobileharness testing.
|
||||
String testId = "drawHlgFrame_toneMap";
|
||||
ColorInfo hlgColor =
|
||||
new ColorInfo(
|
||||
C.COLOR_SPACE_BT2020,
|
||||
C.COLOR_RANGE_LIMITED,
|
||||
C.COLOR_TRANSFER_HLG,
|
||||
/* hdrStaticInfo= */ null);
|
||||
new ColorInfo.Builder()
|
||||
.setColorSpace(C.COLOR_SPACE_BT2020)
|
||||
.setColorRange(C.COLOR_RANGE_LIMITED)
|
||||
.setColorTransfer(C.COLOR_TRANSFER_HLG)
|
||||
.build();
|
||||
ColorInfo toneMapSdrColor =
|
||||
new ColorInfo(
|
||||
C.COLOR_SPACE_BT709,
|
||||
C.COLOR_RANGE_LIMITED,
|
||||
C.COLOR_TRANSFER_GAMMA_2_2,
|
||||
/* hdrStaticInfo= */ null);
|
||||
new ColorInfo.Builder()
|
||||
.setColorSpace(C.COLOR_SPACE_BT709)
|
||||
.setColorRange(C.COLOR_RANGE_LIMITED)
|
||||
.setColorTransfer(C.COLOR_TRANSFER_GAMMA_2_2)
|
||||
.build();
|
||||
setUpAndPrepareFirstFrame(
|
||||
INPUT_HLG_MP4_ASSET_STRING,
|
||||
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
|
||||
/* inputColorInfo= */ hlgColor,
|
||||
/* outputColorInfo= */ toneMapSdrColor,
|
||||
/* effects= */ ImmutableList.of());
|
||||
Bitmap expectedBitmap = readBitmap(TONE_MAP_HDR_TO_SDR_PNG_ASSET_PATH);
|
||||
Bitmap expectedBitmap = readBitmap(TONE_MAP_HLG_TO_SDR_PNG_ASSET_PATH);
|
||||
|
||||
Bitmap actualBitmap = processFirstFrameAndEnd();
|
||||
|
||||
maybeSaveTestBitmapToCacheDirectory(testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||
// TODO(b/207848601): switch to using proper tooling for testing against golden data.
|
||||
float averagePixelAbsoluteDifference =
|
||||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
|
||||
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("b/261877288 Test can only run on physical devices because decoder can't decode PQ.")
|
||||
public void drawPqFrame_toneMap_producesExpectedOutput() throws Exception {
|
||||
// TODO(b/239735341): Move this test to mobileharness testing.
|
||||
String testId = "drawPqFrame_toneMap";
|
||||
ColorInfo pqColor =
|
||||
new ColorInfo.Builder()
|
||||
.setColorSpace(C.COLOR_SPACE_BT2020)
|
||||
.setColorRange(C.COLOR_RANGE_LIMITED)
|
||||
.setColorTransfer(C.COLOR_TRANSFER_ST2084)
|
||||
.build();
|
||||
ColorInfo toneMapSdrColor =
|
||||
new ColorInfo.Builder()
|
||||
.setColorSpace(C.COLOR_SPACE_BT709)
|
||||
.setColorRange(C.COLOR_RANGE_LIMITED)
|
||||
.setColorTransfer(C.COLOR_TRANSFER_GAMMA_2_2)
|
||||
.build();
|
||||
setUpAndPrepareFirstFrame(
|
||||
INPUT_PQ_MP4_ASSET_STRING,
|
||||
DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO,
|
||||
/* inputColorInfo= */ pqColor,
|
||||
/* outputColorInfo= */ toneMapSdrColor,
|
||||
/* effects= */ ImmutableList.of());
|
||||
Bitmap expectedBitmap = readBitmap(TONE_MAP_PQ_TO_SDR_PNG_ASSET_PATH);
|
||||
|
||||
Bitmap actualBitmap = processFirstFrameAndEnd();
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ uniform int uOutputColorTransfer;
|
|||
in vec2 vTexSamplingCoord;
|
||||
out vec4 outColor;
|
||||
|
||||
// LINT.IfChange(color_transfer)
|
||||
const int COLOR_TRANSFER_LINEAR = 1;
|
||||
const int COLOR_TRANSFER_GAMMA_2_2 = 10;
|
||||
const int COLOR_TRANSFER_ST2084 = 6;
|
||||
const int COLOR_TRANSFER_HLG = 7;
|
||||
|
||||
// TODO(b/227624622): Consider using mediump to save precision, if it won't lead
|
||||
// to noticeable quantization errors.
|
||||
|
||||
|
|
@ -93,10 +99,6 @@ highp vec3 pqEotf(highp vec3 pqColor) {
|
|||
// Applies the appropriate EOTF to convert nonlinear electrical values to linear
|
||||
// optical values. Input and output are both normalized to [0, 1].
|
||||
highp vec3 applyEotf(highp vec3 electricalColor) {
|
||||
// LINT.IfChange(color_transfer)
|
||||
const int COLOR_TRANSFER_ST2084 = 6;
|
||||
const int COLOR_TRANSFER_HLG = 7;
|
||||
|
||||
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
|
||||
return pqEotf(electricalColor);
|
||||
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
|
||||
|
|
@ -136,6 +138,25 @@ highp vec3 applyHlgBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
|
|||
return linearRgbBt709;
|
||||
}
|
||||
|
||||
// Apply the PQ BT2020 to BT709 OOTF.
|
||||
highp vec3 applyPqBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
|
||||
float pqPeakLuminance = 10000.0;
|
||||
float sdrPeakLuminance = 500.0;
|
||||
|
||||
return linearRgbBt2020 * pqPeakLuminance / sdrPeakLuminance;
|
||||
}
|
||||
|
||||
highp vec3 applyBt2020ToBt709Ootf(highp vec3 linearRgbBt2020) {
|
||||
if (uInputColorTransfer == COLOR_TRANSFER_ST2084) {
|
||||
return applyPqBt2020ToBt709Ootf(linearRgbBt2020);
|
||||
} else if (uInputColorTransfer == COLOR_TRANSFER_HLG) {
|
||||
return applyHlgBt2020ToBt709Ootf(linearRgbBt2020);
|
||||
} else {
|
||||
// Output red as an obviously visible error.
|
||||
return vec3(1.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// BT.2100 / BT.2020 HLG OETF for one channel.
|
||||
highp float hlgOetfSingleChannel(highp float linearChannel) {
|
||||
// Specification:
|
||||
|
|
@ -194,11 +215,6 @@ vec3 gamma22Oetf(highp vec3 linearColor) {
|
|||
// Applies the appropriate OETF to convert linear optical signals to nonlinear
|
||||
// electrical signals. Input and output are both normalized to [0, 1].
|
||||
highp vec3 applyOetf(highp vec3 linearColor) {
|
||||
// LINT.IfChange(color_transfer_oetf)
|
||||
const int COLOR_TRANSFER_LINEAR = 1;
|
||||
const int COLOR_TRANSFER_GAMMA_2_2 = 10;
|
||||
const int COLOR_TRANSFER_ST2084 = 6;
|
||||
const int COLOR_TRANSFER_HLG = 7;
|
||||
if (uOutputColorTransfer == COLOR_TRANSFER_ST2084) {
|
||||
return pqOetf(linearColor);
|
||||
} else if (uOutputColorTransfer == COLOR_TRANSFER_HLG) {
|
||||
|
|
@ -221,9 +237,8 @@ vec3 yuvToRgb(vec3 yuv) {
|
|||
void main() {
|
||||
vec3 srcYuv = texture(uTexSampler, vTexSamplingCoord).xyz;
|
||||
vec3 opticalColorBt2020 = applyEotf(yuvToRgb(srcYuv));
|
||||
// TODO(b/239735341): Add support for PQ tone-mapping.
|
||||
vec4 opticalColor = (uApplyHdrToSdrToneMapping == 1)
|
||||
? vec4(applyHlgBt2020ToBt709Ootf(opticalColorBt2020), 1.0)
|
||||
? vec4(applyBt2020ToBt709Ootf(opticalColorBt2020), 1.0)
|
||||
: vec4(opticalColorBt2020, 1.0);
|
||||
vec4 transformedColors = uRgbMatrix * opticalColor;
|
||||
outColor = vec4(applyOetf(transformedColors.rgb), 1.0);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ vec3 smpte170mOetf(vec3 opticalColor) {
|
|||
// Applies the appropriate OETF to convert linear optical signals to nonlinear
|
||||
// electrical signals. Input and output are both normalized to [0, 1].
|
||||
highp vec3 applyOetf(highp vec3 linearColor) {
|
||||
// LINT.IfChange(color_transfer_oetf)
|
||||
// LINT.IfChange(color_transfer)
|
||||
const int COLOR_TRANSFER_LINEAR = 1;
|
||||
const int COLOR_TRANSFER_SDR_VIDEO = 3;
|
||||
if (uOutputColorTransfer == COLOR_TRANSFER_LINEAR) {
|
||||
|
|
|
|||
|
|
@ -90,16 +90,15 @@ public final class GlEffectsFrameProcessor implements FrameProcessor {
|
|||
|
||||
if (inputColorInfo.colorSpace != outputColorInfo.colorSpace
|
||||
|| ColorInfo.isTransferHdr(inputColorInfo) != ColorInfo.isTransferHdr(outputColorInfo)) {
|
||||
// GL Tone mapping is only implemented for BT2020 to BT709 and HLG to SDR (Gamma 2.2).
|
||||
// GL Tone mapping is only implemented for BT2020 to BT709 and HDR to SDR (Gamma 2.2).
|
||||
// Gamma 2.2 is used instead of SMPTE 170M for SDR, despite MediaFormat's
|
||||
// COLOR_TRANSFER_SDR_VIDEO being defined as SMPTE 170M. This is to match
|
||||
// other known tone-mapping behavior within the Android ecosystem.
|
||||
// TODO(b/239735341): Consider migrating SDR outside tone-mapping from SMPTE
|
||||
// 170M to gamma 2.2.
|
||||
// TODO(b/239735341): Implement PQ tone-mapping to reduce the scope of these checks.
|
||||
checkArgument(inputColorInfo.colorSpace == C.COLOR_SPACE_BT2020);
|
||||
checkArgument(outputColorInfo.colorSpace != C.COLOR_SPACE_BT2020);
|
||||
checkArgument(inputColorInfo.colorTransfer == C.COLOR_TRANSFER_HLG);
|
||||
checkArgument(ColorInfo.isTransferHdr(inputColorInfo));
|
||||
checkArgument(outputColorInfo.colorTransfer == C.COLOR_TRANSFER_GAMMA_2_2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -217,11 +217,8 @@ import java.util.List;
|
|||
? BT2020_FULL_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX
|
||||
: BT2020_LIMITED_RANGE_YUV_TO_RGB_COLOR_TRANSFORM_MATRIX);
|
||||
|
||||
@C.ColorTransfer int inputColorTransfer = inputColorInfo.colorTransfer;
|
||||
checkArgument(
|
||||
inputColorTransfer == C.COLOR_TRANSFER_HLG
|
||||
|| inputColorTransfer == C.COLOR_TRANSFER_ST2084);
|
||||
glProgram.setIntUniform("uInputColorTransfer", inputColorTransfer);
|
||||
checkArgument(ColorInfo.isTransferHdr(inputColorInfo));
|
||||
glProgram.setIntUniform("uInputColorTransfer", inputColorInfo.colorTransfer);
|
||||
// TODO(b/239735341): Add a setBooleanUniform method to GlProgram.
|
||||
glProgram.setIntUniform(
|
||||
"uApplyHdrToSdrToneMapping",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
package com.google.android.exoplayer2.transformer.mh;
|
||||
|
||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_1080P_4_SECOND_HDR10;
|
||||
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_1080P_4_SECOND_HDR10_FORMAT;
|
||||
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_1080P_5_SECOND_HLG10;
|
||||
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_1080P_5_SECOND_HLG10_FORMAT;
|
||||
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.recordTestSkipped;
|
||||
|
|
@ -97,4 +99,54 @@ public class ToneMapHdrToSdrUsingOpenGlTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transform_toneMap_hdr10File_toneMapsOrThrows() throws Exception {
|
||||
String testId = "transform_glToneMap_hdr10File_toneMapsOrThrows";
|
||||
|
||||
if (Util.SDK_INT < 29) {
|
||||
recordTestSkipped(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
testId,
|
||||
/* reason= */ "OpenGL-based HDR to SDR tone mapping is only supported on API 29+.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GlUtil.isYuvTargetExtensionSupported()) {
|
||||
recordTestSkipped(
|
||||
getApplicationContext(), testId, /* reason= */ "Device lacks YUV extension support.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (AndroidTestUtil.skipAndLogIfInsufficientCodecSupport(
|
||||
getApplicationContext(),
|
||||
testId,
|
||||
/* decodingFormat= */ MP4_ASSET_1080P_4_SECOND_HDR10_FORMAT,
|
||||
/* encodingFormat= */ null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
Transformer transformer =
|
||||
new Transformer.Builder(context)
|
||||
.setTransformationRequest(
|
||||
new TransformationRequest.Builder()
|
||||
.setHdrMode(TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
|
||||
.build())
|
||||
.build();
|
||||
try {
|
||||
TransformationTestResult transformationTestResult =
|
||||
new TransformerAndroidTestRunner.Builder(context, transformer)
|
||||
.build()
|
||||
.run(testId, MediaItem.fromUri(Uri.parse(MP4_ASSET_1080P_4_SECOND_HDR10)));
|
||||
Log.i(TAG, "Tone mapped.");
|
||||
assertFileHasColorTransfer(transformationTestResult.filePath, C.COLOR_TRANSFER_SDR);
|
||||
} catch (TransformationException exception) {
|
||||
Log.i(TAG, checkNotNull(exception.getCause()).toString());
|
||||
if (exception.errorCode != TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ public final class TransformationRequest {
|
|||
* Tone map HDR input to SDR before processing, to generate SDR output, using an OpenGL
|
||||
* tone-mapper.
|
||||
*
|
||||
* <p>Supported on API 29+, for HLG input.
|
||||
* <p>Supported on API 29+.
|
||||
*
|
||||
* <p>This may exhibit mild differences from {@link
|
||||
* #HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_MEDIACODEC}, depending on the device's tone-mapping
|
||||
|
|
@ -86,7 +86,6 @@ public final class TransformationRequest {
|
|||
*
|
||||
* <p>If not supported, {@link Transformer} throws a {@link TransformationException}.
|
||||
*/
|
||||
// TODO(b/239735341): Implement PQ tone-mapping to remove the HLG reference.
|
||||
public static final int HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL = 2;
|
||||
/**
|
||||
* Interpret HDR input as SDR, likely with a washed out look.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 660 KiB |
Loading…
Reference in a new issue