1 /*
2  * Copyright (C) 2010 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 android.graphics.cts;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 import static org.junit.Assert.fail;
22 
23 import android.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.ImageFormat;
29 import android.graphics.Rect;
30 import android.graphics.YuvImage;
31 import android.support.test.InstrumentationRegistry;
32 import android.support.test.filters.SmallTest;
33 import android.support.test.runner.AndroidJUnit4;
34 import android.util.Log;
35 
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 
39 import java.io.ByteArrayOutputStream;
40 import java.util.Arrays;
41 
42 @SmallTest
43 @RunWith(AndroidJUnit4.class)
44 public class YuvImageTest {
45     // Coefficients are taken from jcolor.c in libjpeg.
46     private static final int CSHIFT = 16;
47     private static final int CYR = 19595;
48     private static final int CYG = 38470;
49     private static final int CYB = 7471;
50     private static final int CUR = -11059;
51     private static final int CUG = -21709;
52     private static final int CUB = 32768;
53     private static final int CVR = 32768;
54     private static final int CVG = -27439;
55     private static final int CVB = -5329;
56 
57     private static final String TAG = "YuvImageTest";
58 
59     private static final int[] FORMATS = { ImageFormat.NV21, ImageFormat.YUY2 };
60 
61     private static final int WIDTH = 256;
62     private static final int HEIGHT = 128;
63 
64     private static final int[] RECT_WIDTHS = { 128, 124, 123 };
65     private static final int[] RECT_HEIGHTS = { 64, 60, 59 };
66 
67     // Various rectangles:
68     // mRects[0] : a normal one.
69     // mRects[1] : same size to that of mRects[1], but its left-top point is shifted
70     // mRects[2] : sides are not multiples of 16
71     // mRects[3] : the left-top point is at an odd position
72     private static final Rect[] RECTS = { new Rect(0, 0, 0 + RECT_WIDTHS[0],  0 + RECT_HEIGHTS[0]),
73             new Rect(10, 10, 10 + RECT_WIDTHS[0], 10 + RECT_HEIGHTS[0]),
74             new Rect(0, 0, 0 + RECT_WIDTHS[1], 0 + RECT_HEIGHTS[1]),
75             new Rect(11, 11, 11 + RECT_WIDTHS[1], 11 + RECT_HEIGHTS[1]) };
76 
77     // Two rectangles of same size but at different positions
78     private static final Rect[] RECTS_SHIFTED = { RECTS[0], RECTS[1] };
79 
80     // A rect whose side lengths are odd.
81     private static final Rect RECT_ODD_SIDES = new Rect(10, 10, 10 + RECT_WIDTHS[2],
82             10 + RECT_HEIGHTS[2]);
83 
84     private static final int[] PADDINGS = { 0, 32 };
85 
86     // There are three color components and
87     // each should be within a square difference of 15 * 15.
88     private static final int MSE_MARGIN = 3 * (15 * 15);
89 
90     private Bitmap[] mTestBitmaps = new Bitmap[1];
91 
92     @Test
testYuvImage()93     public void testYuvImage() {
94         int width = 100;
95         int height = 100;
96         byte[] yuv = new byte[width * height * 2];
97         YuvImage image;
98 
99         // normal case: test that the required formats are all supported
100         for (int i = 0; i < FORMATS.length; ++i) {
101             try {
102                 new YuvImage(yuv, FORMATS[i], width, height, null);
103             } catch (Exception e) {
104                 Log.e(TAG, "unexpected exception", e);
105                 fail("unexpected exception");
106             }
107         }
108 
109         // normal case: test that default strides are returned correctly
110         for (int i = 0; i < FORMATS.length; ++i) {
111             int[] expected = null;
112             int[] actual = null;
113             if (FORMATS[i] == ImageFormat.NV21) {
114                 expected = new int[]{width, width};
115             } else if (FORMATS[i] == ImageFormat.YUY2) {
116                 expected = new int[]{width * 2};
117             }
118 
119             try {
120                 image = new YuvImage(yuv, FORMATS[i], width, height, null);
121                 actual = image.getStrides();
122                 assertTrue("default strides not calculated correctly",
123                         Arrays.equals(expected, actual));
124             } catch (Exception e) {
125                 Log.e(TAG, "unexpected exception", e);
126                 fail("unexpected exception");
127             }
128         }
129     }
130 
131     @Test(expected=IllegalArgumentException.class)
testYuvImageNegativeWidth()132     public void testYuvImageNegativeWidth(){
133         new YuvImage(new byte[100 * 100 * 2], FORMATS[0], -1, 100, null);
134     }
135 
136     @Test(expected=IllegalArgumentException.class)
testYuvImageNegativeHeight()137     public void testYuvImageNegativeHeight(){
138         new YuvImage(new byte[100 * 100 * 2], FORMATS[0], 100, -1, null);
139     }
140 
141     @Test(expected=IllegalArgumentException.class)
testYuvImageNullArray()142     public void testYuvImageNullArray(){
143         new YuvImage(null, FORMATS[0], 100, 100, null);
144    }
145 
146     @Test
testCompressYuvToJpeg()147     public void testCompressYuvToJpeg() {
148         generateTestBitmaps(WIDTH, HEIGHT);
149 
150         // test if handling compression parameters correctly
151         verifyParameters();
152 
153         // test various cases by varing
154         // <ImageFormat, Bitmap, HasPaddings, Rect>
155         for (int i = 0; i < FORMATS.length; ++i) {
156             for (int j = 0; j < mTestBitmaps.length; ++j) {
157                 for (int k = 0; k < PADDINGS.length; ++k) {
158                     YuvImage image = generateYuvImage(FORMATS[i],
159                         mTestBitmaps[j], PADDINGS[k]);
160                     for (int l = 0; l < RECTS.length; ++l) {
161 
162                         // test compressing the same rect in
163                         // mTestBitmaps[j] and image.
164                         compressRects(mTestBitmaps[j], image,
165                                 RECTS[l], RECTS[l]);
166                     }
167 
168                     // test compressing different rects in
169                     // mTestBitmap[j] and image.
170                     compressRects(mTestBitmaps[j], image, RECTS_SHIFTED[0],
171                             RECTS_SHIFTED[1]);
172 
173                     // test compressing a rect whose side lengths are odd.
174                     compressOddRect(mTestBitmaps[j], image, RECT_ODD_SIDES);
175                 }
176             }
177         }
178 
179     }
180 
181     @Test
testGetHeight()182     public void testGetHeight() {
183         generateTestBitmaps(WIDTH, HEIGHT);
184         YuvImage image = generateYuvImage(ImageFormat.YUY2, mTestBitmaps[0], 0);
185         assertEquals(mTestBitmaps[0].getHeight(), image.getHeight());
186         assertEquals(mTestBitmaps[0].getWidth(), image.getWidth());
187     }
188 
189     @Test
testGetYuvData()190     public void testGetYuvData() {
191         generateTestBitmaps(WIDTH, HEIGHT);
192         int width = mTestBitmaps[0].getWidth();
193         int height = mTestBitmaps[0].getHeight();
194         int stride = width;
195         int[] argb = new int[stride * height];
196         mTestBitmaps[0].getPixels(argb, 0, stride, 0, 0, width, height);
197         byte[] yuv = convertArgbsToYuvs(argb, stride, height, ImageFormat.NV21);
198         int[] strides = new int[] {
199                 stride, stride
200         };
201         YuvImage image = new YuvImage(yuv, ImageFormat.NV21, width, height, strides);
202         assertEquals(yuv, image.getYuvData());
203     }
204 
205     @Test
testGetYuvFormat()206     public void testGetYuvFormat() {
207         generateTestBitmaps(WIDTH, HEIGHT);
208         YuvImage image = generateYuvImage(ImageFormat.YUY2, mTestBitmaps[0], 0);
209         assertEquals(ImageFormat.YUY2, image.getYuvFormat());
210     }
211 
generateTestBitmaps(int width, int height)212     private void generateTestBitmaps(int width, int height) {
213         Bitmap dst = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
214         Canvas c = new Canvas(dst);
215 
216         // mTestBitmap[0] = scaled testimage.jpg
217         Resources res = InstrumentationRegistry.getTargetContext().getResources();
218         Bitmap src = BitmapFactory.decodeResource(res, R.drawable.testimage);
219         c.drawBitmap(src, null, new Rect(0, 0, WIDTH, HEIGHT), null);
220         mTestBitmaps[0] = dst;
221     }
222 
223     // Generate YuvImage based on the content in bitmap. If paddings > 0, the
224     // strides of YuvImage are calculated by adding paddings to bitmap.getWidth().
generateYuvImage(int format, Bitmap bitmap, int paddings)225     private YuvImage generateYuvImage(int format, Bitmap bitmap, int paddings) {
226         int width = bitmap.getWidth();
227         int height = bitmap.getHeight();
228 
229         int stride = width + paddings;
230 
231         YuvImage image = null;
232         int[] argb = new int [stride * height];
233         bitmap.getPixels(argb, 0, stride, 0, 0, width, height);
234         byte[] yuv = convertArgbsToYuvs(argb, stride, height, format);
235 
236         int[] strides = null;
237         if (format == ImageFormat.NV21) {
238             strides = new int[] {stride, stride};
239         } else if (format == ImageFormat.YUY2) {
240            strides = new int[] {stride * 2};
241         }
242         image = new YuvImage(yuv, format, width, height, strides);
243 
244         return image;
245     }
246 
247     // Compress rect1 in testBitmap and rect2 in image.
248     // Then, compare the two resutls to check their MSE.
compressRects(Bitmap testBitmap, YuvImage image, Rect rect1, Rect rect2)249     private void compressRects(Bitmap testBitmap, YuvImage image,
250             Rect rect1, Rect rect2) {
251         Bitmap expected = null;
252         Bitmap actual = null;
253         boolean sameRect = rect1.equals(rect2) ? true : false;
254 
255 		Rect actualRect = new Rect(rect2);
256         actual = compressDecompress(image, actualRect);
257 
258         Rect expectedRect = sameRect ? actualRect : rect1;
259         expected = Bitmap.createBitmap(testBitmap, expectedRect.left, expectedRect.top,
260                 expectedRect.width(), expectedRect.height());
261         compareBitmaps(expected, actual, MSE_MARGIN, sameRect);
262     }
263 
264     // Compress rect in image.
265     // If side lengths of rect are odd, the rect might be modified by image,
266     // We use the modified one to get pixels from testBitmap.
compressOddRect(Bitmap testBitmap, YuvImage image, Rect rect)267     private void compressOddRect(Bitmap testBitmap, YuvImage image,
268             Rect rect) {
269         Bitmap expected = null;
270         Bitmap actual = null;
271         actual = compressDecompress(image, rect);
272 
273         Rect newRect = rect;
274         expected = Bitmap.createBitmap(testBitmap, newRect.left, newRect.top,
275               newRect.width(), newRect.height());
276 
277         compareBitmaps(expected, actual, MSE_MARGIN, true);
278     }
279 
280     // Compress rect in image to a jpeg and then decode the jpeg to a bitmap.
compressDecompress(YuvImage image, Rect rect)281     private Bitmap compressDecompress(YuvImage image, Rect rect) {
282         Bitmap result = null;
283         ByteArrayOutputStream stream = new ByteArrayOutputStream();
284         try {
285             boolean rt = image.compressToJpeg(rect, 90, stream);
286             assertTrue("fail in compression", rt);
287             byte[] jpegData = stream.toByteArray();
288             result = BitmapFactory.decodeByteArray(jpegData, 0,
289                     jpegData.length);
290         } catch(Exception e){
291             Log.e(TAG, "unexpected exception", e);
292             fail("unexpected exception");
293         }
294         return result;
295     }
296 
convertArgbsToYuvs(int[] argb, int width, int height, int format)297     private byte[] convertArgbsToYuvs(int[] argb, int width, int height,
298             int format) {
299         byte[] yuv = new byte[width * height *
300                 ImageFormat.getBitsPerPixel(format)];
301         if (format == ImageFormat.NV21) {
302             int vuStart = width * height;
303             byte[] yuvColor = new byte[3];
304             for (int row = 0; row < height; ++row) {
305                 for (int col = 0; col < width; ++col) {
306                     int idx = row * width + col;
307                     argb2yuv(argb[idx], yuvColor);
308                     yuv[idx] = yuvColor[0];
309                     if ((row & 1) == 0 && (col & 1) == 0) {
310                         int offset = row / 2 * width + col / 2 * 2;
311                         yuv[vuStart + offset] = yuvColor[2];
312                         yuv[vuStart + offset + 1] = yuvColor[1];
313                     }
314                 }
315             }
316         } else if (format == ImageFormat.YUY2) {
317             byte[] yuvColor0 = new byte[3];
318             byte[] yuvColor1 = new byte[3];
319             for (int row = 0; row < height; ++row) {
320                 for (int col = 0; col < width; col += 2) {
321                     int idx = row * width + col;
322                     argb2yuv(argb[idx], yuvColor0);
323                     argb2yuv(argb[idx + 1], yuvColor1);
324                     int offset = idx / 2 * 4;
325                     yuv[offset] = yuvColor0[0];
326                     yuv[offset + 1] = yuvColor0[1];
327                     yuv[offset + 2] = yuvColor1[0];
328                     yuv[offset + 3] = yuvColor0[2];
329                 }
330             }
331         }
332 
333         return yuv;
334     }
335 
336     // Compare expected to actual to see if their diff is less then mseMargin.
337     // lessThanMargin is to indicate whether we expect the diff to be
338     // "less than" or "no less than".
compareBitmaps(Bitmap expected, Bitmap actual, int mseMargin, boolean lessThanMargin)339     private void compareBitmaps(Bitmap expected, Bitmap actual,
340             int mseMargin, boolean lessThanMargin) {
341         assertEquals("mismatching widths", expected.getWidth(),
342                 actual.getWidth());
343         assertEquals("mismatching heights", expected.getHeight(),
344                 actual.getHeight());
345 
346         double mse = 0;
347         int width = expected.getWidth();
348         int height = expected.getHeight();
349         int[] expColors = new int [width * height];
350         expected.getPixels(expColors, 0, width, 0, 0, width, height);
351 
352         int[] actualColors = new int [width * height];
353         actual.getPixels(actualColors, 0, width, 0, 0, width, height);
354 
355         for (int row = 0; row < height; ++row) {
356             for (int col = 0; col < width; ++col) {
357                 int idx = row * width + col;
358                 mse += distance(expColors[idx], actualColors[idx]);
359             }
360         }
361         mse /= width * height;
362 
363         Log.i(TAG, "MSE: " + mse);
364         if (lessThanMargin) {
365             assertTrue("MSE too large for normal case: " + mse,
366                     mse <= mseMargin);
367         } else {
368             assertFalse("MSE too small for abnormal case: " + mse,
369                     mse <= mseMargin);
370         }
371     }
372 
distance(int exp, int actual)373     private double distance(int exp, int actual) {
374         int r = Color.red(actual) - Color.red(exp);
375         int g = Color.green(actual) - Color.green(exp);
376         int b = Color.blue(actual) - Color.blue(exp);
377         return r * r + g * g + b * b;
378     }
379 
argb2yuv(int argb, byte[] yuv)380     private void argb2yuv(int argb, byte[] yuv) {
381         int r = Color.red(argb);
382         int g = Color.green(argb);
383         int b = Color.blue(argb);
384         yuv[0] = (byte) ((CYR * r + CYG * g + CYB * b) >> CSHIFT);
385         yuv[1] = (byte) (((CUR * r + CUG * g + CUB * b) >> CSHIFT) + 128);
386         yuv[2] = (byte) (((CVR * r + CVG * g + CVB * b) >> CSHIFT) + 128);
387     }
388 
verifyParameters()389     private void verifyParameters() {
390         int format = ImageFormat.NV21;
391         int[] argb = new int[WIDTH * HEIGHT];
392         mTestBitmaps[0].getPixels(argb, 0, WIDTH, 0, 0, WIDTH, HEIGHT);
393         byte[] yuv = convertArgbsToYuvs(argb, WIDTH, HEIGHT, format);
394 
395         YuvImage image = new YuvImage(yuv, format, WIDTH, HEIGHT, null);
396         ByteArrayOutputStream stream = new ByteArrayOutputStream();
397 
398         // abnormal case: quality > 100
399         try{
400             Rect rect = new Rect(0, 0, WIDTH, HEIGHT);
401             image.compressToJpeg(rect, 101, stream);
402             fail("not catching illegal compression quality");
403         } catch(IllegalArgumentException e){
404             // expected
405         }
406 
407         // abnormal case: quality < 0
408         try{
409             Rect rect = new Rect(0, 0, WIDTH, HEIGHT);
410             image.compressToJpeg(rect, -1, stream);
411             fail("not catching illegal compression quality");
412         } catch(IllegalArgumentException e){
413             // expected
414         }
415 
416         // abnormal case: stream is null
417         try {
418             Rect rect = new Rect(0, 0, WIDTH, HEIGHT);
419             image.compressToJpeg(rect, 80, null);
420             fail("not catching null stream");
421         } catch(IllegalArgumentException e){
422             // expected
423         }
424 
425         // abnormal case: rectangle not within the whole image
426         try {
427             Rect rect = new Rect(10, 0, WIDTH, HEIGHT + 5);
428             image.compressToJpeg(rect, 80, stream);
429             fail("not catching illegal rectangular region");
430         } catch(IllegalArgumentException e){
431             // expected
432         }
433     }
434 }
435