• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.android.exoplayer2.transformer;
17 
18 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
19 import static com.google.common.truth.Truth.assertThat;
20 import static java.lang.Math.abs;
21 import static java.lang.Math.max;
22 
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Color;
27 import android.graphics.Matrix;
28 import android.graphics.PixelFormat;
29 import android.media.Image;
30 import android.opengl.GLES20;
31 import android.opengl.GLUtils;
32 import androidx.annotation.Nullable;
33 import com.google.android.exoplayer2.util.GlUtil;
34 import com.google.android.exoplayer2.util.Log;
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.nio.ByteBuffer;
40 
41 /**
42  * Utilities for instrumentation tests for the {@link FrameProcessorChain} and {@link
43  * GlFrameProcessor GlFrameProcessors}.
44  */
45 public class BitmapTestUtil {
46 
47   private static final String TAG = "BitmapTestUtil";
48 
49   /* Expected first frames after transformation. */
50   public static final String FIRST_FRAME_PNG_ASSET_STRING =
51       "media/bitmap/sample_mp4_first_frame.png";
52   public static final String TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING =
53       "media/bitmap/sample_mp4_first_frame_translate_right.png";
54   public static final String SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING =
55       "media/bitmap/sample_mp4_first_frame_scale_narrow.png";
56   public static final String ROTATE_THEN_TRANSLATE_EXPECTED_OUTPUT_PNG_ASSET_STRING =
57       "media/bitmap/sample_mp4_first_frame_rotate_then_translate.png";
58   public static final String TRANSLATE_THEN_ROTATE_EXPECTED_OUTPUT_PNG_ASSET_STRING =
59       "media/bitmap/sample_mp4_first_frame_translate_then_rotate.png";
60   public static final String ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING =
61       "media/bitmap/sample_mp4_first_frame_rotate90.png";
62   public static final String REQUEST_OUTPUT_HEIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING =
63       "media/bitmap/sample_mp4_first_frame_request_output_height.png";
64   public static final String ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING =
65       "media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png";
66   /**
67    * Maximum allowed average pixel difference between the expected and actual edited images in pixel
68    * difference-based tests. The value is chosen so that differences in decoder behavior across
69    * emulator versions don't affect whether the test passes for most emulators, but substantial
70    * distortions introduced by changes in the behavior of the {@link GlFrameProcessor
71    * GlFrameProcessors} will cause the test to fail.
72    *
73    * <p>To run pixel difference-based tests on physical devices, please use a value of 5f, rather
74    * than 0.1f. This higher value will ignore some very small errors, but will allow for some
75    * differences caused by graphics implementations to be ignored. When the difference is close to
76    * the threshold, manually inspect expected/actual bitmaps to confirm failure, as it's possible
77    * this is caused by a difference in the codec or graphics implementation as opposed to a {@link
78    * GlFrameProcessor} issue.
79    */
80   public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f;
81 
82   /**
83    * Reads a bitmap from the specified asset location.
84    *
85    * @param assetString Relative path to the asset within the assets directory.
86    * @return A {@link Bitmap}.
87    * @throws IOException If the bitmap can't be read.
88    */
readBitmap(String assetString)89   public static Bitmap readBitmap(String assetString) throws IOException {
90     Bitmap bitmap;
91     try (InputStream inputStream = getApplicationContext().getAssets().open(assetString)) {
92       bitmap = BitmapFactory.decodeStream(inputStream);
93     }
94     return bitmap;
95   }
96 
97   /**
98    * Returns a bitmap with the same information as the provided alpha/red/green/blue 8-bits per
99    * component image.
100    */
createArgb8888BitmapFromRgba8888Image(Image image)101   public static Bitmap createArgb8888BitmapFromRgba8888Image(Image image) {
102     int width = image.getWidth();
103     int height = image.getHeight();
104     assertThat(image.getPlanes()).hasLength(1);
105     assertThat(image.getFormat()).isEqualTo(PixelFormat.RGBA_8888);
106     Image.Plane plane = image.getPlanes()[0];
107     ByteBuffer buffer = plane.getBuffer();
108     int[] colors = new int[width * height];
109     for (int y = 0; y < height; y++) {
110       for (int x = 0; x < width; x++) {
111         int offset = y * plane.getRowStride() + x * plane.getPixelStride();
112         int r = buffer.get(offset) & 0xFF;
113         int g = buffer.get(offset + 1) & 0xFF;
114         int b = buffer.get(offset + 2) & 0xFF;
115         int a = buffer.get(offset + 3) & 0xFF;
116         colors[y * width + x] = Color.argb(a, r, g, b);
117       }
118     }
119     return Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
120   }
121 
122   /**
123    * Returns the average difference between the expected and actual bitmaps, calculated using the
124    * maximum difference across all color channels for each pixel, then divided by the total number
125    * of pixels in the image. The bitmap resolutions must match and they must use configuration
126    * {@link Bitmap.Config#ARGB_8888}.
127    *
128    * @param expected The expected {@link Bitmap}.
129    * @param actual The actual {@link Bitmap} produced by the test.
130    * @param testId The name of the test that produced the {@link Bitmap}, or {@code null} if the
131    *     differences bitmap should not be saved to cache.
132    * @return The average of the maximum absolute pixel-wise differences between the expected and
133    *     actual bitmaps.
134    */
getAveragePixelAbsoluteDifferenceArgb8888( Bitmap expected, Bitmap actual, @Nullable String testId)135   public static float getAveragePixelAbsoluteDifferenceArgb8888(
136       Bitmap expected, Bitmap actual, @Nullable String testId) {
137     int width = actual.getWidth();
138     int height = actual.getHeight();
139     assertThat(width).isEqualTo(expected.getWidth());
140     assertThat(height).isEqualTo(expected.getHeight());
141     assertThat(actual.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
142     long sumMaximumAbsoluteDifferences = 0;
143     // Debug-only image diff without alpha. To use, set a breakpoint right before the method return
144     // to view the difference between the expected and actual bitmaps. A passing test should show
145     // an image that is completely black (color == 0).
146     Bitmap differencesBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
147     for (int y = 0; y < height; y++) {
148       for (int x = 0; x < width; x++) {
149         int actualColor = actual.getPixel(x, y);
150         int expectedColor = expected.getPixel(x, y);
151 
152         int alphaDifference = abs(Color.alpha(actualColor) - Color.alpha(expectedColor));
153         int redDifference = abs(Color.red(actualColor) - Color.red(expectedColor));
154         int blueDifference = abs(Color.blue(actualColor) - Color.blue(expectedColor));
155         int greenDifference = abs(Color.green(actualColor) - Color.green(expectedColor));
156         differencesBitmap.setPixel(x, y, Color.rgb(redDifference, blueDifference, greenDifference));
157 
158         int maximumAbsoluteDifference = 0;
159         maximumAbsoluteDifference = max(maximumAbsoluteDifference, alphaDifference);
160         maximumAbsoluteDifference = max(maximumAbsoluteDifference, redDifference);
161         maximumAbsoluteDifference = max(maximumAbsoluteDifference, blueDifference);
162         maximumAbsoluteDifference = max(maximumAbsoluteDifference, greenDifference);
163 
164         sumMaximumAbsoluteDifferences += maximumAbsoluteDifference;
165       }
166     }
167     if (testId != null) {
168       try {
169         saveTestBitmapToCacheDirectory(
170             testId, "diff", differencesBitmap, /* throwOnFailure= */ false);
171       } catch (IOException impossible) {
172         throw new IllegalStateException(impossible);
173       }
174     }
175     return (float) sumMaximumAbsoluteDifferences / (width * height);
176   }
177 
178   /**
179    * Saves the {@link Bitmap} to the {@link Context#getCacheDir() cache directory} as a PNG.
180    *
181    * <p>File name will be {@code <testId>_<bitmapLabel>.png}. If {@code throwOnFailure} is {@code
182    * false}, any {@link IOException} will be caught and logged.
183    *
184    * @param testId Name of the test that produced the {@link Bitmap}.
185    * @param bitmapLabel Label to identify the bitmap.
186    * @param bitmap The {@link Bitmap} to save.
187    * @param throwOnFailure Whether to throw an exception if the bitmap can't be saved.
188    * @throws IOException If the bitmap can't be saved and {@code throwOnFailure} is {@code true}.
189    */
saveTestBitmapToCacheDirectory( String testId, String bitmapLabel, Bitmap bitmap, boolean throwOnFailure)190   public static void saveTestBitmapToCacheDirectory(
191       String testId, String bitmapLabel, Bitmap bitmap, boolean throwOnFailure) throws IOException {
192     File file =
193         new File(
194             getApplicationContext().getExternalCacheDir(), testId + "_" + bitmapLabel + ".png");
195     try (FileOutputStream outputStream = new FileOutputStream(file)) {
196       bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream);
197     } catch (IOException e) {
198       if (throwOnFailure) {
199         throw e;
200       } else {
201         Log.e(TAG, "Could not write Bitmap to file path: " + file.getAbsolutePath(), e);
202       }
203     }
204   }
205 
206   /**
207    * Creates a bitmap with the values of the current OpenGL framebuffer.
208    *
209    * <p>This method may block until any previously called OpenGL commands are complete.
210    *
211    * @param width The width of the pixel rectangle to read.
212    * @param height The height of the pixel rectangle to read.
213    * @return A {@link Bitmap} with the framebuffer's values.
214    */
createArgb8888BitmapFromCurrentGlFramebuffer(int width, int height)215   public static Bitmap createArgb8888BitmapFromCurrentGlFramebuffer(int width, int height) {
216     ByteBuffer rgba8888Buffer = ByteBuffer.allocateDirect(width * height * 4);
217     GLES20.glReadPixels(
218         0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgba8888Buffer);
219     GlUtil.checkGlError();
220     Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
221     // According to https://www.khronos.org/opengl/wiki/Pixel_Transfer#Endian_issues,
222     // the colors will have the order RGBA in client memory. This is what the bitmap expects:
223     // https://developer.android.com/reference/android/graphics/Bitmap.Config#ARGB_8888.
224     bitmap.copyPixelsFromBuffer(rgba8888Buffer);
225     // Flip the bitmap as its positive y-axis points down while OpenGL's positive y-axis points up.
226     return flipBitmapVertically(bitmap);
227   }
228 
229   /**
230    * Creates a {@link GLES20#GL_TEXTURE_2D 2-dimensional OpenGL texture} with the bitmap's contents.
231    *
232    * @param bitmap A {@link Bitmap}.
233    * @return The identifier of the newly created texture.
234    */
createGlTextureFromBitmap(Bitmap bitmap)235   public static int createGlTextureFromBitmap(Bitmap bitmap) {
236     int texId = GlUtil.createTexture(bitmap.getWidth(), bitmap.getHeight());
237     // Put the flipped bitmap in the OpenGL texture as the bitmap's positive y-axis points down
238     // while OpenGL's positive y-axis points up.
239     GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, flipBitmapVertically(bitmap), 0);
240     GlUtil.checkGlError();
241     return texId;
242   }
243 
flipBitmapVertically(Bitmap bitmap)244   private static Bitmap flipBitmapVertically(Bitmap bitmap) {
245     Matrix flip = new Matrix();
246     flip.postScale(1f, -1f);
247     return Bitmap.createBitmap(
248         bitmap,
249         /* x= */ 0,
250         /* y= */ 0,
251         bitmap.getWidth(),
252         bitmap.getHeight(),
253         flip,
254         /* filter= */ true);
255   }
256 
BitmapTestUtil()257   private BitmapTestUtil() {}
258 }
259