diff options
Diffstat (limited to 'utils/src/main/java/com')
7 files changed, 671 insertions, 0 deletions
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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.content.Context | ||
| 20 | import android.util.AttributeSet | ||
| 21 | import android.util.Log | ||
| 22 | import android.view.SurfaceView | ||
| 23 | import kotlin.math.roundToInt | ||
| 24 | |||
| 25 | /** | ||
| 26 | * A [SurfaceView] that can be adjusted to a specified aspect ratio and | ||
| 27 | * performs center-crop transformation of input frames. | ||
| 28 | */ | ||
| 29 | class AutoFitSurfaceView @JvmOverloads constructor( | ||
| 30 | context: Context, | ||
| 31 | attrs: AttributeSet? = null, | ||
| 32 | defStyle: Int = 0 | ||
| 33 | ) : SurfaceView(context, attrs, defStyle) { | ||
| 34 | |||
| 35 | private var aspectRatio = 0f | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Sets the aspect ratio for this view. The size of the view will be | ||
| 39 | * measured based on the ratio calculated from the parameters. | ||
| 40 | * | ||
| 41 | * @param width Camera resolution horizontal size | ||
| 42 | * @param height Camera resolution vertical size | ||
| 43 | */ | ||
| 44 | fun setAspectRatio(width: Int, height: Int) { | ||
| 45 | require(width > 0 && height > 0) { "Size cannot be negative" } | ||
| 46 | aspectRatio = width.toFloat() / height.toFloat() | ||
| 47 | holder.setFixedSize(width, height) | ||
| 48 | requestLayout() | ||
| 49 | } | ||
| 50 | |||
| 51 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { | ||
| 52 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) | ||
| 53 | val width = MeasureSpec.getSize(widthMeasureSpec) | ||
| 54 | val height = MeasureSpec.getSize(heightMeasureSpec) | ||
| 55 | if (aspectRatio == 0f) { | ||
| 56 | setMeasuredDimension(width, height) | ||
| 57 | } else { | ||
| 58 | |||
| 59 | // Performs center-crop transformation of the camera frames | ||
| 60 | val newWidth: Int | ||
| 61 | val newHeight: Int | ||
| 62 | val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio | ||
| 63 | if (width < height * actualRatio) { | ||
| 64 | newHeight = height | ||
| 65 | newWidth = (height * actualRatio).roundToInt() | ||
| 66 | } else { | ||
| 67 | newWidth = width | ||
| 68 | newHeight = (width / actualRatio).roundToInt() | ||
| 69 | } | ||
| 70 | |||
| 71 | Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight") | ||
| 72 | setMeasuredDimension(newWidth, newHeight) | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | companion object { | ||
| 77 | private val TAG = AutoFitSurfaceView::class.java.simpleName | ||
| 78 | } | ||
| 79 | } | ||
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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.graphics.Point | ||
| 20 | import android.hardware.camera2.CameraCharacteristics | ||
| 21 | import android.hardware.camera2.params.StreamConfigurationMap | ||
| 22 | import android.util.Size | ||
| 23 | import android.view.Display | ||
| 24 | import kotlin.math.max | ||
| 25 | import kotlin.math.min | ||
| 26 | |||
| 27 | /** Helper class used to pre-compute shortest and longest sides of a [Size] */ | ||
| 28 | class SmartSize(width: Int, height: Int) { | ||
| 29 | var size = Size(width, height) | ||
| 30 | var long = max(size.width, size.height) | ||
| 31 | var short = min(size.width, size.height) | ||
| 32 | override fun toString() = "SmartSize(${long}x${short})" | ||
| 33 | } | ||
| 34 | |||
| 35 | /** Standard High Definition size for pictures and video */ | ||
| 36 | val SIZE_1080P: SmartSize = SmartSize(1920, 1080) | ||
| 37 | |||
| 38 | /** Returns a [SmartSize] object for the given [Display] */ | ||
| 39 | fun getDisplaySmartSize(display: Display): SmartSize { | ||
| 40 | val outPoint = Point() | ||
| 41 | display.getRealSize(outPoint) | ||
| 42 | return SmartSize(outPoint.x, outPoint.y) | ||
| 43 | } | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Returns the largest available PREVIEW size. For more information, see: | ||
| 47 | * https://d.android.com/reference/android/hardware/camera2/CameraDevice and | ||
| 48 | * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap | ||
| 49 | */ | ||
| 50 | fun <T>getPreviewOutputSize( | ||
| 51 | display: Display, | ||
| 52 | characteristics: CameraCharacteristics, | ||
| 53 | targetClass: Class<T>, | ||
| 54 | format: Int? = null | ||
| 55 | ): Size { | ||
| 56 | |||
| 57 | // Find which is smaller: screen or 1080p | ||
| 58 | val screenSize = getDisplaySmartSize(display) | ||
| 59 | val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short | ||
| 60 | val maxSize = if (hdScreen) SIZE_1080P else screenSize | ||
| 61 | |||
| 62 | // If image format is provided, use it to determine supported sizes; else use target class | ||
| 63 | val config = characteristics.get( | ||
| 64 | CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! | ||
| 65 | if (format == null) | ||
| 66 | assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) | ||
| 67 | else | ||
| 68 | assert(config.isOutputSupportedFor(format)) | ||
| 69 | val allSizes = if (format == null) | ||
| 70 | config.getOutputSizes(targetClass) else config.getOutputSizes(format) | ||
| 71 | |||
| 72 | // Get available sizes and sort them by area from largest to smallest | ||
| 73 | val validSizes = allSizes | ||
| 74 | .sortedWith(compareBy { it.height * it.width }) | ||
| 75 | .map { SmartSize(it.width, it.height) }.reversed() | ||
| 76 | |||
| 77 | // Then, get the largest output size that is smaller or equal than our max size | ||
| 78 | return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size | ||
| 79 | } \ 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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.graphics.Bitmap | ||
| 20 | import android.graphics.Matrix | ||
| 21 | import android.util.Log | ||
| 22 | import androidx.exifinterface.media.ExifInterface | ||
| 23 | |||
| 24 | private const val TAG: String = "ExifUtils" | ||
| 25 | |||
| 26 | /** Transforms rotation and mirroring information into one of the [ExifInterface] constants */ | ||
| 27 | fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when { | ||
| 28 | rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL | ||
| 29 | rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL | ||
| 30 | rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180 | ||
| 31 | rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL | ||
| 32 | rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE | ||
| 33 | rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90 | ||
| 34 | rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE | ||
| 35 | rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270 | ||
| 36 | rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE | ||
| 37 | else -> ExifInterface.ORIENTATION_UNDEFINED | ||
| 38 | } | ||
| 39 | |||
| 40 | /** | ||
| 41 | * Helper function used to convert an EXIF orientation enum into a transformation matrix | ||
| 42 | * that can be applied to a bitmap. | ||
| 43 | * | ||
| 44 | * @return matrix - Transformation required to properly display [Bitmap] | ||
| 45 | */ | ||
| 46 | fun decodeExifOrientation(exifOrientation: Int): Matrix { | ||
| 47 | val matrix = Matrix() | ||
| 48 | |||
| 49 | // Apply transformation corresponding to declared EXIF orientation | ||
| 50 | when (exifOrientation) { | ||
| 51 | ExifInterface.ORIENTATION_NORMAL -> Unit | ||
| 52 | ExifInterface.ORIENTATION_UNDEFINED -> Unit | ||
| 53 | ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F) | ||
| 54 | ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F) | ||
| 55 | ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F) | ||
| 56 | ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1F, 1F) | ||
| 57 | ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1F, -1F) | ||
| 58 | ExifInterface.ORIENTATION_TRANSPOSE -> { | ||
| 59 | matrix.postScale(-1F, 1F) | ||
| 60 | matrix.postRotate(270F) | ||
| 61 | } | ||
| 62 | ExifInterface.ORIENTATION_TRANSVERSE -> { | ||
| 63 | matrix.postScale(-1F, 1F) | ||
| 64 | matrix.postRotate(90F) | ||
| 65 | } | ||
| 66 | |||
| 67 | // Error out if the EXIF orientation is invalid | ||
| 68 | else -> Log.e(TAG, "Invalid orientation: $exifOrientation") | ||
| 69 | } | ||
| 70 | |||
| 71 | // Return the resulting matrix | ||
| 72 | return matrix | ||
| 73 | } | ||
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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.view.LayoutInflater | ||
| 20 | import android.view.View | ||
| 21 | import android.view.ViewGroup | ||
| 22 | import androidx.recyclerview.widget.RecyclerView | ||
| 23 | |||
| 24 | /** Type helper used for the callback triggered once our view has been bound */ | ||
| 25 | typealias BindCallback<T> = (view: View, data: T, position: Int) -> Unit | ||
| 26 | |||
| 27 | /** List adapter for generic types, intended used for small-medium lists of data */ | ||
| 28 | class GenericListAdapter<T>( | ||
| 29 | private val dataset: List<T>, | ||
| 30 | private val itemLayoutId: Int? = null, | ||
| 31 | private val itemViewFactory: (() -> View)? = null, | ||
| 32 | private val onBind: BindCallback<T> | ||
| 33 | ) : RecyclerView.Adapter<GenericListAdapter.GenericListViewHolder>() { | ||
| 34 | |||
| 35 | class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) | ||
| 36 | |||
| 37 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder(when { | ||
| 38 | itemViewFactory != null -> itemViewFactory.invoke() | ||
| 39 | itemLayoutId != null -> { | ||
| 40 | LayoutInflater.from(parent.context) | ||
| 41 | .inflate(itemLayoutId, parent, false) | ||
| 42 | } | ||
| 43 | else -> { | ||
| 44 | throw IllegalStateException( | ||
| 45 | "Either the layout ID or the view factory need to be non-null") | ||
| 46 | } | ||
| 47 | }) | ||
| 48 | |||
| 49 | override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { | ||
| 50 | if (position < 0 || position > dataset.size) return | ||
| 51 | onBind(holder.view, dataset[position], position) | ||
| 52 | } | ||
| 53 | |||
| 54 | override fun getItemCount() = dataset.size | ||
| 55 | } \ 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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.content.Context | ||
| 20 | import android.hardware.camera2.CameraCharacteristics | ||
| 21 | import android.view.OrientationEventListener | ||
| 22 | import android.view.Surface | ||
| 23 | import androidx.lifecycle.LiveData | ||
| 24 | |||
| 25 | |||
| 26 | /** | ||
| 27 | * Calculates closest 90-degree orientation to compensate for the device | ||
| 28 | * rotation relative to sensor orientation, i.e., allows user to see camera | ||
| 29 | * frames with the expected orientation. | ||
| 30 | */ | ||
| 31 | class OrientationLiveData( | ||
| 32 | context: Context, | ||
| 33 | characteristics: CameraCharacteristics | ||
| 34 | ): LiveData<Int>() { | ||
| 35 | |||
| 36 | private val listener = object : OrientationEventListener(context.applicationContext) { | ||
| 37 | override fun onOrientationChanged(orientation: Int) { | ||
| 38 | val rotation = when { | ||
| 39 | orientation <= 45 -> Surface.ROTATION_0 | ||
| 40 | orientation <= 135 -> Surface.ROTATION_90 | ||
| 41 | orientation <= 225 -> Surface.ROTATION_180 | ||
| 42 | orientation <= 315 -> Surface.ROTATION_270 | ||
| 43 | else -> Surface.ROTATION_0 | ||
| 44 | } | ||
| 45 | val relative = computeRelativeRotation(characteristics, rotation) | ||
| 46 | if (relative != value) postValue(relative) | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | override fun onActive() { | ||
| 51 | super.onActive() | ||
| 52 | listener.enable() | ||
| 53 | } | ||
| 54 | |||
| 55 | override fun onInactive() { | ||
| 56 | super.onInactive() | ||
| 57 | listener.disable() | ||
| 58 | } | ||
| 59 | |||
| 60 | companion object { | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Computes rotation required to transform from the camera sensor orientation to the | ||
| 64 | * device's current orientation in degrees. | ||
| 65 | * | ||
| 66 | * @param characteristics the [CameraCharacteristics] to query for the sensor orientation. | ||
| 67 | * @param surfaceRotation the current device orientation as a Surface constant | ||
| 68 | * @return the relative rotation from the camera sensor to the current device orientation. | ||
| 69 | */ | ||
| 70 | @JvmStatic | ||
| 71 | private fun computeRelativeRotation( | ||
| 72 | characteristics: CameraCharacteristics, | ||
| 73 | surfaceRotation: Int | ||
| 74 | ): Int { | ||
| 75 | val sensorOrientationDegrees = | ||
| 76 | characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! | ||
| 77 | |||
| 78 | val deviceOrientationDegrees = when (surfaceRotation) { | ||
| 79 | Surface.ROTATION_0 -> 0 | ||
| 80 | Surface.ROTATION_90 -> 90 | ||
| 81 | Surface.ROTATION_180 -> 180 | ||
| 82 | Surface.ROTATION_270 -> 270 | ||
| 83 | else -> 0 | ||
| 84 | } | ||
| 85 | |||
| 86 | // Reverse device orientation for front-facing cameras | ||
| 87 | val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == | ||
| 88 | CameraCharacteristics.LENS_FACING_FRONT) 1 else -1 | ||
| 89 | |||
| 90 | // Calculate desired JPEG orientation relative to camera orientation to make | ||
| 91 | // the image upright relative to the device orientation | ||
| 92 | return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360 | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
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 @@ | |||
| 1 | package com.example.android.camera.utils | ||
| 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 | } \ 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 @@ | |||
| 1 | /* | ||
| 2 | * Copyright 2020 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 | * https://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 | package com.example.android.camera.utils | ||
| 18 | |||
| 19 | import android.content.Context | ||
| 20 | import android.graphics.Bitmap | ||
| 21 | import android.graphics.ImageFormat | ||
| 22 | import android.media.Image | ||
| 23 | import android.renderscript.Allocation | ||
| 24 | import android.renderscript.Element | ||
| 25 | import android.renderscript.RenderScript | ||
| 26 | import android.renderscript.ScriptIntrinsicYuvToRGB | ||
| 27 | import android.renderscript.Type | ||
| 28 | import java.nio.ByteBuffer | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Helper class used to convert a [Image] object from | ||
| 32 | * [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object, it has equivalent | ||
| 33 | * functionality to https://github | ||
| 34 | * .com/androidx/androidx/blob/androidx-main/camera/camera-core/src/main/java/androidx/camera/core/ImageYuvToRgbConverter.java | ||
| 35 | * | ||
| 36 | * NOTE: This has been tested in a limited number of devices and is not | ||
| 37 | * considered production-ready code. It was created for illustration purposes, | ||
| 38 | * since this is not an efficient camera pipeline due to the multiple copies | ||
| 39 | * required to convert each frame. For example, this | ||
| 40 | * implementation | ||
| 41 | * (https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776) | ||
| 42 | * might have better performance. | ||
| 43 | */ | ||
| 44 | class YuvToRgbConverter(context: Context) { | ||
| 45 | private val rs = RenderScript.create(context) | ||
| 46 | private val scriptYuvToRgb = | ||
| 47 | ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) | ||
| 48 | |||
| 49 | // Do not add getters/setters functions to these private variables | ||
| 50 | // because yuvToRgb() assume they won't be modified elsewhere | ||
| 51 | private var yuvBits: ByteBuffer? = null | ||
| 52 | private var bytes: ByteArray = ByteArray(0) | ||
| 53 | private var inputAllocation: Allocation? = null | ||
| 54 | private var outputAllocation: Allocation? = null | ||
| 55 | |||
| 56 | @Synchronized | ||
| 57 | fun yuvToRgb(image: Image, output: Bitmap) { | ||
| 58 | val yuvBuffer = YuvByteBuffer(image, yuvBits) | ||
| 59 | yuvBits = yuvBuffer.buffer | ||
| 60 | |||
| 61 | if (needCreateAllocations(image, yuvBuffer)) { | ||
| 62 | val yuvType = Type.Builder(rs, Element.U8(rs)) | ||
| 63 | .setX(image.width) | ||
| 64 | .setY(image.height) | ||
| 65 | .setYuvFormat(yuvBuffer.type) | ||
| 66 | inputAllocation = Allocation.createTyped( | ||
| 67 | rs, | ||
| 68 | yuvType.create(), | ||
| 69 | Allocation.USAGE_SCRIPT | ||
| 70 | ) | ||
| 71 | bytes = ByteArray(yuvBuffer.buffer.capacity()) | ||
| 72 | val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)) | ||
| 73 | .setX(image.width) | ||
| 74 | .setY(image.height) | ||
| 75 | outputAllocation = Allocation.createTyped( | ||
| 76 | rs, | ||
| 77 | rgbaType.create(), | ||
| 78 | Allocation.USAGE_SCRIPT | ||
| 79 | ) | ||
| 80 | } | ||
| 81 | |||
| 82 | yuvBuffer.buffer.get(bytes) | ||
| 83 | inputAllocation!!.copyFrom(bytes) | ||
| 84 | |||
| 85 | // Convert NV21 or YUV_420_888 format to RGB | ||
| 86 | inputAllocation!!.copyFrom(bytes) | ||
| 87 | scriptYuvToRgb.setInput(inputAllocation) | ||
| 88 | scriptYuvToRgb.forEach(outputAllocation) | ||
| 89 | outputAllocation!!.copyTo(output) | ||
| 90 | } | ||
| 91 | |||
| 92 | private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean { | ||
| 93 | return (inputAllocation == null || // the very 1st call | ||
| 94 | inputAllocation!!.type.x != image.width || // image size changed | ||
| 95 | inputAllocation!!.type.y != image.height || | ||
| 96 | inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed | ||
| 97 | bytes.size == yuvBuffer.buffer.capacity()) | ||
| 98 | } | ||
| 99 | } | ||
