【问题标题】:Affine transformation between contours in OpenCVOpenCV中轮廓之间的仿射变换
【发布时间】:2013-04-05 11:30:44
【问题描述】:

我有一个从胶片扫描的海底图像的历史时间序列,需要配准。

from pylab import *
import cv2
import urllib

urllib.urlretrieve('http://geoport.whoi.edu/images/frame014.png','frame014.png');
urllib.urlretrieve('http://geoport.whoi.edu/images/frame015.png','frame015.png');

gray1=cv2.imread('frame014.png',0)
gray2=cv2.imread('frame015.png',0)
figure(figsize=(14,6))
subplot(121);imshow(gray1,cmap=cm.gray);
subplot(122);imshow(gray2,cmap=cm.gray);

我想使用每张图像左侧的黑色区域进行配准,因为该区域在相机内部,应该及时修复。所以我只需要计算黑色区域之间的仿射变换。

我通过阈值化和找到最大轮廓来确定这些区域:

def find_biggest_contour(gray,threshold=40):
    # threshold a grayscale image 
    ret,thresh = cv2.threshold(gray,threshold,255,1)
    # find the contours
    contours,h = cv2.findContours(thresh,mode=cv2.RETR_LIST,method=cv2.CHAIN_APPROX_NONE)
    # measure the perimeter
    perim = [cv2.arcLength(cnt,True) for cnt in contours]
    # find contour with largest perimeter
    i=perim.index(max(perim))
    return contours[i]

c1=find_biggest_contour(gray1)
c2=find_biggest_contour(gray2)

x1=c1[:,0,0];y1=c1[:,0,1]
x2=c2[:,0,0];y2=c2[:,0,1]

figure(figsize=(8,8))
imshow(gray1,cmap=cm.gray, alpha=0.5);plot(x1,y1,'b-')
imshow(gray2,cmap=cm.gray, alpha=0.5);plot(x2,y2,'g-')
axis([0,1500,1000,0]);

蓝色是第一帧最长的轮廓,绿色是第二帧最长的轮廓。

确定蓝色和绿色轮廓之间的旋转和偏移的最佳方法是什么?

我只想在台阶周围的某个区域使用轮廓的右侧,例如箭头之间的区域。

当然,如果有更好的方法来注册这些图像,我很乐意听到。我已经在原始图像上尝试了标准的特征匹配方法,但效果不够好。

【问题讨论】:

  • 您只需要绿色和蓝色轮廓之间的矢量还是矢量、旋转角度和缩放比例?
  • 仅偏移和旋转(无缩放)

标签: python opencv


【解决方案1】:

按照 Shambool 的建议方法,这就是我想出的方法。我使用了 Ramer-Douglas-Peucker 算法来简化感兴趣区域的轮廓,并确定了两个转折点。我打算使用两个转折点来获得我的三个未知数(xoffset、yoffset 和旋转角度),但是第二个转折点向右有点太远了,因为 RDP 简化了该区域中更平滑的曲线。因此,我使用了通往第一个转折点的线段的角度。在 image1 和 image2 之间区分这个角度给了我旋转角度。我仍然对这个解决方案并不完全满意。它对这两个图像运行良好,但我不确定它是否能在整个图像序列上运行良好。走着瞧。

将轮廓拟合到黑色边框的已知形状会更好。

# select region of interest from largest contour 
ind1=where((x1>190.) & (y1>200.) & (y1<900.))[0]
ind2=where((x2>190.) & (y2>200.) & (y2<900.))[0]
figure(figsize=(10,10))
imshow(gray1,cmap=cm.gray, alpha=0.5);plot(x1[ind1],y1[ind1],'b-')
imshow(gray2,cmap=cm.gray, alpha=0.5);plot(x2[ind2],y2[ind2],'g-')
axis([0,1500,1000,0])

def angle(x1,y1):
    #  Returns angle of each segment along an (x,y) track
    return array([math.atan2(y,x) for (y,x) in zip(diff(y1),diff(x1))])

def simplify(x,y, tolerance=40, min_angle = 60.*pi/180.): 
    """
    Use the Ramer-Douglas-Peucker algorithm to simplify the path
    http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
    Python implementation: https://github.com/sebleier/RDP/
    """
    from RDP import rdp   
    points=vstack((x,y)).T
    simplified = array(rdp(points.tolist(), tolerance))
    sx, sy = simplified.T

    theta=abs(diff(angle(sx,sy)))
    # Select the index of the points with the greatest theta
    # Large theta is associated with greatest change in direction.
    idx = where(theta>min_angle)[0]+1
    return sx,sy,idx

sx1,sy1,i1 = simplify(x1[ind1],y1[ind1])
sx2,sy2,i2 = simplify(x2[ind2],y2[ind2])
fig = plt.figure(figsize=(10,6))
ax =fig.add_subplot(111)

ax.plot(x1, y1, 'b-', x2, y2, 'g-',label='original path')
ax.plot(sx1, sy1, 'ko-', sx2, sy2, 'ko-',lw=2, label='simplified path')
ax.plot(sx1[i1], sy1[i1], 'ro', sx2[i2], sy2[i2], 'ro', 
    markersize = 10, label='turning points')
ax.invert_yaxis()
plt.legend(loc='best')

# determine x,y offset between 1st turning points, and 
# angle from difference in slopes of line segments approaching 1st turning point
xoff = sx2[i2[0]] - sx1[i1[0]]
yoff = sy2[i2[0]] - sy1[i1[0]]
iseg1 = [i1[0]-1, i1[0]]
iseg2 = [i2[0]-1, i2[0]]
ang1 = angle(sx1[iseg1], sy1[iseg1])
ang2 = angle(sx2[iseg2], sy2[iseg2])
ang = -(ang2[0] - ang1[0])
print xoff, yoff, ang*180.*pi

-28 14 5.07775871644

# 2x3 affine matrix M
M=array([cos(ang),sin(ang),xoff,-sin(ang),cos(ang),yoff]).reshape(2,3)
print M

[[  9.99959685e-01   8.97932821e-03  -2.80000000e+01]
 [ -8.97932821e-03   9.99959685e-01   1.40000000e+01]]

# warp 2nd image into coordinate frame of 1st
Minv = cv2.invertAffineTransform(M)
gray2b = cv2.warpAffine(gray2,Minv,shape(gray2.T))

figure(figsize=(10,10))
imshow(gray1,cmap=cm.gray, alpha=0.5);plot(x1[ind1],y1[ind1],'b-')
imshow(gray2b,cmap=cm.gray, alpha=0.5);
axis([0,1500,1000,0]);
title('image1 and transformed image2 overlain with 50% transparency');

【讨论】:

  • 这里的 Stackoverflow 礼仪是什么?我是否选择 Shambool 的建议,因为他启发了我最终使用的解决方案?还是我选择我的解决方案是因为它包含代码,而且是我实际使用的?
【解决方案2】:

好问题。

一种方法是将轮廓表示为二维点云,然后进行配准。 更多simple and clear code in Matlab可以给你仿射变换。

还有more complex C++ code(using VXL lib) 包括python 和matlab 包装器。 或者您可以使用一些改进的 ICP(迭代最近点)算法,该算法对噪声具有鲁棒性并且可以处理仿射变换。

此外,您的轮廓似乎不是很准确,因此可能是个问题。

另一种方法是使用某种使用像素值的配准。 Matlab code(我认为它使用了某种最小化器+互相关度量) 也可能有某种用于医学成像的光流配准(或其他类型)。

您还可以将点特征用作 SIFT(SURF)。

您可以在FIJI(ImageJ) 中快速尝试 还有这个link。

  1. 打开 2 张图片
  2. 插件->特征提取->筛选(或其他)
  3. 将预期变换设置为仿射
  4. 查看 ImageJ 日志中估计的变换模型 [3,3] 单应矩阵。 如果它运行良好,那么您可以使用 OpenCV 在 python 中实现它,或者使用 Jython 和 ImageJ。

如果您发布原始图像并描述所有情况会更好(似乎图像在帧之间发生变化)

【讨论】:

  • 我确实发布了原始图像。图像确实在变化,因为海底条件随时间变化。这就是为什么我想使用不应该改变的黑色区域来注册图像。
【解决方案3】:

您可以用它们各自的椭圆来表示这些轮廓。这些椭圆以轮廓的质心为中心,并且朝向主密度轴。您可以比较质心和方位角。

1) 填充轮廓 => drawContours 厚度=CV_FILLED

2) 寻找时刻 => cvMoments()

3) 和use them。

质心:{ x, y } = {M10/M00, M01/M00 }

方向(θ):

编辑:我为您的案例定制了来自旧版 (enteringblobdetection.cpp) 的示例代码。

            /* Image moments */
            double      M00,X,Y,XX,YY,XY;
            CvMoments   m;
            CvRect      r = ((CvContour*)cnt)->rect;
            CvMat       mat;
            cvMoments( cvGetSubRect(pImgFG,&mat,r), &m, 0 );
            M00 = cvGetSpatialMoment( &m, 0, 0 );
            X = cvGetSpatialMoment( &m, 1, 0 )/M00;
            Y = cvGetSpatialMoment( &m, 0, 1 )/M00;
            XX = (cvGetSpatialMoment( &m, 2, 0 )/M00) - X*X;
            YY = (cvGetSpatialMoment( &m, 0, 2 )/M00) - Y*Y;  
            XY = (cvGetSpatialMoment( &m, 1, 1 )/M00) - X*Y; 

            /* Contour description */
            CvPoint myCentroid(r.x+(float)X,r.y+(float)Y);
            double myTheta =  atan( 2*XY/(XX-YY) );

另外,请检查 this 与 OpenCV 2.0 示例。

【讨论】:

  • 这是一个很酷的想法,但在这种情况下我不认为它会起作用,因为这些时刻会受到我不想要的轮廓顶部和左侧的那些东西的影响使用。
  • 您可以裁剪对象。
  • 我也想过,但在我看来,为了让这种方法给出正确的答案,需要使用旋转和平移的边界框来裁剪对象,这需要知道已经回答了。对不对?
【解决方案4】:

如果您不想找到两个图像之间的单应性并且想要找到仿射变换,您需要三个未知数,即旋转角度 (R) 以及 x 和 y 中的位移 (X,Y)。因此,至少需要两个点(每个点有两个已知值)才能找到未知数。两个点应该在两个图像或两条线之间匹配,每个点都有两个已知值,截距和斜率。如果您使用点匹配方法,则点之间的距离越远,找到的噪声转换就越稳健(如果您记住错误传播规则,这非常简单)。

在两点匹配法中:

  1. 在第一张图像 I1 中找到两个点(A 和 B),在第二张图像 I2 中找到它们对应的点(A',B')
  2. 找到 A 和 B 之间的中点:C,以及 A' 和 B' 之间的中点:C'
  3. C 和 C' 的差异 (C-C') 给出了图像(X 和 Y)之间的平移
  4. 使用 C-A 和 C'-A' 的点积可以求出旋转角度 (R)

为了检测稳健点,我会在计数器侧面找到具有最高二阶导数(Hessian)绝对值的点,然后尝试匹配它们。由于您提到这是一个视频片段,您可以轻松地假设每两帧之间的转换很小以拒绝异常值。

【讨论】:

  • 这似乎最有希望。我正在努力获得这两点。
  • 我今天太忙了,没时间帮忙写代码。一旦有了候选匹配点,有几种简单的方法(由于图像之间的简单变换)可以消除不匹配/异常值,因此一种方法是,与其努力寻找最佳匹配,不如找到一组候选匹配,然后对其进行细化,直到获得最好的两个匹配。可能明天我将能够提供编码,以防到那时您还没有找到可靠的解决方案。
  • 我主要是按照您的建议发布了代码。仍然对我确定我的 3 个未知数的方式不完全满意。如果您有更好的想法,请告诉我。
  • 你能把视频上传到 youtube 或其他地方让我们看看吗?
  • 你想要原始图像的视频,还是使用上面的代码注册它们?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-25
  • 1970-01-01
  • 1970-01-01
  • 2011-12-25
  • 1970-01-01
相关资源
最近更新 更多