具有特征检测和单应性的 OpenCV 对象检测

如何解决具有特征检测和单应性的 OpenCV 对象检测

我正在尝试检查这张图片:

Template image

包含在这样的图像中:

Source image

我正在使用特征检测 (SURF) 和单应性,因为模板匹配不是尺度不变的。可悲的是,除了少数关键点之外,所有关键点都处于错误的位置。我应该通过多次缩放图像来尝试模板匹配吗?如果是这样,尝试缩放图像的最佳方法是什么?

代码:

import java.util.ArrayList;
import java.util.List;
import org.opencv.calib3d.Calib3d;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.DMatch;
import org.opencv.core.KeyPoint;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfDMatch;
import org.opencv.core.MatOfKeyPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.features2d.DescriptorMatcher;
import org.opencv.features2d.Features2d;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.xfeatures2d.SURF;
class SURFFLANNMatchingHomography {
    public void run(String[] args) {
        String filenameObject = args.length > 1 ? args[0] : "../data/box.png";
        String filenameScene = args.length > 1 ? args[1] : "../data/box_in_scene.png";
        Mat imgObject = Imgcodecs.imread(filenameObject,Imgcodecs.IMREAD_GRAYSCALE);
        Mat imgScene = Imgcodecs.imread(filenameScene,Imgcodecs.IMREAD_GRAYSCALE);
        if (imgObject.empty() || imgScene.empty()) {
            System.err.println("Cannot read images!");
            System.exit(0);
        }
        //-- Step 1: Detect the keypoints using SURF Detector,compute the descriptors
        double hessianThreshold = 400;
        int nOctaves = 4,nOctaveLayers = 3;
        boolean extended = false,upright = false;
        SURF detector = SURF.create(hessianThreshold,nOctaves,nOctaveLayers,extended,upright);
        MatOfKeyPoint keypointsObject = new MatOfKeyPoint(),keypointsScene = new MatOfKeyPoint();
        Mat descriptorsObject = new Mat(),descriptorsScene = new Mat();
        detector.detectAndCompute(imgObject,new Mat(),keypointsObject,descriptorsObject);
        detector.detectAndCompute(imgScene,keypointsScene,descriptorsScene);
        //-- Step 2: Matching descriptor vectors with a FLANN based matcher
        // Since SURF is a floating-point descriptor NORM_L2 is used
        DescriptorMatcher matcher = DescriptorMatcher.create(DescriptorMatcher.FLANNBASED);
        List<MatOfDMatch> knnMatches = new ArrayList<>();
        matcher.knnMatch(descriptorsObject,descriptorsScene,knnMatches,2);
        //-- Filter matches using the Lowe's ratio test
        float ratioThresh = 0.75f;
        List<DMatch> listOfGoodMatches = new ArrayList<>();
        for (int i = 0; i < knnMatches.size(); i++) {
            if (knnMatches.get(i).rows() > 1) {
                DMatch[] matches = knnMatches.get(i).toArray();
                if (matches[0].distance < ratioThresh * matches[1].distance) {
                    listOfGoodMatches.add(matches[0]);
                }
            }
        }
        MatOfDMatch goodMatches = new MatOfDMatch();
        goodMatches.fromList(listOfGoodMatches);
        //-- Draw matches
        Mat imgMatches = new Mat();
        Features2d.drawMatches(imgObject,imgScene,goodMatches,imgMatches,Scalar.all(-1),new MatOfByte(),Features2d.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS);
        //-- Localize the object
        List<Point> obj = new ArrayList<>();
        List<Point> scene = new ArrayList<>();
        List<KeyPoint> listOfKeypointsObject = keypointsObject.toList();
        List<KeyPoint> listOfKeypointsScene = keypointsScene.toList();
        for (int i = 0; i < listOfGoodMatches.size(); i++) {
            //-- Get the keypoints from the good matches
            obj.add(listOfKeypointsObject.get(listOfGoodMatches.get(i).queryIdx).pt);
            scene.add(listOfKeypointsScene.get(listOfGoodMatches.get(i).trainIdx).pt);
        }
        MatOfPoint2f objMat = new MatOfPoint2f(),sceneMat = new MatOfPoint2f();
        objMat.fromList(obj);
        sceneMat.fromList(scene);
        double ransacReprojThreshold = 3.0;
        Mat H = Calib3d.findHomography( objMat,sceneMat,Calib3d.RANSAC,ransacReprojThreshold );
        //-- Get the corners from the image_1 ( the object to be "detected" )
        Mat objCorners = new Mat(4,1,CvType.CV_32FC2),sceneCorners = new Mat();
        float[] objCornersData = new float[(int) (objCorners.total() * objCorners.channels())];
        objCorners.get(0,objCornersData);
        objCornersData[0] = 0;
        objCornersData[1] = 0;
        objCornersData[2] = imgObject.cols();
        objCornersData[3] = 0;
        objCornersData[4] = imgObject.cols();
        objCornersData[5] = imgObject.rows();
        objCornersData[6] = 0;
        objCornersData[7] = imgObject.rows();
        objCorners.put(0,objCornersData);
        Core.perspectiveTransform(objCorners,sceneCorners,H);
        float[] sceneCornersData = new float[(int) (sceneCorners.total() * sceneCorners.channels())];
        sceneCorners.get(0,sceneCornersData);
        //-- Draw lines between the corners (the mapped object in the scene - image_2 )
        Imgproc.line(imgMatches,new Point(sceneCornersData[0] + imgObject.cols(),sceneCornersData[1]),new Point(sceneCornersData[2] + imgObject.cols(),sceneCornersData[3]),new Scalar(0,255,0),4);
        Imgproc.line(imgMatches,new Point(sceneCornersData[4] + imgObject.cols(),sceneCornersData[5]),new Point(sceneCornersData[6] + imgObject.cols(),sceneCornersData[7]),4);
        //-- Show detected matches
        HighGui.imshow("Good Matches & Object detection",imgMatches);
        HighGui.waitKey(0);
        System.exit(0);
    }
}
public class SURFFLANNMatchingHomographyDemo {
    public static void main(String[] args) {
        // Load the native OpenCV library
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        new SURFFLANNMatchingHomography().run(args);
    }

结果图像:

Resulting image

解决方法

这是一个可能的解决方案。代码在 Python 中,但操作非常简单,希望您能够将其移植到 Java。我正在使用模板匹配。我想,要点是我正在对从输入图像的 Cyan (C) 组件获得的二进制掩码执行模板匹配。步骤如下:

  1. 修剪您的图像以消除不需要的噪音
  2. 转换图像到CMYK色彩空间并获得青色通道
  3. 清理青色通道
  4. 阅读模板
  5. 将模板转换为二进制图像
  6. 执行模板匹配

让我们看看。模板在目标图像中的位置似乎是恒定的,因此我们可以裁剪图像以去除一些我们确定不会定位模板的部分。我通过指定感兴趣区域 ((top left x,top left y,width,height)) 的坐标 ROI 来裁剪图像以消除部分“页眉”和“页脚”,如下所示:

# imports:
import numpy as np
import cv2

# image path
path = "D://opencvImages//"
fileName = "screen.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Deep copy for results:
inputImageCopy = inputImage.copy()

# Get image dimensions:
(imageHeight,imageWidth) = inputImage.shape[:2]

# Set the ROI location:
roiX = 0
roiY = 225
roiWidth = imageWidth
roiHeight = 1390

# Crop the ROI:
imageROI = inputImage[roiY:roiHeight,roiX:roiWidth]

# Store a deep copy of this image for results:
imageROIcopy = imageROI.copy()

您将获得以下裁剪图像:

您可以裁剪更多,但我不确定您的要求。让我们使用它并将新图像转换为 CYMK 颜色空间。然后,提取 Cyan 频道,因为模板似乎在该特定频道中拥有最多的内容。在 OpenCV 中没有直接转换到 CYMK 色彩空间,所以我直接应用了 conversion formula。我们可以从那个公式中得到每个颜色空间分量,但我们只对 C 通道感兴趣,它只需要预先计算 K(Key)通道。可以这样计算:

# Convert the image to float and divide by 255:
floatImage = imageROI.astype(np.float)/255.

# Calculate channel K (Key):
kChannel = 1 - np.max(floatImage,axis=2)

# Calculate  channel C (Cyan):
cChannel = np.where(kChannel < 0.9,(1-floatImage[...,2] - kChannel)/(1 - kChannel),0)

# Convert Cyan channel to uint 8:
cChannel = (255*cChannel).astype(np.uint8)

小心您的数据类型。我们需要对 float 数组进行操作,所以这是我执行的第一个转换。获得 C 通道后,我们将图像转换回 unsigned 8-bit 数组。这是您为 C 频道获得的图像:

接下来,通过 Otsu 的阈值法从中得到一个二值掩码:

# Threshold via Otsu:
_,binaryImage = cv2.threshold(cChannel,255,cv2.THRESH_BINARY + cv2.THRESH_OTSU)

这是面具:

我们可以通过 flood-filling 用黑色“消除”一些白色区域。让我们对二值图像应用四个填充操作:左上角、右上角、左下角和右下角:

# Get the dimensions of the cropped image:
(imageHeight,imageWidth) = binaryImage.shape[:2]

# Apply flood-fill at seed point (0,0) - Top Left:
cv2.floodFill(binaryImage,mask=None,seedPoint=(0,0),newVal=0)

# Apply flood-fill at seed point (imageWidth - 1,0) - Top Right:
cv2.floodFill(binaryImage,seedPoint=(imageWidth - 1,newVal=0)

# Apply flood-fill at seed point (0,imageHeight - 1) - Bottom Left:
cv2.floodFill(binaryImage,imageHeight - 1),imageHeight - 1) - Bottom Right:
cv2.floodFill(binaryImage,newVal=0)

这是结果。请注意,我们正在寻找的子图像是孤立的,大部分大噪声都消失了:

您可能可以在其上运行区域过滤器以去除更小(和更大)的噪声斑点,但现在让我们考虑这个结果。好了,第一部分完成。让我们读取模板并执行模板匹配。现在,您的模板有一个在这里没有用的 alpha 通道。我在 GIMP 中打开您的图像并将 alpha 通道替换为纯白色,这是我得到的模板:

让我们阅读,将其转换为灰度并执行 Otsu 阈值处理以获得二值图像:

# Read template:
template = cv2.imread(path+"colorTemplate.png")

# Convert it to grayscale:
template = cv2.cvtColor(template,cv2.COLOR_BGR2GRAY)

# Threshold via Otsu:
_,template = cv2.threshold(template,cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

这是二进制模板:

现在,您可以在这里实现渐进式缩放机制,将这个模板按比例缩放并在“步骤”中在目标图像上运行模板匹配,然后在整个“运行”中寻找最佳匹配结果并进行比较它针对最低门槛。但是让我们按原样测试模板:

# Get template dimensions:
(templateHeight,templateWidth) = template.shape[:2]

# Run Template Matching:
result = cv2.matchTemplate(binaryImage,template,cv2.TM_CCOEFF_NORMED)

# Get Template Matching Results:
(minVal,maxVal,minLoc,maxLoc) = cv2.minMaxLoc(result)

# Get Matching Score:
matchScore = maxVal
print("Match Score: "+str(matchScore))

有了这个模板,我得到了 matchScore 的:

Match Score: 0.806335985660553

看起来很能接受。让我们在找到最大匹配分数的位置画一个漂亮的矩形,只是为了可视化结果:

# Set ROI where the largest matching score was found:
matchX = maxLoc[0]
matchY = maxLoc[1]
matchWidth = matchX + templateWidth
matchHeight = matchY + templateHeight

# Draw the ROI on the copy of the cropped BGR image:
cv2.rectangle(imageROIcopy,(matchX,matchY),(matchWidth,matchHeight),(0,255),2)
# Show the result:
cv2.imshow("Result (Local)",imageROIcopy)
cv2.waitKey(0)

这是(裁剪的)结果:

看起来不错。当我们裁剪图像以运行此操作时,让我们在未裁剪的实际图像上定位匹配的 ROI

# Show result on original image:
matchX = roiX + matchX
matchY = roiY + matchY
matchWidth = matchX + templateWidth
matchHeight = matchY + templateHeight

# Draw the ROI on the copy of the cropped BGR image:
cv2.rectangle(inputImage,2)

此外,我们可以绘制一个带有匹配分数的漂亮标签。这是可选的,只是为了在原始图像上绘制所有信息:

# Draw label with match result:
# Round up match score to two significant digits:
matchScore = "{:.2f}".format(matchScore)

# Draw a filled rectangle:
labelOrigin = (matchX-1,matchY - 40)
(labelWidth,labelHeight) = (matchWidth+1,matchY)
cv2.rectangle(inputImage,labelOrigin,(labelWidth,labelHeight),-1)

# Draw the text:
labelOrigin = (matchX-1,matchY - 10)
cv2.putText(inputImage,str(matchScore),cv2.FONT_HERSHEY_SIMPLEX,1.0,2)

cv2.imshow("Result (Global)",inputImage)
cv2.waitKey(0)

这是(全尺寸)结果:


编辑:处理新图片

我注意到您的新图片与原始图片不同。您似乎正在从具有不同屏幕分辨率的不同手机捕获屏幕。现在,问题在于如果您更改目标图像的大小,则必须重新缩放模板,否则模板对于新匹配来说会太小(或太大),从而产生较差的结果。你可以实现我上面提到的重新缩放机制来放大模板,最终你会在某个重新缩放的大小下找到一个不错的结果 - 这是一个现有的选项。

other 选项是将新图像重新缩放到与原始图像相似的大小。您的原始图像大小为 1125 x 2001,而大小为 1600 x 2560。这是一个重要的区别。让我们 resize 新图像具有与原始图像相同的宽度。代码的开头将修改为:

# image path
path = "D://opencvImages//"
fileName = "newScreen.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Set the reference width:
referenceWidth = 1125

# Get image dimensions:
(imageHeight,imageWidth) = inputImage.shape[:2]

# Check input width vs reference width:
if imageWidth != referenceWidth:

    # Get original aspect ratio:
    aspectRatio = imageWidth / imageHeight
    # Compute new height using the reference width:
    newHeight = referenceWidth / aspectRatio
    # Set the new dimensions as a tuple:
    dim = (int(referenceWidth),int(newHeight))
    # Resize the image:
    inputImage = cv2.resize(inputImage,dim,interpolation=cv2.INTER_AREA)
    # Get new dimensions for further processing:
    (imageHeight,imageWidth) = inputImage.shape[:2]


# Deep copy for results:
inputImageCopy = inputImage.copy()

# Set the ROI location:
roiX = 0
roiY = 225
roiWidth = imageWidth
roiHeight = 1390

在这里,我将参考宽度设置为 1125 像素,通过 shape 获取输入图像尺寸并检查输入宽度是否与参考不同。如果是这种情况,我会根据参考宽度和原始纵横比 resize 图像。其余代码没有修改。新图片的结果是这样的:

,

如果可以选择查找特定颜色,则无论大小,您都可以依靠分割来快速找到候选颜色。但是您必须添加一些后过滤。

enter image description here

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res