Add LUT functionalities to transformer.

* Adds SDR 3D LUT functionalities with OpenGL 2.0 support.

PiperOrigin-RevId: 474561060
This commit is contained in:
leonwind 2022-09-15 14:49:19 +00:00 committed by Marc Baechinger
parent ab6562e052
commit f55a5146e0
9 changed files with 735 additions and 0 deletions

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

@ -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