Add HSL Adjustments to the effects module.
PiperOrigin-RevId: 476144167
@ -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.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.util.GlUtil;
|
||||
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,118 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer2.util.Assertions.checkArgument;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
|
||||
/** Adjusts the HSL (Hue, Saturation, and Lightness) of a frame. */
|
||||
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 com.google.android.exoplayer2.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 com.google.android.exoplayer2.util.GlProgram;
|
||||
import com.google.android.exoplayer2.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 |
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/decrease_lightness.png
vendored
Normal file
After Width: | Height: | Size: 148 KiB |
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/increase_lightness.png
vendored
Normal file
After Width: | Height: | Size: 511 KiB |
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/maximum_saturation.png
vendored
Normal file
After Width: | Height: | Size: 560 KiB |
BIN
testdata/src/test/assets/media/bitmap/sample_mp4_first_frame/linear_colors/minimum_saturation.png
vendored
Normal file
After Width: | Height: | Size: 317 KiB |
After Width: | Height: | Size: 535 KiB |
After Width: | Height: | Size: 552 KiB |