前言
现在大部分人的手机自带相册都会有智能分类的功能,会将手机里的照片按照人脸、场景、地点、照片风格等特点进行分类,方便用户查看。
这种分类大概率是使用了机器学习的功能,通过特定的模型对图片进行扫描,基于扫描的结果进行聚类展示。通过之前 Android 机器学习组件-图像标签初探 一文,我们已经学会了使用 Android 官方提供的图像标签组件,识别单个图像内包含信息的功能。我们以此为基础进行扩展,对多张图片进行识别,根据识别结果把具有相同(或相似)标签的图片聚合在一起,就可以实现图片场景分类的功能了。
图片选择器实现场景分类
日常使用 App 的过程中,当我们想在社交媒体上发布图片,无论是斗图还是想分享一张珍藏了很久的图片时,都会遇到找一张图片找好久,在不同的相册目录里来回翻找,要花费很长的时间的场景。因为现在大多数 App 的相册都是基于文件夹进行了简单的归类,对于图片的特征信息没有进行利用,因此也就无法基于图片的特征进行检索。
这方面做的比较好的还是微信,微信图片选择器的归类非常丰富,除了常规的文件夹分类,还有收藏这个类目,最实用的还是图片对图片信息特征的使用,基于时间、地点、人物、智能场景等特征对图片进行了分类,还可以基于这些场景进行搜索。
基于 ML Kit 图像标签组件实现图片聚类展示
根据前文Android 机器学习组件-图像标签初探我们知道
- image-labeling 组件会基于输入的图片信息返回图像包含的多个标签信息,并基于标签的可信度依次返回。
- 每一次对图片进行处理都是异步进行的,因为模型从图片中获取信息是一个耗时的过程。
因此,如果需要对大量的图片进行图像标签的识别,需要通过线程池去处理。这里先用手机相册的所有图片测试一下效果。
获取所有标签
kotlin
fun getLabel(context: Context, uris: List<Uri>, callback: (() -> Unit)? = null) {
val cachedThreadPool = Executors.newFixedThreadPool(4)
var count = 0
val start = System.currentTimeMillis()
uris.forEach {
cachedThreadPool.submit {
// 获取图像标签
getLabel(context, it) {
count++
if (count >= uris.size) {
Log.d(
TAG,
"total cost ${(System.currentTimeMillis() - start) / 1000f} seconds on ${
uris.size
} picture"
)
}
}
}
}
}
这里我们通过线程池去处理每一张图片的识别过程,由于识别的过程中会存在失败的情况,因此通过任务计数的方式来确认所有图片识别完成,我们看一下输出。
shell
11:15:42.237 ImageLabelHelper D total cost 9.563 seconds on 122 picture
在 Pixel 2 的手机上,122 张图片耗时 9 秒,这个耗时还是挺长的。
获取图像标签
kotlin
fun getLabel(context: Context, uri: Uri, callback: () -> Unit) {
val image: InputImage
var categories = HashMap<Int, ArrayList<Uri>>()
try {
image = InputImage.fromFilePath(context, uri)
labeler?.process(image)?.addOnSuccessListener { labels ->
for (label in labels) {
val index = label.index
var list = categories[index]
if (list == null) {
list = ArrayList()
list.add(uri)
}
// 将图片的包含信息按找标签索引进行聚类,
if (categories[index] == null) {
categories[index] = list
} else {
categories[index]?.add(uri)
}
callback()
// 获取第一个就返回,用置信度最高的那个即可
break
}
}?.addOnFailureListener { e ->
callback()
Log.e(TAG, e.stackTraceToString())
}
} catch (e: Exception) {
callback()
e.printStackTrace()
}
}
对于任意一张图片,获取到标签信息后
- 将根据标签索引进行聚类处理,所有属于同一个标签的图片都归类到一个列表里
- 只获取第一个标签即可,因为这个标签基本上就可以代表这个图片的内容。
基于标签信息对图片进行聚类
kotlin
fun getLabel(context: Context, uris: List<Uri>, callback: (() -> Unit)? = null) {
val cachedThreadPool = Executors.newFixedThreadPool(4)
var count = 0
val start = System.currentTimeMillis()
uris.forEach {
cachedThreadPool.submit {
getLabel(context, it) {
count++
if (count >= uris.size) {
convertToLabels(context)
callback?.invoke()
}
}
}
}
}
private fun convertToLabels(context: Context) {
val result = ArrayList<Labels>(categories.keys.size)
val tags = JsonUtil.readJsonStr(context, "tags.json")
val tagsMap = JSONObject.parseObject(tags)
categories.forEach {
val key = it.key
val list = it.value
val realKey: String = tagsMap.getString(key.toString())
val labels = Labels(realKey, list)
result.add(labels)
}
labelList.clear()
labelList.addAll(result)
Log.e(TAG, result.toString())
}
当我们获取到所有的标签索引之后,就可以基于标签索引来映射图片的真实信息了。这里 tags.json
的信息可以参考 标签索引 ,当前手机里图片的归类信息如下(截取部分)。
shell
[Labels(label=团队, subs=[content://media/external/images/media/100064]), Labels(label=手机, subs=[content://media/external/images/media/3224]), Labels(label=摩天轮, subs=[content://media/external/images/media/101122, content://media/external/images/media/3227]), Labels(label=标记, subs=[content://media/external/images/media/5569]), Labels(label=有趣, subs=[content://media/external/images/media/5305, content://media/external/images/media/3332]), Labels(label=新闻, subs=[content://media/external/images/media/100343]), Labels(label=牛仔裤, subs=[content://media/external/images/media/100341]), Labels(label=玩具, subs=[content://media/external/images/media/5487, content://media/external/images/media/97544, content://media/external/images/media/4262, content://media/external/images/media/2182]))]
...
基于这个信息我们就可以展示一个简单版本的智能分类场景下的相册列表了。
从结果来看,image-labeling 组件的识别效果还是挺不错的,对于图像的主要特征能够做出比较好的识别。 有了图像标签信息,就可以针对这些进行图片选择,基于用户输入的文本进行标签的过滤了。
上述相关实例完整代码可以参考 Matisse
小结
在没有类似 image-labeling 这类组件的时候,我们对图像这种媒介的聚类只能基于图片在压缩编码时携带的信息进行处理。比如拍摄时间、拍摄地点、图片格式等这种传统信息进行划分和聚类展示。但是,有了 image-labeling 这样的机器学习组件,我们就可以获取一些原本只有人的肉眼可以识别的信息了。而且最重要的一点是,这些组件可以离线直接在本地使用,不用将图片上传到服务器,也避免了用户隐私的问题。
基于以上思路我们基本可以做出一个图片智能分类的 demo 了。分类效果是否足够准确,其实很大程度上依赖所用的模型,模型训练的效果越好,识别的效果也就越好。
但是除了这个问题,其实还有很多细节需要处理,比如用图像标签组件进行图像识别很耗时,那么每次用户打开图片选择器的时候都要重新全量扫码吗 ? 把图像标签信息存储下来,相册里的图片有变动又该如何处理?下一篇我们接着解决这些问题。