Repo Created
This commit is contained in:
parent
eb305e2886
commit
a8c22c65db
4784 changed files with 329907 additions and 2 deletions
16
play-services-vision/core/src/main/AndroidManifest.xml
Normal file
16
play-services-vision/core/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ SPDX-FileCopyrightText: 2020 microG Project Team
|
||||
~ SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="org.opencv"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
android:required="false"/>
|
||||
|
||||
<application>
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.barcode;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.android.gms.vision.barcode";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite.barcode;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite.barcode";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.dynamite.face;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.android.gms.vision.dynamite.face";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.android.gms.vision.face;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.android.gms.vision.face";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.dynamite.descriptors.com.google.mlkit.dynamite.face;
|
||||
|
||||
public class ModuleDescriptor {
|
||||
public static final String MODULE_ID = "com.google.mlkit.dynamite.face";
|
||||
public static final int MODULE_VERSION = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.barcode
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
|
||||
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
|
||||
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetectorCreator
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import org.microg.gms.vision.barcode.BarcodeDetector
|
||||
|
||||
@Keep
|
||||
class ChimeraNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
|
||||
override fun create(context: IObjectWrapper, options: BarcodeDetectorOptions): INativeBarcodeDetector {
|
||||
return BarcodeDetector(context.unwrap<Context>()!!, options)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.barcode.mlkit
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.mlkit.vision.barcode.aidls.IBarcodeScannerCreator
|
||||
import com.google.mlkit.vision.barcode.aidls.IBarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.internal.BarcodeScannerOptions
|
||||
import org.microg.gms.vision.barcode.BarcodeScanner
|
||||
|
||||
@Keep
|
||||
class BarcodeScannerCreator : IBarcodeScannerCreator.Stub() {
|
||||
override fun create(context: IObjectWrapper, options: BarcodeScannerOptions): IBarcodeScanner {
|
||||
return BarcodeScanner(context.unwrap<Context>()!!, options)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.client
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
|
||||
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
|
||||
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetectorCreator
|
||||
import org.microg.gms.vision.barcode.BarcodeDetector
|
||||
|
||||
@Keep
|
||||
class DynamiteNativeBarcodeDetectorCreator : INativeBarcodeDetectorCreator.Stub() {
|
||||
override fun create(context: IObjectWrapper, options: BarcodeDetectorOptions): INativeBarcodeDetector {
|
||||
return BarcodeDetector(context.unwrap<Context>()!!, options)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.client
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.common.GooglePlayServicesUtil
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.android.gms.vision.face.internal.client.DetectionOptions
|
||||
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
|
||||
import com.google.android.gms.vision.face.internal.client.INativeFaceDetectorCreator
|
||||
import org.microg.gms.vision.face.TAG
|
||||
import org.microg.gms.vision.face.FaceDetector
|
||||
import org.opencv.android.OpenCVLoader
|
||||
|
||||
@Keep
|
||||
class DynamiteNativeFaceDetectorCreator : INativeFaceDetectorCreator.Stub() {
|
||||
|
||||
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: DetectionOptions?): INativeFaceDetector? {
|
||||
Log.d(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector faceDetectionOptions:${faceDetectionOptions.toString()}")
|
||||
try {
|
||||
val elapsedRealtime = SystemClock.elapsedRealtime()
|
||||
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
|
||||
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
|
||||
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
|
||||
if (!OpenCVLoader.initLocal()) {
|
||||
throw RuntimeException("Unable to load OpenCV")
|
||||
}
|
||||
Log.d(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
|
||||
return FaceDetector(remoteContext, faceDetectionOptions)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "DynamiteNativeFaceDetectorCreator newFaceDetector load failed ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.face
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.common.GooglePlayServicesUtil
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.android.gms.vision.face.internal.client.DetectionOptions
|
||||
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
|
||||
import com.google.android.gms.vision.face.internal.client.INativeFaceDetectorCreator
|
||||
import org.microg.gms.vision.face.FaceDetector
|
||||
import org.microg.gms.vision.face.TAG
|
||||
import org.opencv.android.OpenCVLoader
|
||||
|
||||
@Keep
|
||||
class ChimeraNativeFaceDetectorCreator : INativeFaceDetectorCreator.Stub() {
|
||||
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: DetectionOptions?): INativeFaceDetector? {
|
||||
Log.d(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector faceDetectionOptions:${faceDetectionOptions.toString()}")
|
||||
try {
|
||||
val elapsedRealtime = SystemClock.elapsedRealtime()
|
||||
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
|
||||
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
|
||||
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
|
||||
if (!OpenCVLoader.initLocal()) {
|
||||
throw RuntimeException("Unable to load OpenCV")
|
||||
}
|
||||
Log.d(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
|
||||
return FaceDetector(remoteContext, faceDetectionOptions)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "ChimeraNativeFaceDetectorCreator newFaceDetector load failed ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.android.gms.vision.face.mlkit
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.common.GooglePlayServicesUtil
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.mlkit.vision.face.FaceDetectionOptions
|
||||
import com.google.mlkit.vision.face.aidls.IFaceDetector
|
||||
import com.google.mlkit.vision.face.aidls.IFaceDetectorCreator
|
||||
import org.microg.gms.vision.face.TAG
|
||||
import org.microg.gms.vision.face.mlkit.FaceDetector
|
||||
import org.opencv.android.OpenCVLoader
|
||||
|
||||
@Keep
|
||||
class FaceDetectorCreator : IFaceDetectorCreator.Stub() {
|
||||
|
||||
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: FaceDetectionOptions?): IFaceDetector? {
|
||||
Log.d(TAG, "MLKit newFaceDetector options:${faceDetectionOptions}")
|
||||
try {
|
||||
val elapsedRealtime = SystemClock.elapsedRealtime()
|
||||
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
|
||||
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
|
||||
Log.d(TAG, "newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
|
||||
if (!OpenCVLoader.initLocal()) {
|
||||
throw RuntimeException("Unable to load OpenCV")
|
||||
}
|
||||
Log.d(TAG, "FaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
|
||||
return FaceDetector(remoteContext, faceDetectionOptions)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "FaceDetectorCreator newFaceDetector load failed ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.google.mlkit.vision.face.bundled.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.annotation.Keep
|
||||
import com.google.android.gms.common.GooglePlayServicesUtil
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.mlkit.vision.face.FaceDetectionOptions
|
||||
import com.google.mlkit.vision.face.aidls.IFaceDetector
|
||||
import com.google.mlkit.vision.face.aidls.IFaceDetectorCreator
|
||||
import org.microg.gms.vision.face.TAG
|
||||
import org.microg.gms.vision.face.mlkit.FaceDetector
|
||||
import org.opencv.android.OpenCVLoader
|
||||
|
||||
@Keep
|
||||
class ThickFaceDetectorCreator : IFaceDetectorCreator.Stub() {
|
||||
|
||||
override fun newFaceDetector(context: IObjectWrapper?, faceDetectionOptions: FaceDetectionOptions?): IFaceDetector? {
|
||||
Log.d(TAG, "MLKit newFaceDetector options:${faceDetectionOptions}")
|
||||
try {
|
||||
val elapsedRealtime = SystemClock.elapsedRealtime()
|
||||
val context = context.unwrap<Context>() ?: throw RuntimeException("Context is null")
|
||||
val remoteContext = GooglePlayServicesUtil.getRemoteContext(context) ?: throw RuntimeException("remoteContext is null")
|
||||
Log.d(TAG, "ThickFaceDetectorCreator newFaceDetector: context: ${context.packageName} remoteContext: ${remoteContext.packageName}")
|
||||
if (!OpenCVLoader.initLocal()) {
|
||||
throw RuntimeException("Unable to load OpenCV")
|
||||
}
|
||||
Log.d(TAG, "ThickFaceDetectorCreator newFaceDetector: load <openCV> library in ${SystemClock.elapsedRealtime() - elapsedRealtime}ms")
|
||||
return FaceDetector(remoteContext, faceDetectionOptions)
|
||||
} catch (e: Throwable) {
|
||||
Log.w(TAG, "ThickFaceDetectorCreator newFaceDetector load failed ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageFormat
|
||||
import android.media.Image
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.ChecksumException
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.FormatException
|
||||
import com.google.zxing.LuminanceSource
|
||||
import com.google.zxing.NotFoundException
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.IntBuffer
|
||||
|
||||
private const val TAG = "BarcodeDecodeHelper"
|
||||
|
||||
class BarcodeDecodeHelper(formats: List<BarcodeFormat>, multi: Boolean = true) {
|
||||
private val reader = MultiBarcodeReader(
|
||||
mapOf(
|
||||
DecodeHintType.TRY_HARDER to true,
|
||||
DecodeHintType.ALSO_INVERTED to true,
|
||||
DecodeHintType.POSSIBLE_FORMATS to formats
|
||||
)
|
||||
)
|
||||
|
||||
fun decodeFromSource(source: LuminanceSource): List<Result> {
|
||||
return try {
|
||||
reader.multiDecode(BinaryBitmap(HybridBinarizer(source))).also {
|
||||
if (it.isNotEmpty()) reader.reset()
|
||||
}
|
||||
} catch (e: NotFoundException) {
|
||||
emptyList()
|
||||
} catch (e: FormatException) {
|
||||
emptyList()
|
||||
} catch (e: ChecksumException) {
|
||||
emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Exception with $this: $e")
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeFromLuminanceBytes(rawBarcodeData: RawBarcodeData, rotate: Int): List<Result> {
|
||||
Log.d(TAG, "decodeFromLuminanceBytes rotate:")
|
||||
rawBarcodeData.rotateDetail(rotate)
|
||||
return decodeFromSource(
|
||||
PlanarYUVLuminanceSource(
|
||||
rawBarcodeData.bytes, rawBarcodeData.width, rawBarcodeData.height,
|
||||
0, 0, rawBarcodeData.width, rawBarcodeData.height, false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun decodeFromLuminanceBytes(buffer: ByteBuffer, width: Int, height: Int, rotate: Int = 0): List<Result> {
|
||||
val bytes = ByteArray(buffer.remaining())
|
||||
buffer.get(bytes)
|
||||
buffer.rewind()
|
||||
val rawBarcodeData = RawBarcodeData(bytes, width, height)
|
||||
return decodeFromLuminanceBytes(rawBarcodeData, rotate)
|
||||
}
|
||||
|
||||
@RequiresApi(19)
|
||||
fun decodeFromImage(image: Image, rotate: Int = 0): List<Result> {
|
||||
if (image.format !in SUPPORTED_IMAGE_FORMATS) return emptyList()
|
||||
val rawBarcodeData =RawBarcodeData(getYUVBytesFromImage(image), image.width, image.height)
|
||||
return decodeFromLuminanceBytes(rawBarcodeData, rotate)
|
||||
}
|
||||
|
||||
private fun getYUVBytesFromImage(image: Image): ByteArray {
|
||||
val planes = image.planes
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
val yuvBytes = ByteArray(width * height * 3 / 2)
|
||||
var offset = 0
|
||||
|
||||
for (i in planes.indices) {
|
||||
val buffer = planes[i].buffer
|
||||
val rowStride = planes[i].rowStride
|
||||
val pixelStride = planes[i].pixelStride
|
||||
val planeWidth = if ((i == 0)) width else width / 2
|
||||
val planeHeight = if ((i == 0)) height else height / 2
|
||||
|
||||
val planeBytes = ByteArray(buffer.capacity())
|
||||
buffer[planeBytes]
|
||||
|
||||
for (row in 0 until planeHeight) {
|
||||
for (col in 0 until planeWidth) {
|
||||
yuvBytes[offset++] = planeBytes[row * rowStride + col * pixelStride]
|
||||
}
|
||||
}
|
||||
}
|
||||
return yuvBytes
|
||||
}
|
||||
|
||||
fun decodeFromBitmap(bitmap: Bitmap): List<Result> {
|
||||
val frameBuf: IntBuffer = IntBuffer.allocate(bitmap.byteCount)
|
||||
bitmap.copyPixelsToBuffer(frameBuf)
|
||||
return decodeFromSource(RGBLuminanceSource(bitmap.width, bitmap.height, frameBuf.array()))
|
||||
}
|
||||
|
||||
companion object {
|
||||
@RequiresApi(19)
|
||||
val SUPPORTED_IMAGE_FORMATS =
|
||||
listOfNotNull(ImageFormat.YUV_420_888, if (SDK_INT >= 23) ImageFormat.YUV_422_888 else null, if (SDK_INT >= 23) ImageFormat.YUV_444_888 else null)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Point
|
||||
import android.util.Log
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.ObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.android.gms.vision.barcode.Barcode
|
||||
import com.google.android.gms.vision.barcode.internal.client.BarcodeDetectorOptions
|
||||
import com.google.android.gms.vision.barcode.internal.client.INativeBarcodeDetector
|
||||
import com.google.android.gms.vision.internal.FrameMetadataParcel
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.client.result.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.IntBuffer
|
||||
import java.util.*
|
||||
|
||||
private const val TAG = "BarcodeDetector"
|
||||
|
||||
class BarcodeDetector(val context: Context, val options: BarcodeDetectorOptions) : INativeBarcodeDetector.Stub() {
|
||||
private val helper = BarcodeDecodeHelper(options.formats.gmsToZXingBarcodeFormats())
|
||||
private var loggedOnce = false
|
||||
|
||||
override fun detectBitmap(wrappedBitmap: IObjectWrapper, metadata: FrameMetadataParcel): Array<Barcode> {
|
||||
if (!loggedOnce) Log.d(TAG, "detectBitmap(${ObjectWrapper.unwrap(wrappedBitmap)}, $metadata)").also { loggedOnce = true }
|
||||
val bitmap = wrappedBitmap.unwrap<Bitmap>() ?: return emptyArray()
|
||||
return helper.decodeFromBitmap(bitmap)
|
||||
.mapNotNull { runCatching { it.toGms(metadata) }.getOrNull() }.toTypedArray()
|
||||
}
|
||||
|
||||
override fun detectBytes(wrappedByteBuffer: IObjectWrapper, metadata: FrameMetadataParcel): Array<Barcode> {
|
||||
if (!loggedOnce) Log.d(TAG, "detectBytes(${ObjectWrapper.unwrap(wrappedByteBuffer)}, $metadata)").also { loggedOnce = true }
|
||||
val bytes = wrappedByteBuffer.unwrap<ByteBuffer>() ?: return emptyArray()
|
||||
return helper.decodeFromLuminanceBytes(bytes, metadata.width, metadata.height, metadata.rotation)
|
||||
.mapNotNull { runCatching { it.toGms(metadata) }.getOrNull() }.toTypedArray()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.d(TAG, "close()")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.gmsToZXingBarcodeFormats(): List<BarcodeFormat> {
|
||||
return listOfNotNull(
|
||||
BarcodeFormat.AZTEC.takeIf { (this and Barcode.AZTEC) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODABAR.takeIf { (this and Barcode.CODABAR) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_39.takeIf { (this and Barcode.CODE_39) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_93.takeIf { (this and Barcode.CODE_93) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_128.takeIf { (this and Barcode.CODE_128) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.DATA_MATRIX.takeIf { (this and Barcode.DATA_MATRIX) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.EAN_8.takeIf { (this and Barcode.EAN_8) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.EAN_13.takeIf { (this and Barcode.EAN_13) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.ITF.takeIf { (this and Barcode.ITF) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.PDF_417.takeIf { (this and Barcode.PDF417) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.QR_CODE.takeIf { (this and Barcode.QR_CODE) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.UPC_A.takeIf { (this and Barcode.UPC_A) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.UPC_E.takeIf { (this and Barcode.UPC_E) > 0 || this == Barcode.ALL_FORMATS },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun BarcodeFormat.toGms(): Int = when (this) {
|
||||
BarcodeFormat.AZTEC -> Barcode.AZTEC
|
||||
BarcodeFormat.CODABAR -> Barcode.CODABAR
|
||||
BarcodeFormat.CODE_39 -> Barcode.CODE_39
|
||||
BarcodeFormat.CODE_93 -> Barcode.CODE_93
|
||||
BarcodeFormat.CODE_128 -> Barcode.CODE_128
|
||||
BarcodeFormat.DATA_MATRIX -> Barcode.DATA_MATRIX
|
||||
BarcodeFormat.EAN_13 -> Barcode.EAN_13
|
||||
BarcodeFormat.EAN_8 -> Barcode.EAN_8
|
||||
BarcodeFormat.ITF -> Barcode.ITF
|
||||
BarcodeFormat.PDF_417 -> Barcode.PDF417
|
||||
BarcodeFormat.QR_CODE -> Barcode.QR_CODE
|
||||
BarcodeFormat.UPC_A -> Barcode.UPC_A
|
||||
BarcodeFormat.UPC_E -> Barcode.UPC_E
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun ParsedResultType.toGms(): Int = when (this) {
|
||||
ParsedResultType.ADDRESSBOOK -> Barcode.CONTACT_INFO
|
||||
ParsedResultType.CALENDAR -> Barcode.CALENDAR_EVENT
|
||||
ParsedResultType.EMAIL_ADDRESS -> Barcode.EMAIL
|
||||
ParsedResultType.GEO -> Barcode.GEO
|
||||
ParsedResultType.ISBN -> Barcode.ISBN
|
||||
ParsedResultType.PRODUCT -> Barcode.PRODUCT
|
||||
ParsedResultType.SMS -> Barcode.SMS
|
||||
ParsedResultType.TEL -> Barcode.PHONE
|
||||
ParsedResultType.TEXT -> Barcode.TEXT
|
||||
ParsedResultType.URI -> Barcode.URL
|
||||
ParsedResultType.WIFI -> Barcode.WIFI
|
||||
else -> Barcode.TEXT
|
||||
}
|
||||
|
||||
private fun AddressBookParsedResult.toGms(): Barcode.ContactInfo {
|
||||
val contactInfo = Barcode.ContactInfo()
|
||||
// TODO: contactInfo.name
|
||||
contactInfo.organization = org
|
||||
contactInfo.title = title
|
||||
contactInfo.phones = phoneNumbers.orEmpty().mapIndexed { i, a ->
|
||||
Barcode.Phone().apply {
|
||||
type = when (phoneTypes?.getOrNull(i)) {
|
||||
"WORK" -> Barcode.Phone.WORK
|
||||
"HOME" -> Barcode.Phone.HOME
|
||||
"FAX" -> Barcode.Phone.FAX
|
||||
"MOBILE" -> Barcode.Phone.MOBILE
|
||||
else -> Barcode.Phone.UNKNOWN
|
||||
}
|
||||
number = a
|
||||
}
|
||||
}.toTypedArray()
|
||||
contactInfo.emails = emails.orEmpty().mapIndexed { i, a ->
|
||||
Barcode.Email().apply {
|
||||
type = when (emailTypes?.getOrNull(i)) {
|
||||
"WORK" -> Barcode.Email.WORK
|
||||
"HOME" -> Barcode.Email.HOME
|
||||
else -> Barcode.Email.UNKNOWN
|
||||
}
|
||||
address = a
|
||||
}
|
||||
}.toTypedArray()
|
||||
contactInfo.urls = urLs
|
||||
contactInfo.addresses = addresses.orEmpty().mapIndexed { i, a ->
|
||||
Barcode.Address().apply {
|
||||
type = when (addressTypes?.getOrNull(i)) {
|
||||
"WORK" -> Barcode.Address.WORK
|
||||
"HOME" -> Barcode.Address.HOME
|
||||
else -> Barcode.Address.UNKNOWN
|
||||
}
|
||||
addressLines = a.split("\n").toTypedArray()
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
return contactInfo
|
||||
}
|
||||
|
||||
private fun CalendarParsedResult.toGms(): Barcode.CalendarEvent {
|
||||
fun createDateTime(timestamp: Long, isAllDay: Boolean) = Barcode.CalendarDateTime().apply {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.time = Date(timestamp)
|
||||
year = calendar.get(Calendar.YEAR)
|
||||
month = calendar.get(Calendar.MONTH)
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
if (isAllDay) {
|
||||
hours = -1
|
||||
minutes = -1
|
||||
seconds = -1
|
||||
} else {
|
||||
hours = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
minutes = calendar.get(Calendar.MINUTE)
|
||||
seconds = calendar.get(Calendar.SECOND)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val event = Barcode.CalendarEvent()
|
||||
event.summary = summary
|
||||
event.description = description
|
||||
event.location = location
|
||||
event.organizer = organizer
|
||||
event.start = createDateTime(startTimestamp, isStartAllDay)
|
||||
event.end = createDateTime(endTimestamp, isEndAllDay)
|
||||
return event
|
||||
}
|
||||
|
||||
private fun EmailAddressParsedResult.toGms(): Barcode.Email {
|
||||
val email = Barcode.Email()
|
||||
email.address = tos?.getOrNull(0)
|
||||
email.subject = subject
|
||||
email.body = body
|
||||
return email
|
||||
}
|
||||
|
||||
private fun GeoParsedResult.toGms(): Barcode.GeoPoint {
|
||||
val geo = Barcode.GeoPoint()
|
||||
geo.lat = latitude
|
||||
geo.lng = longitude
|
||||
return geo
|
||||
}
|
||||
|
||||
private fun TelParsedResult.toGms(): Barcode.Phone {
|
||||
val phone = Barcode.Phone()
|
||||
phone.number = number
|
||||
return phone
|
||||
}
|
||||
|
||||
private fun SMSParsedResult.toGms(): Barcode.Sms {
|
||||
val sms = Barcode.Sms()
|
||||
sms.message = body
|
||||
sms.phoneNumber = numbers?.getOrNull(0)
|
||||
return sms
|
||||
}
|
||||
|
||||
private fun WifiParsedResult.toGms(): Barcode.WiFi {
|
||||
val wifi = Barcode.WiFi()
|
||||
wifi.ssid = ssid
|
||||
wifi.password = password
|
||||
wifi.encryptionType = when (networkEncryption) {
|
||||
"OPEN" -> Barcode.WiFi.OPEN
|
||||
"WEP" -> Barcode.WiFi.WEP
|
||||
"WPA" -> Barcode.WiFi.WPA
|
||||
"WPA2" -> Barcode.WiFi.WPA
|
||||
else -> 0
|
||||
}
|
||||
return wifi
|
||||
}
|
||||
|
||||
private fun URIParsedResult.toGms(): Barcode.UrlBookmark {
|
||||
val url = Barcode.UrlBookmark()
|
||||
url.url = uri
|
||||
url.title = title
|
||||
return url
|
||||
}
|
||||
|
||||
private fun Result.toGms(metadata: FrameMetadataParcel): Barcode {
|
||||
val barcode = Barcode()
|
||||
barcode.format = barcodeFormat.toGms()
|
||||
barcode.rawBytes = rawBytes
|
||||
barcode.rawValue = text
|
||||
barcode.cornerPoints = resultPoints.map {
|
||||
Point(it.x.toInt(), it.y.toInt())
|
||||
}.toTypedArray()
|
||||
|
||||
val parsed = ResultParser.parseResult(this)
|
||||
|
||||
barcode.displayValue = parsed.displayResult
|
||||
barcode.valueFormat = parsed.type.toGms()
|
||||
when (parsed) {
|
||||
is EmailAddressParsedResult ->
|
||||
barcode.email = parsed.toGms()
|
||||
|
||||
is TelParsedResult ->
|
||||
barcode.phone = parsed.toGms()
|
||||
|
||||
is SMSParsedResult ->
|
||||
barcode.sms = parsed.toGms()
|
||||
|
||||
is WifiParsedResult ->
|
||||
barcode.wifi = parsed.toGms()
|
||||
|
||||
is URIParsedResult ->
|
||||
barcode.url = parsed.toGms()
|
||||
|
||||
is GeoParsedResult ->
|
||||
barcode.geoPoint = parsed.toGms()
|
||||
|
||||
is CalendarParsedResult ->
|
||||
barcode.calendarEvent = parsed.toGms()
|
||||
|
||||
is AddressBookParsedResult ->
|
||||
barcode.contactInfo = parsed.toGms()
|
||||
}
|
||||
|
||||
return barcode
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageFormat
|
||||
import android.media.Image
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.Parcel
|
||||
import android.util.Log
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.ObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.mlkit.vision.barcode.aidls.IBarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.internal.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import org.microg.gms.utils.warnOnTransactionIssues
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
private const val TAG = "BarcodeScanner"
|
||||
|
||||
class BarcodeScanner(val context: Context, val options: BarcodeScannerOptions) : IBarcodeScanner.Stub() {
|
||||
private val helper =
|
||||
BarcodeDecodeHelper(if (options.allPotentialBarcodesEnabled) BarcodeFormat.values().toList() else options.supportedFormats.mlKitToZXingBarcodeFormats())
|
||||
private var loggedOnce = false
|
||||
|
||||
override fun init() {
|
||||
Log.d(TAG, "init()")
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.d(TAG, "close()")
|
||||
}
|
||||
|
||||
override fun detect(wrappedImage: IObjectWrapper, metadata: ImageMetadata): List<Barcode> {
|
||||
if (!loggedOnce) Log.d(TAG, "detect(${ObjectWrapper.unwrap(wrappedImage)}, $metadata)").also { loggedOnce = true }
|
||||
return when (metadata.format) {
|
||||
-1 -> wrappedImage.unwrap<Bitmap>()?.let { helper.decodeFromBitmap(it) }
|
||||
ImageFormat.NV21 -> wrappedImage.unwrap<ByteBuffer>()?.let { helper.decodeFromLuminanceBytes(it, metadata.width, metadata.height, metadata.rotation) }
|
||||
ImageFormat.YUV_420_888 -> if (SDK_INT >= 19) wrappedImage.unwrap<Image>()?.let { image -> helper.decodeFromImage(image, metadata.rotation) } else null
|
||||
|
||||
else -> null
|
||||
}?.map { it.toMlKit(metadata) } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
|
||||
warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) }
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.multi.MultipleBarcodeReader
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class MultiBarcodeReader(val hints: Map<DecodeHintType, *>) : MultipleBarcodeReader, Reader {
|
||||
val delegate = MultiFormatReader().apply { setHints(hints) }
|
||||
|
||||
fun multiDecode(image: BinaryBitmap): List<Result> {
|
||||
return doDecodeMultiple(image)
|
||||
}
|
||||
|
||||
override fun decodeMultiple(image: BinaryBitmap): Array<Result> {
|
||||
return multiDecode(image).toTypedArray()
|
||||
}
|
||||
|
||||
override fun decodeMultiple(image: BinaryBitmap, hints: MutableMap<DecodeHintType, *>?): Array<Result> {
|
||||
return multiDecode(image).toTypedArray()
|
||||
}
|
||||
|
||||
override fun decode(image: BinaryBitmap): Result {
|
||||
return delegate.decodeWithState(image)
|
||||
}
|
||||
|
||||
override fun decode(image: BinaryBitmap, hints: MutableMap<DecodeHintType, *>?): Result {
|
||||
return delegate.decodeWithState(image)
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
delegate.reset()
|
||||
}
|
||||
|
||||
// Derived from com.google.zxing.multi GenericMultipleBarcodeReader
|
||||
// Copyright 2009 ZXing authors
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
private fun doDecodeMultiple(
|
||||
image: BinaryBitmap,
|
||||
results: MutableList<Result> = arrayListOf(),
|
||||
xOffset: Int = 0,
|
||||
yOffset: Int = 0,
|
||||
currentDepth: Int = 0,
|
||||
maxDepth: Int = 2
|
||||
): List<Result> {
|
||||
val result = kotlin.runCatching { delegate.decodeWithState(image) }.getOrNull() ?: return results
|
||||
|
||||
if (results.none { it.text == result.text }) {
|
||||
results.add(translateResultPoints(result, xOffset, yOffset))
|
||||
}
|
||||
|
||||
val resultPoints = result.resultPoints
|
||||
if (resultPoints != null && resultPoints.isNotEmpty() && currentDepth + 1 < maxDepth) {
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
var minX = width.toFloat()
|
||||
var minY = height.toFloat()
|
||||
var maxX = 0.0f
|
||||
var maxY = 0.0f
|
||||
|
||||
for (point in resultPoints) {
|
||||
if (point != null) {
|
||||
minX = min(point.x, minX)
|
||||
minY = min(point.y, minY)
|
||||
maxX = max(point.x, maxX)
|
||||
maxY = max(point.y, maxY)
|
||||
}
|
||||
}
|
||||
|
||||
if (minX > 100.0f) {
|
||||
this.doDecodeMultiple(image.crop(0, 0, minX.toInt(), height), results, xOffset, yOffset, currentDepth + 1, maxDepth)
|
||||
}
|
||||
|
||||
if (minY > 100.0f) {
|
||||
this.doDecodeMultiple(image.crop(0, 0, width, minY.toInt()), results, xOffset, yOffset, currentDepth + 1, maxDepth)
|
||||
}
|
||||
|
||||
if (maxX < (width - 100).toFloat()) {
|
||||
this.doDecodeMultiple(image.crop(maxX.toInt(), 0, width - maxX.toInt(), height), results, xOffset + maxX.toInt(), yOffset, currentDepth + 1, maxDepth)
|
||||
}
|
||||
|
||||
if (maxY < (height - 100).toFloat()) {
|
||||
this.doDecodeMultiple(image.crop(0, maxY.toInt(), width, height - maxY.toInt()), results, xOffset, yOffset + maxY.toInt(), currentDepth + 1, maxDepth)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
private fun translateResultPoints(result: Result, xOffset: Int, yOffset: Int): Result {
|
||||
val oldResultPoints = result.resultPoints
|
||||
if (oldResultPoints == null) {
|
||||
return result
|
||||
} else {
|
||||
val newResultPoints = arrayOfNulls<ResultPoint>(oldResultPoints.size)
|
||||
|
||||
for (i in oldResultPoints.indices) {
|
||||
val oldPoint = oldResultPoints[i]
|
||||
if (oldPoint != null) {
|
||||
newResultPoints[i] = ResultPoint(oldPoint.x + xOffset.toFloat(), oldPoint.y + yOffset.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
val newResult = Result(result.text, result.rawBytes, result.numBits, newResultPoints, result.barcodeFormat, result.timestamp)
|
||||
newResult.putAllMetadata(result.resultMetadata)
|
||||
return newResult
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.mlkit.vision.barcode.internal.Barcode
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.NotFoundException
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@RequiresApi(21)
|
||||
class QRCodeScannerView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
|
||||
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
|
||||
private var onQRCodeScanned: ((Barcode?) -> Unit)? = null
|
||||
private val previewView: PreviewView = PreviewView(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
init {
|
||||
addView(previewView)
|
||||
addView(ScanOverlayView(context))
|
||||
}
|
||||
|
||||
fun startScanner(onScanned: (Barcode?) -> Unit) {
|
||||
this.onQRCodeScanned = onScanned
|
||||
startCamera()
|
||||
}
|
||||
|
||||
private fun startCamera() {
|
||||
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) }
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).build().also {
|
||||
it.setAnalyzer(cameraExecutor, QRCodeAnalyzer { result ->
|
||||
post { onQRCodeScanned?.invoke(result) }
|
||||
})
|
||||
}
|
||||
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(context as androidx.lifecycle.LifecycleOwner, cameraSelector, preview, imageAnalyzer)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
cameraExecutor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private class ScanOverlayView(context: Context) : View(context) {
|
||||
private val cornerLength = 160f
|
||||
private val cornerThickness = 10f
|
||||
private val paint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
strokeWidth = cornerThickness
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val frameSize = width.coerceAtMost(height) * 0.6f
|
||||
val left = (width - frameSize) / 2f
|
||||
val top = (height - frameSize) / 2f
|
||||
val right = left + frameSize
|
||||
val bottom = top + frameSize
|
||||
val frame = RectF(left, top, right, bottom)
|
||||
|
||||
val colors = listOf(0xFF4285F4.toInt(), 0xFFEA4335.toInt(), 0xFFFBBC05.toInt(), 0xFF34A853.toInt())
|
||||
|
||||
paint.color = colors[0]
|
||||
canvas.drawLine(frame.left, frame.top, frame.left + cornerLength, frame.top, paint)
|
||||
canvas.drawLine(frame.left, frame.top, frame.left, frame.top + cornerLength, paint)
|
||||
|
||||
paint.color = colors[1]
|
||||
canvas.drawLine(frame.right, frame.top, frame.right - cornerLength, frame.top, paint)
|
||||
canvas.drawLine(frame.right, frame.top, frame.right, frame.top + cornerLength, paint)
|
||||
|
||||
paint.color = colors[2]
|
||||
canvas.drawLine(frame.left, frame.bottom, frame.left + cornerLength, frame.bottom, paint)
|
||||
canvas.drawLine(frame.left, frame.bottom, frame.left, frame.bottom - cornerLength, paint)
|
||||
|
||||
paint.color = colors[3]
|
||||
canvas.drawLine(frame.right, frame.bottom, frame.right - cornerLength, frame.bottom, paint)
|
||||
canvas.drawLine(frame.right, frame.bottom, frame.right, frame.bottom - cornerLength, paint)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
private class QRCodeAnalyzer(private val onQRCodeScanned: (Barcode?) -> Unit) : ImageAnalysis.Analyzer {
|
||||
|
||||
private val reader = MultiFormatReader().apply {
|
||||
setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
|
||||
}
|
||||
|
||||
override fun analyze(image: ImageProxy) {
|
||||
val buffer = image.planes[0].buffer
|
||||
val bytes = ByteArray(buffer.remaining())
|
||||
buffer.get(bytes)
|
||||
|
||||
val source = PlanarYUVLuminanceSource(bytes, image.width, image.height, 0, 0, image.width, image.height, false)
|
||||
val binaryBitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
|
||||
try {
|
||||
val result = reader.decode(binaryBitmap)
|
||||
onQRCodeScanned(result.toMlKit())
|
||||
} catch (e: NotFoundException) {
|
||||
onQRCodeScanned(null)
|
||||
} finally {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
|
||||
class RawBarcodeData(var bytes: ByteArray, var width: Int, var height: Int) {
|
||||
|
||||
fun rotateDetail(rotate: Int){
|
||||
when (rotate) {
|
||||
Surface.ROTATION_90 -> rotateDegree90()
|
||||
Surface.ROTATION_180 -> rotateDegree180()
|
||||
Surface.ROTATION_270 -> rotateDegree270()
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
private fun rotateDegree90(){
|
||||
val rotatedData = ByteArray(bytes.size)
|
||||
var index = 0
|
||||
|
||||
// Rotate Y plane
|
||||
for (col in 0 until width) {
|
||||
for (row in height - 1 downTo 0) {
|
||||
rotatedData[index++] = bytes[row * width + col]
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate UV planes (UV interleaved)
|
||||
val uvHeight = height / 2
|
||||
for (col in 0 until width step 2) {
|
||||
for (row in uvHeight - 1 downTo 0) {
|
||||
rotatedData[index++] = bytes[width * height + row * width + col]
|
||||
rotatedData[index++] = bytes[width * height + row * width + col + 1]
|
||||
}
|
||||
}
|
||||
bytes = rotatedData
|
||||
val temp = width
|
||||
width = height
|
||||
height = temp
|
||||
}
|
||||
|
||||
private fun rotateDegree180() {
|
||||
val rotatedData = ByteArray(bytes.size)
|
||||
var index = 0
|
||||
|
||||
// Rotate Y plane
|
||||
for (row in height - 1 downTo 0) {
|
||||
for (col in width - 1 downTo 0) {
|
||||
rotatedData[index++] = bytes[row * width + col]
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate UV planes (UV interleaved)
|
||||
val uvHeight = height / 2
|
||||
val uvWidth = width / 2
|
||||
for (row in uvHeight - 1 downTo 0) {
|
||||
for (col in uvWidth - 1 downTo 0) {
|
||||
val offset = width * height + row * width + col * 2
|
||||
rotatedData[index++] = bytes[offset]
|
||||
rotatedData[index++] = bytes[offset + 1]
|
||||
}
|
||||
}
|
||||
bytes = rotatedData
|
||||
}
|
||||
|
||||
|
||||
private fun rotateDegree270(){
|
||||
val rotatedData = ByteArray(bytes.size)
|
||||
var index = 0
|
||||
|
||||
// Rotate Y plane
|
||||
for (col in width - 1 downTo 0) {
|
||||
for (row in 0 until height) {
|
||||
rotatedData[index++] = bytes[row * width + col]
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate UV planes (UV interleaved)
|
||||
val uvHeight = height / 2
|
||||
for (col in width - 1 downTo 0 step 2) {
|
||||
for (row in 0 until uvHeight) {
|
||||
rotatedData[index++] = bytes[width * height + row * width + col - 1]
|
||||
rotatedData[index++] = bytes[width * height + row * width + col]
|
||||
}
|
||||
}
|
||||
bytes = rotatedData
|
||||
val temp = width
|
||||
width = height
|
||||
height = temp
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "RawBarcodeData(bytes=${bytes.size}, width=$width, height=$height)"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.barcode
|
||||
|
||||
import android.graphics.Point
|
||||
import com.google.mlkit.vision.barcode.internal.Address
|
||||
import com.google.mlkit.vision.barcode.internal.Barcode
|
||||
import com.google.mlkit.vision.barcode.internal.CalendarDateTime
|
||||
import com.google.mlkit.vision.barcode.internal.CalendarEvent
|
||||
import com.google.mlkit.vision.barcode.internal.ContactInfo
|
||||
import com.google.mlkit.vision.barcode.internal.Email
|
||||
import com.google.mlkit.vision.barcode.internal.GeoPoint
|
||||
import com.google.mlkit.vision.barcode.internal.ImageMetadata
|
||||
import com.google.mlkit.vision.barcode.internal.Phone
|
||||
import com.google.mlkit.vision.barcode.internal.Sms
|
||||
import com.google.mlkit.vision.barcode.internal.UrlBookmark
|
||||
import com.google.mlkit.vision.barcode.internal.WiFi
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.client.result.AddressBookParsedResult
|
||||
import com.google.zxing.client.result.CalendarParsedResult
|
||||
import com.google.zxing.client.result.EmailAddressParsedResult
|
||||
import com.google.zxing.client.result.GeoParsedResult
|
||||
import com.google.zxing.client.result.ParsedResultType
|
||||
import com.google.zxing.client.result.ResultParser
|
||||
import com.google.zxing.client.result.SMSParsedResult
|
||||
import com.google.zxing.client.result.TelParsedResult
|
||||
import com.google.zxing.client.result.URIParsedResult
|
||||
import com.google.zxing.client.result.WifiParsedResult
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlin.collections.mapIndexed
|
||||
import kotlin.collections.orEmpty
|
||||
import kotlin.text.split
|
||||
|
||||
fun Int.mlKitToZXingBarcodeFormats(): List<BarcodeFormat> {
|
||||
return listOfNotNull(
|
||||
BarcodeFormat.AZTEC.takeIf { (this and Barcode.AZTEC) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODABAR.takeIf { (this and Barcode.CODABAR) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_39.takeIf { (this and Barcode.CODE_39) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_93.takeIf { (this and Barcode.CODE_93) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.CODE_128.takeIf { (this and Barcode.CODE_128) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.DATA_MATRIX.takeIf { (this and Barcode.DATA_MATRIX) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.EAN_8.takeIf { (this and Barcode.EAN_8) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.EAN_13.takeIf { (this and Barcode.EAN_13) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.ITF.takeIf { (this and Barcode.ITF) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.PDF_417.takeIf { (this and Barcode.PDF417) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.QR_CODE.takeIf { (this and Barcode.QR_CODE) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.UPC_A.takeIf { (this and Barcode.UPC_A) > 0 || this == Barcode.ALL_FORMATS },
|
||||
BarcodeFormat.UPC_E.takeIf { (this and Barcode.UPC_E) > 0 || this == Barcode.ALL_FORMATS },
|
||||
)
|
||||
}
|
||||
|
||||
fun BarcodeFormat.toMlKit(): Int = when (this) {
|
||||
BarcodeFormat.AZTEC -> Barcode.AZTEC
|
||||
BarcodeFormat.CODABAR -> Barcode.CODABAR
|
||||
BarcodeFormat.CODE_39 -> Barcode.CODE_39
|
||||
BarcodeFormat.CODE_93 -> Barcode.CODE_93
|
||||
BarcodeFormat.CODE_128 -> Barcode.CODE_128
|
||||
BarcodeFormat.DATA_MATRIX -> Barcode.DATA_MATRIX
|
||||
BarcodeFormat.EAN_8 -> Barcode.EAN_8
|
||||
BarcodeFormat.EAN_13 -> Barcode.EAN_13
|
||||
BarcodeFormat.ITF -> Barcode.ITF
|
||||
BarcodeFormat.PDF_417 -> Barcode.PDF417
|
||||
BarcodeFormat.QR_CODE -> Barcode.QR_CODE
|
||||
BarcodeFormat.UPC_A -> Barcode.UPC_A
|
||||
BarcodeFormat.UPC_E -> Barcode.UPC_E
|
||||
else -> Barcode.UNKNOWN_FORMAT
|
||||
}
|
||||
|
||||
fun ParsedResultType.toMlKit(): Int = when (this) {
|
||||
ParsedResultType.ADDRESSBOOK -> Barcode.CONTACT_INFO
|
||||
ParsedResultType.CALENDAR -> Barcode.CALENDAR_EVENT
|
||||
ParsedResultType.EMAIL_ADDRESS -> Barcode.EMAIL
|
||||
ParsedResultType.GEO -> Barcode.GEO
|
||||
ParsedResultType.ISBN -> Barcode.ISBN
|
||||
ParsedResultType.PRODUCT -> Barcode.PRODUCT
|
||||
ParsedResultType.SMS -> Barcode.SMS
|
||||
ParsedResultType.TEL -> Barcode.PHONE
|
||||
ParsedResultType.TEXT -> Barcode.TEXT
|
||||
ParsedResultType.URI -> Barcode.URL
|
||||
ParsedResultType.WIFI -> Barcode.WIFI
|
||||
else -> Barcode.UNKNOWN_TYPE
|
||||
}
|
||||
|
||||
fun AddressBookParsedResult.toMlKit(): ContactInfo {
|
||||
val contactInfo = ContactInfo()
|
||||
// TODO: contactInfo.name
|
||||
contactInfo.organization = org
|
||||
contactInfo.title = title
|
||||
contactInfo.phones = phoneNumbers.orEmpty().mapIndexed { i, a ->
|
||||
Phone().apply {
|
||||
type = when (phoneTypes?.getOrNull(i)) {
|
||||
"WORK" -> Phone.WORK
|
||||
"HOME" -> Phone.HOME
|
||||
"FAX" -> Phone.FAX
|
||||
"MOBILE" -> Phone.MOBILE
|
||||
else -> Phone.UNKNOWN
|
||||
}
|
||||
number = a
|
||||
}
|
||||
}.toTypedArray()
|
||||
contactInfo.emails = emails.orEmpty().mapIndexed { i, a ->
|
||||
Email().apply {
|
||||
type = when (emailTypes?.getOrNull(i)) {
|
||||
"WORK" -> Email.WORK
|
||||
"HOME" -> Email.HOME
|
||||
else -> Email.UNKNOWN
|
||||
}
|
||||
address = a
|
||||
}
|
||||
}.toTypedArray()
|
||||
contactInfo.urls = urLs
|
||||
contactInfo.addresses = addresses.orEmpty().mapIndexed { i, a ->
|
||||
Address().apply {
|
||||
type = when (addressTypes?.getOrNull(i)) {
|
||||
"WORK" -> Address.WORK
|
||||
"HOME" -> Address.HOME
|
||||
else -> Address.UNKNOWN
|
||||
}
|
||||
addressLines = a.split("\n").toTypedArray()
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
return contactInfo
|
||||
}
|
||||
|
||||
fun CalendarParsedResult.toMlKit(): CalendarEvent {
|
||||
fun createDateTime(timestamp: Long, isAllDay: Boolean) = CalendarDateTime().apply {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.time = Date(timestamp)
|
||||
year = calendar.get(Calendar.YEAR)
|
||||
month = calendar.get(Calendar.MONTH)
|
||||
day = calendar.get(Calendar.DAY_OF_MONTH)
|
||||
if (isAllDay) {
|
||||
hours = -1
|
||||
minutes = -1
|
||||
seconds = -1
|
||||
} else {
|
||||
hours = calendar.get(Calendar.HOUR_OF_DAY)
|
||||
minutes = calendar.get(Calendar.MINUTE)
|
||||
seconds = calendar.get(Calendar.SECOND)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val event = CalendarEvent()
|
||||
event.summary = summary
|
||||
event.description = description
|
||||
event.location = location
|
||||
event.organizer = organizer
|
||||
event.start = createDateTime(startTimestamp, isStartAllDay)
|
||||
event.end = createDateTime(endTimestamp, isEndAllDay)
|
||||
return event
|
||||
}
|
||||
|
||||
fun EmailAddressParsedResult.toMlKit(): Email {
|
||||
val email = Email()
|
||||
email.address = tos?.getOrNull(0)
|
||||
email.subject = subject
|
||||
email.body = body
|
||||
return email
|
||||
}
|
||||
|
||||
fun GeoParsedResult.toMlKit(): GeoPoint {
|
||||
val geo = GeoPoint()
|
||||
geo.lat = latitude
|
||||
geo.lng = longitude
|
||||
return geo
|
||||
}
|
||||
|
||||
fun TelParsedResult.toMlKit(): Phone {
|
||||
val phone = Phone()
|
||||
phone.number = number
|
||||
return phone
|
||||
}
|
||||
|
||||
fun SMSParsedResult.toMlKit(): Sms {
|
||||
val sms = Sms()
|
||||
sms.message = body
|
||||
sms.phoneNumber = numbers?.getOrNull(0)
|
||||
return sms
|
||||
}
|
||||
|
||||
fun WifiParsedResult.toMlKit(): WiFi {
|
||||
val wifi = WiFi()
|
||||
wifi.ssid = ssid
|
||||
wifi.password = password
|
||||
wifi.encryptionType = when (networkEncryption) {
|
||||
"OPEN" -> WiFi.OPEN
|
||||
"WEP" -> WiFi.WEP
|
||||
"WPA" -> WiFi.WPA
|
||||
"WPA2" -> WiFi.WPA
|
||||
else -> 0
|
||||
}
|
||||
return wifi
|
||||
}
|
||||
|
||||
fun URIParsedResult.toMlKit(): UrlBookmark {
|
||||
val url = UrlBookmark()
|
||||
url.url = uri
|
||||
url.title = title
|
||||
return url
|
||||
}
|
||||
|
||||
fun Result.toMlKit(metadata: ImageMetadata? = null): Barcode {
|
||||
val barcode = Barcode()
|
||||
barcode.format = barcodeFormat.toMlKit()
|
||||
barcode.rawBytes = rawBytes
|
||||
barcode.rawValue = text
|
||||
barcode.cornerPoints = resultPoints.map {
|
||||
when (metadata?.rotation ?: -1) {
|
||||
1 -> Point(metadata!!.height - it.y.toInt(), it.x.toInt())
|
||||
2 -> Point(metadata!!.width - it.x.toInt(), metadata.height - it.y.toInt())
|
||||
3 -> Point(it.y.toInt(), metadata!!.width - it.x.toInt())
|
||||
else -> Point(it.x.toInt(), it.y.toInt())
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
val parsed = ResultParser.parseResult(this)
|
||||
|
||||
barcode.displayValue = parsed.displayResult
|
||||
barcode.valueType = parsed.type.toMlKit()
|
||||
when (parsed) {
|
||||
is EmailAddressParsedResult -> barcode.email = parsed.toMlKit()
|
||||
is TelParsedResult -> barcode.phone = parsed.toMlKit()
|
||||
is SMSParsedResult -> barcode.sms = parsed.toMlKit()
|
||||
is WifiParsedResult -> barcode.wifi = parsed.toMlKit()
|
||||
is URIParsedResult -> barcode.urlBookmark = parsed.toMlKit()
|
||||
is GeoParsedResult -> barcode.geoPoint = parsed.toMlKit()
|
||||
is CalendarParsedResult -> barcode.calendarEvent = parsed.toMlKit()
|
||||
is AddressBookParsedResult -> barcode.contactInfo = parsed.toMlKit()
|
||||
}
|
||||
return barcode
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.face
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.android.gms.vision.face.Contour
|
||||
import com.google.android.gms.vision.face.Landmark
|
||||
import com.google.android.gms.vision.face.internal.client.DetectionOptions
|
||||
import com.google.android.gms.vision.face.internal.client.FaceParcel
|
||||
import com.google.android.gms.vision.face.internal.client.INativeFaceDetector
|
||||
import com.google.android.gms.vision.internal.FrameMetadataParcel
|
||||
import com.google.mlkit.vision.face.Face
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class FaceDetector(val context: Context, private val options: DetectionOptions?) : INativeFaceDetector.Stub() {
|
||||
|
||||
private val mFaceDetector by lazy { FaceDetectorHelper(context) }
|
||||
|
||||
override fun closeDetectorJni() {
|
||||
Log.d(TAG, "closeDetectorJni")
|
||||
mFaceDetector.release()
|
||||
}
|
||||
|
||||
override fun isNativeFaceDetectorAvailable(i: Int): Boolean {
|
||||
Log.d(TAG, "isNativeFaceDetectorAvailable type:${i}")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun detectFacesFromPlanes(
|
||||
planeFirst: IObjectWrapper?,
|
||||
planeSencond: IObjectWrapper?,
|
||||
planeThird: IObjectWrapper?,
|
||||
firstPixelStride: Int,
|
||||
secondPixelStride: Int,
|
||||
thirdPixelStride: Int,
|
||||
firstRowStride: Int,
|
||||
secondRowStride: Int,
|
||||
thirdRowStride: Int,
|
||||
metadataParcel: FrameMetadataParcel?
|
||||
): Array<FaceParcel> {
|
||||
Log.d(
|
||||
TAG,
|
||||
"detectFacesFromPlanes planeFirst:${planeFirst} ,planeSecond:${planeSencond} ,planeThird:${planeThird}," + "firstPixelStride:${firstPixelStride} ,secondPixelStride:${secondPixelStride} ,thirdPixelStride:${thirdPixelStride} ," + "firstRowStride:${firstRowStride} ,secondRowStride:${secondRowStride} ,thirdRowStride:${thirdRowStride}," + "metadataParcel:${metadataParcel}"
|
||||
)
|
||||
val yBuffer = planeFirst?.unwrap<ByteBuffer>() ?: return emptyArray()
|
||||
val uBuffer = planeSencond?.unwrap<ByteBuffer>() ?: return emptyArray()
|
||||
val vBuffer = planeThird?.unwrap<ByteBuffer>() ?: return emptyArray()
|
||||
val width = metadataParcel?.width ?: return emptyArray()
|
||||
val height = metadataParcel?.height ?: return emptyArray()
|
||||
val rotation = metadataParcel.rotation
|
||||
val nv21 = ByteArray(width * height * 3 / 2)
|
||||
var offset = 0
|
||||
for (row in 0 until height) {
|
||||
yBuffer.position(row * firstRowStride)
|
||||
yBuffer.get(nv21, offset, width)
|
||||
offset += width
|
||||
}
|
||||
val chromaWidth = width / 2
|
||||
val chromaHeight = height / 2
|
||||
for (row in 0 until chromaHeight) {
|
||||
for (col in 0 until chromaWidth) {
|
||||
val uIndex = row * secondRowStride + col * secondPixelStride
|
||||
val vIndex = row * thirdRowStride + col * thirdPixelStride
|
||||
nv21[offset++] = vBuffer.get(vIndex)
|
||||
nv21[offset++] = uBuffer.get(uIndex)
|
||||
}
|
||||
}
|
||||
return mFaceDetector.detectFaces(nv21, width, height, rotation).map {
|
||||
it.toFaceParcel()
|
||||
}.toTypedArray().also {
|
||||
it.forEach { Log.d(TAG, "detectFacesFromPlanes: $it") }
|
||||
}
|
||||
}
|
||||
|
||||
override fun detectFaceParcels(wrapper: IObjectWrapper?, metadata: FrameMetadataParcel?): Array<FaceParcel> {
|
||||
Log.d(TAG, "detectFaceParcels byteBuffer:${wrapper} ,metadataParcel:${metadata}")
|
||||
if (wrapper == null || metadata == null) return emptyArray()
|
||||
val buffer = wrapper.unwrap<ByteBuffer>() ?: return emptyArray()
|
||||
return mFaceDetector.detectFaces(buffer.array(), metadata.width, metadata.height, metadata.rotation).map {
|
||||
it.toFaceParcel()
|
||||
}.toTypedArray().also {
|
||||
it.forEach { Log.d(TAG, "detectFaceParcels: $it") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun com.google.mlkit.vision.face.aidls.FaceParcel.toFaceParcel() = FaceParcel(
|
||||
1,
|
||||
id,
|
||||
(boundingBox.left + boundingBox.width() / 2).toFloat(),
|
||||
(boundingBox.top + boundingBox.height() / 2).toFloat(),
|
||||
boundingBox.width().toFloat(),
|
||||
boundingBox.height().toFloat(),
|
||||
panAngle,
|
||||
rollAngle,
|
||||
tiltAngle,
|
||||
landmarkParcelList.map { landmark -> Landmark(landmark.type, landmark.position.x, landmark.position.y, landmark.type) }.toTypedArray(),
|
||||
leftEyeOpenProbability,
|
||||
rightEyeOpenProbability,
|
||||
smileProbability,
|
||||
contourParcelList.map { contour -> Contour(contour.type, contour.pointsList) }.toTypedArray(),
|
||||
confidenceScore
|
||||
)
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.face
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.YuvImage
|
||||
import android.media.Image
|
||||
import android.util.Log
|
||||
import com.google.mlkit.vision.face.FaceContour
|
||||
import com.google.mlkit.vision.face.FaceLandmark
|
||||
import com.google.mlkit.vision.face.aidls.ContourParcel
|
||||
import com.google.mlkit.vision.face.aidls.FaceParcel
|
||||
import com.google.mlkit.vision.face.aidls.LandmarkParcel
|
||||
import org.opencv.android.Utils
|
||||
import org.opencv.core.Core
|
||||
import org.opencv.core.CvType
|
||||
import org.opencv.core.Mat
|
||||
import org.opencv.core.MatOfByte
|
||||
import org.opencv.core.Size
|
||||
import org.opencv.imgproc.Imgproc
|
||||
import org.opencv.objdetect.FaceDetectorYN
|
||||
import java.io.ByteArrayOutputStream
|
||||
import kotlin.math.hypot
|
||||
|
||||
const val TAG = "FaceDetection"
|
||||
|
||||
class FaceDetectorHelper(context: Context) {
|
||||
|
||||
private var faceDetectorYN: FaceDetectorYN? = null
|
||||
private var inputSize = Size(320.0, 320.0)
|
||||
|
||||
init {
|
||||
try {
|
||||
val buffer: ByteArray
|
||||
context.assets.open("face_detection_yunet_2023mar.onnx").use {
|
||||
val size = it.available()
|
||||
buffer = ByteArray(size)
|
||||
it.read(buffer)
|
||||
}
|
||||
faceDetectorYN = FaceDetectorYN.create("onnx", MatOfByte(*buffer), MatOfByte(), inputSize, 0.7f, 0.3f, 5000)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("faceDetectorYN initialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
fun detectFaces(bitmap: Bitmap, rotation: Int): List<FaceParcel> {
|
||||
Log.d(TAG, "detectFaces: source is bitmap")
|
||||
val rootMat = bitmapToMat(bitmap) ?: return emptyList()
|
||||
return processMat(rootMat, rotation)
|
||||
}
|
||||
|
||||
fun detectFaces(nv21ByteArray: ByteArray, width: Int, height: Int, rotation: Int): List<FaceParcel> {
|
||||
Log.d(TAG, "detectFaces: source is nv21Buffer")
|
||||
val rootMat = nv21ToMat(nv21ByteArray, width, height) ?: return emptyList()
|
||||
return processMat(rootMat, rotation)
|
||||
}
|
||||
|
||||
fun detectFaces(image: Image, rotation: Int): List<FaceParcel> {
|
||||
Log.d(TAG, "detectFaces: source is image")
|
||||
val rootMat = imageToMat(image) ?: return emptyList()
|
||||
return processMat(rootMat, rotation)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
try {
|
||||
faceDetectorYN = null
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "release failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processMat(mat: Mat, rotation: Int): List<FaceParcel> {
|
||||
val faceDetector = faceDetectorYN ?: return emptyList()
|
||||
val facesMat = Mat()
|
||||
val degree = degree(rotation)
|
||||
Log.d(TAG, "processMat: degree: $degree")
|
||||
when (degree) {
|
||||
2 -> Core.rotate(mat, facesMat, Core.ROTATE_90_COUNTERCLOCKWISE)
|
||||
3 -> Core.rotate(mat, facesMat, Core.ROTATE_180)
|
||||
4 -> Core.rotate(mat, facesMat, Core.ROTATE_90_CLOCKWISE)
|
||||
else -> mat.copyTo(facesMat)
|
||||
}
|
||||
val matSize = Size(facesMat.cols().toDouble(), facesMat.rows().toDouble())
|
||||
Log.d(TAG, "processMat: inputSize: $inputSize")
|
||||
if (inputSize != matSize) {
|
||||
inputSize = matSize
|
||||
faceDetector.inputSize = matSize
|
||||
}
|
||||
Log.d(TAG, "processMat: matSize: $matSize")
|
||||
val result = Mat()
|
||||
val status = faceDetectorYN!!.detect(facesMat, result)
|
||||
Log.d(TAG, "processMat: detect: $status facesMat: ${result.size()}")
|
||||
return parseDetections(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* faces: detection results stored in a 2D cv::Mat of shape [num_faces, 15]
|
||||
* 0-1: x, y of bbox top left corner
|
||||
* 2-3: width, height of bbox
|
||||
* 4-5: x, y of right eye (blue point in the example image)
|
||||
* 6-7: x, y of left eye (red point in the example image)
|
||||
* 8-9: x, y of nose tip (green point in the example image)
|
||||
* 10-11: x, y of right corner of mouth (pink point in the example image)
|
||||
* 12-13: x, y of left corner of mouth (yellow point in the example image)
|
||||
* 14: face score
|
||||
*/
|
||||
private fun parseDetections(detections: Mat): List<FaceParcel> {
|
||||
val faces = mutableListOf<FaceParcel>()
|
||||
val faceData = FloatArray(detections.cols() * detections.channels())
|
||||
for (i in 0 until detections.rows()) {
|
||||
detections.get(i, 0, faceData)
|
||||
val confidence = faceData[14]
|
||||
val boundingBox = Rect(faceData[0].toInt(), faceData[1].toInt(), (faceData[0] + faceData[2]).toInt(), (faceData[1] + faceData[3]).toInt())
|
||||
|
||||
val leftEyeMark = LandmarkParcel(FaceLandmark.LEFT_EYE, PointF(faceData[4], faceData[5]))
|
||||
val mouthLeftMark = LandmarkParcel(FaceLandmark.MOUTH_LEFT, PointF(faceData[10], faceData[11]))
|
||||
val noseBaseMark = LandmarkParcel(FaceLandmark.NOSE_BASE, PointF(faceData[8], faceData[9]))
|
||||
val rightEyeMark = LandmarkParcel(FaceLandmark.RIGHT_EYE, PointF(faceData[6], faceData[7]))
|
||||
val mouthRightMark = LandmarkParcel(FaceLandmark.MOUTH_RIGHT, PointF(faceData[12], faceData[13]))
|
||||
|
||||
// These are calculated for better compatibility, the model doesn't actually provide proper values here
|
||||
val mouthBottomMark = LandmarkParcel(FaceLandmark.MOUTH_BOTTOM, calculateMidPoint(mouthLeftMark, mouthRightMark))
|
||||
val leftCheekMark = LandmarkParcel(FaceLandmark.LEFT_CHEEK, calculateMidPoint(leftEyeMark, mouthLeftMark))
|
||||
val leftEarMark = LandmarkParcel(FaceLandmark.LEFT_EAR, PointF(boundingBox.right.toFloat(), noseBaseMark.position.y))
|
||||
val rightCheekMark = LandmarkParcel(FaceLandmark.RIGHT_CHEEK, calculateMidPoint(rightEyeMark, mouthRightMark))
|
||||
val rightEarMark = LandmarkParcel(FaceLandmark.RIGHT_EAR, PointF(boundingBox.left.toFloat(), noseBaseMark.position.y))
|
||||
|
||||
val smilingProbability = calculateSmilingProbability(mouthLeftMark, mouthRightMark)
|
||||
val leftEyeOpenProbability = calculateEyeOpenProbability(rightEyeMark, mouthRightMark)
|
||||
val rightEyeOpenProbability = calculateEyeOpenProbability(leftEyeMark, mouthLeftMark)
|
||||
|
||||
val faceContour = ContourParcel(FaceContour.FACE, arrayListOf(
|
||||
PointF(boundingBox.left.toFloat(), boundingBox.top.toFloat()),
|
||||
PointF(boundingBox.left.toFloat(), boundingBox.bottom.toFloat()),
|
||||
PointF(boundingBox.right.toFloat(), boundingBox.bottom.toFloat()),
|
||||
PointF(boundingBox.right.toFloat(), boundingBox.top.toFloat()),
|
||||
))
|
||||
val leftEyebrowTopContour = ContourParcel(FaceContour.LEFT_EYEBROW_TOP, arrayListOf(leftEyeMark.position))
|
||||
val leftEyebrowBottomContour = ContourParcel(FaceContour.LEFT_EYEBROW_BOTTOM, arrayListOf(leftEyeMark.position))
|
||||
val rightEyebrowTopContour = ContourParcel(FaceContour.RIGHT_EYEBROW_TOP, arrayListOf(rightEyeMark.position))
|
||||
val rightEyebrowBottomContour = ContourParcel(FaceContour.RIGHT_EYEBROW_BOTTOM, arrayListOf(rightEyeMark.position))
|
||||
val leftEyeContour = ContourParcel(FaceContour.LEFT_EYE, arrayListOf(leftEyeMark.position))
|
||||
val rightEyeContour = ContourParcel(FaceContour.RIGHT_EYE, arrayListOf(rightEyeMark.position))
|
||||
val upperLipTopContour = ContourParcel(FaceContour.UPPER_LIP_TOP, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
|
||||
val upperLipBottomContour = ContourParcel(FaceContour.UPPER_LIP_BOTTOM, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
|
||||
val lowerLipTopContour = ContourParcel(FaceContour.LOWER_LIP_TOP, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
|
||||
val lowerLipBottomContour = ContourParcel(FaceContour.LOWER_LIP_BOTTOM, arrayListOf(mouthLeftMark.position, mouthBottomMark.position, mouthRightMark.position, mouthBottomMark.position))
|
||||
val noseBridgeContour = ContourParcel(FaceContour.NOSE_BRIDGE, arrayListOf(noseBaseMark.position))
|
||||
val noseBottomContour = ContourParcel(FaceContour.NOSE_BOTTOM, arrayListOf(noseBaseMark.position))
|
||||
val leftCheekContour = ContourParcel(FaceContour.LEFT_CHEEK, arrayListOf(leftCheekMark.position))
|
||||
val rightCheekContour = ContourParcel(FaceContour.RIGHT_CHEEK, arrayListOf(rightCheekMark.position))
|
||||
|
||||
faces.add(FaceParcel(
|
||||
i,
|
||||
boundingBox,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
leftEyeOpenProbability,
|
||||
rightEyeOpenProbability,
|
||||
smilingProbability,
|
||||
confidence,
|
||||
arrayListOf(mouthBottomMark, leftCheekMark, leftEarMark, leftEyeMark, mouthLeftMark, noseBaseMark, rightCheekMark, rightEarMark, rightEyeMark, mouthRightMark),
|
||||
arrayListOf(faceContour, leftEyebrowTopContour, leftEyebrowBottomContour, rightEyebrowTopContour, rightEyebrowBottomContour, leftEyeContour, rightEyeContour, upperLipTopContour, upperLipBottomContour, lowerLipTopContour, lowerLipBottomContour, noseBridgeContour, noseBottomContour, leftCheekContour, rightCheekContour)
|
||||
).also {
|
||||
Log.d(TAG, "parseDetections: face->$it")
|
||||
})
|
||||
}
|
||||
Log.d(TAG, "parseDetections: faces->${faces.size}")
|
||||
return faces
|
||||
}
|
||||
|
||||
private fun calculateSmilingProbability(rightMouthCorner: LandmarkParcel, leftMouthCorner: LandmarkParcel): Float {
|
||||
val mouthWidth = hypot(
|
||||
(rightMouthCorner.position.x - leftMouthCorner.position.x).toDouble(), (rightMouthCorner.position.y - leftMouthCorner.position.y).toDouble()
|
||||
).toFloat()
|
||||
return (mouthWidth / 100).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
private fun calculateEyeOpenProbability(eye: LandmarkParcel, mouthCorner: LandmarkParcel): Float {
|
||||
val eyeMouthDistance = hypot(
|
||||
(eye.position.x - mouthCorner.position.x).toDouble(), (eye.position.y - mouthCorner.position.y).toDouble()
|
||||
).toFloat()
|
||||
return (eyeMouthDistance / 50).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
private fun calculateMidPoint(eye: LandmarkParcel, mouth: LandmarkParcel): PointF {
|
||||
return PointF((eye.position.x + mouth.position.x) / 2, (eye.position.y + mouth.position.y) / 2)
|
||||
}
|
||||
|
||||
private fun yuv420ToBitmap(image: Image): Bitmap? {
|
||||
val width = image.width
|
||||
val height = image.height
|
||||
|
||||
val yBuffer = image.planes[0].buffer
|
||||
val uBuffer = image.planes[1].buffer
|
||||
val vBuffer = image.planes[2].buffer
|
||||
|
||||
val ySize = yBuffer.remaining()
|
||||
val uSize = uBuffer.remaining()
|
||||
val vSize = vBuffer.remaining()
|
||||
|
||||
val nv21 = ByteArray(ySize + uSize + vSize)
|
||||
yBuffer.get(nv21, 0, ySize)
|
||||
vBuffer.get(nv21, ySize, vSize)
|
||||
uBuffer.get(nv21, ySize + vSize, uSize)
|
||||
|
||||
return nv21toBitmap(nv21, width, height)
|
||||
}
|
||||
|
||||
private fun nv21toBitmap(byteArray: ByteArray, width: Int, height: Int): Bitmap? {
|
||||
try {
|
||||
val yuvImage = YuvImage(byteArray, ImageFormat.NV21, width, height, null)
|
||||
val out = ByteArrayOutputStream()
|
||||
yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, out)
|
||||
val jpegBytes = out.toByteArray()
|
||||
return BitmapFactory.decodeByteArray(jpegBytes, 0, jpegBytes.size)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "nv21toBitmap: failed ", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun bitmapToMat(bitmap: Bitmap): Mat? {
|
||||
try {
|
||||
val mat = Mat(bitmap.height, bitmap.width, CvType.CV_8UC4)
|
||||
Utils.bitmapToMat(bitmap, mat)
|
||||
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_RGBA2BGR)
|
||||
return mat
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "bitmapToMat: failed", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun imageToMat(image: Image): Mat? {
|
||||
val bitmap = when (image.format) {
|
||||
ImageFormat.JPEG -> {
|
||||
val buffer = image.planes[0].buffer
|
||||
val bytes = ByteArray(buffer.remaining())
|
||||
buffer.get(bytes)
|
||||
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
}
|
||||
|
||||
ImageFormat.YUV_420_888 -> {
|
||||
yuv420ToBitmap(image)
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
return bitmap?.let { bitmapToMat(it) }
|
||||
}
|
||||
|
||||
private fun nv21ToMat(nv21ByteArray: ByteArray, width: Int, height: Int): Mat? {
|
||||
val bitmap = nv21toBitmap(nv21ByteArray, width, height)
|
||||
return bitmap?.let { bitmapToMat(it) }
|
||||
}
|
||||
|
||||
private fun degree(rotation: Int): Int {
|
||||
if (rotation == 0) return 1
|
||||
if (rotation == 1) return 4
|
||||
if (rotation == 2) return 3
|
||||
if (rotation == 3) return 2
|
||||
return 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025, microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.vision.face.mlkit
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageFormat
|
||||
import android.media.Image
|
||||
import android.util.Log
|
||||
import com.google.android.gms.dynamic.IObjectWrapper
|
||||
import com.google.android.gms.dynamic.unwrap
|
||||
import com.google.mlkit.vision.face.FaceDetectionOptions
|
||||
import com.google.mlkit.vision.face.FrameMetadataParcel
|
||||
import com.google.mlkit.vision.face.aidls.FaceParcel
|
||||
import com.google.mlkit.vision.face.aidls.IFaceDetector
|
||||
import org.microg.gms.vision.face.TAG
|
||||
import org.microg.gms.vision.face.FaceDetectorHelper
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class FaceDetector(val context: Context, val options: FaceDetectionOptions?) : IFaceDetector.Stub() {
|
||||
|
||||
private var mFaceDetector: FaceDetectorHelper? = null
|
||||
|
||||
override fun detectFaces(wrapper: IObjectWrapper?, metadata: FrameMetadataParcel?): List<FaceParcel> {
|
||||
Log.d(TAG, "MLKit detectFaces method: metadata:${metadata}")
|
||||
if (wrapper == null || metadata == null || mFaceDetector == null) return arrayListOf()
|
||||
val format = metadata.format
|
||||
val rotation = metadata.rotation
|
||||
if (format == -1) {
|
||||
val bitmap = wrapper.unwrap<Bitmap>() ?: return arrayListOf()
|
||||
return mFaceDetector?.detectFaces(bitmap, rotation) ?: arrayListOf()
|
||||
}
|
||||
if (format == ImageFormat.NV21) {
|
||||
val byteBuffer = wrapper.unwrap<ByteBuffer>() ?: return arrayListOf()
|
||||
return mFaceDetector?.detectFaces(byteBuffer.array(), metadata.width, metadata.height, rotation) ?: arrayListOf()
|
||||
}
|
||||
if (format == ImageFormat.YUV_420_888) {
|
||||
val image = wrapper.unwrap<Image>() ?: return arrayListOf()
|
||||
return mFaceDetector?.detectFaces(image, rotation) ?: arrayListOf()
|
||||
}
|
||||
return arrayListOf()
|
||||
}
|
||||
|
||||
override fun initDetector() {
|
||||
Log.d(TAG, "MLKit initDetector method isInitialized")
|
||||
if (mFaceDetector == null) {
|
||||
try {
|
||||
mFaceDetector = FaceDetectorHelper(context)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "initDetector: failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.d(TAG, "MLKit close")
|
||||
mFaceDetector?.release()
|
||||
mFaceDetector = null
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue