:Camera2+OpenCV實現(xiàn)實時磨皮美白與瘦臉大眼)
簡介一份華南農(nóng)業(yè)大學畢業(yè)設計項目源碼實現(xiàn)類似美顏相機/美圖秀秀的Android應用面向計算機專業(yè)學生、Java及Android開發(fā)學習者可作為畢業(yè)設計或課程設計的完整參考方案。項目覆蓋圖像采集、濾鏡渲染、人像美化等核心模塊包含豐富的界面布局與業(yè)務邏輯代碼可幫助理解移動端圖像處理的實現(xiàn)思路。壓縮包共2000個文件其中1216個xml布局文件、378個json配置、352個java源碼另有shell、properties、c及md等輔助文件整體大小約19.43MB目錄結構完整便于對照學習。源碼基于Java與Android平臺涉及GPUImage等圖像處理框架適合需要快速搭建同類應用或擴展美顏功能的開發(fā)者使用。目前已有118人學習下載具有不錯的參考價值。1. 別把美顏相機畢設做成濾鏡相冊先想清楚鏈路畢設答辯時評審最常追問的往往是那句“你這個磨皮用的是什么原理”。很多同學拿到“類似美顏相機”的 Android 題目后先花兩三周搭界面、接相機、做相冊最后塞一個第三方渲染引擎了事結果演示時被問兩句就露餡。這個題目的本質(zhì)不是做“濾鏡相冊”而是把一條實時圖像處理鏈路完整跑通相機采集、預覽、人臉檢測、磨皮美白、瘦臉大眼、特效濾鏡最后輸出到屏幕或保存成圖片。它適合有一點 Java/Kotlin 基礎、愿意碰 Camera2 和 OpenCV 的讀者工程量適中、算法有講頭是圖像處理方向里性價比很高的畢設選題。2. 工程骨架與選型Camera2 TextureView 是最穩(wěn)的組合2.1 先把一條美顏鏈路畫清楚再決定代碼往哪放常見的做法是先把處理流程拆成獨立階段每個階段只做一件事階段之間用“接口”連接而不是把所有邏輯堆在一個 Activity 里。典型鏈路是這樣的相機傳感器輸出 YUV 幀 → 顯示到 TextureView / GLSurfaceView 預覽 → 從幀里檢測人臉框 → 提取關鍵點 → 對整幀做磨皮、美白 → 以關鍵點為圓心做瘦臉、大眼 → 疊加實時濾鏡GLSL→ 屏幕預覽或拍照保存。畫鏈路的價值在于你能在動手前就確定每個模塊的輸入輸出。比如“磨皮模塊”接收一張 Mat返回一張 Mat“濾鏡模塊”接收紋理 ID輸出到屏幕。這樣后續(xù)替換算法、調(diào)參、加功能都不需要重寫相機部分。另一個好處是答辯時要畫架構圖你已經(jīng)有一張現(xiàn)成的模塊圖不需要臨時編。我一般會把“預覽顯示”和“圖像處理”分開預覽顯示用 TextureView 承接相機幀圖像處理在后臺線程里跑 OpenCV Mat。如果混在一起UI 線程會被拖慢滑動濾鏡參數(shù)時會明顯卡頓。這個劃分幾乎決定了整個工程最后能不能穩(wěn)定跑起來。2.2 為什么用 Camera2 而不是 CameraXOpenCV 而不是 RenderScript選型是畢設里最容易被追問的部分理由要說得出。CameraX 確實簡單PreviewView 開箱即用生命周期自動綁定寫幾十行就能看到預覽。但它有兩個問題一是拿 YUV 幀做處理要走 ImageAnalysis把 Image 轉成 OpenCV Mat 還要自己寫轉換二是對預覽方向、sensor 旋轉等細節(jié)控制比較黑盒答辯時“相機是怎么打開的”這個問題很難講透。Camera2 代碼量大但每一步都透明openCamera、CaptureSession、CaptureRequest 全是顯式調(diào)用評審問起來你能一條條答清楚。算法側要注意 RenderScript 已經(jīng)被 Google 標記廢棄新工程不要碰。OpenCV Android SDK 提供人臉檢測、矩陣運算、顏色空間轉換磨皮美白這類需求直接調(diào)用即可。人臉關鍵點如果要求更高比如 68 點、106 點可以接一個輕量級模型但純畢設場景下 OpenCV 自帶級聯(lián)檢測器已經(jīng)夠用。渲染側選 GLSurfaceView GLSL濾鏡用著色器在 GPU 上跑比在 CPU 上逐像素快兩個數(shù)量級這也是“實時美顏”能成立的原因。2.3 跑通預覽的最小步驟Gradle、權限、開相機和方向修正先加依賴版本號以 Android Studio 能拉到的最新穩(wěn)定版為準不寫死implementation(org.opencv:opencv) // 在 AS 的 SDK Manager 里同步最新版本AndroidManifest 里申報相機權限uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue /打開相機的最小代碼注意回調(diào)要跑在后臺線程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()跑主線程否則相機回調(diào)會把主線程堵住。openCamera指定前后攝時cameraIdList的下標并不總是 0 后 1 前要看CameraCharacteristics.LENS_FACING判斷這也是一個經(jīng)典踩坑點。拿到CameraDevice后創(chuàng)建會話把 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的傳感器像素格式問題但按上面的寫法基本不會遇到。預覽跑通后整個工程就有了“地基”后續(xù)做算法都在這條鏈路上加。3. 磨皮美白怎么落地從膚色檢測到 SeekBar 參數(shù)3.1 磨皮不是無腦模糊膚色蒙版 高斯模糊 羽化混合很多人的第一版磨皮是整張圖高斯模糊結果頭發(fā)絲、眉毛、背景全糊了臉還像貼了層塑料。正確做法是只對皮膚區(qū)域做模糊而且邊緣要過渡自然。完整思路是先檢測膚色區(qū)域生成蒙版對原圖做高斯模糊用蒙版作為權重把原圖和模糊圖線性混合。蒙版越白的地方模糊的權重越高。實現(xiàn)代碼如下直接輸入一張 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. 羽化蒙版避免皮膚邊緣出現(xiàn)硬邊 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, ...)蒙版邊緣會是銳利的硬邊臉上會出現(xiàn)一塊一塊的“補丁感”。強度控制在 0.3~0.7 之間超過 0.8 就會變成塑料臉。模糊半徑 9 是 720p 下的經(jīng)驗值分辨率翻倍時要對應增大。3.2 美白在 YCrCb 空間做提亮 Y、微調(diào) Cb防過曝美白和磨皮是兩件事磨皮解決“皮膚不平”美白解決“皮膚不夠白”。美白在 YCrCb 空間做最直接Y 通道提亮Cr/Cb 做小幅偏移讓膚色不發(fā)黃、不發(fā)綠。實現(xiàn)很輕量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 通道降一點膚色調(diào)得更透 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。超過這個范圍高光區(qū)域會變成一塊死白尤其是額頭和鼻梁。高光保護那一步不能省很多人美白翻車就是栽在這里。3.3 三組核心參數(shù)的范圍與映射預覽時參數(shù)放輕一點拍照出圖時調(diào)重一點這是美顏相機的通用邏輯。以下是 720p 預覽分辨率下的經(jīng)驗值參數(shù)推薦范圍說明高斯模糊半徑5~15 像素半徑越大越糊超過 15 臉部紋理全丟磨皮強度0.3~0.7預覽用 0.3~0.4拍照用 0.5~0.7膚色檢測 Cr133~173數(shù)值過大會把背景的紅墻、紅衣服也當成臉膚色檢測 Cb77~127與 Cr 配合使用兩個范圍收斂一點更穩(wěn)美白 Y 增益0.05~0.2超過 0.2 高光區(qū)域會過曝銳化強度0~0.3磨皮后加一點銳化找回紋理SeekBar 的滑動值一般是 0~100傳到算法里時除以 100 再乘一個系數(shù)。注意不能把 SeekBar 的原始值直接當 strength 用否則調(diào)到一半效果就爆表了。4. 瘦臉大眼和實時濾鏡局部變形 GLSL 著色器4.1 瘦臉大眼用“局部液化”就夠了以關鍵點為圓心的像素偏移瘦臉大眼在美顏相機里屬于“局部變形”算法核心是讓一個圓域內(nèi)的像素按權重偏移。商業(yè)產(chǎn)品用的是液化Liquify和網(wǎng)格變形畢設實現(xiàn)一個簡化版完全夠講。思路是選一個中心點瘦臉用臉中心大眼用眼睛中心給定一個半徑圓內(nèi)像素距離中心越近偏移越大圓外不處理。下面是 Java 寫法直接遍歷目標區(qū)域內(nèi)像素反向采樣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為正數(shù)時像素向中心收攏用于瘦臉如果對眼睛中心做負向偏移像素從中心向外推就放大眼睛。半徑一般取臉寬的 0.4~0.6 倍太大鼻子會變形太小看不出效果。注意整幀遍歷會卡實際只處理以關鍵點為中心裁出來的一塊 ROI能省掉 70% 的計算量。4.2 濾鏡走 GLSL一段可以放進 GLSurfaceView 的片元著色器濾鏡如果也用 OpenCV 逐像素調(diào)色預覽幀率會很難看。正確做法是交給 GPU把 OpenCV 處理后的 Mat 轉成紋理上傳到 GLSurfaceView片元著色器里做顏色映射。下面是一段基礎的美顏濾鏡著色器包含提亮、輕微去飽和和色調(diào)微調(diào)#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); // 亮度調(diào)整 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 就好太大整體發(fā)白。這里有個關鍵點Camera2 傳過來的紋理是 OES 外部紋理GL_TEXTURE_EXTERNAL_OES著色器第一個版本要用#extension GL_OES_EGL_image_external : require統(tǒng)一在工程里做一層紋理拉直后續(xù)所有濾鏡就都能復用。如果你用 GLSurfaceView 配合SurfaceTexture做相機預覽這塊可以省掉OpenCV的轉Bitmap過程直接顯示。4.3 720p 預覽、隔幀檢測、ROI 處理實時性從這三處摳實時美顏的瓶頸從來不在濾鏡而在人臉檢測和局部變形。常見做法是三個約束預覽處理分辨率鎖 720p不要碰 4K人臉檢測每 5 幀做一次關鍵點滯后 100ms 人眼看不出問題局部變形只處理人臉 ROI而不是整幀。還有一條容易被忽略OpenCV 的 Mat 處理完要立刻 release否則內(nèi)存會持續(xù)上漲。我用這套約束在“某千元測試機”上把處理幀率穩(wěn)定在 25 幀以上而第一版全幀處理只有 12 幀。差距就來自 ROI 和隔幀檢測這兩處。答辯時被問“你的算法為什么快”能把這三點答出來比背一堆理論更有說服力。5. 美顏相機常見問題排查黑屏、鏡像與內(nèi)存泄漏5.1 預覽方向顛倒或橫屏現(xiàn)象豎屏拿著手機預覽畫面是橫過來的或者翻轉 90 度后才正常。原因相機 Sensor 的默認方向和屏幕方向不一致Camera2 不會幫你自動旋轉。很多教程只給了設置TEMPLATE_PREVIEW的代碼沒給方向補償預覽豎起來是運氣的。解決按第 2.3 節(jié)的sensorOrientation - displayRotation公式對TextureView做setTransform(Matrix)。注意前置和后置的旋轉方向相反前置還要額外鏡像。這個方法同時管預覽顯示和后續(xù)拍照方向存圖前對Mat做一次Core.flip(mat, mat, 1)即可。5.2 權限彈窗后第一次打開黑屏第二次正?,F(xiàn)象首次安裝進 App彈出相機權限授權框點“允許”后預覽是黑色的殺掉重進一次就正常。原因onResume()里先執(zhí)行了openCamera()但權限授權結果還沒回來checkSelfPermission返回拒絕代碼直接return了。之后權限回調(diào)里也沒有再次調(diào)用openCamera于是相機一直沒打開。解決把打開流程改成“條件觸發(fā)”而不是“順序觸發(fā)”。權限授權成功回調(diào)里調(diào)用openCamera()TextureView 的SurfaceTextureListener回調(diào)里也檢查權限兩邊都滿足才真正打開。最穩(wěn)妥的是寫一個tryStartCamera()內(nèi)部判斷permissionGranted surfaceReady兩個條件都滿足執(zhí)行openCamera。這一個改動就能消掉 80% 的黑屏問題。5.3 前置自拍照片左右顛倒現(xiàn)象預覽里看自己是正常的拍出來的照片卻是鏡像的和日常自拍習慣相反。原因前置相機的畫面本身是鏡像的你為了讓預覽符合習慣做了Matrix.postScale(-1f, 1f)但保存照片時用的是ImageReader的原始幀沒有做同樣的翻轉。解決預覽和存儲是兩條線。預覽方向用 Matrix 修正保存幀在寫入文件前對 Mat 做水平翻轉Core.flip(rotatedMat, flippedMat, 1)。同時注意焦點的位置不會因為翻轉而偏移如果是固定點對焦翻轉后還要把焦點坐標映射到翻轉后的坐標系里。這個坑在自拍場景下一抓一個準評審演示時經(jīng)常 crash 在這。5.4 反復進出頁面內(nèi)存暴漲最終閃退現(xiàn)象從列表頁進相機頁再退出循環(huán)五六次內(nèi)存曲線一路上漲最后 OOM 閃退。原因CameraDevice沒有 close、SurfaceTexture沒有釋放、OpenCV Mat 沒有 recycle。尤其CameraDevice不 close下一次openCamera會直接拋CAMERA_IN_USE_ERROR。這條在低端機型上是必踩的坑。解決onPause()里執(zhí)行device?.close()并置空onDestroy()里textureView.surfaceTextureListener null后臺線程用handlerThread.quitSafely()。另外你在第 4.1 節(jié) localWarp 里每次dst.create出來的 Mat用完必須release。我見過最夸張的泄漏是把磨皮中間變量寫在while(true)預覽循環(huán)里不釋放兩分鐘內(nèi)存漲了 300MB。檢查泄漏最快的方式是開 Android Studio 的 Memory Profiler反復進出頁面時盯著Mat count。5.5 磨皮過度“塑料臉”現(xiàn)象磨皮后皮膚紋理全無像貼了一張塑料面具尤其額頭和臉頰反光嚴重。原因模糊半徑和強度都用得太狠膚色蒙版還把高光區(qū)域全覆蓋進去了。很多同學的調(diào)參邏輯是“效果越明顯越好”但美顏相機的評審既看效果也看自然度。解決把高斯模糊半徑控制在 9 像素以內(nèi)強度降到 0.4蒙版羽化半徑加大。磨皮之后加一道銳化把高頻細節(jié)疊回去 15%~20%皮膚紋理就不會完全消失。還有個技巧先做一遍小半徑磨皮再疊加一層更小半徑的磨皮比一次大半徑更自然。這套“局部小劑量多次處理”的思路也可以直接寫進論文比單純講濾波要有內(nèi)容。6. 給畢設加分離線精修對比 幀耗時統(tǒng)計回答“你的美顏快不快”實時預覽為了流暢只能跑 720p 和輕量參數(shù)但答辯時最加分的效果恰恰是“對比”同一張原圖左邊是原始效果右邊是離線精修后的效果。我建議單獨加一個“拍照后離線精修頁”把拍照得到的全尺寸原圖跑一遍完整磨皮美白再疊加瘦臉大眼和濾鏡和原圖左右分屏對比。這個頁面不要求實時算法可以跑重參數(shù)效果會比預覽好一大截評審的視覺沖擊力也強得多。第二件事是把自己算法的耗時測出來用數(shù)據(jù)回答“實時性夠不夠”。核心做法是在處理管線外包一層計時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 }連續(xù)跑 30 幀取平均把數(shù)據(jù)整理成表格720p 預覽平均耗時 XXms全部美化開啟時幀率 XXfps這個數(shù)據(jù)放進論文的“性能分析”一節(jié)非常有用。注意測的時候關掉系統(tǒng)省電模式用同一臺測試機跑完整流程別用模擬器數(shù)據(jù)模擬器的幀率沒有參考價值。我的習慣是先把實時預覽做到流暢再單獨做離線精修頁這樣即使答辯現(xiàn)場預覽翻車也能用離線對比撐住場面。這也是我從模擬項目 X 的答辯經(jīng)驗里學到的教訓——當時我把所有美化都堆在預覽鏈路里低溫環(huán)境下低端機掉到 12 幀現(xiàn)場演示很被動。后來改成 ROI 處理加隔幀檢測才穩(wěn)定在 25 幀以上。希望你做這個方向時少走這段彎路希望幫到你。本文還有配套的精品資源點擊獲取