简介:这份资源是基于飞桨PaddleSeg的ModNet算法实现的人像抠图安卓版Demo,面向移动端视觉开发者、AI应用学习者以及嵌入式系统工程师。它能帮助解决在手机等移动设备上高效分离人物与背景的问题,直观演示深度学习大模型在资源受限环境中的部署与调用。资源包共55个文件,约2.9MB,包含10个Java源码、12个XML布局与配置、11张PNG示例图,以及Gradle构建脚本、properties配置、LICENSE和README文档,目录层次清晰,可直接作为完整Android Studio工程导入运行。项目采用模块化设计,代码注释风格统一,便于阅读和二次扩展;自带的demo.jpg、human.jpg、bg.jpg等样片和gradlew脚本,能让开发者快速跑通端侧人像抠图流程。目前已有248人学习浏览,适合希望研究ModNet移动端落地、熟悉安卓图像处理工程结构,或进行嵌入式AI部署实践的技术人员下载交流。
1. 一张照片在手机上实时分层,背后不全是魔法
人像抠图在移动端一直是个“看着简单、落地费劲”的方向。早期方案要么走传统分割(GrabCut、抠绿幕),要么上重型网络,推理一次要几百毫秒,手机发热明显。ModNet(Matting Objective Decomposition Network)是少有的、把“语义分割”和“细节边缘”拆开并行处理的轻量级网络,单张图在骁龙中端芯片上能做到实时。而PaddleSeg的安卓端demo把这套算法封装成了一个可以直接改的Android Studio工程。本文围绕这个human_matting_android_demo拆解三件事:ModNet的三分支结构如何降低计算量、PaddleSeg模型如何导出并转成Paddle Lite格式塞进APK、以及安卓端从取图到输出alpha通道的完整数据流。
2. ModNet三分支结构与PaddleSeg部署链路
2.1 为什么要拆成三分支而不是直接端到端
ModNet的出发点很直接:人像抠图要同时处理“这是人”的全局信息和“头发丝/衣服边缘”的局部分界。人类视觉系统也是这么分工的——先锁定主体,再描边缘。ModNet把网络拆成S1(语义估计)、S2(细节预测)、S3(语义-细节融合)三个分支,对应低分辨率定位、高分辨率边缘、最终融合输出。
- S1分支:输入下采样到低分辨率特征图,跑一个轻量backbone,输出是粗糙的alpha matte(大致区分人和背景区域)。
- S2分支:在S1基础上,对“可能是边缘”的像素区域做精细回归,专门处理发丝、半透明衣物、阴影过渡。
- S3分支:把S1的全局语义和S2的局部细节做通道维度的融合(不是简单相加),再用一个卷积头输出最终的1通道alpha。
这种设计最直接的收益是:模型不需要一次性在全分辨率上做高密度计算。S1用低分辨率省算力,S2只在边缘区域集中计算,整体FLOPs被压到U-Net类方案的三分之一左右。PaddleSeg仓库中的ModNet预训练模型,backbone基于MobileNetV3-Large(基于PaddleClas的预训练权重),输入尺寸为256x256或512x512(不同导出配置有差异),最终输出的alpha通道尺寸与输入一致。
关键点是,这个网络不需要trimap作为额外输入,而是“自动学习”哪里需要精细边缘,这大大降低了安卓端的输入复杂度。用户拍照或选图后,直接丢进网络,得到的就是0到1的alpha灰度图。
2.2 PaddleSeg导出到Paddle Lite的转换路径
训练好的PaddleSeg模型不能直接塞进安卓工程。Paddle Seg训练产出的模型文件是model.pdmodel(网络结构)和model.pdiparams(权重),安卓端运行需要Paddle Lite格式(.nb文件)。转换分两步:
# 第一步:PaddleSeg导出推理模型(在服务器或PC上执行) python export.py \ --config configs/matting/modnet/modnet_mobilenetv3_large_8x8_256x256.yml \ --model_path output/best_model/model.pdparams \ --save_dir export_output \ --input_shape 1 3 256 256 # 第二步:用Paddle Lite opt工具将推理模型转成.nb文件 paddle_lite_opt \ --model_file=export_output/model.pdmodel \ --param_file=export_output/model.pdiparams \ --optimize_out=human_matting_model \ --valid_targets=arm \ --quantize=true参数说明:
--input_shape需要与模型训练时的输入尺寸一致,PaddleSeg的ModNet配置中通常为1x3x256x256。如果导出时指定了动态shape,安卓端Java代码里获取Tensor时要调用setShapeFromIndex,尽量保持静态shape,省去动态shape分支的兼容处理。--quantize=true表示开启量化。对ModNet这种中小型模型,量化后权重从FP32降到FP16,体积减少约50%,推理速度提升20%~30%,精度损失在头发丝边缘区域肉眼几乎不可见。Paddle Lite在ARM上对FP16有专门优化(arm_fp16内核),中端手机和旗舰手机都能吃到这个红利。valid_targets=arm指定目标平台为ARM CPU,如果你的demo要跑在GPU或NPU上,需要改为opencl或者npu,但对应的Paddle Lite lib包也要选择包含GPU/NPU的版本。
转换完成后拿到human_matting_model.nb,把它放到安卓工程的app/src/main/assets/model/目录下。这是后续所有Java层推理调用的输入起点。值得留意的是,PaddleSeg官方提供的modnet_mobilenetv3_large_8x8_256x256.yml配置中,8x8指的是训练时的batch_size和GPUs数量,跟推理无关,不要误解成输入尺寸。
2.3 模型文件在安卓工程中的组织方式
解压human_matting_android_demo.zip后,目录结构是标准的Android Gradle工程。与模型部署直接相关的路径如下:
human_matting_android_demo-main/ ├── app/ │ ├── src/main/ │ │ ├── assets/model/human_matting_model.nb │ │ ├── java/.../PaddleLiteWrapper.java │ │ └── res/ │ ├── build.gradle │ └── proguard-rules.pro ├── gradle/ ├── build.gradle ├── settings.gradle └── local.propertieslocal.properties文件通常记录本机的SDK路径,这个文件不应提交到Git仓库(demo的.gitignore已经做了排除),但解压后直接在Android Studio中打开会自动生成。编码时注意:build.gradle里建议将abiFilters设置为arm64-v8a和armeabi-v7a,Paddle Lite的libpaddle_lite_jni.so会根据ABI加载。如果你的测试机是较新的64位设备,只保留arm64-v8a可以减小APK体积约15MB。
3. 安卓端推理链路:从Bitmap到alpha通道
3.1 加载模型与创建Predictor
Paddle Lite在安卓端通过JNI桥接Java层与C++推理内核。Java层的入口是PaddlePredictor,配置信息包装在MobileConfig中。demo中PaddleLiteWrapper.java的核心初始化代码大致如下:
// PaddleLiteWrapper.java import com.baidu.paddle.lite.MobileConfig; import com.baidu.paddle.lite.PaddlePredictor; import com.baidu.paddle.lite.Tensor; public class PaddleLiteWrapper { private PaddlePredictor predictor; private Tensor inputTensor; private Tensor outputTensor; public boolean init(Context context, String modelPath) { MobileConfig config = new MobileConfig(); config.setModelFromFile(modelPath); // 直接从assets复制到缓存目录后的路径 config.setThreads(4); // 四线程跑CPU推理 config.setPowerMode(PowerMode.LITE_POWER_HIGH); // 性能优先,不受限频 predictor = PaddlePredictor.createPaddlePredictor(config); inputTensor = predictor.getInput(0); outputTensor = predictor.getOutput(0); return predictor != null; } }逻辑说明:
setThreads(4)对应手机的四核或八核中的四个性能核。线程数设太高(比如8)反而会因核间调度和内存带宽竞争掉帧,实测4线程是ModNet在多数骁龙平台上的甜点值。setPowerMode(LITE_POWER_HIGH)让CPU在高频模式下运行,这会带来额外发热。对demo足够,但正式产品建议改为LITE_POWER_BALANCED,在大核和功耗之间折中。
3.2 输入数据的归一化与Tensor填充
ModNet在PaddleSeg中的输入标准化参数是mean=[0.5, 0.5, 0.5],std=[0.5, 0.5, 0.5](即像素从[0,255]映射到[-1,1])。在将Bitmap数据交给Tensor之前,需要按该规则做像素级变换。注意Android的Bitmap默认是ARGB_8888格式,而Paddle Lite的Tensor期望的是RGB三通道连续内存:
// 将Bitmap按RGBA读取,再剔除Alpha通道得到RGB,按CHW排布 public float[] bitmapToNormArray(Bitmap bitmap, int width, int height) { int[] pixels = new int[width * height]; bitmap.getPixels(pixels, 0, width, 0, 0, width, height); float[] rgb = new float[3 * width * height]; for (int i = 0; i < pixels.length; i++) { int color = pixels[i]; float r = ((color >> 16) & 0xFF) / 255.0f; float g = ((color >> 8) & 0xFF) / 255.0f; float b = (color & 0xFF) / 255.0f; // mean=0.5, std=0.5 -> (x - 0.5) / 0.5 = 2x - 1 rgb[i] = 2.0f * r - 1.0f; // 通道0:R rgb[i + width * height] = 2.0f * g - 1.0f; // 通道1:G rgb[i + 2 * width * height] = 2.0f * b - 1.0f; // 通道2:B } return rgb; }参数说明:
[0.5, 0.5, 0.5]不是随意取的,它匹配训练时的数据增强。若你用paddle.vision.transforms.Normalize换过mean/std,这里的数值必须同步修改,否则推理结果会整体偏灰或偏黑。- 内存排布是NCHW。也就是先放Channel0(R)所有像素,再放Channel1(G)所有像素,最后放Channel2(B)。如果按NHWC排布,模型输出的alpha会整体偏移或呈噪声状,这是最常见的接入错误。
3.3 执行推理并取回alpha结果
Tensor填充完毕后,调用predictor.run()执行前向计算。ModNet的S3分支输出shape为[1, 1, H, W],其中H和W与输入尺寸一致(256x256),数值范围在[0, 1]之间(sigmoid直接输出)。将输出转换成灰度Bitmap后即可用于合成:
// 推理 predictor.run(); // 取输出Tensor float[] output = outputTensor.getFloatData(); int outW = 256; int outH = 256; // 将0~1的浮点alpha映射为0~255的灰度值 Bitmap alphaBitmap = Bitmap.createBitmap(outW, outH, Bitmap.Config.ALPHA_8); for (int y = 0; y < outH; y++) { for (int x = 0; x < outW; x++) { float alpha = Math.min(Math.max(output[y * outW + x], 0.0f), 1.0f); alphaBitmap.setPixel(x, y, (int) (alpha * 255.0f)); } }这段代码里的getFloatData()返回的是行优先的连续数组,索引y * outW + x对应输出特征图的第y行第x列。ALPHA_8格式的Bitmap只保存一个8位alpha通道,正好用来当作mask或直接参与混合。如果你要预览每一路分支的中间输出(比如只看S2的边缘高亮),需要在PaddleSeg导出时保留中间层节点,demo没有保留,因此只取最后的融合结果。
4. 人像合成与背景替换的实现细节
4.1 前景与背景的alpha混合公式
拿到alpha mask之后,人像替换背景本质上就是一个alpha blending计算。标准公式为:
output_pixel = alpha * fg_pixel + (1 - alpha) * bg_pixel
这里的alpha是归一化到0~1的浮点数。demo中提供了三张测试图:demo.jpg(原图)、human.jpg(纯人像图)、bg.jpg(背景图),它们分别对应原图、alpha mask生成结果、待替换背景。实际替换流程是:
// ImageBlender.java public Bitmap composite(Bitmap fg, Bitmap bg, Bitmap alphaMask) { int w = fg.getWidth(); int h = fg.getHeight(); Bitmap result = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int fgColor = fg.getPixel(x, y); int bgColor = bg.getPixel(x, y); int alpha = alphaMask.getPixel(x, y) & 0xFF; // 0~255 float a = alpha / 255.0f; int r = (int) (((fgColor >> 16) & 0xFF) * a + ((bgColor >> 16) & 0xFF) * (1 - a)); int g = (int) (((fgColor >> 8) & 0xFF) * a + ((bgColor >> 8) & 0xFF) * (1 - a)); int b = (int) ((fgColor & 0xFF) * a + (bgColor & 0xFF) * (1 - a)); result.setPixel(x, y, (0xFF << 24) | (r << 16) | (g << 8) | b); } } return result; }逻辑说明:fg.getPixel()返回ARGB四通道值,部分Android机型可能包含预乘alpha信息,对非透明前景(demo.jpg)无影响。如果前景本身是透明PNG,需要先做去预乘处理(将RGB除以alpha)再参与混合。循环里对每个像素分别计算R、G、B通道,这种CPU实现适合演示,对着256x256的素材一帧约5ms。
4.2 边缘柔化与发丝保留技巧
ModNet输出的alpha在发丝区域往往处于0.3~0.8之间的半透明值,直接二值化(alpha>0.5当白,否则当黑)会让头发看起来像剪贴画。两个工程上常用的处理技巧:
- 对alpha做轻高斯模糊(radius=1)可以压掉边缘噪点,但会稍微损失发丝细节。对于发丝密集的区域,优先保留原始alpha不做模糊,只对背景边缘过渡带模糊。
- alpha值的线性拉伸:将原alpha范围[0.1, 0.9]拉伸到[0, 1],增加明暗对比,让发丝看起来更干净。实现公式:
alpha_new = (alpha - 0.1) * (1.0 / 0.8),clip到[0,1]。
demo中默认没有做后处理,直接输出S3分支的原始结果。如果你要处理室外复杂背景(树叶间隙、风吹衣角),建议在PaddleLiteWrapper.predict()返回后、进入Bitmap转换前加上这两个步骤。
4.3 输出分辨率与输入尺寸的关系
ModNet模型输入是256x256,但原图可能是1080x1920。PaddleSeg导出时指定了静态输入shape,因此安卓端必须先将Bitmap缩放到256x256再送进网络,输出alpha mask同时是256x256。把mask放大回原图尺寸时,用Matrix.setScale(w / 256f, h / 256f)配合Bitmap.createBitmap做线性插值,边缘不会出现锯齿(MipMap效果)。但注意:放大后的mask边缘是软的,这是合理现象,因为256x256输入本就不能表达像素级的发丝细节。如果对边缘质量要求高,建议改用PaddleSeg提供的高分辨率导出配置(512x512),推理时间会从约30ms涨到约80ms(骁龙8系)。
| 处理阶段 | 尺寸 | 耗时(骁龙778G) | 说明 |
|---|---|---|---|
| Bitmap缩放+归一化 | 256x256 | ~3ms | Matrix缩放,单线程 |
| CPU推理(4线程) | 1x3x256x256 | ~28ms | FP32模式 |
| 输出转Bitmap | 256x256 | ~2ms | ALPHA_8格式 |
| alpha放大至原图 | 1080x1920 | ~6ms | 线性插值 |
| alpha合成背景 | 1080x1920 | ~12ms | CPU逐像素 |
5. 模型量化、线程数调优与验证方法
5.1 量化对不同机型的影响
.nb文件在转换时加了--quantize=true,但这仅对权重做FP16/INT8量化,运行时激活值仍以FP32计算。在骁龙8 Gen 1上,FP16推理比FP32快约25%,在麒麟中端芯片上差异更明显。如果你的项目对包体大小不敏感,可以尝试INT8完全量化,体积最小,但ModNet的S2分支对边缘细节敏感,INT8在高频边缘容易产生条纹状伪影。推荐方案是对S1分支保持高精度,但这个策略在Paddle Lite的静态图上做不到,所以实际取舍是:demo阶段跑FP32,发布版跑FP16。
5.2 把线程数和输入尺寸做成动态配置
正式工程中不要硬编码线程数。demo里setThreads(4)写死,但低端机(4核A53)和高性能机(8核X2+A710)的最优线程数不同。建议在设置页暴露两个选项:
// 运行时动态创建Predictor,切换线程数 public void updateConfig(int threads, String sizeMode) { MobileConfig config = new MobileConfig(); config.setModelFromFile(modelPath); config.setThreads(threads); config.setPowerMode(PowerMode.LITE_POWER_BALANCED); if (predictor != null) { predictor.close(); // 释放旧实例 } predictor = PaddlePredictor.createPaddlePredictor(config); inputTensor = predictor.getInput(0); }切换线程数后predictor.close()是必须的,否则C++层会残留上一次会话的线程池,占着内存不释放。同时要注意:PaddlePredictor不是线程安全的,多线程调用run()会崩溃,一种折中是多个Predictor实例并发跑不同输入图。
5.3 验证抠图质量的可复现手段
验证不只是肉眼看demo.jpg效果,需要用像素级的指标。推荐在两个维度做量化对比:SAD(绝对误差和)与MSE(均方误差),这两个指标在PPM100数据集评测中用来衡量alpha matte质量和真实alpha的差距。demo中给出三张图恰好能组成最小验证集——用同一张人像分别跑CPU和另一台设备或模拟器,比较输出的human.jpg灰度分布直方图。具体做法是导出一张PNG格式的alpha图,用Python脚本计算两个批量结果之间的差异:
import cv2 import numpy as np alpha_ref = cv2.imread('output/alpha_reference.png', cv2.IMREAD_GRAYSCALE) alpha_test = cv2.imread('output/alpha_quantized.png', cv2.IMREAD_GRAYSCALE) # SAD:平均绝对像素差 sad = np.mean(np.abs(alpha_ref.astype(np.float32) - alpha_test.astype(np.float32))) # MSE mse = np.mean((alpha_ref.astype(np.float32) - alpha_test.astype(np.float32)) ** 2) print(f"SAD: {sad:.4f}, MSE: {mse:.4f}") # 发丝区域单独统计(取alpha在40~200之间的像素) mask = (alpha_ref > 40) & (alpha_ref < 200) edge_sad = np.mean(np.abs(alpha_ref[mask] - alpha_test[mask])) print(f"Edge SAD: {edge_sad:.4f}")参数说明:
- SAD接近0表示两张alpha几乎一致;量化后SAD控制在5以下,肉眼无法感知差异。
- 单独统计发丝边缘区域(alpha值处于40~200之间的像素)的SAD尤为重要,因为ModNet的设计初衷就是保证这些半透明区域的精度,如果量化后Edge SAD超过15,说明量化配置过于激进,应放弃INT8或改用FP16。
最后提到的验证点和调参路径都收敛在human_matting_android_demo这个工程内,处理换背景、调线程、量化压缩三步做完,你手上的demo就具备直接往产品里放的底气了。
本文还有配套的精品资源,点击获取