java.lang.Object
↳androidx.media3.test.utils.BitmapPixelTestUtil
Gradle dependencies
compile group: 'androidx.media3', name: 'media3-test-utils', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-test-utils
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-test-utils:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
Utilities for pixel tests.
Summary
Methods |
---|
public static BitmapPixelTestUtil.ImageBuffer | copyByteBufferFromRbga8888Image(Image image)
Returns the BitmapPixelTestUtil.ImageBuffer that is copied from the Image 's internal buffer. |
public static void | copyRbga8888BitmapToImage(Bitmap bitmap, Image image)
Copies image data from the specified Bitmap into the Image , which must be an
image. |
public static Bitmap | createArgb8888BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer. |
public static Bitmap | createArgb8888BitmapFromRgba8888Image(Image image)
Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per
component image. |
public static Bitmap | createArgb8888BitmapFromRgba8888ImageBuffer(BitmapPixelTestUtil.ImageBuffer imageBuffer)
|
public static Bitmap | createArgb8888BitmapWithSolidColor(int width, int height, int color)
Returns a solid Bitmap with every pixel having the same color. |
public static Bitmap | createFp16BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer. |
public static int | createGlTextureFromBitmap(Bitmap bitmap)
Creates a with the bitmap's contents. |
public static Bitmap | createGrayscaleBitmapFromYuv420888Image(Image image, Bitmap.Config bitmapConfig)
Returns a grayscale bitmap from the Luma channel in the image. |
public static Bitmap | createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer. |
public static Bitmap | flipBitmapVertically(Bitmap bitmap)
|
public static float | getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual, java.lang.String testId)
Returns the average difference between the expected and actual bitmaps, calculated using the
maximum difference across all color channels for each pixel, then divided by the total number
of pixels in the image, without saving the difference bitmap. |
public static float | getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual, java.lang.String testId, java.lang.String differencesBitmapPath)
Returns the average difference between the expected and actual bitmaps. |
public static float | getBitmapAveragePixelAbsoluteDifferenceFp16(Bitmap expected, Bitmap actual)
Returns the average difference between the expected and actual bitmaps. |
public static void | maybeSaveTestBitmap(java.lang.String testId, java.lang.String bitmapLabel, Bitmap bitmap, java.lang.String path)
Tries to save the Bitmap as a PNG to the , and if not provided, tries to
save to the . |
public static Bitmap | readBitmap(java.lang.String assetString)
Reads a bitmap from the specified asset location. |
public static Bitmap | readBitmapUnpremultipliedAlpha(java.lang.String assetString)
Reads a bitmap with unpremultiplied alpha from the specified asset location. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final float
MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICEMaximum allowed average pixel difference between bitmaps generated using devices.
This value is for for 8-bit primaries in pixel difference-based tests.
The value is chosen so that differences in decoder behavior across devices don't affect
whether the test passes, but substantial distortions introduced by changes in tested components
will cause the test to fail.
When the difference is close to the threshold, manually inspect expected/actual bitmaps to
confirm failure, as it's possible this is caused by a difference in the codec or graphics
implementation as opposed to an issue in the tested component.
This value is larger than BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE to support the
larger variance in decoder outputs between different physical devices and emulators.
public static final float
MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCEMaximum allowed average pixel difference between bitmaps generated.
This value is for for 8-bit primaries in pixel difference-based tests.
The value is chosen so that differences in decoder behavior across devices don't affect
whether the test passes, but substantial distortions introduced by changes in tested components
will cause the test to fail.
When the difference is close to the threshold, manually inspect expected/actual bitmaps to
confirm failure, as it's possible this is caused by a difference in the codec or graphics
implementation as opposed to an issue in the tested component.
The value is the same as BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE
if running on physical devices.
public static final float
MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE_FP16Maximum allowed average pixel difference between bitmaps with 16-bit primaries generated using
devices.
The value is chosen so that differences in decoder behavior across devices in pixel
difference-based tests don't affect whether the test passes, but substantial distortions
introduced by changes in tested components will cause the test to fail.
When the difference is close to the threshold, manually inspect expected/actual bitmaps to
confirm failure, as it's possible this is caused by a difference in the codec or graphics
implementation as opposed to an issue in the tested component.
This value is larger than BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE to support the
larger variance in decoder outputs between different physical devices and emulators.
public static final float
MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMAMaximum allowed average pixel difference between bitmaps generated from luma values.
See also: BitmapPixelTestUtil.MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE
Methods
public static Bitmap
readBitmap(java.lang.String assetString)
Reads a bitmap from the specified asset location.
Parameters:
assetString: Relative path to the asset within the assets directory.
Returns:
A Bitmap
.
public static Bitmap
readBitmapUnpremultipliedAlpha(java.lang.String assetString)
Reads a bitmap with unpremultiplied alpha from the specified asset location.
Parameters:
assetString: Relative path to the asset within the assets directory.
Returns:
A Bitmap
.
public static Bitmap
createArgb8888BitmapFromRgba8888Image(Image image)
Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per
component image.
Returns the BitmapPixelTestUtil.ImageBuffer that is copied from the Image
's internal buffer.
public static void
copyRbga8888BitmapToImage(Bitmap bitmap, Image image)
Copies image data from the specified Bitmap
into the Image
, which must be an
image.
public static Bitmap
createGrayscaleBitmapFromYuv420888Image(Image image, Bitmap.Config bitmapConfig)
Returns a grayscale bitmap from the Luma channel in the image.
public static Bitmap
createArgb8888BitmapWithSolidColor(int width, int height, int color)
Returns a solid Bitmap
with every pixel having the same color.
Parameters:
width: The width of image to create, in pixels.
height: The height of image to create, in pixels.
color: An RGBA color created by .
public static float
getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual, java.lang.String testId, java.lang.String differencesBitmapPath)
Returns the average difference between the expected and actual bitmaps.
Calculated using the maximum difference across all color channels for each pixel, then
divided by the total number of pixels in the image. Bitmap resolutions must match and must use
configuration .
Tries to save a difference bitmap between expected and actual bitmaps.
Parameters:
expected: The expected Bitmap
.
actual: The actual Bitmap
produced by the test.
testId: The name of the test that produced the Bitmap
, or null if the
differences bitmap should not be saved to cache.
differencesBitmapPath: Folder path for the produced pixel-wise difference Bitmap
to be saved in or null if the assumed default save path should be used.
Returns:
The average of the maximum absolute pixel-wise differences between the expected and
actual bitmaps.
public static float
getBitmapAveragePixelAbsoluteDifferenceFp16(Bitmap expected, Bitmap actual)
Returns the average difference between the expected and actual bitmaps.
Calculated using the maximum difference across all color channels for each pixel, then
divided by the total number of pixels in the image. Bitmap resolutions must match and must use
configuration .
Parameters:
expected: The expected Bitmap
.
actual: The actual Bitmap
produced by the test.
Returns:
The average of the maximum absolute pixel-wise differences between the expected and
actual bitmaps.
public static float
getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap expected, Bitmap actual, java.lang.String testId)
Returns the average difference between the expected and actual bitmaps, calculated using the
maximum difference across all color channels for each pixel, then divided by the total number
of pixels in the image, without saving the difference bitmap. See BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap, Bitmap, String, String).
This method is the overloaded version of BitmapPixelTestUtil.getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap, Bitmap, String, String) without a specified saved path.
public static void
maybeSaveTestBitmap(java.lang.String testId, java.lang.String bitmapLabel, Bitmap bitmap, java.lang.String path)
Tries to save the Bitmap
as a PNG to the , and if not provided, tries to
save to the .
File name will be _.png. If the file failed to write, any
java.io.IOException
will be caught and logged. The path will be logged regardless of success.
Parameters:
testId: Name of the test that produced the Bitmap
.
bitmapLabel: Label to identify the bitmap.
bitmap: The Bitmap
to save.
path: Folder path for the supplied Bitmap
to be saved in or null if the
should be saved in.
public static Bitmap
createArgb8888BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer.
This method may block until any previously called OpenGL commands are complete.
This method incorrectly marks the output Bitmap as premultiplied
, even though OpenGL typically outputs only non-premultiplied alpha. Use BitmapPixelTestUtil.createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer(int, int) to properly handle alpha.
Parameters:
width: The width of the pixel rectangle to read.
height: The height of the pixel rectangle to read.
Returns:
A Bitmap
with the framebuffer's values.
public static Bitmap
createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer.
This method may block until any previously called OpenGL commands are complete.
Parameters:
width: The width of the pixel rectangle to read.
height: The height of the pixel rectangle to read.
Returns:
A Bitmap
with the framebuffer's values.
public static Bitmap
createFp16BitmapFromFocusedGlFramebuffer(int width, int height)
Creates a bitmap with the values of the focused OpenGL
framebuffer.
This method may block until any previously called OpenGL commands are complete.
This method incorrectly marks the output Bitmap as premultiplied
, even though OpenGL typically outputs only non-premultiplied alpha. Call Bitmap
with false on the output bitmap to properly handle alpha.
Parameters:
width: The width of the pixel rectangle to read.
height: The height of the pixel rectangle to read.
Returns:
A Bitmap
with the framebuffer's values.
public static int
createGlTextureFromBitmap(Bitmap bitmap)
Creates a with the bitmap's contents.
Parameters:
bitmap: A Bitmap
.
Returns:
The identifier of the newly created texture.
public static Bitmap
flipBitmapVertically(Bitmap bitmap)
Source
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* 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.test.utils;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.isRunningOnEmulator;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.media.Image;
import android.opengl.GLES20;
import android.opengl.GLES30;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Arrays;
/** Utilities for pixel tests. */
// TODO(b/263395272): After the bug is fixed and dependent tests are moved back to media3.effect,
// move this back to the effect tests directory.
@UnstableApi
public class BitmapPixelTestUtil {
/** Represents a {@link ByteBuffer} read from an {@link Image}. */
public static final class ImageBuffer {
public final ByteBuffer buffer;
public final int width;
public final int height;
public final int rowStride;
public final int pixelStride;
public ImageBuffer(ByteBuffer buffer, int width, int height, int rowStride, int pixelStride) {
this.buffer = buffer;
this.width = width;
this.height = height;
this.rowStride = rowStride;
this.pixelStride = pixelStride;
}
}
private static final String TAG = "BitmapPixelTestUtil";
/**
* Maximum allowed average pixel difference between bitmaps generated using devices.
*
* <p>This value is for for 8-bit primaries in pixel difference-based tests.
*
* <p>The value is chosen so that differences in decoder behavior across devices don't affect
* whether the test passes, but substantial distortions introduced by changes in tested components
* will cause the test to fail.
*
* <p>When the difference is close to the threshold, manually inspect expected/actual bitmaps to
* confirm failure, as it's possible this is caused by a difference in the codec or graphics
* implementation as opposed to an issue in the tested component.
*
* <p>This value is larger than {@link #MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} to support the
* larger variance in decoder outputs between different physical devices and emulators.
*/
// TODO: b/279154364 - Stop allowing 15f threshold after bug is fixed.
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE =
!Util.MODEL.equals("H8266") && !Util.MODEL.equals("H8416") ? 5f : 15f;
/**
* Maximum allowed average pixel difference between bitmaps generated.
*
* <p>This value is for for 8-bit primaries in pixel difference-based tests.
*
* <p>The value is chosen so that differences in decoder behavior across devices don't affect
* whether the test passes, but substantial distortions introduced by changes in tested components
* will cause the test to fail.
*
* <p>When the difference is close to the threshold, manually inspect expected/actual bitmaps to
* confirm failure, as it's possible this is caused by a difference in the codec or graphics
* implementation as opposed to an issue in the tested component.
*
* <p>The value is the same as {@link #MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE}
* if running on physical devices.
*/
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE =
isRunningOnEmulator() ? 1f : MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE;
/**
* Maximum allowed average pixel difference between bitmaps with 16-bit primaries generated using
* devices.
*
* <p>The value is chosen so that differences in decoder behavior across devices in pixel
* difference-based tests don't affect whether the test passes, but substantial distortions
* introduced by changes in tested components will cause the test to fail.
*
* <p>When the difference is close to the threshold, manually inspect expected/actual bitmaps to
* confirm failure, as it's possible this is caused by a difference in the codec or graphics
* implementation as opposed to an issue in the tested component.
*
* <p>This value is larger than {@link #MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE} to support the
* larger variance in decoder outputs between different physical devices and emulators.
*/
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_DIFFERENT_DEVICE_FP16 = .01f;
/**
* Maximum allowed average pixel difference between bitmaps generated from luma values.
*
* @see #MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE
*/
public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE_LUMA = 8.0f;
/**
* Reads a bitmap from the specified asset location.
*
* @param assetString Relative path to the asset within the assets directory.
* @return A {@link Bitmap}.
* @throws IOException If the bitmap can't be read.
*/
// TODO: b/295523484 - Update all tests using readBitmap to instead use
// readBitmapUnpremultipliedAlpha, and rename readBitmapUnpremultipliedAlpha back to readBitmap.
public static Bitmap readBitmap(String assetString) throws IOException {
Bitmap bitmap;
try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) {
bitmap = BitmapFactory.decodeStream(inputStream);
}
return bitmap;
}
/**
* Reads a bitmap with unpremultiplied alpha from the specified asset location.
*
* @param assetString Relative path to the asset within the assets directory.
* @return A {@link Bitmap}.
* @throws IOException If the bitmap can't be read.
*/
public static Bitmap readBitmapUnpremultipliedAlpha(String assetString) throws IOException {
Bitmap bitmap;
try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) {
// Media3 expected bitmaps are generated from OpenGL, which uses non-premultiplied colors.
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inPremultiplied = false;
bitmap = BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, bitmapOptions);
}
return checkNotNull(bitmap);
}
/**
* Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per
* component image.
*/
public static Bitmap createArgb8888BitmapFromRgba8888Image(Image image) {
checkArgument(image.getPlanes().length == 1);
checkArgument(image.getFormat() == PixelFormat.RGBA_8888);
Image.Plane plane = image.getPlanes()[0];
return createArgb8888BitmapFromRgba8888ImageBuffer(
new ImageBuffer(
plane.getBuffer(),
image.getWidth(),
image.getHeight(),
plane.getRowStride(),
plane.getPixelStride()));
}
/** Returns the {@link ImageBuffer} that is copied from the {@link Image}'s internal buffer. */
public static ImageBuffer copyByteBufferFromRbga8888Image(Image image) {
assertThat(image.getPlanes()).hasLength(1);
assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888);
Image.Plane plane = image.getPlanes()[0];
ByteBuffer copiedBuffer = ByteBuffer.allocate(plane.getBuffer().remaining());
copiedBuffer.put(plane.getBuffer());
copiedBuffer.flip();
return new ImageBuffer(
copiedBuffer,
image.getWidth(),
image.getHeight(),
plane.getRowStride(),
plane.getPixelStride());
}
/**
* Copies image data from the specified {@link Bitmap} into the {@link Image}, which must be an
* {@linkplain PixelFormat#RGBA_8888} image.
*/
public static void copyRbga8888BitmapToImage(Bitmap bitmap, Image image) {
assertThat(image.getPlanes()).hasLength(1);
assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888);
Image.Plane imagePlane = image.getPlanes()[0];
ByteBuffer imageBuffer = imagePlane.getBuffer();
for (int y = 0; y < bitmap.getHeight(); y++) {
for (int x = 0; x < bitmap.getWidth(); x++) {
int imageBufferOffset = y * imagePlane.getRowStride() + x * imagePlane.getPixelStride();
int argbPixel = bitmap.getPixel(x, y);
imageBuffer.position(imageBufferOffset);
imageBuffer.put((byte) ((argbPixel >> 16) & 0xFF));
imageBuffer.put((byte) ((argbPixel >> 8) & 0xFF));
imageBuffer.put((byte) (argbPixel & 0xFF));
imageBuffer.put((byte) ((argbPixel >> 24) & 0xFF));
}
}
}
public static Bitmap createArgb8888BitmapFromRgba8888ImageBuffer(ImageBuffer imageBuffer) {
int[] colors = new int[imageBuffer.width * imageBuffer.height];
for (int y = 0; y < imageBuffer.height; y++) {
for (int x = 0; x < imageBuffer.width; x++) {
int offset = y * imageBuffer.rowStride + x * imageBuffer.pixelStride;
int r = imageBuffer.buffer.get(offset) & 0xFF;
int g = imageBuffer.buffer.get(offset + 1) & 0xFF;
int b = imageBuffer.buffer.get(offset + 2) & 0xFF;
int a = imageBuffer.buffer.get(offset + 3) & 0xFF;
colors[y * imageBuffer.width + x] = Color.argb(a, r, g, b);
}
}
return Bitmap.createBitmap(
colors, imageBuffer.width, imageBuffer.height, Bitmap.Config.ARGB_8888);
}
/**
* Returns a grayscale bitmap from the Luma channel in the {@link ImageFormat#YUV_420_888} image.
*/
public static Bitmap createGrayscaleBitmapFromYuv420888Image(
Image image, Bitmap.Config bitmapConfig) {
int width = image.getWidth();
int height = image.getHeight();
assertThat(image.getPlanes()).hasLength(3);
assertThat(image.getFormat()).isEqualTo(ImageFormat.YUV_420_888);
Image.Plane lumaPlane = image.getPlanes()[0];
ByteBuffer lumaBuffer = lumaPlane.getBuffer();
int[] colors = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = y * lumaPlane.getRowStride() + x * lumaPlane.getPixelStride();
int lumaValue = lumaBuffer.get(offset) & 0xFF;
colors[y * width + x] =
Color.argb(
/* alpha= */ 255,
/* red= */ lumaValue,
/* green= */ lumaValue,
/* blue= */ lumaValue);
}
}
return Bitmap.createBitmap(colors, width, height, bitmapConfig);
}
/**
* Returns a solid {@link Bitmap} with every pixel having the same color.
*
* @param width The width of image to create, in pixels.
* @param height The height of image to create, in pixels.
* @param color An RGBA color created by {@link Color}.
*/
public static Bitmap createArgb8888BitmapWithSolidColor(int width, int height, int color) {
int[] colors = new int[width * height];
Arrays.fill(colors, color);
return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
}
/**
* Returns the average difference between the expected and actual bitmaps.
*
* <p>Calculated using the maximum difference across all color channels for each pixel, then
* divided by the total number of pixels in the image. Bitmap resolutions must match and must use
* configuration {@link Bitmap.Config#ARGB_8888}.
*
* <p>Tries to save a difference bitmap between expected and actual bitmaps.
*
* @param expected The expected {@link Bitmap}.
* @param actual The actual {@link Bitmap} produced by the test.
* @param testId The name of the test that produced the {@link Bitmap}, or {@code null} if the
* differences bitmap should not be saved to cache.
* @param differencesBitmapPath Folder path for the produced pixel-wise difference {@link Bitmap}
* to be saved in or {@code null} if the assumed default save path should be used.
* @return The average of the maximum absolute pixel-wise differences between the expected and
* actual bitmaps.
*/
public static float getBitmapAveragePixelAbsoluteDifferenceArgb8888(
Bitmap expected,
Bitmap actual,
@Nullable String testId,
@Nullable String differencesBitmapPath) {
assertBitmapsMatch(expected, actual);
int width = actual.getWidth();
int height = actual.getHeight();
long sumMaximumAbsoluteDifferences = 0;
// Debug-only image diff without alpha. To use, set a breakpoint right before the method return
// to view the difference between the expected and actual bitmaps. A passing test should show
// an image that is completely black (color == 0).
Bitmap differencesBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int actualColor = actual.getPixel(x, y);
int expectedColor = expected.getPixel(x, y);
if (Color.alpha(actualColor) == 0 && Color.alpha(expectedColor) == 0) {
// If both colors are transparent, ignore RGB pixel differences for this pixel.
differencesBitmap.setPixel(x, y, Color.TRANSPARENT);
continue;
}
int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor));
int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor));
int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor));
int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor));
differencesBitmap.setPixel(x, y, Color.rgb(redDifference, greenDifference, blueDifference));
int maximumAbsoluteDifference = 0;
maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference);
sumMaximumAbsoluteDifferences += maximumAbsoluteDifference;
}
}
if (testId != null) {
maybeSaveTestBitmap(
testId, /* bitmapLabel= */ "diff", differencesBitmap, differencesBitmapPath);
}
return (float) sumMaximumAbsoluteDifferences / (width * height);
}
/**
* Returns the average difference between the expected and actual bitmaps.
*
* <p>Calculated using the maximum difference across all color channels for each pixel, then
* divided by the total number of pixels in the image. Bitmap resolutions must match and must use
* configuration {@link Bitmap.Config#RGBA_F16}.
*
* @param expected The expected {@link Bitmap}.
* @param actual The actual {@link Bitmap} produced by the test.
* @return The average of the maximum absolute pixel-wise differences between the expected and
* actual bitmaps.
*/
@RequiresApi(29) // Bitmap#getColor()
public static float getBitmapAveragePixelAbsoluteDifferenceFp16(Bitmap expected, Bitmap actual) {
assertBitmapsMatch(expected, actual);
int width = actual.getWidth();
int height = actual.getHeight();
float sumMaximumAbsoluteDifferences = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Color actualColor = actual.getColor(x, y);
Color expectedColor = expected.getColor(x, y);
if (actualColor.alpha() == 0 && expectedColor.alpha() == 0) {
// If both colors are transparent, ignore RGB pixel differences for this pixel.
continue;
}
float alphaDifference = abs(actualColor.alpha() - expectedColor.alpha());
float redDifference = abs(actualColor.red() - expectedColor.red());
float blueDifference = abs(actualColor.blue() - expectedColor.blue());
float greenDifference = abs(actualColor.green() - expectedColor.green());
float maximumAbsoluteDifference = 0;
maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference);
maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference);
sumMaximumAbsoluteDifferences += maximumAbsoluteDifference;
}
}
return sumMaximumAbsoluteDifferences / (width * height);
}
private static void assertBitmapsMatch(Bitmap expected, Bitmap actual) {
assertThat(actual.getWidth()).isEqualTo(expected.getWidth());
assertThat(actual.getHeight()).isEqualTo(expected.getHeight());
assertThat(actual.getConfig()).isEqualTo(expected.getConfig());
}
/**
* Returns the average difference between the expected and actual bitmaps, calculated using the
* maximum difference across all color channels for each pixel, then divided by the total number
* of pixels in the image, without saving the difference bitmap. See {@link
* BitmapPixelTestUtil#getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap, Bitmap, String,
* String)}.
*
* <p>This method is the overloaded version of {@link
* BitmapPixelTestUtil#getBitmapAveragePixelAbsoluteDifferenceArgb8888(Bitmap, Bitmap, String,
* String)} without a specified saved path.
*/
public static float getBitmapAveragePixelAbsoluteDifferenceArgb8888(
Bitmap expected, Bitmap actual, @Nullable String testId) {
return getBitmapAveragePixelAbsoluteDifferenceArgb8888(
expected, actual, testId, /* differencesBitmapPath= */ null);
}
/**
* Tries to save the {@link Bitmap} as a PNG to the {@code <path>}, and if not provided, tries to
* save to the {@link Context#getCacheDir() cache directory}.
*
* <p>File name will be {@code <testId>_<bitmapLabel>.png}. If the file failed to write, any
* {@link IOException} will be caught and logged. The path will be logged regardless of success.
*
* @param testId Name of the test that produced the {@link Bitmap}.
* @param bitmapLabel Label to identify the bitmap.
* @param bitmap The {@link Bitmap} to save.
* @param path Folder path for the supplied {@link Bitmap} to be saved in or {@code null} if the
* {@link Context#getCacheDir() cache directory} should be saved in.
*/
public static void maybeSaveTestBitmap(
String testId, String bitmapLabel, Bitmap bitmap, @Nullable String path) {
String fileName = testId + (bitmapLabel.isEmpty() ? "" : "_" + bitmapLabel) + ".png";
File file;
if (path != null) {
File folder = new File(path);
checkState(
folder.exists() || folder.mkdirs(), "Could not create directory to save images: " + path);
file = new File(path, fileName);
} else {
file = new File(getApplicationContext().getExternalCacheDir(), fileName);
}
try (FileOutputStream outputStream = new FileOutputStream(file)) {
bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream);
Log.d(TAG, "Saved bitmap to file path: " + file.getAbsolutePath());
} catch (IOException e) {
Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e);
}
try {
// Use reflection here as this is an experimental API that may not work for all users
Class<?> testStorageClass = Class.forName("androidx.test.services.storage.TestStorage");
Method method = testStorageClass.getMethod("openOutputFile", String.class);
Object testStorage = testStorageClass.getDeclaredConstructor().newInstance();
OutputStream outputStream = checkNotNull((OutputStream) method.invoke(testStorage, fileName));
bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream);
} catch (ClassNotFoundException e) {
// Do nothing
} catch (Exception e) {
Log.i(TAG, "Could not write Bitmap to test storage: " + fileName, e);
}
}
/**
* Creates a {@link Bitmap.Config#ARGB_8888} bitmap with the values of the focused OpenGL
* framebuffer.
*
* <p>This method may block until any previously called OpenGL commands are complete.
*
* <p>This method incorrectly marks the output Bitmap as {@link Bitmap#isPremultiplied()
* premultiplied}, even though OpenGL typically outputs only non-premultiplied alpha. Use {@link
* #createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer} to properly handle alpha.
*
* @param width The width of the pixel rectangle to read.
* @param height The height of the pixel rectangle to read.
* @return A {@link Bitmap} with the framebuffer's values.
*/
// TODO: b/295523484 - Update all tests using createArgb8888BitmapFromFocusedGlFramebuffer to
// instead use createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer, and rename
// createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer back to
// createArgb8888BitmapFromFocusedGlFramebuffer. Also, apply
// setPremultiplied(false) to createBitmapFromFocusedGlFrameBuffer.
public static Bitmap createArgb8888BitmapFromFocusedGlFramebuffer(int width, int height)
throws GlUtil.GlException {
return createBitmapFromFocusedGlFrameBuffer(
width, height, /* pixelSize= */ 4, GLES20.GL_UNSIGNED_BYTE, Bitmap.Config.ARGB_8888);
}
/**
* Creates a {@link Bitmap.Config#ARGB_8888} bitmap with the values of the focused OpenGL
* framebuffer.
*
* <p>This method may block until any previously called OpenGL commands are complete.
*
* @param width The width of the pixel rectangle to read.
* @param height The height of the pixel rectangle to read.
* @return A {@link Bitmap} with the framebuffer's values.
*/
public static Bitmap createUnpremultipliedArgb8888BitmapFromFocusedGlFramebuffer(
int width, int height) throws GlUtil.GlException {
Bitmap bitmap =
createBitmapFromFocusedGlFrameBuffer(
width, height, /* pixelSize= */ 4, GLES20.GL_UNSIGNED_BYTE, Bitmap.Config.ARGB_8888);
bitmap.setPremultiplied(false); // OpenGL represents colors as unpremultiplied.
return bitmap;
}
/**
* Creates a {@link Bitmap.Config#RGBA_F16} bitmap with the values of the focused OpenGL
* framebuffer.
*
* <p>This method may block until any previously called OpenGL commands are complete.
*
* <p>This method incorrectly marks the output Bitmap as {@link Bitmap#isPremultiplied()
* premultiplied}, even though OpenGL typically outputs only non-premultiplied alpha. Call {@link
* Bitmap#setPremultiplied} with {@code false} on the output bitmap to properly handle alpha.
*
* @param width The width of the pixel rectangle to read.
* @param height The height of the pixel rectangle to read.
* @return A {@link Bitmap} with the framebuffer's values.
*/
@RequiresApi(26) // Bitmap.Config.RGBA_F16
public static Bitmap createFp16BitmapFromFocusedGlFramebuffer(int width, int height)
throws GlUtil.GlException {
return createBitmapFromFocusedGlFrameBuffer(
width, height, /* pixelSize= */ 8, GLES30.GL_HALF_FLOAT, Bitmap.Config.RGBA_F16);
}
private static Bitmap createBitmapFromFocusedGlFrameBuffer(
int width, int height, int pixelSize, int glReadPixelsFormat, Bitmap.Config bitmapConfig)
throws GlUtil.GlException {
ByteBuffer pixelBuffer = ByteBuffer.allocateDirect(width * height * pixelSize);
GLES20.glReadPixels(
/* x= */ 0, /* y= */ 0, width, height, GLES20.GL_RGBA, glReadPixelsFormat, pixelBuffer);
GlUtil.checkGlError();
Bitmap bitmap = Bitmap.createBitmap(width, height, bitmapConfig);
// According to https://www.khronos.org/opengl/wiki/Pixel_Transfer#Endian_issues,
// the colors will have the order RGBA in client memory. This is what the bitmap expects:
// https://developer.android.com/reference/android/graphics/Bitmap.Config.
bitmap.copyPixelsFromBuffer(pixelBuffer);
// Flip the bitmap as its positive y-axis points down while OpenGL's positive y-axis points up.
return flipBitmapVertically(bitmap);
}
/**
* Creates a {@link GLES20#GL_TEXTURE_2D 2-dimensional OpenGL texture} with the bitmap's contents.
*
* @param bitmap A {@link Bitmap}.
* @return The identifier of the newly created texture.
*/
public static int createGlTextureFromBitmap(Bitmap bitmap) throws GlUtil.GlException {
// Put the flipped bitmap in the OpenGL texture as the bitmap's positive y-axis points down
// while OpenGL's positive y-axis points up.
return GlUtil.createTexture(flipBitmapVertically(bitmap));
}
public static Bitmap flipBitmapVertically(Bitmap bitmap) {
boolean wasPremultiplied = bitmap.isPremultiplied();
if (!wasPremultiplied) {
// Bitmap.createBitmap must be called on a premultiplied bitmap.
bitmap.setPremultiplied(true);
}
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
Bitmap flippedBitmap =
Bitmap.createBitmap(
bitmap,
/* x= */ 0,
/* y= */ 0,
bitmap.getWidth(),
bitmap.getHeight(),
flip,
/* filter= */ true);
flippedBitmap.setPremultiplied(wasPremultiplied);
return flippedBitmap;
}
private BitmapPixelTestUtil() {}
}