Android美颜相机毕设实战:CameraX+OpenCV实现磨皮美白瘦脸

发布时间:2026/10/20 22:41:47
Android美颜相机毕设实战:CameraX+OpenCV实现磨皮美白瘦脸
简介华南农业大学毕业设计项目实现一个类似美颜相机的安卓应用基于Java语言开发可帮助计算机专业本科生快速掌握图像处理类应用的完整实现流程。尤其适合毕业设计选题涉及相机、美颜、滤镜或实时渲染的学生也可作为课程设计和安卓进阶开发的参考资料。压缩包共包含两千个文件其中xml文件一千二百一十六个、json文件三百七十八个、Java源文件三百五十二个另有sh脚本、properties配置、c源码及说明文档等压缩包大小约十九点四三兆字节目录按功能模块组织便于检索和二次开发。项目完整覆盖相机预览、YUV数据解码、图像滤镜工具封装、美颜特效处理等关键环节从界面交互到底层算法均有对应代码直观呈现美颜相机应用的工程结构和技术细节。已有一百一十八人学习下载整体体量适中、资料集中既可支撑毕业设计项目框架也能作为安卓图像处理学习的基础参考。1. 毕设选这个题到底在做什么每年毕业季安卓方向的毕设题目里“仿美颜相机 / 仿美图秀秀”出现频率一直很高。原因其实很直白这类 app 功能链条完整、界面效果直观、技术点覆盖广从相机采集、人脸检测、图像算法到性能优化全都能沾上工作量可控答辩时演示效果又足够亮眼。但很多同学拿到题目后容易卡在同一个地方——拿着相机预览画面不知道第一步该干什么或者以为美颜就是套个高斯模糊结果做出来要么是“整张脸糊成一片”要么是“磨皮和没磨一样”。这篇文章就按我实际做过的一个毕业设计项目来讲实现一个类似美颜相机的 Android app核心功能包括实时预览美颜、拍照保存、相册回显和基础编辑。全程用 Kotlin CameraX OpenCV 人脸关键点检测来实现不依赖任何商业美颜 SDK。整个项目从零到能跑大概需要 6 到 8 周如果你已经有一段时间的 Android 开发经验代码部分可以直接照着抄重点是理解每一步的选型理由和参数设置因为这些才是答辩时老师真正会追问的东西。2. 先定技术方案为什么是 CameraX OpenCV 人脸关键点2.1 整体架构和选型逻辑这个项目不是从零开始发明一个美颜引擎而是把成熟的开源组件按正确的姿势组合起来。常见的做法是三个核心模块相机采集用 CameraX、图像处理用 OpenCV、人脸检测用 FaceMesh 或者 dlib 的模型。选型的关键是“能跑通”和“能讲清楚原理”而不是追求最新最炫的框架。我最终定的方案是这样的模块技术选型理由相机预览与拍照CameraX生命周期感知、适配简单、官方维护图像处理OpenCV Android SDK磨皮、美白、瘦脸都有成熟算法人脸检测ML Kit Face Detection免费、准确率高、能返回关键点坐标UIJetpack Compose写滤镜面板和参数滑杆比 XML 快架构MVVM ViewModel答辩时架构是加分项这里有几个选择上的取舍。CameraX 相比 Camera2 的优势在于你不用写大量的状态机代码预览、拍照、分析三个 UseCase 的组合非常直观。ML Kit 的人脸检测虽然需要依赖 Google Play 服务但它在本地也能跑而且能返回 100 多个关键点坐标对“瘦脸 大眼”这种需要精准定位的功能来说比 OpenCV 自带的 Haar 级联检测器靠谱得多。注意如果你做的是“离线可用”这个卖点需要把 ML Kit 的模型打包进 APK具体做法是加com.google.mlkit:face-detection依赖然后在 manifest 里指定com.google.mlkit.vision.DEFAULT为local。2.2 项目结构和依赖配置在 build.gradleapp 模块里加上这些依赖dependencies { // CameraX 核心 implementation androidx.camera:camera-core:1.3.4 implementation androidx.camera:camera-camera2:1.3.4 implementation androidx.camera:camera-lifecycle:1.3.4 implementation androidx.camera:camera-view:1.3.4 // ML Kit 人脸检测 implementation com.google.mlkit:face-detection:16.1.7 // OpenCV implementation project(:opencv) // 协程 implementation org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 }camerasax 那个库建议跳过它的滤镜 API 不够底层做美颜时拿不到完整帧数据最后还是得自己用 ImageAnalysis 处理。OpenCV 我用的是官网下载的 Android SDK然后作为 module 引入比用远程依赖省去很多 native 库冲突的麻烦。2.3 为什么要自己写算法而不是用现成 SDK答辩时最容易暴露的问题就是“用了 xxx 美颜 SDK 一行代码搞定”。老师一定会追问SDK 底层做了什么你有没有看过它的效果原理所以这个题目真正的分数点在于你能否在白板上画出“高斯模糊 → 边缘保留 → 图像融合”这条管线并且说出每个步骤的参数含义。所以我明确一点这个项目的定位不是做一个能和商业 app 打对台的产品而是做一个“原理讲得清楚、效果看得见”的教学型实现。磨皮用双边滤波bilateral filter美白用亮度曲线调整瘦脸用局部平移变形——这三个算法足够你写出一篇有深度的毕业论文。3. 相机模块预览、拍照与帧数据流转3.1 从权限申请到预览画面出现的完整链路相机模块是美颜 app 的地基。你要处理的不只是“打开摄像头”还有权限申请、旋转角度适配、帧数据回调、拍照存储。第一步把预览跑起来先不急着加美颜。class CameraViewModel : ViewModel() { private var imageCapture: ImageCapture? null private val _isCameraReady MutableStateFlow(false) fun startCamera(lifecycleOwner: LifecycleOwner, previewView: PreviewView) { val preview Preview.Builder() .setTargetResolution(Size(1080, 1920)) .build() val analysis ImageAnalysis.Builder() .setTargetResolution(Size(480, 640)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() val capture ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() val cameraProviderFuture ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ val provider cameraProviderFuture.get() val cameraSelector CameraSelector.DEFAULT_BACK_CAMERA provider.bindToLifecycle( lifecycleOwner, cameraSelector, preview, analysis, capture ) preview.setSurfaceProvider(previewView.surfaceProvider) imageCapture capture }, ContextCompat.getMainExecutor(context)) } }逻辑说明预览的分辨率和分析的分辨率是分开设置的。预览用 1080p 保证清晰度分析的 480x640 是为了让人脸检测和图像处理的计算量可控——你不需要把每一帧都放到 2K 分辨率下去磨皮那样低端机会直接卡死。STRATEGY_KEEP_ONLY_LATEST策略表示分析器只处理最新的一帧中间帧直接丢掉这是保证实时性的关键设置。参数说明setTargetResolution(Size(480, 640))这个值不要随意调大OpenCV 的双边滤波在 480x640 上大概耗时 10ms到了 1080p 会飙到 60ms 以上实时预览直接掉到 15 帧以下。如果你想提高分析分辨率就必须同步优化你的算法不然预览就废了。3.2 拍照与保存YUV 转 Bitmap 的坑拍照要解决的核心问题是CameraX 给的是 YUV_420_888 格式的图像但 OpenCV 的Imgproc函数只处理 RGBA 或 BGR 格式。你需要写一个 YUV 转 Bitmap 的工具方法这个转换的耗时和内存占用会直接影响拍照速度。fun yuvToBitmap(image: Image, rotationDegrees: Int): Bitmap { val buffer image.planes[0].buffer val width image.width val height image.height val ySize buffer.remaining() val uvBuffer image.planes[2].buffer val uBuffer image.planes[1].buffer val uvSize uvBuffer.remaining() val nv21 ByteArray(ySize uvSize) buffer.get(nv21, 0, ySize) // 将 U 和 V 通道交错排列为 NV21 格式 val uArr ByteArray(uvSize) val vArr ByteArray(uvSize) uBuffer.get(uArr, 0, uvSize) vBuffer.get(vArr, 0, uvSize) for (i in 0 until uvSize step 2) { nv21[ySize i] vArr[i / 2] nv21[ySize i 1] uArr[i / 2] } val yuvImage YuvImage(nv21, ImageFormat.NV21, width, height, null) val out ByteArrayOutputStream() yuvImage.compressToJpeg(Rect(0, 0, width, height), 100, out) val jpegData out.toByteArray() return BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size) }这个方法的可行性很高但要注意两个问题。一是内存一张 1080x1920 的 NV21 原始数据大约 3.5MB加上 Bitmap 本身如果你在做完磨皮后没及时recycle()很容易 OOM。二是有个很容易遇到的坑——CameraX 在分析模式下拿到的 Image 是有旋转角度的你必须根据摄像头朝向做旋转否则拍出来的照片是横的。3.3 预览帧的实时处理管线美颜的核心不在拍照而在预览时能让用户看到实时效果。完整的管线是这样的analysis.setAnalyzer(executor) { image - val bitmap yuvToBitmap(image, rotationDegrees) val rgbaMat Mat() Utils.bitmapToMat(bitmap, rgbaMat) // 检测人脸关键点 val faceList faceDetector.detect(rgbaMat) // 对检测到的人脸区域做美颜 if (faceList.isNotEmpty()) { val beautifiedMat beautifyFace(rgbaMat, faceList[0]) Utils.matToBitmap(beautifiedMat, bitmap) runOnUiThread { previewView.drawOverlay(faceList[0]) imageView.setImageBitmap(bitmap) } } image.close() }注意这里image.close()必须调用否则分析器的缓冲区会被耗尽几帧之后相机就没法预览了。我刚做的时候漏了这个方法调试时经常出现“预览正常几秒后黑屏”的诡异现象查了很久才发现是缓冲区没释放。还有一个架构上的取舍预览画面的实时显示用imageView.setImageBitmap来实现但这会多一次内存拷贝。更高效的做法是直接用 OpenGL 渲染 YUV 纹理但那样代码复杂度会高很多不利于毕设的答辩讲解。所以我采用了“算法效果好理解 性能可接受”的方案。4. 美颜算法实现磨皮、美白、瘦脸、大眼4.1 磨皮双边滤波的高斯差分替代方案磨皮的核心是“保留边缘的同时抹平皮肤纹理”。商业 app 用的是皮肤分割 保边滤波的组合这里我用一个简化但效果明显的方案在 YCrCb 色彩空间下对 Y 通道做高斯模糊然后用原图和模糊图的差做边缘保护。fun beautifyFace(src: Mat, faceRect: Rect): Mat { val dst Mat() // 转 YCrCb 色彩空间 val ycrcb Mat() Imgproc.cvtColor(src, ycrcb, Imgproc.COLOR_BGR2YCrCb) // 分离通道 val channels ArrayListMat() Core.split(ycrcb, channels) val yChannel channels[0] // 对 Y 通道做双边滤波 val filtered Mat() Imgproc.bilateralFilter(yChannel, filtered, 9, 75, 75) // 计算高斯差分保留边缘信息 val gaussian Mat() Imgproc.GaussianBlur(yChannel, gaussian, Size(5.0, 5.0), 0.0) val diff Mat() Core.absdiff(yChannel, gaussian, diff) // 通过边缘权重合成最终 Y 通道 val weight Mat() Core.add(diff, Scalar(10.0), weight) Core.normalize(weight, weight, 0.0, 1.0, Core.NORM_MINMAX) val result Mat() Core.multiply(filtered, weight, result) Core.multiply(yChannel, Scalar(1.0) - weight, tmp) Core.add(result, tmp, result) channels[0] result Core.merge(channels, ycrcb) Imgproc.cvtColor(ycrcb, dst, Imgproc.COLOR_YCrCb2BGR) return dst }参数说明bilateralFilter的三个关键参数是邻域直径 9、σColor 75、σSpace 75。σColor 越大颜色相近的像素越容易被一起平滑磨皮感越强σSpace 越大影响范围越广但计算越慢。把这两个值调到 75 是我试出来的“效果自然 耗时可控”的平衡点想要更强磨皮可以调到 100但画面会有点“假人感”。边缘权重计算的逻辑是高斯模糊后的图和原图差的绝对值越大说明该处是边缘比如眼睛、眉毛、嘴唇权重越高越应该保留原始像素差越小说明是平滑区域比如脸颊权重越低越应该用滤波后的结果。这就是磨皮不糊眼睛的原理。4.2 美白亮度通道的线性映射与肤色保护美白比磨皮简单难在“不要白得发灰、不要连嘴唇都漂白”。常见做法是在 YCrCb 空间对 Y 通道做亮度提升同时对 Cr、Cb 通道做轻微调整以保持肤色自然。fun whitenSkin(src: Mat, intensity: Float): Mat { val ycrcb Mat() Imgproc.cvtColor(src, ycrcb, Imgproc.COLOR_BGR2YCrCb) val channels ArrayListMat() Core.split(ycrcb, channels) val yChannel channels[0] val crChannel channels[1] val cbChannel channels[2] // 亮度调整使用 Gamma 曲线避免过曝 val lut Mat(1, 256, CvType.CV_8UC1) for (i in 0 until 256) { val value i.toDouble() / 255.0 val gamma 1.0 - intensity * 0.3 lut.put(0, i, Math.pow(value, gamma) * 255.0) } val newY Mat() Core.LUT(yChannel, lut, newY) // 轻微调整 Cr红色差和 Cb蓝色差让肤色更粉嫩 Core.add(crChannel, Scalar(-intensity * 5.0), crChannel) Core.add(cbChannel, Scalar(intensity * 3.0), cbChannel) channels[0] newY channels[1] crChannel channels[2] cbChannel Core.merge(channels, ycrcb) val dst Mat() Imgproc.cvtColor(ycrcb, dst, Imgproc.COLOR_YCrCb2BGR) return dst }逻辑说明用 Gamma 曲线而不是简单地Core.add(yChannel, Scalar(50))的原因是直接加亮度会让高光区域过曝额头、鼻尖这些本来就亮的地方会变成一块死白。Gamma 变换对暗部提升明显、对亮部影响较小是美白效果自然的必要条件。美白强度的取值范围是 0.0 到 1.0滑块默认是 0.3。超过 0.5 后画面会有明显的“粉感”超过 0.7 基本不适合正常使用可以在 UI 层做上限截断。4.3 瘦脸与大眼基于关键点的局部变形算法瘦脸的核心原理是人脸关键点定位出下颌轮廓然后把轮廓内的像素向中心方向做非线性平移。常用的算法叫“局部平移变形”Local Warping本质是为每个目标像素计算一个源像素位置然后做重映射。fun thinFace(src: Mat, landmarks: FaceLandmark, intensity: Float): Mat { val dst src.clone() // 获取左右脸颊关键点ML Kit 返回的轮廓点 val leftCheek landmarks.getPosition(FaceLandmark.LEFT_CHEEK) val rightCheek landmarks.getPosition(FaceLandmark.RIGHT_CHEEK) val jawBottom landmarks.getPosition(FaceLandmark.LOWER_JAW) // 计算面部的中心点作为变形基准点 val faceCenter Point( (leftCheek.x rightCheek.x) / 2.0, jawBottom.y ) // 对左脸颊区域进行局部变形 val radius 60.0 * intensity val mapX Mat(src.rows(), src.cols(), CvType.CV_32FC1) val mapY Mat(src.rows(), src.cols(), CvType.CV_32FC1) for (row in 0 until src.rows()) { for (col in 0 until src.cols()) { val dx col - leftCheek.x val dy row - leftCheek.y val distance Math.sqrt(dx * dx dy * dy) if (distance radius) { val ratio 1.0 - (distance / radius) val strength ratio * ratio * intensity * 0.3 val newX col - dx * strength val newY row - dy * strength mapX.put(row, col, newX) mapY.put(row, col, newY) } else { mapX.put(row, col, col.toDouble()) mapY.put(row, col, row.toDouble()) } } } val warped Mat() Imgproc.remap(src, warped, mapX, mapY, Imgproc.INTER_LINEAR) return warped }这个实现有个明显的性能问题双层 for 循环在 Java 层跑480x640 的分辨率下每秒最多处理 5 帧根本达不到实时。优化方法是把算法逻辑用 C 写成 JNI 层或者在 Kotlin 里缩小处理区域——只对左右脸颊的 ROI感兴趣区域做 remap而不是全图。在实际项目里我采用了后者先根据关键点裁剪出 200x200 的 ROI变形完成后再贴回原图。这样处理一帧的时间能从 200ms 降到 15ms 左右实时预览就能稳定在 25fps 以上。4.4 滤镜LUT 映射与色彩分级滤镜像“一键上色”在 OpenCV 里实现方式有两种颜色查找表LUT即 Look-Up Table和色彩矩阵变换。对毕设来说做 3 个 LUT 滤镜黑白、暖阳、清新就足够了。fun applyLut(src: Mat, lutData: ByteArray): Mat { val lut Mat(1, 256, CvType.CV_8UC3) for (i in 0 until 256) { val r lutData[i * 3].toInt() and 0xFF val g lutData[i * 3 1].toInt() and 0xFF val b lutData[i * 3 2].toInt() and 0xFF lut.put(0, i, byteArrayOf(r.toByte(), g.toByte(), b.toByte())) } val dst Mat() Core.LUT(src, lut, dst) return dst }LUT 的原理是对于 R、G、B 三个通道的每个像素值0-255查表得出一个新的像素值。商业相机 app 里的滤镜其实就是一张 256x3 的查找表。你可以用 Photoshop 生成 LUT 文件也可以自己写脚本调整 curve 后导出。注意Core.LUT要求源图和 LUT 图的类型匹配源图应该是 CV_8UC3不能对 CV_8UC1 的灰度图直接用。如果你的滤镜管线是先做了磨皮再套滤镜磨皮输出是 BGR 格式直接传给applyLut即可。5. 避坑指南毕业设计阶段最常踩的 6 个坑5.1 模拟器上预览黑屏真机却正常原因很多模拟器不支持 CameraX 所需的硬件编码能力或者 OpenCV 的 native 库在 x86 架构的模拟器上加载失败。解决项目强制要求用真机调试优先选择国产中低端机测试因为它们在 OpenCV 的兼容性上更容易暴露问题。另外模拟器上用 Genymotion 可能会比 Android Studio 自带的模拟器好一些但整体不推荐。5.2 YUV 旋转方向不对人脸检测完全失灵原因前置摄像头默认图像是镜像的并且有 90 度或 270 度的旋转如果你直接把原始帧送给人脸检测器检测率会严重下降。解决在送入检测器之前先根据image.getImageInfo().getRotationDegrees()做旋转和镜像处理。fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, isFront: Boolean): Bitmap { val matrix Matrix() if (isFront) { matrix.postScale(-1f, 1f) } matrix.postRotate(rotationDegrees.toFloat()) return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) }5.3 磨皮后画面严重掉帧原因每一帧都在做双边滤波而双边滤波的时间复杂度远高于普通高斯模糊。解决一是降低分析分辨率到 480x640二是对 ROI 区域做处理而不是全图三是引入“隔帧处理”策略美颜算法每隔一帧执行一次中间帧直接复用上一帧的结果用户感知不到差异但帧率翻倍。5.4 OpenCV native 库加载崩溃原因OpenCV Android SDK 的initLoad()是异步加载的如果你在加载完成前就调用Mat()相关方法就会出现UnsatisfiedLinkError。解决在 Application 的onCreate里显式调用OpenCVLoader.initLocal()并检查返回值加载失败时给出提示而不是默默崩溃。5.5 Bitmap 内存泄漏导致 OOM原因美颜Bitmap是不断创建的如果在处理完一帧后没有及时回收上一帧的 Bitmap 和 Mat内存会像滚雪球一样增长。解决在finally块中调用mat.release()和bitmap.recycle()并考虑用BitmapPool复用 Bitmap 对象。5.6 毕业生容易忽略的“演示前崩溃”原因相机权限在 Android 13 以上从CAMERA分裂为CAMERARECORD_AUDIO如果用到录制如果你在拍照后还要录制视频权限不全会在运行时崩溃。解决在 manifest 中声明所有可能需要用到的权限并在启动时统一申请uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.RECORD_AUDIO / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion32 /6. 效果验证与答辩准备的进阶技巧6.1 如何量化和展示美颜效果答辩时如果只说“你们看这样更好看”是不够的。需要准备三样东西对比图、耗时统计、主观评测表。对比图同一张原图分别展示“原图、磨皮 30%、磨皮 70%、美白开启、瘦脸开启”的效果标注参数。这是最直观的展示。耗时统计用System.nanoTime()统计每帧美颜各步骤的耗时导出一张表格步骤平均耗时YUV 转 Bitmap12ms人脸检测8ms磨皮双边滤波18ms美白Gamma LUT5ms瘦脸ROI remap15ms合计58ms这个表格证明你的算法有性能意识也方便你说出优化方向人脸检测可以换更轻量的模型磨皮可以用可分离滤波近似。主观评测表找 5 个同学对同一张人像原图的“磨皮自然度、美白效果、瘦脸变形程度”打分1-5 分汇总平均分。这个在论文里属于“用户调研”章节。6.2 答辩时的功能演示脚本答辩演示不要从冷启动开始提前把 app 打开、取景器对准一张打印好的人像照片真人模特也可以但打印照片更可控。演示顺序建议先展示原始取景画面拉动磨皮滑杆从 0 到 100面部逐渐变光滑。切换滤镜展示三种 LUT 效果。拍一张照片进入编辑页展示保存到相册和读取回收。最后展示 Android Profiler 的内存和 CPU 曲线证明没有内存泄漏。一个加分的细节准备一张“极端角度”的照片——侧面 45 度人像现场让 app 人脸检测并给出关键点覆盖层。这能展示你做的关键是配准不是花架子。6.3 代码层面留一个可扩展的接口美颜算法不要写死在一个方法里。接口设计为BeautyFilter { fun apply(src: Mat, intensity: Float): Mat }然后磨皮、美白、瘦脸各实现一个过滤器UI 层的滑杆参数通过FilterChain串联调用。这样答辩时老师说“如果现在要加一个红润效果你怎么做”你可以直接现场演示写一个新的RuddyFilter实现接口加一行代码挂载到链上。我自己做的时候最后一晚还加了一个“对比模式”屏幕左侧显示原图预览、右侧显示美颜后效果中间一条可拖动的分割线。这个效果在答辩演示时观感极好而且代码量不大就是一个自定义 View 绘制两帧 Bitmap强烈建议加上。最后有一个血泪教训毕设作品不要只存在一个地方GitHub 私有仓库 本地移动硬盘 U 盘各存一份。我当年有个同学做到答辩前一天电脑硬盘坏了最后只能用残缺版演示成绩打了不少折扣。代码写得好不好是水平问题作品能不能呈现在评委面前是运气问题后者也要提前准备。希望这篇笔记能帮你在做毕设的路上少踩几个坑把更多精力花在真正能加分的技术细节上。本文还有配套的精品资源点击获取