外挂三部曲(三) —— Android 图片相似度对比

本文已授权郭霖公众号独家发布

一、第一种对比方式

第一种对比方式是:取出两张 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

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值