带有特征检测和单应性的 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不是很熟悉。 @butexacv2.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增强现实