From b4695e613f0bca451485f95572f23d464b56a95e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 24 Apr 2026 16:42:18 +0200 Subject: Initial import --- .../android/camera/utils/AutoFitSurfaceView.kt | 79 +++++++++ .../example/android/camera/utils/CameraSizes.kt | 79 +++++++++ .../com/example/android/camera/utils/ExifUtils.kt | 73 ++++++++ .../android/camera/utils/GenericListAdapter.kt | 55 ++++++ .../android/camera/utils/OrientationLiveData.kt | 95 ++++++++++ .../java/com/example/android/camera/utils/Yuv.kt | 191 +++++++++++++++++++++ .../android/camera/utils/YuvToRgbConverter.kt | 99 +++++++++++ 7 files changed, 671 insertions(+) create mode 100644 utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/Yuv.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt (limited to 'utils/src/main/java/com') diff --git a/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt new file mode 100644 index 0000000..3d900d1 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceView +import kotlin.math.roundToInt + +/** + * A [SurfaceView] that can be adjusted to a specified aspect ratio and + * performs center-crop transformation of input frames. + */ +class AutoFitSurfaceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : SurfaceView(context, attrs, defStyle) { + + private var aspectRatio = 0f + + /** + * Sets the aspect ratio for this view. The size of the view will be + * measured based on the ratio calculated from the parameters. + * + * @param width Camera resolution horizontal size + * @param height Camera resolution vertical size + */ + fun setAspectRatio(width: Int, height: Int) { + require(width > 0 && height > 0) { "Size cannot be negative" } + aspectRatio = width.toFloat() / height.toFloat() + holder.setFixedSize(width, height) + requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + if (aspectRatio == 0f) { + setMeasuredDimension(width, height) + } else { + + // Performs center-crop transformation of the camera frames + val newWidth: Int + val newHeight: Int + val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio + if (width < height * actualRatio) { + newHeight = height + newWidth = (height * actualRatio).roundToInt() + } else { + newWidth = width + newHeight = (width / actualRatio).roundToInt() + } + + Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight") + setMeasuredDimension(newWidth, newHeight) + } + } + + companion object { + private val TAG = AutoFitSurfaceView::class.java.simpleName + } +} diff --git a/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt new file mode 100644 index 0000000..6db01d3 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.graphics.Point +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.params.StreamConfigurationMap +import android.util.Size +import android.view.Display +import kotlin.math.max +import kotlin.math.min + +/** Helper class used to pre-compute shortest and longest sides of a [Size] */ +class SmartSize(width: Int, height: Int) { + var size = Size(width, height) + var long = max(size.width, size.height) + var short = min(size.width, size.height) + override fun toString() = "SmartSize(${long}x${short})" +} + +/** Standard High Definition size for pictures and video */ +val SIZE_1080P: SmartSize = SmartSize(1920, 1080) + +/** Returns a [SmartSize] object for the given [Display] */ +fun getDisplaySmartSize(display: Display): SmartSize { + val outPoint = Point() + display.getRealSize(outPoint) + return SmartSize(outPoint.x, outPoint.y) +} + +/** + * Returns the largest available PREVIEW size. For more information, see: + * https://d.android.com/reference/android/hardware/camera2/CameraDevice and + * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap + */ +fun getPreviewOutputSize( + display: Display, + characteristics: CameraCharacteristics, + targetClass: Class, + format: Int? = null +): Size { + + // Find which is smaller: screen or 1080p + val screenSize = getDisplaySmartSize(display) + val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short + val maxSize = if (hdScreen) SIZE_1080P else screenSize + + // If image format is provided, use it to determine supported sizes; else use target class + val config = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + if (format == null) + assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) + else + assert(config.isOutputSupportedFor(format)) + val allSizes = if (format == null) + config.getOutputSizes(targetClass) else config.getOutputSizes(format) + + // Get available sizes and sort them by area from largest to smallest + val validSizes = allSizes + .sortedWith(compareBy { it.height * it.width }) + .map { SmartSize(it.width, it.height) }.reversed() + + // Then, get the largest output size that is smaller or equal than our max size + return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt new file mode 100644 index 0000000..561c14b --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.util.Log +import androidx.exifinterface.media.ExifInterface + +private const val TAG: String = "ExifUtils" + +/** Transforms rotation and mirroring information into one of the [ExifInterface] constants */ +fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when { + rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL + rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL + rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180 + rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL + rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE + rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90 + rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE + rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270 + rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE + else -> ExifInterface.ORIENTATION_UNDEFINED +} + +/** + * Helper function used to convert an EXIF orientation enum into a transformation matrix + * that can be applied to a bitmap. + * + * @return matrix - Transformation required to properly display [Bitmap] + */ +fun decodeExifOrientation(exifOrientation: Int): Matrix { + val matrix = Matrix() + + // Apply transformation corresponding to declared EXIF orientation + when (exifOrientation) { + ExifInterface.ORIENTATION_NORMAL -> Unit + ExifInterface.ORIENTATION_UNDEFINED -> Unit + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1F, 1F) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1F, -1F) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.postScale(-1F, 1F) + matrix.postRotate(270F) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.postScale(-1F, 1F) + matrix.postRotate(90F) + } + + // Error out if the EXIF orientation is invalid + else -> Log.e(TAG, "Invalid orientation: $exifOrientation") + } + + // Return the resulting matrix + return matrix +} diff --git a/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt new file mode 100644 index 0000000..a55af27 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +/** Type helper used for the callback triggered once our view has been bound */ +typealias BindCallback = (view: View, data: T, position: Int) -> Unit + +/** List adapter for generic types, intended used for small-medium lists of data */ +class GenericListAdapter( + private val dataset: List, + private val itemLayoutId: Int? = null, + private val itemViewFactory: (() -> View)? = null, + private val onBind: BindCallback +) : RecyclerView.Adapter() { + + class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder(when { + itemViewFactory != null -> itemViewFactory.invoke() + itemLayoutId != null -> { + LayoutInflater.from(parent.context) + .inflate(itemLayoutId, parent, false) + } + else -> { + throw IllegalStateException( + "Either the layout ID or the view factory need to be non-null") + } + }) + + override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { + if (position < 0 || position > dataset.size) return + onBind(holder.view, dataset[position], position) + } + + override fun getItemCount() = dataset.size +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt new file mode 100644 index 0000000..f9d9a47 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.view.OrientationEventListener +import android.view.Surface +import androidx.lifecycle.LiveData + + +/** + * Calculates closest 90-degree orientation to compensate for the device + * rotation relative to sensor orientation, i.e., allows user to see camera + * frames with the expected orientation. + */ +class OrientationLiveData( + context: Context, + characteristics: CameraCharacteristics +): LiveData() { + + private val listener = object : OrientationEventListener(context.applicationContext) { + override fun onOrientationChanged(orientation: Int) { + val rotation = when { + orientation <= 45 -> Surface.ROTATION_0 + orientation <= 135 -> Surface.ROTATION_90 + orientation <= 225 -> Surface.ROTATION_180 + orientation <= 315 -> Surface.ROTATION_270 + else -> Surface.ROTATION_0 + } + val relative = computeRelativeRotation(characteristics, rotation) + if (relative != value) postValue(relative) + } + } + + override fun onActive() { + super.onActive() + listener.enable() + } + + override fun onInactive() { + super.onInactive() + listener.disable() + } + + companion object { + + /** + * Computes rotation required to transform from the camera sensor orientation to the + * device's current orientation in degrees. + * + * @param characteristics the [CameraCharacteristics] to query for the sensor orientation. + * @param surfaceRotation the current device orientation as a Surface constant + * @return the relative rotation from the camera sensor to the current device orientation. + */ + @JvmStatic + private fun computeRelativeRotation( + characteristics: CameraCharacteristics, + surfaceRotation: Int + ): Int { + val sensorOrientationDegrees = + characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! + + val deviceOrientationDegrees = when (surfaceRotation) { + Surface.ROTATION_0 -> 0 + Surface.ROTATION_90 -> 90 + Surface.ROTATION_180 -> 180 + Surface.ROTATION_270 -> 270 + else -> 0 + } + + // Reverse device orientation for front-facing cameras + val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == + CameraCharacteristics.LENS_FACING_FRONT) 1 else -1 + + // Calculate desired JPEG orientation relative to camera orientation to make + // the image upright relative to the device orientation + return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360 + } + } +} diff --git a/utils/src/main/java/com/example/android/camera/utils/Yuv.kt b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt new file mode 100644 index 0000000..c476ad0 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt @@ -0,0 +1,191 @@ +package com.example.android.camera.utils + +import android.graphics.ImageFormat +import android.media.Image +import androidx.annotation.IntDef +import java.nio.ByteBuffer + +/* +This file is converted from part of https://github.com/gordinmitya/yuv2buf. +Follow the link to find demo app, performance benchmarks and unit tests. + +Intro to YUV image formats: +YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12. +420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V. + +* I420 format represents an image as Y plane followed by U then followed by V plane + without chroma channels interleaving. + For example: + Y Y Y Y + Y Y Y Y + U U V V + +* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U. + For example: + Y Y Y Y + Y Y Y Y + V U V U + +* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V) + +Visualization of these 4 formats: +https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg + +It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888. +https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + +Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN) +the conversion is done into these formats. + +More about each format: https://www.fourcc.org/yuv.php +*/ + +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) +@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888) +annotation class YuvType + +class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { + @YuvType + val type: Int + val buffer: ByteBuffer + + init { + val wrappedImage = ImageWrapper(image) + + type = if (wrappedImage.u.pixelStride == 1) { + ImageFormat.YUV_420_888 + } else { + ImageFormat.NV21 + } + val size = image.width * image.height * 3 / 2 + buffer = if ( + dstBuffer == null || dstBuffer.capacity() < size || + dstBuffer.isReadOnly || !dstBuffer.isDirect + ) { + ByteBuffer.allocateDirect(size) } + else { + dstBuffer + } + buffer.rewind() + + removePadding(wrappedImage) + } + + // Input buffers are always direct as described in + // https://developer.android.com/reference/android/media/Image.Plane#getBuffer() + private fun removePadding(image: ImageWrapper) { + val sizeLuma = image.y.width * image.y.height + val sizeChroma = image.u.width * image.u.height + if (image.y.rowStride > image.y.width) { + removePaddingCompact(image.y, buffer, 0) + } else { + buffer.position(0) + buffer.put(image.y.buffer) + } + if (type == ImageFormat.YUV_420_888) { + if (image.u.rowStride > image.u.width) { + removePaddingCompact(image.u, buffer, sizeLuma) + removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma) + } else { + buffer.position(sizeLuma) + buffer.put(image.u.buffer) + buffer.position(sizeLuma + sizeChroma) + buffer.put(image.v.buffer) + } + } else { + if (image.u.rowStride > image.u.width * 2) { + removePaddingNotCompact(image, buffer, sizeLuma) + } else { + buffer.position(sizeLuma) + var uv = image.v.buffer + val properUVSize = image.v.height * image.v.rowStride - 1 + if (uv.capacity() > properUVSize) { + uv = clipBuffer(image.v.buffer, 0, properUVSize) + } + buffer.put(uv) + val lastOne = image.u.buffer[image.u.buffer.capacity() - 1] + buffer.put(buffer.capacity() - 1, lastOne) + } + } + buffer.rewind() + } + + private fun removePaddingCompact( + plane: PlaneWrapper, + dst: ByteBuffer, + offset: Int + ) { + require(plane.pixelStride == 1) { + "use removePaddingCompact with pixelStride == 1" + } + + val src = plane.buffer + val rowStride = plane.rowStride + var row: ByteBuffer + dst.position(offset) + for (i in 0 until plane.height) { + row = clipBuffer(src, i * rowStride, plane.width) + dst.put(row) + } + } + + private fun removePaddingNotCompact( + image: ImageWrapper, + dst: ByteBuffer, + offset: Int + ) { + require(image.u.pixelStride == 2) { + "use removePaddingNotCompact pixelStride == 2" + } + val width = image.u.width + val height = image.u.height + val rowStride = image.u.rowStride + var row: ByteBuffer + dst.position(offset) + for (i in 0 until height - 1) { + row = clipBuffer(image.v.buffer, i * rowStride, width * 2) + dst.put(row) + } + row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2) + dst.put(row) + } + + private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer { + val duplicate = buffer.duplicate() + duplicate.position(start) + duplicate.limit(start + size) + return duplicate.slice() + } + + private class ImageWrapper(image:Image) { + val width= image.width + val height = image.height + val y = PlaneWrapper(width, height, image.planes[0]) + val u = PlaneWrapper(width / 2, height / 2, image.planes[1]) + val v = PlaneWrapper(width / 2, height / 2, image.planes[2]) + + // Check this is a supported image format + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + init { + require(y.pixelStride == 1) { + "Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead." + } + require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) { + "U and V planes must have the same pixel and row strides " + + "but got pixel=${u.pixelStride} row=${u.rowStride} for U " + + "and pixel=${v.pixelStride} and row=${v.rowStride} for V" + } + require(u.pixelStride == 1 || u.pixelStride == 2) { + "Supported" + " pixel strides for U and V planes are 1 and 2" + } + } + } + + private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) { + val width = width + val height = height + val buffer: ByteBuffer = plane.buffer + val rowStride = plane.rowStride + val pixelStride = plane.pixelStride + } +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt new file mode 100644 index 0000000..8dcd559 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.media.Image +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicYuvToRGB +import android.renderscript.Type +import java.nio.ByteBuffer + +/** + * Helper class used to convert a [Image] object from + * [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object, it has equivalent + * functionality to https://github + * .com/androidx/androidx/blob/androidx-main/camera/camera-core/src/main/java/androidx/camera/core/ImageYuvToRgbConverter.java + * + * NOTE: This has been tested in a limited number of devices and is not + * considered production-ready code. It was created for illustration purposes, + * since this is not an efficient camera pipeline due to the multiple copies + * required to convert each frame. For example, this + * implementation + * (https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776) + * might have better performance. + */ +class YuvToRgbConverter(context: Context) { + private val rs = RenderScript.create(context) + private val scriptYuvToRgb = + ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + + // Do not add getters/setters functions to these private variables + // because yuvToRgb() assume they won't be modified elsewhere + private var yuvBits: ByteBuffer? = null + private var bytes: ByteArray = ByteArray(0) + private var inputAllocation: Allocation? = null + private var outputAllocation: Allocation? = null + + @Synchronized + fun yuvToRgb(image: Image, output: Bitmap) { + val yuvBuffer = YuvByteBuffer(image, yuvBits) + yuvBits = yuvBuffer.buffer + + if (needCreateAllocations(image, yuvBuffer)) { + val yuvType = Type.Builder(rs, Element.U8(rs)) + .setX(image.width) + .setY(image.height) + .setYuvFormat(yuvBuffer.type) + inputAllocation = Allocation.createTyped( + rs, + yuvType.create(), + Allocation.USAGE_SCRIPT + ) + bytes = ByteArray(yuvBuffer.buffer.capacity()) + val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)) + .setX(image.width) + .setY(image.height) + outputAllocation = Allocation.createTyped( + rs, + rgbaType.create(), + Allocation.USAGE_SCRIPT + ) + } + + yuvBuffer.buffer.get(bytes) + inputAllocation!!.copyFrom(bytes) + + // Convert NV21 or YUV_420_888 format to RGB + inputAllocation!!.copyFrom(bytes) + scriptYuvToRgb.setInput(inputAllocation) + scriptYuvToRgb.forEach(outputAllocation) + outputAllocation!!.copyTo(output) + } + + private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean { + return (inputAllocation == null || // the very 1st call + inputAllocation!!.type.x != image.width || // image size changed + inputAllocation!!.type.y != image.height || + inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed + bytes.size == yuvBuffer.buffer.capacity()) + } +} -- cgit v1.2.3