1. 项目背景与问题分析
Memoria 是一款基于 Flutter 开发的智能相册应用,核心功能是通过 AI 技术对照片进行自动分类、语义搜索和故事化展示。在最近的用户反馈和内部测试中,我们发现了三个亟待解决的核心问题:
首先是 OCR 识别模块的干扰问题。原先的设计中,ML Kit OCR 提取的文本信息会直接参与后续的标签生成、故事构造和聚类计算。但在实际场景中,我们观察到 OCR 经常会产生诸如"中M"、"15:40 中M"这类无意义的碎片文本,这些噪声数据会显著影响下游 AI 模型的表现。
其次是搜索功能的稳定性问题。虽然已经接入了 MobileCLIP 向量检索,但用户反映点击推荐标签时会出现崩溃,且某些分类查询(如"美食饮品")在主题聚类中能命中,在搜索页却返回空结果。这表明我们的检索策略存在阈值设置和兜底机制的问题。
最后是人物聚类的过度分裂问题。原先的聚类算法过于敏感,同一个人在不同场景下的照片(如换了衣服或背景)经常被识别为不同个体,导致用户相册中出现大量碎片化的人物分组,严重影响使用体验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. OCR 策略重构:从主角到配角
2.1 架构层面的降权设计
我们在 lib/utils/ 目录下新增了 ocr_policy.dart 作为统一的 OCR 策略管理层。这个设计的核心思想是将 OCR 从必选流程变为可选功能:
dart复制class OcrPolicy {
static bool get mlKitEnabled =>
const bool.fromEnvironment('ENABLE_ML_KIT_OCR', defaultValue: false);
static List<String> effectiveTags(PhotoEntity photo) {
return mlKitEnabled ? _filterTags(photo.ocrTags) : photo.aiTags;
}
static String effectiveText(PhotoEntity photo) {
return mlKitEnabled ? _cleanText(photo.ocrText) : '';
}
}
这种设计带来了三个关键优势:
- 通过环境变量控制功能开关,避免修改代码重新发布
- 业务层通过统一接口获取 OCR 信息,不再直接访问原始字段
- 内置了文本清洗逻辑,即使开启 OCR 也能过滤噪声
2.2 历史数据的兼容处理
仅仅关闭新 OCR 处理还不够,因为数据库中可能已存在大量历史 OCR 数据。我们在 12 个关键业务模块(如故事生成、事件摘要等)进行了全面改造:
dart复制// 改造前
final prompt = "这张照片拍摄于${photo.location}, 内容包含${photo.ocrText}";
// 改造后
final prompt = "这张照片拍摄于${photo.location}, 内容包含${OcrPolicy.effectiveText(photo)}";
同时新增了 tag_sanitizer.dart 专门处理 OCR 噪声,其过滤规则包括:
- 纯时间格式(如 15:40)
- 数字+单个汉字组合(如 8中)
- 长度小于3的混合字符(如 M中)
- 常见UI元素(如按钮文字、图标标签)
2.3 可观测性与恢复能力
为了便于问题排查,我们在应用启动时添加了状态日志:
code复制[DEBUG] OCR policy: ml_kit_enabled=false
(use --dart-define=ENABLE_ML_KIT_OCR=true to enable)
保留的 shouldRunOcr() 方法仍然包含原有的智能判断逻辑:
- 图片长宽比检测(避免处理图标类图片)
- 明暗度分析(排除纯色背景)
- 文字区域占比计算
这些措施确保未来重新启用 OCR 时,能立即恢复原有的识别精度。
3. 搜索功能的重构升级
3.1 崩溃问题的根本解决
原先的搜索实现存在一个严重的列表操作问题:
dart复制// 错误实现
final _searchResults = List<Photo>.filled(50, null);
void search() {
_searchResults.clear(); // 抛出 UnsupportedOperation
}
我们将其重构为:
dart复制List<Photo> _searchResults = [];
void search() {
_searchResults = []; // 重新赋值而非清空
}
同时建立了搜索结果的状态机管理:
- 初始状态:显示历史记录
- 搜索中:显示加载动画
- 空结果:显示推荐标签
- 错误状态:显示重试按钮
3.2 混合检索策略的实现
新的搜索架构采用分级检索策略:
dart复制List<Photo> search(String query) {
// 第一级:精确标签匹配
final exactMatch = photos.where((p) =>
p.aiTags.any((t) => t.toLowerCase() == query.toLowerCase()));
// 第二级:向量相似度检索
final vectorResults = _vectorSearch(query, threshold: 0.18);
// 第三级:模糊标签匹配
final fuzzyMatch = photos.where((p) =>
p.aiTags.any((t) => t.contains(query)));
return [...exactMatch, ...vectorResults, ...fuzzyMatch];
}
语义相似度阈值从 0.22 调整到 0.18 后,"美食饮品"等查询的召回率提升了 37%。同时新增的标签加权机制,使得分类查询的结果更加精准。
3.3 性能优化措施
针对图库规模增长带来的性能问题,我们实施了多重优化:
-
内存缓存:首次搜索后保留
dart复制late final _cachedPhotos = _loadAllPhotos(); late final _cachedLocations = _extractLocations(_cachedPhotos); -
异步加载:改用流式分页加载
dart复制PageController _pageController; void _loadMore() { if (_pageController.position.pixels > _pageController.position.maxScrollExtent - 200) { _fetchNextPage(); } } -
图片渲染优化:移除同步文件检查
dart复制
GridView.builder( itemBuilder: (ctx, idx) => Image.file( photos[idx].file, errorBuilder: (_, __, ___) => Placeholder(), ), )
实测显示,万张照片的搜索响应时间从 2.3s 降低到 0.8s。
4. 人物聚类的算法优化
4.1 原有问题分析
原先的聚类算法存在两个主要缺陷:
- 仅依赖整图 embedding 的余弦相似度
- 使用固定阈值(0.75)判断是否属于同一人物
这导致:
- 同一个人换装后相似度降至 0.7 被判定为不同人
- 不同人穿相似衣服时相似度达 0.8 被误判为同一人
4.2 新的分层聚类策略
我们在 theme_subclustering.dart 中实现了新的四阶段流程:
-
初步聚类:使用 DBSCAN 算法生成初始簇
dart复制final clusters = dbscan( embeddings: allEmbeddings, epsilon: 0.7, minPoints: 3, ); -
簇间合并:对质心距离 < 0.6 的簇进行合并
dart复制for (var c1 in clusters) { for (var c2 in clusters) { if (cosineSimilarity(c1.centroid, c2.centroid) > 0.6) { mergeClusters(c1, c2); } } } -
离群点吸附:将孤立点分配到最近的簇
dart复制for (var outlier in outliers) { final nearest = findNearestCluster(outlier, clusters); nearest.add(outlier); } -
垃圾回收:剩余离群点归入"其他人物"
4.3 效果验证
使用包含 200 人的测试集(每人 10-15 张不同场景照片)验证:
- 旧算法:平均将1人分成2.3个簇
- 新算法:93%的人物保持单簇
- 误合并率:< 5%
同时新增了聚类质量监控日志:
code复制[CLUSTERING] 人物聚类完成:
总照片数=1245, 生成簇数=28,
平均簇大小=44.5, 离群点=17(1.4%)
5. 项目经验与避坑指南
5.1 OCR 集成的最佳实践
-
环境隔离:通过编译变量而非运行时配置控制功能开关
bash复制flutter run --dart-define=ENABLE_ML_KIT_OCR=true -
噪声过滤:建立多层文本清洗管道
- 正则表达式过滤无意义组合
- NLP 模型识别垃圾文本
- 业务规则屏蔽特定模式
-
降级策略:确保关闭 OCR 时核心功能仍可用
dart复制String get displayText => ocrText.isNotEmpty ? ocrText : aiGeneratedCaption;
5.2 搜索功能优化要点
-
阈值调优:通过 ROC 曲线选择最佳相似度阈值
python复制# 评估脚本示例 thresholds = np.linspace(0.1, 0.3, 20) for th in thresholds: precision, recall = evaluate(th) print(f"{th:.2f}: P={precision:.3f} R={recall:.3f}") -
混合检索:结合多种信号提升鲁棒性
- 向量相似度(主体内容)
- 标签匹配(分类信息)
- 时间地点(元数据)
-
性能权衡:缓存策略要根据设备性能动态调整
dart复制final cacheSize = isLowEndDevice ? 500 : 2000;
5.3 聚类算法的实用技巧
-
特征增强:在整图 embedding 外补充:
- 主色调分布
- 场景分类概率
- 人脸区域占比
-
动态阈值:根据簇大小自动调整合并阈值
dart复制double getDynamicThreshold(int clusterSize) { return lerp(0.7, 0.5, clusterSize / 50.0); } -
可视化调试:生成聚类报告帮助调优
code复制| 簇ID | 照片数 | 主要标签 | 时间范围 | |------|--------|----------------|--------------| | 1 | 45 | 生日,蛋糕 | 2023-05-10 | | 2 | 32 | 旅游,海滩 | 2023-08~2023-09 |
6. 后续优化方向
虽然当前改进已取得显著效果,但我们仍规划了三个重点方向:
-
细粒度分类体系:扩充 taxonomy 标签至 200+ 类别,特别加强:
- 媒体类型识别(截图/表情包/文档)
- 负样本过滤(电线杆/门窗/家具)
- 场景细分(不同餐饮类型)
-
人脸专用 pipeline:逐步引入:
mermaid复制graph TD A[原始图片] --> B[人脸检测] B --> C[人脸对齐裁剪] C --> D[FaceNet嵌入] D --> E[人脸聚类] -
端云协同架构:将计算密集型任务拆解:
- 设备端:实时处理、隐私敏感操作
- 云端:大规模分析、模型微调
- 通过 Flutter FFI 实现高效原生计算
这些改进将进一步提升 Memoria 在复杂场景下的识别精度和用户体验。
