magicCamera—程序员文档
magicCamera — 程序员文档本文档面向开发者和贡献者说明项目的技术架构、算法实现和开发流程。普通用户请参考我其它文章(magicCamera—魔术师的 AR 卡牌应用)。github地址: https://github.com/Anyuer9837/magicCamera 项目概述magicCamera是一个 Android 应用利用摄像头、OpenCV 图像处理和 CameraX 框架实现实时卡牌检测和 AR 替换。核心功能实时卡牌检测使用 Canny 边界检测和图像处理算法检测视野中的卡牌AR 卡牌替换将检测到的卡牌实时替换为指定的卡牌图像交互式选牌流程通过屏幕触摸进行两步选牌先选花色再选点数⚙️动态参数调节长按快门按钮呼出参数面板实时调整检测算法参数双摄像头支持前后摄像头切换每个摄像头独立保存参数配置️ 技术架构核心库和框架库/框架用途CameraX相机捕获和预览管理OpenCV图像处理和卡牌检测Kotlin Coroutines异步任务处理AndroidX AppCompatMaterial Design UI 组件SharedPreferences参数持久化存储核心类结构MainActivity.kt ├── 相机管理 │ ├── ProcessCameraProvider │ ├── CameraSelector (前/后) │ └── ImageAnalysis │ ├── 图像处理 │ ├── imageProxyToNv21() [YUV → NV21 转换] │ ├── normalizeFrame() [颜色空间转换和旋转] │ └── detectObjectContours() [核心检测逻辑] │ ├── 卡牌替换 │ ├── replaceDetectedCard() [透视变换] │ └── loadSelectedCardImage() [加载卡牌图片] │ ├── UI 交互 │ ├── shutterBtn.setOnClickListener() │ ├── switchCameraBtn.setOnClickListener() │ ├── detectionView.setOnTouchListener() │ └── showSettingsDialog() [参数调节面板] │ └── 状态管理 └── magicState (0待机, 1选花色, 2选点数, 3激活贴图)多线程架构┌─────────────────────────────────────┐ │ Main Thread (UI) │ │ - UI 渲染 │ │ - 用户交互处理 │ └────────────┬────────────────────────┘ │ ┌────────┴────────┐ │ │ ┌───▼──────┐ ┌────▼────────────┐ │ Processing │ CameraExecutor │ │ Thread │ │ │ (HandlerThread)│ (Executors) │ │ │ │ │ - OpenCV 处理 │ - 相机帧捕获 │ │ - Mat 变换 │ - ImageProxy │ └────────────┘ └─────────────────┘三层执行模型CameraExecutor→ ImageAnalysis 获取相机帧ProcessingHandler→ 后台线程执行 OpenCV 算法MainThread→ UI 线程更新预览和参数 图像处理管线完整流程ImageProxy (NV21) ↓ imageProxyToNv21() ─── YUV420 格式转 NV21 字节数组 ↓ normalizeFrame() ─── 颜色空间转换 旋转 镜像 ├─ CVTColor: YUV2RGB_NV21 ├─ Core.rotate(ROTATE_90_CLOCKWISE) └─ Core.flip (前置摄像头) ↓ detectObjectContours() ├─ CVTColor: RGB2GRAY ├─ GaussianBlur ├─ Canny ├─ MorphologyEx (MORPH_CLOSE) ├─ FindContours └─ 卡牌识别与选择 ↓ replaceDetectedCard() ├─ Perspective Transform ├─ WarpPerspective └─ Mask Copy ↓ Mat → Bitmap → ImageView关键函数详解1.imageProxyToNv21(imageProxy: ImageProxy): ByteArray目的将 ImageProxy 转换为 NV21 格式字节数组privatefunimageProxyToNv21(image:ImageProxy):ByteArray{valyBufferimage.planes[0].buffer// Y planevaluBufferimage.planes[1].buffer// U planevalvBufferimage.planes[2].buffer// V planevalySizeyBuffer.remaining()valuSizeuBuffer.remaining()valvSizevBuffer.remaining()valnv21ByteArray(ySizeuSizevSize)yBuffer.get(nv21,0,ySize)vBuffer.get(nv21,ySize,vSize)// V 先uBuffer.get(nv21,ySizevSize,uSize)// U 后 (NV21 NV12 V/U 交换)returnnv21}关键点NV21 格式Y plane (V, U 交错)必须按照 V, U 顺序放置不是 U, V2.normalizeFrame(data: ByteArray): Mat目的标准化帧颜色转换、旋转、镜像privatefunnormalizeFrame(data:ByteArray):Mat{valyuvMat(previewHeightpreviewHeight/2,previewWidth,CvType.CV_8UC1)yuv.put(0,0,data)valrgbMat()Imgproc.cvtColor(yuv,rgb,Imgproc.COLOR_YUV2RGB_NV21)// NV21 → RGByuv.release()valrotatedMat()Core.rotate(rgb,rotated,Core.ROTATE_90_CLOCKWISE)// 旋转 90°rgb.release()if(lensFacingCameraSelector.LENS_FACING_FRONT){Core.flip(rotated,rotated,0)// 前置镜像}returnrotated}关键点CameraX 提供的帧默认是 90° 旋转的前置摄像头需要镜像flip axis 0 竖直翻转3.detectObjectContours(data: ByteArray)— 核心检测逻辑流程// 1. 颜色转换Imgproc.cvtColor(frame,gray,Imgproc.COLOR_RGB2GRAY)// 2. 高斯模糊去噪Imgproc.GaussianBlur(gray,blurred,Size(blurSize,blurSize),0.0)// 3. Canny 边界检测Imgproc.Canny(blurred,edges,cannyLower,cannyUpper)// 4. 形态学闭运算补缝valkernelImgproc.getStructuringElement(Imgproc.MORPH_RECT,Size(morphKernelSize,morphKernelSize))Imgproc.morphologyEx(edges,edges,Imgproc.MORPH_CLOSE,kernel)// 5. 轮廓提取valcontoursmutableListOfMatOfPoint()Imgproc.findContours(edges,contours,hierarchy,Imgproc.RETR_EXTERNAL,Imgproc.CHAIN_APPROX_SIMPLE)// 6. 卡牌识别选择最佳轮廓varbestContour:MatOfPoint?selectBestContour(contours)// 7. 角点提取val(tl,tr,br,bl)extractCorners(bestContour)// 8. 替换卡牌如果状态为 3if(magicState3){replaceDetectedCard(frame,tl,tr,br,bl)}卡牌识别算法前置摄像头策略中心优先if(lensFacingCameraSelector.LENS_FACING_FRONT){valdisthypot(minRect.center.x-frameCenterX,minRect.center.y-frameCenterY)if(distminCenterDist){minCenterDistdist bestContourcontour}}原理前置摄像头通常只能看到一张卡牌选择离画面中心最近的轮廓。后置摄像头策略形状面积筛选if(lensFacingCameraSelector.LENS_FACING_BACK){valwminRect.size.widthvalhminRect.size.heightvalratiow.coerceAtLeast(h)/w.coerceAtMost(h)// 宽高比valrectAreaw*hvalextentarea/rectArea// 面积填充度// 条件宽高比 1.3~1.9卡牌比例 填充度 73%if(ratioin1.3..1.9extent0.73){if(areamaxArea){maxAreaarea bestContourcontour}}}原理后置摄像头可能看到多张卡牌需要形状和面积双重筛选。角点提取算法前置摄像头使用和与差vartlpoints[0];vartrpoints[0];varblpoints[0];varbrpoints[0]varminSumDouble.MAX_VALUE;varmaxDiff-Double.MAX_VALUE;varminDiffDouble.MAX_VALUEfor(pinpoints){valsump.xp.y// x y左上最小右下最大valdiffp.x-p.y// x - y右上最大左下最小if(summinSum){minSumsum;tlp}// 左上if(diffmaxDiff){maxDiffdiff;trp}// 右上if(diffminDiff){minDiffdiff;blp}// 左下}brPoint(tr.xbl.x-tl.x,tr.ybl.y-tl.y)// 由对角线推导原理t l tltl:x y x yxy最小t r trtr:x − y x - yx−y最大b l blbl:x − y x - yx−y最小b r brbr: 平行四边形性质推导后置摄像头标准四点排序vartlpoints[0];vartrpoints[0];varbrpoints[0];varblpoints[0]varminSumDouble.MAX_VALUE;varmaxSum-Double.MAX_VALUEvarmaxDiff-Double.MAX_VALUE;varminDiffDouble.MAX_VALUEfor(pinpoints){valsump.xp.yvaldiffp.x-p.yif(summinSum){minSumsum;tlp}// 左上if(summaxSum){maxSumsum;brp}// 右下if(diffmaxDiff){maxDiffdiff;trp}// 右上if(diffminDiff){minDiffdiff;blp}// 左下}透视变换和替换privatefunreplaceDetectedCard(frame:Mat,tl:Point,tr:Point,br:Point,bl:Point){valsrcMatreplacementCardMat!!valwsrcMat.cols().toDouble()valhsrcMat.rows().toDouble()// 源图片的四个角valsrcPtsMatOfPoint2f(Point(0.0,0.0),Point(w,0.0),Point(w,h),Point(0.0,h))// 目标图片的四个角检测到的卡牌位置valdstPtsMatOfPoint2f(tl,tr,br,bl)// 计算透视变换矩阵valtransformMatrixImgproc.getPerspectiveTransform(srcPts,dstPts)// 应用变换valwarpedCardMat()Imgproc.warpPerspective(srcMat,warpedCard,transformMatrix,frame.size())// 创建遮罩只显示卡牌区域valmaskMat.zeros(frame.size(),CvType.CV_8UC1)valmaskPolygonlistOf(MatOfPoint(tl,tr,br,bl))Imgproc.fillPoly(mask,maskPolygon,Scalar(255.0))// 合成到原图warpedCard.copyTo(frame,mask)// 清理warpedCard.release()mask.release()transformMatrix.release()} 魔术状态机状态转移图┌──────────┐ │ 状态0 │ ◄──────┐ │ 待机 │ │ │ 不贴图 │ │ └────┬─────┘ │ │ 短按快门 │ ▼ │ ┌──────────┐ │ │ 状态1 │ │ │ 选花色 │ │ │ 2×2网格 │ │ └────┬─────┘ │ │ 触摸选花色 │ ▼ │ ┌──────────┐ │ │ 状态2 │ │ │ 选点数 │ │ │ 3×5网格 │ │ └────┬─────┘ │ │ 触摸选点数 │ ▼ │ ┌──────────┐ │ │ 状态3 │ │ │ 激活贴图 │ │ │ 实时替换 │ │ └────┬─────┘ │ │ │ └──────────────┘状态定义privatevarmagicState0// 0 待机 (不贴图)// 1 选花色中// 2 选点数中// 3 激活贴图privatevarselectedSuit-1// 0黑桃, 1红心, 2梅花, 3方块privatevarselectedRank-1// 1~12 A~12, 13 K卡牌 ID 计算finalCardId selectedSuit * 13 selectedRank 范围0~5152 张牌 黑桃0~12 (A~K) 红心13~25 (A~K) 梅花26~38 (A~K) 方块39~51 (A~K) 参数持久化SharedPreferences 设计privatefungetPrefPrefix():Stringif(lensFacingCameraSelector.LENS_FACING_FRONT)front_elseback_privatefunloadParamsForCurrentCamera(){valprefixgetPrefPrefix()blurSizesharedPrefs.getFloat(prefixblurSize,13.0f).toDouble()cannyLowersharedPrefs.getFloat(prefixcannyLower,0.0f).toDouble()cannyUppersharedPrefs.getFloat(prefixcannyUpper,150.0f).toDouble()morphKernelSizesharedPrefs.getFloat(prefixmorphKernelSize,30.0f).toDouble()}privatefunsaveParamsToLocal(){valprefixgetPrefPrefix()sharedPrefs.edit().apply{putFloat(prefixblurSize,blurSize.toFloat())putFloat(prefixcannyLower,cannyLower.toFloat())putFloat(prefixcannyUpper,cannyUpper.toFloat())putFloat(prefixmorphKernelSize,morphKernelSize.toFloat())apply()}}存储键front_blurSize,front_cannyLower,front_cannyUpper,front_morphKernelSizeback_blurSize,back_cannyLower,back_cannyUpper,back_morphKernelSize 防闪烁机制缓存机制privatevarlastTl:Point?nullprivatevarlastTr:Point?nullprivatevarlastBl:Point?nullprivatevarlastBr:Point?nullprivatevarmissedFrames1privatevalMAX_MISSED_FRAMES3// 检测失败时的处理if(bestContour!null){lastTltl;lastTrtr;lastBlbl;lastBrbr missedFrames0}else{if(lastTl!nullmissedFramesMAX_MISSED_FRAMES){missedFramesif(magicState3)replaceDetectedCard(frame,lastTl!!,lastTr!!,lastBr!!,lastBl!!)}else{clearCache()}}原理连续检测失败最多 3 帧使用最后一次成功的卡牌位置继续替换保证 AR 贴图的连贯性 项目结构magicCamera/ ├── app/ │ ├── src/main/ │ │ ├── java/com/yuer/magicCamera/ │ │ │ └── MainActivity.kt # 主应用文件完整实现 │ │ ├── res/ │ │ │ ├── drawable/ │ │ │ │ ├── card_0.png ~ card_51.png # 52 张卡牌图片 │ │ │ │ └── ... │ │ │ ├── layout/ │ │ │ │ └── activity_main.xml # UI 布局 │ │ │ └── values/ │ │ │ └── strings.xml # 字符串资源 │ │ └── AndroidManifest.xml │ ├── build.gradle.kts # App 模块配置 │ └── proguard-rules.pro ├── gradle/ │ └── libs.versions.toml # 依赖版本管理 ├── build.gradle.kts # 根项目配置 ├── settings.gradle.kts ├── gradlew / gradlew.bat ├── gradle.properties ├── README.md # 普通用户文档 ├── DEVELOPER.md # 本文件 └── local.properties 开发环境和构建系统要求SDK最低 API 21 (Android 5.0)目标 API 34 (Android 14)JDKJava 11Gradle7.0依赖关键库在gradle/libs.versions.toml中定义androidx.camera:camera-core— CameraX 核心androidx.camera:camera-camera2— Camera2 实现androidx.camera:camera-lifecycle— 生命周期绑定org.opencv:opencv-android— OpenCVandroidx.appcompat:appcompat— Material Design编译和运行# 克隆项目gitclonerepository-urlcdmagicCamera# 编译./gradlew build# 安装到连接的设备/模拟器./gradlew installDebug# 直接运行./gradlew installDebugAndRun准备卡牌资源需要 52 张卡牌图片标准的 52 张牌在app/src/main/res/drawable/下放置图片文件命名card_0.png~card_51.png分配方案花色顺序黑桃(0~12)、红心(13~25)、梅花(26~38)、方块(39~51) 每花色内1A、2~122~12、13K推荐图片规格分辨率360×504px或比例 5:7格式PNG支持透明度文件大小50~100KB/张调试本地运行# 启用 Android Studio debugger./gradlew installDebug日志输出使用 Logcat 查看运行时日志adb logcat|grepmagicCamera发布版本# Release 编译需要签名配置./gradlew assembleRelease 代码风格和贡献指南代码规范语言Kotlin格式遵循 Kotlin 风格指南命名函数/变量camelCase常量UPPER_SNAKE_CASE类名PascalCase贡献流程Fork 项目创建 feature 分支git checkout -b feature/amazing-feature提交更改git commit -m Add amazing feature推送分支git push origin feature/amazing-feature提交 Pull Request 常见问题和故障排除问题 1OpenCV 初始化失败E/OpenCVLoader: Cannot load info about opencv_java4 library.解决确保项目中引入了org.opencv:opencv-android检查OpenCVLoader.initDebug()调用问题 2卡顿或帧率过低原因图像处理线程阻塞Mat 对象未及时释放导致内存泄漏Canny 参数过激进解决检查detectObjectContours()中所有 Mat 是否正确release()使用AtomicBoolean防止重复处理调整算法参数问题 3前置摄像头镜像不对检查normalizeFrame()中的镜像逻辑if(lensFacingCameraSelector.LENS_FACING_FRONT){Core.flip(rotated,rotated,0)// 0 竖直翻转}问题 4内存泄漏检查清单所有 Mat 对象是否在使用后release()ImageProxy 是否在 analyzer 中关闭Handler/Thread 是否在 onDestroy 中关闭 参考资源Android CameraX 官方文档OpenCV Java API 文档Android Architecture 最佳实践Kotlin Coroutines 文档 技术支持如有开发相关问题欢迎提交 Issue 或 Discussion。Happy Coding!

相关新闻

最新新闻

日新闻

周新闻

月新闻