本文已授权郭霖公众号独家发布
一、第一种对比方式
第一种对比方式是:取出两张 bitmap 中的所有像素,然后一一进行对比。匹配的点除以总点数就能得到一个相似度。代码如下:
object SimilarityUtils {
fun similarity(bitmap1: Bitmap, bitmap2: Bitmap): Double {
// 获取图片所有的像素
val pixels1 = getPixels(bitmap1)
val pixels2 = getPixels(bitmap2)
// 总的像素点数以较大图片为准
val totalCount = pixels1.size.coerceAtLeast(pixels2.size)
if (totalCount == 0) return 0.00
var matchCount = 0
var i = 0
while (i < pixels1.size && i < pixels2.size) {
if (pixels1[i] == pixels2[i]) {
// 统计相同的像素点数量
matchCount++
}
i++
}
// 相同的像素点数量除以总的像素点数,得到相似比例。
return String.format("%.2f", matchCount.toDouble() / totalCount).toDouble()
}
private fun getPixels(bitmap: Bitmap): IntArray {
val pixels = IntArray(bitmap.width * bitmap.height)
// 获取每个像素的 RGB 值
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
return pixels
}
}
可以看到,similarity
函数接收两个 Bitmap
,返回一个 Double
值,这个值的取值范围是 0.00~1.00,表示相似度。
首先通过 bitmap.getPixels
取出所有的像素点,以其中较多的像素点作为总点数。
然后通过 pixels1[i] == pixels2[i]
对比每个像素点,如果相同则 matchCount
加一,最后用 matchCount / totalCount
计算出相似度。
这种比较方式特别直观,容易理解,通过每个像素点依次比较得出相似度。我们也很容易想到它的缺点:如果第二张图片是由第一张图片缩放、变形、旋转等变换得来的,那么每个像素点可能都无法匹配上,所以相似度会很低很低。
也就是说,这个算法几乎只能用于比较图片是否一模一样,只要两张图的像素点有细微的错位,比较结果就会完全不准确。
不过其实这种算法已经能够满足我们的需求了,只要我们每次都取一样的 Bitmap
进行比较就可以了。只要保证整张图都一样,或者从 Bitmap 裁剪出的固定区域一样就可以了。此时比较结果可以供我们正常使用。
但更好的做法是通过 SIFT 算法计算相似度。
二、通过 SIFT 算法计算相似度
SIFT 算法指的是尺度不变特征转换 (Scale Invariant Feature Transform)。它是计算机视觉领域中描述图片特征的一种算法,应用非常广泛。
这个算法是由一些大神们研究出来的,由于本文不是在写论文,所以我也不会对这个算法进行深究,简单介绍一下它的大概原理:
先将图片映射为空间中的坐标:
再从所有坐标中过滤出其中的特征点:
再为特征点分配一个方向值,使得图片变形后仍然能够正确匹配:
将这些信息转换成数学描述:
注:算法原理的这段内容,只是我个人一点粗浅的理解,可能和算法的实际实现有出入。但这个算法的实现不是本文的重点,重点在于这个算法可以用于对比两张图片的相似度。所以于我而言,我愿将其称之为魔法。
这个算法被封装在 OpenCV 库中,所以使用前需要导入 OpenCV 库。
OpenCV 官方没有提供 gradle 导入的方式,所以网上有许多导入 OpenCV 库的教程,讲的都是去下载 OpenCV 的源码,再通过 Module 的方式加入项目中。
所以现在我们可以直接在 build.gradle 中直接导入 OpenCV 库:
implementation 'com.quickbirdstudios:opencv:4.5.3.0'
需要注意的是,OpenCV 库非常大,导入这个库会让 apk 的体积增加 100 多 M,所以要慎用。
有了 OpenCV 库,就可以编写图片相似度对比工具类了:
object SIFTUtils {
// SIFT detector
private val siftDetector by lazy { SIFT.create() }
fun similarity(bitmap1: Bitmap, bitmap2: Bitmap): Double {
// 计算每张图片的特征点
val descriptors1 = computeDescriptors(bitmap1)
val descriptors2 = computeDescriptors(bitmap2)
// 比较两张图片的特征点
val descriptorMatcher = DescriptorMatcher.create(DescriptorMatcher.FLANNBASED)
val matches: List<MatOfDMatch> = ArrayList()
// 计算大图中包含多少小图的特征点。
// 如果计算小图中包含多少大图的特征点,结果会不准确。
// 比如:若小图中的 50 个点都包含在大图中的 100 个特征点中,则计算出的相似度为 100%,显然不符合我们的预期
if (bitmap1.byteCount > bitmap2.byteCount) {
descriptorMatcher.knnMatch(descriptors1, descriptors2, matches, 2)
} else {
descriptorMatcher.knnMatch(descriptors2, descriptors1, matches, 2)
}
Log.i("~~~", "matches.size: ${matches.size}")
if (matches.isEmpty()) return 0.00
// 获取匹配的特征点数量
var matchCount = 0
// 邻近距离阀值,这里设置为 0.7,该值可自行调整
val nndrRatio = 0.7f
matches.forEach { match ->
val array = match.toArray()
// 用邻近距离比值法(NNDR)计算匹配点数
if (array[0].distance <= array[1].distance * nndrRatio) {
matchCount++
}
}
Log.i("~~~", "matchCount: $matchCount")
return String.format("%.2f", matchCount.toDouble() / matches.size).toDouble()
}
private fun computeDescriptors(bitmap: Bitmap): MatOfKeyPoint {
val mat = Mat()
Utils.bitmapToMat(bitmap, mat)
val keyPoints = MatOfKeyPoint()
siftDetector.detect(mat, keyPoints)
val descriptors = MatOfKeyPoint()
// 计算图片的特征点
siftDetector.compute(mat, keyPoints, descriptors)
return descriptors
}
}
在这个类中,同样有一个similarity
方法,接收两个 Bitmap
,返回一个 0.00~1.00
的 Double
型数据,表示图片的相似度。
首先通过SIFT.create()
构建出用 SIFT
算法实现的图片检测器 siftDetector
,再通过 siftDetector.compute
计算出图片的特征点。
再通过 DescriptorMatcher.create
构建出 descriptorMatcher
对象,通过 descriptorMatcher.knnMatch 方法比较出两张图片相似的特征点数量。
这里比较时有一个if
条件判断,它的作用是保证比较的是大图中包含多少小图中的特征点。因为如果计算小图中包含多少大图的特征点,结果会不准确。
比如:若小图中的 50 个点都包含在大图中的 100 个特征点中,则计算出的相似度为 100%,显然不符合我们的预期。
最后通过array[0].distance <= array[1].distance * nndrRatio
判断特征点是否相似,统计出相似的特征点数量后,通过 matchCount / matches.size
计算出相似度。
三、测试
先在 res/drawable 文件夹下放一张图片,比如我放了一张我的头像,命名为 img.png:
然后修改 MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.img)
val bitmap2 = Bitmap.createBitmap(bitmap1, 0, 0, bitmap1.width / 2, bitmap1.height / 2)
Log.d("~~~", "similarity: ${SIFTUtils.similarity(bitmap1, bitmap2)}")
}
}
首先通过 BitmapFactory.decodeResource 将 res/drawable 文件夹中的图片取出来,转换成 Bitmap,构建出 bitmap1。
bitmap2 由 bitmap1 裁剪而来,通过 Bitmap.createBitmap 方法,从 bitmap1 的 (0, 0) 位置开始,裁剪出宽为原图一半、高为原图一半的 Bitmap。
然后调用 SIFTUtils.similarity(bitmap1, bitmap2)
比较两张图片的相似度。
非常完美!
运行代码,立马 crash:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.imagesimilarity, PID: 21924
java.lang.UnsatisfiedLinkError: No implementation found for long org.opencv.core.Mat.n_Mat() (tried Java_org_opencv_core_Mat_n_1Mat and Java_org_opencv_core_Mat_n_1Mat__)
at org.opencv.core.Mat.n_Mat(Native Method)
at org.opencv.core.Mat.<init>(Mat.java:23)
at com.example.imagesimilarity.SIFTUtils.computeDescriptors(SIFTUtils.kt:50)
at com.example.imagesimilarity.SIFTUtils.similarity(SIFTUtils.kt:19)
at com.example.imagesimilarity.MainActivity.onCreate(MainActivity.kt:38)
at android.app.Activity.performCreate(Activity.java:8000)
果然凡事都没有一帆风顺的。这个报错大致意思是没有找到 OpenCV
中的某个方法的具体实现。奇了怪了,我们明明已经导入过 OpenCV 库了。
查询一番后,在 StackOverflow
上找到了答案,原因是 OpenCV 使用前需要先初始化。
MainActivity 代码修改如下:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val loaded = OpenCVLoader.initDebug()
Log.d("~~~", "loaded: $loaded")
if (loaded) {
val bitmap1 = BitmapFactory.decodeResource(resources, R.drawable.img)
val bitmap2 = Bitmap.createBitmap(bitmap1, 0, 0, bitmap1.width / 2, bitmap1.height / 2)
Log.d("~~~", "similarity: ${SIFTUtils.similarity(bitmap1, bitmap2)}")
}
}
}
在 onCreate 方法中,先调用 OpenCVLoader.initDebug 方法初始化 OpenCV,通过其返回值判断是否加载成功,当加载成功后再执行我们刚才的比较相似度逻辑。
运行程序,Logcat 控制台输出如下:
D/~~~: loaded: true
I/~~~: matches.size: 190
I/~~~: matchCount: 88
D/~~~: similarity: 0.46
表示两张图片的相似度为 46%,说明我们的程序已经正常工作了。
四、后记
到这里,我们的外挂三部曲系列就完结了。这三篇文章讲述了三个独立的技术点:模拟点击、应用外截屏、图像识别。这些技术对用户而言有些风险,所以通常都需要用户手动授权。比如模拟点击前需要用户开启辅助功能,截取屏幕前需要用户同意应用读取屏幕。
为什么没有讲他们的综合运用呢?这实际上是我无奈之举。这些技术像是黑魔法,有些黑科技成分,不便细讲,我平时也只运用在自己的个人手机上,让它们帮我做一些机械的重复工作。
这几篇文章只是给大家介绍锤子、钉子、板子,如何用它们制作桌椅板凳还需要读者亲自动手。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、架构师筑基必备技能
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
二、Android百大框架源码解析
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
三、Android性能优化实战解析
- 腾讯Bugly:对字符串匹配算法的一点理解
- 爱奇艺:安卓APP崩溃捕获方案——xCrash
- 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
- 百度APP技术:Android H5首屏优化实践
- 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
- 携程:从智行 Android 项目看组件化架构实践
- 网易新闻构建优化:如何让你的构建速度“势如闪电”?
- …
四、高级kotlin强化实战
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
-
从一个膜拜大神的 Demo 开始
-
Kotlin 写 Gradle 脚本是一种什么体验?
-
Kotlin 编程的三重境界
-
Kotlin 高阶函数
-
Kotlin 泛型
-
Kotlin 扩展
-
Kotlin 委托
-
协程“不为人知”的调试技巧
-
图解协程:suspend
五、Android高级UI开源框架进阶解密
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
六、NDK模块开发
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
七、Flutter技术进阶
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
作者:wkxjc
链接:https://juejin.cn/post/7113578494864408612