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:
~ChecksumAlgChecksumAlg72 virtual ~ChecksumAlg() {}
73 };
74
75 struct Adler32 : ChecksumAlg {
Adler32Adler3276 Adler32() {
77 init();
78 }
initAdler3279 void init() {
80 a = 1;
81 len = b = 0;
82 }
updateAdler3283 void update(uint8_t c) {
84 a += c;
85 b += a;
86 ++len;
87 }
checksumAdler3288 uint32_t checksum() {
89 return (a % 65521) + ((b % 65521) << 16);
90 }
lengthAdler3291 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
initializeGlobalFields(JNIEnv * env)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
getNativeImage(JNIEnv * env,jobject image,jobject area=NULL)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
Java_android_media_cts_CodecUtils_getImageChecksumAlder32(JNIEnv * env,jclass,jobject image)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
Java_android_media_cts_CodecUtils_getImageChecksumMD5(JNIEnv * env,jclass,jobject image)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 */
Java_android_media_cts_CodecUtils_copyFlexYUVImage(JNIEnv * env,jclass,jobject target,jobject source)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
Java_android_media_cts_CodecUtils_fillImageRectWithYUV(JNIEnv * env,jclass,jobject image,jobject area,jint y,jint u,jint v)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
getRawStats(NativeImage * img,jlong rawStats[10])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
Raw2YUVStats(jlong rawStats[10],jfloat stats[9])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
Java_android_media_cts_CodecUtils_getRawStats(JNIEnv * env,jclass,jobject image,jobject area)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
Java_android_media_cts_CodecUtils_getYUVStats(JNIEnv * env,jclass,jobject image,jobject area)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
Java_android_media_cts_CodecUtils_Raw2YUVStats(JNIEnv * env,jclass,jobject jrawStats)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