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

Posted

技术标签:

【中文标题】带有特征检测和单应性的 OpenCV 对象检测【英文标题】:OpenCV Object detection with Feature Detection and Homography 【发布时间】:2021-08-23 05:52:34 【问题描述】:

我正在尝试检查此图像是否:

包含在像这样的图像中:

我正在使用特征检测 (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, new Mat(), 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, keypointsObject, imgScene, keypointsScene, goodMatches, imgMatches, Scalar.all(-1),
                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, 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, 0, objCornersData);
        Core.perspectiveTransform(objCorners, sceneCorners, H);
        float[] sceneCornersData = new float[(int) (sceneCorners.total() * sceneCorners.channels())];
        sceneCorners.get(0, 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[2] + imgObject.cols(), sceneCornersData[3]),
                new Point(sceneCornersData[4] + imgObject.cols(), sceneCornersData[5]), 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]), new Scalar(0, 255, 0), 4);
        Imgproc.line(imgMatches, new Point(sceneCornersData[6] + imgObject.cols(), sceneCornersData[7]),
                new Point(sceneCornersData[0] + imgObject.cols(), sceneCornersData[1]), new Scalar(0, 255, 0), 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);
    

生成的图像:

【问题讨论】:

使用 matchTemplate。它是游戏 UI,它是 2D 叠加层。它将始终具有相同的比例。预测规模,相应地调整模板的大小 我不知道如何预测规模。有什么建议吗? @butexa 你试过Template Matching吗? @Bilal 模板匹配不是尺度不变的。我得到了很多错误的匹配。我无法设置阈值来过滤掉误报。 【参考方案1】:

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

【讨论】:

我无法复制与您相同的结果:i.stack.imgur.com/RiIL9.jpg您的意思是在分割后仍然使用冲浪检测?顺便问一下,你能附上一些代码吗? @butexa:取决于您比较颜色的准确程度。您的方法似乎也可行(三个相同的斑点彼此靠近)。我使用了专有软件。【参考方案2】:

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

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

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

# 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, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

这是面具:

我们可以通过flood-filling 用黑色“消除”一些白色区域。让我们对二值图像应用四个 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, mask=None, seedPoint=(imageWidth - 1, 0), newVal=0)

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

# Apply flood-fill at seed point (imageWidth - 1, imageHeight - 1) - Bottom Right:
cv2.floodFill(binaryImage, mask=None, seedPoint=(imageWidth - 1, imageHeight - 1), 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, 0, 255, 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, 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, (matchX, matchY), (matchWidth, matchHeight), (0, 0, 255), 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), (0, 0, 255), -1)

# Draw the text:
labelOrigin = (matchX-1, matchY - 10)
cv2.putText(inputImage, str(matchScore), labelOrigin, cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 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根据参考宽度和原始纵横比图像。其余代码没有修改。您的新图像上的结果将是:

【讨论】:

非常感谢!我马上去测试!你在哪里定义 maxLoc 因为我在运行脚本时遇到错误。对不起,我对python不是很熟悉。 @butexa cv2.minMaxLoc 返回 4 件事:(minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(result)。匹配的最小值、最大值、最小匹配的位置和最大匹配的位置。在 Python 中,返回值存储在 4 个参数的元组中。此外,位置是(x, y) 坐标,例如maxLoc 也是两个值的元组。因此,x 值将存储在 maxLoc[0] 中,y 值将存储在 maxLoc[1] 中。 我收到cv2.error: OpenCV(4.5.2) /tmp/opencv-20210603-28323-1e2w9q5/opencv-4.5.2/modules/imgproc/src/floodfill.cpp:509: error: (-211:One of the arguments' values is out of range) Seed point is outside of image in function 'floodFill' 这张图片:i.stack.imgur.com/VyL1L.jpg 这怎么可能呢? imageHeight 是从inputImage.shape[:2] 获得的。 @butexa 啊,是的,伙计,对不起,我在回答中犯了两个错误:1)shape函数返回参数的顺序是height, width。我反其道而行之。 2) 我忘记在第一个flood-fill 操作之前添加一行。该行在填充之前获取裁剪图像的新高度和宽度。我已经纠正了答案中的这些错误,并添加了一个四角洪水填充程序(而不是原来的两个角)。现在,我注意到您的新图片有一些问题,请参阅我的编辑了解详细信息。

以上是关于带有特征检测和单应性的 OpenCV 对象检测的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV-Python实战(12)——一文详解AR增强现实

从特征匹配/单应性中过滤掉误报——OpenCV

从对象的不同角度找到一个好的单应性?

第十一节Harris角点检测原理

opencv cv2.findHomography sift、surf特征匹配 单应性矩阵理解

OpenCV单应性矩阵发现参数估算方法详解