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