Add HSL Adjustments to the effects module.

PiperOrigin-RevId: 476144167
This commit is contained in:
leonwind 2022-09-22 17:56:54 +00:00 committed by Marc Baechinger
parent afd829e5db
commit 2c27cd82f1
11 changed files with 650 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB