好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 --Eternal-- 于 2017-3-12 11:48 编辑
本来想利用OpenCV的工具实现手机上的图片识别,但随着对OpenCV内容的探索,严格地说,我认为所谓的“识别”,只不过是图片的“匹配”而已。因为计算机在图片的相似度比较上太笨了,不像人眼一看就能判断,它需要依赖大量计算的算法方可“匹配”出相似点。而且!根据相似点匹配,从而判断两张图片的内容是否一样,还需要很大的人为主观干涉。目前研究了一下OpenCV关于图片特征匹配的基本内容,基本上实现的原理大致相同,不同的是算法在计算机上的性能表现。要理解原理的话,可以去搜索“sift算法”,可以找到一篇小姐姐写的博客,该博客内容介绍了sift算法的原理和图像的匹配过程,详细得感人,需要掌握一定的高数和线性代数才能看得懂。数学不好的话,实际上也能大概看明白整个过程是怎么样的。类似与sift算法的图像匹配,在计算机上实现的过程基本是: 加载图片-->找关键点-->计算关键点特征-->匹配关键点特征-->判断或者画线展示(非必须)。其中匹配关键点是需要插入一些人为主观判断的,最后一步只是展示用,所以不一定需要。
好吧,扯了那么多,说是图片识别,连张图片都没有,没图说个j八~~好,先放一张原始的测试图:
有人会怀疑,Android用Java开发,用java来做图片处理会不会很慢?其实,这里的java部分只是负责接口而已,实际上还是调用了native层的C/C++代码进行处理的,这个在上一篇关于OpenCV在Android Studio上的搭建博客里已经知道了,ctrl+右键去看源码,会发现最终到native修饰的函数就探索不下去了。
Go now~1. 加载图片。低版本加载图片是用Highgui这个类加载的,但高版本的opencv sdk里把读取图片的加载接口移到了Imgcodecs类去了。
[java] view plain copy - Mat src=Imgcodecs.imread(imgPath);
其中参数imgPath为String类型,即图片的路径。又或者使用Utils类加载图片
[java] view plain copy - Mat src=Utils.loadResource(context, resId);
其中参数为Context,和图片id,如R.drawable.xxx。
Mat类是英文Matrix的缩写,即数学上的矩阵,用于保存图片。图片的在计算机上的处理,基本上也是矩阵的数值作各种变换处理。大学里的线性代数真的要学好呐~
2. 找关键点。找关键点的算法有很多,如 fast, orb, mser, gftt, harris, simpleblob, brisk, akaze, sift 和 surf等
[java] view plain copy - MatOfKeyPoint keyPoint=new MatOfKeyPoint();
- FeatureDetector fd=FeatureDetector.create(FeatureDetector.BRISK);
- fd.detect(src, keyPoint);
因为sift和surf算法有专利,所以opencv官方并没有集成,所以使用sift算法会报错。github上已有三方集成了。每个算法找关键点不一样,导致在计算机上的性能也会不一样,最直接的表现就是时间。fast算法,risk算法,orb算法 效果比较:左,fast算法找了许多关键点,用时仅仅4ms;中,brisk算法的关键点不多,但却用时1944ms;(这个算法真的很久,我也不知道为啥)右,orb算法关键点比brisk稍微多一点,用时12ms。(测试机cpu很渣~)至于其他的算法我就不一一去比较了,基本上就是时间和精度的矛盾和取舍了,用时间换精度,or用精度换时间。
虽然感觉性能fast貌似比brisk要好,但我觉得brisk找的关键点质量比较好,而fast只是简单地找了一些边缘点而已。而且!并不是说关键点越多越好,因为会影响后面的计算速度,越多,自然而然下一步的特征计算量就越大。
3. 关键点的特征计算。单单靠关键点是没法匹配两张图片的,所以我们还需要计算出关键点的特征,才能进行比较。
[java] view plain copy - Mat description=new Mat();
- DescriptorExtractor de=DescriptorExtractor.create(DescriptorExtractor.ORB);
- de.compute(src, keyPoint, description);
关键函数compute(),计算其关键点的特征,参数src为原图Mat,keyPoint为上一步计算src关键点的MatOfKeypoit, 当然计算出来的description也是一个Mat,可见基本图形处理都是基于矩阵Matrix的运算。这里的话,opencv提供的算法也有几种:orb, brisk, brief, akaze, freak, surf, sift等其中的surf和sift也是不能使用的。耗时:brisk:1877ms(仍表示怀疑,难到跟手机有关?), orb:16ms。注意:找关键点和计算特征的算法不能随便乱配,会有报错的情况,测试下面几种情况没问题:都是brisk,都是orb,或者fast配brisk,fast配orb,这个这里就不一一测试了。
4. 匹配特征。匹配主要有两个类一个BFMatcher,一个FlannBasedMatcher,均继承DescriptorMatcher。BFMatcher,即Brute Force,暴力,尽可能多地匹配符合的特征点;FlannBasedMatcher,基于flann算法,这个就不扯了,我也不太懂。但实际使用过程,试过用flannbasedmatcher会报错,大概是跟Mat容器的数据类型有关。搜索一下区别,发现两者对Mat容器的数据类型是有要求的,这里就不多扯了,有时间的话可以慢慢探索,这里就先用着BFMatcher。
[java] view plain copy - List<MatOfDMatch> matches=new ArrayList<>();
- DescriptorMatcher matcher=DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE);
- matcher.radiusMatch(descriptionA, descriptionB, matches, 30f);
或者直接用子类
[java] view plain copy - List<MatOfDMatch> matches=new ArrayList<>();
- BFMatcher matcher=BFMatcher.create();
- matcher.radiusMatch(descriptionA, descriptionB, matches, 30f);
对于radiusMatch()这个函数,前两个参数分别是图片A和图片B经过上面几步算出来的关键点特征,均是Mat类型,而第三个参数matches是一个List<MatOfDMatch>类型,即存储了n个MatOFDMatch (n个元素里包含1*1矩阵,即匹配成功;和0*0空矩阵,即匹配不成功) 的列表,也就是输出,第四个参数float,指的是MatOfDMatch中的DMatch元素的属性distance不能超过这个最大“距离”,说简单点,就是超过这个“距离”就不要,也即是过滤器。断点Debug下matches列表的元素:
MatOfDMatch实际上是一个1*1的矩阵,调用toArray()[0],可以直接获得对象DMatch,该对象存储了两个关键点特征的匹配信息,里面有一个很重要的属性distance.
距离属性:DMatch类的属性distance,该distance比不是指物理上的距离,而是指欧氏距离,但处于2维或者3维的时候,你还可以用物理空间或者勾股定理来理解,但超过了3维,那维数就不是能用物理空间来表示的了,更多的是指两个物体之间的相似度,值越小,表示越相似。举个栗子:好比如小姐姐写的图文并茂的博客里,对于sift算法,计算特征值的时候,会取一个半径范围内的所有采样点的关于颜色变化梯度的向量相加,得到8个方向的特征向量,那么这8个特征向量就可以当做8维,也就是说这个关键点有8维。(我随便说说的,便于理解~)回到正题,除了matcher.radiusMach()接口外,还有一个matcher.knnMatch()该接口基于knn算法,没有radiusMatch那样的过滤器,所以有很多不准确的点都被当做是匹配对了。匹配同一张图,然后对radiusMatch()和knnMatch()做个比较:radius: 连线都平行
knn: 连线不完全平行
可以看到,radius有过滤器,可以把一些distance较大的配对点滤掉,即那些不平行的连线的匹配点。上面是两张相同的照片匹配结果,在所有的匹配点List<MatOfDmatch>里,其distance基本都是0。网上有C++的实现方法,基本都是knnMatch后,再根据匹配点的distance进行一次过滤,去掉不太符合的匹配点,基本和radiusMatch类似。
5. 判断或连线(非必须)画图可以方便展示,调用Feature2d接口即可,除了连线,也可以标注关键点。
[java] view plain copy - Mat dst=new Mat();
- Features2d.drawMatches2(srcA, keyPointA, srcB, keyPointB, matches, dst);
- Bitmap bitmap=Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_4444);
- Utils.matToBitmap(dst, bitmap);
- ivTest.setImageBitmap(bitmap);
基本就是创建一个输出用的容器Mat,然后drawMatches,接着创建Bitmap,尺寸要和输出Mat的尺寸对应,即width==cols,height==rows,最后将Mat转Bitmap,输出到ImageView上。
再贴出几个对比图:
拉伸,distance范围2.236068--870.503906, 限制max distance=500:
拉伸,distance范围2.236068--870.503906, 限制max distance=200:
旋转,distance范围9.797959--885.404418, 限制max distance=200:
拉伸并旋转,distance范围29.966648--819.875610, 限制max distance=200:
截取部分,distance范围0.0--1052.601562,限制max distance=200:
从上面几个例子可以看到,匹配特征点有一个范围,表示所有匹配点从准确到粗糙的一个范围,匹配率=1*1矩阵数 / (1*1矩阵数+0*0矩阵数)*100%。然而,影响匹配率最大的因素,就是radiusMatch()接口中的第四个参数,maxDistance,即匹配特征相似度的一个阀值。如果该阀值太高,那么会匹配到许多不符合的关键点;如果太低,虽然精度变高,但匹配率也会很低,对于一些变化比较大的图片,但内容一样,却有可能识别不出来。因此,该阀值的选取,最终还是取决于你个人对识别的精度要求。那么,你觉得匹配率多大才算是同一张图片呢?3个关键点,有2个匹配上了,难道你就敢说是同一张图片了?匹配率只有百分之几,难道你敢说不是同一张?
于是,就回到了刚开始说的,对于图片识别,实际上还是插入了很大的人为主观干涉进去。计算机处理图片速度很快,但却很笨~
下面贴出完整代码:
[java] view plain copy - public class MainActivity extends AppCompatActivity {
-
- private Button btnTest;
- private ImageView ivTest;
-
- static{
- if(!OpenCVLoader.initDebug()){
- Log.d("zz", "init failed");
- }
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- ivTest=(ImageView)findViewById(R.id.iv_test);
-
- btnTest=(Button)findViewById(R.id.btn_test);
- btnTest.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- String imgA="/sdcard/a.jpg";
- String imgB="/sdcard/d.jpg";
- if(match(imgA, imgB)){
- Toast.makeText(MainActivity.this, "Match succeed", Toast.LENGTH_SHORT).show();
- }
- }
- });
-
- }
-
- private boolean match(String imgPath1, String imgPath2){
-
- //加载图片
- File a=new File(imgPath1);
- File b=new File(imgPath2);
- if(!a.exists() || !b.exists()) return false;
- Mat srcA=Imgcodecs.imread(imgPath1);
- Mat srcB=Imgcodecs.imread(imgPath2);
-
- //寻找关键点
- FeatureDetector fd=FeatureDetector.create(FeatureDetector.ORB);
- MatOfKeyPoint keyPointA=new MatOfKeyPoint();
- MatOfKeyPoint keyPointB=new MatOfKeyPoint();
- fd.detect(srcA, keyPointA);
- fd.detect(srcB, keyPointB);
-
- //计算关键点的特征
- DescriptorExtractor de=DescriptorExtractor.create(DescriptorExtractor.ORB);
- Mat descriptionA=new Mat();
- Mat descriptionB=new Mat();
- de.compute(srcA, keyPointA, descriptionA);
- de.compute(srcB, keyPointB, descriptionB);
-
- //匹配两张图片关键点的特征
- List<MatOfDMatch> matches=new ArrayList<>();
- DescriptorMatcher matcher=DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE);
- matcher.radiusMatch(descriptionA, descriptionB, matches, 200f);
-
- //画图显示匹配结果(演示用,非必须,可注释)
- Mat dst=new Mat();
- Features2d.drawMatches2(srcA, keyPointA, srcB, keyPointB, matches, dst);
- Bitmap bitmap=Bitmap.createBitmap(dst.cols(), dst.rows(), Bitmap.Config.ARGB_4444);
- Utils.matToBitmap(dst, bitmap);
- ivTest.setImageBitmap(bitmap);
-
- //根据匹配率判断是否匹配(以下处理不科学,无视掉)
- int total=Math.min(keyPointA.rows(), keyPointB.rows());
- int matchedNum=0;
- for(MatOfDMatch match : matches){
- if(match.rows()!=0) matchedNum++;
- }
- float ratio=matchedNum*1.0f/total;
- if(ratio>0.75f) return true;
-
- return false;
- }
- }
布局文件很简单,就一个button和一个imageview,不贴了。因为读图片是从sdcard读的,需要加权限
[html] view plain copy - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
不由惊叹人类视觉神经处理的伟大和奇妙~
|
免费评分
-
查看全部评分
|