
简介这是一份面向毕业设计场景的Android美颜相机应用完整源代码基于Android Studio开发对标美图秀秀的核心功能适合移动开发初学者、Android方向学生及准备毕业设计答辩的开发者参考。包内共2000个文件压缩后约20MB以XML布局与配置文件1216个、Java核心逻辑352个、JSON数据与配置文件378个为主同时包含少量C语言解码器、Shell脚本及Markdown说明覆盖从界面设计到图像算法的完整工程结构。源码围绕相机API调用、美颜滤镜处理、图像存储与分享等模块展开涉及Camera2 API、OpenCV/GPUImage等常见技术GPUImageFilterTools与yuv-decoder等文件展示了滤镜工具封装和YUV数据解码的关键实现便于对照学习。已有121人学习下载适合作为毕业设计选题、移动应用课程项目以及美颜类产品开发的实例参考能够帮助读者理解Android图像处理应用的整体开发流程。1. 美颜相机不只是套滤镜这个项目到底在解什么题如果你在 Android Studio 里做过图像处理 App会发现一个尴尬现象把美颜参数调得再高真机上一跑预览卡成幻灯片、肤色发灰、拍出来和预览不一致。这个毕业设计项目的源代码里没有花哨的界面核心文件是yuv-decoder.c、GPUImage.java、GPUImageFilterTools.java说明作者是把渲染管线而不是 UI 当作主战场。它解决的问题很具体如何用 GPU 实时处理相机帧如何把磨皮、美白、滤镜组合成一条可复用的链再干净地把结果保存和分享。适合三类人正在做 Android 图像方向毕业设计的学生、想把 GPUImage 迁移到 Camera2 的开发者、以及想知道美图秀秀这类工具在技术底层拆开是什么样子的工程师。接下来说的是我基于这套源码拆出来的实现路线。2. 相机采集层Camera2 API 与纹理管线的落地方式2.1 为什么不用传统 CameraPreview而用 TextureView SurfaceTexture早期 Android 相机开发常用 Camera SurfaceView 直接预览但 Camera1 在 Android 5.0 之后已经不再建议使用。这套源码选择 Camera2 API 是有道理的Camera2 把拍摄流程拆成了CaptureRequest、CaptureSession、ImageReader三个环节美颜相机需要每一帧都进入 GPU 管线如果走SurfaceView自带的预览通道数据复制次数多性能损耗明显。常见做法是使用TextureView配合SurfaceTextureTextureView textureView findViewById(R.id.texture_view); textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { openCamera(surface, width, height); } Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // 预览尺寸变化时重新配置 camera 参数 } Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { // 每帧更新入口 } });SurfaceTexture的本质是生产消费者队列相机会把帧送入纹理 IDGPUImage 的GPUImage类可以直接接收这个纹理 ID 做片元着色器处理。这样避免了 YUV 数据在 Java 层反复拷贝效率比Camera.setPreviewCallback高一个数量级。SurfaceTextureUpdated这个回调是后期做帧率统计的好位置你可以在这里计数判断实时美颜有没有掉帧。2.2 Camera2 打开相机的关键步骤与权限处理Camera2 的权限不只是 AndroidManifest 里声明一句Android 6.0 以上必须动态申请。把这个按钮动作和回调写完整private static final int REQUEST_CAMERA_PERMISSION 1; private void requestCameraPermission() { if (checkSelfPermission(Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } } Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode REQUEST_CAMERA_PERMISSION) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { // 授权后重新启动 TextureView 的监听流程 } else { // 引导用户到设置页否则应用一打开就黑屏 } } }权限回调里必须处理拒绝和“不再询问”两种情况很多美颜 App 在首次启动时死在权限上用户拒绝后界面一直黑屏连个提示都没有。Open Camera 的入口一般放在onResume里但要在权限确认后再调用openCamera。具体参数上预览尺寸建议选StreamConfigurationMap.getOutputSizes(SurfaceTexture.class)里与屏幕比例相近的尺寸比如 16:9 手机选 1920x1080 而不是 1280x720不然画面会被TextureView拉伸。保留一个表格辅助选择目标机型推荐预览尺寸为什么中低端机1280x720渲染压力小磨皮实时不掉帧主流旗舰1920x1080清晰度和性能平衡高刷屏平板2560x1440但需要考虑 GPU 浮点能力实际选尺寸要遍历系统提供的预览尺寸不要硬编码。高清屏上强行用 4K 预览GPUImage 的离屏缓冲会占掉大量显存美颜效果没提升多少功耗先上去了。2.3 yuv-decoder.c 在源码里的真实作用项目正文里出现yuv-decoder.c很容易被忽略但它很关键。GPUImage 默认的相机输入是SurfaceTexture转成的GL_TEXTURE_EXTERNAL_OES这个纹理在片元着色器里采样得到的是 RGBA。但如果你的滤镜链需要读取原始 YUV 数据做肤色检测就必须在 Native 层先把 YUV 转成 RGB 或者直接处理 Y 分量。yuv-decoder.c一般是 JNI 实现里面会有类似这样的函数JNIEXPORT void JNICALL Java_com_example_beauty_BeautyNative_yuvToRgb(JNIEnv *env, jobject thiz, jbyteArray yuvData, jint width, jint height, jintArray rgbOut) { jbyte *yuv (*env)-GetByteArrayElements(env, yuvData, NULL); jint *rgb (*env)-GetIntArrayElements(env, rgbOut, NULL); // YUV420SP 转 RGB 的实现 for (int j 0; j height; j) { for (int i 0; i width; i) { int y yuv[j * width i] 0xff; int u yuv[width * height (j / 2) * width (i / 2) * 2] 0xff; int v yuv[width * height (j / 2) * width (i / 2) * 2 1] 0xff; int r y 1.402f * (v - 128); int g y - 0.344f * (u - 128) - 0.714f * (v - 128); int b y 1.772f * (u - 128); // 范围裁剪 rgb[j * width i] 0xff000000 | ((r 0xff) 16) | ((g 0xff) 8) | (b 0xff); } } }注意这个解码器的格式要和ImageReader获取到的Image.getFormat()一致。绝大多数手机输出的是ImageFormat.YUV_420_888但在 Java 层它的内存布局分多个 plane直接当成 NV21 传进 JNI 会花屏。我一般会在 JNI 函数里先判断isCopy参数其次做行列对齐处理否则在部分机型上颜色会偏绿。3. 美颜核心GPUImage 滤镜链与参数调优实操3.1 GPUImage 和 GPUImageFilterTools 的关系这套源码里的GPUImage.java是引擎GPUImageFilterTools.java是滤镜工厂。GPUImage 做的事情本质上是把相机纹理渲染到一个离屏的FrameBuffer再串联一组GPUImageFilterGPUImage gpuImage new GPUImage(this); gpuImage.setGLSurfaceView(glSurfaceView); gpuImage.setUpCamera(camera); gpuImage.setFilter(createBeautyFilter());GPUImageFilterTools里通常有一个createFilterForType方法根据枚举返回对应滤镜实例。你加减一个滤镜不需要动引擎代码只要在工厂里加一条分支。如果自己重新实现一遍管理类注意 GL 上下文要在 GLSurfaceView 所在线程切换否则GLES20.glUseProgram会报GL_INVALID_OPERATION。实际的滤镜链构造顺序是先做颜色校正再做保边平滑最后做锐化。顺序不能反过来否则先锐化会放大噪点再磨皮会把边缘也磨没。比如一个常见的组合public ListGPUImageFilter buildBeautyChain() { ListGPUImageFilter filters new ArrayList(); filters.add(new GPUImageBrightnessFilter(0.05f)); // 提亮 filters.add(new GPUImageBoxBlurFilter(4.0f)); // 磨皮 filters.add(new GPUImageSaturationFilter(1.05f)); // 色彩增强 filters.add(new GPUImageSharpenFilter(0.2f)); // 细节锐化 return filters; }GPUImage 的官方实现里没有内置多滤镜串联的GroupFilter需要自己写一个GPUImageFilterGroup内部按顺序把上一个滤镜的输出纹理传给下一个。如果只设置一个滤镜效果就是单层磨皮容易出现塑料感这也是很多简化版美颜 App 的通病。3.2 高斯模糊、亮度、饱和度参数取值范围高斯模糊半径是磨皮效果最敏感的项。GPUImageBoxBlurFilter的blurSize不是像素半径而是片元着色器里的纹理坐标偏移值一般从 0.0 到 10.0 之间超过 8.0 会出现明显的层叠感。亮度调节用GPUImageBrightnessFilter推荐范围 -1.0 到 1.0但实际美颜场景 0.02 到 0.08 就够了提太多脸部过曝轮廓阴影全丢。饱和度GPUImageSaturationFilter以 1.0 为基准1.0 是原始状态1.1 左右适合人像超过 1.3 后嘴唇和背景色会粘在一起。滤镜关键参数推荐范围超出后的问题BoxBlurblurSize2.0 - 6.0高光溢出成雾面Brightnessbrightness-0.1 - 0.1脸部轮廓失真Saturationsaturation0.9 - 1.2皮肤偏红/偏黄Sharpensharpness0.1 - 0.5噪点被放大成颗粒WhiteBalancetemperature2500 - 5000肤色不自然这些参数不要做成固定常量。我的做法是给每个滤镜生成一个 JSON 配置从后端动态下发。毕业设计如果不想搭服务器可以放在assets/beauty_config.json里通过Gson读出来再设置到对应滤镜。这样一台真机预览时可以直接改 JSON 看效果不需要反复改代码重编译。3.3 用片元着色器做肤色补偿的进阶思路上面的滤镜链是通用参数但要达到“美图秀秀感”还需要肤色区域加权。思路是在片元着色器里先判断当前像素是否属于皮肤属于则降低透明度混合不属于则正常渲染precision mediump float; uniform sampler2D inputImageTexture; varying vec2 textureCoordinate; void main() { vec4 color texture2D(inputImageTexture, textureCoordinate); float r color.r; float g color.g; float b color.b; // 一种简单的肤色判断r g b 且通道差在一定范围内 bool skin (r 0.3) (g 0.15) (b 0.05) (r - g 0.05) (r - b 0.15) (r g) (g b); vec3 smoothColor color.rgb * 0.9 vec3(0.02, 0.01, 0.005); if (skin) { gl_FragColor vec4(mix(color.rgb, smoothColor, 0.5), color.a); } else { gl_FragColor color; } }这个方法的优点是性能开销极小缺点是误判率高穿红色衣服时会把衣服当皮肤磨。更严谨的做法是用GPUImageTwoPassFilter先做一次 YUV 肤色统计再把统计结果作为 uniform 传入。这就是为什么源码里同时存在yuv-decoder.cNative 处理肤色检测GLSL 做混合渲染。4. 拍完怎么存从 GL 缓冲到相册的全链路实现4.1 从 GPUImage 的 onDraw 结果中读取像素实时预览是循环渲染的但拍照时要先把当前帧停住。GPUImage 里的GLSurfaceView在onPause时停止渲染不能在渲染线程里直接glReadPixels否则会阻塞 GL 管线的执行。正确做法是设置一个GPUImage.OnRenderCompletedListener在里面标记拍照请求gpuImage.setOnRenderCompletedListener(new GPUImage.OnRenderCompletedListener() { Override public void onRenderCompleted() { Bitmap bitmap gpuImage.capture(); if (bitmap ! null) { saveBitmapToGallery(bitmap); } } });gpuImage.capture()内部会调用glReadPixels从当前绑定的FrameBuffer读取像素。注意这个Bitmap是上下颠倒的因为在 OpenGL 坐标系里原点在左下角。保存前需要做一次垂直翻转Matrix matrix new Matrix(); matrix.postScale(1f, -1f); Bitmap flipped Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);如果嫌createBitmap多一次内存拷贝可以直接在 shader 阶段把 v 坐标翻转这样glReadPixels拿到的就是正像。4.2 保存到 MediaStore 的正确姿势Android 10 以后不能直接往公共目录写文件了要使用MediaStore.Images.Media.insert。代码示例private void saveBitmapToGallery(Bitmap bitmap) { ContentValues values new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, beauty_ System.currentTimeMillis() .jpg); values.put(MediaStore.Images.Media.MIME_TYPE, image/jpeg); if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { values.put(MediaStore.Images.Media.RELATIVE_PATH, Pictures/BeautyCamera); values.put(MediaStore.Images.Media.IS_PENDING, 1); } Uri uri getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); try (OutputStream out getContentResolver().openOutputStream(uri)) { bitmap.compress(Bitmap.CompressFormat.JPEG, 95, out); } catch (IOException e) { e.printStackTrace(); } if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { values.clear(); values.put(MediaStore.Images.Media.IS_PENDING, 0); getContentResolver().update(uri, values, null, null); } }这段代码里IS_PENDING字段很关键不加的话照片在相册里可能会显示为损坏文件因为系统是在事务完成后才扫描到文件的。压缩质量我用 95对美颜照片来说JPEG 90 以下会在肤色渐变区域出现色带。4.3 分享到微信、微博等 App 的 Intent 写法保存完成后用FileProvider提供 content URI不能直接用file://否则在 Android 7.0 以上会抛FileUriExposedExceptionIntent shareIntent new Intent(Intent.ACTION_SEND); shareIntent.setType(image/jpeg); Uri shareUri FileProvider.getUriForFile(this, getPackageName() .fileprovider, new File(savedPath)); shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(Intent.createChooser(shareIntent, 分享到));同时在AndroidManifest.xml里注册FileProviderprovider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerfile_paths.xml里要包含你图片保存的缓存路径比如cache-path nameshare pathshare_image/ /。分享到微信朋友圈时微信会读取这个 URI所以你要保证在分享前文件已经写完。5. 性能与出错排查真机上的调试技巧5.1 帧率统计与动态降级美颜相机最怕的不是功能不好而是帧率不稳定。我用一个简单的时间戳计数来监控long lastTime 0; int frameCount 0; Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { frameCount; if (System.currentTimeMillis() - lastTime 1000) { Log.d(FPS, fps frameCount); frameCount 0; lastTime System.currentTimeMillis(); // 如果 fps 15自动降低当前滤镜的 blurSize } }这是一个很实用的技巧把磨皮半径、预览尺寸、是否启用 4K 做成可动态切换的选项。低于 15 fps 时先把流水线最重的滤镜降级而不是直接关闭。这样能保证预览不中断。在真机上一般中端机跑 1080p 预览加双滤镜是没问题的但打开 4K 视频录制同时做美颜的时候预计会掉到 10 fps 以下这时候建议关闭实时美颜只保留拍照美颜。5.2 GL 异常与黑屏调试手段GPUImage 项目常见的一个现象是预览黑屏这通常是因为YuvDecoder和 GPUImage 的纹理格式不匹配。你需要打开 OpenGL 的调试输出GLSurfaceView glSurfaceView new GLSurfaceView(this); glSurfaceView.setEGLContextClientVersion(2); glSurfaceView.setDebugFlags(GLSurfaceView.DEBUG_CHECK_GL_ERROR | GLSurfaceView.DEBUG_LOG_GL_CALLS);DEBUG_CHECK_GL_ERROR会在每次 GL 调用后检查错误码日志里出现GL_INVALID_OPERATION就说明glUseProgram用了未链接的着色器。另一个头疼的问题是IllegalStateException: Renderer has already been set一般是因为在onPause和onResume循环里重复调用了setRenderer()正确做法是只在onCreate里设置一次之后只调用onPause/onResume。5.3 多机型适配检查清单验证项目测试方法通过标准预览比例竖屏拍一张横屏拍一张人脸不变形肤色还原灯光下和自然光下各拍一张不偏黄不偏蓝极端脸型侧脸、戴眼镜磨皮不把边缘糊掉内存占用adb shell dumpsys meminfo观察低于 300MB耗电连续预览 15 分钟温度上升小于 8 度还有一个容易忽视的点OEM 机型对Camera2的CONTROL_AE_TARGET_FPS_RANGE支持不同。部分小米机型最大帧率只给到 30fps你强行设置 60fps 会导致自动对焦失效。我的做法是查询RangeInteger[] ranges后取最小值大于等于 15、最大值小于等于 30 的组合。最后提一个录制视频时的技巧GPUImage 的GPUImageMovieWriter在高通机型上如果出现音画不同步可以把编码器的setRealtime(true)打开并在MediaCodec配置里设置KEY_COLOR_FORMAT为COLOR_FormatSurface。这是我在真机上踩过最久的坑希望这份源码分析能帮你少折腾两个星期。本文还有配套的精品资源点击获取