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