1+ package com.nativescript.cameraview
2+
3+ import android.graphics.ImageFormat
4+ import android.media.Image
5+ import androidx.annotation.IntDef
6+ import java.nio.ByteBuffer
7+
8+ /*
9+ This file is converted from part of https://github.com/gordinmitya/yuv2buf.
10+ Follow the link to find demo app, performance benchmarks and unit tests.
11+
12+ Intro to YUV image formats:
13+ YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12.
14+ 420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V.
15+
16+ * I420 format represents an image as Y plane followed by U then followed by V plane
17+ without chroma channels interleaving.
18+ For example:
19+ Y Y Y Y
20+ Y Y Y Y
21+ U U V V
22+
23+ * NV21 format represents an image as Y plane followed by V and U interleaved. First V then U.
24+ For example:
25+ Y Y Y Y
26+ Y Y Y Y
27+ V U V U
28+
29+ * YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V)
30+
31+ Visualization of these 4 formats:
32+ https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg
33+
34+ It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888.
35+ https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
36+
37+ Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN)
38+ the conversion is done into these formats.
39+
40+ More about each format: https://www.fourcc.org/yuv.php
41+ */
42+
43+ @kotlin.annotation.Retention (AnnotationRetention .SOURCE )
44+ @IntDef(ImageFormat .NV21 , ImageFormat .YUV_420_888 )
45+ annotation class YuvType
46+
47+ class YuvByteBuffer (image : Image , dstBuffer : ByteBuffer ? = null ) {
48+ @YuvType
49+ val type: Int
50+ val buffer: ByteBuffer
51+
52+ init {
53+ val wrappedImage = ImageWrapper (image)
54+
55+ type = if (wrappedImage.u.pixelStride == 1 ) {
56+ ImageFormat .YUV_420_888
57+ } else {
58+ ImageFormat .NV21
59+ }
60+ val size = image.width * image.height * 3 / 2
61+ buffer = if (
62+ dstBuffer == null || dstBuffer.capacity() < size ||
63+ dstBuffer.isReadOnly || ! dstBuffer.isDirect
64+ ) {
65+ ByteBuffer .allocateDirect(size) }
66+ else {
67+ dstBuffer
68+ }
69+ buffer.rewind()
70+
71+ removePadding(wrappedImage)
72+ }
73+
74+ // Input buffers are always direct as described in
75+ // https://developer.android.com/reference/android/media/Image.Plane#getBuffer()
76+ private fun removePadding (image : ImageWrapper ) {
77+ val sizeLuma = image.y.width * image.y.height
78+ val sizeChroma = image.u.width * image.u.height
79+ if (image.y.rowStride > image.y.width) {
80+ removePaddingCompact(image.y, buffer, 0 )
81+ } else {
82+ buffer.position(0 )
83+ buffer.put(image.y.buffer)
84+ }
85+ if (type == ImageFormat .YUV_420_888 ) {
86+ if (image.u.rowStride > image.u.width) {
87+ removePaddingCompact(image.u, buffer, sizeLuma)
88+ removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma)
89+ } else {
90+ buffer.position(sizeLuma)
91+ buffer.put(image.u.buffer)
92+ buffer.position(sizeLuma + sizeChroma)
93+ buffer.put(image.v.buffer)
94+ }
95+ } else {
96+ if (image.u.rowStride > image.u.width * 2 ) {
97+ removePaddingNotCompact(image, buffer, sizeLuma)
98+ } else {
99+ buffer.position(sizeLuma)
100+ var uv = image.v.buffer
101+ val properUVSize = image.v.height * image.v.rowStride - 1
102+ if (uv.capacity() > properUVSize) {
103+ uv = clipBuffer(image.v.buffer, 0 , properUVSize)
104+ }
105+ buffer.put(uv)
106+ val lastOne = image.u.buffer[image.u.buffer.capacity() - 1 ]
107+ buffer.put(buffer.capacity() - 1 , lastOne)
108+ }
109+ }
110+ buffer.rewind()
111+ }
112+
113+ private fun removePaddingCompact (
114+ plane : PlaneWrapper ,
115+ dst : ByteBuffer ,
116+ offset : Int
117+ ) {
118+ require(plane.pixelStride == 1 ) {
119+ " use removePaddingCompact with pixelStride == 1"
120+ }
121+
122+ val src = plane.buffer
123+ val rowStride = plane.rowStride
124+ var row: ByteBuffer
125+ dst.position(offset)
126+ for (i in 0 until plane.height) {
127+ row = clipBuffer(src, i * rowStride, plane.width)
128+ dst.put(row)
129+ }
130+ }
131+
132+ private fun removePaddingNotCompact (
133+ image : ImageWrapper ,
134+ dst : ByteBuffer ,
135+ offset : Int
136+ ) {
137+ require(image.u.pixelStride == 2 ) {
138+ " use removePaddingNotCompact pixelStride == 2"
139+ }
140+ val width = image.u.width
141+ val height = image.u.height
142+ val rowStride = image.u.rowStride
143+ var row: ByteBuffer
144+ dst.position(offset)
145+ for (i in 0 until height - 1 ) {
146+ row = clipBuffer(image.v.buffer, i * rowStride, width * 2 )
147+ dst.put(row)
148+ }
149+ row = clipBuffer(image.u.buffer, (height - 1 ) * rowStride - 1 , width * 2 )
150+ dst.put(row)
151+ }
152+
153+ private fun clipBuffer (buffer : ByteBuffer , start : Int , size : Int ): ByteBuffer {
154+ val duplicate = buffer.duplicate()
155+ duplicate.position(start)
156+ duplicate.limit(start + size)
157+ return duplicate.slice()
158+ }
159+
160+ private class ImageWrapper (image : Image ) {
161+ val width= image.width
162+ val height = image.height
163+ val y = PlaneWrapper (width, height, image.planes[0 ])
164+ val u = PlaneWrapper (width / 2 , height / 2 , image.planes[1 ])
165+ val v = PlaneWrapper (width / 2 , height / 2 , image.planes[2 ])
166+
167+ // Check this is a supported image format
168+ // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
169+ init {
170+ require(y.pixelStride == 1 ) {
171+ " Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead."
172+ }
173+ require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) {
174+ " U and V planes must have the same pixel and row strides " +
175+ " but got pixel=${u.pixelStride} row=${u.rowStride} for U " +
176+ " and pixel=${v.pixelStride} and row=${v.rowStride} for V"
177+ }
178+ require(u.pixelStride == 1 || u.pixelStride == 2 ) {
179+ " Supported" + " pixel strides for U and V planes are 1 and 2"
180+ }
181+ }
182+ }
183+
184+ private class PlaneWrapper (width : Int , height : Int , plane : Image .Plane ) {
185+ val width = width
186+ val height = height
187+ val buffer: ByteBuffer = plane.buffer
188+ val rowStride = plane.rowStride
189+ val pixelStride = plane.pixelStride
190+ }
191+ }
0 commit comments