Add HSL Adjustments to the effects module.
PiperOrigin-RevId: 476144167
(cherry picked from commit dc9fa4f463)
|
|
@ -0,0 +1,360 @@
|
||||||
|
/*
|
||||||
|
* 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.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 java.io.IOException;
|
||||||
|
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 tests for {@link HslAdjustment}.
|
||||||
|
*
|
||||||
|
* <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 final class HslAdjustmentPixelTest {
|
||||||
|
public static final String ORIGINAL_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/original.png";
|
||||||
|
public static final String MINIMUM_SATURATION_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png";
|
||||||
|
public static final String MAXIMUM_SATURATION_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png";
|
||||||
|
public static final String ROTATE_HUE_BY_NEGATIVE_90_DEGREES_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_negative_90_degrees.png";
|
||||||
|
public static final String ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/rotate_hue_by_60_degrees.png";
|
||||||
|
public static final String DECREASE_LIGHTNESS_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png";
|
||||||
|
public static final String INCREASE_LIGHTNESS_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png";
|
||||||
|
public static final String ADJUST_ALL_HSL_SETTINGS_PNG_ASSET_PATH =
|
||||||
|
"media/bitmap/sample_mp4_first_frame/linear_colors/adjust_all_hsl_settings.png";
|
||||||
|
|
||||||
|
private final Context context = getApplicationContext();
|
||||||
|
|
||||||
|
private @MonotonicNonNull EGLDisplay eglDisplay;
|
||||||
|
private @MonotonicNonNull EGLContext eglContext;
|
||||||
|
private @MonotonicNonNull SingleFrameGlTextureProcessor hslProcessor;
|
||||||
|
private @MonotonicNonNull EGLSurface placeholderEglSurface;
|
||||||
|
private int inputTexId;
|
||||||
|
private int outputTexId;
|
||||||
|
private int inputWidth;
|
||||||
|
private int inputHeight;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void createGlObjects() throws IOException, GlUtil.GlException {
|
||||||
|
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);
|
||||||
|
|
||||||
|
outputTexId =
|
||||||
|
GlUtil.createTexture(inputWidth, inputHeight, /* useHighPrecisionColorComponents= */ false);
|
||||||
|
int frameBuffer = GlUtil.createFboForTexture(outputTexId);
|
||||||
|
GlUtil.focusFramebuffer(
|
||||||
|
eglDisplay, eglContext, placeholderEglSurface, frameBuffer, inputWidth, inputHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void release() throws GlUtil.GlException, FrameProcessingException {
|
||||||
|
if (hslProcessor != null) {
|
||||||
|
hslProcessor.release();
|
||||||
|
}
|
||||||
|
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_noOpAdjustment_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_noOpAdjustment";
|
||||||
|
HslAdjustment noOpAdjustment = new HslAdjustment.Builder().build();
|
||||||
|
hslProcessor = noOpAdjustment.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_minimumSaturation_producesGrayFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_minimumSaturation";
|
||||||
|
HslAdjustment minimumSaturation = new HslAdjustment.Builder().adjustSaturation(-100).build();
|
||||||
|
hslProcessor = minimumSaturation.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(MINIMUM_SATURATION_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_maximumSaturation_producesHighlySaturatedFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_maximumSaturation";
|
||||||
|
HslAdjustment maximumSaturation = new HslAdjustment.Builder().adjustSaturation(100).build();
|
||||||
|
hslProcessor = maximumSaturation.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(MAXIMUM_SATURATION_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_rotateHueByNegative90Degrees_producesExpectedOutput() throws Exception {
|
||||||
|
String testId = "drawFrame_rotateHueByNegative90Degrees";
|
||||||
|
HslAdjustment negativeHueRotation90Degrees = new HslAdjustment.Builder().adjustHue(-90).build();
|
||||||
|
hslProcessor = negativeHueRotation90Degrees.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap =
|
||||||
|
BitmapTestUtil.readBitmap(ROTATE_HUE_BY_NEGATIVE_90_DEGREES_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_rotateHueBy60Degrees_producesExpectedOutput() throws Exception {
|
||||||
|
String testId = "drawFrame_rotateHueBy60Degrees";
|
||||||
|
HslAdjustment hueRotation60Degrees = new HslAdjustment.Builder().adjustHue(60).build();
|
||||||
|
hslProcessor = hueRotation60Degrees.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_rotateHueByNegative300Degrees_producesSameOutputAsRotateBy60DegreesHue()
|
||||||
|
throws Exception {
|
||||||
|
String testId = "drawFrame_rotateHueByNegative300Degrees";
|
||||||
|
HslAdjustment hueRotation420Degrees = new HslAdjustment.Builder().adjustHue(-300).build();
|
||||||
|
hslProcessor = hueRotation420Degrees.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_HUE_BY_60_DEGREES_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_rotateHueBy360Degrees_leavesFrameUnchanged() throws Exception {
|
||||||
|
String testId = "drawFrame_rotateHueBy360Degrees";
|
||||||
|
HslAdjustment hueRotation360Degrees = new HslAdjustment.Builder().adjustHue(360).build();
|
||||||
|
hslProcessor = hueRotation360Degrees.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ORIGINAL_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_minimumLightness_producesBlackFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_minimumLightness";
|
||||||
|
HslAdjustment minimumLightness = new HslAdjustment.Builder().adjustLightness(-100).build();
|
||||||
|
hslProcessor = minimumLightness.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.BLACK);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_decreaseLightness_producesDarkerFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_decreaseLightness";
|
||||||
|
HslAdjustment decreasedLightness = new HslAdjustment.Builder().adjustLightness(-50).build();
|
||||||
|
hslProcessor = decreasedLightness.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(DECREASE_LIGHTNESS_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_increaseLightness_producesBrighterFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_increaseLightness";
|
||||||
|
HslAdjustment increasedLightness = new HslAdjustment.Builder().adjustLightness(50).build();
|
||||||
|
hslProcessor = increasedLightness.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(INCREASE_LIGHTNESS_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_maximumLightness_producesWhiteFrame() throws Exception {
|
||||||
|
String testId = "drawFrame_maximumLightness";
|
||||||
|
HslAdjustment maximumLightness = new HslAdjustment.Builder().adjustLightness(100).build();
|
||||||
|
hslProcessor = maximumLightness.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapWithSolidColor(inputWidth, inputHeight, Color.WHITE);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void drawFrame_adjustAllHslSettings_producesExpectedOutput() throws Exception {
|
||||||
|
String testId = "drawFrame_adjustAllHslSettings";
|
||||||
|
HslAdjustment allHslSettingsAdjusted =
|
||||||
|
new HslAdjustment.Builder().adjustHue(60).adjustSaturation(30).adjustLightness(50).build();
|
||||||
|
hslProcessor = allHslSettingsAdjusted.toGlTextureProcessor(context, /* useHdr= */ false);
|
||||||
|
Pair<Integer, Integer> outputSize = hslProcessor.configure(inputWidth, inputHeight);
|
||||||
|
Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ADJUST_ALL_HSL_SETTINGS_PNG_ASSET_PATH);
|
||||||
|
|
||||||
|
hslProcessor.drawFrame(inputTexId, /* presentationTimeUs= */ 0);
|
||||||
|
Bitmap actualBitmap =
|
||||||
|
BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(
|
||||||
|
outputSize.first, outputSize.second);
|
||||||
|
|
||||||
|
BitmapTestUtil.maybeSaveTestBitmapToCacheDirectory(
|
||||||
|
testId, /* bitmapLabel= */ "actual", actualBitmap);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
BitmapTestUtil.getAveragePixelAbsoluteDifferenceArgb8888(
|
||||||
|
expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference).isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
#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.
|
||||||
|
|
||||||
|
// ES 2 fragment shader that samples from a (non-external) texture with
|
||||||
|
// uTexSampler. It then converts the RGB color input into HSL and adjusts
|
||||||
|
// the Hue, Saturation, and Lightness and converts it then back to RGB.
|
||||||
|
|
||||||
|
// We use the algorithm based on the work by Sam Hocevar, which optimizes
|
||||||
|
// for an efficient branchless RGB <-> HSL conversion. A blog post is
|
||||||
|
// at https://www.chilliant.com/rgb2hsv.html and it is further explained at
|
||||||
|
// http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
uniform sampler2D uTexSampler;
|
||||||
|
// uHueAdjustmentDegrees, uSaturationAdjustment, and uLightnessAdjustment
|
||||||
|
// are normalized to the unit interval [0, 1].
|
||||||
|
uniform float uHueAdjustmentDegrees;
|
||||||
|
uniform float uSaturationAdjustment;
|
||||||
|
uniform float uLightnessAdjustment;
|
||||||
|
varying vec2 vTexSamplingCoord;
|
||||||
|
|
||||||
|
const float epsilon = 1e-10;
|
||||||
|
|
||||||
|
vec3 rgbToHcv(vec3 rgb) {
|
||||||
|
vec4 p = (rgb.g < rgb.b)
|
||||||
|
? vec4(rgb.bg, -1.0, 2.0 / 3.0)
|
||||||
|
: vec4(rgb.gb, 0.0, -1.0 / 3.0);
|
||||||
|
vec4 q = (rgb.r < p.x)
|
||||||
|
? vec4(p.xyw, rgb.r)
|
||||||
|
: vec4(rgb.r, p.yzx);
|
||||||
|
float c = q.x - min(q.w, q.y);
|
||||||
|
float h = abs((q.w - q.y) / (6.0 * c + epsilon) + q.z);
|
||||||
|
return vec3(h, c, q.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rgbToHsl(vec3 rgb) {
|
||||||
|
vec3 hcv = rgbToHcv(rgb);
|
||||||
|
float l = hcv.z - hcv.y * 0.5;
|
||||||
|
float s = hcv.y / (1.0 - abs(l * 2.0 - 1.0) + epsilon);
|
||||||
|
return vec3(hcv.x, s, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hueToRgb(float hue) {
|
||||||
|
float r = abs(hue * 6.0 - 3.0) - 1.0;
|
||||||
|
float g = 2.0 - abs(hue * 6.0 - 2.0);
|
||||||
|
float b = 2.0 - abs(hue * 6.0 - 4.0);
|
||||||
|
return clamp(vec3(r, g, b), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hslToRgb(vec3 hsl) {
|
||||||
|
vec3 rgb = hueToRgb(hsl.x);
|
||||||
|
float c = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
|
||||||
|
return (rgb - 0.5) * c + hsl.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
|
||||||
|
vec3 hslColor = rgbToHsl(inputColor.rgb);
|
||||||
|
|
||||||
|
hslColor.x = mod(hslColor.x + uHueAdjustmentDegrees, 1.0);
|
||||||
|
hslColor.y = clamp(hslColor.y + uSaturationAdjustment, 0.0, 1.0);
|
||||||
|
hslColor.z = clamp(hslColor.z + uLightnessAdjustment, 0.0, 1.0);
|
||||||
|
|
||||||
|
gl_FragColor = vec4(hslToRgb(hslColor), inputColor.a);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* 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 androidx.media3.common.FrameProcessingException;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
|
||||||
|
/** Adjusts the HSL (Hue, Saturation, and Lightness) of a frame. */
|
||||||
|
@UnstableApi
|
||||||
|
public class HslAdjustment implements GlEffect {
|
||||||
|
|
||||||
|
/** A builder for {@code HslAdjustment} instances. */
|
||||||
|
public static final class Builder {
|
||||||
|
private float hueAdjustment;
|
||||||
|
private float saturationAdjustment;
|
||||||
|
private float lightnessAdjustment;
|
||||||
|
|
||||||
|
/** Creates a new instance with the default values. */
|
||||||
|
public Builder() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the hue of the frame by {@code hueAdjustmentDegrees}.
|
||||||
|
*
|
||||||
|
* <p>The Hue of the frame is defined in the interval of [0, 360] degrees. The actual degrees of
|
||||||
|
* hue adjustment applied is {@code hueAdjustmentDegrees % 360}.
|
||||||
|
*
|
||||||
|
* @param hueAdjustmentDegrees The hue adjustment in rotation degrees. The default value is
|
||||||
|
* {@code 0}, which means no change is applied.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder adjustHue(float hueAdjustmentDegrees) {
|
||||||
|
hueAdjustment = hueAdjustmentDegrees % 360;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the saturation of the frame by {@code saturationAdjustment}.
|
||||||
|
*
|
||||||
|
* <p>Saturation is defined in the interval of [0, 100] where a saturation of {@code 0} will
|
||||||
|
* generate a grayscale frame and a saturation of {@code 100} has a maximum separation between
|
||||||
|
* the colors.
|
||||||
|
*
|
||||||
|
* @param saturationAdjustment The difference of how much the saturation will be adjusted in
|
||||||
|
* either direction. Needs to be in the interval of [-100, 100] and the default value is
|
||||||
|
* {@code 0}, which means no change is applied.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder adjustSaturation(float saturationAdjustment) {
|
||||||
|
checkArgument(
|
||||||
|
-100 <= saturationAdjustment && saturationAdjustment <= 100,
|
||||||
|
"Can adjust the saturation by only 100 in either direction, but provided "
|
||||||
|
+ saturationAdjustment);
|
||||||
|
this.saturationAdjustment = saturationAdjustment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the lightness of the frame by {@code lightnessAdjustment}.
|
||||||
|
*
|
||||||
|
* <p>Lightness is defined in the interval of [0, 100] where a lightness of {@code 0} is a black
|
||||||
|
* frame and a lightness of {@code 100} is a white frame.
|
||||||
|
*
|
||||||
|
* @param lightnessAdjustment The difference by how much the lightness will be adjusted in
|
||||||
|
* either direction. Needs to be in the interval of [-100, 100] and the default value is
|
||||||
|
* {@code 0}, which means no change is applied.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder adjustLightness(float lightnessAdjustment) {
|
||||||
|
checkArgument(
|
||||||
|
-100 <= lightnessAdjustment && lightnessAdjustment <= 100,
|
||||||
|
"Can adjust the lightness by only 100 in either direction, but provided "
|
||||||
|
+ lightnessAdjustment);
|
||||||
|
this.lightnessAdjustment = lightnessAdjustment;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a new {@link HslAdjustment} instance. */
|
||||||
|
public HslAdjustment build() {
|
||||||
|
return new HslAdjustment(hueAdjustment, saturationAdjustment, lightnessAdjustment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Indicates the hue adjustment in degrees. */
|
||||||
|
public final float hueAdjustmentDegrees;
|
||||||
|
/** Indicates the saturation adjustment. */
|
||||||
|
public final float saturationAdjustment;
|
||||||
|
/** Indicates the lightness adjustment. */
|
||||||
|
public final float lightnessAdjustment;
|
||||||
|
|
||||||
|
private HslAdjustment(
|
||||||
|
float hueAdjustmentDegrees, float saturationAdjustment, float lightnessAdjustment) {
|
||||||
|
this.hueAdjustmentDegrees = hueAdjustmentDegrees;
|
||||||
|
this.saturationAdjustment = saturationAdjustment;
|
||||||
|
this.lightnessAdjustment = lightnessAdjustment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HslProcessor toGlTextureProcessor(Context context, boolean useHdr)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
return new HslProcessor(context, /* hslAdjustment= */ this, useHdr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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 the {@link HslAdjustment} to each frame in the fragment shader. */
|
||||||
|
/* package */ final class HslProcessor 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_hsl_es2.glsl";
|
||||||
|
|
||||||
|
private final GlProgram glProgram;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
* @param hslAdjustment The {@link HslAdjustment} 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 HslProcessor(Context context, HslAdjustment hslAdjustment, boolean useHdr)
|
||||||
|
throws FrameProcessingException {
|
||||||
|
super(useHdr);
|
||||||
|
// TODO(b/241241680): Check if HDR <-> HSL works the same or not.
|
||||||
|
checkArgument(!useHdr, "HDR is not yet supported.");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// OpenGL operates in a [0, 1] unit range and thus we transform the HSL intervals into
|
||||||
|
// the unit interval as well. The hue is defined in the [0, 360] interval and saturation
|
||||||
|
// and lightness in the [0, 100] interval.
|
||||||
|
glProgram.setFloatUniform("uHueAdjustmentDegrees", hslAdjustment.hueAdjustmentDegrees / 360);
|
||||||
|
glProgram.setFloatUniform("uSaturationAdjustment", hslAdjustment.saturationAdjustment / 100);
|
||||||
|
glProgram.setFloatUniform("uLightnessAdjustment", hslAdjustment.lightnessAdjustment / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.bindAttributesAndUniforms();
|
||||||
|
|
||||||
|
// The four-vertex triangle strip forms a quad.
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
throw new FrameProcessingException(e, presentationTimeUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 544 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 511 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 535 KiB |
|
After Width: | Height: | Size: 552 KiB |