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