mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add LUT functionalities to transformer.
* Adds SDR 3D LUT functionalities with OpenGL 2.0 support. PiperOrigin-RevId: 474561060
This commit is contained in:
parent
ab6562e052
commit
f55a5146e0
@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.effect.BitmapTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
|
||||||
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.opengl.EGLContext;
|
||||||
|
import android.opengl.EGLDisplay;
|
||||||
|
import android.opengl.EGLSurface;
|
||||||
|
import android.util.Pair;
|
||||||
|
import androidx.media3.common.FrameProcessingException;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pixel test for Lookup Tables via {@link ColorLutProcessor}.
|
||||||
|
*
|
||||||
|
* <p>Expected images are taken from an emulator, so tests on different emulators or physical
|
||||||
|
* devices may fail. To test on other devices, please increase the {@link
|
||||||
|
* BitmapTestUtil#MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} and/or inspect the saved output bitmaps
|
||||||
|
* as recommended in {@link GlEffectsFrameProcessorPixelTest}.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class SingleColorLutPixelTest {
|
||||||
|
public static final String ORIGINAL_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/original.png";
|
||||||
|
public static final String LUT_MAP_WHITE_TO_GREEN_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/lut_map_white_to_green.png";
|
||||||
|
public static final String GRAYSCALE_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/grayscale.png";
|
||||||
|
public static final String INVERT_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/invert.png";
|
||||||
|
public static final String VERTICAL_HALD_IDENTITY_LUT = "media/bitmap/lut/identity.png";
|
||||||
|
public static final String VERTICAL_HALD_GRAYSCALE_LUT = "media/bitmap/lut/grayscale.png";
|
||||||
|
public static final String VERTICAL_HALD_INVERTED_LUT = "media/bitmap/lut/inverted.png";
|
||||||
|
|
||||||
|
private final Context context = getApplicationContext();
|
||||||
|
|
||||||
|
private @MonotonicNonNull EGLDisplay eglDisplay;
|
||||||
|
private @MonotonicNonNull EGLContext eglContext;
|
||||||
|
private @MonotonicNonNull EGLSurface placeholderEglSurface;
|
||||||
|
private @MonotonicNonNull SingleFrameGlTextureProcessor colorLutProcessor;
|
||||||
|
private int inputTexId;
|
||||||
|
private int outputTexId;
|
||||||
|
private int inputWidth;
|
||||||
|
private int inputHeight;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void createGlObjects() throws Exception {
|
||||||
|
eglDisplay = GlUtil.createEglDisplay();
|
||||||
|
eglContext = GlUtil.createEglContext(eglDisplay);
|
||||||
|
|
||||||
|
Bitmap inputBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
inputWidth = inputBitmap.getWidth();
|
||||||
|
inputHeight = inputBitmap.getHeight();
|
||||||
|
|
||||||
|
placeholderEglSurface = GlUtil.createPlaceholderEglSurface(eglDisplay);
|
||||||
|
GlUtil.focusEglSurface(eglDisplay, eglContext, placeholderEglSurface, inputWidth, inputHeight);
|
||||||
|
inputTexId = BitmapTestUtil.createGlTextureFromBitmap(inputBitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void release() throws GlUtil.GlException, FrameProcessingException {
|
||||||
|
if (colorLutProcessor != null) {
|
||||||
|
colorLutProcessor.release();
|
||||||
|
}
|
||||||
|
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_identityCubeLutSize2_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_identityLutCubeSize2";
|
||||||
|
int[][][] cubeIdentityLut = createIdentityLutCube(/* length= */ 2);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromCube(cubeIdentityLut)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_identityCubeLutSize64_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_identityLutCubeSize64";
|
||||||
|
int[][][] cubeIdentityLut = createIdentityLutCube(/* length= */ 64);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromCube(cubeIdentityLut)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_identityBitmapLutSize2_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_identityBitmapLutSize2";
|
||||||
|
Bitmap bitmapLut = createIdentityLutBitmap(/* length= */ 2);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromBitmap(bitmapLut)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_identityBitmapLutSize64_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_identityBitmapLutSize64";
|
||||||
|
Bitmap bitmapLut = createIdentityLutBitmap(/* length= */ 64);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromBitmap(bitmapLut)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_identityLutFromHaldImage_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_identityLutFromHaldImage";
|
||||||
|
Bitmap bitmapLut = BitmapTestUtil.readBitmap(VERTICAL_HALD_IDENTITY_LUT);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromBitmap(bitmapLut)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_mapWhiteToGreen_producesGreenHighlights() throws Exception {
|
||||||
|
String testId = "drawFrame_mapWhiteToGreen";
|
||||||
|
int length = 3;
|
||||||
|
int[][][] mapWhiteToGreen = createIdentityLutCube(length);
|
||||||
|
mapWhiteToGreen[length - 1][length - 1][length - 1] = Color.GREEN;
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromCube(mapWhiteToGreen)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(LUT_MAP_WHITE_TO_GREEN_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_applyInvertedLut_producesInvertedFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_applyInvertedLut";
|
||||||
|
Bitmap invertedLutBitmap = BitmapTestUtil.readBitmap(VERTICAL_HALD_INVERTED_LUT);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromBitmap(invertedLutBitmap)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(INVERT_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_applyGrayscaleLut_producesGrayscaleFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_applyGrayscaleLut";
|
||||||
|
Bitmap grayscaleLutBitmap = BitmapTestUtil.readBitmap(VERTICAL_HALD_GRAYSCALE_LUT);
|
||||||
|
colorLutProcessor =
|
||||||
|
SingleColorLut.createFromBitmap(grayscaleLutBitmap)
|
||||||
|
.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = colorLutProcessor.configure(inputWidth, inputHeight);
|
||||||
|
setupOutputTexture(outputSize.first, outputSize.second);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(GRAYSCALE_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
colorLutProcessor.drawFrame(inputTexId, /* presentationTimeUs = */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(testId, "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupOutputTexture(int outputWidth, int outputHeight) throws GlUtil.GlException {
|
||||||
|
outputTexId =
|
||||||
|
GlUtil.createTexture(
|
||||||
|
outputWidth, outputHeight, /* useHighPrecisionColorComponents= */ false);
|
||||||
|
int frameBuffer = GlUtil.createFboForTexture(outputTexId);
|
||||||
|
GlUtil.focusFramebuffer(
|
||||||
|
checkNotNull(eglDisplay),
|
||||||
|
checkNotNull(eglContext),
|
||||||
|
checkNotNull(placeholderEglSurface),
|
||||||
|
frameBuffer,
|
||||||
|
outputWidth,
|
||||||
|
outputHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[][][] createIdentityLutCube(int length) {
|
||||||
|
int[][][] lut = new int[length][length][length];
|
||||||
|
float scale = 1f / (length - 1);
|
||||||
|
for (int r = 0; r < length; r++) {
|
||||||
|
for (int g = 0; g < length; g++) {
|
||||||
|
for (int b = 0; b < length; b++) {
|
||||||
|
lut[r][g][b] =
|
||||||
|
Color.rgb(/* red= */ r * scale, /* green= */ g * scale, /* blue= */ b * scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lut;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap createIdentityLutBitmap(int length) {
|
||||||
|
int[][][] lutCube = createIdentityLutCube(length);
|
||||||
|
int[] colors = new int[length * length * length];
|
||||||
|
|
||||||
|
for (int r = 0; r < length; r++) {
|
||||||
|
for (int g = 0; g < length; g++) {
|
||||||
|
for (int b = 0; b < length; b++) {
|
||||||
|
int color = lutCube[r][g][b];
|
||||||
|
int planePosition = b + length * (g + length * r);
|
||||||
|
colors[planePosition] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
colors, /* width= */ length, /* height= */ length * length, Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
#version 100
|
||||||
|
// Copyright 2022 The Android Open Source Project
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// ES2 fragment shader that samples from a (non-external) texture with
|
||||||
|
// uTexSampler, copying from this texture to the current output while
|
||||||
|
// applying a 3D color lookup table to change the pixel colors.
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
uniform sampler2D uTexSampler;
|
||||||
|
// The uColorLut texture is a N x N^2 2D texture where each z-plane of the 3D
|
||||||
|
// LUT is vertically stacked on top of each other. The red channel of the input
|
||||||
|
// color (z-axis in LUT[R][G][B] = LUT[z][y][x]) points to the plane to sample
|
||||||
|
// from. For more information check the
|
||||||
|
// androidx/media3/effect/SingleColorLut.java class, especially the function
|
||||||
|
// #transformCubeIntoBitmap with a provided example.
|
||||||
|
uniform sampler2D uColorLut;
|
||||||
|
uniform float uColorLutLength;
|
||||||
|
varying vec2 vTexSamplingCoord;
|
||||||
|
|
||||||
|
// Applies the color lookup using uLut based on the input colors.
|
||||||
|
vec3 applyLookup(vec3 color) {
|
||||||
|
// Reminder: Inside OpenGL vector.xyz is the same as vector.rgb.
|
||||||
|
// Here we use mentions of x and y coordinates to references to
|
||||||
|
// the position to sample from inside the 2D LUT plane and
|
||||||
|
// rgb to create the 3D coordinates based on the input colors.
|
||||||
|
|
||||||
|
// To sample from the 3D LUT we interpolate bilinearly twice in the 2D LUT
|
||||||
|
// to replicate the trilinear interpolation in a 3D LUT. Thus we sample
|
||||||
|
// from the plane of position redCoordLow and on the plane above.
|
||||||
|
// redCoordLow points to the lower plane to sample from.
|
||||||
|
float redCoord = color.r * (uColorLutLength - 1.0);
|
||||||
|
// Clamping to uColorLutLength - 2 is only needed if redCoord points to the
|
||||||
|
// most upper plane. In this case there would not be any plane above
|
||||||
|
// available to sample from.
|
||||||
|
float redCoordLow = clamp(floor(redCoord), 0.0, uColorLutLength - 2.0);
|
||||||
|
|
||||||
|
// lowerY is indexed in two steps. First redCoordLow defines the plane to
|
||||||
|
// sample from. Next the green color component is added to index the row in
|
||||||
|
// the found plane. As described in the NVIDIA blog article about LUTs
|
||||||
|
// https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-24-using-lookup-tables-accelerate-color
|
||||||
|
// (Section 24.2), we sample from color * scale + offset, where offset is
|
||||||
|
// defined by 1 / (2 * uColorLutLength) and the scale is defined by
|
||||||
|
// (uColorLutLength - 1.0) / uColorLutLength.
|
||||||
|
|
||||||
|
// The following derives the equation of lowerY. For this let
|
||||||
|
// N = uColorLutLenght. The general formula to sample at row y
|
||||||
|
// is defined as y = N * r + g.
|
||||||
|
// Using the offset and scale as described in NVIDIA's blog article we get:
|
||||||
|
// y = offset + (N * r + g) * scale
|
||||||
|
// y = 1 / (2 * N) + (N * r + g) * (N - 1) / N
|
||||||
|
// y = 1 / (2 * N) + N * r * (N - 1) / N + g * (N - 1) / N
|
||||||
|
// We have defined redCoord as r * (N - 1) if we excluded the clamping for
|
||||||
|
// now, giving us:
|
||||||
|
// y = 1 / (2 * N) + N * redCoord / N + g * (N - 1) / N
|
||||||
|
// This simplifies to:
|
||||||
|
// y = 0.5 / N + (N * redCoord + g * (N - 1)) / N
|
||||||
|
// y = (0.5 + N * redCoord + g * (N - 1)) / N
|
||||||
|
// This formula now assumes a coordinate system in the range of [0, N] but
|
||||||
|
// OpenGL uses a [0, 1] unit coordinate system internally. Thus dividing
|
||||||
|
// by N gives us the final formula for y:
|
||||||
|
// y = ((0.5 + N * redCoord + g * (N - 1)) / N) / N
|
||||||
|
// y = (0.5 + redCoord * N + g * (N - 1)) / (N * N)
|
||||||
|
float lowerY =
|
||||||
|
(0.5
|
||||||
|
+ redCoordLow * uColorLutLength
|
||||||
|
+ color.g * (uColorLutLength - 1.0))
|
||||||
|
/ (uColorLutLength * uColorLutLength);
|
||||||
|
// The upperY is the same position moved up by one LUT plane.
|
||||||
|
float upperY = lowerY + 1.0 / uColorLutLength;
|
||||||
|
|
||||||
|
// The x position is the blue color channel (x-axis in LUT[R][G][B]).
|
||||||
|
float x = (0.5 + color.b * (uColorLutLength - 1.0)) / uColorLutLength;
|
||||||
|
|
||||||
|
vec3 lowerRgb = texture2D(uColorLut, vec2(x, lowerY)).rgb;
|
||||||
|
vec3 upperRgb = texture2D(uColorLut, vec2(x, upperY)).rgb;
|
||||||
|
|
||||||
|
// Linearly interpolate between lowerRgb and upperRgb based on the
|
||||||
|
// distance of the actual in the plane and the lower sampling position.
|
||||||
|
return mix(lowerRgb, upperRgb, redCoord - redCoordLow);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
|
||||||
|
|
||||||
|
gl_FragColor.rgb = applyLookup(inputColor.rgb);
|
||||||
|
gl_FragColor.a = inputColor.a;
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.media3.common.FrameProcessingException;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies color transformations using color lookup tables to apply to each frame in the fragment
|
||||||
|
* shader.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public interface ColorLut extends GlEffect {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OpenGL texture ID of the LUT to apply to the pixels of the frame with the given
|
||||||
|
* timestamp.
|
||||||
|
*/
|
||||||
|
int getLutTextureId(long presentationTimeUs);
|
||||||
|
|
||||||
|
/** Returns the length N of the 3D N x N x N LUT cube with the given timestamp. */
|
||||||
|
int getLength(long presentationTimeUs);
|
||||||
|
|
||||||
|
/** Releases the OpenGL texture of the LUT. */
|
||||||
|
void release() throws GlUtil.GlException;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default ColorLutProcessor toGlTextureProcessor(Context context, boolean useHdr)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
return new ColorLutProcessor(context, /* colorLut= */ this, useHdr);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.opengl.Matrix;
|
||||||
|
import android.util.Pair;
|
||||||
|
import androidx.media3.common.FrameProcessingException;
|
||||||
|
import androidx.media3.common.util.GlProgram;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/** Applies a {@link ColorLut} to each frame in the fragment shader. */
|
||||||
|
/* package */ final class ColorLutProcessor extends SingleFrameGlTextureProcessor {
|
||||||
|
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
|
||||||
|
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_lut_es2.glsl";
|
||||||
|
|
||||||
|
private final GlProgram glProgram;
|
||||||
|
private final ColorLut colorLut;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
* @param colorLut The {@link ColorLut} to apply to each frame in order.
|
||||||
|
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
|
||||||
|
* in linear RGB BT.2020. If {@code false}, colors will be in gamma RGB BT.709.
|
||||||
|
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||||
|
*/
|
||||||
|
public ColorLutProcessor(Context context, ColorLut colorLut, boolean useHdr)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
super(useHdr);
|
||||||
|
// TODO(b/246315245): Add HDR support.
|
||||||
|
checkArgument(!useHdr, "LutProcessor does not support HDR colors.");
|
||||||
|
this.colorLut = colorLut;
|
||||||
|
|
||||||
|
try {
|
||||||
|
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||||
|
} catch (IOException | GlUtil.GlException e) {
|
||||||
|
throw new FrameProcessingException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
|
||||||
|
glProgram.setBufferAttribute(
|
||||||
|
"aFramePosition",
|
||||||
|
GlUtil.getNormalizedCoordinateBounds(),
|
||||||
|
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||||
|
|
||||||
|
float[] identityMatrix = new float[16];
|
||||||
|
Matrix.setIdentityM(identityMatrix, /* smOffset= */ 0);
|
||||||
|
glProgram.setFloatsUniform("uTransformationMatrix", identityMatrix);
|
||||||
|
glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
|
||||||
|
return Pair.create(inputWidth, inputHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
|
||||||
|
try {
|
||||||
|
glProgram.use();
|
||||||
|
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||||
|
glProgram.setSamplerTexIdUniform(
|
||||||
|
"uColorLut", colorLut.getLutTextureId(presentationTimeUs), /* texUnitIndex= */ 1);
|
||||||
|
glProgram.setFloatUniform("uColorLutLength", colorLut.getLength(presentationTimeUs));
|
||||||
|
glProgram.bindAttributesAndUniforms();
|
||||||
|
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
throw new FrameProcessingException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() throws FrameProcessingException {
|
||||||
|
super.release();
|
||||||
|
try {
|
||||||
|
colorLut.release();
|
||||||
|
glProgram.delete();
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
throw new FrameProcessingException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.opengl.GLUtils;
|
||||||
|
import androidx.media3.common.FrameProcessingException;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
|
||||||
|
/** Transforms the colors of a frame by applying the same color lookup table to each frame. */
|
||||||
|
@UnstableApi
|
||||||
|
public class SingleColorLut implements ColorLut {
|
||||||
|
private final int lutTextureId;
|
||||||
|
private final int length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* <p>{@code lutCube} needs to be a {@code N x N x N} cube and each element is an integer
|
||||||
|
* representing a color using the {@link Bitmap.Config#ARGB_8888} format.
|
||||||
|
*/
|
||||||
|
public static SingleColorLut createFromCube(int[][][] lutCube) throws GlUtil.GlException {
|
||||||
|
checkArgument(
|
||||||
|
lutCube.length > 0 && lutCube[0].length > 0 && lutCube[0][0].length > 0,
|
||||||
|
"LUT must have three dimensions.");
|
||||||
|
checkArgument(
|
||||||
|
lutCube.length == lutCube[0].length && lutCube.length == lutCube[0][0].length,
|
||||||
|
Util.formatInvariant(
|
||||||
|
"All three dimensions of a LUT must match, received %d x %d x %d.",
|
||||||
|
lutCube.length, lutCube[0].length, lutCube[0][0].length));
|
||||||
|
|
||||||
|
return new SingleColorLut(transformCubeIntoBitmap(lutCube));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* <p>LUT needs to be a Bitmap of a flattened HALD image of width {@code N} and height {@code
|
||||||
|
* N^2}. Each element must be an integer representing a color using the {@link
|
||||||
|
* Bitmap.Config#ARGB_8888} format.
|
||||||
|
*/
|
||||||
|
public static SingleColorLut createFromBitmap(Bitmap lut) throws GlUtil.GlException {
|
||||||
|
checkArgument(
|
||||||
|
lut.getWidth() * lut.getWidth() == lut.getHeight(),
|
||||||
|
Util.formatInvariant(
|
||||||
|
"LUT needs to be in a N x N^2 format, received %d x %d.",
|
||||||
|
lut.getWidth(), lut.getHeight()));
|
||||||
|
checkArgument(
|
||||||
|
lut.getConfig() == Bitmap.Config.ARGB_8888, "Color representation needs to be ARGB_8888.");
|
||||||
|
|
||||||
|
return new SingleColorLut(lut);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SingleColorLut(Bitmap lut) throws GlUtil.GlException {
|
||||||
|
length = lut.getWidth();
|
||||||
|
lutTextureId = storeLutAsTexture(lut);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int storeLutAsTexture(Bitmap bitmap) throws GlUtil.GlException {
|
||||||
|
int lutTextureId =
|
||||||
|
GlUtil.createTexture(
|
||||||
|
bitmap.getWidth(), bitmap.getHeight(), /* useHighPrecisionColorComponents= */ false);
|
||||||
|
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, bitmap, /* border= */ 0);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
return lutTextureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the N x N x N {@code cube} into a N x N^2 {@code bitmap}.
|
||||||
|
*
|
||||||
|
* @param cube The 3D Color Lut which gets indexed using {@code cube[R][G][B]}.
|
||||||
|
* @return A {@link Bitmap} of size {@code N x N^2}, where the {@code cube[R][G][B]} color can be
|
||||||
|
* indexed at {@code bitmap.getColor(B, N * R + G)}.
|
||||||
|
*/
|
||||||
|
private static Bitmap transformCubeIntoBitmap(int[][][] cube) {
|
||||||
|
// The support for 3D textures starts in OpenGL 3.0 and the Android API 8, Version 2.2
|
||||||
|
// uses OpenGL 2.0 which only supports 2D textures. Thus we need to transform the 3D LUT
|
||||||
|
// into 2D to support all Android SDKs.
|
||||||
|
|
||||||
|
// The cube consists of N planes on the z-direction in the coordinate system where each plane
|
||||||
|
// has a size of N x N. To transform the cube into a 2D bitmap we stack each N x N plane
|
||||||
|
// vertically on top of each other. This gives us a bitmap of width N and height N^2.
|
||||||
|
//
|
||||||
|
// As an example, lets take the following 3D identity LUT of size 2x2x2:
|
||||||
|
// cube = [
|
||||||
|
// [[(0, 0, 0), (0, 0, 1)],
|
||||||
|
// [(0, 1, 0), (0, 1, 1)]],
|
||||||
|
// [[(1, 0, 0), (1, 0, 1)],
|
||||||
|
// [(1, 1, 0), (1, 1, 1)]]
|
||||||
|
// ];
|
||||||
|
// If we transform this cube now into a 2x2^2 = 2x4 bitmap we yield the following 2D plane:
|
||||||
|
// bitmap = [[(0, 0, 0), (0, 0, 1)],
|
||||||
|
// [(0, 1, 0), (0, 1, 1)],
|
||||||
|
// [(1, 0, 0), (1, 0, 1)],
|
||||||
|
// [(1, 1, 0), (1, 1, 1)]];
|
||||||
|
// media/bitmap/lut/identity.png is an example of how a 32x32x32 3D LUT looks like as an
|
||||||
|
// 32x32^2 bitmap.
|
||||||
|
int length = cube.length;
|
||||||
|
int[] bitmapColorsArray = new int[length * length * length];
|
||||||
|
|
||||||
|
for (int r = 0; r < length; r++) {
|
||||||
|
for (int g = 0; g < length; g++) {
|
||||||
|
for (int b = 0; b < length; b++) {
|
||||||
|
int color = cube[r][g][b];
|
||||||
|
int planePosition = b + length * (g + length * r);
|
||||||
|
bitmapColorsArray[planePosition] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bitmap.createBitmap(
|
||||||
|
bitmapColorsArray,
|
||||||
|
/* width= */ length,
|
||||||
|
/* height= */ length * length,
|
||||||
|
Bitmap.Config.ARGB_8888);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLutTextureId(long presentationTimeUs) {
|
||||||
|
return lutTextureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLength(long presentationTimeUs) {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() throws GlUtil.GlException {
|
||||||
|
GlUtil.deleteTexture(lutTextureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ColorLutProcessor toGlTextureProcessor(Context context, boolean useHdr)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
checkState(!useHdr, "HDR is currently not supported.");
|
||||||
|
return new ColorLutProcessor(context, /* colorLut= */ this, useHdr);
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 937 B |
Binary file not shown.
After Width: | Height: | Size: 937 B |
Binary file not shown.
After Width: | Height: | Size: 546 KiB |
Loading…
x
Reference in New Issue
Block a user