简介:这份资源是华南农业大学毕业设计项目,用Java实现一个类似美颜相机(美图秀秀)的Android应用,面向计算机相关专业做毕业设计或课程设计的学生,以及想练手Android图像处理的开发者。压缩包共约2000个文件,以1216个xml布局与资源文件、378个json配置、352个java源码为主,另有少量sh脚本、properties配置和c语言文件,整体约19.43MB,结构完整,可直接导入Android Studio参考。项目围绕相机预览、滤镜渲染与图像处理展开,内容预览中可见GPUImage滤镜框架相关类与YUV解码实现,能帮助读者理解美颜类App从采集、解码到滤镜叠加的基本链路。已有118人学习,适合作为毕设选题的完整实现参考,也可用于拆解模块划分、复用滤镜工具类与排查相机兼容问题。
1. 从“拍完不敢发”说起:一套美颜相机类 Android 应用到底要交付什么
做毕业设计选题时,很多同学会本能地避开“美颜相机”这类题目,觉得它太像现成商业应用,怕做出来只是套壳。但真正动手后会发现,这个题目的难点恰恰不在界面,而在于一条完整的图像处理链路:相机预览、拍照取帧、人脸区域定位、磨皮与美白、滤镜叠加、结果保存与回显。它把 Android 四大组件、Camera 或 CameraX 调用、Bitmap 内存管理、JNI 或纯 Java 像素运算、权限适配全部串了起来,是一个能撑起完整方案的项目。
这套方案面向两类人:一类是要交毕业设计或课程设计、需要一份结构完整且能跑通的 Android 项目;另一类是从业者想快速搭一个图像处理 Demo 验证思路。下面按需求、架构、模块实现、避坑、验收的顺序展开,Java 技术栈为主,关键处给出可直接改参数复用的代码。
2. 需求与选型:为什么是 Java + 原生相机 + 分阶段处理
2.1 功能边界先划清楚
美颜相机类应用的功能可以无限扩展,毕设最怕的是范围失控。我一般会把功能分成三层:核心层是拍照、预览、磨皮、美白、滤镜、保存;增强层是亮度对比度调节、贴纸、裁剪旋转;展示层是相册回显、参数面板、前后摄切换。核心层必须全部跑通,增强层选两三个做,展示层保证可用即可。这样答辩时讲得清主次,也不会因为贴纸功能没做完被追问整条链路。
需求上还要明确一个容易被忽略的点:处理是“拍后处理”还是“实时预览处理”。拍后处理实现简单、性能压力小,适合毕设周期;实时预览处理体验好,但对帧率和内存要求高。通常的做法是核心链路走拍后处理,预览阶段只做基础滤镜预览,磨皮放在拍照后执行,这样既保证效果又控制风险。
2.2 技术选型的三条硬约束
第一条是相机 API。老项目常用 Camera1,新项目推荐 CameraX,但 CameraX 对 Gradle 版本和 AndroidX 依赖有要求,如果学校给的模板工程较旧,强行引入会踩一堆依赖冲突。我的建议是:模板工程能升就升到 AndroidX 并用 CameraX,升不动就用 Camera1 加 SurfaceView,功能一样能交付,别在依赖上耗掉两周。
第二条是图像处理放在哪。纯 Java 逐像素操作直观、可调试,但大图会卡;RenderScript 性能好但已逐步废弃;OpenGL ES 性能最好但学习曲线陡。毕设场景下,纯 Java 加 Bitmap 分块处理完全够用,配合采样缩放控制输入尺寸,实测 1080P 图片处理在可接受范围内。
第三条是数据存储。照片存应用私有目录还是公共相册,直接影响权限适配。Android 10 以后分区存储是默认行为,存公共相册要走 MediaStore,存私有目录则简单得多。方案里我倾向私有目录加应用内相册回显,减少权限适配工作量,导出功能作为可选增强。
2.3 模块划分与职责
整个应用拆成五个模块:相机模块负责预览、对焦、拍照取帧;图像处理模块负责磨皮、美白、滤镜、亮度对比度;人脸定位模块负责给出人脸区域,供磨皮做局部处理;存储模块负责图片落盘与读取;UI 模块负责参数面板、前后摄切换、相册列表。模块之间通过一个 ImageTask 数据对象传递 Bitmap 和参数,避免到处传裸 Bitmap 导致内存泄漏。
// ImageTask:贯穿处理链路的数据载体 public class ImageTask { public Bitmap source; // 原始位图,处理前统一缩放到长边不超过 1600 public Bitmap working; // 当前处理结果,每步覆盖 public float smoothLevel; // 磨皮强度 0~1 public float whitenLevel; // 美白强度 0~1 public int filterType; // 滤镜编号,0 表示原图 public Rect faceRect; // 人脸区域,未检测到则为 null }这个类看起来简单,但它是整条链路的“黑匣子”:任何一步出问题,打印 source 和 working 的尺寸、faceRect 是否为空,就能快速定位是取帧失败、缩放失败还是人脸没检到。参数说明上,smoothLevel 和 whitenLevel 用 0 到 1 的浮点而不是 0 到 100 的整数,是为了后续做插值和叠加时不用反复换算。
3. 总体架构设计:一条主链路加三个支撑点
3.1 主链路的时序关系
主链路是:启动相机预览 → 用户点击拍照 → 取到原始帧 → 缩放到工作尺寸 → 人脸检测 → 按参数执行磨皮美白 → 应用滤镜 → 保存并回显。这条链路的关键是“缩放要早”。很多同学把原图直接送进处理函数,结果 4000×3000 的图做逐像素卷积,手机直接卡死甚至 OOM。正确做法是在取帧后立刻按长边 1600 缩放,后续所有处理都基于工作尺寸,保存时再按需放大或直接保存工作尺寸。
人脸检测放在缩放之后、磨皮之前,原因是磨皮如果全图执行会把背景也糊掉,观感很差。检测到人脸就只对人脸矩形做磨皮,检测不到就退化为全图轻度磨皮,保证功能不中断。这个降级策略在答辩时是个加分点,说明考虑过异常路径。
3.2 三个支撑点:权限、线程、内存
权限支撑点要处理相机权限和存储权限。相机权限在进入预览前申请,存储权限按目标版本决定是否申请,Android 10 以上存私有目录不需要额外存储权限,这一点要在方案里写清楚,避免答辩被问“为什么没申请 WRITE_EXTERNAL_STORAGE”时答不上来。
线程支撑点是把拍照取帧、图像处理、保存全部放到工作线程,主线程只负责 UI 更新。图像处理用单线程串行执行,不要开线程池并行处理同一张图,否则参数叠加顺序会乱。保存完成后通过 Handler 或 LiveData 回主线程刷新相册列表。
内存支撑点有三条纪律:Bitmap 用完及时 recycle(在确认不再引用后)、工作尺寸受控、列表缩略图单独生成小图而不是加载原图。这三条做到,基本不会出现处理几张图就崩的情况。
3.3 目录结构与关键类
工程目录按功能分包:camera 包放相机控制类,image 包放处理算法,face 包放人脸定位,store 包放存储,ui 包放 Activity 和自定义 View。关键类包括 CameraController(预览与拍照)、ImageProcessor(磨皮美白滤镜)、FaceLocator(人脸区域)、PhotoStore(落盘与读取)、EditActivity(参数面板与回显)。这个划分不追求架构模式上的花哨,追求的是“打开哪个包就知道改哪块”。
4. 核心模块实现:磨皮、美白、滤镜与相机取帧
4.1 磨皮:先做局部均值再谈效果
磨皮的常见实现是高斯模糊后与原图按强度混合,但纯高斯在大图上很慢。毕设场景我一般用“局部均值加边缘保留”的简化方案:对人脸区域做分块均值滤波,再按强度与原图插值。这样代码短、可解释,效果也过得去。
// 简化磨皮:对指定区域做均值滤波后按强度混合 public static Bitmap smooth(Bitmap src, Rect area, float level) { int w = src.getWidth(), h = src.getHeight(); Bitmap out = src.copy(Bitmap.Config.ARGB_8888, true); int radius = (int) (3 + level * 5); // 半径随强度增大,3~8 像素 int left = area == null ? 0 : Math.max(0, area.left); int top = area == null ? 0 : Math.max(0, area.top); int right = area == null ? w : Math.min(w, area.right); int bottom = area == null ? h : Math.min(h, area.bottom); int[] pixels = new int[w * h]; src.getPixels(pixels, 0, w, 0, 0, w, h); int[] result = pixels.clone(); for (int y = top; y < bottom; y++) { for (int x = left; x < right; x++) { int r = 0, g = 0, b = 0, count = 0; for (int dy = -radius; dy <= radius; dy += 2) { for (int dx = -radius; dx <= radius; dx += 2) { int nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; int c = pixels[ny * w + nx]; r += (c >> 16) & 0xff; g += (c >> 8) & 0xff; b += c & 0xff; count++; } } int idx = y * w + x; int or = (pixels[idx] >> 16) & 0xff; int og = (pixels[idx] >> 8) & 0xff; int ob = pixels[idx] & 0xff; int nr = (int) (or * (1 - level) + (r / count) * level); int ng = (int) (og * (1 - level) + (g / count) * level); int nb = (int) (ob * (1 - level) + (b / count) * level); result[idx] = 0xff000000 | (nr << 16) | (ng << 8) | nb; } } out.setPixels(result, 0, w, 0, 0, w, h); return out; }这段代码里 radius 随 level 变化,level 为 0 时几乎不处理,为 1 时半径最大;采样步长取 2 是为了减少循环次数,视觉上差异很小。area 传人脸矩形就只磨人脸,传 null 就全图处理。需要提醒的是,这段是教学向实现,逐像素循环在 1600 长边的图上大约几百毫秒到一秒,放在工作线程里可以接受,但不要在预览帧上调用。
4.2 美白:查表比逐像素乘系数更稳
美白的朴素做法是每个通道乘一个大于 1 的系数,但容易过曝。更稳的做法是建一张 256 项的查找表,把亮度映射成一条平滑曲线,然后逐像素查表。这样参数调整只改表,处理逻辑不变。
// 美白查找表:level 0~1,曲线在中高亮度段抬升,暗部少动 public static int[] buildWhitenLut(float level) { int[] lut = new int[256]; for (int i = 0; i < 256; i++) { float v = i / 255f; // 暗部权重低,亮部权重高,避免死黑区域发灰 float weight = v * v; float mapped = v + (1 - v) * weight * level * 0.6f; lut[i] = Math.min(255, Math.round(mapped * 255)); } return lut; }weight 用 v 的平方,意味着越亮的像素被抬升得越多,暗部基本不动,这样人脸不会整体发灰。level 上限乘 0.6 是经验值,防止拉满时高光溢出。实际使用时对 R、G、B 三通道共用同一张表即可,肤色偏移不明显;如果追求更自然,可以给 B 通道略低的权重。
4.3 滤镜:矩阵叠加与顺序问题
滤镜用颜色矩阵实现最省事,Android 自带 ColorMatrix。常见滤镜包括暖色、冷色、复古、黑白,每个滤镜对应一组矩阵参数。关键点是滤镜要放在磨皮美白之后执行,否则磨皮会把滤镜色调抹平。多个滤镜不要叠加使用,方案里限定单选,避免矩阵连乘后颜色失真且难以解释。
// 暖色滤镜矩阵,sat 为饱和度微调系数 public static ColorMatrix warmMatrix(float sat) { return new ColorMatrix(new float[]{ 1.1f * sat, 0, 0, 0, 12, 0, 1.0f * sat, 0, 0, 6, 0, 0, 0.9f * sat, 0, -4, 0, 0, 0, 1, 0 }); }矩阵最后一列是偏移量,暖色给红绿通道正偏移、蓝通道负偏移,观感偏暖。sat 控制在 0.9 到 1.1 之间,超出范围颜色会明显失真。这个矩阵可以直接塞进 Paint 的 ColorFilter 做预览,也可以在处理链里用 Canvas 绘制到新 Bitmap 上。
4.4 相机取帧与尺寸控制
用 CameraX 时,ImageCapture 拿到的图片默认可能是全尺寸,取到后第一件事就是缩放。缩放用 BitmapFactory 的 inSampleSize 最省内存,先读边界算采样率,再解码。
// 按长边目标值计算采样率并解码,避免全尺寸进内存 public static Bitmap decodeScaled(String path, int maxSide) { BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, opts); int sample = 1; int longSide = Math.max(opts.outWidth, opts.outHeight); while (longSide / sample > maxSide) { sample *= 2; } opts.inJustDecodeBounds = false; opts.inSampleSize = sample; return BitmapFactory.decodeFile(path, opts); }maxSide 传 1600 是折中值:再大处理慢,再小保存出来偏糊。inSampleSize 取 2 的幂是系统建议做法,解码器内部会按这个值做降采样,比解码后再缩放省内存。如果拍照直接拿到的是 ImageProxy 而不是文件,就先转成 JPEG 字节再走这套逻辑,链路保持一致。
5. 避坑与排查:这类项目最容易翻车的五个点
5.1 现象:处理几张图后应用闪退
原因几乎都是 Bitmap 没有及时释放,加上列表缩略图加载了原图,内存迅速堆积。解决方式是处理完的中间 Bitmap 在确认不再使用后 recycle,列表统一用单独生成的缩略图,缩略图长边控制在 300 以内。另外工作尺寸从源头控制住,别让 4000 宽的图进处理函数。
5.2 现象:磨皮后人脸像塑料,背景反而清晰得突兀
原因是全图磨皮且强度拉满,人脸和背景的处理程度不一致。解决方式是优先用人脸区域做局部磨皮,强度上限压到 0.7 左右,检测不到人脸时用 0.3 的轻度全图处理兜底。参数面板上给实时预览,让用户自己看到边界,比事后解释有用。
5.3 现象:拍照后图片方向转了 90 度
原因是相机传感器方向和 EXIF 旋转信息没有处理。解决方式是读取图片的 EXIF Orientation,按值对 Bitmap 做旋转或翻转,再进入处理链路。这个坑很隐蔽,因为模拟器上经常不出现,真机上必现,建议在取帧后统一做一次方向校正。
5.4 现象:预览画面拉伸变形
原因是预览尺寸和 SurfaceView 宽高比不匹配。解决方式是遍历相机支持的预览尺寸,选一个宽高比最接近控件比例的,而不是直接用最大分辨率。CameraX 的 Preview 用 setTargetAspectRatio 可以缓解,但仍要在布局上做适配。
5.5 现象:Android 10 以上保存失败或相册看不到
原因是分区存储下直接写公共目录路径被拒。解决方式是存应用私有目录,通过应用内相册回显;确实要进系统相册就走 MediaStore 插入。方案里把这条写进适配说明,答辩时能直接对应到版本差异。
6. 验收与答辩:用一条可复现的检查链证明它能跑
验收不要靠“我觉得效果还行”,要有一条能重复执行的检查链。我一般按四步走:第一步,冷启动进预览,前后摄切换各一次,确认无黑屏无拉伸;第二步,拍一张含人脸的图,参数全默认,确认能出结果且方向正确;第三步,磨皮、美白、滤镜分别拉到中间值和最大值各处理一次,确认不崩、不卡死、效果单调变化;第四步,连续处理十张图,观察内存曲线和是否闪退。这四步走完,核心链路基本可信。
答辩时最容易被问的是“你的磨皮和商业应用差在哪”。不要硬吹效果,直接讲清楚取舍:商业应用多用双边滤波或导向滤波加 GPU 加速,本方案用局部均值加插值换取可解释性和实现周期,参数可调、链路完整,效果差距在预期内。这种回答比含糊其辞更让人信服。另一个高频问题是“人脸检测用的什么”,如果用的是系统自带的人脸检测或轻量方案,就如实说明检测精度有限、检测不到时有降级策略,把边界讲明白。
下面这张表可以直接放进答辩 PPT 的验收页,逐项打勾即可。
| 检查项 | 操作 | 通过标准 |
|---|---|---|
| 预览 | 冷启动、前后摄切换 | 无黑屏、无拉伸 |
| 拍照 | 拍含人脸图 | 方向正确、能回显 |
| 磨皮 | 强度 0.5 与 1.0 | 不崩、人脸区域变化明显 |
| 美白 | 强度 0.5 与 1.0 | 不过曝、暗部不发灰 |
| 滤镜 | 逐个切换 | 颜色变化符合预期、可回原图 |
| 稳定性 | 连续处理 10 张 | 无闪退、内存无明显持续增长 |
还有一个小技巧:把每次处理的输入尺寸、人脸是否检出、各步骤耗时打到 Log 里,答辩现场如果被要求演示,出问题能立刻定位到是哪一步。血泪经验是,演示机一定要提前清缓存、关后台,别让内存问题在台上暴露。
从那以后我每次带这类图像处理项目,都会强制走一遍“四步检查链加一张验收表”,不靠感觉验收。希望这套结构能帮你把方案稳稳交出去。
本文还有配套的精品资源,点击获取