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