基于OpenCV的AndroidSDK实现的特征点匹配案例,其中SDK的版本为4.5.5
SDK下载
OpenCV的SDK这里下载。
matchTemplate方案的试错
自己一个项目需要使用图像匹配,其实就是从一个大截图中截出来一个小按钮,然后去识别这个按钮的位置然后做点击。一开始使用的是模板匹配,也就是Imgproc._matchTemplate()_
方法,但使用后发现该方法对图片的分辨率要求过高(其实还有旋转、角度等限制),即时是从大图中截图出来的小图,也是需要分辨率对得上才能匹配成功(也就是说不同分辨率的手机截图出来的文件不一定能匹配成功)。
决定使用drawMatches
后来找了好久的资料,终于在OpenCV的官网文档找到一个基于特征点匹配的方法(Features2d._drawMatches()_
),不受限分辨率、角度、旋转等限制(虽然我这个项目不涉及到角度和旋转)。
要注意的是这个方法可以使用好几种不同的算法(BRISK、ORB、KAZE、AKAZE、MSER等),这里我最终选用的是BRISK,关于这几种算法的优劣可以百度,我是参考了链接。
代码实现
// image2Bitmap:截图出来的Bitmap对象
// templateBitmap:要匹配的按钮的Bitmap对象
// src:截图的Mat对象
// template:要匹配的按钮的Mat对象
if (image2Bitmap != null) {
LogUtils.d("截图完成,准备匹配截图");
src = new Mat();
Utils.bitmapToMat(image2Bitmap, src);
LogUtils.d("获取的截图宽高", image2Bitmap.getWidth(), image2Bitmap.getHeight());
if (templateBitmap == null) {
templateBitmap = ImageUtils.getBitmap(R.drawable.image_open);
}
if (template == null) {
template = new Mat();
Utils.bitmapToMat(templateBitmap, template);
}
MatOfKeyPoint keyPointTemplate = new MatOfKeyPoint();
Mat templateDescriptorMat = new Mat();
MatOfKeyPoint keyPointSrc = new MatOfKeyPoint();
Mat srcDescriptorMat = new Mat();
// ORB me = ORB.create(1000, 1.2f);
// ORB me = ORB.create();
// KAZE me = KAZE.create();
// AKAZE me = AKAZE.create();
BRISK me = BRISK.create();
// MSER me = MSER.create();
me.detect(template, keyPointTemplate);
me.compute(template, keyPointTemplate, templateDescriptorMat);
me.detect(src, keyPointSrc);
me.compute(src, keyPointSrc, srcDescriptorMat);
if (templateDescriptorMat.type() != CvType.CV_32F && srcDescriptorMat.type() != CvType.CV_32F) {
templateDescriptorMat.convertTo(templateDescriptorMat, CvType.CV_32F);
srcDescriptorMat.convertTo(srcDescriptorMat, CvType.CV_32F);
}
MatOfDMatch matches = new MatOfDMatch();
FlannBasedMatcher matcher = FlannBasedMatcher.create();
matcher.match(templateDescriptorMat, srcDescriptorMat, matches);
List<DMatch> matchList = matches.toList();
LogUtils.d("优化前的匹配点数量:", matchList.size());
// 按照distance升序
Collections.sort(matchList, (a, b) -> {
return (int) (a.distance * 1000 - b.distance * 1000);
});
LogUtils.d("排序后的匹配点列表", matchList);
float min = matchList.get(0).distance;
float max = matchList.get(matchList.size() - 1).distance;
List<DMatch> goodMatchList = new ArrayList(matchList);
// 对列表进行筛选,去除distance小于(最大的distance * 0.4)的特征点
CollectionUtils.filter(goodMatchList, new CollectionUtils.Predicate<DMatch>() {
@Override
public boolean evaluate(DMatch item) {
return item.distance < max * 0.4;
}
});
LogUtils.d("优化后的匹配点数量:", goodMatchList.size());
// 如果匹配点小于4个,是无法继续执行的话,不然OpenCV会报错
if (goodMatchList.size() < 4) {
LogUtils.d("匹配点小于4个,跳过本次!");
continue;
}
Mat result = new Mat();// 承载最终结果的Mat
MatOfDMatch matOfDMatch = new MatOfDMatch();
matOfDMatch.fromList(goodMatchList);
// 把匹配图在大图中的特征点关系线画上
Features2d.drawMatches(template, keyPointTemplate, src, keyPointSrc, matOfDMatch, result);
// 以上其实已经能标识出大图中关于匹配图的特征点关系了(用线连接),下面要做的是找出匹配图在大图中的位置
//-- 定位对象
List<Point> obj = new ArrayList<>();
List<Point> scene = new ArrayList<>();
List<KeyPoint> listOfKeypointsObject = keyPointTemplate.toList();
List<KeyPoint> listOfKeypointsScene = keyPointSrc.toList();
for (int i = 0; i < goodMatchList.size(); i++) {
//-- 从良好的匹配中获取关键点
obj.add(listOfKeypointsObject.get(goodMatchList.get(i).queryIdx).pt);
scene.add(listOfKeypointsScene.get(goodMatchList.get(i).trainIdx).pt);
}
MatOfPoint2f objMat = new MatOfPoint2f();
MatOfPoint2f sceneMat = new MatOfPoint2f();
objMat.fromList(obj);
sceneMat.fromList(scene);
double ransacReprojThreshold = 3.0;
Mat H = Calib3d.findHomography(objMat, sceneMat, Calib3d.RANSAC, ransacReprojThreshold);
//-- 从image_1(要“检测”的对象)获取角
Mat objCorners = new Mat(4, 1, CvType.CV_32FC2), sceneCorners = new Mat();
float[] objCornersData = new float[(int) (objCorners.total() * objCorners.channels())];
objCorners.get(0, 0, objCornersData);
objCornersData[0] = 0;
objCornersData[1] = 0;
objCornersData[2] = template.cols();
objCornersData[3] = 0;
objCornersData[4] = template.cols();
objCornersData[5] = template.rows();
objCornersData[6] = 0;
objCornersData[7] = template.rows();
objCorners.put(0, 0, objCornersData);
Core.perspectiveTransform(objCorners, sceneCorners, H);
float[] sceneCornersData = new float[(int) (sceneCorners.total() * sceneCorners.channels())];
sceneCorners.get(0, 0, sceneCornersData);
//-- 在角之间绘制线,也就是我要找的按钮的四个边
Imgproc.line(result, new Point(sceneCornersData[0] + template.cols(), sceneCornersData[1]),
new Point(sceneCornersData[2] + template.cols(), sceneCornersData[3]), new Scalar(255, 0, 0, 255), 4);
Imgproc.line(result, new Point(sceneCornersData[2] + template.cols(), sceneCornersData[3]),
new Point(sceneCornersData[4] + template.cols(), sceneCornersData[5]), new Scalar(255, 0, 0, 255), 4);
Imgproc.line(result, new Point(sceneCornersData[4] + template.cols(), sceneCornersData[5]),
new Point(sceneCornersData[6] + template.cols(), sceneCornersData[7]), new Scalar(255, 0, 0, 255), 4);
Imgproc.line(result, new Point(sceneCornersData[6] + template.cols(), sceneCornersData[7]),
new Point(sceneCornersData[0] + template.cols(), sceneCornersData[1]), new Scalar(255, 0, 0, 255), 4);
Bitmap bitmap = Bitmap.createBitmap(result.width(), result.height(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(result, bitmap);
// 保存图片到本地
ImageUtils.save2Album(bitmap, Bitmap.CompressFormat.PNG);
} else {
LogUtils.d("截图的Bitmap是空!!!");
}