持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
上篇文章我们一起学习了如何截取屏幕,本文介绍如何对比图片相似度,这个功能可以帮助我们将截屏结果与预存的一些图片进行对比,以了解当前处于哪个屏幕。
再通过第一篇文章介绍的辅助点击,就可以给手机整活了。
一、第一种对比方式
第一种对比方式是:取出两张 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 的方式加入项目中。
但国外有民间大佬为我们封装了 gradle 导入的方式,大佬封装的 github 地址:github.com/quickbirdst…
所以现在我们可以直接在 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%,说明我们的程序已经正常工作了。
四、后记
到这里,我们的外挂三部曲系列就完结了。这三篇文章讲述了三个独立的技术点:模拟点击、应用外截屏、图像识别。这些技术对用户而言有些风险,所以通常都需要用户手动授权。比如模拟点击前需要用户开启辅助功能,截取屏幕前需要用户同意应用读取屏幕。
为什么没有讲他们的综合运用呢?这实际上是我无奈之举。这些技术像是黑魔法,有些黑科技成分,不便细讲,我平时也只运用在自己的个人手机上,让它们帮我做一些机械的重复工作。这几篇文章只是给大家介绍锤子、钉子、板子,如何用它们制作桌椅板凳还需要读者亲自动手。
五、参考文章
No implementation found for long org.opencv.core.Mat.n_Mat() error Using OpenCV
继续阅读: