ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Android美颜相机毕设实战:Camera2+OpenCV实现实时磨皮美白与瘦脸大眼

Android美颜相机毕设实战:Camera2+OpenCV实现实时磨皮美白与瘦脸大眼 简介一份华南农业大学毕业设计项目源码实现类似美颜相机/美图秀秀的Android应用面向计算机专业学生、Java及Android开发学习者可作为毕业设计或课程设计的完整参考方案。项目覆盖图像采集、滤镜渲染、人像美化等核心模块包含丰富的界面布局与业务逻辑代码可帮助理解移动端图像处理的实现思路。压缩包共2000个文件其中1216个xml布局文件、378个json配置、352个java源码另有shell、properties、c及md等辅助文件整体大小约19.43MB目录结构完整便于对照学习。源码基于Java与Android平台涉及GPUImage等图像处理框架适合需要快速搭建同类应用或扩展美颜功能的开发者使用。目前已有118人学习下载具有不错的参考价值。1. 别把美颜相机毕设做成滤镜相册先想清楚链路毕设答辩时评审最常追问的往往是那句“你这个磨皮用的是什么原理”。很多同学拿到“类似美颜相机”的 Android 题目后先花两三周搭界面、接相机、做相册最后塞一个第三方渲染引擎了事结果演示时被问两句就露馅。这个题目的本质不是做“滤镜相册”而是把一条实时图像处理链路完整跑通相机采集、预览、人脸检测、磨皮美白、瘦脸大眼、特效滤镜最后输出到屏幕或保存成图片。它适合有一点 Java/Kotlin 基础、愿意碰 Camera2 和 OpenCV 的读者工程量适中、算法有讲头是图像处理方向里性价比很高的毕设选题。2. 工程骨架与选型Camera2 TextureView 是最稳的组合2.1 先把一条美颜链路画清楚再决定代码往哪放常见的做法是先把处理流程拆成独立阶段每个阶段只做一件事阶段之间用“接口”连接而不是把所有逻辑堆在一个 Activity 里。典型链路是这样的相机传感器输出 YUV 帧 → 显示到 TextureView / GLSurfaceView 预览 → 从帧里检测人脸框 → 提取关键点 → 对整帧做磨皮、美白 → 以关键点为圆心做瘦脸、大眼 → 叠加实时滤镜GLSL→ 屏幕预览或拍照保存。画链路的价值在于你能在动手前就确定每个模块的输入输出。比如“磨皮模块”接收一张 Mat返回一张 Mat“滤镜模块”接收纹理 ID输出到屏幕。这样后续替换算法、调参、加功能都不需要重写相机部分。另一个好处是答辩时要画架构图你已经有一张现成的模块图不需要临时编。我一般会把“预览显示”和“图像处理”分开预览显示用 TextureView 承接相机帧图像处理在后台线程里跑 OpenCV Mat。如果混在一起UI 线程会被拖慢滑动滤镜参数时会明显卡顿。这个划分几乎决定了整个工程最后能不能稳定跑起来。2.2 为什么用 Camera2 而不是 CameraXOpenCV 而不是 RenderScript选型是毕设里最容易被追问的部分理由要说得出。CameraX 确实简单PreviewView 开箱即用生命周期自动绑定写几十行就能看到预览。但它有两个问题一是拿 YUV 帧做处理要走 ImageAnalysis把 Image 转成 OpenCV Mat 还要自己写转换二是对预览方向、sensor 旋转等细节控制比较黑盒答辩时“相机是怎么打开的”这个问题很难讲透。Camera2 代码量大但每一步都透明openCamera、CaptureSession、CaptureRequest 全是显式调用评审问起来你能一条条答清楚。算法侧要注意 RenderScript 已经被 Google 标记废弃新工程不要碰。OpenCV Android SDK 提供人脸检测、矩阵运算、颜色空间转换磨皮美白这类需求直接调用即可。人脸关键点如果要求更高比如 68 点、106 点可以接一个轻量级模型但纯毕设场景下 OpenCV 自带级联检测器已经够用。渲染侧选 GLSurfaceView GLSL滤镜用着色器在 GPU 上跑比在 CPU 上逐像素快两个数量级这也是“实时美颜”能成立的原因。2.3 跑通预览的最小步骤Gradle、权限、开相机和方向修正先加依赖版本号以 Android Studio 能拉到的最新稳定版为准不写死implementation(org.opencv:opencv) // 在 AS 的 SDK Manager 里同步最新版本AndroidManifest 里申报相机权限uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue /打开相机的最小代码注意回调要跑在后台线程private fun openCamera() { val manager getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraId cameraManager.cameraIdList[if (useFront) 1 else 0] if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { return } manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { device camera createCameraSession() } override fun onDisconnected(camera: CameraDevice) { camera.close() device null } override fun onError(camera: CameraDevice, error: Int) { camera.close() device null } }, backgroundHandler) }这里backgroundHandler必须绑定到HandlerThread的 Looper不能new Handler()跑主线程否则相机回调会把主线程堵住。openCamera指定前后摄时cameraIdList的下标并不总是 0 后 1 前要看CameraCharacteristics.LENS_FACING判断这也是一个经典踩坑点。拿到CameraDevice后创建会话把 TextureView 的 SurfaceTexture 设成预览目标private fun createCameraSession() { val texture textureView.surfaceTexture ?: return texture.setDefaultBufferSize(PREVIEW_WIDTH, PREVIEW_HEIGHT) // 用 1280x720别用 4K val surface Surface(texture) val request device?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)?.apply { addTarget(surface) set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) } device?.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { session.setRepeatingRequest(request!!.build(), null, null) } override fun onConfigureFailed(session: CameraCaptureSession) { // 提示用户相机不可用不要静默吞掉 } }, backgroundHandler) }预览方向修正是另一个必做项。相机的 Sensor 方向和屏幕方向不一致竖屏预览会横过来。常见做法是读SENSOR_ORIENTATION再结合当前屏幕旋转角度做补偿val matrix Matrix() val displayRotation windowManager.defaultDisplay.rotation val sensorOrientation characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0 val degrees when (displayRotation) { Surface.ROTATION_90 - 90 Surface.ROTATION_180 - 180 Surface.ROTATION_270 - 270 else - 0 } matrix.postRotate((sensorOrientation - degrees 360) % 360) if (useFront) matrix.postScale(-1f, 1f) // 前置要镜像 textureView.setTransform(matrix)这段代码能解决 90% 的方向问题。剩下的那种画面旋转 180 度多半是TEMPLATE_PREVIEW的传感器像素格式问题但按上面的写法基本不会遇到。预览跑通后整个工程就有了“地基”后续做算法都在这条链路上加。3. 磨皮美白怎么落地从肤色检测到 SeekBar 参数3.1 磨皮不是无脑模糊肤色蒙版 高斯模糊 羽化混合很多人的第一版磨皮是整张图高斯模糊结果头发丝、眉毛、背景全糊了脸还像贴了层塑料。正确做法是只对皮肤区域做模糊而且边缘要过渡自然。完整思路是先检测肤色区域生成蒙版对原图做高斯模糊用蒙版作为权重把原图和模糊图线性混合。蒙版越白的地方模糊的权重越高。实现代码如下直接输入一张 OpenCV Mat返回磨皮后的 Matfun smoothSkin(src: Mat, strength: Double): Mat { // 1. 转 YCrCb检测肤色 val ycrcb Mat() Imgproc.cvtColor(src, ycrcb, Imgproc.COLOR_RGB2YCrCb) val mask Mat() Core.inRange(ycrcb, Scalar(0.0, 133.0, 77.0), Scalar(255.0, 173.0, 127.0), mask) // 2. 羽化蒙版避免皮肤边缘出现硬边 Imgproc.GaussianBlur(mask, mask, Size(25.0, 25.0), 0.0) // 3. 对原图做高斯模糊 val blur Mat() Imgproc.GaussianBlur(src, blur, Size(9.0, 9.0), 0.0) // 4. 把蒙版转成 0~1 权重并扩展成三通道 val maskF Mat() mask.convertTo(maskF, CvType.CV_32F, 1.0 / 255.0) val w Mat() Core.merge(listOf(maskF, maskF, maskF), w) // 5. 原图和模糊图也转浮点 val srcF Mat() src.convertTo(srcF, CvType.CV_32F, 1.0 / 255.0) val blurF Mat() blur.convertTo(blurF, CvType.CV_32F, 1.0 / 255.0) // 6. 用权重混合out blur * w * strength src * (1 - w * strength) val weight Mat() Core.multiply(w, Scalar(strength, strength, strength), weight) val oneMinusWeight Mat() Core.subtract(Scalar(1.0, 1.0, 1.0), weight, oneMinusWeight) val blurPart Mat() Core.multiply(blurF, weight, blurPart) val srcPart Mat() Core.multiply(srcF, oneMinusWeight, srcPart) val outF Mat() Core.add(srcPart, blurPart, outF) val out Mat() outF.convertTo(out, CvType.CV_8UC3, 255.0) return out }这段代码里最关键的是蒙版的羽化那一步。如果不做GaussianBlur(mask, mask, ...)蒙版边缘会是锐利的硬边脸上会出现一块一块的“补丁感”。强度控制在 0.3~0.7 之间超过 0.8 就会变成塑料脸。模糊半径 9 是 720p 下的经验值分辨率翻倍时要对应增大。3.2 美白在 YCrCb 空间做提亮 Y、微调 Cb防过曝美白和磨皮是两件事磨皮解决“皮肤不平”美白解决“皮肤不够白”。美白在 YCrCb 空间做最直接Y 通道提亮Cr/Cb 做小幅偏移让肤色不发黄、不发绿。实现很轻量fun whitening(src: Mat, strength: Double): Mat { val ycrcb Mat() Imgproc.cvtColor(src, ycrcb, Imgproc.COLOR_RGB2YCrCb) val channels ArrayListMat() Core.split(ycrcb, channels) // Y 通道提亮最多提 20% val gain 1.0 0.2 * strength Core.multiply(channels[0], Scalar(gain), channels[0]) // 高光保护超过 240 的像素压回 240防止过曝 Core.min(channels[0], Scalar(240.0), channels[0]) // Cb 通道降一点肤色调得更透 Core.subtract(channels[2], Scalar(10.0 * strength), channels[2]) Core.merge(channels, ycrcb) Imgproc.cvtColor(ycrcb, src, Imgproc.COLOR_YCrCb2RGB) return src }strength范围建议 0~1Y 通道增益0.05~0.2Cb 偏移5~10。超过这个范围高光区域会变成一块死白尤其是额头和鼻梁。高光保护那一步不能省很多人美白翻车就是栽在这里。3.3 三组核心参数的范围与映射预览时参数放轻一点拍照出图时调重一点这是美颜相机的通用逻辑。以下是 720p 预览分辨率下的经验值参数推荐范围说明高斯模糊半径5~15 像素半径越大越糊超过 15 脸部纹理全丢磨皮强度0.3~0.7预览用 0.3~0.4拍照用 0.5~0.7肤色检测 Cr133~173数值过大会把背景的红墙、红衣服也当成脸肤色检测 Cb77~127与 Cr 配合使用两个范围收敛一点更稳美白 Y 增益0.05~0.2超过 0.2 高光区域会过曝锐化强度0~0.3磨皮后加一点锐化找回纹理SeekBar 的滑动值一般是 0~100传到算法里时除以 100 再乘一个系数。注意不能把 SeekBar 的原始值直接当 strength 用否则调到一半效果就爆表了。4. 瘦脸大眼和实时滤镜局部变形 GLSL 着色器4.1 瘦脸大眼用“局部液化”就够了以关键点为圆心的像素偏移瘦脸大眼在美颜相机里属于“局部变形”算法核心是让一个圆域内的像素按权重偏移。商业产品用的是液化Liquify和网格变形毕设实现一个简化版完全够讲。思路是选一个中心点瘦脸用脸中心大眼用眼睛中心给定一个半径圆内像素距离中心越近偏移越大圆外不处理。下面是 Java 写法直接遍历目标区域内像素反向采样void localWarp(Mat src, Mat dst, Point center, double radius, double strength) { dst dst.zeros(src.size(), src.type()); int x0 (int) Math.max(0, center.x - radius); int x1 (int) Math.min(src.cols() - 1, center.x radius); int y0 (int) Math.max(0, center.y - radius); int y1 (int) Math.min(src.rows() - 1, center.y radius); for (int y y0; y y1; y) { for (int x x0; x x1; x) { double dx x - center.x; double dy y - center.y; double dist Math.sqrt(dx * dx dy * dy); if (dist radius) { dst.put(y, x, src.get(y, x)); continue; } // 权重越靠近中心偏移越大边缘平滑过渡到 0 double w 1.0 - (dist * dist) / (radius * radius); double srcX x - dx * w * strength; // strength 为正像素向中心收拢 double srcY y - dy * w * strength; // 双线性插值采样避免锯齿 int x0s (int) srcX, y0s (int) srcY; double fx srcX - x0s, fy srcY - y0s; double[] p00 src.get(y0s, x0s); double[] p10 src.get(y0s, Math.min(x0s 1, src.cols() - 1)); double[] p01 src.get(Math.min(y0s 1, src.rows() - 1), x0s); double[] p11 src.get(Math.min(y0s 1, src.rows() - 1), Math.min(x0s 1, src.cols() - 1)); double[] out new double[p00.length]; for (int c 0; c out.length; c) { out[c] p00[c] * (1 - fx) * (1 - fy) p10[c] * fx * (1 - fy) p01[c] * (1 - fx) * fy p11[c] * fx * fy; } dst.put(y, x, out); } } }这个写法里strength为正数时像素向中心收拢用于瘦脸如果对眼睛中心做负向偏移像素从中心向外推就放大眼睛。半径一般取脸宽的 0.4~0.6 倍太大鼻子会变形太小看不出效果。注意整帧遍历会卡实际只处理以关键点为中心裁出来的一块 ROI能省掉 70% 的计算量。4.2 滤镜走 GLSL一段可以放进 GLSurfaceView 的片元着色器滤镜如果也用 OpenCV 逐像素调色预览帧率会很难看。正确做法是交给 GPU把 OpenCV 处理后的 Mat 转成纹理上传到 GLSurfaceView片元着色器里做颜色映射。下面是一段基础的美颜滤镜着色器包含提亮、轻微去饱和和色调微调#version 300 es precision mediump float; uniform sampler2D u_texture; uniform float u_brightness; in vec2 v_texCoord; out vec4 fragColor; void main() { vec4 color texture(u_texture, v_texCoord); // 亮度调整 color.rgb u_brightness; // 轻微去饱和让肤色更柔和 float gray dot(color.rgb, vec3(0.299, 0.587, 0.114)); color.rgb mix(color.rgb, vec3(gray), 0.15); fragColor color; }u_brightness从 Java 侧通过glUniform1f传入取值 0.02~0.06 就好太大整体发白。这里有个关键点Camera2 传过来的纹理是 OES 外部纹理GL_TEXTURE_EXTERNAL_OES着色器第一个版本要用#extension GL_OES_EGL_image_external : require统一在工程里做一层纹理拉直后续所有滤镜就都能复用。如果你用 GLSurfaceView 配合SurfaceTexture做相机预览这块可以省掉OpenCV的转Bitmap过程直接显示。4.3 720p 预览、隔帧检测、ROI 处理实时性从这三处抠实时美颜的瓶颈从来不在滤镜而在人脸检测和局部变形。常见做法是三个约束预览处理分辨率锁 720p不要碰 4K人脸检测每 5 帧做一次关键点滞后 100ms 人眼看不出问题局部变形只处理人脸 ROI而不是整帧。还有一条容易被忽略OpenCV 的 Mat 处理完要立刻 release否则内存会持续上涨。我用这套约束在“某千元测试机”上把处理帧率稳定在 25 帧以上而第一版全帧处理只有 12 帧。差距就来自 ROI 和隔帧检测这两处。答辩时被问“你的算法为什么快”能把这三点答出来比背一堆理论更有说服力。5. 美颜相机常见问题排查黑屏、镜像与内存泄漏5.1 预览方向颠倒或横屏现象竖屏拿着手机预览画面是横过来的或者翻转 90 度后才正常。原因相机 Sensor 的默认方向和屏幕方向不一致Camera2 不会帮你自动旋转。很多教程只给了设置TEMPLATE_PREVIEW的代码没给方向补偿预览竖起来是运气的。解决按第 2.3 节的sensorOrientation - displayRotation公式对TextureView做setTransform(Matrix)。注意前置和后置的旋转方向相反前置还要额外镜像。这个方法同时管预览显示和后续拍照方向存图前对Mat做一次Core.flip(mat, mat, 1)即可。5.2 权限弹窗后第一次打开黑屏第二次正常现象首次安装进 App弹出相机权限授权框点“允许”后预览是黑色的杀掉重进一次就正常。原因onResume()里先执行了openCamera()但权限授权结果还没回来checkSelfPermission返回拒绝代码直接return了。之后权限回调里也没有再次调用openCamera于是相机一直没打开。解决把打开流程改成“条件触发”而不是“顺序触发”。权限授权成功回调里调用openCamera()TextureView 的SurfaceTextureListener回调里也检查权限两边都满足才真正打开。最稳妥的是写一个tryStartCamera()内部判断permissionGranted surfaceReady两个条件都满足执行openCamera。这一个改动就能消掉 80% 的黑屏问题。5.3 前置自拍照片左右颠倒现象预览里看自己是正常的拍出来的照片却是镜像的和日常自拍习惯相反。原因前置相机的画面本身是镜像的你为了让预览符合习惯做了Matrix.postScale(-1f, 1f)但保存照片时用的是ImageReader的原始帧没有做同样的翻转。解决预览和存储是两条线。预览方向用 Matrix 修正保存帧在写入文件前对 Mat 做水平翻转Core.flip(rotatedMat, flippedMat, 1)。同时注意焦点的位置不会因为翻转而偏移如果是固定点对焦翻转后还要把焦点坐标映射到翻转后的坐标系里。这个坑在自拍场景下一抓一个准评审演示时经常 crash 在这。5.4 反复进出页面内存暴涨最终闪退现象从列表页进相机页再退出循环五六次内存曲线一路上涨最后 OOM 闪退。原因CameraDevice没有 close、SurfaceTexture没有释放、OpenCV Mat 没有 recycle。尤其CameraDevice不 close下一次openCamera会直接抛CAMERA_IN_USE_ERROR。这条在低端机型上是必踩的坑。解决onPause()里执行device?.close()并置空onDestroy()里textureView.surfaceTextureListener null后台线程用handlerThread.quitSafely()。另外你在第 4.1 节 localWarp 里每次dst.create出来的 Mat用完必须release。我见过最夸张的泄漏是把磨皮中间变量写在while(true)预览循环里不释放两分钟内存涨了 300MB。检查泄漏最快的方式是开 Android Studio 的 Memory Profiler反复进出页面时盯着Mat count。5.5 磨皮过度“塑料脸”现象磨皮后皮肤纹理全无像贴了一张塑料面具尤其额头和脸颊反光严重。原因模糊半径和强度都用得太狠肤色蒙版还把高光区域全覆盖进去了。很多同学的调参逻辑是“效果越明显越好”但美颜相机的评审既看效果也看自然度。解决把高斯模糊半径控制在 9 像素以内强度降到 0.4蒙版羽化半径加大。磨皮之后加一道锐化把高频细节叠回去 15%~20%皮肤纹理就不会完全消失。还有个技巧先做一遍小半径磨皮再叠加一层更小半径的磨皮比一次大半径更自然。这套“局部小剂量多次处理”的思路也可以直接写进论文比单纯讲滤波要有内容。6. 给毕设加分离线精修对比 帧耗时统计回答“你的美颜快不快”实时预览为了流畅只能跑 720p 和轻量参数但答辩时最加分的效果恰恰是“对比”同一张原图左边是原始效果右边是离线精修后的效果。我建议单独加一个“拍照后离线精修页”把拍照得到的全尺寸原图跑一遍完整磨皮美白再叠加瘦脸大眼和滤镜和原图左右分屏对比。这个页面不要求实时算法可以跑重参数效果会比预览好一大截评审的视觉冲击力也强得多。第二件事是把自己算法的耗时测出来用数据回答“实时性够不够”。核心做法是在处理管线外包一层计时val t0 System.nanoTime() val outMat pipeline.process(frameMat) // 磨皮美白瘦脸滤镜 val costMs (System.nanoTime() - t0) / 1_000_000.0 runOnUiThread { tvFps.text 帧耗时 ${costMs}ms / 帧率 ${(1000.0 / costMs).toInt()}fps }连续跑 30 帧取平均把数据整理成表格720p 预览平均耗时 XXms全部美化开启时帧率 XXfps这个数据放进论文的“性能分析”一节非常有用。注意测的时候关掉系统省电模式用同一台测试机跑完整流程别用模拟器数据模拟器的帧率没有参考价值。我的习惯是先把实时预览做到流畅再单独做离线精修页这样即使答辩现场预览翻车也能用离线对比撑住场面。这也是我从模拟项目 X 的答辩经验里学到的教训——当时我把所有美化都堆在预览链路里低温环境下低端机掉到 12 帧现场演示很被动。后来改成 ROI 处理加隔帧检测才稳定在 25 帧以上。希望你做这个方向时少走这段弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表