mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Migrate Gaussian Blur Effect to media3.
PiperOrigin-RevId: 593164068
This commit is contained in:
parent
e3056dacac
commit
0ab7bafa87
11 changed files with 1187 additions and 175 deletions
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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.VideoFrameProcessor.INPUT_TYPE_SURFACE;
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
|
||||||
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
||||||
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
||||||
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
||||||
|
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.text.SpannableString;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.ColorInfo;
|
||||||
|
import androidx.media3.common.DebugViewProvider;
|
||||||
|
import androidx.media3.common.Effect;
|
||||||
|
import androidx.media3.common.FrameInfo;
|
||||||
|
import androidx.media3.common.VideoFrameProcessingException;
|
||||||
|
import androidx.media3.common.VideoFrameProcessor;
|
||||||
|
import androidx.media3.common.util.Consumer;
|
||||||
|
import androidx.media3.common.util.NullableType;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.test.utils.TextureBitmapReader;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/** Utilities for effects tests. */
|
||||||
|
@UnstableApi
|
||||||
|
/* package */ class EffectsTestUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link Bitmap}s (generated at the timestamps in {@code presentationTimesUs}) from the
|
||||||
|
* {@link TextureBitmapReader}, and asserts that they are equal to files stored in the {@code
|
||||||
|
* goldenFileAssetPath} with the same {@code testId}.
|
||||||
|
*
|
||||||
|
* <p>Tries to save the {@link Bitmap}s as PNGs to the {@link Context#getCacheDir() cache
|
||||||
|
* directory}.
|
||||||
|
*/
|
||||||
|
public static void getAndAssertOutputBitmaps(
|
||||||
|
TextureBitmapReader textureBitmapReader,
|
||||||
|
List<Long> presentationTimesUs,
|
||||||
|
String testId,
|
||||||
|
String goldenFileAssetPath)
|
||||||
|
throws IOException {
|
||||||
|
for (int i = 0; i < presentationTimesUs.size(); i++) {
|
||||||
|
long presentationTimeUs = presentationTimesUs.get(i);
|
||||||
|
Bitmap actualBitmap = textureBitmapReader.getBitmapAtPresentationTimeUs(presentationTimeUs);
|
||||||
|
Bitmap expectedBitmap =
|
||||||
|
readBitmap(
|
||||||
|
Util.formatInvariant("%s/pts_%d.png", goldenFileAssetPath, presentationTimeUs));
|
||||||
|
maybeSaveTestBitmap(
|
||||||
|
testId, String.valueOf(presentationTimeUs), actualBitmap, /* path= */ null);
|
||||||
|
float averagePixelAbsoluteDifference =
|
||||||
|
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
|
||||||
|
assertThat(averagePixelAbsoluteDifference)
|
||||||
|
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates and processes a frame for each timestamp in {@code presentationTimesUs} through a
|
||||||
|
* {@link DefaultVideoFrameProcessor}, applying the given {@link GlEffect}, and outputting the
|
||||||
|
* resulting frame to the provided {@link TextureBitmapReader}.
|
||||||
|
*
|
||||||
|
* <p>The generated frames have their associated timestamps overlaid on them.
|
||||||
|
*
|
||||||
|
* @param frameWidth The width of the generated frames.
|
||||||
|
* @param frameHeight The height of the generated frames.
|
||||||
|
* @param presentationTimesUs The timestamps of the generated frames, in microseconds.
|
||||||
|
* @param glEffect The effect to apply to the frames.
|
||||||
|
* @param textSpanConsumer A {@link Consumer} used to set the spans that styles the text overlaid
|
||||||
|
* onto the frames.
|
||||||
|
*/
|
||||||
|
// MoreExecutors.directExecutor() pattern is consistent with our codebase.
|
||||||
|
@SuppressWarnings("StaticImportPreferred")
|
||||||
|
public static ImmutableList<Long> generateAndProcessFrames(
|
||||||
|
int frameWidth,
|
||||||
|
int frameHeight,
|
||||||
|
List<Long> presentationTimesUs,
|
||||||
|
GlEffect glEffect,
|
||||||
|
TextureBitmapReader textureBitmapReader,
|
||||||
|
Consumer<SpannableString> textSpanConsumer)
|
||||||
|
throws Exception {
|
||||||
|
ImmutableList.Builder<Long> actualPresentationTimesUs = new ImmutableList.Builder<>();
|
||||||
|
@Nullable DefaultVideoFrameProcessor defaultVideoFrameProcessor = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
AtomicReference<@NullableType VideoFrameProcessingException>
|
||||||
|
videoFrameProcessingExceptionReference = new AtomicReference<>();
|
||||||
|
BlankFrameProducer blankFrameProducer = new BlankFrameProducer(frameWidth, frameHeight);
|
||||||
|
CountDownLatch videoFrameProcessorReadyCountDownLatch = new CountDownLatch(1);
|
||||||
|
CountDownLatch videoFrameProcessingEndedCountDownLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
defaultVideoFrameProcessor =
|
||||||
|
checkNotNull(
|
||||||
|
new DefaultVideoFrameProcessor.Factory.Builder()
|
||||||
|
.setTextureOutput(
|
||||||
|
(textureProducer, outputTexture, presentationTimeUs, token) -> {
|
||||||
|
checkNotNull(textureBitmapReader)
|
||||||
|
.readBitmap(outputTexture, presentationTimeUs);
|
||||||
|
textureProducer.releaseOutputTexture(presentationTimeUs);
|
||||||
|
},
|
||||||
|
/* textureOutputCapacity= */ 1)
|
||||||
|
.build()
|
||||||
|
.create(
|
||||||
|
getApplicationContext(),
|
||||||
|
DebugViewProvider.NONE,
|
||||||
|
/* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED,
|
||||||
|
/* renderFramesAutomatically= */ true,
|
||||||
|
MoreExecutors.directExecutor(),
|
||||||
|
new VideoFrameProcessor.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onInputStreamRegistered(
|
||||||
|
@VideoFrameProcessor.InputType int inputType,
|
||||||
|
List<Effect> effects,
|
||||||
|
FrameInfo frameInfo) {
|
||||||
|
videoFrameProcessorReadyCountDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputSizeChanged(int width, int height) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOutputFrameAvailableForRendering(long presentationTimeUs) {
|
||||||
|
actualPresentationTimesUs.add(presentationTimeUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(VideoFrameProcessingException exception) {
|
||||||
|
videoFrameProcessingExceptionReference.set(exception);
|
||||||
|
videoFrameProcessorReadyCountDownLatch.countDown();
|
||||||
|
videoFrameProcessingEndedCountDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnded() {
|
||||||
|
videoFrameProcessingEndedCountDownLatch.countDown();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
defaultVideoFrameProcessor.getTaskExecutor().submit(blankFrameProducer::configureGlObjects);
|
||||||
|
// A frame needs to be registered despite not queuing any external input to ensure
|
||||||
|
// that the video frame processor knows about the stream offset.
|
||||||
|
checkNotNull(defaultVideoFrameProcessor)
|
||||||
|
.registerInputStream(
|
||||||
|
INPUT_TYPE_SURFACE,
|
||||||
|
/* effects= */ ImmutableList.of(
|
||||||
|
(GlEffect) (context, useHdr) -> blankFrameProducer,
|
||||||
|
// Use an overlay effect to generate bitmaps with timestamps on it.
|
||||||
|
new OverlayEffect(
|
||||||
|
ImmutableList.of(
|
||||||
|
new TextOverlay() {
|
||||||
|
@Override
|
||||||
|
public SpannableString getText(long presentationTimeUs) {
|
||||||
|
SpannableString text =
|
||||||
|
new SpannableString(String.valueOf(presentationTimeUs));
|
||||||
|
textSpanConsumer.accept(text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
glEffect),
|
||||||
|
new FrameInfo.Builder(ColorInfo.SDR_BT709_LIMITED, frameWidth, frameHeight).build());
|
||||||
|
videoFrameProcessorReadyCountDownLatch.await();
|
||||||
|
checkNoVideoFrameProcessingExceptionIsThrown(videoFrameProcessingExceptionReference);
|
||||||
|
blankFrameProducer.produceBlankFrames(presentationTimesUs);
|
||||||
|
defaultVideoFrameProcessor.signalEndOfInput();
|
||||||
|
videoFrameProcessingEndedCountDownLatch.await();
|
||||||
|
checkNoVideoFrameProcessingExceptionIsThrown(videoFrameProcessingExceptionReference);
|
||||||
|
} finally {
|
||||||
|
if (defaultVideoFrameProcessor != null) {
|
||||||
|
defaultVideoFrameProcessor.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actualPresentationTimesUs.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkNoVideoFrameProcessingExceptionIsThrown(
|
||||||
|
AtomicReference<@NullableType VideoFrameProcessingException>
|
||||||
|
videoFrameProcessingExceptionReference)
|
||||||
|
throws Exception {
|
||||||
|
@Nullable
|
||||||
|
Exception videoFrameProcessingException = videoFrameProcessingExceptionReference.get();
|
||||||
|
if (videoFrameProcessingException != null) {
|
||||||
|
throw videoFrameProcessingException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private EffectsTestUtil() {}
|
||||||
|
}
|
||||||
|
|
@ -15,43 +15,24 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.effect;
|
package androidx.media3.effect;
|
||||||
|
|
||||||
import static androidx.media3.common.VideoFrameProcessor.INPUT_TYPE_SURFACE;
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE;
|
import static androidx.media3.effect.EffectsTestUtil.generateAndProcessFrames;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888;
|
import static androidx.media3.effect.EffectsTestUtil.getAndAssertOutputBitmaps;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
|
||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.style.AbsoluteSizeSpan;
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.TypefaceSpan;
|
import android.text.style.TypefaceSpan;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.media3.common.util.Consumer;
|
||||||
import androidx.media3.common.ColorInfo;
|
|
||||||
import androidx.media3.common.DebugViewProvider;
|
|
||||||
import androidx.media3.common.Effect;
|
|
||||||
import androidx.media3.common.FrameInfo;
|
|
||||||
import androidx.media3.common.VideoFrameProcessingException;
|
|
||||||
import androidx.media3.common.VideoFrameProcessor;
|
|
||||||
import androidx.media3.common.util.NullableType;
|
|
||||||
import androidx.media3.common.util.Util;
|
|
||||||
import androidx.media3.test.utils.TextureBitmapReader;
|
import androidx.media3.test.utils.TextureBitmapReader;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
@ -64,12 +45,9 @@ public class FrameDropTest {
|
||||||
@Rule public final TestName testName = new TestName();
|
@Rule public final TestName testName = new TestName();
|
||||||
|
|
||||||
private static final String ASSET_PATH = "media/bitmap/FrameDropTest";
|
private static final String ASSET_PATH = "media/bitmap/FrameDropTest";
|
||||||
private static final int BLANK_FRAME_WIDTH = 100;
|
|
||||||
private static final int BLANK_FRAME_HEIGHT = 50;
|
|
||||||
|
|
||||||
private @MonotonicNonNull String testId;
|
|
||||||
private @MonotonicNonNull TextureBitmapReader textureBitmapReader;
|
private @MonotonicNonNull TextureBitmapReader textureBitmapReader;
|
||||||
private @MonotonicNonNull DefaultVideoFrameProcessor defaultVideoFrameProcessor;
|
private @MonotonicNonNull String testId;
|
||||||
|
|
||||||
@EnsuresNonNull({"textureBitmapReader", "testId"})
|
@EnsuresNonNull({"textureBitmapReader", "testId"})
|
||||||
@Before
|
@Before
|
||||||
|
|
@ -78,24 +56,20 @@ public class FrameDropTest {
|
||||||
testId = testName.getMethodName();
|
testId = testName.getMethodName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
|
||||||
public void tearDown() {
|
|
||||||
checkNotNull(defaultVideoFrameProcessor).release();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull({"textureBitmapReader", "testId"})
|
@RequiresNonNull({"textureBitmapReader", "testId"})
|
||||||
public void frameDrop_withDefaultStrategy_outputsFramesAtTheCorrectPresentationTimesUs()
|
public void frameDrop_withDefaultStrategy_outputsFramesAtTheCorrectPresentationTimesUs()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<Long> frameTimesUs =
|
ImmutableList<Long> frameTimesUs =
|
||||||
ImmutableList.of(0L, 16_000L, 32_000L, 48_000L, 58_000L, 71_000L, 86_000L);
|
ImmutableList.of(0L, 16_000L, 32_000L, 48_000L, 58_000L, 71_000L, 86_000L);
|
||||||
|
FrameDropEffect frameDropEffect =
|
||||||
|
FrameDropEffect.createDefaultFrameDropEffect(/* targetFrameRate= */ 30);
|
||||||
|
|
||||||
ImmutableList<Long> actualPresentationTimesUs =
|
ImmutableList<Long> actualPresentationTimesUs =
|
||||||
processFramesToEndOfStream(
|
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||||
frameTimesUs, FrameDropEffect.createDefaultFrameDropEffect(/* targetFrameRate= */ 30));
|
|
||||||
|
|
||||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 32_000L, 71_000L).inOrder();
|
assertThat(actualPresentationTimesUs).containsExactly(0L, 32_000L, 71_000L).inOrder();
|
||||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -104,162 +78,60 @@ public class FrameDropTest {
|
||||||
throws Exception {
|
throws Exception {
|
||||||
ImmutableList<Long> frameTimesUs =
|
ImmutableList<Long> frameTimesUs =
|
||||||
ImmutableList.of(0L, 250_000L, 500_000L, 750_000L, 1_000_000L, 1_500_000L);
|
ImmutableList.of(0L, 250_000L, 500_000L, 750_000L, 1_000_000L, 1_500_000L);
|
||||||
|
FrameDropEffect frameDropEffect =
|
||||||
|
FrameDropEffect.createSimpleFrameDropEffect(
|
||||||
|
/* expectedFrameRate= */ 6, /* targetFrameRate= */ 2);
|
||||||
|
|
||||||
ImmutableList<Long> actualPresentationTimesUs =
|
ImmutableList<Long> actualPresentationTimesUs =
|
||||||
processFramesToEndOfStream(
|
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||||
frameTimesUs,
|
|
||||||
FrameDropEffect.createSimpleFrameDropEffect(
|
|
||||||
/* expectedFrameRate= */ 6, /* targetFrameRate= */ 2));
|
|
||||||
|
|
||||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 750_000L).inOrder();
|
assertThat(actualPresentationTimesUs).containsExactly(0L, 750_000L).inOrder();
|
||||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@RequiresNonNull({"textureBitmapReader", "testId"})
|
@RequiresNonNull({"textureBitmapReader", "testId"})
|
||||||
public void frameDrop_withSimpleStrategy_outputsAllFrames() throws Exception {
|
public void frameDrop_withSimpleStrategy_outputsAllFrames() throws Exception {
|
||||||
ImmutableList<Long> frameTimesUs = ImmutableList.of(0L, 333_333L, 666_667L);
|
ImmutableList<Long> frameTimesUs = ImmutableList.of(0L, 333_333L, 666_667L);
|
||||||
|
FrameDropEffect frameDropEffect =
|
||||||
|
FrameDropEffect.createSimpleFrameDropEffect(
|
||||||
|
/* expectedFrameRate= */ 3, /* targetFrameRate= */ 3);
|
||||||
|
|
||||||
ImmutableList<Long> actualPresentationTimesUs =
|
ImmutableList<Long> actualPresentationTimesUs =
|
||||||
processFramesToEndOfStream(
|
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||||
frameTimesUs,
|
|
||||||
FrameDropEffect.createSimpleFrameDropEffect(
|
|
||||||
/* expectedFrameRate= */ 3, /* targetFrameRate= */ 3));
|
|
||||||
|
|
||||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 333_333L, 666_667L).inOrder();
|
assertThat(actualPresentationTimesUs).containsExactly(0L, 333_333L, 666_667L).inOrder();
|
||||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void getAndAssertOutputBitmaps(
|
private ImmutableList<Long> generateAndProcessBlackTimeStampedFrames(
|
||||||
TextureBitmapReader textureBitmapReader, List<Long> presentationTimesUs, String testId)
|
ImmutableList<Long> frameTimesUs, FrameDropEffect frameDropEffect) throws Exception {
|
||||||
throws IOException {
|
int blankFrameWidth = 100;
|
||||||
for (int i = 0; i < presentationTimesUs.size(); i++) {
|
int blankFrameHeight = 50;
|
||||||
long presentationTimeUs = presentationTimesUs.get(i);
|
Consumer<SpannableString> textSpanConsumer =
|
||||||
Bitmap actualBitmap = textureBitmapReader.getBitmapAtPresentationTimeUs(presentationTimeUs);
|
(text) -> {
|
||||||
Bitmap expectedBitmap =
|
text.setSpan(
|
||||||
readBitmap(Util.formatInvariant("%s/pts_%d.png", ASSET_PATH, presentationTimeUs));
|
new ForegroundColorSpan(Color.BLACK),
|
||||||
maybeSaveTestBitmap(
|
/* start= */ 0,
|
||||||
testId, String.valueOf(presentationTimeUs), actualBitmap, /* path= */ null);
|
text.length(),
|
||||||
float averagePixelAbsoluteDifference =
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
|
text.setSpan(
|
||||||
assertThat(averagePixelAbsoluteDifference)
|
new AbsoluteSizeSpan(/* size= */ 24),
|
||||||
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
/* start= */ 0,
|
||||||
}
|
text.length(),
|
||||||
}
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
text.setSpan(
|
||||||
@EnsuresNonNull("defaultVideoFrameProcessor")
|
new TypefaceSpan(/* family= */ "sans-serif"),
|
||||||
private ImmutableList<Long> processFramesToEndOfStream(
|
/* start= */ 0,
|
||||||
List<Long> inputPresentationTimesUs, FrameDropEffect frameDropEffect) throws Exception {
|
text.length(),
|
||||||
AtomicReference<@NullableType VideoFrameProcessingException>
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
videoFrameProcessingExceptionReference = new AtomicReference<>();
|
};
|
||||||
BlankFrameProducer blankFrameProducer =
|
return generateAndProcessFrames(
|
||||||
new BlankFrameProducer(BLANK_FRAME_WIDTH, BLANK_FRAME_HEIGHT);
|
blankFrameWidth,
|
||||||
CountDownLatch videoFrameProcessorReadyCountDownLatch = new CountDownLatch(1);
|
blankFrameHeight,
|
||||||
CountDownLatch videoFrameProcessingEndedCountDownLatch = new CountDownLatch(1);
|
frameTimesUs,
|
||||||
ImmutableList.Builder<Long> actualPresentationTimesUs = new ImmutableList.Builder<>();
|
frameDropEffect,
|
||||||
|
checkNotNull(textureBitmapReader),
|
||||||
defaultVideoFrameProcessor =
|
textSpanConsumer);
|
||||||
checkNotNull(
|
|
||||||
new DefaultVideoFrameProcessor.Factory.Builder()
|
|
||||||
.setTextureOutput(
|
|
||||||
(textureProducer, outputTexture, presentationTimeUs, token) -> {
|
|
||||||
checkNotNull(textureBitmapReader)
|
|
||||||
.readBitmap(outputTexture, presentationTimeUs);
|
|
||||||
textureProducer.releaseOutputTexture(presentationTimeUs);
|
|
||||||
},
|
|
||||||
/* textureOutputCapacity= */ 1)
|
|
||||||
.build()
|
|
||||||
.create(
|
|
||||||
getApplicationContext(),
|
|
||||||
DebugViewProvider.NONE,
|
|
||||||
/* outputColorInfo= */ ColorInfo.SDR_BT709_LIMITED,
|
|
||||||
/* renderFramesAutomatically= */ true,
|
|
||||||
MoreExecutors.directExecutor(),
|
|
||||||
new VideoFrameProcessor.Listener() {
|
|
||||||
@Override
|
|
||||||
public void onInputStreamRegistered(
|
|
||||||
@VideoFrameProcessor.InputType int inputType,
|
|
||||||
List<Effect> effects,
|
|
||||||
FrameInfo frameInfo) {
|
|
||||||
videoFrameProcessorReadyCountDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOutputSizeChanged(int width, int height) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOutputFrameAvailableForRendering(long presentationTimeUs) {
|
|
||||||
actualPresentationTimesUs.add(presentationTimeUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(VideoFrameProcessingException exception) {
|
|
||||||
videoFrameProcessingExceptionReference.set(exception);
|
|
||||||
videoFrameProcessorReadyCountDownLatch.countDown();
|
|
||||||
videoFrameProcessingEndedCountDownLatch.countDown();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEnded() {
|
|
||||||
videoFrameProcessingEndedCountDownLatch.countDown();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
defaultVideoFrameProcessor.getTaskExecutor().submit(blankFrameProducer::configureGlObjects);
|
|
||||||
// A frame needs to be registered despite not queuing any external input to ensure
|
|
||||||
// that the video frame processor knows about the stream offset.
|
|
||||||
checkNotNull(defaultVideoFrameProcessor)
|
|
||||||
.registerInputStream(
|
|
||||||
INPUT_TYPE_SURFACE,
|
|
||||||
/* effects= */ ImmutableList.of(
|
|
||||||
(GlEffect) (context, useHdr) -> blankFrameProducer,
|
|
||||||
// Use an overlay effect to generate bitmaps with timestamps on it.
|
|
||||||
new OverlayEffect(
|
|
||||||
ImmutableList.of(
|
|
||||||
new TextOverlay() {
|
|
||||||
@Override
|
|
||||||
public SpannableString getText(long presentationTimeUs) {
|
|
||||||
SpannableString text =
|
|
||||||
new SpannableString(String.valueOf(presentationTimeUs));
|
|
||||||
text.setSpan(
|
|
||||||
new ForegroundColorSpan(Color.BLACK),
|
|
||||||
/* start= */ 0,
|
|
||||||
text.length(),
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
text.setSpan(
|
|
||||||
new AbsoluteSizeSpan(/* size= */ 24),
|
|
||||||
/* start= */ 0,
|
|
||||||
text.length(),
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
text.setSpan(
|
|
||||||
new TypefaceSpan(/* family= */ "sans-serif"),
|
|
||||||
/* start= */ 0,
|
|
||||||
text.length(),
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
frameDropEffect),
|
|
||||||
new FrameInfo.Builder(
|
|
||||||
ColorInfo.SDR_BT709_LIMITED, BLANK_FRAME_WIDTH, BLANK_FRAME_HEIGHT)
|
|
||||||
.build());
|
|
||||||
videoFrameProcessorReadyCountDownLatch.await();
|
|
||||||
checkNoVideoFrameProcessingExceptionIsThrown(videoFrameProcessingExceptionReference);
|
|
||||||
blankFrameProducer.produceBlankFrames(inputPresentationTimesUs);
|
|
||||||
defaultVideoFrameProcessor.signalEndOfInput();
|
|
||||||
videoFrameProcessingEndedCountDownLatch.await();
|
|
||||||
checkNoVideoFrameProcessingExceptionIsThrown(videoFrameProcessingExceptionReference);
|
|
||||||
return actualPresentationTimesUs.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkNoVideoFrameProcessingExceptionIsThrown(
|
|
||||||
AtomicReference<@NullableType VideoFrameProcessingException>
|
|
||||||
videoFrameProcessingExceptionReference)
|
|
||||||
throws Exception {
|
|
||||||
@Nullable
|
|
||||||
Exception videoFrameProcessingException = videoFrameProcessingExceptionReference.get();
|
|
||||||
if (videoFrameProcessingException != null) {
|
|
||||||
throw videoFrameProcessingException;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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
|
||||||
|
*
|
||||||
|
* https://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.EffectsTestUtil.generateAndProcessFrames;
|
||||||
|
import static androidx.media3.effect.EffectsTestUtil.getAndAssertOutputBitmaps;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.TypefaceSpan;
|
||||||
|
import androidx.media3.common.util.Consumer;
|
||||||
|
import androidx.media3.test.utils.TextureBitmapReader;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TestName;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Tests for {@link GaussianBlur}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class GaussianBlurTest {
|
||||||
|
@Rule public final TestName testName = new TestName();
|
||||||
|
|
||||||
|
// Golden images were generated on an API 33 emulator. API 26 emulators have a different text
|
||||||
|
// rendering implementation that leads to a larger pixel difference.
|
||||||
|
private static final String ASSET_PATH = "media/bitmap/GaussianBlurTest";
|
||||||
|
private static final int BLANK_FRAME_WIDTH = 200;
|
||||||
|
private static final int BLANK_FRAME_HEIGHT = 100;
|
||||||
|
private static final Consumer<SpannableString> TEXT_SPAN_CONSUMER =
|
||||||
|
(text) -> {
|
||||||
|
text.setSpan(
|
||||||
|
new BackgroundColorSpan(Color.BLUE),
|
||||||
|
/* start= */ 0,
|
||||||
|
text.length(),
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
text.setSpan(
|
||||||
|
new ForegroundColorSpan(Color.WHITE),
|
||||||
|
/* start= */ 0,
|
||||||
|
text.length(),
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
text.setSpan(
|
||||||
|
new AbsoluteSizeSpan(/* size= */ 100),
|
||||||
|
/* start= */ 0,
|
||||||
|
text.length(),
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
text.setSpan(
|
||||||
|
new TypefaceSpan(/* family= */ "sans-serif"),
|
||||||
|
/* start= */ 0,
|
||||||
|
text.length(),
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
};
|
||||||
|
|
||||||
|
private @MonotonicNonNull String testId;
|
||||||
|
private @MonotonicNonNull TextureBitmapReader textureBitmapReader;
|
||||||
|
|
||||||
|
@EnsuresNonNull({"textureBitmapReader", "testId"})
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
textureBitmapReader = new TextureBitmapReader();
|
||||||
|
testId = testName.getMethodName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@RequiresNonNull({"textureBitmapReader", "testId"})
|
||||||
|
public void gaussianBlur_blursFrame() throws Exception {
|
||||||
|
ImmutableList<Long> frameTimesUs = ImmutableList.of(32_000L);
|
||||||
|
ImmutableList<Long> actualPresentationTimesUs =
|
||||||
|
generateAndProcessFrames(
|
||||||
|
BLANK_FRAME_WIDTH,
|
||||||
|
BLANK_FRAME_HEIGHT,
|
||||||
|
frameTimesUs,
|
||||||
|
new GaussianBlur(/* sigma= */ 5f),
|
||||||
|
textureBitmapReader,
|
||||||
|
TEXT_SPAN_CONSUMER);
|
||||||
|
|
||||||
|
assertThat(actualPresentationTimesUs).containsExactly(32_000L);
|
||||||
|
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
#version 100
|
||||||
|
// Copyright 2023 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
|
||||||
|
//
|
||||||
|
// https://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.
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
varying vec2 vTexSamplingCoord;
|
||||||
|
uniform sampler2D uTexSampler;
|
||||||
|
// 1D function LUT, only 2D due to OpenGL ES 2.0 limitations.
|
||||||
|
uniform int uIsHorizontal;
|
||||||
|
// Size of one texel in the source image, along the axis of interest.
|
||||||
|
// To properly leverage the bilinear texture sampling for efficient weighted
|
||||||
|
// lookup, it is important to know exactly where texels are centered.
|
||||||
|
uniform float uSourceTexelSize;
|
||||||
|
// Size of source texture in texels.
|
||||||
|
uniform float uSourceFullSize;
|
||||||
|
// Starting point of the convolution, in units of the source texels.
|
||||||
|
uniform float uConvStartTexels;
|
||||||
|
// Width of the convolution, in units of the source texels.
|
||||||
|
uniform float uConvWidthTexels;
|
||||||
|
// Convolution function has a different resolution than the source texture.
|
||||||
|
// Need to be able convert steps in source texels to steps in the function
|
||||||
|
// lookup texture.
|
||||||
|
uniform float uFunctionLookupStepSize;
|
||||||
|
// Center position of the function in the lookup texture.
|
||||||
|
uniform vec2 uFunctionLookupCenter;
|
||||||
|
uniform sampler2D uFunctionLookupSampler;
|
||||||
|
|
||||||
|
// Reference Implementation:
|
||||||
|
void main() {
|
||||||
|
const float epsilon = 0.0001;
|
||||||
|
vec2 singleTexelStep;
|
||||||
|
float centerPositionTexels;
|
||||||
|
if (uIsHorizontal > 0) {
|
||||||
|
singleTexelStep = vec2(uSourceTexelSize, 0.0);
|
||||||
|
centerPositionTexels = vTexSamplingCoord.x * uSourceFullSize;
|
||||||
|
} else {
|
||||||
|
singleTexelStep = vec2(0.0, uSourceTexelSize);
|
||||||
|
centerPositionTexels = vTexSamplingCoord.y * uSourceFullSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
float supportStartEdgeTexels =
|
||||||
|
max(0.0, centerPositionTexels + uConvStartTexels);
|
||||||
|
|
||||||
|
// Perform calculations at texel centers.
|
||||||
|
// Find first texel center > supportStartEdge.
|
||||||
|
// Texels are centered at 1/2 pixel offsets.
|
||||||
|
float startSampleTexels = floor(supportStartEdgeTexels + 0.5 - epsilon) + 0.5;
|
||||||
|
// Make use of bilinear sampling below, so each step is actually 2 samples.
|
||||||
|
int numSteps = int(ceil(uConvWidthTexels / 2.0));
|
||||||
|
|
||||||
|
// Loop through, leveraging linear texture sampling to perform 2 texel
|
||||||
|
// samples at once.
|
||||||
|
vec4 accumulatedRgba = vec4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
float accumulatedWeight = 0.0;
|
||||||
|
|
||||||
|
vec2 functionLookupStepPerTexel = vec2(uFunctionLookupStepSize, 0.0);
|
||||||
|
|
||||||
|
for (int i = 0; i < numSteps; ++i) {
|
||||||
|
float sample0Texels = startSampleTexels + float(2 * i);
|
||||||
|
|
||||||
|
float sample0OffsetTexels = sample0Texels - centerPositionTexels;
|
||||||
|
float sample1OffsetTexels = sample0OffsetTexels + 1.0;
|
||||||
|
|
||||||
|
vec2 function0Coord = uFunctionLookupCenter +
|
||||||
|
sample0OffsetTexels * functionLookupStepPerTexel;
|
||||||
|
vec2 function1Coord = uFunctionLookupCenter +
|
||||||
|
sample1OffsetTexels * functionLookupStepPerTexel;
|
||||||
|
|
||||||
|
float sample0Weight = texture2D(uFunctionLookupSampler, function0Coord).x;
|
||||||
|
float sample1Weight = texture2D(uFunctionLookupSampler, function1Coord).x;
|
||||||
|
float totalSampleWeight = sample0Weight + sample1Weight;
|
||||||
|
|
||||||
|
// Skip samples with very low weight to avoid unnecessary lookups and
|
||||||
|
// avoid dividing by 0.
|
||||||
|
if (abs(totalSampleWeight) > epsilon) {
|
||||||
|
// Select a coordinate so that a linear sample at that location
|
||||||
|
// intrinsically includes the relative sampling weights.
|
||||||
|
float sampleOffsetTexels = (sample0OffsetTexels * sample0Weight +
|
||||||
|
sample1OffsetTexels * sample1Weight) /
|
||||||
|
totalSampleWeight;
|
||||||
|
|
||||||
|
vec2 textureSamplePos =
|
||||||
|
vTexSamplingCoord + sampleOffsetTexels * singleTexelStep;
|
||||||
|
|
||||||
|
vec4 textureSampleColor = texture2D(uTexSampler, textureSamplePos);
|
||||||
|
accumulatedRgba += textureSampleColor * totalSampleWeight;
|
||||||
|
accumulatedWeight += totalSampleWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulatedWeight > 0.0) {
|
||||||
|
gl_FragColor = accumulatedRgba / accumulatedWeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for 1 dimensional convolution functions.
|
||||||
|
*
|
||||||
|
* <p>The domain defines the region over which the function operates, in pixels.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public interface ConvolutionFunction1D {
|
||||||
|
|
||||||
|
/** Returns the start of the domain. */
|
||||||
|
float domainStart();
|
||||||
|
|
||||||
|
/** Returns the end of the domain. */
|
||||||
|
float domainEnd();
|
||||||
|
|
||||||
|
/** Returns the width of the domain. */
|
||||||
|
default float width() {
|
||||||
|
return domainEnd() - domainStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the value of the function at the {@code samplePosition}. */
|
||||||
|
float value(float samplePosition);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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 androidx.annotation.FloatRange;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SeparableConvolution} to apply a Gaussian blur on image data.
|
||||||
|
*
|
||||||
|
* <p>The width of the blur is specified in pixels and applied symmetrically.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
@RequiresApi(26) // See SeparableConvolution.
|
||||||
|
public final class GaussianBlur extends SeparableConvolution {
|
||||||
|
private final float sigma;
|
||||||
|
private final float numStandardDeviations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param sigma The half-width of 1 standard deviation, in pixels.
|
||||||
|
* @param numStandardDeviations The size of function domain, measured in the number of standard
|
||||||
|
* deviations.
|
||||||
|
*/
|
||||||
|
public GaussianBlur(
|
||||||
|
@FloatRange(from = 0.0, fromInclusive = false) float sigma,
|
||||||
|
@FloatRange(from = 0.0, fromInclusive = false) float numStandardDeviations) {
|
||||||
|
this.sigma = sigma;
|
||||||
|
this.numStandardDeviations = numStandardDeviations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance with {@code numStandardDeviations} set to {@code 2.0f}.
|
||||||
|
*
|
||||||
|
* @param sigma The half-width of 1 standard deviation, in pixels.
|
||||||
|
*/
|
||||||
|
public GaussianBlur(float sigma) {
|
||||||
|
this.sigma = sigma;
|
||||||
|
this.numStandardDeviations = 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConvolutionFunction1D getConvolution() {
|
||||||
|
return new GaussianFunction(sigma, numStandardDeviations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
|
import static java.lang.Math.PI;
|
||||||
|
import static java.lang.Math.exp;
|
||||||
|
import static java.lang.Math.sqrt;
|
||||||
|
|
||||||
|
import androidx.annotation.FloatRange;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of a symmetric Gaussian function with a limited domain.
|
||||||
|
*
|
||||||
|
* <p>The half-width of the domain is {@code sigma} times {@code numStdDev}. Values strictly outside
|
||||||
|
* of that range are zero.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
public final class GaussianFunction implements ConvolutionFunction1D {
|
||||||
|
private final float sigma;
|
||||||
|
private final float numStdDev;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param sigma The one standard deviation, in pixels.
|
||||||
|
* @param numStandardDeviations The half-width of function domain, measured in the number of
|
||||||
|
* standard deviations.
|
||||||
|
*/
|
||||||
|
public GaussianFunction(
|
||||||
|
@FloatRange(from = 0.0, fromInclusive = false) float sigma,
|
||||||
|
@FloatRange(from = 0.0, fromInclusive = false) float numStandardDeviations) {
|
||||||
|
checkArgument(sigma > 0 && numStandardDeviations > 0);
|
||||||
|
this.sigma = sigma;
|
||||||
|
this.numStdDev = numStandardDeviations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float domainStart() {
|
||||||
|
return -numStdDev * sigma;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float domainEnd() {
|
||||||
|
return numStdDev * sigma;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float value(float samplePosition) {
|
||||||
|
if (Math.abs(samplePosition) > numStdDev * sigma) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
float samplePositionOverSigma = samplePosition / sigma;
|
||||||
|
return (float)
|
||||||
|
(exp(-samplePositionOverSigma * samplePositionOverSigma / 2) / sqrt(2 * PI) / sigma);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package androidx.media3.effect;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.media3.common.VideoFrameProcessingException;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link GlEffect} for performing separable convolutions.
|
||||||
|
*
|
||||||
|
* <p>A single 1D convolution function is applied horizontally on a first pass and vertically on a
|
||||||
|
* second pass.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
@RequiresApi(26) // See SeparableConvolutionShaderProgram.
|
||||||
|
public abstract class SeparableConvolution implements GlEffect {
|
||||||
|
private final float scaleFactor;
|
||||||
|
|
||||||
|
/** Creates an instance with a {@code scaleFactor} of {@code 1}. */
|
||||||
|
public SeparableConvolution() {
|
||||||
|
this(/* scaleFactor= */ 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param scaleFactor The scaling factor used to determine the size of the output relative to the
|
||||||
|
* input. The aspect ratio remains constant.
|
||||||
|
*/
|
||||||
|
public SeparableConvolution(float scaleFactor) {
|
||||||
|
this.scaleFactor = scaleFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a {@linkplain ConvolutionFunction1D 1D convolution function}. */
|
||||||
|
public abstract ConvolutionFunction1D getConvolution();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GlShaderProgram toGlShaderProgram(Context context, boolean useHdr)
|
||||||
|
throws VideoFrameProcessingException {
|
||||||
|
return new SeparableConvolutionShaderProgram(context, useHdr, this, scaleFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,446 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.MatrixUtils.getGlMatrixArray;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.opengl.GLUtils;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import androidx.media3.common.GlObjectsProvider;
|
||||||
|
import androidx.media3.common.GlTextureInfo;
|
||||||
|
import androidx.media3.common.VideoFrameProcessingException;
|
||||||
|
import androidx.media3.common.util.Assertions;
|
||||||
|
import androidx.media3.common.util.GlProgram;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
|
import androidx.media3.common.util.Size;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link GlShaderProgram} for performing separable convolutions.
|
||||||
|
*
|
||||||
|
* <p>A single {@link ConvolutionFunction1D} is applied horizontally on a first pass and vertically
|
||||||
|
* on a second pass.
|
||||||
|
*/
|
||||||
|
@RequiresApi(26) // Uses Bitmap.Config.RGBA_F16.
|
||||||
|
/* package */ final class SeparableConvolutionShaderProgram implements GlShaderProgram {
|
||||||
|
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
|
||||||
|
private static final String FRAGMENT_SHADER_PATH =
|
||||||
|
"shaders/fragment_shader_separable_convolution_es2.glsl";
|
||||||
|
|
||||||
|
// Constants specifically for fp16FromFloat().
|
||||||
|
// TODO (b/282767994): Fix TAP hanging issue and update samples per texel.
|
||||||
|
private static final int RASTER_SAMPLES_PER_TEXEL = 5;
|
||||||
|
// Apply some padding in the function LUT to avoid any issues from GL sampling off the texture.
|
||||||
|
private static final int FUNCTION_LUT_PADDING = RASTER_SAMPLES_PER_TEXEL;
|
||||||
|
|
||||||
|
// BEGIN COPIED FP16 code.
|
||||||
|
// Source: libcore/luni/src/main/java/libcore/util/FP16.java
|
||||||
|
private static final int FP16_EXPONENT_BIAS = 15;
|
||||||
|
private static final int FP16_SIGN_SHIFT = 15;
|
||||||
|
private static final int FP16_EXPONENT_SHIFT = 10;
|
||||||
|
private static final int FP32_SIGN_SHIFT = 31;
|
||||||
|
private static final int FP32_EXPONENT_SHIFT = 23;
|
||||||
|
private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff;
|
||||||
|
private static final int FP32_SIGNIFICAND_MASK = 0x7fffff;
|
||||||
|
private static final int FP32_EXPONENT_BIAS = 127;
|
||||||
|
// END FP16 copied code.
|
||||||
|
|
||||||
|
private final GlProgram glProgram;
|
||||||
|
private final GlProgram sharpTransformGlProgram;
|
||||||
|
private final float[] sharpTransformMatrixValues;
|
||||||
|
private final boolean useHdr;
|
||||||
|
private final SeparableConvolution convolution;
|
||||||
|
private final float scaleFactor;
|
||||||
|
|
||||||
|
private GlShaderProgram.InputListener inputListener;
|
||||||
|
private GlShaderProgram.OutputListener outputListener;
|
||||||
|
private GlShaderProgram.ErrorListener errorListener;
|
||||||
|
private Executor errorListenerExecutor;
|
||||||
|
private Size outputSize;
|
||||||
|
private Size lastInputSize;
|
||||||
|
private Size intermediateSize;
|
||||||
|
private GlTextureInfo outputTexture;
|
||||||
|
private boolean outputTextureInUse;
|
||||||
|
private GlTextureInfo intermediateTexture;
|
||||||
|
private GlTextureInfo functionLutTexture; // Values for the function LUT as a texture.
|
||||||
|
private float functionLutTexelStep;
|
||||||
|
private float functionLutCenterX;
|
||||||
|
private float functionLutDomainStart;
|
||||||
|
private float functionLutWidth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance.
|
||||||
|
*
|
||||||
|
* @param context The {@link Context}.
|
||||||
|
* @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 linear RGB BT.709.
|
||||||
|
* @param convolution The {@link SeparableConvolution} to apply in each direction.
|
||||||
|
* @param scaleFactor The scaling factor used to determine the size of the output relative to the
|
||||||
|
* input. The aspect ratio remains constant.
|
||||||
|
* @throws VideoFrameProcessingException If a problem occurs while reading shader files.
|
||||||
|
*/
|
||||||
|
public SeparableConvolutionShaderProgram(
|
||||||
|
Context context, boolean useHdr, SeparableConvolution convolution, float scaleFactor)
|
||||||
|
throws VideoFrameProcessingException {
|
||||||
|
this.useHdr = useHdr;
|
||||||
|
this.convolution = convolution;
|
||||||
|
this.scaleFactor = scaleFactor;
|
||||||
|
inputListener = new InputListener() {};
|
||||||
|
outputListener = new OutputListener() {};
|
||||||
|
errorListener = (frameProcessingException) -> {};
|
||||||
|
|
||||||
|
errorListenerExecutor = MoreExecutors.directExecutor();
|
||||||
|
lastInputSize = Size.ZERO;
|
||||||
|
intermediateSize = Size.ZERO;
|
||||||
|
outputSize = Size.ZERO;
|
||||||
|
try {
|
||||||
|
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||||
|
sharpTransformGlProgram =
|
||||||
|
new GlProgram(
|
||||||
|
context,
|
||||||
|
"shaders/vertex_shader_transformation_es2.glsl",
|
||||||
|
"shaders/fragment_shader_copy_es2.glsl");
|
||||||
|
} catch (IOException | GlUtil.GlException e) {
|
||||||
|
throw new VideoFrameProcessingException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix sharpTransformMatrix = new Matrix();
|
||||||
|
sharpTransformMatrix.setScale(/* sx= */ .5f, /* sy= */ 1f);
|
||||||
|
sharpTransformMatrixValues = getGlMatrixArray(sharpTransformMatrix);
|
||||||
|
functionLutTexture = GlTextureInfo.UNSET;
|
||||||
|
intermediateTexture = GlTextureInfo.UNSET;
|
||||||
|
outputTexture = GlTextureInfo.UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setInputListener(InputListener inputListener) {
|
||||||
|
this.inputListener = inputListener;
|
||||||
|
if (!outputTextureInUse) {
|
||||||
|
inputListener.onReadyToAcceptInputFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOutputListener(OutputListener outputListener) {
|
||||||
|
this.outputListener = outputListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setErrorListener(Executor errorListenerExecutor, ErrorListener errorListener) {
|
||||||
|
this.errorListenerExecutor = errorListenerExecutor;
|
||||||
|
this.errorListener = errorListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void queueInputFrame(
|
||||||
|
GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) {
|
||||||
|
Assertions.checkState(
|
||||||
|
!outputTextureInUse,
|
||||||
|
"The shader program does not currently accept input frames. Release prior output frames"
|
||||||
|
+ " first.");
|
||||||
|
try {
|
||||||
|
ensureTexturesAreConfigured(
|
||||||
|
glObjectsProvider, new Size(inputTexture.width, inputTexture.height));
|
||||||
|
outputTextureInUse = true;
|
||||||
|
renderHorizontal(inputTexture);
|
||||||
|
renderVertical();
|
||||||
|
float[] identityMatrix = GlUtil.create4x4IdentityMatrix();
|
||||||
|
sharpTransformGlProgram.use();
|
||||||
|
sharpTransformGlProgram.setSamplerTexIdUniform(
|
||||||
|
"uTexSampler", inputTexture.texId, /* texUnitIndex= */ 0);
|
||||||
|
sharpTransformGlProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix);
|
||||||
|
sharpTransformGlProgram.setFloatsUniform("uTransformationMatrix", sharpTransformMatrixValues);
|
||||||
|
sharpTransformGlProgram.setBufferAttribute(
|
||||||
|
"aFramePosition",
|
||||||
|
GlUtil.getNormalizedCoordinateBounds(),
|
||||||
|
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||||
|
sharpTransformGlProgram.bindAttributesAndUniforms();
|
||||||
|
|
||||||
|
// The four-vertex triangle strip forms a quad.
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* i1= */ 0, /* i2= */ 4);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
inputListener.onInputFrameProcessed(inputTexture);
|
||||||
|
outputListener.onOutputFrameAvailable(outputTexture, presentationTimeUs);
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
errorListenerExecutor.execute(
|
||||||
|
() -> errorListener.onError(VideoFrameProcessingException.from(e, presentationTimeUs)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void releaseOutputFrame(GlTextureInfo outputTexture) {
|
||||||
|
outputTextureInUse = false;
|
||||||
|
inputListener.onReadyToAcceptInputFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void signalEndOfCurrentInputStream() {
|
||||||
|
outputListener.onCurrentOutputStreamEnded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {
|
||||||
|
outputTextureInUse = false;
|
||||||
|
inputListener.onFlush();
|
||||||
|
inputListener.onReadyToAcceptInputFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() throws VideoFrameProcessingException {
|
||||||
|
try {
|
||||||
|
outputTexture.release();
|
||||||
|
intermediateTexture.release();
|
||||||
|
functionLutTexture.release();
|
||||||
|
glProgram.delete();
|
||||||
|
sharpTransformGlProgram.delete();
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
throw new VideoFrameProcessingException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderOnePass(int inputTexId, boolean isHorizontal) throws GlUtil.GlException {
|
||||||
|
int size = isHorizontal ? lastInputSize.getWidth() : intermediateSize.getHeight();
|
||||||
|
glProgram.use();
|
||||||
|
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||||
|
glProgram.setIntUniform("uIsHorizontal", isHorizontal ? 1 : 0);
|
||||||
|
glProgram.setFloatUniform("uSourceTexelSize", 1.0f / size);
|
||||||
|
glProgram.setFloatUniform("uSourceFullSize", (float) size);
|
||||||
|
glProgram.setFloatUniform("uConvStartTexels", functionLutDomainStart);
|
||||||
|
glProgram.setFloatUniform("uConvWidthTexels", functionLutWidth);
|
||||||
|
glProgram.setFloatUniform("uFunctionLookupStepSize", functionLutTexelStep);
|
||||||
|
glProgram.setFloatsUniform("uFunctionLookupCenter", new float[] {functionLutCenterX, 0.5f});
|
||||||
|
glProgram.setSamplerTexIdUniform(
|
||||||
|
"uFunctionLookupSampler", functionLutTexture.texId, /* texUnitIndex= */ 1);
|
||||||
|
glProgram.bindAttributesAndUniforms();
|
||||||
|
|
||||||
|
// The four-vertex triangle strip forms a quad.
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Size configure(Size inputSize) {
|
||||||
|
// 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 = GlUtil.create4x4IdentityMatrix();
|
||||||
|
glProgram.setFloatsUniform("uTransformationMatrix", identityMatrix);
|
||||||
|
glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix);
|
||||||
|
|
||||||
|
return new Size(
|
||||||
|
(int) (inputSize.getWidth() * scaleFactor * 2),
|
||||||
|
(int) (inputSize.getHeight() * scaleFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderHorizontal(GlTextureInfo inputTexture) throws GlUtil.GlException {
|
||||||
|
// Render horizontal reads from the input texture and renders to the intermediate texture.
|
||||||
|
GlUtil.focusFramebufferUsingCurrentContext(
|
||||||
|
intermediateTexture.fboId, intermediateTexture.width, intermediateTexture.height);
|
||||||
|
GlUtil.clearFocusedBuffers();
|
||||||
|
renderOnePass(inputTexture.texId, /* isHorizontal= */ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderVertical() throws GlUtil.GlException {
|
||||||
|
// Render vertical reads from the intermediate and renders to the output texture.
|
||||||
|
GlUtil.focusFramebufferUsingCurrentContext(
|
||||||
|
outputTexture.fboId, outputTexture.width, outputTexture.height);
|
||||||
|
GlUtil.clearFocusedBuffers();
|
||||||
|
renderOnePass(intermediateTexture.texId, /* isHorizontal= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureTexturesAreConfigured(GlObjectsProvider glObjectsProvider, Size inputSize)
|
||||||
|
throws GlUtil.GlException {
|
||||||
|
// Always update the function texture, as it could change on each render cycle.
|
||||||
|
updateFunctionTexture(glObjectsProvider);
|
||||||
|
|
||||||
|
// Only update intermediate and output textures if the size changes.
|
||||||
|
if (inputSize.equals(lastInputSize)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
outputSize = configure(inputSize);
|
||||||
|
// If there is a size change with the filtering (for example, a scaling operation), the first
|
||||||
|
// pass is applied horizontally. As a result, width of the intermediate texture will match the
|
||||||
|
// output size, while the height will be unchanged from the input
|
||||||
|
intermediateSize = new Size(outputSize.getWidth(), inputSize.getHeight());
|
||||||
|
intermediateTexture =
|
||||||
|
configurePixelTexture(glObjectsProvider, intermediateTexture, intermediateSize);
|
||||||
|
outputTexture = configurePixelTexture(glObjectsProvider, outputTexture, outputSize);
|
||||||
|
|
||||||
|
this.lastInputSize = inputSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a function lookup table for the convolution, and stores it in a 16b floating point
|
||||||
|
* texture for GPU access.
|
||||||
|
*/
|
||||||
|
private void updateFunctionTexture(GlObjectsProvider glObjectsProvider)
|
||||||
|
throws GlUtil.GlException {
|
||||||
|
|
||||||
|
ConvolutionFunction1D convolutionFunction = convolution.getConvolution();
|
||||||
|
|
||||||
|
int lutRasterSize =
|
||||||
|
(int)
|
||||||
|
Math.ceil(
|
||||||
|
convolutionFunction.width() * RASTER_SAMPLES_PER_TEXEL + 2 * FUNCTION_LUT_PADDING);
|
||||||
|
|
||||||
|
// The function LUT is mapped to [0, 1] texture coords. We need to calculate what change
|
||||||
|
// in texture coordinated corresponds exactly with a size of 1 texel (or pixel) in the function.
|
||||||
|
// This is basically 1 / function_width, but due to the ceil() call above, it needs to be
|
||||||
|
// calculated based on the actual raster size.
|
||||||
|
this.functionLutTexelStep = 1.0f / ((float) lutRasterSize / RASTER_SAMPLES_PER_TEXEL);
|
||||||
|
|
||||||
|
// The function values are stored in an FP16 texture. Setting FP16 values in a Bitmap requires
|
||||||
|
// multiple steps. For each step, calculate the function value as a Float, and then use the
|
||||||
|
// Half class to convert to FP16 and then read the value as a Short int
|
||||||
|
ShortBuffer functionShortBuffer = ShortBuffer.allocate(lutRasterSize * 4);
|
||||||
|
float rasterSampleStep = 1.0f / RASTER_SAMPLES_PER_TEXEL;
|
||||||
|
float functionDomainStart = convolutionFunction.domainStart();
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < lutRasterSize; i++) {
|
||||||
|
float sampleValue = 0.0f;
|
||||||
|
int unpaddedI = i - FUNCTION_LUT_PADDING;
|
||||||
|
float samplePosition = functionDomainStart + unpaddedI * rasterSampleStep;
|
||||||
|
|
||||||
|
if (unpaddedI >= 0 && i <= lutRasterSize - FUNCTION_LUT_PADDING) {
|
||||||
|
sampleValue = convolutionFunction.value(samplePosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert float to half (fp16) and read out the bits as a short.
|
||||||
|
// Texture for Bitmap is RGBA_F16, so we store the function value in RGB channels and 1.0
|
||||||
|
// in A.
|
||||||
|
short shortEncodedValue = fp16FromFloat(sampleValue);
|
||||||
|
|
||||||
|
// Set RGB
|
||||||
|
functionShortBuffer.put(index++, shortEncodedValue);
|
||||||
|
functionShortBuffer.put(index++, shortEncodedValue);
|
||||||
|
functionShortBuffer.put(index++, shortEncodedValue);
|
||||||
|
|
||||||
|
// Set Alpha
|
||||||
|
functionShortBuffer.put(index++, fp16FromFloat(1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the center of the function in the raster. The formula below is a slight
|
||||||
|
// adjustment on (value - min) / (max - min), where value = 0 at center and
|
||||||
|
// rasterSampleStep * lutRasterSize is equal to (max - min) over the range of the raster
|
||||||
|
// samples, which may be slightly different than the difference between the function's max
|
||||||
|
// and min domain values.
|
||||||
|
// To find the value associated at position 0 in the texture, is the value corresponding with
|
||||||
|
// the leading edge position of the first sample. This needs to account for the padding and
|
||||||
|
// the 1/2 texel offsets used in texture lookups (index 0 is centered at 0.5 / numTexels).
|
||||||
|
float minValueWithPadding =
|
||||||
|
functionDomainStart - rasterSampleStep * (FUNCTION_LUT_PADDING + 0.5f);
|
||||||
|
this.functionLutCenterX = -minValueWithPadding / (rasterSampleStep * lutRasterSize);
|
||||||
|
this.functionLutDomainStart = convolutionFunction.domainStart();
|
||||||
|
this.functionLutWidth = convolutionFunction.width();
|
||||||
|
|
||||||
|
// TODO(b/276982847): Use alternative to Bitmap to create function LUT texture.
|
||||||
|
Bitmap functionLookupBitmap =
|
||||||
|
Bitmap.createBitmap(lutRasterSize, /* height= */ 1, Bitmap.Config.RGBA_F16);
|
||||||
|
functionLookupBitmap.copyPixelsFromBuffer(functionShortBuffer);
|
||||||
|
|
||||||
|
// Create new GL texture if needed.
|
||||||
|
if (functionLutTexture == GlTextureInfo.UNSET || functionLutTexture.width != lutRasterSize) {
|
||||||
|
functionLutTexture.release();
|
||||||
|
|
||||||
|
// Need to use high precision to force 16FP color.
|
||||||
|
int functionLutTextureId =
|
||||||
|
GlUtil.createTexture(
|
||||||
|
lutRasterSize, /* height= */ 1, /* useHighPrecisionColorComponents= */ true);
|
||||||
|
|
||||||
|
functionLutTexture =
|
||||||
|
glObjectsProvider.createBuffersForTexture(
|
||||||
|
functionLutTextureId, lutRasterSize, /* height= */ 1);
|
||||||
|
}
|
||||||
|
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, functionLookupBitmap, /* border= */ 0);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GlTextureInfo configurePixelTexture(
|
||||||
|
GlObjectsProvider glObjectsProvider, GlTextureInfo existingTexture, Size size)
|
||||||
|
throws GlUtil.GlException {
|
||||||
|
if (size.getWidth() == existingTexture.width && size.getHeight() == existingTexture.height) {
|
||||||
|
return existingTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingTexture.release();
|
||||||
|
int texId = GlUtil.createTexture(size.getWidth(), size.getHeight(), useHdr);
|
||||||
|
|
||||||
|
return glObjectsProvider.createBuffersForTexture(texId, size.getWidth(), size.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// BEGIN COPIED FP16 code.
|
||||||
|
// Source: libcore/luni/src/main/java/libcore/util/FP16.java
|
||||||
|
// Float to half float conversion, copied from FP16. This code is introduced in API26, so the
|
||||||
|
// one required method is copied here.
|
||||||
|
private static short fp16FromFloat(float f) {
|
||||||
|
int bits = Float.floatToRawIntBits(f);
|
||||||
|
int s = bits >>> FP32_SIGN_SHIFT;
|
||||||
|
int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK;
|
||||||
|
int m = bits & FP32_SIGNIFICAND_MASK;
|
||||||
|
int outE = 0;
|
||||||
|
int outM = 0;
|
||||||
|
if (e == 0xff) { // Infinite or NaN
|
||||||
|
outE = 0x1f;
|
||||||
|
outM = (m != 0) ? 0x200 : 0;
|
||||||
|
} else {
|
||||||
|
e = e - FP32_EXPONENT_BIAS + FP16_EXPONENT_BIAS;
|
||||||
|
if (e >= 0x1f) { // Overflow
|
||||||
|
outE = 0x1f;
|
||||||
|
} else if (e <= 0) { // Underflow
|
||||||
|
if (e >= -10) {
|
||||||
|
// The fp32 value is a normalized float less than MIN_NORMAL,
|
||||||
|
// we convert to a denorm fp16
|
||||||
|
m |= 0x800000;
|
||||||
|
int shift = 14 - e;
|
||||||
|
outM = m >>> shift;
|
||||||
|
int lowm = m & ((1 << shift) - 1);
|
||||||
|
int hway = 1 << (shift - 1);
|
||||||
|
// if above halfway or exactly halfway and outM is odd
|
||||||
|
if (lowm + (outM & 1) > hway) {
|
||||||
|
// Round to nearest even
|
||||||
|
// Can overflow into exponent bit, which surprisingly is OK.
|
||||||
|
// This increment relies on the +outM in the return statement below
|
||||||
|
outM++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outE = e;
|
||||||
|
outM = m >>> 13;
|
||||||
|
// if above halfway or exactly halfway and outM is odd
|
||||||
|
if ((m & 0x1fff) + (outM & 0x1) > 0x1000) {
|
||||||
|
// Round to nearest even
|
||||||
|
// Can overflow into exponent bit, which surprisingly is OK.
|
||||||
|
// This increment relies on the +outM in the return statement below
|
||||||
|
outM++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The outM is added here as the +1 increments for outM above can
|
||||||
|
// cause an overflow in the exponent bit which is OK.
|
||||||
|
return (short) ((s << FP16_SIGN_SHIFT) | ((outE << FP16_EXPONENT_SHIFT) + outM));
|
||||||
|
}
|
||||||
|
// END FP16 copied code.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit tests for {@link GaussianFunction}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class GaussianFunctionTest {
|
||||||
|
|
||||||
|
private final GaussianFunction function =
|
||||||
|
new GaussianFunction(/* sigma= */ 2.55f, /* numStandardDeviations= */ 4.45f);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void value_samplePositionAboveRange_returnsZero() {
|
||||||
|
assertThat(function.value(/* samplePosition= */ function.domainEnd() + .00001f)).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void value_samplePositionBelowRange_returnsZero() {
|
||||||
|
assertThat(function.value(/* samplePosition= */ -10000000000000f)).isEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void value_samplePositionInRange_returnsSymmetricGaussianFunction() {
|
||||||
|
assertThat(function.value(/* samplePosition= */ 9.999f)).isEqualTo(7.1712595E-5f);
|
||||||
|
assertThat(function.value(/* samplePosition= */ -9.999f)).isEqualTo(7.1712595E-5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Loading…
Reference in a new issue