寻找由区域掩码表示的多边形的角落。

8

BW = poly2mask(x, y, m, n)函数从由向量x和y表示的ROI多边形计算二进制感兴趣区域(ROI)掩模BW。BW的大小为m乘n。

poly2mask将在多边形(X,Y)内部的像素设置为1,并将多边形外部的像素设置为0。

问题: 给定凸四边形的二进制掩模BW,确定四个角点的最有效方法是什么?

例如,

Example

目前最佳解决方案: 使用edge查找边界线,在边缘图像中使用霍夫变换查找4条直线,然后找到这4条直线的交点或在边缘图像上使用角点检测器。看起来很复杂,我无法帮助自己感觉还有更简单的解决方案。

顺便说一下,convhull并不总是返回4个点(也许有人可以建议qhull选项来防止这种情况):它还返回沿着边缘的一些点。

编辑: Amro的答案看起来相当优雅和高效。但是,每个真实角上可能会有多个“角”,因为峰值不唯一。我可以基于θ对它们进行聚类,并在真实角周围平均“角”,但主要问题是使用order(1:10)

10是否足以考虑所有角或者这将排除真正角上的“角”?

5个回答

12
这与@AndyL建议的有些相似。但是我使用的是极坐标系中的边界签名,而不是切线。
请注意,我首先提取边缘,获取边界,然后将其转换为签名。最后,我们找到距离质心最远的边界上的点,这些点构成了找到的角落。(或者我们也可以检测签名中的峰值以找到角落)。
以下是完整的实现:
I = imread('oxyjj.png');
if ndims(I)==3
    I = rgb2gray(I);
end
subplot(221), imshow(I), title('org')

%%# Process Image
%# edge detection
BW = edge(I, 'sobel');
subplot(222), imshow(BW), title('edge')

%# dilation-erosion
se = strel('disk', 2);
BW = imdilate(BW,se);
BW = imerode(BW,se);
subplot(223), imshow(BW), title('dilation-erosion')

%# fill holes
BW = imfill(BW, 'holes');
subplot(224), imshow(BW), title('fill')

%# get boundary
B = bwboundaries(BW, 8, 'noholes');
B = B{1};

%%# boudary signature
%# convert boundary from cartesian to ploar coordinates
objB = bsxfun(@minus, B, mean(B));
[theta, rho] = cart2pol(objB(:,2), objB(:,1));

%# find corners
%#corners = find( diff(diff(rho)>0) < 0 );     %# find peaks
[~,order] = sort(rho, 'descend');
corners = order(1:10);

%# plot boundary signature + corners
figure, plot(theta, rho, '.'), hold on
plot(theta(corners), rho(corners), 'ro'), hold off
xlim([-pi pi]), title('Boundary Signature'), xlabel('\theta'), ylabel('\rho')

%# plot image + corners
figure, imshow(BW), hold on
plot(B(corners,2), B(corners,1), 's', 'MarkerSize',10, 'MarkerFaceColor','r')
hold off, title('Corners')

screenshot1 screenshot2


编辑: 回应Jacob的评论,我首先尝试使用一阶/二阶导数找到签名中的峰值,但最终选择了距离最远的N个点。10只是一个特定的值,很难推广(我尝试过取4个角落的数量,但并没有覆盖所有角落)。我认为将它们聚类以消除重复的想法值得探究。

就我所看到的,第一种方法的问题在于,如果你在不考虑θ的情况下绘制rho,你会得到一个不同的形状(不是相同的峰值),因为我们沿着边界追踪的速度是不同的,并且取决于曲率。如果我们能够找出如何归一化这种影响,我们可以使用导数得到更准确的结果。


我刚刚意识到你已经有了一个二进制图像BW。因此,你可以直接跳过获取边界的部分,从调用bwboundaries开始。 - Amro
+1——边界签名。但是,我在编辑后的帖子中详细说明了一些担忧。 - Jacob
@Amro:非常感谢您提供边界特征点的想法,但我决定挖掘我的旧哈里斯角检测实现并使用它。 - Jacob
1
您可以使用一些线条近似技术来解决角点重复的问题。OpenCV在findContours函数中使用CV_CHAIN_APPROX_SIMPLE标志来实现这一点。至于Matlab,我没有找到内置的方法来处理它,但在文件交换中找到了Douglas-Peucker简化算法的实现http://www.mathworks.com/matlabcentral/fileexchange/21132-line-simplification - omarzouk
1
OpenCV的approxPolyDP函数还实现了Douglas-Peucker算法。http://docs.opencv.org/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html?highlight=cvapproxpoly#approxpolydp - omarzouk
@OSaad:感谢您提供的参考。实际上不久前,在Steve Eddins 博客上出现了一个类似的问题,这个问题被转化为了Cody挑战赛,关于如何简化多边形的bwboundaries输出。我认为线简化技术被提到了:) - Amro

8
如果您拥有图像处理工具箱,则可以使用名为cornermetric的函数来实现Harris角检测器或Shi和Tomasi的最小特征值方法。该函数自MATLAB版本R2008b以来一直存在于图像处理工具箱的6.2版中。
使用此功能,我提出了与其他答案略有不同的方法。以下解决方案基于一个想法:以每个“真正”的角点为中心的圆形区域将比实际在边缘上的错误角点上方的圆形区域重叠多少。该解决方案还可以处理在同一角处检测到多个点的情况...
第一步是加载数据:
rawImage = imread('oxyjj.png');
rawImage = rgb2gray(rawImage(7:473, 9:688, :));  % Remove the gray border
subplot(2, 2, 1);
imshow(rawImage);
title('Raw image');

接下来,使用cornermetric计算角点度量。请注意,我通过原始多边形掩蔽了角点度量,以便我们寻找在多边形内部的角点(即尝试找到多边形的角像素)。然后使用imregionalmax查找局部最大值。由于可能存在具有相同角点度量的大于1个像素的聚类,因此我添加噪声到极值并重新计算,以便每个极大区域只得到1个像素。然后使用bwlabel对每个极大区域进行标记:
cornerImage = cornermetric(rawImage).*(rawImage > 0);
maxImage = imregionalmax(cornerImage);
noise = rand(nnz(maxImage), 1);
cornerImage(maxImage) = cornerImage(maxImage)+noise;
maxImage = imregionalmax(cornerImage);
labeledImage = bwlabel(maxImage);

接下来,使用圆形结构元素(使用strel创建)对标记区域进行膨胀操作(使用imdilate):

diskSize = 5;
dilatedImage = imdilate(labeledImage, strel('disk', diskSize));
subplot(2, 2, 2);
imshow(dilatedImage);
title('Dilated corner points');

现在标记的角落区域已经被膨胀,它们将部分重叠原始多边形。在多边形边缘的区域将有约50%的重叠,而在角上的区域将有约25%的重叠。函数regionprops可用于查找每个标记区域的重叠面积,因此可以将具有最少重叠量的4个区域视为真正的角:
maskImage = dilatedImage.*(rawImage > 0);       % Overlap with the polygon
stats = regionprops(maskImage, 'Area');         % Compute the areas
[sortedValues, index] = sort([stats.Area]);     % Sort in ascending order
cornerLabels = index(1:4);                      % The 4 smallest region labels
maskImage = ismember(maskImage, cornerLabels);  % Mask of the 4 smallest regions
subplot(2, 2, 3);
imshow(maskImage);
title('Regions of minimal overlap');

现在,我们可以使用findismember获取角落的像素坐标:

[r, c] = find(ismember(labeledImage, cornerLabels));
subplot(2, 2, 4);
imshow(rawImage);
hold on;
plot(c, r, 'r+', 'MarkerSize', 16, 'LineWidth', 2);
title('Corner points');

这里输入图片描述

以下是一个菱形区域的测试:

这里输入图片描述


+1 我不知道这个。看它的实现,它类似于 @Jacob 的,但更加优化了。 - Amro
我正在使用R2009a,所以我猜它是一个相对较新的添加。 - gnovice

4
我喜欢通过使用边界来解决这个问题,因为它将其从2D问题简化为1D问题。
使用图像处理工具包中的bwtraceboundary()提取边界上的点列表。然后将边界转换为一系列切向量(有许多方法可以做到这一点,一种方法是将沿边界的第i个点从第i+delta个点中减去)。一旦您获得了向量列表,请对相邻向量进行点积运算。点积最小的四个点就是您的角!
如果您希望您的算法适用于具有任意顶点数量的多边形,则只需搜索标准差低于中位数点积的点积即可。

2

我决定使用哈里斯角点检测器(这里有更正式的描述)来获取角点。实现方法如下:

%% Constants
Window = 3;
Sigma = 2;
K = 0.05;
nCorners = 4;

%% Derivative masks
dx = [-1 0 1; -1 0 1; -1 0 1];
dy = dx';   %SO code color fix '

%% Find the image gradient
% Mask is the binary image of the quadrilateral
Ix = conv2(double(Mask),dx,'same');   
Iy = conv2(double(Mask),dy,'same');

%% Use a gaussian windowing function and compute the rest
Gaussian = fspecial('gaussian',Window,Sigma);
Ix2 = conv2(Ix.^2,  Gaussian, 'same');  
Iy2 = conv2(Iy.^2,  Gaussian, 'same');
Ixy = conv2(Ix.*Iy, Gaussian, 'same');    

%% Find the corners
CornerStrength = (Ix2.*Iy2 - Ixy.^2) - K*(Ix2 + Iy2).^2;
[val ind] = sort(CornerStrength(:),'descend');    
[Ci Cj] = ind2sub(size(CornerStrength),ind(1:nCorners));

%% Display
imshow(Mask,[]);
hold on;
plot(Cj,Ci,'r*');

这里,由于使用了高斯窗函数平滑了强度变化,导致产生了多个角点问题。下面是一个使用hot色标放大的角点版本。

corner


很好!作为一条注释,我喜欢在MATLAB注释中在“%”后面加上“#”,这样它就可以得到正确的SO颜色。 - Amro
谢谢!使用 ' 有什么修复方法吗? - Jacob
不行。听着,我正在测试你的方法,看起来它也不完美(这在计算机视觉中总是如此!)。尝试使用上面的代码和以下掩码:x = [16 282 276 30 16]; y = [14 29 200 225 14]; BW = poly2mask(x,y, 246,300); 即使调整了其他参数,你仍会得到一些重复的角落,并且必须增加 nCorners - Amro
你是完全正确的 - 看起来一些多边形需要聚类。 - Jacob
我不想过于强调我的想法,但我认为如果我们定位到确切的4个theta峰值,则签名方法可以被完善用于大多数四边形,这些峰值在图中清晰可见。挑选最高的N个点可能不是最好的方法,但也许有人可以改进这一部分... - Amro
我找到了一种方法来解决角点处多个点的问题,即通过向最大角度指标值添加一些噪声(我已经更新了我的答案以包括它)。我还能够确保我的实现始终从多边形内的像素中选择一个角点,方法是使用原始多边形遮罩角度指标。 - gnovice

1

这里有一个使用Ruby和HornetsEye的例子。基本上,该程序创建了一个直方图,用于量化Sobel梯度方向以查找主导方向。如果找到四个主导方向,则拟合线条并假定相邻线条之间的交点是投影矩形的角落。

#!/usr/bin/env ruby
require 'hornetseye'
include Hornetseye
Q = 36
img = MultiArray.load_ubyte 'http://imgur.com/oxyjj.png'
dx, dy = 8, 6
box = [ dx ... 688, dy ... 473 ]
crop = img[ *box ]
crop.show
s0, s1 = crop.sobel( 0 ), crop.sobel( 1 )
mag = Math.sqrt s0 ** 2 + s1 ** 2
mag.normalise.show
arg = Math.atan2 s1, s0
msk = mag >= 500
arg_q = ( ( arg.mask( msk ) / Math::PI + 1 ) * Q / 2 ).to_int % Q
hist = arg_q.hist_weighted Q, mag.mask( msk )
segments = ( hist >= hist.max / 4 ).components
lines = arg_q.map segments
lines.unmask( msk ).normalise.show
if segments.max == 4
  pos = MultiArray.scomplex *crop.shape
  pos.real = MultiArray.int( *crop.shape ).indgen! % crop.shape[0]
  pos.imag = MultiArray.int( *crop.shape ).indgen! / crop.shape[0]
  weights = lines.hist( 5 ).major 1.0
  centre = lines.hist_weighted( 5, pos.mask( msk ) ) / weights
  vector = pos.mask( msk ) - lines.map( centre )
  orientation = lines.hist_weighted( 5, vector ** 2 ) ** 0.5
  corner = Sequence[ *( 0 ... 4 ).collect do |i|
    i1, i2 = i + 1, ( i + 1 ) % 4 + 1
    l1, a1, l2, a2 = centre[i1], orientation[i1], centre[i2], orientation[i2]
    ( l1 * a1.conj * a2 - l2 * a1 * a2.conj -
      l1.conj * a1 * a2 + l2.conj * a1 * a2 ) /
      ( a1.conj * a2 - a1 * a2.conj )
  end ] 
  result = MultiArray.ubytergb( *img.shape ).fill! 128
  result[ *box ] = crop
  corner.to_a.each do |c|
    result[ c.real.to_i + dx - 1 .. c.real.to_i + dx + 1,
            c.imag.to_i + dy - 1 .. c.imag.to_i + dy + 1 ] = RGB 255, 0, 0
  end
  result.show
end

Image with estimated position of corners


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接