综合项目:智能相册应用(二)——实现图像分类模块、人脸检测模块、UI集成
上一章我们把智能相册的架子搭好了,数据层和基础UI都有了。这一章,咱们来给它装上“眼睛”和“大脑”——图像分类和人脸检测。这两个模块一上,你的相册就不再是冷冰冰的文件列表了,它能看懂照片里是什么,还能认出谁是谁。
我个人习惯把这类AI模块拆成独立的“引擎”类,这样测试方便,以后换模型也容易。你想想看,如果所有逻辑都写在Activity里,那代码得多乱?
图像分类模块:给照片打标签
图像分类说白了就是让手机看懂照片内容。我用的是Google官方的ImageNet分类模型,在Android上跑起来很流畅。
核心思路:加载预训练模型 → 预处理图片 → 执行推理 → 解析结果
先看代码,我写了一个ImageClassifier类,封装了NNAPI的调用逻辑:
public class ImageClassifier {
private NNAPI nnapi;
private Model model;
private int inputIndex, outputIndex;
public ImageClassifier(Context context) {
// 加载模型文件
try {
model = Model.createFromFile(context, "mobilenet_v2_1.0_224.tflite");
nnapi = new NNAPI(model);
inputIndex = model.getInputIndex(0);
outputIndex = model.getOutputIndex(0);
} catch (Exception e) {
Log.e("ImageClassifier", "模型加载失败", e);
}
}
public String classify(Bitmap bitmap) {
// 1. 预处理:缩放到224x224,归一化
Bitmap resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true);
float[] input = preprocess(resized);
// 2. 执行推理
float[] output = new float[1001];
nnapi.execute(inputIndex, input, outputIndex, output);
// 3. 解析结果:取top-1标签
int maxIdx = argmax(output);
return loadLabel(context, maxIdx);
}
private float[] preprocess(Bitmap bitmap) {
// 嗯,这里要注意:NNAPI要求输入是float数组,范围[0,1]
float[] result = new float[224 * 224 * 3];
int[] pixels = new int[224 * 224];
bitmap.getPixels(pixels, 0, 224, 0, 0, 224, 224);
for (int i = 0; i < pixels.length; i++) {
result[i * 3] = ((pixels[i] >> 16) & 0xFF) / 255.0f;
result[i * 3 + 1] = ((pixels[i] >> 8) & 0xFF) / 255.0f;
result[i * 3 + 2] = (pixels[i] & 0xFF) / 255.0f;
}
return result;
}
}
避坑指南:我曾经在预处理上栽过跟头。模型训练时用的是RGB顺序,但Android的Bitmap默认是ARGB。如果你直接拿像素值塞进去,分类结果会完全不对。一定要把Alpha通道去掉,并且按RGB顺序排列。
分类结果怎么用?我把它存在数据库里,这样相册首页就能直接展示标签云了。你想想看,用户一打开App就看到“风景”、“美食”、“宠物”这些标签,体验是不是好很多?
人脸检测模块:认出照片里的人
人脸检测比图像分类复杂一些。我选的是Google的FaceNet模型,它不仅能检测人脸位置,还能生成128维的特征向量——说白了就是给每张脸算一个“指纹”。
为什么要特征向量?因为有了它,我们才能做聚类。同一个人的脸,特征向量之间的距离很小;不同人的脸,距离很大。
public class FaceDetector {
private NNAPI nnapi;
private Model model;
public FaceDetector(Context context) {
model = Model.createFromFile(context, "facenet.tflite");
nnapi = new NNAPI(model);
}
public List<FaceResult> detect(Bitmap bitmap) {
// 1. 先做人脸检测(用MTCNN或类似算法)
List<Rect> faces = detectFaces(bitmap);
// 2. 对每张脸提取特征
List<FaceResult> results = new ArrayList<>();
for (Rect rect : faces) {
Bitmap faceBitmap = cropFace(bitmap, rect);
float[] embedding = extractEmbedding(faceBitmap);
results.add(new FaceResult(rect, embedding));
}
return results;
}
private float[] extractEmbedding(Bitmap faceBitmap) {
// 缩放到160x160(FaceNet的输入尺寸)
Bitmap resized = Bitmap.createScaledBitmap(faceBitmap, 160, 160, true);
float[] input = preprocess(resized);
float[] output = new float[128];
nnapi.execute(0, input, 0, output);
return output;
}
}
性能警告:人脸检测很吃CPU。如果你在列表滑动时实时检测,手机会发烫。我的做法是:只在照片导入时做一次检测,结果存数据库。用户查看照片时直接读缓存,不重复计算。
特征向量存下来之后,怎么把同一个人聚到一起?我用的是简单的K-Means算法,K值动态调整。每次新照片入库,就重新跑一次聚类。嗯,这里要注意:聚类结果要存到数据库里,不然每次启动App都要重新算一遍。
UI集成:让AI能力可见
模块写好了,得让用户看得见摸得着。我设计了三个核心界面:
| 界面 | 功能 | 数据来源 |
|---|---|---|
| 标签页 | 按分类标签展示照片 | ImageClassifier结果 |
| 人物页 | 按人脸聚类展示照片 | FaceDetector + K-Means |
| 详情页 | 单张照片的AI信息 | 两者结合 |
标签页的实现很简单,就是一个RecyclerView,每个item显示一个标签名和对应的照片数量。点击标签,跳转到该标签下的照片列表。
人物页稍微复杂一点。每个“人物”其实是一个聚类中心,我给它取了个临时名字叫“人物1”、“人物2”。用户也可以手动改名,比如改成“妈妈”、“爸爸”。
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder> {
private List<PersonCluster> clusters;
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
PersonCluster cluster = clusters.get(position);
holder.nameView.setText(cluster.getDisplayName());
holder.countView.setText(cluster.getPhotoCount() + "张照片");
// 显示聚类中最近的一张照片作为封面
Glide.with(holder.itemView)
.load(cluster.getCoverPhotoUri())
.into(holder.coverView);
}
}
体验优化:我建议在人物页加一个“合并”功能。有时候聚类算法会把同一个人分成两个簇,用户可以把它们手动合并。这个功能实现起来不复杂,就是把两个簇的特征向量合并,重新计算中心点。
SVG:智能相册AI模块架构图
这张图把整个流程串起来了。你看,照片进来后兵分两路:一路做分类,一路做人脸检测。结果分别存到数据库里,最后在UI层汇总展示。模块之间完全解耦,想换模型?改一个类就行。
踩过的坑和优化建议
最后分享几个我在项目中遇到的坑:
- 模型加载慢:第一次加载模型可能要几百毫秒。我建议在Application.onCreate里异步预加载,别让用户等。
- 内存泄漏:NNAPI的模型对象很大,一定要在Activity销毁时释放。我写了一个
ModelManager单例,统一管理模型的生命周期。 - 线程管理:AI推理不能在主线程跑。我用的是
CoroutineScope(Dispatchers.Default),配合LiveData把结果抛回主线程。 - 模型兼容性:不同手机的NNAPI驱动版本不一样。我遇到过小米手机上模型跑不起来的情况,最后加了回退方案——如果NNAPI不支持,就自动切换到CPU模式。
核心总结:图像分类和人脸检测是智能相册的两大支柱。分类让照片可搜索,人脸检测让照片可归类。两者结合,你的相册就从“文件管理器”升级成了“智能管家”。
代码写到这里,智能相册的核心功能已经齐了。下一章我们会做搜索和推荐,让相册更“懂”用户。嗯,不过那是后话了,先把这章的代码跑通再说。
公众号:蓝海资料掘金营,微信deep3321