图像处理基础+OpenCV实践

图像概述和直方图

相关算子

g = f ⊗ h g=f\otimes h g=fh

定义为:
g ( i , j ) = ∑ k , l f ( i + k , j + l ) h ( k , l ) g(i,j)=\sum_{k,l}f(i+k,j+l)h(k,l) g(i,j)=k,lf(i+k,j+l)h(k,l)
以3*3的h矩阵为例,如果锚点在矩阵中央的话,则 ( i − 1 ≤ k ≤ i + 1 , j − 1 ≤ l ≤ j + 1 ) (i-1\le k\le i+1,j-1\le l\le j+1) (i1ki+1,j1lj+1)。如果锚点在左上角的话,则 ( i ≤ k ≤ i + 2 , j ≤ l ≤ j + 2 ) (i\le k\le i+2,j\le l\le j+2) (iki+2,jlj+2)

卷积算子

g = f ∗ h g=f\ast h g=fh

定义为:
g ( i , j ) = ∑ k , l f ( i − k , j − l ) h ( k , l ) g(i,j)=\sum_{k,l}f(i-k,j-l)h(k,l) g(i,j)=k,lf(ik,jl)h(k,l)
显然
f ∗ h = f ⊗ r o t 180 ( h ) f\ast h=f\otimes rot180(h) fh=frot180(h)
rotN表示将矩阵元素绕中心逆时针旋转N度。

灰度化

以RGB格式的彩图为例,通常灰度化采用的方法主要有:

  • G r a y = ( R + G + B ) / 3 Gray=(R+G+B)/3 Gray=(R+G+B)/3;
  • G r a y = max ⁡ ( R , G , B ) Gray=\max(R,G,B) Gray=max(R,G,B);
  • G r a y = 0.299 R + 0.587 G + 0.114 B Gray=0.299R+0.587G+0.114B Gray=0.299R+0.587G+0.114B.

灰度直方图

灰度直方图是灰度级的函数,描述图像中该灰度级的像素个数(或该灰度级像素出现的频率):其横坐标是灰度级,纵坐标表示图像中该灰度级出现的个数(频率)。

一维直方图的结构表示为:
N ( P ) = [ n 1 , n 2 , … , n L − 1 ] N = ∑ i = 0 L − 1 n i , p i = n i N N(P)=[n_1,n_2,\dots,n_{L-1}] \\ N=\sum_{i=0}^{L-1}n_i,p_i=\frac{n_i}{N} N(P)=[n1,n2,,nL1]N=i=0L1ni,pi=Nni
其中,L为灰度级的个数, n i n_i ni为每个灰度的像素个数,其频率为 p i p_i pi

直方图均衡化

直方图均衡化主要处理曝光不够或曝光过度的图像,用于提高图片的对比度。

算法步骤:

  1. 建立图像灰度直方图。

  2. 计算累积分布函数 P ( k ) P(k) P(k):
    P ( k ) = ∑ i = 0 k p i P(k)=\sum_{i=0}^k p_i P(k)=i=0kpi

  3. 生成新的灰度和旧的灰度的对应关系数组:
    T ( i ) = P ( i ) ∗ L T(i)=P(i)*L T(i)=P(i)L
    其中 T ( i ) T(i) T(i)表示新灰度值。

  4. 用新的灰度值代替旧的灰度值。

图像几何变换

图像处理基础+OpenCV实践_第1张图片

  1. 平移
    自由度为2.
    x ′ = [ I t ] x o r x ′ = [ I t 0 T 1 ] x \bold{x}'= \begin{bmatrix}\bold{I} & \bold{t} \end{bmatrix} \bold{x} \quad or \quad \bold{x}'= \begin{bmatrix}\bold{I} & \bold{t} \\ \bold{0}^T & 1\end{bmatrix} \bold{x} x=[It]xorx=[I0Tt1]x

  2. 刚体运动(旋转+平移)
    自由度为3。
    x ′ = [ R t ] x \bold{x}'= \begin{bmatrix}\bold{R} & \bold{t} \end{bmatrix} \bold{x} x=[Rt]x
    其中
    R = [ cos ⁡ θ − sin ⁡ θ sin ⁡ θ cos ⁡ θ ] \bold{R}=\begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} R=[cosθsinθsinθcosθ]

  3. 相似变换(缩放旋转+平移)
    自由度为4.
    x ′ = [ s R t ] x = [ a − b t x b a t y ] x \bold{x}'= \begin{bmatrix}s\bold{R} & \bold{t} \end{bmatrix} \bold{x}= \begin{bmatrix}a & -b & t_x \\ b & a & t_y \end{bmatrix} \bold{x} x=[sRt]x=[abbatxty]x

  4. 仿射变换

    自由度为6,仿射变换后的平行线仍然保持平行。
    x ′ = A x = [ a 00 a 01 a 02 a 10 a 11 a 12 ] x \bold{x}'= \bold{A} \bold{x}= \begin{bmatrix}a_{00} & a_{01} & a_{02} \\ a_{10} & a_{11} & a_{12} \end{bmatrix} \bold{x} x=Ax=[a00a10a01a11a02a12]x

    // 根据Point2f[] srcPts和dstPts计算仿射变换矩阵,warp_mat为2x3的矩阵
    Mat warp_mat = getAffineTransform(srcPts, dstPts);
    
    // 将图像进行仿射变换
    Mat warp_dst = Mat::zeros(src.rows, src.cols, src.type());
    // 输入图像、输出图像、仿射变换矩阵、输出图像的尺寸
    warpAffine(src, warp_dst, warp_mat, warp_dst.size());
    
  5. 透视/单应性变换
    自由度为8.
    x 3 × 1 ′ = H 3 × 3 x 3 × 1 \bold{x}'_{3\times1}= \bold{H}_{3\times3} \bold{x}_{3\times1} x3×1=H3×3x3×1
    齐次坐标 x 3 × 1 ′ \bold{x}'_{3\times1} x3×1必须经过归一化以得到非齐次坐标,即
    x ′ = h 00 x + h 01 y + h 02 h 20 x + h 21 y + h 22 , y ′ = h 10 x + h 11 y + h 12 h 20 x + h 21 y + h 22 x'=\frac{h_{00}x+h_{01}y+h_{02}}{h_{20}x+h_{21}y+h_{22}},\quad y'=\frac{h_{10}x+h_{11}y+h_{12}}{h_{20}x+h_{21}y+h_{22}} x=h20x+h21y+h22h00x+h01y+h02,y=h20x+h21y+h22h10x+h11y+h12
    单应性变换保留了直线。

模板匹配

匹配方法有平方差TM_SQDIFF、归一化平方差TM_SQDIFF_NORMED、相关性TM_CCORR、归一化相关性TM_CCORR_NORMED、协方差TM_CCOEFF、归一化协方差TM_CCOEFF_NORMED。

// 输入图像I、模板T、匹配结果R、匹配方法、掩码图像M
matchTemplate(img, temp1, result, match_method=TM_SQDIFF, mask);
// 定位结果R中的最大和最小值
double minVal; double maxVal; Point minLoc; Point maxLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc, Mat());
// 对于平方差方法最佳匹配是R的最小值,对于其他方法最佳匹配是R的最大值
Point matchLoc;
if (match_method == TM_SQDIFF || match_method == TM_SQDIFF_NORMED) {
    matchLoc = minLoc;
} else {
    matchLoc = maxLoc;
}

图像滤波和变换

图像处理基础+OpenCV实践_第2张图片

高通滤波

高通滤波器(HPF)是检测图像的某个区域,然后根据像素与周围像素的亮度差值来提升该像素的亮度的滤波器。如果亮度变化很大,中央像素的亮度会增加,反之则不会。主要用于边缘检测。

# 高通滤波器
kernel_3x3 = np.array([[-1,-1,-1],[-1,8,-1],[-1,-1,-1]])
# 卷积运算
k3 = ndimage.convolve(img, kernel_3x3)

边缘检测

梯度算子
  • Sobel算子
    G x = [ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] ∗ I , G y = [ − 1 − 2 − 1 0 0 0 + 1 + 2 + 1 ] ∗ I 梯 度 向 量 的 模 G = G x 2 + G y 2 梯 度 方 向 θ = a r c t a n ( G y G x ) G_{x} = \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \end{bmatrix} * I, G_{y} = \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \end{bmatrix} * I \\ 梯度向量的模G=\sqrt{G_x^2+G_y^2}\\ 梯度方向\theta=arctan(\frac{G_y}{G_x}) Gx=121000+1+2+1I,Gy=10+120+210+1IG=Gx2+Gy2 θ=arctan(GxGy)

    Mat grad_x, grad_y;
    int ddepth = CV_16S;
    int ksize = 3;
    double scale = 1;
    double delta = 0;
    // 输入图像、输出图像、输出图像的深度、x方向导数的阶数、y方向导数的阶数、核尺寸、...
    Sobel(src_gray, grad_x, ddepth, 1, 0, ksize, scale, delta, BORDER_DEFAULT); // x方向梯度
    Sobel(src_gray, grad_y, ddepth, 0, 1, ksize, scale, delta, BORDER_DEFAULT); // y方向梯度
    
    // 将输出转换为CV_8U图像
    Mat abs_grad_x, abs_grad_y;
    convertScaleAbs(grad_x, abs_grad_x);
    convertScaleAbs(grad_y, abs_grad_y);
    
    // 合并梯度:将x、y方向的梯度进行加权
    Mat grad;
    addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
    imshow("sobel gradient", grad);
    
  • Laplace算子
    拉普拉斯算子是一种二阶微分算子,因此,它一般用二阶微分符号 ∇ 2 f ∇^2f 2f来表示。其常用的相关核有:
    h 1 = [ 0 − 1 0 − 1 4 − 1 0 − 1 0 ] , h 2 = [ − 1 − 1 − 1 − 1 8 − 1 − 1 − 1 − 1 ] h_1=\left[\begin{array}{ccc} 0&-1&0\\ -1&4&-1 \\ 0&-1&0\end{array} \right],h_2=\left[\begin{array}{ccc} -1&-1&-1\\ -1&8&-1 \\ -1&-1&-1\end{array} \right] h1=010141010,h2=111181111

    Mat dst, abs_dst;
    // 输入、输出、输出图像深度(应为CV_16S防止溢出)、核尺寸、...
    Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );
    // 将输出转换为CV_8U
    convertScaleAbs(dst, abs_dst);
    imshow("Laplace gradient", abs_dst);
    
Canny边缘

Canny边缘检测算法非常复杂,但是也很有趣,它有5个步骤,即使用高斯滤波器对图像进行去噪,计算梯度,在边缘上使用非最大抑制,在检测到的边缘上使用双阈值去除假阳性,最后还会分析所有的边缘及其之间的连接,以保留真正的边缘并消除不明显的边缘。

算法步骤

  1. 使用高斯滤波去噪:
    K = 1 159 [ 2 4 5 4 2 4 9 12 9 4 5 12 15 12 5 4 9 12 9 4 2 4 5 4 2 ] K = \dfrac{1}{159}\begin{bmatrix} 2 & 4 & 5 & 4 & 2 \\ 4 & 9 & 12 & 9 & 4 \\ 5 & 12 & 15 & 12 & 5 \\ 4 & 9 & 12 & 9 & 4 \\ 2 & 4 & 5 & 4 & 2 \end{bmatrix} K=1591245424912945121512549129424542

  2. 使用Sobel算子计算梯度,梯度方向离散化为4个方向。

  3. 非最大值抑制,消除边误检。

  4. 双阈值:若像素梯度大于上界,认为该像素为边缘;若像素梯度小于下界,则去除;若像素梯度位于中间,则仅当该像素和一个梯度大于上界的像素相连时,才认为该像素为边缘。上下界比通常为2:1或3:1。

cv2.Canny函数部分参数如下:

  • 第一个参数是需要处理的原图像,该图像必须是单通道的灰度图;
  • 第二个参数是阈值1。
  • 第三个参数是阈值2.

其中较大的阈值2用于检测图像中明显的边缘,但是一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候较小的第一个阈值用于将这些断续的边缘连接起来。

Mat img_gray, canny_output; 
// 输入、输出(二值化Canny图像)、下界、上界、Sobel算子的核尺寸
Canny(img_gray, canny_output, lowThresh, highThresh, kernel_size);
轮廓提取

在提取了Canny边缘后,可以通过findContours函数进一步提取图像的轮廓,函数签名如下:

/*
image: 单通道输入图像,通常用经过Canny、Laplace等边缘检测算子处理过的二值化图像
contours: 是一个二重向量,向量的每个元素代表了一组Point点集构成的一个轮廓
hierarchy: 也是一个向量,其元素和contours内的轮廓一一对应,其内部的4个int值分别表示轮廓的后一个轮廓、前一个轮廓、父轮廓和内嵌轮廓的索引。
mode: 轮廓的检索模式,包括:
		CV_RETR_EXTERNAL 只检测最外围轮廓
		CV_RETR_LIST 	 检测所有轮廓,但是检测的轮廓不建立等级关系(没有父和内嵌轮廓),彼此独立
		CV_RETR_CCOMP	 检测所有轮廓,但是轮廓只建立两个等级关系,外围为顶层。
		CV_RETR_TREE 	 检测所有轮廓,建立一个等级树结构,外层轮廓包含内层轮廓。
method: 轮廓近似方法,包括:
		CV_CHAIN_APPROX_NONE  	保存物体边界上所有连续的轮廓点
		CV_CHAIN_APPROX_SIMPLW  仅保存轮廓的拐点信息
offset: 所有轮廓信息相对于原图对应点的偏移量,相当于在轮廓点上加一个偏移
*/
findContours(InputOutputArray image, vector> contours, vector hierarchy, 
             int mode, int method, Point offset=Point());


// 实例程序
Canny(img_gray, img_canny, 100, 250);
vector> contours;
vector hierarchy;
findContours(img_canny, contours, hierarchy, RETR_TREE, CHAIN_APPORX_SIMPLE);

// 绘制轮廓
Mat drawing = Mat::zeros(canny_output.size(), CV_8UC3);
for (size_t i = 0; i < contours.size(); i++) {
    Scalar color = Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
    drawContours(drawing, contours, (int)i, color, 2, 8, hierarchy, 0);
}

低通滤波

低通滤波器(LPF)是在像素与周围像素的亮度差值小于一定特征值,平滑该像素的亮度。它主要用于去噪和模糊化,比如说,高斯模糊是最常用的模糊滤波器(平滑滤波器)之一,它是削弱高频信号强度的低通滤波器。

  • 方框滤波
    g = f ⊗ h , h = α [ 1 1 1 ⋯ 1 1 1 1 ⋯ 1 ⋯ ⋯ ⋯ ⋯ ⋯ 1 1 1 ⋯ 1 ] , α = { 1 S U M ( h ) , normalize=true 1 , normalize=false g=f\otimes h,h=\alpha \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & 1 & 1 & \cdots & 1 \\ \cdots & \cdots & \cdots & \cdots & \cdots \\ 1 & 1 & 1 & \cdots & 1 \end{bmatrix} ,\alpha = \begin{cases} \frac{1}{SUM(h)}, & \text{normalize=true} \\ 1, & \text{normalize=false} \\ \end{cases} g=fh,h=α111111111111,α={SUM(h)1,1,normalize=truenormalize=false
    当normalize=true时方框滤波变为均值滤波。
// 输入图像、输出图像、滤波器尺寸、anchor点位置(默认为核中心)
blur(src, dst, Size(i, i), Point(-1, -1));
  • 中值滤波
// 输入、输出、核尺寸
medianBlur(src, dst, i);
  • 高斯滤波

G 0 ( x , y ) = A e − ( x − μ x ) 2 2 σ x 2 + − ( y − μ y ) 2 2 σ y 2 G_{0}(x, y) = A e^{ \dfrac{ -(x - \mu_{x})^{2} }{ 2\sigma^{2}_{x} } + \dfrac{ -(y - \mu_{y})^{2} }{ 2\sigma^{2}_{y} } } G0(x,y)=Ae2σx2(xμx)2+2σy2(yμy)2

最常用的3x3高斯滤波器为:
[ 0.0625 0.125 0.0625 0.125 0.25 0.125 0.0625 0.125 0.0625 ] \left[\begin{array}{ccc} 0.0625&0.125&0.0625\\ 0.125&0.25&0.125 \\ 0.0625&0.125&0.0625\end{array}\right] 0.06250.1250.06250.1250.250.1250.06250.1250.0625

// 输入、输出、核尺寸、X方向的高斯核标准差、Y方向的标准差
GaussianBlur(src, dst, Size(i, i), 0, 0);
  • 膨胀(最大值滤波)
    g ( i , j ) = m a x k , l f ( i , j ) g(i,j)=max_{k,l}f(i,j) g(i,j)=maxk,lf(i,j)

    // 矩形结构元、结构元尺寸、anchor位置
    Mat element = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
    // 输入、输出、结构元
    dilate(src, dst, element);
    
  • 腐蚀(最小值滤波)
    g ( i , j ) = m i n k , l f ( i , j ) g(i,j)=min_{k,l}f(i,j) g(i,j)=mink,lf(i,j)

    // 十字结构元、结构元尺寸、anchor位置
    Mat element = getStructuringElement(MORPH_CROSS, Size(3, 3), Point(-1, -1));
    // 输入、输出、结构元
    erode(src, dst, element);
    
  • 双边滤波
    用于保边去噪,如美颜。其输出像素依赖于邻域像素值的加权组合
    g ( i , j ) = ∑ k , l f ( k , l ) w ( i , j , k , l ) ∑ k , l w ( i , j , k , l ) g(i,j)=\frac{\sum_{k,l}f(k,l)w(i,j,k,l)}{\sum_{k,l}w(i,j,k,l)} g(i,j)=k,lw(i,j,k,l)k,lf(k,l)w(i,j,k,l)
    其中,
    w ( i , j , k , l ) = d ( i , j , k , l ) ⋅ r ( i , j , k , l ) = exp ⁡ ( − ( i − k ) 2 + ( j − l ) 2 2 σ d 2 ) ⋅ exp ⁡ ( − ∥ f ( i , j ) − f ( k , l ) ∥ 2 2 σ r 2 ) w(i,j,k,l)=d(i,j,k,l)\cdot r(i,j,k,l)=\exp\left(-\frac{(i-k)^2+(j-l)^2}{2\sigma_d^2}\right)\cdot \exp\left(-\frac{\|f(i,j)-f(k,l)\|^2}{2\sigma_r^2}\right) w(i,j,k,l)=d(i,j,k,l)r(i,j,k,l)=exp(2σd2(ik)2+(jl)2)exp(2σr2f(i,j)f(k,l)2)

形态学操作

  • 开操作
    用以消除小物体(噪点)
    o p e n ( s r c ) = d i l a t e ( e r o d e ( s r c ) ) open(src)=dilate(erode(src)) open(src)=dilate(erode(src))

  • 闭操作
    用于消除小的黑洞
    d s t = c l o s e ( s r c , e l e m e n t ) = e r o d e ( d i l a t e ( s r c , e l e m e n t ) ) dst = close( src, element ) = erode( dilate( src, element ) ) dst=close(src,element)=erode(dilate(src,element))

  • 形态学梯度

    对二值图像进行这一操作可以将团块(blob)的边缘突出出来。我们可以用形态学梯度来保留物体的边缘轮廓。
    m o r p h g r a d ( s r c ) = d i l a t e ( s r c ) − e r o d e ( s r c ) morphgrad(src)=dilate(src)-erode(src) morphgrad(src)=dilate(src)erode(src)

代码实现

// operation为形态学操作,包括:
// 开=MORPH_OPEN,闭=MORPH_CLOSE,梯度=MORPH_GRADIENT
morphologyEx(src, dst, operation, element);

图像金字塔

  • 高斯金字塔(用于图像下采样)
    下采样:高斯滤波,然后移除图像的偶数列和行(取1/4)。

    上采样:将图像在每个方向扩大为原来的2倍,新增的值以0填充;使用先前同样的核(乘以4)与放大后的图像卷积,获得“新值像素”的近似值。

    // 输入、输出、输出尺寸
    pyrDown( src, src, Size( src.cols/2, src.rows/2 ) );
    pyrUp(src, src, Size(src.cols*2, src.rows*2));
    
  • 拉普拉斯金字塔(用于下层图像的重建上采样)
    为使上采样和下采样可逆,拉普拉斯金字塔定义为:
    L i = G i − P y r U p ( P y r D o w n ( G i ) ) = G i − U p ( P y r D o w n ( G i ) ) ⊗ H 5 × 5 L_i=G_i-PyrUp(PyrDown(G_i))=G_i-Up(PyrDown(G_i))\otimes H_{5\times 5} Li=GiPyrUp(PyrDown(Gi))=GiUp(PyrDown(Gi))H5×5
    其中Up操作是将源图像中位置为(x,y)的像素映射到目标图像的(2x+1,2y+1)位置, H 5 × 5 H_{5×5} H5×5表示5x5的高斯核。

Mat src; // 原图
Mat laplace_pyramid, temp; // 金字塔图像、暂存原图
  
pyrDown(src, src, Size(src.cols / 2, src.rows / 2));
pyrUp(src, src, Size(src.cols * 2, src.rows * 2));
  
laplace_pyr![请添加图片描述](https://img-blog.csdnimg.cn/03511b9e32294adc9f4592d0d5c613be.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2JlcmV6ZQ==,size_16,color_FFFFFF,t_70)
amid = temp - src;  
imshow("Laplace_pyramid", laplace_pyramid);

Hough变换

Hough变换用于检测直线,在使用前需要先经过边缘检测的预处理。

对于一条直线,可以使用极坐标形式描述:
图像处理基础+OpenCV实践_第3张图片
y = ( − cos ⁡ θ sin ⁡ θ ) x + ( r sin ⁡ θ ) y = (-\frac{\cos \theta}{\sin \theta})x+(\frac{r}{\sin \theta}) y=(sinθcosθ)x+(sinθr)

  1. 对于每个点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),可以定义穿过该点的直线簇:
    r θ = x 0 cos ⁡ θ + y 0 sin ⁡ θ r_{\theta}=x_0\cos\theta+y_0\sin\theta rθ=x0cosθ+y0sinθ
    一对 ( r θ , θ ) (r_{\theta},\theta) (rθ,θ)就代表一条穿过 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)的直线。

  2. 对于给定的一个点 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),可以绘制 ( r , θ ) (r,\theta) (r,θ)的曲线,其为一条正弦曲线:(只考虑 r > 0 r>0 r>0 0 < θ < 2 π 0<\theta<2\pi 0<θ<2π

图像处理基础+OpenCV实践_第4张图片

  1. 我们对图像中的所有点都绘制这样的曲线。如果两个不同点的曲线相交于一点 ( r , θ ) (r,\theta) (r,θ),这表明这两个点都在这条直线上。
    图像处理基础+OpenCV实践_第5张图片

  2. 由此,可以定义一个交点阈值来检测一条直线,当交点个数超过该阈值时,表明该交点 ( r , θ ) (r,\theta) (r,θ)可以描述一条直线。这就是Hough线变换。

// 原图、边缘检测处理过的二值化图、标准Hough变换检测结果、概率Hough变换检测结果
Mat src, img_canny, cdst, cdst_p;  
// step 1边缘检测预处理
Canny(src, img_canny, 50, 200, 3);
// 将边缘图像变为BGR图像,以便结果可视化
cvtColor(img_canny, cdst, COLOR_GRAY2BGR);
cdst_p = cdst.clone();

// step 2 Standard Hough line transform
vector<Vec2f> lines;  // 存储检测到的直线的向量,其中每个元素表示一条直线(rho, theta)
// 输入图像、直线向量、以像素为单位的距离精度、以弧度为单位的角度精度、检测阈值、(...)
HoughLines(img_canny, lines, 1, CV_PI/180, 150, 0, 0);
// Draw the lines
for (size_t i = 0; i < lines.size(); i++) {
    float rho = lines[i][0], theta = lines[i][1];
    Point pt1, pt2;
    double a = cos(theta), b = sin(theta);
    double x0 = a * rho, y0 = b * rho;
    pt1.x = cvRound(x0 + 1000 * (-b));
    pt1.y = cvRound(y0 + 1000 * a);
    pt2.x = cvRound(x0 + 1000 * b);
    pt2.y = cvRound(y0 + 1000 * (-a));
    line(cdst, pt1, pt2, Scalar(0, 0, 255), 3, LINE_AA);
}

// step 3 Probabilistic Hough transform 累计概率霍夫变换(相比标准霍夫变换效果更好)
vector<Vec4i> lines_p;  // 每条直线由四个int值(x1, y1, x2, y2)表示,(x1, y1)和(x2, y2)为线段的端点
// 输入图像、直线向量、距离精度、角度精度、检测阈值、最短线段长度、同一直线上两点之间的最大间隔
HoughLinesP(img_canny, lines_p, 1, CV_PI/180, 50, 50, 10);
// Draw the lines
for (size_t i = 0; i < lines_p.size(); i++) {
    Vec4i l = lines_p[i];
    line(cdst_p, Point(l[0], l[1]), Point(l[2],l[3]), Scalar(0, 0, 255), 3, LINE_AA);
}

// Show results
imshow("Source", src);
imshow("Detected Lines (in red) - Standard", cdst);
imshow("Detected Lines (in red) - Probabilistic", cdst_p);

经典手工特征

Harris角点

Harris角点具有旋转不变性,但不具有尺度不变性。

基本思想

使用一个固定窗口在图像上进行任意方向上的滑动,比较滑动前后窗口中像素灰度变化,如果存在任意方向上的滑动,使得灰度变化都较大,则认为该窗口中存在角点。

算法步骤

  1. 计算图像 I ( x , y ) I(x,y) I(x,y) x x x y y y方向上的梯度 I x , I y I_x,I_y Ix,Iy
    I x = ∂ I ∂ x = I × [ − 1 0 1 ] I y = ∂ I ∂ y = I × [ − 1 0 1 ] T I_x=\frac{∂I}{∂x}=I×\begin{bmatrix} -1 & 0 & 1 \end{bmatrix} \\ I_y=\frac{∂I}{∂y}=I×\begin{bmatrix} -1 & 0 & 1 \end{bmatrix}^T Ix=xI=I×[101]Iy=yI=I×[101]T

  2. 计算图像两个方向梯度的乘积:
    I x 2 = I x ∗ I x I y 2 = I y ∗ I y I x I y = I x ∗ I y I_x^2 =I_x*I_x \\ I_y^2 =I_y*I_y \\ I_xI_y =I_x*I_y Ix2=IxIxIy2=IyIyIxIy=IxIy

  3. 使用高斯函数对 I x 2 I_x^2 Ix2 I y 2 I_y^2 Iy2 I x I y I_xI_y IxIy进行高斯加权(取 σ = 2 σ=2 σ=2, k s i z e = 3 ksize=3 ksize=3),计算中心点为 ( x , y ) (x,y) (x,y)的窗口 W W W对应的矩阵 M M M
    A = ∑ ( x , y ) € W g ( I x 2 ) = ∑ ( x , y ) € W I x 2 ∗ w ( x , y ) B = ∑ ( x , y ) € W g ( I y 2 ) = ∑ ( x , y ) € W I y 2 ∗ w ( x , y ) C = ∑ ( x , y ) € W g ( I x I y ) = ∑ ( x , y ) € W I x I y ∗ w ( x , y ) A=\sum\limits_{(x,y)€W}g(I_x^2)=\sum\limits_{(x,y)€W}I_x^2*w(x,y)\\ B=\sum\limits_{(x,y)€W}g(I_y^2)=\sum\limits_{(x,y)€W}I_y^2*w(x,y)\\ C=\sum\limits_{(x,y)€W}g(I_xI_y)=\sum\limits_{(x,y)€W}I_xI_y*w(x,y)\\ A=(x,y)Wg(Ix2)=(x,y)WIx2w(x,y)B=(x,y)Wg(Iy2)=(x,y)WIy2w(x,y)C=(x,y)Wg(IxIy)=(x,y)WIxIyw(x,y)
    其中 M = [ A C C B ] = ∑ ( x , y ) € W w ( x , y ) [ I x 2 I x I y I x I y I y 2 ] M=\begin{bmatrix} A & C \\C & B\end{bmatrix}=\sum\limits_{(x,y)€W}w(x,y)\begin{bmatrix} I_x^2 & I_xIy \\I_xI_y & I_y^2\end{bmatrix} M=[ACCB]=(x,y)Ww(x,y)[Ix2IxIyIxIyIy2] w ( x , y ) w(x,y) w(x,y)表示高斯滑动窗口。

  4. 计算每个像素点 ( x , y ) (x,y) (x,y)处的Harris响应值 R R R
    R = d e t ( M ) − k ( t r a c e ( M ) ) 2 d e t ( M ) = λ 1 λ 2 t r a c e ( M ) = λ 1 + λ 2 R=det(M)-k(trace(M))^2 \\ det(M)=λ_1λ_2 \\ trace(M)=λ_1+λ_2 R=det(M)k(trace(M))2det(M)=λ1λ2trace(M)=λ1+λ2
    这里 λ 1 , λ 2 λ_1,λ_2 λ1,λ2是矩阵 M M M的2个特征值, k k k是一个指定值,这是一个经验参数,需要实验确定它的合适大小,通常它的值在0.04和0.06之间,它的存在只是调节函数的形状而已。

    R R R取决于 M M M的特征值,对于角点 ∣ R ∣ |R| R很大,平坦的区域 ∣ R ∣ |R| R很小,边缘的 R R R为负值。

  5. 滤除小于阈值 t t t R R R值:
    R = { R : d e t ( M ) − k ( t r a c e ( M ) ) 2 > t } R=\{R:det(M)-k(trace(M))^2 \gt t \} R={R:det(M)k(trace(M))2>t}
    如果在3×3或者5×5的邻域进行非最大值抑制,则局部最大值点即为图像中的角点。

代码实现

Mat dst = Mat::zeros(src.size(), CV_32FC1);  // 输出图像(存储角点响应R)
int block_size = 2;		// 窗口大小
int aperture_size = 3;	// 计算梯度时Sobel算子的核尺寸
double k = 0.04;		// 角点响应计算中的k值,增大k的值会减少被检测角点的数量

cornerHarris(src_gray, dst, block_size, aperture_size, k);

Mat dst_norm, dst_norm_scaled;
// 将角点响应归一化到[0, 255]
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
// 转换为单通道CV_8UC1
converScaleAbs(dst_norm, dst_norm_scaled);

// 绘制满足阈值条件的角点
int thresh = 200;
for (int i = 0; i < dst_norm.rows; i++) {
    for (int j = 0; j < dst_norm.cols; j++) {
        if ((int) dst_norm.at<float>(i,j) > thresh) {
            circle(dst_norm_scaled, Point(j, i), 5, Scalar(0), 2, 8, 0);
        }
    }
}
imshow("harris corner", dst_norm_scaled);

Shi-Tomasi角点(GFFT)

Shi-Tomasi角点检测原理与Harris角点检测相同,只是在最后判别式的选取上不同,Shi-Tomasi角点检测选取特征中的最小的那个来判别。即角点响应变为
R = min ⁡ ( λ 1 , λ 2 ) R=\min(\lambda_1,\lambda_2) R=min(λ1,λ2)

vector corners;  	  // 角点向量,每个元素为一个角点
int max_corners = 23;  	  	  // 检测的角点数目最大值,若有更多的角点,则返回响应值大的
double quality_level = 0.01;  // 可接受的最小响应值,若响应最大值为1500,quality_level=0.01,则小于15的响应都被拒绝
double min_distance = 10;				// 角点之间的最小欧式距离
int block_size = 3, gradient_size = 3;  // 计算M矩阵的窗口大小、梯度算子的核尺寸
bool use_harris_detector = false;		// 是否使用harris角点检测器
double k = 0.04;						// harris角点检测的k值
goodFeaturesToTrack(src_gray,
                   corners,
                   max_corners,
                   quality_level,
                   min_distance,
                   Mat(),  // ROI mask
                   block_size,
                   gradient_size,
                   use_harris_detector,
                   k);
// 绘制角点
for (size_t i = 0; i < corners.size(); i++) {
    circle(src, corners[i], 3, Scalar(0,0,255), FILLED);
}
imshow("Image", src);

图像处理基础+OpenCV实践_第6张图片

SIFT特征

SIFT具有旋转、尺度、亮度不变性。

基本思想

尺度不变特征变换(Scale-Invariant Feature Transform),使用DoG(差分高斯)来提取特征点,DoG是用不同的尺度空间因子(高斯分布的标准差 σ \sigma σ)对图像进行平滑,然后比较平滑后图像的区别,差别大的像素就可能是特征点。对得到的所有特征点,剔除一些不好的,SIFT算子会把剩下的每个特征点用一个128维的向量进行描述。

算法步骤

  1. DoG尺度空间的极值检测:
    首先构造DoG尺度空间,在SIFT中使用不同参数的高斯模糊来表示不同的尺度空间。而构造尺度空间是为了检测在不同尺度下都存在的特征点,特征点的检测使用DoG(利用差分代替微分)来近似计算LoG(图像的二阶导数)。

  2. 删除不稳定的极值点:

    主要删除两类:低对比度的极值点和不稳定的边缘响应点。

  3. 确定特征点的主方向:

    以特征点的坐标为中心、以 3 × 1.5 σ 3\times1.5\sigma 3×1.5σ为半径的邻域内计算各个像素点的梯度的幅值和幅角,然后使用直方图对梯度的辐角进行统计。直方图的横轴为梯度的方向,纵轴为梯度方向对应梯度幅值的累和,直方图中的最高峰所对应的方向即为特征点的主方向。

  4. 生成特征点的描述子

    首先将坐标轴旋转为特征点的方向,以特征点为中心的 16 × 16 16\times16 16×16窗口的像素的梯度幅值和方向,将窗口内的像素分成 4 × 4 4\times4 4×4的块,每块是其内部16个像素的8个方向的直方图统计,对于一个特征点,可形成 4 × 4 × 8 = 128 4\times4\times8=128 4×4×8=128维的特征向量。
    图像处理基础+OpenCV实践_第7张图片

代码实现

int numFeatures = 400;  // 提取的特征点数目
Ptr detector = SIFT::create(numFeatures);

// step 1 detect the keypoints
vector keypoints;
detector->detect(src_gray, keypoints);

// draw keypoints
Mat img_keypoints;
drawKeypoints(src, keypoints, img_keypoints);

// show detected keypoints
imshow("SIFT", img_keypoints);
waitKey(0);

// detect the keypoints and descriptors
vector kp1, kp2;
Mat desc1, desc2;
detector->detectAndCompute(img1, Mat(), kp1, desc1);
detector->detectAndCompute(img2, Mat(), kp2, desc2);

// match,使用flann加速匹配
vector> nn_matches;
Ptr matcher = DescriptorMatcher::create(DescriptorMatcher::FLANNBASED);
matcher->knnMatch(desc1, desc2, nn_matches, 2);

// 选择好的匹配
const float match_ratio = 0.8f;
vector good_matches;
for (size_t i = 0; i < nn_matches.size(); i++) {
    if (nn_matches[i][0].distance < match_ratio * nn_matches[i][1].distance) {
        good_matches.push_back(nn_matches[i][0]);
    }
}

ORB特征

基本原理

ORB算法将FAST特征点的检测方法和BRIEF特征描述子结合起来,并进行改进和优化。

算法步骤

  1. 利用FAST检测特征点,然后利用Harris角点的度量方法,从FAST特征点中挑选除Harris角点响应值最大的N个特征点。其中响应函数为
    R = d e t ( M ) − k ( t r a c e ( M ) ) 2 R=det(M)-k(trace(M))^2 R=det(M)k(trace(M))2
    ORB中用NMS来筛选关键点,只保留分数最大的前n个关键点。由于FAST关键点不具备尺度不变性,因此ORB采用了图像金字塔来实现尺度不变性,通过构建高斯金字塔,然后在每一层金字塔图像上检测角点。

  2. 利用灰度质心法实现BRIEF描述子的旋转不变性。
    灰度质心法认为角点的灰度和质心之间存在一个偏移,这个向量可以用于表示方向。对于任意特征点p,p的领域像素的矩定义为:
    m i j = ∑ x = − r r ∑ y = − r r x i y j I ( x , y ) m_{ij}=\sum\limits_{x=-r}^{r}\sum\limits_{y=-r}^{r}x^iy^jI(x,y) mij=x=rry=rrxiyjI(x,y)
    其中 I ( x , y ) I(x,y) I(x,y)为点 ( x , y ) (x,y) (x,y)处的灰度值, q q q为质心, i , j = 0 , 1 i,j=0,1 i,j=0,1。那么我们可以得到图像的质心为:
    C = ( m 10 m 00 , m 01 m 00 ) C=(\frac{m_{10}}{m_{00}},\frac{m_{01}}{m_{00}}) C=(m00m10,m00m01)
    那么特征点与质心的夹角定义为FAST特征点的方向:
    θ = a r c t a n ( m 01 , m 10 ) \theta=arctan(m_{01},m_{10}) θ=arctan(m01,m10)
    BRIEF描述子是通过比较中心像素点p的任意邻域内的256对像素间的差值来生成二进制描述子,其结果是一个长度为256的二值码串,由特征点邻域内的256个点对组成,这 2 × 256 2\times256 2×256个点 ( x i , y i ) , i = 1 , 2 , . . . . . , 2 n (x_i,y_i),i=1,2,.....,2n (xi,yi),i=1,2,.....,2n组成一个矩阵 S S S
    S = [ x 1 x 2 . . . x 2 n y 1 y 2 . . . y 2 n ] S=\begin{bmatrix} x_1 & x_2 & ... &x_{2n} \\ y_1 & y_2 & ... & y_{2n} \end{bmatrix} S=[x1y1x2y2......x2ny2n]
    ORB使用邻域方向 θ \theta θ和对应的旋转矩阵 R θ R_{\theta} Rθ,构建 S S S的校正版本 S θ S_{\theta} Sθ:
    S θ = R θ S S_{\theta}=R_{\theta}S Sθ=RθS
    其中,
    R θ = [ c o s θ s i n θ − s i n θ c o s θ ] R_{\theta}=\begin{bmatrix} cos\theta & sin\theta \\ -sin\theta & cos\theta \end{bmatrix} Rθ=[cosθsinθsinθcosθ]
    θ \theta θ就是上面求得的FAST特征点的方向。

    对于任意一对像素点 x i , y i x_i,y_i xi,yi,若 x i < y i x_ixi<yi,则该位置的描述子为1,否则为0。

  3. rBRIEF解决描述子的区分性。

代码实现

Ptr<ORB> detector = ORB::create();
vector<KeyPoint> keypoints1, keypoints2;
Mat descriptors1, descriptors2;
// step 1: Detect the keypoints using ORB detector, compute the descriptors
detector->detectAndCompute(img_gray1, Mat(), keypoints1, descriptors1);  // 第二个参数为ROI mask
detector->detectAndCompute(img_gray2, Mat(), keypoints2, descriptors2);

// step 2: Matching descriptor vectors with a brute force matcher
// 由于ORB的描述子是uchar类型的,因此只能用BF匹配法
// Ptr matcher = DescriptorMatcher::create("BruteForce-Hamming");
Ptr<DescriptorMatcher> matcher = 
    DescriptorMatcher::create(DescriptorMatcher::BRUTEFORCE_HAMMING);
// 单一匹配
// vector matches;
// matcher->match(descriptors1, decriptors2, matches);
// knn匹配,knn_matches[i]包括k对候选匹配
vector<vector<DMatch>> knn_matches;  
matcher->knnMatch(descriptors1, decriptors2, knn_matches, 2);  // 最后一个参数为k近邻的k值

// step 3: Filter matches using the Lowe's ratio test
const float ratio_thresh = 0.7f;
vector<DMatch> good_matches;
for (size_t i = 0; i < knn_matches.size(); i++) {
    // 若[i]中最好的匹配[i][0]小于第二好的匹配[i][1]的距离的0.7,则认为是好的匹配对
    if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance) {
        good_matches.push_back(knn_matches[i][0]);
    }
}

// step 4: Draw matches
Mat img_matches;
drawMatches(img1, keypoints1, img2, keypoints2, good_matches, img_matches, 
            // 其他默认参数
            Scalar::all(-1),  // 匹配的颜色
            Scalar::all(-1),  // 关键点的颜色
            vector<char>(),   // 匹配掩码
            DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
imshow("Good matches", img_matches);
waitKey(0);

光流Optical flow

光流是空间运动物体在观察成像平面上得像素运动的瞬时速度,是利用图像序列中像素在时间域上的变化和相邻帧之间的相关性来找到上一帧和当前帧之间的对应关系,从而计算出相邻帧之间的运动信息的一种方法。

可以通过不同目标的光流运动速度判断它们离我们的距离。一些较远的目标,如云,运动得很慢;而一些较近的目标,如门、运动得较快。

光流约束方程

光流基于如下两个假设:

  1. 在很短的时间内目标的像素值不会变化;
  2. 邻近的像素具有相似的运动。

I ( x , y , t ) I(x,y,t) I(x,y,t)表示t时刻的像素点 ( x , y ) (x,y) (x,y)的灰度值,则根据亮度恒定假设,我们有:
I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t ) I(x,y,t) = I(x + \Delta x, y + \Delta y, t + \Delta t) I(x,y,t)=I(x+Δx,y+Δy,t+Δt)
我们对上式右侧进行一阶Taylor展开,可得:
I ( x + Δ x , y + Δ y , t + Δ t ) ≈ I ( x , y , t ) + ∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t I(x + \Delta x, y + \Delta y, t + \Delta t) \approx I(x,y,t) + \frac{\partial I}{\partial x}\Delta x + \frac{\partial I}{\partial y}\Delta y + \frac{\partial I}{\partial t}\Delta t I(x+Δx,y+Δy,t+Δt)I(x,y,t)+xIΔx+yIΔy+tIΔt
最后得到光流约束方程:
∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t = 0 \frac{\partial I}{\partial x}\Delta x + \frac{\partial I}{\partial y}\Delta y + \frac{\partial I}{\partial t}\Delta t = 0 xIΔx+yIΔy+tIΔt=0
由于这个方程有两个未知数 ( Δ x , Δ y ) (\Delta x,\Delta y) (Δx,Δy),所以没有唯一解。为了得到唯一解,就必须新增约束或假设,因此也就有了不同的算法。

Lucas-Kanade算法

Lucas-Kanade算法假设局部光流恒定,他使用一个3x3的patch,patch内的9个像素都具有同样的运动。因此,问题变为了:9个方程,求解2个未知数。算法使用最小二乘法(GN)迭代求解:
[ Δ x Δ y ] = [ ∑ i f x i 2 ∑ i f x i f y i ∑ i f x i f y i ∑ i f y i 2 ] − 1 [ − ∑ i f x i f t i − ∑ i f y i f t i ] = [ J J T ] − 1 [ − J r ] \begin{bmatrix} \Delta x \\ \Delta y \end{bmatrix} = \begin{bmatrix} \sum_{i}{f_{x_i}}^2 & \sum_{i}{f_{x_i} f_{y_i} } \\ \sum_{i}{f_{x_i} f_{y_i}} & \sum_{i}{f_{y_i}}^2 \end{bmatrix}^{-1} \begin{bmatrix} - \sum_{i}{f_{x_i} f_{t_i}} \\ - \sum_{i}{f_{y_i} f_{t_i}} \end{bmatrix} = [JJ^T]^{-1}[-Jr] [ΔxΔy]=[ifxi2ifxifyiifxifyiifyi2]1[ifxiftiifyifti]=[JJT]1[Jr]
其中
f x = ∂ I ∂ x = I 2 ( x + Δ x + 1 , y + Δ y ) − I 2 ( x + Δ x − 1 , y + Δ y ) 2 f y = ∂ I ∂ y = I 2 ( x + Δ x , y + Δ y + 1 ) − I 2 ( x + Δ x , y + Δ y − 1 ) 2 J = [ f x , f y ] T , r = f t = 预 测 值 − 真 实 值 = I 2 ( x + Δ x , y + Δ y ) − I 1 ( x , y ) f_x = \frac{\partial I}{\partial x}=\frac{I_2(x+\Delta x+1,y+\Delta y)-I_2(x+\Delta x-1,y+\Delta y)}{2} \\ f_y = \frac{\partial I}{\partial y}=\frac{I_2(x+\Delta x,y+\Delta y+1)-I_2(x+\Delta x,y+\Delta y-1)}{2}\\ J = [f_x,f_y]^T , \quad r=f_t=预测值-真实值=I_2(x+\Delta x,y+\Delta y)-I_1(x,y) fx=xI=2I2(x+Δx+1,y+Δy)I2(x+Δx1,y+Δy)fy=yI=2I2(x+Δx,y+Δy+1)I2(x+Δx,y+Δy1)J=[fx,fy]T,r=ft==I2(x+Δx,y+Δy)I1(x,y)
注意,式中的逆矩阵和Harris角点检测中的M很像,这表明角点更适合跟踪。

为了解决较大运动时无法跟踪的问题,算法使用图像金字塔,当图像进行上采样时,大运动变成了小运动,小运动被去除了。

代码实现

vector<Point2f> prevPts, currPts;
// 输入图像、输出角点、角点数目最大值、角点质量、角点之间的最小欧式距离、ROI区域、计算互相关函数的窗口尺寸、是否使用Harris角点、Harris角点的k值
goodFeaturesToTrack(prevImg, prevPts, 100, 0.3, 7, Mat(), 7, false, 0.04);
vector<uchar> status;
vector<float> err;
TermCriteria criteria = TermCriteria((TermCriteria::COUNT) + (TermCriteria::EPS), 10, 0.03);

// 前一帧图像、后一帧图像、前一帧跟踪点、后一帧跟踪点、是否找到光流(是否跟踪成功)、误差、金字塔的窗口尺寸、金字塔的层数-1、迭代终止条件
calcOpticalFlowPyrLK(prevImg, currImg, prevPts, currPts, status, err, Size(15,15), 2, criteria);

// 选择好点
vector<Point2f> good_new;
for (uint i = 0; i < prevPts.size(); i++) {
    if (status[i] == 1) {
        good_new.push_back(currPts[i]);
        // draw the tracks
        line(prevImg, currPts[i], prevPts[i], color, thickness);
    }
}

图像插值

最近邻插值

图像处理基础+OpenCV实践_第8张图片
在实际应用中,直接先逐行复制,再逐列复制。

双线性插值

已知 Q 11 ( x 1 , y 1 ) 、 Q 12 ( x 1 , y 2 ) 、 Q 21 ( x 2 , y 1 ) 、 Q 22 ( x 2 , y 2 ) Q11(x1,y1)、Q12(x1,y2)、Q21(x2,y1)、Q22(x2,y2) Q11(x1,y1)Q12(x1,y2)Q21(x2,y1)Q22(x2,y2),求其中点P(x,y)的值。
图像处理基础+OpenCV实践_第9张图片

双线性插值是分别在两个方向计算了共3次单线性插值,如图所示,先在x方向求2次单线性插值,获得R1(x, y1)、R2(x, y2)两个临时点,再在y方向计算1次单线性插值得出P(x, y)(实际上调换2次轴的方向先y后x也是一样的结果)。

  1. 几何中心点重合对应
    s r c x = ( d s t x + 0.5 ) ∗ s r c w / d s t w − 0.5 s r c y = ( d s t y + 0.5 ) ∗ s r c h / d s t h − 0.5 src_x=(dst_x+0.5)*src_w/dst_w-0.5 \\src_y=(dst_y+0.5)*src_h/dst_h-0.5 srcx=(dstx+0.5)srcw/dstw0.5srcy=(dsty+0.5)srch/dsth0.5
    对于计算出来得 s r c x , s r c y src_x, src_y srcx,srcy,由于一般是浮点数,需要找到其邻近的四个实际存在的像素点。
    比如 s r c = ( 1.2 , 3.4 ) src = (1.2, 3.4) src=(1.2,3.4),先找到邻近像素(1, 3) (2, 3) (1, 4) (2, 4),

  2. x方向单线性插值
    f ( R 1 ) = x 2 − x x 2 − x 1 f ( Q 11 ) + x − x 1 x 2 − x 1 f ( Q 21 ) f ( R 2 ) = x 2 − x x 2 − x 1 f ( Q 12 ) + x − x 1 x 2 − x 1 f ( Q 22 ) f(R_1)=\frac{x_2-x}{x_2-x_1}f(Q_{11})+\frac{x-x_1}{x_2-x_1}f(Q_{21}) \\ f(R_2)=\frac{x_2-x}{x_2-x_1}f(Q_{12})+\frac{x-x_1}{x_2-x_1}f(Q_{22}) f(R1)=x2x1x2xf(Q11)+x2x1xx1f(Q21)f(R2)=x2x1x2xf(Q12)+x2x1xx1f(Q22)

  3. y方向单线性插值

f ( P ) = y 2 − y y 2 − y 1 f ( R 1 ) + y − y 1 y 2 − y 1 f ( R 2 ) f(P) =\frac{y_2-y}{y_2-y_1}f(R_1)+\frac{y-y_1}{y_2-y_1}f(R_2) f(P)=y2y1y2yf(R1)+y2y1yy1f(R2)

  1. 将上式整理得
    f ( x , y ) = ( x 2 − x ) ( y 2 − y ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 11 ) + ( x − x 1 ) ( y 2 − y ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 21 ) + ( x 2 − x ) ( y − y 1 ) ( x 2 − x 1 ) ( y 2 − y 1 ) f ( Q 12 ) f(x,y) =\frac{(x_2-x)(y_2-y)}{(x_2-x_1)(y_2-y_1)}f(Q_{11})+\frac{(x-x_1)(y_2-y)}{(x_2-x_1)(y_2-y_1)}f(Q_{21})+\frac{(x_2-x)(y-y_1)}{(x_2-x_1)(y_2-y_1)}f(Q_{12}) f(x,y)=(x2x1)(y2y1)(x2x)(y2y)f(Q11)+(x2x1)(y2y1)(xx1)(y2y)f(Q21)+(x2x1)(y2y1)(x2x)(yy1)f(Q12)

    由于 x 2 = x 1 + 1 , y 2 = y 1 + 1 x_2=x_1+1, y_2=y_1+1 x2=x1+1,y2=y1+1,因此有
    f ( x , y ) = f ( Q 11 ) ( x 2 − x ) ( y 2 − y ) + f ( Q 21 ) ( x − x 1 ) ( y 2 − y ) + f ( Q 12 ) ( x 2 − x ) ( y − y 1 ) + f ( Q 22 ) ( x − x 1 ) ( y − y 1 ) \begin{aligned}f(x,y) = &f(Q_{11})(x_2-x)(y_2-y)+f(Q_{21})(x-x_1)(y_2-y)+ \\&f(Q_{12})(x_2-x)(y-y_1)+f(Q_{22})(x-x_1)(y-y_1) \end{aligned} f(x,y)=f(Q11)(x2x)(y2y)+f(Q21)(xx1)(y2y)+f(Q12)(x2x)(yy1)+f(Q22)(xx1)(yy1)

相机标定

成像

对于一个3D点 P ( X , Y , Z ) P(X,Y,Z) P(X,Y,Z),其对应相机图像坐标为 p ( u , v ) p(u,v) p(u,v),可以建立如下投影关系:
s [ u v 1 ] = [ a r u 0 0 b v 0 0 0 1 ] [ r 1 r 2 r 3 t ] [ X Y Z 1 ] = K T P s\left[ \begin{matrix} u \\ v \\ 1 \end{matrix} \right] = \left[ \begin{matrix} a & r & u_0 \\ 0 & b & v_0 \\ 0 & 0 & 1 \end{matrix} \right] \begin{bmatrix} \bold{r}_1 & \bold{r}_2 & \bold{r}_3 & \bold{t} \end{bmatrix} \left[ \begin{matrix} X \\ Y \\ Z \\ 1\end{matrix} \right] = \bold{K} \bold{T} \bold{P} suv1=a00rb0u0v01[r1r2r3t]XYZ1=KTP

其中K为相机内参矩阵,[R, t]为相机外参。此外还有相机畸变参数:
{ x d i s t o r t e d = x ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) + 2 p 1 x y + p 2 ( r 2 + 2 x 2 ) y d i s t o r t e d = y ( 1 + k 1 r 2 + k 2 r 4 + k 3 r 6 ) + p 1 ( r 2 + 2 y 2 ) + 2 p 2 x y \left \{ \begin{aligned}x_{distorted}=x(1+k_1r^2+k_2r_4+k_3r^6)+2p_1xy+p_2(r^2+2x^2) \\y_{distorted}=y(1+k_1r^2+k_2r_4+k_3r^6)+p_1(r^2+2y^2)+2p_2xy\end{aligned}\right. {xdistorted=x(1+k1r2+k2r4+k3r6)+2p1xy+p2(r2+2x2)ydistorted=y(1+k1r2+k2r4+k3r6)+p1(r2+2y2)+2p2xy
(x, y)为相机归一化平面坐标。相机标定的目的就是求解上述这些参数。

闭式解(不考虑畸变)

我们使用标定板来进行标定,其所有特征点都共面,可以假设该平面为Z=0,从而去掉 r 3 r_3 r3,于是投影关系可以写为:

s [ u v 1 ] = [ a r u 0 0 b v 0 0 0 1 ] [ r 1 r 2 t ] [ X Y 1 ] s\left[ \begin{matrix} u \\ v \\ 1 \end{matrix} \right] = \left[ \begin{matrix} a & r & u_0 \\ 0 & b & v_0 \\ 0 & 0 & 1 \end{matrix} \right] \begin{bmatrix} \bold{r}_1 & \bold{r}_2 & \bold{t} \end{bmatrix} \left[ \begin{matrix} X \\ Y \\ 1\end{matrix} \right] suv1=a00rb0u0v01[r1r2t]XY1

中间的这块 K [ r 1 , r 2 , t ] \bold{K}[\bold{r}_1 , \bold{r}_2 , \bold{t}] K[r1,r2,t]就代表3D点和其图像坐标间的单应H,因此有:

H = [ h 1 h 2 h 3 ] = λ K [ r 1 r 2 t ] \bold{H}= \begin{bmatrix} \bold{h}_1 & \bold{h}_2 & \bold{h}_3 \end{bmatrix} =\lambda \bold{K} \begin{bmatrix} \bold{r}_1 & \bold{r}_2 & \bold{t} \end{bmatrix} H=[h1h2h3]=λK[r1r2t]

λ \lambda λ为标量(可以理解为1/s),将矩阵里面的元素展开有
{ r 1 = K − 1 h 1 r 2 = K − 1 h 2 \left \{ \begin{aligned} \bold{r}_1=\bold{K}^{-1}\bold{h}_1 \\ \bold{r}_2=\bold{K}^{-1}\bold{h}_2 \end{aligned}\right. {r1=K1h1r2=K1h2
又由于旋转矩阵有正交性和单位性,因此我们有
{ r 1 T r 2 = 0 → h 1 T K − T K − 1 h 2 = 0 ∣ ∣ r 1 ∣ ∣ = ∣ ∣ r 2 ∣ ∣ → h 1 T K − T K − 1 h 1 = h 2 T K − T K − 1 h 2 \left \{ \begin{aligned} \bold{r}_1^T\bold{r}_2 &= 0 \rightarrow \bold{h}_1^T\bold{K}^{-T}\bold{K}^{-1} \bold{h}_2 = 0 \\ ||\bold{r}_1||&=||\bold{r}_2|| \rightarrow \bold{h}_1^T\bold{K}^{-T}\bold{K}^{-1} \bold{h}_1= \bold{h}_2^T\bold{K}^{-T}\bold{K}^{-1} \bold{h}_2 \end{aligned}\right. {r1Tr2r1=0h1TKTK1h2=0=r2h1TKTK1h1=h2TKTK1h2
我们令 B = K − T K − 1 \bold{B}=\bold{K}^{-T}\bold{K}^{-1} B=KTK1,则有
B = K − T K − 1 = [ B 11 B 12 B 13 B 21 B 22 B 23 B 31 B 32 B 33 ] = [ 1 a 2 − r a 2 b v 0 r − u 0 b a 2 b − r a 2 b r a 2 b + 1 b 2 − r ( v 0 r − u 0 b ) a 2 b 2 − v 0 b 2 v 0 r − u 0 b a 2 b − r ( v 0 r − u 0 b ) a 2 b 2 − v 0 b 2 ( v 0 r − u 0 b ) 2 a 2 b 2 + v 0 2 b 2 + 1 ] \bold{B}=\bold{K}^{-T}\bold{K}^{-1}= \begin{bmatrix} B_{11} & B_{12} &B_{13}\\ B_{21} & B_{22} & B_{23} \\ B_{31} & B_{32} & B_{33} \end{bmatrix} = \\ \begin{bmatrix} \frac{1}{a^2} & -\frac{r}{a^2b} & \frac{v_0r-u_0b}{a^2b} \\ -\frac{r}{a^2b} & \frac{r}{a^2b}+\frac{1}{b^2} & -\frac{r(v_0r-u_0b)}{a^2b^2}-\frac{v_0}{b^2} \\ \frac{v_0r-u_0b}{a^2b} & -\frac{r(v_0r-u_0b)}{a^2b^2}-\frac{v_0}{b^2} & \frac{(v_0r-u_0b)^2}{a^2b^2}+\frac{v_0^2}{b^2}+1 \end{bmatrix} B=KTK1=B11B21B31B12B22B32B13B23B33=a21a2bra2bv0ru0ba2bra2br+b21a2b2r(v0ru0b)b2v0a2bv0ru0ba2b2r(v0ru0b)b2v0a2b2(v0ru0b)2+b2v02+1
因为B是对称矩阵,因此不妨设向量 b \bold{b} b为:
b = [ B 11 , B 12 , B 22 , B 13 , B 23 , B 33 ] T \bold{b}=\begin{bmatrix} B_{11},B_{12},B_{22},B_{13},B_{23},B_{33} \end{bmatrix}^T b=[B11,B12,B22,B13,B23,B33]T
对于单应矩阵H的列向量 h i \bold{h}_i hi,有:
h i = [ h i 1 , h i 2 , h i 3 ] T \bold{h}_i=[h_{i1},h_{i2},h_{i3}]^T hi=[hi1,hi2,hi3]T
b b b h i h_i hi代入到旋转矩阵的两个约束中去,得到:

h i T B h j = m i j T b = 0 \bold{h}_i^T\bold{B}\bold{h}_j=\bold{m}_{ij}^T\bold{b}=0 hiTBhj=mijTb=0
其中, m i j = [ h i 1 h j 1 , h i 1 h j 2 + h i 2 h j 1 , h i 2 h j 2 , h i 3 h j 1 + h i 1 h j 3 , h i 3 h j 2 + h i 2 h j 3 , h i 3 h j 3 ] T \bold{m}_{ij}=[h_{i1}h_{j1},h_{i1}h_{j2}+h_{i2}h_{j1},h_{i2}h_{j2},h_{i3}h_{j1}+h_{i1}h_{j3},h_{i3}h_{j2}+h_{i2}h_{j3},h_{i3}h_{j3}]^T mij=[hi1hj1,hi1hj2+hi2hj1,hi2hj2,hi3hj1+hi1hj3,hi3hj2+hi2hj3,hi3hj3]T

最终,两个约束可以写为齐次方程:
[ m 12 T ( m 11 − m 22 ) T ] b = M 2 × 6 b = 0 \begin{bmatrix} \bold{m}_{12}^T \\ (\bold{m}_{11}-\bold{m}_{22})^T \end{bmatrix} \bold{b}=\bold{M}_{2\times6}\bold{b}= \bold{0} [m12T(m11m22)T]b=M2×6b=0
M是由单应H决定的已知量,因此只要求解上述方程即可得到b。由于一张图像就可以得到2个约束,因此求解该方程至少需要采集3张图像,实际中,通常会采集15-20张左右。

该方程可以使用SVD进行求解,即 M = U D V 6 × 6 T \bold{M}=\bold{U}\bold{D}\bold{V}_{6\times6}^T M=UDV6×6T,将V按D中特征值的降序排序,取V的最后一列的列向量即为所求b。

当求出b和矩阵B后,即可按(55)描述的 B = K − T K − 1 \bold{B}=\bold{K}^{-T}\bold{K}^{-1} B=KTK1求解内参K:
v 0 = ( B 12 B 13 − B 11 B 23 ) / ( B 11 B 22 − B 12 2 ) λ = B 33 − [ B 13 2 + v 0 ( B 12 B 13 − B 11 B 23 ) ] / B 11 a = λ / B 11 b = λ B 11 / ( B 11 B 22 − B 12 2 ) r = − B 12 a 2 b / λ u 0 = r v 0 / a − B 13 a 2 / λ \begin{aligned} v_0&=(B_{12}B_{13}-B_{11}B_{23})/(B_{11}B_{22}-B_{12}^2) \\ \lambda&=B_{33}-[B_{13}^2+v_0(B_{12}B_{13}-B_{11}B_{23})]/B_{11} \\ a&=\sqrt{\lambda/B_{11}} \\ b&=\sqrt{\lambda B_{11}/(B_{11}B_{22}-B_{12}^2)} \\ r&=-B_{12}a^2b/\lambda \\ u_0&=rv_0/a-B_{13}a^2/\lambda \end{aligned} v0λabru0=(B12B13B11B23)/(B11B22B122)=B33[B132+v0(B12B13B11B23)]/B11=λ/B11 =λB11/(B11B22B122) =B12a2b/λ=rv0/aB13a2/λ
外参可以由内参和单应求出:
r 1 = λ K − 1 h 1 r 2 = λ K − 1 h 2 r 1 = r 1 × r 2 t = λ K − 1 h 3 \begin{aligned} \bold{r}_1=\lambda \bold{K}^{-1}\bold{h}_1 \\ \bold{r}_2=\lambda \bold{K}^{-1}\bold{h}_2 \\ \bold{r}_1=\bold{r}_{1}\times\bold{r}_2 \\ \bold{t}=\lambda \bold{K}^{-1}\bold{h}_3 \end{aligned} r1=λK1h1r2=λK1h2r1=r1×r2t=λK1h3
其中, λ = 1 / ∣ ∣ K − 1 h 1 ∣ ∣ = 1 / ∣ ∣ K − 1 h 2 ∣ ∣ \lambda=1/||\bold{K}^{-1}\bold{h}_1||=1/||\bold{K}^{-1}\bold{h}_2|| λ=1/K1h1=1/K1h2,由于噪声存在,计算出来的旋转矩阵R并不一定满足单位正交的性质,因此可以用QR分解,或者将结果从矩阵空间重投影到SE(3)流形上:
R = ( R R T ) − 1 / 2 R \bold{R}=(\bold{R}\bold{R}^T)^{-1/2}\bold{R} R=(RRT)1/2R

优化解(考虑畸变)

之前的闭式解计算没有考虑畸变,因此我们还需要对闭式解进一步优化。我们定义考虑了畸变的重投影误差目标函数:
∑ i = 1 n ∑ j = 1 m ∣ ∣ p i j − p ^ ( K , k 1 , k 2 , k 3 , p 1 , p 2 , R i , t i , P j ) ∣ ∣ \sum_{i=1}^n\sum_{j=1}^m ||\bold{p}_{ij}-\bold{\hat{p}}(\bold{K},k_1,k_2,k_3,p_1,p_2,\bold{R_i},\bold{t_i},\bold{P}_j)|| i=1nj=1mpijp^(K,k1,k2,k3,p1,p2,Ri,ti,Pj)
其中,有n张图像,m个3D点。通过最小化该目标函数,即可求解相机的内参、畸变、外参。

我们设定内外参的初始值即为先前求得闭式解,畸变参数初始为0。采用LM算法进行优化,最后即可求得相机的参数,完成相机的标定。

你可能感兴趣的:(SLAM,opencv)