• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.webrtc;
12 
13 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
14 import static org.hamcrest.Matchers.lessThanOrEqualTo;
15 import static org.junit.Assert.assertEquals;
16 import static org.junit.Assert.assertThat;
17 
18 import android.graphics.Matrix;
19 import android.opengl.GLES20;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import android.support.test.filters.SmallTest;
23 import java.nio.ByteBuffer;
24 import java.util.ArrayList;
25 import java.util.Collections;
26 import java.util.List;
27 import org.chromium.base.test.params.BaseJUnit4RunnerDelegate;
28 import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
29 import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
30 import org.chromium.base.test.params.ParameterSet;
31 import org.chromium.base.test.params.ParameterizedRunner;
32 import org.junit.BeforeClass;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 
36 /**
37  * Test VideoFrame buffers of different kind of formats: I420, RGB, OES, NV12, NV21, and verify
38  * toI420() and cropAndScale() behavior. Creating RGB/OES textures involves VideoFrameDrawer and
39  * GlRectDrawer and we are testing the full chain I420 -> OES/RGB texture -> I420, with and without
40  * cropping in the middle. Reading textures back to I420 also exercises the YuvConverter code.
41  */
42 @RunWith(ParameterizedRunner.class)
43 @UseRunnerDelegate(BaseJUnit4RunnerDelegate.class)
44 public class VideoFrameBufferTest {
45   /**
46    * These tests are parameterized on this enum which represents the different VideoFrame.Buffers.
47    */
48   private static enum BufferType { I420, RGB_TEXTURE, OES_TEXTURE, NV21, NV12 }
49 
50   @ClassParameter private static List<ParameterSet> CLASS_PARAMS = new ArrayList<>();
51 
52   static {
53     for (BufferType bufferType : BufferType.values()) {
54       // The parameterization framework can not send Enums, so we pass it as a string instead.
CLASS_PARAMS.add(new ParameterSet().value(bufferType.name()).name(bufferType.name()))55       CLASS_PARAMS.add(new ParameterSet().value(bufferType.name()).name(bufferType.name()));
56     }
57   }
58 
59   @BeforeClass
setUp()60   public static void setUp() {
61     // Needed for JniCommon.nativeAllocateByteBuffer() to work, which is used from JavaI420Buffer.
62     System.loadLibrary(TestConstants.NATIVE_LIBRARY);
63   }
64 
65   private final BufferType bufferType;
66 
VideoFrameBufferTest(String bufferTypeName)67   public VideoFrameBufferTest(String bufferTypeName) {
68     // Parse the string back to an enum.
69     bufferType = BufferType.valueOf(bufferTypeName);
70   }
71 
72   /**
73    * Create a VideoFrame.Buffer of the given type with the same pixel content as the given I420
74    * buffer.
75    */
createBufferWithType( BufferType bufferType, VideoFrame.I420Buffer i420Buffer)76   private static VideoFrame.Buffer createBufferWithType(
77       BufferType bufferType, VideoFrame.I420Buffer i420Buffer) {
78     switch (bufferType) {
79       case I420:
80         return i420Buffer.toI420();
81       case RGB_TEXTURE:
82         return createRgbTextureBuffer(/* eglContext= */ null, i420Buffer);
83       case OES_TEXTURE:
84         return createOesTextureBuffer(/* eglContext= */ null, i420Buffer);
85       case NV21:
86         return createNV21Buffer(i420Buffer);
87       case NV12:
88         return createNV12Buffer(i420Buffer);
89       default:
90         throw new IllegalArgumentException("Unknown buffer type: " + bufferType);
91     }
92   }
93 
createBufferToTest(VideoFrame.I420Buffer i420Buffer)94   private VideoFrame.Buffer createBufferToTest(VideoFrame.I420Buffer i420Buffer) {
95     return createBufferWithType(this.bufferType, i420Buffer);
96   }
97 
98   /**
99    * Creates a 16x16 I420 buffer that varies smoothly and spans all RGB values.
100    */
createTestI420Buffer()101   public static VideoFrame.I420Buffer createTestI420Buffer() {
102     final int width = 16;
103     final int height = 16;
104     final int[] yData = new int[] {156, 162, 167, 172, 177, 182, 187, 193, 199, 203, 209, 214, 219,
105         224, 229, 235, 147, 152, 157, 162, 168, 173, 178, 183, 188, 193, 199, 205, 210, 215, 220,
106         225, 138, 143, 148, 153, 158, 163, 168, 174, 180, 184, 190, 195, 200, 205, 211, 216, 128,
107         133, 138, 144, 149, 154, 159, 165, 170, 175, 181, 186, 191, 196, 201, 206, 119, 124, 129,
108         134, 140, 145, 150, 156, 161, 166, 171, 176, 181, 187, 192, 197, 109, 114, 119, 126, 130,
109         136, 141, 146, 151, 156, 162, 167, 172, 177, 182, 187, 101, 105, 111, 116, 121, 126, 132,
110         137, 142, 147, 152, 157, 162, 168, 173, 178, 90, 96, 101, 107, 112, 117, 122, 127, 132, 138,
111         143, 148, 153, 158, 163, 168, 82, 87, 92, 97, 102, 107, 113, 118, 123, 128, 133, 138, 144,
112         149, 154, 159, 72, 77, 83, 88, 93, 98, 103, 108, 113, 119, 124, 129, 134, 139, 144, 150, 63,
113         68, 73, 78, 83, 89, 94, 99, 104, 109, 114, 119, 125, 130, 135, 140, 53, 58, 64, 69, 74, 79,
114         84, 89, 95, 100, 105, 110, 115, 120, 126, 131, 44, 49, 54, 59, 64, 70, 75, 80, 85, 90, 95,
115         101, 106, 111, 116, 121, 34, 40, 45, 50, 55, 60, 65, 71, 76, 81, 86, 91, 96, 101, 107, 113,
116         25, 30, 35, 40, 46, 51, 56, 61, 66, 71, 77, 82, 87, 92, 98, 103, 16, 21, 26, 31, 36, 41, 46,
117         52, 57, 62, 67, 72, 77, 83, 89, 94};
118     final int[] uData = new int[] {110, 113, 116, 118, 120, 123, 125, 128, 113, 116, 118, 120, 123,
119         125, 128, 130, 116, 118, 120, 123, 125, 128, 130, 132, 118, 120, 123, 125, 128, 130, 132,
120         135, 120, 123, 125, 128, 130, 132, 135, 138, 123, 125, 128, 130, 132, 135, 138, 139, 125,
121         128, 130, 132, 135, 138, 139, 142, 128, 130, 132, 135, 138, 139, 142, 145};
122     final int[] vData = new int[] {31, 45, 59, 73, 87, 100, 114, 127, 45, 59, 73, 87, 100, 114, 128,
123         141, 59, 73, 87, 100, 114, 127, 141, 155, 73, 87, 100, 114, 127, 141, 155, 168, 87, 100,
124         114, 128, 141, 155, 168, 182, 100, 114, 128, 141, 155, 168, 182, 197, 114, 127, 141, 155,
125         168, 182, 196, 210, 127, 141, 155, 168, 182, 196, 210, 224};
126     return JavaI420Buffer.wrap(width, height, toByteBuffer(yData),
127         /* strideY= */ width, toByteBuffer(uData), /* strideU= */ width / 2, toByteBuffer(vData),
128         /* strideV= */ width / 2,
129         /* releaseCallback= */ null);
130   }
131 
132   /**
133    * Create an RGB texture buffer available in |eglContext| with the same pixel content as the given
134    * I420 buffer.
135    */
createRgbTextureBuffer( EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer)136   public static VideoFrame.TextureBuffer createRgbTextureBuffer(
137       EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer) {
138     final int width = i420Buffer.getWidth();
139     final int height = i420Buffer.getHeight();
140 
141     final HandlerThread renderThread = new HandlerThread("RGB texture thread");
142     renderThread.start();
143     final Handler renderThreadHandler = new Handler(renderThread.getLooper());
144     return ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
145       // Create EGL base with a pixel buffer as display output.
146       final EglBase eglBase = EglBase.create(eglContext, EglBase.CONFIG_PIXEL_BUFFER);
147       eglBase.createDummyPbufferSurface();
148       eglBase.makeCurrent();
149 
150       final GlTextureFrameBuffer textureFrameBuffer = new GlTextureFrameBuffer(GLES20.GL_RGBA);
151       textureFrameBuffer.setSize(width, height);
152 
153       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.getFrameBufferId());
154       drawI420Buffer(i420Buffer);
155       GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
156 
157       final YuvConverter yuvConverter = new YuvConverter();
158       return new TextureBufferImpl(width, height, VideoFrame.TextureBuffer.Type.RGB,
159           textureFrameBuffer.getTextureId(),
160           /* transformMatrix= */ new Matrix(), renderThreadHandler, yuvConverter,
161           /* releaseCallback= */ () -> renderThreadHandler.post(() -> {
162             textureFrameBuffer.release();
163             yuvConverter.release();
164             eglBase.release();
165             renderThread.quit();
166           }));
167     });
168   }
169 
170   /**
171    * Create an OES texture buffer available in |eglContext| with the same pixel content as the given
172    * I420 buffer.
173    */
174   public static VideoFrame.TextureBuffer createOesTextureBuffer(
175       EglBase.Context eglContext, VideoFrame.I420Buffer i420Buffer) {
176     final int width = i420Buffer.getWidth();
177     final int height = i420Buffer.getHeight();
178 
179     // Create resources for generating OES textures.
180     final SurfaceTextureHelper surfaceTextureHelper =
181         SurfaceTextureHelper.create("SurfaceTextureHelper test", eglContext);
182     surfaceTextureHelper.setTextureSize(width, height);
183 
184     final HandlerThread renderThread = new HandlerThread("OES texture thread");
185     renderThread.start();
186     final Handler renderThreadHandler = new Handler(renderThread.getLooper());
187     final VideoFrame.TextureBuffer oesBuffer =
188         ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, () -> {
189           // Create EGL base with the SurfaceTexture as display output.
190           final EglBase eglBase = EglBase.create(eglContext, EglBase.CONFIG_PLAIN);
191           eglBase.createSurface(surfaceTextureHelper.getSurfaceTexture());
192           eglBase.makeCurrent();
193           assertEquals(width, eglBase.surfaceWidth());
194           assertEquals(height, eglBase.surfaceHeight());
195 
196           final SurfaceTextureHelperTest.MockTextureListener listener =
197               new SurfaceTextureHelperTest.MockTextureListener();
198           surfaceTextureHelper.startListening(listener);
199 
200           // Draw the frame and block until an OES texture is delivered.
201           drawI420Buffer(i420Buffer);
202           eglBase.swapBuffers();
203           final VideoFrame.TextureBuffer textureBuffer = listener.waitForTextureBuffer();
204           surfaceTextureHelper.stopListening();
205           surfaceTextureHelper.dispose();
206 
207           return textureBuffer;
208         });
209     renderThread.quit();
210 
211     return oesBuffer;
212   }
213 
214   /** Create an NV21Buffer with the same pixel content as the given I420 buffer. */
215   public static NV21Buffer createNV21Buffer(VideoFrame.I420Buffer i420Buffer) {
216     final int width = i420Buffer.getWidth();
217     final int height = i420Buffer.getHeight();
218     final int chromaStride = width;
219     final int chromaWidth = (width + 1) / 2;
220     final int chromaHeight = (height + 1) / 2;
221     final int ySize = width * height;
222 
223     final ByteBuffer nv21Buffer = ByteBuffer.allocateDirect(ySize + chromaStride * chromaHeight);
224     // We don't care what the array offset is since we only want an array that is direct.
225     @SuppressWarnings("ByteBufferBackingArray") final byte[] nv21Data = nv21Buffer.array();
226 
227     for (int y = 0; y < height; ++y) {
228       for (int x = 0; x < width; ++x) {
229         final byte yValue = i420Buffer.getDataY().get(y * i420Buffer.getStrideY() + x);
230         nv21Data[y * width + x] = yValue;
231       }
232     }
233     for (int y = 0; y < chromaHeight; ++y) {
234       for (int x = 0; x < chromaWidth; ++x) {
235         final byte uValue = i420Buffer.getDataU().get(y * i420Buffer.getStrideU() + x);
236         final byte vValue = i420Buffer.getDataV().get(y * i420Buffer.getStrideV() + x);
237         nv21Data[ySize + y * chromaStride + 2 * x + 0] = vValue;
238         nv21Data[ySize + y * chromaStride + 2 * x + 1] = uValue;
239       }
240     }
241     return new NV21Buffer(nv21Data, width, height, /* releaseCallback= */ null);
242   }
243 
244   /** Create an NV12Buffer with the same pixel content as the given I420 buffer. */
245   public static NV12Buffer createNV12Buffer(VideoFrame.I420Buffer i420Buffer) {
246     final int width = i420Buffer.getWidth();
247     final int height = i420Buffer.getHeight();
248     final int chromaStride = width;
249     final int chromaWidth = (width + 1) / 2;
250     final int chromaHeight = (height + 1) / 2;
251     final int ySize = width * height;
252 
253     final ByteBuffer nv12Buffer = ByteBuffer.allocateDirect(ySize + chromaStride * chromaHeight);
254 
255     for (int y = 0; y < height; ++y) {
256       for (int x = 0; x < width; ++x) {
257         final byte yValue = i420Buffer.getDataY().get(y * i420Buffer.getStrideY() + x);
258         nv12Buffer.put(y * width + x, yValue);
259       }
260     }
261     for (int y = 0; y < chromaHeight; ++y) {
262       for (int x = 0; x < chromaWidth; ++x) {
263         final byte uValue = i420Buffer.getDataU().get(y * i420Buffer.getStrideU() + x);
264         final byte vValue = i420Buffer.getDataV().get(y * i420Buffer.getStrideV() + x);
265         nv12Buffer.put(ySize + y * chromaStride + 2 * x + 0, uValue);
266         nv12Buffer.put(ySize + y * chromaStride + 2 * x + 1, vValue);
267       }
268     }
269     return new NV12Buffer(width, height, /* stride= */ width, /* sliceHeight= */ height, nv12Buffer,
270         /* releaseCallback */ null);
271   }
272 
273   /** Print the ByteBuffer plane to the StringBuilder. */
274   private static void printPlane(
275       StringBuilder stringBuilder, int width, int height, ByteBuffer plane, int stride) {
276     for (int y = 0; y < height; ++y) {
277       for (int x = 0; x < width; ++x) {
278         final int value = plane.get(y * stride + x) & 0xFF;
279         if (x != 0) {
280           stringBuilder.append(", ");
281         }
282         stringBuilder.append(value);
283       }
284       stringBuilder.append("\n");
285     }
286   }
287 
288   /** Convert the pixel content of an I420 buffer to a string representation. */
289   private static String i420BufferToString(VideoFrame.I420Buffer buffer) {
290     final StringBuilder stringBuilder = new StringBuilder();
291     stringBuilder.append(
292         "I420 buffer with size: " + buffer.getWidth() + "x" + buffer.getHeight() + ".\n");
293     stringBuilder.append("Y-plane:\n");
294     printPlane(stringBuilder, buffer.getWidth(), buffer.getHeight(), buffer.getDataY(),
295         buffer.getStrideY());
296     final int chromaWidth = (buffer.getWidth() + 1) / 2;
297     final int chromaHeight = (buffer.getHeight() + 1) / 2;
298     stringBuilder.append("U-plane:\n");
299     printPlane(stringBuilder, chromaWidth, chromaHeight, buffer.getDataU(), buffer.getStrideU());
300     stringBuilder.append("V-plane:\n");
301     printPlane(stringBuilder, chromaWidth, chromaHeight, buffer.getDataV(), buffer.getStrideV());
302     return stringBuilder.toString();
303   }
304 
305   /**
306    * Assert that the given I420 buffers are almost identical, allowing for some difference due to
307    * numerical errors. It has limits for both overall PSNR and maximum individual pixel difference.
308    */
309   public static void assertAlmostEqualI420Buffers(
310       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
311     final int diff = maxDiff(bufferA, bufferB);
312     assertThat("Pixel difference too high: " + diff + "."
313             + "\nBuffer A: " + i420BufferToString(bufferA)
314             + "Buffer B: " + i420BufferToString(bufferB),
315         diff, lessThanOrEqualTo(4));
316     final double psnr = calculatePsnr(bufferA, bufferB);
317     assertThat("PSNR too low: " + psnr + "."
318             + "\nBuffer A: " + i420BufferToString(bufferA)
319             + "Buffer B: " + i420BufferToString(bufferB),
320         psnr, greaterThanOrEqualTo(50.0));
321   }
322 
323   /** Returns a flattened list of pixel differences for two ByteBuffer planes. */
324   private static List<Integer> getPixelDiffs(
325       int width, int height, ByteBuffer planeA, int strideA, ByteBuffer planeB, int strideB) {
326     List<Integer> res = new ArrayList<>();
327     for (int y = 0; y < height; ++y) {
328       for (int x = 0; x < width; ++x) {
329         final int valueA = planeA.get(y * strideA + x) & 0xFF;
330         final int valueB = planeB.get(y * strideB + x) & 0xFF;
331         res.add(Math.abs(valueA - valueB));
332       }
333     }
334     return res;
335   }
336 
337   /** Returns a flattened list of pixel differences for two I420 buffers. */
338   private static List<Integer> getPixelDiffs(
339       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
340     assertEquals(bufferA.getWidth(), bufferB.getWidth());
341     assertEquals(bufferA.getHeight(), bufferB.getHeight());
342     final int width = bufferA.getWidth();
343     final int height = bufferA.getHeight();
344     final int chromaWidth = (width + 1) / 2;
345     final int chromaHeight = (height + 1) / 2;
346     final List<Integer> diffs = getPixelDiffs(width, height, bufferA.getDataY(),
347         bufferA.getStrideY(), bufferB.getDataY(), bufferB.getStrideY());
348     diffs.addAll(getPixelDiffs(chromaWidth, chromaHeight, bufferA.getDataU(), bufferA.getStrideU(),
349         bufferB.getDataU(), bufferB.getStrideU()));
350     diffs.addAll(getPixelDiffs(chromaWidth, chromaHeight, bufferA.getDataV(), bufferA.getStrideV(),
351         bufferB.getDataV(), bufferB.getStrideV()));
352     return diffs;
353   }
354 
355   /** Returns the maximum pixel difference from any of the Y/U/V planes in the given buffers. */
356   private static int maxDiff(VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
357     return Collections.max(getPixelDiffs(bufferA, bufferB));
358   }
359 
360   /**
361    * Returns the PSNR given a sum of squared error and the number of measurements that were added.
362    */
363   private static double sseToPsnr(long sse, int count) {
364     if (sse == 0) {
365       return Double.POSITIVE_INFINITY;
366     }
367     final double meanSquaredError = (double) sse / (double) count;
368     final double maxPixelValue = 255.0;
369     return 10.0 * Math.log10(maxPixelValue * maxPixelValue / meanSquaredError);
370   }
371 
372   /** Returns the PSNR of the given I420 buffers. */
373   private static double calculatePsnr(
374       VideoFrame.I420Buffer bufferA, VideoFrame.I420Buffer bufferB) {
375     final List<Integer> pixelDiffs = getPixelDiffs(bufferA, bufferB);
376     long sse = 0;
377     for (int pixelDiff : pixelDiffs) {
378       sse += pixelDiff * pixelDiff;
379     }
380     return sseToPsnr(sse, pixelDiffs.size());
381   }
382 
383   /**
384    * Convert an int array to a byte array and make sure the values are within the range [0, 255].
385    */
386   private static byte[] toByteArray(int[] array) {
387     final byte[] res = new byte[array.length];
388     for (int i = 0; i < array.length; ++i) {
389       final int value = array[i];
390       assertThat(value, greaterThanOrEqualTo(0));
391       assertThat(value, lessThanOrEqualTo(255));
392       res[i] = (byte) value;
393     }
394     return res;
395   }
396 
397   /** Convert a byte array to a direct ByteBuffer. */
398   private static ByteBuffer toByteBuffer(int[] array) {
399     final ByteBuffer buffer = ByteBuffer.allocateDirect(array.length);
400     buffer.put(toByteArray(array));
401     buffer.rewind();
402     return buffer;
403   }
404 
405   /**
406    * Draw an I420 buffer on the currently bound frame buffer, allocating and releasing any
407    * resources necessary.
408    */
409   private static void drawI420Buffer(VideoFrame.I420Buffer i420Buffer) {
410     final GlRectDrawer drawer = new GlRectDrawer();
411     final VideoFrameDrawer videoFrameDrawer = new VideoFrameDrawer();
412     videoFrameDrawer.drawFrame(
413         new VideoFrame(i420Buffer, /* rotation= */ 0, /* timestampNs= */ 0), drawer);
414     videoFrameDrawer.release();
415     drawer.release();
416   }
417 
418   /**
419    * Helper function that tests cropAndScale() with the given cropping and scaling parameters, and
420    * compares the pixel content against a reference I420 buffer.
421    */
422   private void testCropAndScale(
423       int cropX, int cropY, int cropWidth, int cropHeight, int scaleWidth, int scaleHeight) {
424     final VideoFrame.I420Buffer referenceI420Buffer = createTestI420Buffer();
425     final VideoFrame.Buffer bufferToTest = createBufferToTest(referenceI420Buffer);
426 
427     final VideoFrame.Buffer croppedReferenceBuffer = referenceI420Buffer.cropAndScale(
428         cropX, cropY, cropWidth, cropHeight, scaleWidth, scaleHeight);
429     referenceI420Buffer.release();
430     final VideoFrame.I420Buffer croppedReferenceI420Buffer = croppedReferenceBuffer.toI420();
431     croppedReferenceBuffer.release();
432 
433     final VideoFrame.Buffer croppedBufferToTest =
434         bufferToTest.cropAndScale(cropX, cropY, cropWidth, cropHeight, scaleWidth, scaleHeight);
435     bufferToTest.release();
436 
437     final VideoFrame.I420Buffer croppedOutputI420Buffer = croppedBufferToTest.toI420();
438     croppedBufferToTest.release();
439 
440     assertAlmostEqualI420Buffers(croppedReferenceI420Buffer, croppedOutputI420Buffer);
441     croppedReferenceI420Buffer.release();
442     croppedOutputI420Buffer.release();
443   }
444 
445   @Test
446   @SmallTest
447   /** Test calling toI420() and comparing pixel content against I420 reference. */
448   public void testToI420() {
449     final VideoFrame.I420Buffer referenceI420Buffer = createTestI420Buffer();
450     final VideoFrame.Buffer bufferToTest = createBufferToTest(referenceI420Buffer);
451 
452     final VideoFrame.I420Buffer outputI420Buffer = bufferToTest.toI420();
453     bufferToTest.release();
454 
455     assertAlmostEqualI420Buffers(referenceI420Buffer, outputI420Buffer);
456     referenceI420Buffer.release();
457     outputI420Buffer.release();
458   }
459 
460   @Test
461   @SmallTest
462   /** Pure 2x scaling with no cropping. */
463   public void testScale2x() {
464     testCropAndScale(0 /* cropX= */, 0 /* cropY= */, /* cropWidth= */ 16, /* cropHeight= */ 16,
465         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
466   }
467 
468   @Test
469   @SmallTest
470   /** Test cropping only X direction, with no scaling. */
471   public void testCropX() {
472     testCropAndScale(8 /* cropX= */, 0 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 16,
473         /* scaleWidth= */ 8, /* scaleHeight= */ 16);
474   }
475 
476   @Test
477   @SmallTest
478   /** Test cropping only Y direction, with no scaling. */
479   public void testCropY() {
480     testCropAndScale(0 /* cropX= */, 8 /* cropY= */, /* cropWidth= */ 16, /* cropHeight= */ 8,
481         /* scaleWidth= */ 16, /* scaleHeight= */ 8);
482   }
483 
484   @Test
485   @SmallTest
486   /** Test center crop, with no scaling. */
487   public void testCenterCrop() {
488     testCropAndScale(4 /* cropX= */, 4 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 8,
489         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
490   }
491 
492   @Test
493   @SmallTest
494   /** Test non-center crop for right bottom corner, with no scaling. */
495   public void testRightBottomCornerCrop() {
496     testCropAndScale(8 /* cropX= */, 8 /* cropY= */, /* cropWidth= */ 8, /* cropHeight= */ 8,
497         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
498   }
499 
500   @Test
501   @SmallTest
502   /** Test combined cropping and scaling. */
503   public void testCropAndScale() {
504     testCropAndScale(4 /* cropX= */, 4 /* cropY= */, /* cropWidth= */ 12, /* cropHeight= */ 12,
505         /* scaleWidth= */ 8, /* scaleHeight= */ 8);
506   }
507 }
508