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