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