1 /* 2 * Copyright 2014 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 17 /* Original code copied from NDK Native-media sample code */ 18 19 //#define LOG_NDEBUG 0 20 #define TAG "CodecUtilsJNI" 21 #include <log/log.h> 22 23 #include <stdint.h> 24 #include <sys/types.h> 25 #include <jni.h> 26 27 // workaround for using ScopedLocalRef with system runtime 28 // TODO: Remove this after b/74632104 is fixed 29 namespace std 30 { 31 typedef decltype(nullptr) nullptr_t; 32 } 33 34 #include <nativehelper/JNIHelp.h> 35 #include <nativehelper/ScopedLocalRef.h> 36 37 #include <math.h> 38 39 #include "md5_utils.h" 40 41 typedef ssize_t offs_t; 42 43 struct NativeImage { 44 struct crop { 45 int left; 46 int top; 47 int right; 48 int bottom; 49 } crop; 50 struct plane { 51 const uint8_t *buffer; 52 size_t size; 53 ssize_t colInc; 54 ssize_t rowInc; 55 offs_t cropOffs; 56 size_t cropWidth; 57 size_t cropHeight; 58 } plane[3]; 59 int width; 60 int height; 61 int format; 62 long timestamp; 63 size_t numPlanes; 64 }; 65 66 struct ChecksumAlg { 67 virtual void init() = 0; 68 virtual void update(uint8_t c) = 0; 69 virtual uint32_t checksum() = 0; 70 virtual size_t length() = 0; 71 protected: 72 virtual ~ChecksumAlg() {} 73 }; 74 75 struct Adler32 : ChecksumAlg { 76 Adler32() { 77 init(); 78 } 79 void init() { 80 a = 1; 81 len = b = 0; 82 } 83 void update(uint8_t c) { 84 a += c; 85 b += a; 86 ++len; 87 } 88 uint32_t checksum() { 89 return (a % 65521) + ((b % 65521) << 16); 90 } 91 size_t length() { 92 return len; 93 } 94 private: 95 uint32_t a, b; 96 size_t len; 97 }; 98 99 static struct ImageFieldsAndMethods { 100 // android.graphics.ImageFormat 101 int YUV_420_888; 102 // android.media.Image 103 jmethodID methodWidth; 104 jmethodID methodHeight; 105 jmethodID methodFormat; 106 jmethodID methodTimestamp; 107 jmethodID methodPlanes; 108 jmethodID methodCrop; 109 // android.media.Image.Plane 110 jmethodID methodBuffer; 111 jmethodID methodPixelStride; 112 jmethodID methodRowStride; 113 // android.graphics.Rect 114 jfieldID fieldLeft; 115 jfieldID fieldTop; 116 jfieldID fieldRight; 117 jfieldID fieldBottom; 118 } gFields; 119 static bool gFieldsInitialized = false; 120 121 void initializeGlobalFields(JNIEnv *env) { 122 if (gFieldsInitialized) { 123 return; 124 } 125 { // ImageFormat 126 jclass imageFormatClazz = env->FindClass("android/graphics/ImageFormat"); 127 const jfieldID fieldYUV420888 = env->GetStaticFieldID(imageFormatClazz, "YUV_420_888", "I"); 128 gFields.YUV_420_888 = env->GetStaticIntField(imageFormatClazz, fieldYUV420888); 129 env->DeleteLocalRef(imageFormatClazz); 130 imageFormatClazz = NULL; 131 } 132 133 { // Image 134 jclass imageClazz = env->FindClass("android/media/cts/CodecImage"); 135 gFields.methodWidth = env->GetMethodID(imageClazz, "getWidth", "()I"); 136 gFields.methodHeight = env->GetMethodID(imageClazz, "getHeight", "()I"); 137 gFields.methodFormat = env->GetMethodID(imageClazz, "getFormat", "()I"); 138 gFields.methodTimestamp = env->GetMethodID(imageClazz, "getTimestamp", "()J"); 139 gFields.methodPlanes = env->GetMethodID( 140 imageClazz, "getPlanes", "()[Landroid/media/cts/CodecImage$Plane;"); 141 gFields.methodCrop = env->GetMethodID( 142 imageClazz, "getCropRect", "()Landroid/graphics/Rect;"); 143 env->DeleteLocalRef(imageClazz); 144 imageClazz = NULL; 145 } 146 147 { // Image.Plane 148 jclass planeClazz = env->FindClass("android/media/cts/CodecImage$Plane"); 149 gFields.methodBuffer = env->GetMethodID(planeClazz, "getBuffer", "()Ljava/nio/ByteBuffer;"); 150 gFields.methodPixelStride = env->GetMethodID(planeClazz, "getPixelStride", "()I"); 151 gFields.methodRowStride = env->GetMethodID(planeClazz, "getRowStride", "()I"); 152 env->DeleteLocalRef(planeClazz); 153 planeClazz = NULL; 154 } 155 156 { // Rect 157 jclass rectClazz = env->FindClass("android/graphics/Rect"); 158 gFields.fieldLeft = env->GetFieldID(rectClazz, "left", "I"); 159 gFields.fieldTop = env->GetFieldID(rectClazz, "top", "I"); 160 gFields.fieldRight = env->GetFieldID(rectClazz, "right", "I"); 161 gFields.fieldBottom = env->GetFieldID(rectClazz, "bottom", "I"); 162 env->DeleteLocalRef(rectClazz); 163 rectClazz = NULL; 164 } 165 gFieldsInitialized = true; 166 } 167 168 NativeImage *getNativeImage(JNIEnv *env, jobject image, jobject area = NULL) { 169 if (image == NULL) { 170 jniThrowNullPointerException(env, "image is null"); 171 return NULL; 172 } 173 174 initializeGlobalFields(env); 175 176 NativeImage *img = new NativeImage; 177 img->format = env->CallIntMethod(image, gFields.methodFormat); 178 img->width = env->CallIntMethod(image, gFields.methodWidth); 179 img->height = env->CallIntMethod(image, gFields.methodHeight); 180 img->timestamp = env->CallLongMethod(image, gFields.methodTimestamp); 181 182 jobject cropRect = NULL; 183 if (area == NULL) { 184 cropRect = env->CallObjectMethod(image, gFields.methodCrop); 185 area = cropRect; 186 } 187 188 img->crop.left = env->GetIntField(area, gFields.fieldLeft); 189 img->crop.top = env->GetIntField(area, gFields.fieldTop); 190 img->crop.right = env->GetIntField(area, gFields.fieldRight); 191 img->crop.bottom = env->GetIntField(area, gFields.fieldBottom); 192 if (img->crop.right == 0 && img->crop.bottom == 0) { 193 img->crop.right = img->width; 194 img->crop.bottom = img->height; 195 } 196 197 if (cropRect != NULL) { 198 env->DeleteLocalRef(cropRect); 199 cropRect = NULL; 200 } 201 202 if (img->format != gFields.YUV_420_888) { 203 jniThrowException( 204 env, "java/lang/UnsupportedOperationException", 205 "only support YUV_420_888 images"); 206 delete img; 207 img = NULL; 208 return NULL; 209 } 210 img->numPlanes = 3; 211 212 ScopedLocalRef<jobjectArray> planesArray( 213 env, (jobjectArray)env->CallObjectMethod(image, gFields.methodPlanes)); 214 int xDecim = 0; 215 int yDecim = 0; 216 for (size_t ix = 0; ix < img->numPlanes; ++ix) { 217 ScopedLocalRef<jobject> plane( 218 env, env->GetObjectArrayElement(planesArray.get(), (jsize)ix)); 219 img->plane[ix].colInc = env->CallIntMethod(plane.get(), gFields.methodPixelStride); 220 img->plane[ix].rowInc = env->CallIntMethod(plane.get(), gFields.methodRowStride); 221 ScopedLocalRef<jobject> buffer( 222 env, env->CallObjectMethod(plane.get(), gFields.methodBuffer)); 223 224 img->plane[ix].buffer = (const uint8_t *)env->GetDirectBufferAddress(buffer.get()); 225 img->plane[ix].size = env->GetDirectBufferCapacity(buffer.get()); 226 227 img->plane[ix].cropOffs = 228 (img->crop.left >> xDecim) * img->plane[ix].colInc 229 + (img->crop.top >> yDecim) * img->plane[ix].rowInc; 230 img->plane[ix].cropHeight = 231 ((img->crop.bottom + (1 << yDecim) - 1) >> yDecim) - (img->crop.top >> yDecim); 232 img->plane[ix].cropWidth = 233 ((img->crop.right + (1 << xDecim) - 1) >> xDecim) - (img->crop.left >> xDecim); 234 235 // sanity check on increments 236 ssize_t widthOffs = 237 (((img->width + (1 << xDecim) - 1) >> xDecim) - 1) * img->plane[ix].colInc; 238 ssize_t heightOffs = 239 (((img->height + (1 << yDecim) - 1) >> yDecim) - 1) * img->plane[ix].rowInc; 240 if (widthOffs < 0 || heightOffs < 0 241 || widthOffs + heightOffs >= (ssize_t)img->plane[ix].size) { 242 jniThrowException( 243 env, "java/lang/IndexOutOfBoundsException", "plane exceeds bytearray"); 244 delete img; 245 img = NULL; 246 return NULL; 247 } 248 xDecim = yDecim = 1; 249 } 250 return img; 251 } 252 253 extern "C" jint Java_android_media_cts_CodecUtils_getImageChecksumAlder32(JNIEnv *env, 254 jclass /*clazz*/, jobject image) 255 { 256 NativeImage *img = getNativeImage(env, image); 257 if (img == NULL) { 258 return 0; 259 } 260 261 Adler32 adler; 262 for (size_t ix = 0; ix < img->numPlanes; ++ix) { 263 const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs; 264 for (size_t y = img->plane[ix].cropHeight; y > 0; --y) { 265 const uint8_t *col = row; 266 ssize_t colInc = img->plane[ix].colInc; 267 for (size_t x = img->plane[ix].cropWidth; x > 0; --x) { 268 adler.update(*col); 269 col += colInc; 270 } 271 row += img->plane[ix].rowInc; 272 } 273 } 274 ALOGV("adler %zu/%u", adler.length(), adler.checksum()); 275 return adler.checksum(); 276 } 277 278 extern "C" jstring Java_android_media_cts_CodecUtils_getImageChecksumMD5(JNIEnv *env, 279 jclass /*clazz*/, jobject image) 280 { 281 NativeImage *img = getNativeImage(env, image); 282 if (img == NULL) { 283 return 0; 284 } 285 286 MD5Context md5; 287 char res[33]; 288 MD5Init(&md5); 289 290 for (size_t ix = 0; ix < img->numPlanes; ++ix) { 291 const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs; 292 for (size_t y = img->plane[ix].cropHeight; y > 0; --y) { 293 const uint8_t *col = row; 294 ssize_t colInc = img->plane[ix].colInc; 295 for (size_t x = img->plane[ix].cropWidth; x > 0; --x) { 296 MD5Update(&md5, col, 1); 297 col += colInc; 298 } 299 row += img->plane[ix].rowInc; 300 } 301 } 302 303 static const char hex[16] = { 304 '0', '1', '2', '3', '4', '5', '6', '7', 305 '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 306 }; 307 uint8_t tmp[16]; 308 309 MD5Final(tmp, &md5); 310 for (int i = 0; i < 16; i++) { 311 res[i * 2 + 0] = hex[tmp[i] >> 4]; 312 res[i * 2 + 1] = hex[tmp[i] & 0xf]; 313 } 314 res[32] = 0; 315 316 return env->NewStringUTF(res); 317 } 318 319 /* tiled copy that loops around source image boundary */ 320 extern "C" void Java_android_media_cts_CodecUtils_copyFlexYUVImage(JNIEnv *env, 321 jclass /*clazz*/, jobject target, jobject source) 322 { 323 NativeImage *tgt = getNativeImage(env, target); 324 NativeImage *src = getNativeImage(env, source); 325 if (tgt != NULL && src != NULL) { 326 ALOGV("copyFlexYUVImage %dx%d (%d,%d..%d,%d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd <= " 327 "%dx%d (%d, %d..%d, %d) (%zux%zu) %+zd%+zd %+zd%+zd %+zd%+zd", 328 tgt->width, tgt->height, 329 tgt->crop.left, tgt->crop.top, tgt->crop.right, tgt->crop.bottom, 330 tgt->plane[0].cropWidth, tgt->plane[0].cropHeight, 331 tgt->plane[0].rowInc, tgt->plane[0].colInc, 332 tgt->plane[1].rowInc, tgt->plane[1].colInc, 333 tgt->plane[2].rowInc, tgt->plane[2].colInc, 334 src->width, src->height, 335 src->crop.left, src->crop.top, src->crop.right, src->crop.bottom, 336 src->plane[0].cropWidth, src->plane[0].cropHeight, 337 src->plane[0].rowInc, src->plane[0].colInc, 338 src->plane[1].rowInc, src->plane[1].colInc, 339 src->plane[2].rowInc, src->plane[2].colInc); 340 for (size_t ix = 0; ix < tgt->numPlanes; ++ix) { 341 uint8_t *row = const_cast<uint8_t *>(tgt->plane[ix].buffer) + tgt->plane[ix].cropOffs; 342 for (size_t y = 0; y < tgt->plane[ix].cropHeight; ++y) { 343 uint8_t *col = row; 344 ssize_t colInc = tgt->plane[ix].colInc; 345 const uint8_t *srcRow = (src->plane[ix].buffer + src->plane[ix].cropOffs 346 + src->plane[ix].rowInc * (y % src->plane[ix].cropHeight)); 347 for (size_t x = 0; x < tgt->plane[ix].cropWidth; ++x) { 348 *col = srcRow[src->plane[ix].colInc * (x % src->plane[ix].cropWidth)]; 349 col += colInc; 350 } 351 row += tgt->plane[ix].rowInc; 352 } 353 } 354 } 355 } 356 357 extern "C" void Java_android_media_cts_CodecUtils_fillImageRectWithYUV(JNIEnv *env, 358 jclass /*clazz*/, jobject image, jobject area, jint y, jint u, jint v) 359 { 360 NativeImage *img = getNativeImage(env, image, area); 361 if (img == NULL) { 362 return; 363 } 364 365 for (size_t ix = 0; ix < img->numPlanes; ++ix) { 366 const uint8_t *row = img->plane[ix].buffer + img->plane[ix].cropOffs; 367 uint8_t val = ix == 0 ? y : ix == 1 ? u : v; 368 for (size_t y = img->plane[ix].cropHeight; y > 0; --y) { 369 uint8_t *col = (uint8_t *)row; 370 ssize_t colInc = img->plane[ix].colInc; 371 for (size_t x = img->plane[ix].cropWidth; x > 0; --x) { 372 *col = val; 373 col += colInc; 374 } 375 row += img->plane[ix].rowInc; 376 } 377 } 378 } 379 380 void getRawStats(NativeImage *img, jlong rawStats[10]) 381 { 382 // this works best if crop area is even 383 384 uint64_t sum_x[3] = { 0, 0, 0 }; // Y, U, V 385 uint64_t sum_xx[3] = { 0, 0, 0 }; // YY, UU, VV 386 uint64_t sum_xy[3] = { 0, 0, 0 }; // YU, YV, UV 387 388 const uint8_t *yrow = img->plane[0].buffer + img->plane[0].cropOffs; 389 const uint8_t *urow = img->plane[1].buffer + img->plane[1].cropOffs; 390 const uint8_t *vrow = img->plane[2].buffer + img->plane[2].cropOffs; 391 392 ssize_t ycolInc = img->plane[0].colInc; 393 ssize_t ucolInc = img->plane[1].colInc; 394 ssize_t vcolInc = img->plane[2].colInc; 395 396 ssize_t yrowInc = img->plane[0].rowInc; 397 ssize_t urowInc = img->plane[1].rowInc; 398 ssize_t vrowInc = img->plane[2].rowInc; 399 400 size_t rightOdd = img->crop.right & 1; 401 size_t bottomOdd = img->crop.bottom & 1; 402 403 for (size_t y = img->plane[0].cropHeight; y; --y) { 404 uint8_t *ycol = (uint8_t *)yrow; 405 uint8_t *ucol = (uint8_t *)urow; 406 uint8_t *vcol = (uint8_t *)vrow; 407 408 for (size_t x = img->plane[0].cropWidth; x; --x) { 409 uint64_t Y = *ycol; 410 uint64_t U = *ucol; 411 uint64_t V = *vcol; 412 413 sum_x[0] += Y; 414 sum_x[1] += U; 415 sum_x[2] += V; 416 sum_xx[0] += Y * Y; 417 sum_xx[1] += U * U; 418 sum_xx[2] += V * V; 419 sum_xy[0] += Y * U; 420 sum_xy[1] += Y * V; 421 sum_xy[2] += U * V; 422 423 ycol += ycolInc; 424 if (rightOdd ^ (x & 1)) { 425 ucol += ucolInc; 426 vcol += vcolInc; 427 } 428 } 429 430 yrow += yrowInc; 431 if (bottomOdd ^ (y & 1)) { 432 urow += urowInc; 433 vrow += vrowInc; 434 } 435 } 436 437 rawStats[0] = img->plane[0].cropWidth * (uint64_t)img->plane[0].cropHeight; 438 for (size_t i = 0; i < 3; i++) { 439 rawStats[i + 1] = sum_x[i]; 440 rawStats[i + 4] = sum_xx[i]; 441 rawStats[i + 7] = sum_xy[i]; 442 } 443 } 444 445 bool Raw2YUVStats(jlong rawStats[10], jfloat stats[9]) { 446 int64_t sum_x[3], sum_xx[3]; // Y, U, V 447 int64_t sum_xy[3]; // YU, YV, UV 448 449 int64_t num = rawStats[0]; // #Y,U,V 450 for (size_t i = 0; i < 3; i++) { 451 sum_x[i] = rawStats[i + 1]; 452 sum_xx[i] = rawStats[i + 4]; 453 sum_xy[i] = rawStats[i + 7]; 454 } 455 456 if (num > 0) { 457 stats[0] = sum_x[0] / (float)num; // y average 458 stats[1] = sum_x[1] / (float)num; // u average 459 stats[2] = sum_x[2] / (float)num; // v average 460 461 // 60 bits for 4Mpixel image 462 // adding 1 to avoid degenerate case when deviation is 0 463 stats[3] = sqrtf((sum_xx[0] + 1) * num - sum_x[0] * sum_x[0]) / num; // y stdev 464 stats[4] = sqrtf((sum_xx[1] + 1) * num - sum_x[1] * sum_x[1]) / num; // u stdev 465 stats[5] = sqrtf((sum_xx[2] + 1) * num - sum_x[2] * sum_x[2]) / num; // v stdev 466 467 // yu covar 468 stats[6] = (float)(sum_xy[0] + 1 - sum_x[0] * sum_x[1] / num) / num / stats[3] / stats[4]; 469 // yv covar 470 stats[7] = (float)(sum_xy[1] + 1 - sum_x[0] * sum_x[2] / num) / num / stats[3] / stats[5]; 471 // uv covar 472 stats[8] = (float)(sum_xy[2] + 1 - sum_x[1] * sum_x[2] / num) / num / stats[4] / stats[5]; 473 return true; 474 } else { 475 return false; 476 } 477 } 478 479 extern "C" jobject Java_android_media_cts_CodecUtils_getRawStats(JNIEnv *env, 480 jclass /*clazz*/, jobject image, jobject area) 481 { 482 NativeImage *img = getNativeImage(env, image, area); 483 if (img == NULL) { 484 return NULL; 485 } 486 487 jlong rawStats[10]; 488 getRawStats(img, rawStats); 489 jlongArray jstats = env->NewLongArray(10); 490 if (jstats != NULL) { 491 env->SetLongArrayRegion(jstats, 0, 10, rawStats); 492 } 493 return jstats; 494 } 495 496 extern "C" jobject Java_android_media_cts_CodecUtils_getYUVStats(JNIEnv *env, 497 jclass /*clazz*/, jobject image, jobject area) 498 { 499 NativeImage *img = getNativeImage(env, image, area); 500 if (img == NULL) { 501 return NULL; 502 } 503 504 jlong rawStats[10]; 505 getRawStats(img, rawStats); 506 jfloat stats[9]; 507 jfloatArray jstats = NULL; 508 if (Raw2YUVStats(rawStats, stats)) { 509 jstats = env->NewFloatArray(9); 510 if (jstats != NULL) { 511 env->SetFloatArrayRegion(jstats, 0, 9, stats); 512 } 513 } else { 514 jniThrowRuntimeException(env, "empty area"); 515 } 516 517 return jstats; 518 } 519 520 extern "C" jobject Java_android_media_cts_CodecUtils_Raw2YUVStats(JNIEnv *env, 521 jclass /*clazz*/, jobject jrawStats) 522 { 523 jfloatArray jstats = NULL; 524 jlong rawStats[10]; 525 env->GetLongArrayRegion((jlongArray)jrawStats, 0, 10, rawStats); 526 if (!env->ExceptionCheck()) { 527 jfloat stats[9]; 528 if (Raw2YUVStats(rawStats, stats)) { 529 jstats = env->NewFloatArray(9); 530 if (jstats != NULL) { 531 env->SetFloatArrayRegion(jstats, 0, 9, stats); 532 } 533 } else { 534 jniThrowRuntimeException(env, "no raw statistics"); 535 } 536 } 537 return jstats; 538 } 539