Update Lanczos Effect to support different orientations

PiperOrigin-RevId: 739884019
This commit is contained in:
Googler 2025-03-24 04:06:10 -07:00 committed by Copybara-Service
parent 75a067a223
commit ae563dbab0
4 changed files with 165 additions and 22 deletions

View File

@ -217,7 +217,8 @@ public final class CompositionPreviewActivity extends AppCompatActivity {
String selectedResolutionHeight = String.valueOf(resolutionHeightSpinner.getSelectedItem()); String selectedResolutionHeight = String.valueOf(resolutionHeightSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) {
int resolutionHeight = Integer.parseInt(selectedResolutionHeight); int resolutionHeight = Integer.parseInt(selectedResolutionHeight);
videoEffectsBuilder.add(LanczosResample.scaleToFit(10000, resolutionHeight)); videoEffectsBuilder.add(
LanczosResample.scaleToFitWithFlexibleOrientation(10000, resolutionHeight));
videoEffectsBuilder.add(Presentation.createForShortSide(resolutionHeight)); videoEffectsBuilder.add(Presentation.createForShortSide(resolutionHeight));
} }
ImmutableList<Effect> videoEffects = videoEffectsBuilder.build(); ImmutableList<Effect> videoEffects = videoEffectsBuilder.build();

View File

@ -677,7 +677,7 @@ public final class TransformerActivity extends AppCompatActivity {
int resolutionHeight = int resolutionHeight =
bundle.getInt(ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET); bundle.getInt(ConfigurationActivity.RESOLUTION_HEIGHT, /* defaultValue= */ C.LENGTH_UNSET);
if (resolutionHeight != C.LENGTH_UNSET) { if (resolutionHeight != C.LENGTH_UNSET) {
effects.add(LanczosResample.scaleToFit(10000, resolutionHeight)); effects.add(LanczosResample.scaleToFitWithFlexibleOrientation(10000, resolutionHeight));
effects.add(Presentation.createForShortSide(resolutionHeight)); effects.add(Presentation.createForShortSide(resolutionHeight));
} }

View File

@ -24,6 +24,7 @@ import static androidx.media3.test.utils.TestUtil.PSNR_THRESHOLD;
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar; import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 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 static java.lang.Math.round;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
@ -93,7 +94,7 @@ public class LanczosResampleTest {
GlTextureInfo inputTextureInfo = setupInputTexture(ORIGINAL_JPG_ASSET_PATH); GlTextureInfo inputTextureInfo = setupInputTexture(ORIGINAL_JPG_ASSET_PATH);
float scale = 1f / 6; float scale = 1f / 6;
Size outputSize = Size outputSize =
new Size((int) (inputTextureInfo.width * scale), (int) (inputTextureInfo.height * scale)); new Size(round(inputTextureInfo.width * scale), round(inputTextureInfo.height * scale));
lanczosShaderProgram = lanczosShaderProgram =
LanczosResample.scaleToFit(outputSize.getWidth(), outputSize.getHeight()) LanczosResample.scaleToFit(outputSize.getWidth(), outputSize.getHeight())
.toGlShaderProgram(context, /* useHdr= */ false); .toGlShaderProgram(context, /* useHdr= */ false);
@ -109,12 +110,35 @@ public class LanczosResampleTest {
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD); assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
} }
@Test
public void queueInputFrame_with6xDownscaleFlexibleOrientation_matchesGoldenFile()
throws Exception {
GlTextureInfo inputTextureInfo = setupInputTexture(ORIGINAL_JPG_ASSET_PATH);
float scale = 1f / 6;
Size outputSize =
new Size(round(inputTextureInfo.width * scale), round(inputTextureInfo.height * scale));
lanczosShaderProgram =
LanczosResample.scaleToFitWithFlexibleOrientation(
outputSize.getHeight(), outputSize.getWidth())
.toGlShaderProgram(context, /* useHdr= */ false);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(DOWNSCALED_6X_PNG_ASSET_PATH);
lanczosShaderProgram.queueInputFrame(
new DefaultGlObjectsProvider(eglContext), inputTextureInfo, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
@Test @Test
public void queueInputFrame_with3xUpscale_matchesGoldenFile() throws Exception { public void queueInputFrame_with3xUpscale_matchesGoldenFile() throws Exception {
GlTextureInfo inputTextureInfo = setupInputTexture(SMALLER_JPG_ASSET_PATH); GlTextureInfo inputTextureInfo = setupInputTexture(SMALLER_JPG_ASSET_PATH);
float scale = 3; float scale = 3;
Size outputSize = Size outputSize =
new Size((int) (inputTextureInfo.width * scale), (int) (inputTextureInfo.height * scale)); new Size(round(inputTextureInfo.width * scale), round(inputTextureInfo.height * scale));
lanczosShaderProgram = lanczosShaderProgram =
LanczosResample.scaleToFit(outputSize.getWidth(), outputSize.getHeight()) LanczosResample.scaleToFit(outputSize.getWidth(), outputSize.getHeight())
.toGlShaderProgram(context, /* useHdr= */ false); .toGlShaderProgram(context, /* useHdr= */ false);
@ -130,6 +154,29 @@ public class LanczosResampleTest {
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD); assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
} }
@Test
public void queueInputFrame_with3xUpscaleFlexibleOrientation_matchesGoldenFile()
throws Exception {
GlTextureInfo inputTextureInfo = setupInputTexture(SMALLER_JPG_ASSET_PATH);
float scale = 3;
Size outputSize =
new Size((int) (inputTextureInfo.width * scale), (int) (inputTextureInfo.height * scale));
lanczosShaderProgram =
LanczosResample.scaleToFitWithFlexibleOrientation(
outputSize.getWidth(), outputSize.getHeight())
.toGlShaderProgram(context, /* useHdr= */ false);
setupOutputTexture(outputSize.getWidth(), outputSize.getHeight());
Bitmap expectedBitmap = readBitmap(UPSCALED_3X_PNG_ASSET_PATH);
lanczosShaderProgram.queueInputFrame(
new DefaultGlObjectsProvider(eglContext), inputTextureInfo, /* presentationTimeUs= */ 0);
Bitmap actualBitmap =
createArgb8888BitmapFromFocusedGlFramebuffer(outputSize.getWidth(), outputSize.getHeight());
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", actualBitmap, /* path= */ null);
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
@Test @Test
public void isNoOp_whenSizeDoesntChange_returnsTrue() { public void isNoOp_whenSizeDoesntChange_returnsTrue() {
LanczosResample lanczosResample = LanczosResample.scaleToFit(720, 1280); LanczosResample lanczosResample = LanczosResample.scaleToFit(720, 1280);
@ -137,6 +184,14 @@ public class LanczosResampleTest {
assertThat(lanczosResample.isNoOp(720, 1280)).isTrue(); assertThat(lanczosResample.isNoOp(720, 1280)).isTrue();
} }
@Test
public void isNoOp_whenSizeDoesntChangeFlexibleOrientation_returnsTrue() {
LanczosResample lanczosResample = LanczosResample.scaleToFitWithFlexibleOrientation(720, 1280);
assertThat(lanczosResample.isNoOp(720, 1280)).isTrue();
assertThat(lanczosResample.isNoOp(1280, 720)).isTrue();
}
@Test @Test
public void isNoOp_forSmallScalingFactors_returnsTrue() { public void isNoOp_forSmallScalingFactors_returnsTrue() {
LanczosResample lanczosResample = LanczosResample.scaleToFit(1920, 1072); LanczosResample lanczosResample = LanczosResample.scaleToFit(1920, 1072);
@ -145,12 +200,28 @@ public class LanczosResampleTest {
} }
@Test @Test
public void isNoOp_forLargeScalingFactors_returnsTrue() { public void isNoOp_forSmallScalingFactorsFlexibleOrientation_returnsTrue() {
LanczosResample lanczosResample = LanczosResample.scaleToFitWithFlexibleOrientation(1920, 1072);
assertThat(lanczosResample.isNoOp(1920, 1080)).isTrue();
assertThat(lanczosResample.isNoOp(1080, 1920)).isTrue();
}
@Test
public void isNoOp_forLargeScalingFactors_returnsFalse() {
LanczosResample lanczosResample = LanczosResample.scaleToFit(1920, 1068); LanczosResample lanczosResample = LanczosResample.scaleToFit(1920, 1068);
assertThat(lanczosResample.isNoOp(1920, 1080)).isFalse(); assertThat(lanczosResample.isNoOp(1920, 1080)).isFalse();
} }
@Test
public void isNoOp_forLargeScalingFactorsFlexibleOrientation_returnsFalse() {
LanczosResample lanczosResample = LanczosResample.scaleToFitWithFlexibleOrientation(1920, 1068);
assertThat(lanczosResample.isNoOp(1920, 1080)).isFalse();
assertThat(lanczosResample.isNoOp(1080, 1920)).isFalse();
}
private static GlTextureInfo setupInputTexture(String path) throws Exception { private static GlTextureInfo setupInputTexture(String path) throws Exception {
Bitmap inputBitmap = readBitmap(path); Bitmap inputBitmap = readBitmap(path);
return new GlTextureInfo( return new GlTextureInfo(

View File

@ -43,8 +43,9 @@ public final class LanczosResample implements GlEffect {
private static final float NO_OP_THRESHOLD = 0.01f; private static final float NO_OP_THRESHOLD = 0.01f;
private final float radius; private final float radius;
private final int width; private final int longSide;
private final int height; private final int shortSide;
private final boolean assumeLandscapeOrientation;
/** /**
* Creates an instance. * Creates an instance.
@ -56,20 +57,56 @@ public final class LanczosResample implements GlEffect {
@IntRange(from = 1) int width, @IntRange(from = 1) int height) { @IntRange(from = 1) int width, @IntRange(from = 1) int height) {
checkArgument(width > 0); checkArgument(width > 0);
checkArgument(height > 0); checkArgument(height > 0);
return new LanczosResample(DEFAULT_RADIUS, width, height); return new LanczosResample(
DEFAULT_RADIUS, width, height, /* assumeLandscapeOrientation= */ true);
} }
private LanczosResample(float radius, int width, int height) { /**
* Creates an instance.
*
* <p>The output resolution will be either {@code firstDimension} x {@code secondDimension} or
* {@code secondDimension} x {@code firstDimension}. The longer of {@code firstDimension} or
* {@code secondDimension} will have the same orientation as the longer side of the {@link Size}
* passed in to {@link LanczosResampleScaledFunctionProvider#configure}.
*
* @param firstDimension The first dimension of the output contents.
* @param secondDimension The second dimension of the output contents.
*/
public static LanczosResample scaleToFitWithFlexibleOrientation(
@IntRange(from = 1) int firstDimension, @IntRange(from = 1) int secondDimension) {
checkArgument(firstDimension > 0);
checkArgument(secondDimension > 0);
if (firstDimension > secondDimension) {
return new LanczosResample(
DEFAULT_RADIUS,
/* longSide= */ firstDimension,
/* shortSide= */ secondDimension,
/* assumeLandscapeOrientation= */ false);
} else {
return new LanczosResample(
DEFAULT_RADIUS,
/* longSide= */ secondDimension,
/* shortSide= */ firstDimension,
/* assumeLandscapeOrientation= */ false);
}
}
private LanczosResample(
float radius, int longSide, int shortSide, boolean assumeLandscapeOrientation) {
this.radius = radius; this.radius = radius;
this.width = width; this.longSide = longSide;
this.height = height; this.shortSide = shortSide;
this.assumeLandscapeOrientation = assumeLandscapeOrientation;
} }
@Override @Override
public GlShaderProgram toGlShaderProgram(Context context, boolean useHdr) public GlShaderProgram toGlShaderProgram(Context context, boolean useHdr)
throws VideoFrameProcessingException { throws VideoFrameProcessingException {
return new SeparableConvolutionShaderProgram( return new SeparableConvolutionShaderProgram(
context, useHdr, new LanczosResampleScaledFunctionProvider(radius, width, height)); context,
useHdr,
new LanczosResampleScaledFunctionProvider(
radius, longSide, shortSide, assumeLandscapeOrientation));
} }
/** /**
@ -80,7 +117,13 @@ public final class LanczosResample implements GlEffect {
*/ */
@Override @Override
public boolean isNoOp(int inputWidth, int inputHeight) { public boolean isNoOp(int inputWidth, int inputHeight) {
return abs(scalingFactorToFit(inputWidth, inputHeight, width, height) - 1f) < NO_OP_THRESHOLD; Size targetSize =
getTargetSize(inputWidth, inputHeight, longSide, shortSide, assumeLandscapeOrientation);
return abs(
scalingFactorToFit(
inputWidth, inputHeight, targetSize.getWidth(), targetSize.getHeight())
- 1f)
< NO_OP_THRESHOLD;
} }
/** /**
@ -108,21 +151,24 @@ public final class LanczosResample implements GlEffect {
// Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero. // Note: We deliberately don't use Float.MIN_VALUE because it's positive & very close to zero.
private static final float SCALE_UNSET = -Float.MAX_VALUE; private static final float SCALE_UNSET = -Float.MAX_VALUE;
private final float radius; private final float radius;
private final int width; private final int longSide;
private final int height; private final int shortSide;
private final boolean assumeLandscapeOrientation;
private float scale; private float scale;
private LanczosResampleScaledFunctionProvider( private LanczosResampleScaledFunctionProvider(
@FloatRange(from = 0, fromInclusive = false) float radius, @FloatRange(from = 0, fromInclusive = false) float radius,
@IntRange(from = 1) int width, @IntRange(from = 1) int longSide,
@IntRange(from = 1) int height) { @IntRange(from = 1) int shortSide,
boolean assumeLandscapeOrientation) {
checkArgument(radius > 0); checkArgument(radius > 0);
checkArgument(width > 0); checkArgument(longSide > 0);
checkArgument(height > 0); checkArgument(shortSide > 0);
this.radius = radius; this.radius = radius;
this.width = width; this.longSide = longSide;
this.height = height; this.shortSide = shortSide;
this.assumeLandscapeOrientation = assumeLandscapeOrientation;
scale = SCALE_UNSET; scale = SCALE_UNSET;
} }
@ -136,8 +182,33 @@ public final class LanczosResample implements GlEffect {
@Override @Override
public Size configure(Size inputSize) { public Size configure(Size inputSize) {
scale = scalingFactorToFit(inputSize.getWidth(), inputSize.getHeight(), width, height); Size targetSize =
LanczosResample.getTargetSize(
inputSize.getWidth(),
inputSize.getHeight(),
longSide,
shortSide,
assumeLandscapeOrientation);
scale =
scalingFactorToFit(
inputSize.getWidth(),
inputSize.getHeight(),
targetSize.getWidth(),
targetSize.getHeight());
return new Size(round(inputSize.getWidth() * scale), round(inputSize.getHeight() * scale)); return new Size(round(inputSize.getWidth() * scale), round(inputSize.getHeight() * scale));
} }
} }
private static Size getTargetSize(
int inputWidth,
int inputHeight,
int longSide,
int shortSide,
boolean assumeLandscapeOrientation) {
if (assumeLandscapeOrientation || inputWidth > inputHeight) {
return new Size(longSide, shortSide);
} else {
return new Size(shortSide, longSide);
}
}
} }