11package com.nativescript.cameraview
22
3+ import android.annotation.SuppressLint
34import android.annotation.TargetApi
45import android.content.ContentResolver
56import android.graphics.*
@@ -8,9 +9,11 @@ import android.net.Uri
89import android.os.Build
910import android.provider.MediaStore
1011import android.util.Log
12+ import androidx.annotation.OptIn
1113import androidx.annotation.RequiresApi
1214import androidx.camera.core.ExperimentalGetImage
1315import androidx.camera.core.ImageProxy
16+ import androidx.camera.core.internal.utils.ImageUtil
1417import androidx.exifinterface.media.ExifInterface
1518import java.io.ByteArrayOutputStream
1619import java.io.IOException
@@ -39,58 +42,40 @@ import java.nio.ByteBuffer
3942object BitmapUtils {
4043 private const val TAG = " BitmapUtils"
4144
42-
43- /* * Converts NV21 format byte buffer to bitmap. */
44- fun getBitmap (data : ByteArray , metadata : FrameMetadata ): Bitmap ? {
45+ // based on androidx.camera.core.ImageSaver#imageToJpegByteArray(),
46+ // optimized to avoid extracting uncropped image twice and to close ImageProxy sooner
47+ @SuppressLint(" RestrictedApi" )
48+ @Throws(ImageUtil .CodecFailedException ::class )
49+ public fun extractJpegBytes (image : ImageProxy , jpegQuality : Int ): ByteArray {
4550 try {
46- val image = YuvImage (
47- data, ImageFormat .NV21 , metadata.width, metadata.height, null
48- )
49- val stream = ByteArrayOutputStream ()
50- image.compressToJpeg(Rect (0 , 0 , metadata.width, metadata.height), 80 , stream)
51- val bmp = BitmapFactory .decodeByteArray(stream.toByteArray(), 0 , stream.size())
52- stream.close()
53- return rotateBitmap(bmp, metadata.rotation, false , false )
54- } catch (e: Exception ) {
55- Log .e(" VisionProcessorBase" , " Error: " + e.message)
51+ var cropRect = if (ImageUtil .shouldCropImage(image)) image.cropRect else null
52+ val imageFormat = image.format
53+
54+ var origJpegBytes = if (imageFormat == ImageFormat .JPEG ) {
55+ ImageUtil .jpegImageToJpegByteArray(image)
56+ } else if (imageFormat == ImageFormat .YUV_420_888 ) {
57+ ImageUtil .yuvImageToJpegByteArray(image, cropRect, jpegQuality)
58+ } else {
59+ throw IllegalStateException (" unknown imageFormat $imageFormat " )
60+ }
61+ return origJpegBytes!! ;
62+ } finally {
63+ /*
64+ from javadoc of the Image class:
65+ Since Images are often directly produced or consumed by hardware components, they are
66+ a limited resource shared across the system, and should be closed as soon as
67+ they are no longer needed.
68+ */
69+ image.close()
5670 }
57- return null
5871 }
59-
60-
61- /* * Converts NV21 format byte buffer to bitmap. */
62- fun getBitmap (data : ByteBuffer , metadata : FrameMetadata ): Bitmap ? {
63- data.rewind()
64- val imageInBuffer = ByteArray (data.limit())
65- data[imageInBuffer, 0 , imageInBuffer.size]
66- try {
67- val image = YuvImage (
68- imageInBuffer, ImageFormat .NV21 , metadata.width, metadata.height, null
69- )
70- val stream = ByteArrayOutputStream ()
71- image.compressToJpeg(Rect (0 , 0 , metadata.width, metadata.height), 80 , stream)
72- val bmp = BitmapFactory .decodeByteArray(stream.toByteArray(), 0 , stream.size())
73- stream.close()
74- return rotateBitmap(bmp, metadata.rotation, false , false )
75- } catch (e: Exception ) {
76- Log .e(" VisionProcessorBase" , " Error: " + e.message)
77- }
78- return null
72+ @OptIn(ExperimentalGetImage ::class ) public fun byteArrayFromProxy (image : ImageProxy , jpegQuality : Int ): ByteArray? {
73+ return extractJpegBytes(image,jpegQuality);
7974 }
80-
81- /* * Converts a YUV_420_888 image from CameraX API to a bitmap. */
82- @RequiresApi(Build .VERSION_CODES .LOLLIPOP )
83- @ExperimentalGetImage
84- fun getBitmap (image : ImageProxy ): Bitmap ? {
85- val frameMetadata = FrameMetadata .Builder ()
86- .setWidth(image.width)
87- .setHeight(image.height)
88- .setRotation(image.imageInfo.rotationDegrees)
89- .build()
90- val nv21Buffer = yuv420ThreePlanesToNV21(
91- image.image!! .planes, image.width, image.height
92- )
93- return getBitmap(nv21Buffer, frameMetadata)
75+ @OptIn(ExperimentalGetImage ::class ) public fun getBitmap (image : ImageProxy , jpegQuality : Int ): Bitmap {
76+ val byteArray = extractJpegBytes(image, jpegQuality);
77+ var bm = BitmapFactory .decodeByteArray(byteArray, 0 , byteArray.size);
78+ return bm;
9479 }
9580
9681 /* * Rotates a bitmap if it is converted from a bytebuffer. */
@@ -169,113 +154,4 @@ object BitmapUtils {
169154 }
170155 return exif.getAttributeInt(ExifInterface .TAG_ORIENTATION , ExifInterface .ORIENTATION_NORMAL )
171156 }
172-
173- /* *
174- * Converts YUV_420_888 to NV21 bytebuffer.
175- *
176- *
177- * The NV21 format consists of a single byte array containing the Y, U and V values. For an
178- * image of size S, the first S positions of the array contain all the Y values. The remaining
179- * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both
180- * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain
181- * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU
182- *
183- *
184- * YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled
185- * by a factor of 2 in both dimensions. [Image.getPlanes] returns an array with the Y, U and
186- * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into
187- * the first part of the NV21 array. The U and V planes may already have the representation in the
188- * NV21 format. This happens if the planes share the same buffer, the V buffer is one position
189- * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy
190- * them to the NV21 array.
191- */
192- @RequiresApi(Build .VERSION_CODES .KITKAT )
193- private fun yuv420ThreePlanesToNV21 (
194- yuv420888planes : Array <Image .Plane >, width : Int , height : Int
195- ): ByteBuffer {
196- val imageSize = width * height
197- val out = ByteArray (imageSize + 2 * (imageSize / 4 ))
198- if (areUVPlanesNV21(yuv420888planes, width, height)) {
199- // Copy the Y values.
200- yuv420888planes[0 ].buffer[out , 0 , imageSize]
201- val uBuffer = yuv420888planes[1 ].buffer
202- val vBuffer = yuv420888planes[2 ].buffer
203- // Get the first V value from the V buffer, since the U buffer does not contain it.
204- vBuffer[out , imageSize, 1 ]
205- // Copy the first U value and the remaining VU values from the U buffer.
206- uBuffer[out , imageSize + 1 , 2 * imageSize / 4 - 1 ]
207- } else {
208- // Fallback to copying the UV values one by one, which is slower but also works.
209- // Unpack Y.
210- unpackPlane(yuv420888planes[0 ], width, height, out , 0 , 1 )
211- // Unpack U.
212- unpackPlane(yuv420888planes[1 ], width, height, out , imageSize + 1 , 2 )
213- // Unpack V.
214- unpackPlane(yuv420888planes[2 ], width, height, out , imageSize, 2 )
215- }
216- return ByteBuffer .wrap(out )
217- }
218-
219- /* * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. */
220- @RequiresApi(Build .VERSION_CODES .KITKAT )
221- private fun areUVPlanesNV21 (planes : Array <Image .Plane >, width : Int , height : Int ): Boolean {
222- val imageSize = width * height
223- val uBuffer = planes[1 ].buffer
224- val vBuffer = planes[2 ].buffer
225-
226- // Backup buffer properties.
227- val vBufferPosition = vBuffer.position()
228- val uBufferLimit = uBuffer.limit()
229-
230- // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
231- vBuffer.position(vBufferPosition + 1 )
232- // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
233- uBuffer.limit(uBufferLimit - 1 )
234-
235- // Check that the buffers are equal and have the expected number of elements.
236- val areNV21 =
237- vBuffer.remaining() == 2 * imageSize / 4 - 2 && vBuffer.compareTo(uBuffer) == 0
238-
239- // Restore buffers to their initial state.
240- vBuffer.position(vBufferPosition)
241- uBuffer.limit(uBufferLimit)
242- return areNV21
243- }
244-
245- /* *
246- * Unpack an image plane into a byte array.
247- *
248- *
249- * The input plane data will be copied in 'out', starting at 'offset' and every pixel will be
250- * spaced by 'pixelStride'. Note that there is no row padding on the output.
251- */
252- @TargetApi(Build .VERSION_CODES .KITKAT )
253- private fun unpackPlane (
254- plane : Image .Plane , width : Int , height : Int , out : ByteArray , offset : Int , pixelStride : Int
255- ) {
256- val buffer = plane.buffer
257- buffer.rewind()
258-
259- // Compute the size of the current plane.
260- // We assume that it has the aspect ratio as the original image.
261- val numRow = (buffer.limit() + plane.rowStride - 1 ) / plane.rowStride
262- if (numRow == 0 ) {
263- return
264- }
265- val scaleFactor = height / numRow
266- val numCol = width / scaleFactor
267-
268- // Extract the data in the output buffer.
269- var outputPos = offset
270- var rowStart = 0
271- for (row in 0 until numRow) {
272- var inputPos = rowStart
273- for (col in 0 until numCol) {
274- out [outputPos] = buffer[inputPos]
275- outputPos + = pixelStride
276- inputPos + = plane.pixelStride
277- }
278- rowStart + = plane.rowStride
279- }
280- }
281157}
0 commit comments