mirror of
https://github.com/samsonjs/media.git
synced 2026-03-25 09:25:53 +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;
|
||||
|
||||
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 androidx.media3.effect.EffectsTestUtil.generateAndProcessFrames;
|
||||
import static androidx.media3.effect.EffectsTestUtil.getAndAssertOutputBitmaps;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
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.NullableType;
|
||||
import androidx.media3.common.util.Util;
|
||||
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 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.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
|
@ -64,12 +45,9 @@ public class FrameDropTest {
|
|||
@Rule public final TestName testName = new TestName();
|
||||
|
||||
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 DefaultVideoFrameProcessor defaultVideoFrameProcessor;
|
||||
private @MonotonicNonNull String testId;
|
||||
|
||||
@EnsuresNonNull({"textureBitmapReader", "testId"})
|
||||
@Before
|
||||
|
|
@ -78,24 +56,20 @@ public class FrameDropTest {
|
|||
testId = testName.getMethodName();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
checkNotNull(defaultVideoFrameProcessor).release();
|
||||
}
|
||||
|
||||
@Test
|
||||
@RequiresNonNull({"textureBitmapReader", "testId"})
|
||||
public void frameDrop_withDefaultStrategy_outputsFramesAtTheCorrectPresentationTimesUs()
|
||||
throws Exception {
|
||||
ImmutableList<Long> frameTimesUs =
|
||||
ImmutableList.of(0L, 16_000L, 32_000L, 48_000L, 58_000L, 71_000L, 86_000L);
|
||||
FrameDropEffect frameDropEffect =
|
||||
FrameDropEffect.createDefaultFrameDropEffect(/* targetFrameRate= */ 30);
|
||||
|
||||
ImmutableList<Long> actualPresentationTimesUs =
|
||||
processFramesToEndOfStream(
|
||||
frameTimesUs, FrameDropEffect.createDefaultFrameDropEffect(/* targetFrameRate= */ 30));
|
||||
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||
|
||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 32_000L, 71_000L).inOrder();
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -104,162 +78,60 @@ public class FrameDropTest {
|
|||
throws Exception {
|
||||
ImmutableList<Long> frameTimesUs =
|
||||
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 =
|
||||
processFramesToEndOfStream(
|
||||
frameTimesUs,
|
||||
FrameDropEffect.createSimpleFrameDropEffect(
|
||||
/* expectedFrameRate= */ 6, /* targetFrameRate= */ 2));
|
||||
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||
|
||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 750_000L).inOrder();
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||
}
|
||||
|
||||
@Test
|
||||
@RequiresNonNull({"textureBitmapReader", "testId"})
|
||||
public void frameDrop_withSimpleStrategy_outputsAllFrames() throws Exception {
|
||||
ImmutableList<Long> frameTimesUs = ImmutableList.of(0L, 333_333L, 666_667L);
|
||||
FrameDropEffect frameDropEffect =
|
||||
FrameDropEffect.createSimpleFrameDropEffect(
|
||||
/* expectedFrameRate= */ 3, /* targetFrameRate= */ 3);
|
||||
|
||||
ImmutableList<Long> actualPresentationTimesUs =
|
||||
processFramesToEndOfStream(
|
||||
frameTimesUs,
|
||||
FrameDropEffect.createSimpleFrameDropEffect(
|
||||
/* expectedFrameRate= */ 3, /* targetFrameRate= */ 3));
|
||||
generateAndProcessBlackTimeStampedFrames(frameTimesUs, frameDropEffect);
|
||||
|
||||
assertThat(actualPresentationTimesUs).containsExactly(0L, 333_333L, 666_667L).inOrder();
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId);
|
||||
getAndAssertOutputBitmaps(textureBitmapReader, actualPresentationTimesUs, testId, ASSET_PATH);
|
||||
}
|
||||
|
||||
private static void getAndAssertOutputBitmaps(
|
||||
TextureBitmapReader textureBitmapReader, List<Long> presentationTimesUs, String testId)
|
||||
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", ASSET_PATH, presentationTimeUs));
|
||||
maybeSaveTestBitmap(
|
||||
testId, String.valueOf(presentationTimeUs), actualBitmap, /* path= */ null);
|
||||
float averagePixelAbsoluteDifference =
|
||||
getBitmapAveragePixelAbsoluteDifferenceArgb8888(expectedBitmap, actualBitmap, testId);
|
||||
assertThat(averagePixelAbsoluteDifference)
|
||||
.isAtMost(MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE);
|
||||
}
|
||||
}
|
||||
|
||||
@EnsuresNonNull("defaultVideoFrameProcessor")
|
||||
private ImmutableList<Long> processFramesToEndOfStream(
|
||||
List<Long> inputPresentationTimesUs, FrameDropEffect frameDropEffect) throws Exception {
|
||||
AtomicReference<@NullableType VideoFrameProcessingException>
|
||||
videoFrameProcessingExceptionReference = new AtomicReference<>();
|
||||
BlankFrameProducer blankFrameProducer =
|
||||
new BlankFrameProducer(BLANK_FRAME_WIDTH, BLANK_FRAME_HEIGHT);
|
||||
CountDownLatch videoFrameProcessorReadyCountDownLatch = new CountDownLatch(1);
|
||||
CountDownLatch videoFrameProcessingEndedCountDownLatch = new CountDownLatch(1);
|
||||
ImmutableList.Builder<Long> actualPresentationTimesUs = new ImmutableList.Builder<>();
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
private ImmutableList<Long> generateAndProcessBlackTimeStampedFrames(
|
||||
ImmutableList<Long> frameTimesUs, FrameDropEffect frameDropEffect) throws Exception {
|
||||
int blankFrameWidth = 100;
|
||||
int blankFrameHeight = 50;
|
||||
Consumer<SpannableString> textSpanConsumer =
|
||||
(text) -> {
|
||||
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 generateAndProcessFrames(
|
||||
blankFrameWidth,
|
||||
blankFrameHeight,
|
||||
frameTimesUs,
|
||||
frameDropEffect,
|
||||
checkNotNull(textureBitmapReader),
|
||||
textSpanConsumer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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