综合项目:智能相册应用(二)——实现图像分类模块、人脸检测模块、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模块架构图

智能相册AI模块架构 照片输入 图像分类引擎 MobileNet V2 人脸检测引擎 FaceNet + MTCNN 分类标签数据库 标签名 + 照片ID 人脸特征数据库 128维向量 + 聚类ID UI展示:标签页 / 人物页 / 详情页 RecyclerView + Glide + 自定义Adapter

这张图把整个流程串起来了。你看,照片进来后兵分两路:一路做分类,一路做人脸检测。结果分别存到数据库里,最后在UI层汇总展示。模块之间完全解耦,想换模型?改一个类就行。

踩过的坑和优化建议

最后分享几个我在项目中遇到的坑:

  • 模型加载慢:第一次加载模型可能要几百毫秒。我建议在Application.onCreate里异步预加载,别让用户等。
  • 内存泄漏:NNAPI的模型对象很大,一定要在Activity销毁时释放。我写了一个ModelManager单例,统一管理模型的生命周期。
  • 线程管理:AI推理不能在主线程跑。我用的是CoroutineScope(Dispatchers.Default),配合LiveData把结果抛回主线程。
  • 模型兼容性:不同手机的NNAPI驱动版本不一样。我遇到过小米手机上模型跑不起来的情况,最后加了回退方案——如果NNAPI不支持,就自动切换到CPU模式。

核心总结:图像分类和人脸检测是智能相册的两大支柱。分类让照片可搜索,人脸检测让照片可归类。两者结合,你的相册就从“文件管理器”升级成了“智能管家”。

代码写到这里,智能相册的核心功能已经齐了。下一章我们会做搜索和推荐,让相册更“懂”用户。嗯,不过那是后话了,先把这章的代码跑通再说。


公众号:蓝海资料掘金营,微信deep3321