摘要
將攝像頭預覽頁面封裝到Android模組中並在app中使用這個本地模組.
關鍵資訊
- Android Studio:Iguana | 2023.2.1
- Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-8.4-bin.zip
- jvmTarget = '1.8'
- minSdk 26
- targetSdk 34
- compileSdk 34
- 開發語言:Kotlin,Java
- ndkVersion = '21.1.6352462'
- kotlin版本:1.9.20
- kotlinCompilerExtensionVersion '1.5.4'
- com.android.library:8.3
原理簡介
CameraX簡介
[https://juejin.cn/post/6895278749630070798]
CameraX 是一個 Jetpack 支援庫,旨在幫助您簡化相機應用的開發工作。
它提供一致且易於使用的 API Surface,適用於大多數 Android 裝置,並可向後相容至 Android 5.0
基於Camera2,即對Camera2的封裝.
Android模組開發簡介
[https://www.jianshu.com/p/0ea37b2c7ce7]
模組化開發思路就是:單獨開發每個模組,用整合的方式把他們組合起來,就能拼出一個app。app可以理解成很多功能模組的組合,而且有些功能模組是通用的,必備的,像自動更新,反饋,推送,都可以提煉成模組,和搭積木很像,由一個殼包含很多個模組。
Android使用OpenCV庫的簡單方式
[https://github.com/QuickBirdEng/opencv-android]
Easy way to integrate OpenCV into your Android project via Gradle.
No NDK dependency needed - just include this library and you are good to go.
OpenCV Contribution's package naming has been changed to make it as per the naming guideline.
Old: opencv:VERSION-contrib
New: opencv-contrib:VERSION
Each versions is available in only OpenCV as well as OpenCV with contributions.
‼️ Please use 4.5.3.0 instead of 4.5.3. They are both the same versions, however, 4.5.3 has some runtime issues on some of the Android versions while 4.5.3.0 works fine.
實現
核心程式碼
- Android Studio 檔案->New->New Module新建安卓模組
grape_realtime_detect
- 編輯模組的build.gradle.kts檔案
repositories {
mavenCentral()
}
implementation `com.quickbirdstudios:opencv:4.5.3.0`
- app新增模組:
settings.gradle
include ':app:grape_realtime_detect'
build.gradle
implementation ('com.github.zynkware:Document-Scanning-Android-SDK:1.1.1') /* 文件掃描庫*/ {
// 去除OpenCV衝突依賴
exclude group: "com.github.zynkware", module: "Tiny-OpenCV"
}
/* start 實時識別相關 */
implementation project(':app:grape_realtime_detect')
/* end 實時識別相關 */
- 編輯模組的AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera2" android:required="false"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application>
<!-- 目標檢測頁模板 -->
<activity
android:name=".RealtimeDetectActivityTemplate"
android:exported="true"
android:label="@string/title_activity_realtime_detect"
android:theme="@style/Theme.Grapeyolov5detectandroid" />
</application>
</manifest>
- 模組程式碼
RealtimeDetectActivityTemplate.kt
package cn.qsbye.grape_realtime_detect
import android.app.Activity
import android.content.Context
import android.content.res.AssetManager
import android.graphics.BitmapFactory
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.OptIn
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.core.UseCase
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.Recorder
import androidx.camera.view.PreviewView
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.camera.core.Preview as PreviewCameraX
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import cn.qsbye.grape_realtime_detect.ui.theme.Grapeyolov5detectandroidTheme
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.opencv.videoio.VideoCapture
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import org.opencv.core.Core
import org.opencv.core.Mat
import org.opencv.core.Scalar
import org.opencv.imgcodecs.Imgcodecs
import java.io.File
// 全域性引用Activity
val LocalActivity = compositionLocalOf<Activity?> { null }
/* start 擴充套件Context */
// 新增相機畫面
suspend fun Context.getCameraProvider(): ProcessCameraProvider = suspendCoroutine { continuation ->
ProcessCameraProvider.getInstance(this).also { future ->
future.addListener({
continuation.resume(future.get())
}, executor)
}
}
val Context.executor: Executor
get() = ContextCompat.getMainExecutor(this)
/* end 擴充套件Context */
abstract class RealtimeDetectActivityTemplate : ComponentActivity() {
// 顯示Toast訊息
private fun Context.toast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* start 檢查相機許可權 */
XXPermissions.with(this)
// 申請單個許可權
.permission(Permission.CAMERA)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (!allGranted) {
// toast("相機許可權正常")
return
}
toast("獲取相機許可權成功")
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
if (doNotAskAgain) {
toast("被永久拒絕授權,請手動授予相機許可權")
// 如果是被永久拒絕就跳轉到應用許可權系統設定頁面
XXPermissions.startPermissionActivity(this@RealtimeDetectActivityTemplate, permissions)
} else {
toast("獲取相機許可權失敗")
// 結束當前活動
finish()
}
}
})
/* end 檢查相機許可權 */
// 初始化assets資料夾例項
val assetManager: AssetManager = assets
setContent {
Grapeyolov5detectandroidTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
provideActivity(activity = this) {
realtimeDetectPage()
}
}
}
}// end setContent
}
}
// 實時檢測頁面佈局
@Preview(showBackground = true)
@Composable
fun realtimeDetectPage(_modifier: Modifier = Modifier){
Column (
_modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
// horizontalAlignment = Alignment.CenterHorizontally
){
cameraArea(_modifier = Modifier.weight(0.8f))
navigationBarArea(
_modifier = Modifier
.weight(1f)
)
}
}
// 實時顯示區域
@Composable
fun cameraArea(_modifier: Modifier = Modifier, onUseCase: (UseCase) -> Unit = { }, onImageFileSaveSucceed:(File)-> Unit={
}){
// 獲取上下文
val context = LocalContext.current
// 協程檢視
val coroutineScope = rememberCoroutineScope()
// 繫結當前生命週期
val lifecycleOwner = LocalLifecycleOwner.current
// 相機縮放方式
val scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER
// 選定使用後置攝像頭
val cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// 圖片預覽例項
var previewUseCase by remember { mutableStateOf<UseCase>(PreviewCameraX.Builder().build()) }
// 圖片捕捉例項
val imageCaptureUseCase by remember {
mutableStateOf(
ImageCapture.Builder()
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.build()
)
}
Box(
modifier = _modifier
){
// 影像Bitmap狀態
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
// 使用remember來記憶boxes陣列和isRandom變數的變化
val s_boxes = remember { mutableStateOf(arrayOf(arrayOf<Float>())) }
val s_isRandom = remember { mutableStateOf<Boolean>(false) }
// 獲取上下文
val context2 = LocalContext.current
/* start 攝像頭畫面Preview */
AndroidView(
modifier = _modifier,
factory = { context ->
val previewView = PreviewView(context).apply {
this.scaleType = scaleType
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
// CameraX Preview UseCase
// 這裡注意匯入androidx.camera.core.Preview as PreviewCameraX而不是androidx.compose.ui.tooling.preview.Preview
previewUseCase = PreviewCameraX.Builder()
.build()
.also {it->
it.setSurfaceProvider(previewView.surfaceProvider)
}
// 協程啟動
coroutineScope.launch {
val cameraProvider = context.getCameraProvider()
try {
// 在下一次繫結之前需要解綁上一個
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, previewUseCase, imageCaptureUseCase
)
} catch (ex: Exception) {
Log.e("CameraPreview", "Use case binding failed", ex)
}
}
// 返回previewView畫面
previewView
}
)
/* end 攝像頭畫面Preview */
/* start 預覽圖片 */
// 繪製影像
imageBitmap?.let { bitmap ->
Canvas(modifier = _modifier
.height(120.dp)
.width(90.dp)) {
drawImage(
image = bitmap,
topLeft = Offset(x = 0.dp.toPx(), y = 0.dp.toPx()),
alpha = 0.5f, // 設定透明度為50%
)
}
}
/* end 預覽圖片 */
/* start 繪製矩形框 */
// 使用remember監聽boxes變數和isRandom變數變化並更新顯示
s_boxes.value = arrayOf(
arrayOf(100f, 200f, 400f, 500f),
arrayOf(50f, 150f, 300f, 450f),
arrayOf(20f, 300f, 100f, 500f)
)
drawRectangleBox(_modifier = _modifier, isRandom = s_isRandom.value, boxes = s_boxes.value)
/* end 繪製矩形框 */
/* start 拍照協程 */
LaunchedEffect(Unit) {
coroutineScope.launch(Dispatchers.IO) {
while (true) {
try {
/* start 呼叫takePicture */
imageCaptureUseCase.takePicture(
context.executor, // 執行器
object : ImageCapture.OnImageCapturedCallback() {
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// 計算實際尺寸
val (actualWidth, actualHeight) = options.run { outWidth to outHeight }
// 計算縮放因子
var inSampleSize = 1
if (actualWidth > reqWidth || actualHeight > reqHeight) {
val halfWidth = actualWidth / 2
val halfHeight = actualHeight / 2
inSampleSize = if (halfWidth < reqWidth || halfHeight < reqHeight) {
halfWidth.coerceAtLeast(halfHeight).toInt()
} else {
halfWidth.coerceAtMost(halfHeight).toInt()
}
}
// 計算最終的縮放因子
inSampleSize = Math.round(Math.pow(2.0, ((Integer.SIZE - Integer.numberOfLeadingZeros(
(inSampleSize - 1)
)).toDouble()))).toInt()
return inSampleSize
}
@OptIn(ExperimentalGetImage::class)
override fun onCaptureSuccess(imageProxy: ImageProxy) {
// 圖片捕獲成功,可以在這裡處理ImageProxy
Log.d("CameraX", "圖片捕獲成功")
// 將ImageProxy轉換為Bitmap
val image = imageProxy.image
val buffer = image!!.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
options.inSampleSize = calculateInSampleSize(options, 256, 256)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
//val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
// 將Bitmap轉換為ImageBitmap
val _imageBitmap = bitmap?.asImageBitmap()
// 更新影像狀態
imageBitmap = _imageBitmap
// 呼叫TFLite識別葡萄
LocalActivity.run{
s_boxes.value=GrapeDetect.detectGrapes(imageBitmap!!,context2)
}
Log.d("GrapeRealtimeDetect", "識別葡萄完成!")
// 釋放資源
imageProxy.close()
} // end imageProxy
override fun onError(exception: ImageCaptureException) {
// 處理捕獲過程中的錯誤
Log.d("CameraX", "圖片捕獲失敗: ${exception.message}")
}
}
)
/* end 呼叫takePicture */
} catch (e: Exception) {
// 處理異常情況
Log.d("CameraX","圖片捕獲異常")
}
// 等待500毫秒
delay(500)
}
}
}
/* end 拍照協程 */
} // end Box
}
// 返回按鈕區域
@Composable
fun navigationBarArea(_modifier: Modifier = Modifier){
// 獲取當前上下文
val context = LocalContext.current
// 獲取當前活動
val activity = LocalActivity.current
Column(
modifier = Modifier
.background(Color(0xFFDBDF74))
.fillMaxWidth(),
horizontalAlignment= Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
){
Button(
modifier = Modifier
.fillMaxWidth(0.8f),
onClick = {
// 關閉當前Activity
activity?.finish()
}){
Text(
text = "返回"
)
}
}
}
// 提供當前活動
@Composable
fun provideActivity(activity: Activity, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalActivity provides activity) {
content()
}
}
// 繪製矩形框
@Composable
fun drawRectangleBox(_modifier : Modifier,isRandom: Boolean = false, boxes: Array<Array<Float>>) {
Canvas(modifier = _modifier.fillMaxSize()) {
// 遍歷每個矩形框
for ((startX, startY, endX, endY) in boxes) {
// 判斷isRandom,選擇顏色
var color = if (isRandom) Color.Red else Color.Green
// 繪製空心矩形框
drawRect(
color = color,
// start = Offset(startX, startY),
size = Size(endX - startX, endY - startY),
style = Stroke(width = 5f) // 設定邊框寬度
)
// 計算矩形框的中心點
val centerX = startX + (endX - startX) / 2
val centerY = startY + (endY - startY) / 2
class TextMeasurer {
fun measureText(text: String, fontSize: Float, font: Font): Size {
// 這裡是測量文字尺寸的程式碼
// 可能需要根據實際的圖形庫或API來編寫
// 返回文字的寬度和高度
return Size(50f, 30f)
}
}
// 使用 TextMeasurer 來測量文字尺寸
val textMeasurer = TextMeasurer()
val nativePaint = android.graphics.Paint().let {
it.apply {
textSize = 36f
color = Color(0,255,0)
}
}
// 繪製文字
drawContext.canvas.nativeCanvas.drawText(
"葡萄",
centerX,
centerY,
nativePaint
)
}
}
}
GrapeRealtimeDetect.kt
package cn.qsbye.grape_realtime_detect
import android.content.Context
import android.content.res.AssetManager
import android.graphics.Bitmap
import android.graphics.RectF
import android.util.Log
import androidx.compose.ui.graphics.ImageBitmap
import org.opencv.android.OpenCVLoader
import kotlin.random.Random
/*
GrapeRealtimeDetect模組入口
*/
object GrapeRealtimeDetect {
// 初始化模組
fun init() {
/* start 初始化OpenCV庫 */
Log.d("GrapeRealtimeDetect","開始初始化!")
if (!OpenCVLoader.initDebug()) {
Log.d("GrapeRealtimeDetect", "無法載入OpenCV庫!");
} else {
Log.d("GrapeRealtimeDetect", "OpenCV載入成功!");
}
/* end 初始化OpenCV庫 */
}
}
/* start 葡萄檢測 */
// 葡萄識別總函式
object GrapeDetect {
fun detectGrapes(imageBitmap: ImageBitmap,context: Context): Array<Array<Float>> {
// TODO 呼叫TFLite識別葡萄
// 假設這裡會有一些影像處理的程式碼來識別葡萄
// ...
// 隨機生成矩形的寬度和高度(>=100f)
val width = Random.nextFloat() * (imageBitmap.width - 100) + 100
val height = Random.nextFloat() * (imageBitmap.height - 100) + 100
// 隨機生成矩形的座標
val x = Random.nextFloat() * (imageBitmap.width - width)
val y = Random.nextFloat() * (imageBitmap.height - height)
// 更新 s_boxes 狀態
return arrayOf(arrayOf(x, y, x + width, y + height))
}
}
// 影像預處理函式
private fun preprocessImage(bitmap: Bitmap): Bitmap {
// 這裡應根據模型的要求對影像進行預處理
// 例如,調整影像大小、歸一化等
return bitmap
}
// TensorFlow Lite模型載入和推理的輔助類
class TfLiteModelLoader {
companion object {
// fun loadModelFromFile(context: Context, modelName: String): TfLiteModel {
// // 實現模型載入邏輯
//
// return null
// }
}
}
class TfLiteModel {
// fun recognizeImage(image: Bitmap): List<RecognitionResult> {
// // 實現推理邏輯,並返回檢測結果
// return null
// }
}
// 檢測結果的資料類
data class RecognitionResult(
val boundingBox: RectF,
// 其他可能的欄位,如類別、置信度等
)
/* end 葡萄檢測 */
build.gradle.kts
import com.android.build.api.dsl.AaptOptions
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "cn.qsbye.grape_realtime_detect"
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
packagingOptions {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
aaptOptions {
// 不壓縮模型檔案
noCompress("tflite")
noCompress("lite")
}
}
dependencies {
/* start Android系統相關 */
implementation("androidx.core:core-ktx:1.13.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2024.04.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
/* end Android系統相關 */
/* start 相機相關 */
implementation("io.reactivex.rxjava3:rxandroid:3.0.0")
implementation("androidx.camera:camera-camera2:1.3.3")
implementation("androidx.camera:camera-lifecycle:1.3.3")
implementation("androidx.camera:camera-view:1.3.3")
implementation("androidx.camera:camera-core:1.3.3")
implementation("com.google.android.exoplayer:exoplayer-core:2.19.1")
/* end 相機相關 */
/* start 影像處理相關 */
//implementation("com.github.zynkware:Tiny-OpenCV:4.4.0-4")
implementation("com.quickbirdstudios:opencv-contrib:4.5.3.0")
/* end 影像處理相關 */
/* start 資料同步框架相關 */
implementation("com.tencent:mmkv:1.3.4")
/* end 資料同步框架相關 */
/* start 許可權相關 */
implementation("com.github.getActivity:XXPermissions:18.6")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.04.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest") // 動態許可權庫
/* end 許可權相關 */
/* start 測試相關 */
testImplementation("androidx.test.ext:junit:1.1.5")
testImplementation("androidx.test:rules:1.5.0")
testImplementation("androidx.test:runner:1.5.2")
testImplementation("androidx.test.espresso:espresso-core:3.5.1")
testImplementation("org.robolectric:robolectric:4.4")
/* end 測試相關 */
/* start tflite相關 */
implementation("androidx.window:window:1.2.0")
implementation("org.tensorflow:tensorflow-lite-task-vision:0.4.0")
implementation("org.tensorflow:tensorflow-lite-gpu-delegate-plugin:0.4.0")
implementation("org.tensorflow:tensorflow-lite-gpu:2.9.0")
/* end tflite相關 */
}
- 編輯app的初始化程式碼以初始化模組
BootApp.kt
class BootApp:Application() {
companion object {
private const val FILE_SIZE = 1000000L
private const val FILE_QUALITY = 100
private val FILE_TYPE = Bitmap.CompressFormat.JPEG
}
override fun onCreate() {
super.onCreate()
/* start 初始化實時識別模組 */
GrapeRealtimeDetect.init()
Log.d("GrapeRealtimeDetect","初始化完成!")
/* end 初始化實時識別模組 */
}
}
效果
模組中呼叫CameraX相機 |
---|