inital commit
This commit is contained in:
2
utils/.gitignore
vendored
Normal file
2
utils/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
.idea
|
||||
75
utils/build.gradle
Normal file
75
utils/build.gradle
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility rootProject.ext.java_version
|
||||
targetCompatibility rootProject.ext.java_version
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "$rootProject.ext.java_version"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Kotlin lang
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4'
|
||||
|
||||
// App compat and UI things
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
|
||||
// EXIF Interface
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
testImplementation 'androidx.test:rules:1.2.0'
|
||||
testImplementation 'androidx.test:runner:1.2.0'
|
||||
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||
|
||||
// Instrumented testing
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
17
utils/src/main/AndroidManifest.xml
Normal file
17
utils/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<manifest package="de.weseng.camera.utils" />
|
||||
@@ -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 de.weseng.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
|
||||
}
|
||||
}
|
||||
79
utils/src/main/java/de/weseng/camera/utils/CameraSizes.kt
Normal file
79
utils/src/main/java/de/weseng/camera/utils/CameraSizes.kt
Normal file
@@ -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 de.weseng.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 <T>getPreviewOutputSize(
|
||||
display: Display,
|
||||
characteristics: CameraCharacteristics,
|
||||
targetClass: Class<T>,
|
||||
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
|
||||
}
|
||||
73
utils/src/main/java/de/weseng/camera/utils/ExifUtils.kt
Normal file
73
utils/src/main/java/de/weseng/camera/utils/ExifUtils.kt
Normal file
@@ -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 de.weseng.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
|
||||
}
|
||||
@@ -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 de.weseng.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<T> = (view: View, data: T, position: Int) -> Unit
|
||||
|
||||
/** List adapter for generic types, intended used for small-medium lists of data */
|
||||
class GenericListAdapter<T>(
|
||||
private val dataset: List<T>,
|
||||
private val itemLayoutId: Int? = null,
|
||||
private val itemViewFactory: (() -> View)? = null,
|
||||
private val onBind: BindCallback<T>
|
||||
) : RecyclerView.Adapter<GenericListAdapter.GenericListViewHolder>() {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 de.weseng.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<Int>() {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
198
utils/src/main/java/de/weseng/camera/utils/YuvToRgbConverter.kt
Normal file
198
utils/src/main/java/de/weseng/camera/utils/YuvToRgbConverter.kt
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.Rect
|
||||
import android.media.Image
|
||||
import android.renderscript.Allocation
|
||||
import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicYuvToRGB
|
||||
import android.renderscript.Type
|
||||
|
||||
/**
|
||||
* Helper class used to efficiently convert a [Media.Image] object from
|
||||
* [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object.
|
||||
*
|
||||
* The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
|
||||
* analysis use case on a Pixel 3 XL device at the default analyzer resolution,
|
||||
* which is 30 FPS with 640x480.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
class YuvToRgbConverter(context: Context) {
|
||||
private val rs = RenderScript.create(context)
|
||||
private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
|
||||
|
||||
private var pixelCount: Int = -1
|
||||
private lateinit var yuvBuffer: ByteArray
|
||||
private lateinit var inputAllocation: Allocation
|
||||
private lateinit var outputAllocation: Allocation
|
||||
|
||||
@Synchronized
|
||||
fun yuvToRgb(image: Image, output: Bitmap) {
|
||||
|
||||
// Ensure that the intermediate output byte buffer is allocated
|
||||
if (!::yuvBuffer.isInitialized) {
|
||||
pixelCount = image.cropRect.width() * image.cropRect.height()
|
||||
// Bits per pixel is an average for the whole image, so it's useful to compute the size
|
||||
// of the full buffer but should not be used to determine pixel offsets
|
||||
val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888)
|
||||
yuvBuffer = ByteArray(pixelCount * pixelSizeBits / 8)
|
||||
}
|
||||
|
||||
// Get the YUV data in byte array form using NV21 format
|
||||
imageToByteArray(image, yuvBuffer)
|
||||
|
||||
// Ensure that the RenderScript inputs and outputs are allocated
|
||||
if (!::inputAllocation.isInitialized) {
|
||||
// Explicitly create an element with type NV21, since that's the pixel format we use
|
||||
val elemType = Type.Builder(rs, Element.YUV(rs)).setYuvFormat(ImageFormat.NV21).create()
|
||||
inputAllocation = Allocation.createSized(rs, elemType.element, yuvBuffer.size)
|
||||
}
|
||||
if (!::outputAllocation.isInitialized) {
|
||||
outputAllocation = Allocation.createFromBitmap(rs, output)
|
||||
}
|
||||
|
||||
// Convert NV21 format YUV to RGB
|
||||
inputAllocation.copyFrom(yuvBuffer)
|
||||
scriptYuvToRgb.setInput(inputAllocation)
|
||||
scriptYuvToRgb.forEach(outputAllocation)
|
||||
outputAllocation.copyTo(output)
|
||||
}
|
||||
|
||||
private fun imageToByteArray(image: Image, outputBuffer: ByteArray) {
|
||||
assert(image.format == ImageFormat.YUV_420_888)
|
||||
|
||||
val imageCrop = image.cropRect
|
||||
val imagePlanes = image.planes
|
||||
|
||||
imagePlanes.forEachIndexed { planeIndex, plane ->
|
||||
// How many values are read in input for each output value written
|
||||
// Only the Y plane has a value for every pixel, U and V have half the resolution i.e.
|
||||
//
|
||||
// Y Plane U Plane V Plane
|
||||
// =============== ======= =======
|
||||
// Y Y Y Y Y Y Y Y U U U U V V V V
|
||||
// Y Y Y Y Y Y Y Y U U U U V V V V
|
||||
// Y Y Y Y Y Y Y Y U U U U V V V V
|
||||
// Y Y Y Y Y Y Y Y U U U U V V V V
|
||||
// Y Y Y Y Y Y Y Y
|
||||
// Y Y Y Y Y Y Y Y
|
||||
// Y Y Y Y Y Y Y Y
|
||||
val outputStride: Int
|
||||
|
||||
// The index in the output buffer the next value will be written at
|
||||
// For Y it's zero, for U and V we start at the end of Y and interleave them i.e.
|
||||
//
|
||||
// First chunk Second chunk
|
||||
// =============== ===============
|
||||
// Y Y Y Y Y Y Y Y V U V U V U V U
|
||||
// Y Y Y Y Y Y Y Y V U V U V U V U
|
||||
// Y Y Y Y Y Y Y Y V U V U V U V U
|
||||
// Y Y Y Y Y Y Y Y V U V U V U V U
|
||||
// Y Y Y Y Y Y Y Y
|
||||
// Y Y Y Y Y Y Y Y
|
||||
// Y Y Y Y Y Y Y Y
|
||||
var outputOffset: Int
|
||||
|
||||
when (planeIndex) {
|
||||
0 -> {
|
||||
outputStride = 1
|
||||
outputOffset = 0
|
||||
}
|
||||
1 -> {
|
||||
outputStride = 2
|
||||
// For NV21 format, U is in odd-numbered indices
|
||||
outputOffset = pixelCount + 1
|
||||
}
|
||||
2 -> {
|
||||
outputStride = 2
|
||||
// For NV21 format, V is in even-numbered indices
|
||||
outputOffset = pixelCount
|
||||
}
|
||||
else -> {
|
||||
// Image contains more than 3 planes, something strange is going on
|
||||
return@forEachIndexed
|
||||
}
|
||||
}
|
||||
|
||||
val planeBuffer = plane.buffer
|
||||
val rowStride = plane.rowStride
|
||||
val pixelStride = plane.pixelStride
|
||||
|
||||
// We have to divide the width and height by two if it's not the Y plane
|
||||
val planeCrop = if (planeIndex == 0) {
|
||||
imageCrop
|
||||
} else {
|
||||
Rect(
|
||||
imageCrop.left / 2,
|
||||
imageCrop.top / 2,
|
||||
imageCrop.right / 2,
|
||||
imageCrop.bottom / 2
|
||||
)
|
||||
}
|
||||
|
||||
val planeWidth = planeCrop.width()
|
||||
val planeHeight = planeCrop.height()
|
||||
|
||||
// Intermediate buffer used to store the bytes of each row
|
||||
val rowBuffer = ByteArray(plane.rowStride)
|
||||
|
||||
// Size of each row in bytes
|
||||
val rowLength = if (pixelStride == 1 && outputStride == 1) {
|
||||
planeWidth
|
||||
} else {
|
||||
// Take into account that the stride may include data from pixels other than this
|
||||
// particular plane and row, and that could be between pixels and not after every
|
||||
// pixel:
|
||||
//
|
||||
// |---- Pixel stride ----| Row ends here --> |
|
||||
// | Pixel 1 | Other Data | Pixel 2 | Other Data | ... | Pixel N |
|
||||
//
|
||||
// We need to get (N-1) * (pixel stride bytes) per row + 1 byte for the last pixel
|
||||
(planeWidth - 1) * pixelStride + 1
|
||||
}
|
||||
|
||||
for (row in 0 until planeHeight) {
|
||||
// Move buffer position to the beginning of this row
|
||||
planeBuffer.position(
|
||||
(row + planeCrop.top) * rowStride + planeCrop.left * pixelStride)
|
||||
|
||||
if (pixelStride == 1 && outputStride == 1) {
|
||||
// When there is a single stride value for pixel and output, we can just copy
|
||||
// the entire row in a single step
|
||||
planeBuffer.get(outputBuffer, outputOffset, rowLength)
|
||||
outputOffset += rowLength
|
||||
} else {
|
||||
// When either pixel or output have a stride > 1 we must copy pixel by pixel
|
||||
planeBuffer.get(rowBuffer, 0, rowLength)
|
||||
for (col in 0 until planeWidth) {
|
||||
outputBuffer[outputOffset] = rowBuffer[col * pixelStride]
|
||||
outputOffset += outputStride
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
utils/src/main/res/drawable/ic_shutter.xml
Normal file
21
utils/src/main/res/drawable/ic_shutter.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_pressed="true" android:drawable="@drawable/ic_shutter_pressed" />
|
||||
<item android:state_focused="true" android:drawable="@drawable/ic_shutter_focused" />
|
||||
<item android:drawable="@drawable/ic_shutter_normal" />
|
||||
</selector>
|
||||
28
utils/src/main/res/drawable/ic_shutter_focused.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_focused.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="74"
|
||||
android:viewportHeight="74">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
|
||||
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#58A0C4" android:fillType="evenOdd"
|
||||
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
||||
28
utils/src/main/res/drawable/ic_shutter_normal.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_normal.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="74"
|
||||
android:viewportHeight="74">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
|
||||
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#CFD7DB" android:fillType="evenOdd"
|
||||
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
||||
28
utils/src/main/res/drawable/ic_shutter_pressed.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_pressed.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="74"
|
||||
android:viewportHeight="74">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
|
||||
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#58A0C4" android:fillType="evenOdd"
|
||||
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
||||
Reference in New Issue
Block a user