本博客为《OpenCV算法精解:基于Python与C++》一书(参阅源代码链接)的阅读笔记,根据理解对书中绝大多数算法做了总结和描述,对Numpy较为熟悉,Python方面仅对与C++不同的注意事项做了标注。书作者整体按照冈萨雷斯的经典教材《数字图像处理(第三版)》和OpenCV知识脉络组织内容,每个算法均用Python和C++两种语言实现。除官方函数外本书给出了多数算法函数的自定义版本便于读者理解,以及部分官方未包含的算法(利于导向滤波、Marr-Hildreth边缘检测等)
源代码下载地址 www.broadview.com.cn/32495
1. Mat(行, 列, 类型)
2. Mat(Size(列, 行), 类型)
3. 对已声明Mat对象 m.create 格式同上
4. Mat::ones, Mat::zeros
5. 直接初始化小型矩阵值 (Mat_(2,3) << 1,2,3), 把int和后面元素换成Vec2f等可以直接初始化双通道矩阵
6. Mat(vector)构造N行1列的双通道矩阵Mat
7. 可以用Mat的reshape方法变换矩阵形状和通道
rows, cols, size(), channels(), total(), dims, 转置t()等
1. 成员函数at(最慢,最可读): m.at(r,c), 可用Point对象: m.at(Point(c,r)) 注:Point和Size对象先列再行
2. 成员函数ptr可获取任意一行的行指针: const int * ptr = m.ptr(r) 获取行指针后访问列 ptr[c]
3. 成员函数isContinuous可获取矩阵每一行之间存储是否连续, 如果连续,则可:
int * ptr = m.ptr(0) //获取矩阵首地址
ptr[r*m.rows+c] 访问(r,c)
4. 成员函数step和data
step[0]获取矩阵每一行所占字节数, step[1]获取每一数值所占字节数, data为指向第一个数值的指针, 类型为uchar
*((int*) (m.data + r*m.step[0] + c*m.step[1])) //访问int类型矩阵的(r,c)
注: 矩阵的同一行和一维数组一样存储必连续, 不同行可能连续可能不连续,对于连续的可用方法三访问
step[0]不仅可以得出连续矩阵每一行所占字节数, 若行间隔, 间隔字节数也被计入了step[0]内(若有间隔则间隔也是固定的)
创建: Vec
成员变量类似Mat, 有cols, rols等
用(), [] 访问
官方别名 typedef Vec Vec3b,
typedef Vec Vec2i,
typedef Vec Vec4f,
typedef Vec Vec3d 等等
单通道Mat每一元素为一个数值, 多通道矩阵每一元素为一个Vec
1. 元素为Vec, 同样连续存储
2. 访问方式同单通道的4种方式, 例如三通道对应的typename变为Vec3f, Vec3b等等
例如第四种访问方式:
Vec3f* ptr = (Vec3f*)(mm.data + r*mm.step[0] + c*mm.step[1]);
之后取ptr值: *ptr
3. 通道分离
使用函数split(mat, vector)
先创建一个元素为Mat的vector, 调用split函数把原多通道Mat分离为多个单通道Mat存到vector中
4. 通道合并
函数原型一: void merge(const Mat * mv, size_t count, OutputArray dst)
多个单通道矩阵初始化一个矩阵 Mat plane[] = {plane0, plane1, plane2}, 声明一个目标矩阵mat
merge(plane, 3, mat)
函数原型二: void merge(InputArrayOfArrays mv, OutputArray dst)
将单通道矩阵放入mat的vector中, merge(plane, mat)
1. 某一行: m.row(r), 某一列: m.col(c)
2. 连续行: m.rolRange(start, end), m.rolRange(Range(start, end))
连续列: m.colRange(start, end), m.colRange(Range(start, end)) 都是左闭右开区间
3. clone和copyTo
clone: 2的方法仍然指向原矩阵, 末尾加.clone()创建并指向副本
copyTo同理, matrix.rolRange(start, end).copyTo(r_range)
4. Rect类
Rect(int_x, int_y, int_width, int_height);
Rect(int_x, int_y, Size(int_width, int_height)), Rect(Point2i&pt1, Size(int_width, int_height)) //即坐标可以int x,y也可以Point, 宽高可以int width,height也可以Size
Rect(Point2i&pt1, Point2i&pt2) //左上角右下角 注:实测不包括右下角,为左上角到右下角左上一个点的矩形roi,即也是左闭右开
1. 加法: 法一: OpenCV重载了+号, 适用两种情况, 第一是矩阵+矩阵, 要求同size同类型; 第二是矩阵加数值, 不要求同类型, 会把数值类型转换后加到矩阵每一个元素上
注: 不会发生溢出情况, 会截断为类型最大值
Python则不同, 缺点ndarray的加法不会截断, 而是溢出, 这点与C++接口区别
优点ndarray有强大的广播能力, 也适用于不同类型相加(取范围大的类型)
同时cv2.add函数接口同大部分OpenCV的Python函数一样, 不再在参数中有dst, 而是将dst返回
法二: add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask=noArray(), int dtype=-1)
使用add而不是+时候矩阵类型可以不同, 输出类型可以由dtype参数自行指定, 默认参数-1仅适用于同类型时
2. 减法: 与加法同理, 用-或者subtract函数, 注意事项相同, C++截断Python溢出
3. 点乘: 法一: Mat成员函数mul, dst = src1.mul(src2)
法二: multiply(InputArray src1, InputArray src2, OutputArray dst, double scale=1, int dtype=-1)
scale为点乘结果基础上乘的系数, dtype同上指定类型, -1同类型
注: Python的ndarray重载了*表示点乘, 用法同+, -, C++的OpenCV也重载了*但是表示矩阵乘法
4. 点除: OpenCV重载了/, 同时有函数divide, 用法同上
注:Python的ndarray在uint8类型时除以0得0, 其他情况返回inf
5. 矩阵乘法
法一: 重载了*, 要求符合矩阵乘法要求, 类型一致, *仅支持同为float或double, 两个双通道矩阵也可相乘, 两通道会被分别当成实部和虚部
法二: gemm(InputArray src1, InputArray src2, double alpha, InputArray src3, double beta, OutputArray dst, int flags=0)
alpha为相乘后的系数, beta为src3系数, flags控制了三个矩阵是否转置
flags=0: alpha*src1*src2 + beta*src3
flags=GEMM_1_T: alpha*src1的转置*src2 + beta*src3
flags=GEMM_2_T: alpha*src1*src2的转置 + beta*src3
flags=GEMM_3_T: alpha*src1*src2 + beta*src3的转置
不需要src3时候可以用NULL, beta设为0, 即*等价于gemm(src1, src2, 1, NULL, 0, dst, 0)
Python: np.dot(src1, src2)结果返回
6. 其他运算:
指数函数exp, 对数函数log(以e为底)函数, 仅支持float和double
Python的ndarray的exp和log支持任意数值类型,返回各种长度的float
幂指数函数pow(Python为power), 开平方运算sqrt
注意截断的问题
1. 图像读取imread函数, 第一个参数为文件路径字符串, 第二个参数flags:
0: IMREAD_GRAYSCALE 灰度图(若是读取的为彩色图会自动灰度化), 1: IMREAD_COLOR BGR图, 默认1, 更多可选参数参考imgcodecs.hpp
2. 显示imshow函数, 第一个参数字符串的窗口名, 第二个要显示的Mat对象
namedWindow(窗口名, 模式) 可预先创建窗口, imshow同时创建的窗口为不可调的
模式0: WINDOW_NORMAL大小可调
1: WINDOW_AUTOSIZE(默认)按图像大小, 不可调
imshow图像需要配上waitKey(等待毫秒数)才能被观察, 最少1, 0为等待任意键按下
destroyAllWindows()关闭所有窗口
OpenCV的彩色转灰度公式 gray = 0.114B + 0.587G + 0.299R
仿射变换 = 线性变换 + 平移
非齐次形式 x' = Ax + b
齐次形式 x' = Ax A(3x3矩阵)左上角为一个二维的线性变换, 最右列上两个元素为平移量, 最下行为(0, 0, 1)
基本仿射变换包括: 平移, 缩放, 旋转
1.平移: 单位阵基础上带两个平移量, 方向同正负
2.放缩: 以原点为中心缩放, 平移量为0, 主对角前两个量分别表x, y轴的放缩, 绝对值大于1放大, 小于1缩小
以任一点(x0, y0)为中心缩放相当于先将(x0, y0)平移到原点, 再按原点缩放, 再向相反方向平移第一步相同的量, 即三个平移, 原点放缩, 平移矩阵相乘得到变换矩阵
公式(x', y') = (x0 + Sx(x-x0), y0 + Sy(y-y0))
3.旋转: 以原点为中心的二维齐次旋转阵仅有左上角四个量, 右下角为1, 其余为0; 基本旋转阵是对称阵, 其逆矩阵就是他的转置
同放缩变换, 以任一点(x0, y0)为中心旋转相当于先将(x0, y0)平移到原点, 再按原点旋转, 再向相反方向平移第一步相同的量, 即三个平移, 原点旋转, 平移矩阵相乘得到变换矩阵
以上可知由基本变换: 平移, 原点放缩, 原点旋转这三个基本矩阵相乘可以等效得到所有仿射变换矩阵, 且这三个基本矩阵的逆易求得, 基本逆矩阵逆序相乘即可得对应仿射变换的逆矩阵
计算仿射变换:
1.方程法: 任意仿射变换矩阵含有前两行共6个未知数, 故只需要最少三对已知变换前后坐标的点构造6个方程组即可求得
函数: getAffineTransform(src, dst)
Python和C++接口都是返回的形式
src, dst为两个三行两列的对应变换前后点坐标, 返回一个两行三列的矩阵, 为齐次仿射变换矩阵的前两行, 返回类型为CV_64F
Python直接用两个三行两列ndarray输入
C++有两种形式, 第一为输入两个Point2f数组 Point2f src[] = {三个Point2f点}
第二为输入两个三行两列的Mat对象, 类型必须是float(CV_32F)
2.矩阵法:若是知道仿射变换每一步的过程, 则对于正变换可以直接矩阵相乘(先右后左)得到完整仿射变换矩阵
对于逆变换则如上所述, 正变换的基本逆矩阵逆序相乘即可得对应仿射变换的逆矩阵
函数: getRotationMatrix2D(center, angle, scale) 可返回以任一点为中心的旋转等比例放缩矩阵(等比例放缩时先旋转还是先放缩结果相同)
angle单位为角度不是弧度, 返回值同样是一个两行三列的矩阵, 返回类型为CV_64F
4.图像中的仿射变换
插值: 以上为空间坐标的仿射变换, 若要对图像进行仿射变换, 则应用现有变换矩阵由上述计算出变换后坐标, 再用插值算法得到变换后图像
变换矩阵给了我们图像函数fI到fO的变换关系, 我们的目标是要填满fO的整个定义域, 但是对于部分fO值映射后就成了空值(fI定义域外, 例如小数), 我们反过来考虑, 由逆矩阵得到从fO到fI的映射关系, 在fO的整个定义域遍历依次找到对应位置的fI值插入, 若是fI没有定义, 则根据特定算法求出估计fI值再插入fO, 这便是一个完整插值算法要做的事情
反过来考虑的好处是我们按照fO的定义域来找, 对于fI上无定义的映射点自然不会有冗余计算, 同时也覆盖了整个fO定义域
最近邻插值: 最近的一个值作为(x, y)的估计值
优点: 简单快速; 缺点: 边缘处锯齿状外观
双线性插值: 任然考虑四个最近点, 分别固定[y], [y]+1, 进行水平方向插值得到f(x, [y]), f(x, [y]+1]), 再进行竖直方向插值, 求得f(x, y)
其中f(x, [y]) = a*f([x]+1, [y]) + (1-a)*f([x], [y]), a = |x-[x]|
f(x, [y]+1) = a*f([x]+1, [y]+1) + (1-a)*f([x], [y]+1), a = |x-[x]|
f(x, y) = b*f(x, [y]) + (1-b)*f(x, [y]+1), b = |y-[y]|
双线性插值是一种二界函数, 有效处理锯齿状外观, 速度也较快, 是很多函数和工具的默认插值法
更高阶插值能达到更好效果, 但速度也会降低, 例如三次样条插值, Leagendre中心函数和sin(axs)函数, 高阶函数常用二维离散卷积实现
函数: warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue ]]]]) 可实现图像的仿射变换
src: 图像; M: 2行3列的仿射变换矩阵(通常是getAffineTransform, getRotationMatrix2D的结果);
dsize: 二元元组(宽, 高); flags: 插值法INTE_NEAREST, INTE_LINEAR(默认)等; boderMode: 填充模式BORDER_CONSTANT等, borderValue: 当boderMode是BORDER_CONSTANT时的填充值
函数: resize(InputArray src, OutputArray dst, Size dsize, double fx=o, double fy=0, int interpolation=INTE_LINEAR)
fx: 水平方向缩放比例, 默认0; fy: 垂直方向缩放比例, 默认0; interpolation: 插值法, 同上
注意一个问题, dsize优先, 若dszie设定了大小, fx, fy不再起作用; 若dsize为Size(), fx, fy起作用
函数: rotate(InputArray src, OutputArray dst, int rotateCode)
用于顺时针旋转90, 180, 270度
ROTATE_90_CLOCKWISE, ROTATE_180, ROTATE_90_COUNTERCLOCKWISE
仿射变换仅涉及二维空间, 若在三维空间发生了旋转, 则为投影变换, 具体原理图多较长参阅《计算机视觉: 一种现代方法》第一章
投影变换涉及三维, 有距离z, 非齐次形式的投影矩阵也为3x3
函数: getPerspectiveTranstorm(src, dst) 返回CV_64F投影矩阵
两个重载函数, 一接收两个四个点的Point2f数组, 二接收两个Mat_(4, 2)
函数: warpPerspective(原图, 结果, 3x3投影矩阵, 结果大小) 返回投影图像
函数: 画圆/圆圈circle(InputOutputArray img, Point center, int radius, const Scalar& color, int thickness = 1, int lineType = LINE_8, int shift = 0)
画矩形rectangle, 椭圆ellipse, 线段line
注: 定义鼠标事件: 定义回调函数, 参数列表(int event, int x, int y, int flags, void *param), event为事件, xy为鼠标事件的坐标, flags自定义标志, param为窗口内的图像等, 写好需要的每个event(CV_EVNET_......)对应的操作
创建窗口, setMouseCallback绑定回调函数, 循环show出来不断刷新, 程序结束后解绑回调函数
可用于矫正图像中的圆形物体或被包含在圆环中的物体
1.笛卡尔转极坐标
笛卡尔坐标中同圆的点在极坐标(Θ, r)图中在同一直线上(以圆心作为极点)
原点(x0,y0) r=||(x-x0, y-y0)||, Θ = 2π + aractan2(y-y0, x-x0), y-y0<=0
= aractan2(y-y0, x-x0), y-y0>0
Python用numpy的sqrt和atan2函数易实现, 也可用下列OpenCV函数
C++OpenCV函数: cartToPolar(x, y[, magnitude[, angle[, angleInDegree]]])
x, y, magnitude, angle相同尺寸Mat, 只可为float32或float64, angleInDegree是true则为角度, false为弧度
注: 该函数相当于以(0,0)作为极点, 并未提供的改极点接口, 若想以x0, y0为极点, 则可以在x, y矩阵上分别减去x0, y0再输入函数即可实现
2.极坐标转笛卡尔坐标
x = x0 + rcosΘ, y = y0 + rsinΘ
函数: polarToCart 把cartToPolar的x, y和r, theta顺序交换即可
注: 这两个函数Python接口变为返回形式的
3.图像的极坐标变换
Θ和r都需要设定步长做离散化, 步长小装不下, 步长大可能会损失过多信息, 设变化范围为[r_min, r_max], [Θ_min, Θ_max], 则通常设计步长为Θ_step=360/(180*N), N>=2;
0
概括了一幅图像的灰度级信息, 即每个灰度级在图像中出现的次数
也可以化为归一化直方图(概率直方图), 反映了灰度级的概率密度
Python中可以自行写两重循环统计直方图, 也可利用numpy的hist函数, 还可以用cv2.calcHist函数
C++用OpenCV的calcHist函数, 但对8位直方图略显复杂, 也可同Python一样自定义calcGrayHist函数
1. 线性变换
O(r, c) = a x I(r, c) + b
系数 a 主要控制了对比度, b 主要控制亮度, 对比度和亮度是由两者综合控制的不是单一变量
由于OpenCV和numpy都定义了矩阵数乘和整体加减, 线性变换易实现, 但要注意OpenCV的截断和numpy的溢出问题
Mat的成员函数: convertTo(OutputArray m, int rtype, double alpha=1, double beta=0)
alpha为系数, beta为偏置, rtype为输出类型(使用OpenCVMat的类型), 该函数可用于线性变换, 也可以用于矩阵类型转换
非成员函数: convertScaleAbs(InputArray src, OutputArray dst, double alpha=1, double beta=0)
用法含义同上, 只是调法不同
2. 直方图正规化
设定输出灰度范围Omin, Omax
O(r, c) = (Omax-Omin)/(Imax-Imin)(I(r,c)-Imin)+Omin
这个操作是一种由范围自动选取a, b的线性变换, 一般令)Omax=255, Omin=0
实现同以上的线性变换, 只需事先由设定的Omax, Omin算出a和b即可
函数: minMaxLoc(InputArray src, double* minVal, double* maxVal=0, Point* minLoc=0, Point* maxLoc=0, InputArray mask=noArray)
该函数可获取图像最大值最小值以及他们的位置, minVal最小值, maxVal最大值, minLoc最小值的位置索引, maxLoc最大值的位置索引, 不想要位置可以调用的时候实参赋NULL
正规化函数: normalize(InputArray src, OutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray)
alpha结构元锚点, beta腐蚀操作次数, norm_type边界扩充方法类型, dtype边界扩充值类型
norm_type取值NORM_L1, NORM_L2, NORM_INF分别代表L1, L2, ∞范数, 结果为alpha*矩阵/范数+beta
NORM_MINMAX则alpha*(矩阵-Imin)/(Imax-Imin)+beta
3. 伽马变换
O(r, c) = I(r, c)^γ
伽马变换的一个性质是与gamma与此低对比度图像亮度有关, γ=1时输出原图,
图像整体或感兴趣区域较暗时0<γ<1时增加图像对比度, 较亮时γ>1时增加图像对比度
伽马变换的一个缺陷是输出图像可能整体灰度偏高或偏低
Python可用numpy的pow函数, C++用OpenCV的函数pow(InputArray src, double power, OutputArray dst)
对图像进行伽马变换时, 务必先把图像归一化到[0, 1]范围后再进行幂运算(保证幂运算结果始终在0到1之间), 变换完后再将范围扩回原始范围
实现上变换前后使用convertTo函数, 变换前alpha=1.0/255, CV_64F, 变换后alpha=255, CV_8U
4. 全局直方图均衡化
全局直方图均衡化就是对I_hist进行变换使得O_hist是"平"的, 即使每个灰度级概率密度函数值相等
实际上由于一张图上通常很多灰度级是缺失的, 难以定义概率密度函数(直方图)相等, 我们的目标变成了使得概率分布函数(累加直方图)尽可能线性
同时, 同一变换前灰度级相等的像素变换后灰度级也应相等, 则变换前后对应灰度级处累加直方图值也应该相等
变换原理: 图像宽高W, H确定, 则总像素确定, 均衡化后每个灰度级的概率也确定(直方图值), 即概率分布函数(累加直方图)的斜率也确定: HxW/256
任给一灰度级p, 计算到这一灰度级的累加直方图值, 变换后输出图像对应灰度级的累计直方图值与之相等, 且斜率已知, 即可求出变换后映射的灰度级
q ≈ [∑(p, k=0)I_hist(k)]/(H*W) * 256 - 1
由此可得变换前后的灰度级映射表, 遍历原图每个像素按表映射即可完成变换
实现上按如上步骤能够自定义实现(总结四步:求直方图, 求累加直方图, 建立映射关系, 映射), 另外OpenCV有函数: equalizeHist(src, dst)可以直接调用
5.自适应直方图均衡化
全局直方图均衡化(HE)简单粗暴高效, 但是原图较亮区域易出现失真, 而且易出现噪声
限制对比度自适应直方图均衡化(CLAHE)首先将图像划分为不同的区域块(tiles), 然后对每一块分别进行直方图均衡化, 无噪声时每一块都被限制在很小的范围内, 有噪声时噪声会被加强
由此引入限制对比度, 即如果直方图bin超过了提前预置好的"限制对比度"就会被裁剪, 将裁剪部分平均分到其他的bin中, 重构直方图
Ptr clahe = createCLAHE(double 限制对比度(默认40.0), Size 块大小)创建一个CLAHE算法对象指针, clahe->apply(src, dst)调用
该算法对HE高亮度失真和噪声加强的问题有较好的改善, 但是有两个需要调节的参数, HE则无需调参
直观上同一维卷积, 卷积核反褶移位与图像相乘, 有时为了简便卷积核也指反褶后的卷积核
实际上通常卷积的实现与直观理解不同, 一般将其展开成矩阵与向量相乘的形式
1. full卷积: 先对输入图I和卷积核K在右方和下方补0, 将其尺寸扩展到HxW, 称为Ip和Kp, H=H1+H2-1, W=W1+W2-1
把Ip按行堆叠展开成为(HxW)x1的一维列向量, 基于Kp每一行的转置列向量(作为Gr的第一列, 移位作为下一列, 共WxW)构建H个WxW的循环矩阵Gr
把这H个循环矩阵Gr排成分块矩阵的列, 移位构建新的列, 构建一个(HxW)x(Hxw)的循环矩阵G, 二维full卷积的结果就是矩阵G和Ip展成的列向量相乘的结果, 为一(HxW)的列向量, 最后把他按行重新排列成HxW的矩阵, 即为卷积结果图
2. valid卷积: full卷积对于移位时若有超出部分则补0相乘或者说不考虑超出的仅计算未超出部分, valid卷积则只考虑Kflip(反褶后的卷积核)完全覆盖在输入图I的的情况
由此可见valid卷记的结果实际上就是full卷积结果图的中心区域, full卷积结果尺寸为(H1+H2-1, W1+W2-1), valid卷积的结果尺寸为(H1-H2+1, W1-W2+1)
Python语法: Cvalid = Cfull[H2-1:H1, W2-1:W1]
C++OpenCV语法: Cvalid = Cfull(Rect(W2-1, H2-1, W1-W2+1, H1-H2+1))
3. same卷积: full卷积只要卷积核与图像有交集就补0计算, 尺寸经常会比原图大, same卷积要求结果图尺寸与原图相同, 在Kflip上指定一个锚点, 令锚点按顺序从图像左上角(0,0)移动到右下角做卷积, 结果自然与原图像相同
同样, same卷积的结果也是full卷积的一部分, 令卷积核上的锚点为第kr行, kc列(仍然是从0开始算)
Python语法: Csame = Cfull[H-kr-1:H1+H2-kr-1, W2-kc-1:W1+W2-kc-1]
C++OpenCV语法: Csame = Cfull(Rect(W2-kc-1, H2-kr-1, W1, H1))
大多数时候为了更好的指定锚点, 通常令卷积核宽高为奇数, 锚点为卷积核中心即可
4. full和same卷积都需要对图像外填充, 使用函数:
void copyMakeBorder(cv::InputArray src, cv::OutputArray dst, int top, int bottom, int left, int right, int borderType, const cv::Scalar &value = cv::Scalar())
top:上填充行数, bottom下填充行数, left左填充列数, right右填充列数, borderType填充方法
BORDER_REPLICATE边界复制, BORDER_CONSTANT常数填充, BORDER_REFLECT反射填充, BORDER_REFLECT_101边界为中心反射填充(干扰最小), BORDER_WARP平铺填充, 还有其他填充方式参考头文件
value: BORDER_CONSTANT时填充的常数值 函数默认填充方式为 BORDER_REFLECT_101
5. 卷积的实现:
Python: Scipy.signal有函数convolve2d(原图, 卷积核, 卷积方式mode="full/valid/same", 边界填充boundary="fill/wrap/symm", fillvalue即boundary为fill时的填充值默认0)
same卷积时自动确定锚点, W2H2为奇数,正好取中心; W2偶数H2奇数,纵取中心横取W2-1; W2奇数H2偶数与上个相反; 两个偶数,取(H2-1,W2-1)
同时, 想计算任意锚点的same卷积, 可以利用3的关系取full卷积的roi即可
C++OpenCV: filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel, Point anchor = Point(-1, -1), double delta = (0.0), int borderType = 4)
ddepth输出矩阵的数据类型(位深), kernel卷积核类型为CV_32F/CV_64F, anthor锚点位置, delta为卷积后加的偏置(默认0), borderType边界填充类型同copyMakerBorder函数定义
注: src.depth()=CV_8U, ddepth=-1/CV_16S/CV_32F/CV_64F
src.depth()=CV_16S/16U, ddepth=-1/CV_32F/CV_64F
src.depth()=CV_32F, ddepth=-1/CV_32F/CV_64F
src.depth()=CV_64F, ddepth=-1/CV_64F 同以前一样, -1表示输出类型与输入相同
严格意义上的卷积, 应该先用flip函数(flipCode取-1)将卷积核反褶后再用filter2D函数卷积
6. 可分离卷积
若卷积核K至少由两个比它小的卷积核full卷积而成, 并且计算过程中在所有边界出均进行扩充0的操作, 且满足K=K1*K2*...*Kn(此处*为卷积符号), 则卷积核K是可分离的
full卷积不满足交换律, 但是一维水平方向和一维垂直方向的卷积核full卷积是满足交换律的, 一个3x3的卷积核可以分解为一个1x3的卷积核和一个3x1的卷积核
7. 离散卷积的性质
full卷积的性质: 如果卷积核K可被分离为K1, K2, 那么 I*K = I*(K1*K2) = (I*K1)*K2
从结果上易得两者是相等的, 但是卷积核分离后计算量被减少了
same卷积的性质: 对于WH都为奇数的卷积核K, 如果可被分离为K1, K2, 那么 I*K = I*(K1*K2) = (I*K1)*K2, 锚点默认为中心位置, 若采用0扩充, 使用signal.convolve2d函数分解前后结果相同, 不使用0扩充分解前后边界处可能不同
不用0扩充, 分解前后上下(H2-1)/2行, 前后(W2-1)/2列会有差异, 单由于图像领域卷积核通常很小, 差异可以忽略不计
分解的作用: 不分解的same卷积计算量为(H1xW1)x(H2xW2), 分离后的计算量变为(H1xW1)*(H2+W2)
注: 众多卷积函数里anchor取Point(-1,-1)表示的是取卷积核中心作为锚点
顾名思义, 高斯平滑使用高斯卷积算子作为卷积核, 通常使用奇数尺寸的正方形卷积核, 用均值为0的二维高斯函数构造
设定卷积核尺寸和方差sigma, 令均值为0, 用二维高斯函数整数离散化得到卷积核, 每个元素再除以该矩阵的元素和进行归一化即得到高斯卷积核
性质: 1. 高斯卷积算子是对称的, 反褶180度结果相同
2. 高斯卷积核都是可分离的, 可以分离成两个列, 行向量卷积核, 基于这种性质, OpenCV仅给出了构建1维垂直方向的高斯卷积核函数
Mat getGaussianKernel(int ksize, double sigma, int ktype=CV_64F)
该函数构建的是列方向的高斯卷积核, 想要行方向只需取转置即可
3. 高斯卷积核的二项式近似: 我们用p=1/2, n的一维伯努利分布来近似均值μ=n/2, 方差sigma=n/4的高斯分布, 奇数卷积核, n取偶数, n越大, 对应的高斯分布近似程度就越高
利用二项式近似例如3x3高斯卷积核就可取n=2的二项式近似得到一维高斯卷积, 自卷积就能得到二项式近似的二维高斯卷积核, 提出2^n项可以令卷积核整数化
4. OpenCV函数 void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY = (0.0), int borderType = 4)
sigmaY取0表示与sigmaX相同
一般取纵横标准差相同, 窗口小标准差变化不是很敏感, 窗口大时对标准差变化敏感
考虑多帧运行速度的实现时可以考虑自行实现该函数, 即预先构建好卷积核而不是每次调用都构建一次
均值平滑卷积核的所有元素相同, 本质就是一个等权重的窗口平均
均值平滑卷积核也是可分离卷积核
快速均值平滑: 虽然可分离卷积减少了运算量, 从H1xH2xW1xW2变为H1xW1x(H2+W2)但随图像增大, 需要的窗口增加, 复杂度还是与图像宽高成正比, 利用图像积分, 可以实现复杂度为O(1)的快速均值平均
均值平滑系数相同, 容易知道相邻处的计算存在大量冗余. 矩阵积分就是求和, 积分结果也是矩阵, 存储了原图像对应该位置坐标积分值, 利用非负函数的积分特性, 很容易根据积分矩阵求得任意矩形区域的积分值
公式: I(r,c) = I(rBottom, rRight) + I(rTop-1, cTop-1) - I(rBottom, cLeft-1) - I(rTop-1, cRight)
取到卷积区域积分后只需除以窗口大小就可算出卷积值
OpenCV函数:
void boxFilter(InputArray src, OutputArray dst, int ddepth, Size ksize, Point anchor = Point(-1, -1), bool normalize = true, int borderType = 4)
void blur(InputArray src, OutputArray dst, Size ksize, Point anchor = Point(-1, -1), int borderType = 4)
两函数效果相同, boxFilter可以考虑是否normalize
类似卷积, 也是领域运算, 输出的是区域内的中值
对于边界同卷积一样有多种处理方法, 镜像补充较为理想
中值滤波最重要的能力是去除椒盐噪声(椒盐噪声是图像在传输系统中由于解码错误等原因导致图像中出现孤立白点或黑点, 是一种典型噪声)
中值滤波对于边缘的保留效果要好过均值滤波, 但可能存在边缘小移位的情况, 依赖于窗口大小, 均值滤波则对边缘,非边缘,噪声等一视同仁
OpenCV函数: void sort(InputArray src, OutputArray dst, int flags)
flags: CV_SORT_EVERY_ROW 每行排序
CV_SORT_EVERY_COLUMN 每列排序
CV_SORT_ASCENDING 升序
CV_SORT_DESCENDING 降序
注: 这个函数的flags是可以组合使用的, 用+连接即可, 该函数的Python接口flags名字不带CV
OpenCV函数: medianBlur(InputArray src, OutputArray dst, int ksize)
窗口越大, 噪声点响应越小, 大过噪声点则基本会被滤掉, 中值滤波只是排序统计滤波中的一种, 形态学基础便是使用了最大和最小值
均值与高斯滤波每个位置的模板相同, 双边滤波对每个位置的邻域构建不同权重模板: 空间距离权重模板和相似性权重模板
空间距离模板: closenessWeight(h, w)=exp(-((h-(winH-1)^2/2)+(w-(winW-1)^2/2))/(2σ^2))
每个位置的空间距离模板是相同的, winW, winH还是设定为奇数
相似性权重模板: similarityWeight(h, w)=exp(-(ⅡI(r,c)-I(r+(h-(winH-1)/2), c+(w-(winW-1)/2)Ⅱ^2)/2σ^2)
可见每个位置的相似性权重模板与该位置的邻域有关
将这两个模板进行点乘并归一化就得到权重模板, 使用该模板与图像"卷积"(没有反褶的操作)就得到了双边滤波的输出图像
双边滤波在平滑纹理的基础上保留了边缘, 但需要对每个位置都需要重新计算权重模板会非常耗时
当想滤去纹理保留边缘时, 双边滤波边缘保留能力强于高斯滤波, 但纹理平滑效果稍弱
函数: void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType = 4)实现双边滤波
构建空间距离模板, 同双边滤波
构建相似性权重模板, 对原图进行高斯平滑, 由平滑结果, 用当前位置及其邻域的值的差来估计相似性权重模板
空间距离模板与相似性权重模板点乘, 归一化
循环引导滤波: 多次迭代的联合双边滤波, 每次计算相似性权重模板时利用本次联合双边滤波的结果作为下一次计算相似性模板的依据
平滑图像基础上具有保护边的作用, 并且细节增强也有良好表现, 执行时间快于双边滤波
伪代码:
mean_I = f_mean(I, r)
mean_p = f_mean(p, r)
corr_I = f_mean(I x I, r)
corr_Ip = f_mean(I x p)
var_I = corr_I - mean_I x mean_I
cov_Ip = corr_Ip - mean_I x mean_p
a = cov_Ip / (var_I + ε)
b = mean_p - a x mean_I
mean_a = f_mean(a, r)
mean_b = f_mean(b, r)
q = mean_a x I + mean_b
x代表两图像矩阵对应点相乘, /为对应点相除, f_mean均值平滑, r为模板尺寸, I和q均值归一化的图像矩阵
快速导向滤波: 通过先缩小图像和窗口, 进行导向滤波, 最后再放大图像加速导向滤波的处理速度
阈值分割是图像分割中的一种方法(常见分割方法: 基于阈值, 基于边缘, 基于区域, 基于特定理论, 深度学习等), 阈值分割输出图像往往仅有0和255两种灰度, 通常又被称为二值化
设定阈值, 简单地将高于阈值的像素设为白色, 低于阈值的像素设为黑色, 或者反过来等
对于对比度较低的图像, 应该先做对比度增强, 再做阈值分割
函数: thresold(InputArray src, OutputArray dst, double thresh, double maxval, int type)
maxval最大值(一般取255), type有THRESH_BINARY, THRESH_BINARY_INV, THRESH_TRUNC, THRESH_TOZERO, THRESH_TOZERO_INV等等
THRESH_TRIANGLE和THRESH_OTSU与前type+连接使用
1. 直方图技术
前背景明显对比的图像具有包含双峰的直方图, 寻找阈值就是寻找双峰之间波谷的最小值, 但是由于灰度的波动, 波峰和波谷不是很好确定
法一: 对直方图进行高斯平滑, 逐渐增大标准差, 直到出现两个唯一的波峰和他们之间唯一的最小值
法二: 找灰度直方图的第一个峰值及其灰度, 显然灰度直方图的最大值就是第一个峰值, 其灰度称firstPeak
找直方图第二个峰值, 用与第一个峰值的距离作为系数加权, 公式:
secondPeak=arg_k_max{(k-firstPeak)^2 x histogramI(k)}
找两个峰值的波谷, 多个波谷取左侧(THRESH_TRIANGLE就是类似算法)
直方图技术在处理明显两个峰值的直方图时效果好, 不是明显两个峰值的直方图效果不好
2. 熵阈值法
求归一化累加直方图, 即零阶累积矩, 概率分布函数cH(k)
计算各个灰度级的熵E(t)
f1(t) = E(t)log(cH(t)) / E(255)log(max{cH(0), cH(1)...cH(t)})
f2(t) = (1-E(t)/E(255))log(1-cH(t)) / log(max{cH(0), cH(1)...cH(t)})
求使f=f1+f2最大化的t值, 即为分割阈值
熵算法同直方图一样是一种自动选取阈值的全局阈值分割算法, 两种算法间并没有明显的优势, 分情况对待
3. Otsu算法
最大方差法, 使所选取的前景平均灰度, 背景平均灰度与整幅图平均灰度之间差异最大, 常用的自动阈值分割稳定算法
算零阶累积矩(累加直方图)zC(k), 算一阶累积矩oC(k), 求总体平均值即k=255时的一阶累积矩mean, 计算每个灰度级作为阈值时前景平均灰度, 背景平均灰度与整幅图像的平均灰度的方差, 取方差最大的那个阈值作为输出阈值
方差的度量: σ(k)^2 = (mean x zC(k)-oC(K))^2 / (zC(k) x (1-zC(k)))
Otsu算法通常效果比熵阈值法和直方图技术的效果要好和稳定
全局使用一个阈值分割不总是起作用, 当光照不均匀时单一阈值的全局分割方法(包括OTSU这样的自动阈值方法)效果通常很糟糕
局部阈值分割的核心是计算阈值矩阵, 常用的是自适应阈值算法(移动平均)
使用平滑处理后的输出结果作为每个像素设置阈值的参考值, 自适应阈值中平滑算子的尺寸决定了分割物体的尺寸, 如果滤波器尺寸太小则估计的局部阈值不理想, 平滑算子的宽度必须大于被识别物体的宽度
算法: 对图像平滑处理, 记平滑结果为f_smooth(I), 可用高斯, 均值, 中值平滑
自适应矩阵阈值Thresh = (1 - ratio) x f_smooth(I), 常令ratio为0.15
函数: void adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
adaptiveMethod: ADAPTIVE_THRESH_MEAN_C均值平滑, ADAPTIVE_THRESH_GAUSSIAN_C高斯平滑; thresholdType: THRESH_BINARY, THRESH_BINARY_INV
blockSize: 平滑算子尺寸; C比例系数
注: 对于低对比度图像, 做不做对比度增强对自适应阈值影响不大
按位与: void bitwise_and(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray())
按位或: bitwise_or
按位异或: bitwise_xor
按位非: bit_not
腐蚀与中值平滑类似, 但取的是邻域内最小值输出, 同时邻域不再是单纯的矩形结构, 可以是椭圆形结构, 十字交叉形结构等(同样需要指定一个锚点)
I的腐蚀结果E较亮区域缩小甚至消失, 较暗区域扩大, 腐蚀操作可以去亮噪点, 但会损失非噪区域, 还可以用I-E来实现边界提取
函数: void erode(InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1, -1), int iterations = 1, int borderType = 0, const cv::Scalar &borderValue = morphologyDefaultBorderValue())
kernel结构元, iterations腐蚀次数
kernel一般用函数: getStructuringElement(shape, ksize[, anchor])构建
shape: MORPH_RECT矩形结构元, MORPH_ELLIPSEM椭圆形结构元, MORPH_CROSS十字交叉形结构元; ksize结构元尺寸, anchor结构元锚点
ksize可自由调控, 可以设成1xn或nx1的形式只做某一维的腐蚀, 其他形态学处理同理
形态学处理后会隐约看到结构元形状, 次数越多越明显, 矩形就类似马赛克, 椭圆和十字交叉形状也类似
腐蚀取邻域最小值, 膨胀结果D就是取最大值, 效果与腐蚀相反
膨胀可以填补前景缺口, 消暗噪点, 增强量噪点
函数: void dilate(InputArray src, OutputArray dst, InputArray kernel, Point anchor = Point(-1, -1), int iterations = 1, int borderType = 0, const cv::Scalar &borderValue = morphologyDefaultBorderValue())
注: 创建滑动调的步骤声明: 必要的全局变量, 定义回调函数, 创建窗口, createTrackbar(调名, 绑定窗口名, &调的全局变量, 最大值, 回调函数)waitKey前手动调一次回调函数让窗口一开始就有图(调一次显示一次, 最开始没调不手动调就没有), 也可以用函数getTrackbarPos(调名, 窗口名)加上循环, 空回调函数省去全局变量
开运算就是先腐蚀再膨胀, 闭运算就是先膨胀再腐蚀
开运算能够消除高亮噪点同时保持大块亮前景, 同时开运算对大物体可以在不明显改变面积的前提下平滑边界
闭运算可以填充内部有小黑色的白色物体, 多次迭代也可以在不明显改变面积的情况下平滑其边界
即开运算可以消除暗背景亮纹理,区域; 闭运算消除亮背景的暗区域
开闭运算是腐蚀膨胀的组合, 可以直接用几次erode和dilate函数组合实现
函数: void morphologyEx(InputArray src, OutputArray dst, int op, InputArray kernel, Point anchor = Point(-1, -1), int iterations = 1, int borderType = 0, const Scalar &borderValue = morphologyDefaultBorderValue())
op: MORPH_OPEN开运算, MORPH_CLOSE闭运算, MORPH_GRADIENT形态梯度, MORPH_TOPHAT顶帽, MORPH_BLACKHAT底帽 其他参数同上
顶帽就是原图减开运算T=I-O, 底帽就是原图减闭运算
顶帽得到原图较亮区域, 矫正不均匀光照, 底帽得到原图较暗区域
膨胀减腐蚀, 得到物体边界
顶帽底帽形态学梯度都是腐蚀膨胀的组合, 一样用函数: morphologyEx, 选用对应op即可
图像的边缘是值灰度值发生急剧变化的位置, 边缘对感知很重要, 有时仅需边缘就可理解场景内容, 边缘在某种程度上不随光照和视角变化而变化
亮度, 纹理, 颜色, 阴影等物理因素不同都会导致灰度值突变形成边缘
边缘是通过检查每个像素邻域并对灰度变化进行量化的, 相当于微积分里连续函数中方向导数或者离散数列的差分
边缘检测算法大多通过基于方向导数掩码求卷积的方法
某种边缘检测算子通常有多个卷积核对应不同方向的梯度, 多数方法单独一个卷积核仅能显示某一个方向的边缘, 需要综合多个卷积核的结果输出, 一般有四种方式衡量输出强度:
1. 取对应位置绝对值和
2. 取对应位置平方和的开方
3. 取对应位置绝对值的最大值
4. 插值
取绝对值最大值对边缘走向会敏感, 一般取对应位置的平方和开方效果最好
Roberts135 = [[1, 0], [0, -1]], Roberts45 = [[0, 1], [-1, 0]]
本质是对角方向的差分, 135卷积的结果绝对值反映45度方向的边缘; 45卷积的结果反映135度方向的边缘
Roberts_x = [1, -1], Roberts_y = [[1], [-1]]反映垂直和水平方向的边缘
Roberts边缘检测因为使用了很少的邻域像素近似边缘强度, 对图像中噪声具有高敏感性
prewitt_x = [[1, 0, -1], [1, 0, -1], [1, 0, -1]], prewitt_y = [[1, 1, 1], [0, 0, 0], [-1, -1, -1]]
Prewitt算子是可分离的, 分别反映垂直和水平方向边缘:
prewitt_x = [[1], [1], [1]] ★ [[1], [0], [-1]], prewitt_y = [1, 1, 1] ★ [[1], [0], [-1]]
利用分离卷积加速边缘检测, 另一方面可以很直观的理解Prewitt_x算子实际上先对图像进行了垂直方向的非归一化均值平滑, 再进行水平方向差分, Prewitt_y算子同理
由于加入了平滑处理, 对噪声多的图像Prewitt算子要好过Roberts算子
对45度和135有:
Prewitt_135 = [[1, 1, 0], [1, 0, -1], [0, -1, -1]], Prewitt_45 = [[0, 1, 1], [-1, 0, 1], [-1, -1, 0]]
Prewitt算子的改进是差分前引入了均值平滑, 在平滑处理中, 通常高斯平滑要好过均值平滑, Sobel算子的改进就是把Prewitt算子的未归一化均值平滑卷积核换成了二项式近似的未归一化高斯平滑卷积核
Sobel_x = [[1], [2], [1]] ★ [1, 0, -1] = [[1, 0, -1], [2, 0, -2], [1, 0, -1]]
Sobel_y = [1, 2, 1] ★ [[1], [0], [-1]] = [[1, 2, 1], [0, 0 ,0], [-1, -2, -1]]
显然Sobel算子也是可分离的
同理阶数n的二项式近似高斯卷积核(即一列帕斯卡三角形)和n+1的差分卷积核可以构建(n+1, n+1)的Sobel算子
OpenCV函数: void Sobel(InputArray src, OutputArray dst, int ddepth, int dx, int dy, int ksize = 3, double scale = (1.0), double delta = (0.0), int borderType = 4)
dx≠0时, 输出水平方向Sobel卷积核的结果, dx=0且dy≠0时, 输出垂直方向Sobel卷积的结果; ksize卷积核尺寸, 值为1,3,5,7; scale比例系数; delta平移系数
ksize设为1时表示ksize=3的差分卷积核, 不带平滑算子
Scharr_x = [[3, 0, -3], [10, 0, -10], [3, 0, -3]], Scharr_y = [[3, 10, 3], [0, 0, 0], [-3, -10, -3]]
Scharr_45 = [[0, 3, 10], [-3, 0, 3], [-10, -3, 0]], Scharr_135 = [[10, 3, 0], [3, 0, -3], [0, -3, -10]]
Scharr算子与Sobel算子类似, 但是Scharr卷积核是不可分离的, Scharr系数较大, 相比Prewitt卷积对灰度变化更为敏感, 灰度变化较小区域也有较强的边缘强度
对比Sobel算子细节更加丰富(三阶推荐用Scharr代替Sobel, 更高阶用Sobel, Canny中就是如此)
OpenCV函数: void Scharr(InputArray src, OutputArray dst, int ddepth, int dx, int dy, double scale = (1.0), double delta = (0.0), int borderType = 4)
参数同Sobel但是实现上不能使用分离卷积
以上都是两个方向的算子, 这两个为多个方向的算子
1. Kirsch算子
八个卷积核: k1 = [[5, 5, 5], [-3, 0, -3], [-3, -3, -3]], k2 = [[-3, -3, -3], [-3, 0, -3], [5, 5, 5]], k3 = [[-3, 5, 5], [-3, 0, 5], [-3, -3, -3]], k4 = [[-3, -3, -3], [5, 0, -3], [5, 5, -3]]
k5 = [[-3, -3, 5], [-3, 0, 5], [-3, -3, 5]], k6 = [[5, -3, -3], [5, 0, -3], [5, -3, -3]], k7 = [[-3, -3, -3], [-3, 0, 5], [-3, 5, 5]], k8 = [[5, 5, -3], [5, 0, -3], [-3, -3, -3]]
八个卷积核八个方向, 图像与每个卷积核卷积取绝对值作为对应方向的边缘强度量化, 对对应位置取最大值作为最后输出的边缘强度
2. Robinson算子
八个卷积核: r1 = [[1, 1, 1], [1, -2, 1], [-1, -1, -1]], r2 = [[1, 1, 1], [-1, -2, 1], [-1, -2, 1], [-1, -1, 1]], r3 = [[-1, 1, 1], [-1, -2, 1], [-1, 1, 1]], r4 = [[-1, -1, 1], [-1, -2, 1], [1, 1, 1]]
r5 = [[-1, -1, -1], [1, -2, 1], [1, 1, 1]], r6 = [[1, -1, -1], [1, -2, -1], [1, 1, 1]], r7 = [[1, 1, -1], [1, -2, -1], [1, 1, -1]], r8 = [[1, 1, 1], [1, -2, -1], [1, -1, -1]]
使用过程同Kirsch算子
由于使用了8个卷积核, 通常检测的边缘会比Sobel等方法更加丰富, 以上方法对于输出的边缘图显示时通常255截断转uint8, 除了这种方法外也可以用normalize正规化函数归一化处理
基于卷积的边缘检测方法有两个问题:
1. 没有充分利用梯度方向
2. 最后输出的边缘二值图只是简单利用阈值处理, 阈值太大会损失过多边缘信息, 太小则噪声过多
Canny的改进:
1. 基于边缘梯度方向的非极大值抑制
2. 双阈值滞后处理
第一步: 对图像做Sobel算子处理得到dx, dy, 两者平方和开方得边缘强度magnitude
第二步: 由dx, dy得到梯度方向角 angle = arctan2(dy, dx)
第三步: 对每个位置进行非极大值抑制处理, 仍然返回一个矩阵nonMaxSup, nonMaxSup上每个点查找对应位置的magnitude和angle矩阵的点, 根据angle的方向确定邻域, 查找邻域内的magnitude, 若当前位置的magnitude大于所有邻域magnitude值则为极大值, nonMaxSup当前位置填入当前magnitude值, 否则为非极大值, nonMaxSup当前位置填入0
(由于sobel的方向为左值减右值, 下值减上值, 算出的角度因该是y轴朝下的坐标, 坐标系的选取会影响到非极大值抑制的邻域选取)
(第一种邻域选取方法: 梯度方向的邻域根据角度离散化为四种情况, 左右, 上下, 左对角线, 右对角线)
(第一种邻域选取方法: 根据梯度方向插值, 离散化为四种情况: 一种对角线与左右或上下的组合, 这种方法损失最少)
非极大值抑制因为只保留了极大值, 实际是对Sobel边缘强度图进行了细化
第四步: 双阈值的滞后处理, 设定高低双阈值, 强度大于高阈值确定为边缘, 小于低阈值直接剔除, 两者之间考虑连通性, 只有当这些点能够按某一路径(该路径所有点强度大于低阈值)与确定边缘点(强度大于高阈值的点)相连时被接收为边缘
OpenCV函数: void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize = 3, bool L2gradient = false)
threshold1低阈值, threshold2高阈值, apertureSize为Sobel核大小(3时使用Scharr核), L2gradient表示强度计算方式,true为平方和开方false为绝对值和
二维离散函数的拉普拉斯变换定义为: f(x+1, y)+f(x-1, y)+f(x, y-1)+f(x, y+1)-4f(x, y)
推广到构造图像卷积核: l0 = [[0, -1, 0], [-1, 4, -1], [0, -1, -]], l0- = [[0, 1, 0], [1, -4, 1], [0, 1, 0]]
1. Laplacian算子只有一个卷积核, 近似覆盖多方向, 计算量小
2. Laplacian算子相比Sobel和Prewitt等不含平滑处理, 对噪声产生较大响应误认为边缘, 使用时最好先进行高斯平滑
3. Laplacian算子得不到有方向的边缘
4. Laplacian算子的所有值和必须为0, 保证恒等灰度区域不会产生错误边缘
OpenCV函数: void Laplacian(InputArray src, OutputArray dst, int ddepth, int ksize = 1, double scale = (1.0), double delta = (0.0), int borderType = 4)
ksize=1时用的l0-卷积核, ksize=3时用的l4卷积核
Laplacian对噪声敏感, 一般要先进行高斯平滑, 需要两次卷积, 为了一次卷积就实现使用二维高斯函数的拉普拉斯变换离散化后的模板进行卷积
效果基本同先高斯平滑后拉普拉斯但速度更快(一次卷积, 并且LoG卷积核是可分离的, 可以用分离卷积进一步加速), 随标准差增大边缘尺度也会越来越大
虽然高斯拉普拉斯卷积核可分离, 但当核尺寸较大时计算量仍然很大
二维高斯函数相对σ的一阶偏导数就是高斯函数的拉普拉斯变换乘以σ, 由此根据导数定义(kσ处和σ处斜率, 极限k趋近于1)用该导数的近似值除以σ近似高斯拉普拉斯, k越接近1近似值越接近真实值
DoG根据这个原理构建高斯差分卷积核与图像卷积进行边缘检测
又因为DoG卷积核是两个非归一化高斯核的差, 同时高斯核又是可分离的, 为了减少计算量可以不创建DoG核, 而是根据卷积的加法分配率和结合律性质图像分别与两个不同的高斯核卷积然后做差实现
DoG结果几乎同LoG, 速度更快
LoG和DoG最后一步仅简单的阈值化处理显得过于粗糙, Marr-Hildreth对其进行了改进
该算法首先构建LoG或这DoG与图像卷积得到初始边缘结果, 再在此结果上寻找过零点位置, 过零点位置即边缘位置
过零点的思想: 一阶导的极大值是二阶导的零点, 边缘是一阶导的极大值, 拉普拉斯算子相当于图像的二阶导, 在二阶导里找零点就是找局部强度最大的边缘
判过零点的方法: 对左右, 上下, 两对角线四种邻域分别判是否异号, 异号则为零点
对当前位置8邻域分别取左上, 左下, 右上, 右下四个2x2矩形的均值, 任意两个均值是异号的则该位置为零点
该算法能够有效细化边缘去除噪声, 结果类似于Canny的结果
点集是指坐标点的集
1. 最小外包旋转矩形
OpenCV提供了两个关于矩形的类, Rect直立矩形, RotatedRect旋转矩形, 三个要素(中心坐标, 尺寸, 旋转角度)就可确定一个旋转矩形
函数 RotatedRect minAreaRect(InputArray points)
返回点集的最小外包旋转矩形, points可接收三种参数: Nx2的Mat, vector或vector, Nx1的双通道Mat
2. 旋转矩形的四个角点
函数: void boxPoints(RotatedRect box, OutputArray points)
接收旋转矩阵对象返回四个角点, 可用画线函数line绘制旋转矩形(line函数需要Point, 该函数返回的是Mat, 需要处理一下)
3. 最小外包圆
函数: void minEnclosingCircle(InputArray points, Point2f ¢er, float &radius)
4. 最小外包直立矩形
函数: Rect boundingRect(cv::InputArray array) 输入的array形式同以上两最小外包函数三种
注: C++的line等函数输入对象为Point, rectangle输入对象为Rect, Python接口通常都为int元组
5. 最小凸包
凸包就是将最外层的点连接起来构成的凸多边形, 能包含点集中所有点
函数: void convexHull(InputArray points, OutputArray hull, bool clockwise = false, bool returnPoints = true)
points输入点集类型为Mat或vector, hull构成凸包的点vector, vector
clockwise顺时针或逆时针
returnPoints为true时hull储存坐标点, false时储存的是坐标点在点集中的索引
6. 最小外包三角形
函数: double minEnclosingTriangle(InputArray points, OutputArray triangle)
该函数的区别是不支持Nx2单通道Mat(可用reshape方法转成双通道), 返回的double是三角形面积
笛卡尔坐标系下任意一直线, 作原点到其的垂线oN得到距离ρ, 根据象限确定oN与x轴(一二象限正方向, 三四象限负方向)的夹角Θ, 直线总满足方程:
ρ = xcosΘ + ysinΘ
即一对ρ和Θ能唯一确定一直线, 反之亦然
称ρ和西塔组成的坐标系为霍夫空间, 则笛卡尔空间内一条直线唯一确定霍夫空间一个点, 反之亦然
另一角度考虑, 过笛卡尔空间任意一点有无数条直线, 对应霍夫空间无数个点, 将这无数个点相连就可得到霍夫空间一条曲线, 满足方程ρ = xcosΘ + ysinΘ
要验证笛卡尔空间内多个点是否共线, 只需要看分别过这多个点的直线在霍夫空间对应的多条曲线, 交于同一点的曲线对应的笛卡尔空间那几个点就共线
1. 霍夫直线检测
过一点有无数条直线, 故需要离散化, 例如取Θ从0到180度, 间隔1度, ρ从0到最大可能值(即图像对角线长度)每次间隔1, 由此可构建一个矩阵(二维直方图)用于计数
对每个点遍历每个Θ值由ρ = xcosΘ + ysinΘ算出ρ值并离散化到最近ρ值中, 在计数矩阵的(ρ, Θ)处加一
遍历所有待检测点后, 根据设定阈值, 取高于阈值的(ρ, Θ)作为输出直线(即高于阈值个点在此直线上的直线)
函数: 标准霍夫 void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = (0.0), double stn = (0.0), double min_theta = (0.0), double max_theta = (3.141592653589793116))
函数: 计算量更小的概率霍夫 void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = (0.0), double maxLineGap = (0.0))
2. 霍夫圆
标准霍夫圆: 笛卡尔空间内任意一点(x, y)与一确定r带入方程(x-a)^2 + (y-b)^2 = r^2中可得到一个关于a和b的方程, 以a, b为坐标轴建系得到ab空间, 即确定r下笛卡尔空间一点对应ab空间内一个圆
对于笛卡尔空间多个点, 设定r范围(大小和间隔), 对每个r遍历所有点, 得到ab空间一系列, ab空间内交于一点的多个圆对应的笛卡尔空间的点共圆, 半径为当前r, 遍历完所有范围内r得到了各种半径下共圆的点
实现上类似霍夫直线, 每个r下, 离散化的ab矩阵为一个计数器, 笛卡尔空间每个点对应的圆轨迹过的坐标都加一, 设定阈值, 遍历所有点后ab计数器高于阈值的点对应了输出的笛卡尔空间的圆, ab空间过这点的圆对应的笛卡尔空间的点共圆, 半径为当前遍历到的半径值
基于梯度的霍夫圆: 标准霍夫圆计数器是三维的(r可以视为一维), 同时计算的曲线太多
梯度霍夫圆, 求取前景每一点梯度, 梯度方向就是切线方向, 与梯度方向相垂直就可做出每一点的法线, 多条法线相交的地方就可能是这些点共圆的圆心, 再检查这些点到圆心的半径, 半径相同的点就共圆(设定一个阈值, 共圆点超过阈值的就输出圆)
求梯度方向同Canny, 用两方向Sobel算子arctan2求得
实现上会有两个计数器, 圆心的二维计数器, 对每个圆心半径的一维计数器
函数: void HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double param1 = (100.0), double param2 = (100.0), int minRadius = 0, int maxRadius = 0)
内部实现的了Canny边缘提取, 输入可以是灰度图(Hough直线只能是二值图)
circles: 返回圆, vector, 每个Vec3f一圆(x, y, r), method: 目前只支持CV_HOUGH_GRADIENT即梯度霍夫圆
dp: 计数器分辨率 minDist: 圆心间最小距离, 太小相交圆太多太大会漏圆
param1: Canny高阈值, 低阈值被定为他的一半 param2: 最小投票数
minRadius: 最小半径 maxRadius: 最大半径
该函数的Python api是返回式的, N个圆存在1xNx3的数组中
霍夫变换不局限于圆和直线, 对椭圆等形状也可以进行检测, 优点是遮挡也能检测, 缺点是无先验情况下需要多次调参
1. 找轮廓
函数: void findContours(InputOutputArray image, OutputArrayOfArrays contours, int mode, int method, cv::Point offset = cv::Point())
输入为二值图; 输出contours为多个轮廓, C++中一个轮廓用vector, Python中为Nx1x2的ndarray, 多个轮廓C++再外包一个vector即vector>, Python则是外包一个list
mode一般为RETR_EXTERNAL, method一般为CHAIN_APPROX_SIMPLE, 其余参阅头文件
2. 画轮廓
函数: void drawContours(InputOutputArray image, InputArrayOfArrays contours, int contourIdx, const Scalar &color, int thickness = 1, int lineType = 8, InputArray hierarchy = noArray(), int maxLevel = 2147483647, Point offset = Point())
画轮廓函数, contourIdx是索引, 表示画第几个轮廓, color为轮廓颜色, thickness表示粗细若小于0则填充轮廓内的区域
技巧: 找完轮廓后通常会用到一中的最小外包函数(圆, 矩形, 旋转矩形, 多边形, 凸包), 可用多边形拟合函数approxPolyDP, 直线拟合函数fitline, 椭圆拟合函数fitEllipse等拟合, 可以通过面积(.area)等方式限制过滤
找轮廓函数输入为二值图, 对于灰度差别较大的图像可以使用阈值化二值图, 不易阈值化的可以用Canny(易阈值化的时候阈值化二值图后找轮廓更方便效果更好, 但是Canny多余轮廓也可利用后续拟合解决)
3. 周长与面积
周长: double arcLength(InputArray curve, bool closed) closed表示是否将首尾相连
面积: double contourArea(InputArray contour, bool oriented=false)
两函数输入都是点集, 一般三种形式:vector, nx2单通道Mat, nx1的双通道Mat
注: 把轮廓近似拟合成各种最小外包后一般会有内部成员得到周长面积
4. 点与轮廓关系
函数: double pointPolygonTest(InputArray contour, Point2f pt, bool measureDist)
点与点集关系, measureDist为true时返回点到轮廓距离, false时返回值有三种+1点在轮廓内, 0轮廓上, -1轮廓外
5. 凸包缺陷
函数: void convexityDefects(InputArray contour, InputArray convexhull, OutputArray convexityDefects)
contour为轮廓(vector), convexhull是convexHull函数的输出(vector), convexityDefects代表凸包缺陷信息(vector)
每个Vec4i代表一个缺陷, 四个元素依次代表(缺陷起点索引, 终点索引, 最远点的索引, 最远点到凸包的距离)
类似的还有矩形度, 椭圆度, 圆度检测
原理公式较多, 参阅冈萨雷斯《数字图像处理》
二维离散傅里叶变换可以分解为先后对纵横两个方向做一维傅里叶变换
傅里叶变换可以写成矩阵形式: F = UfV, f = U^-1fV^-1 = 1/MN (U* F V*) *表复共轭 M行N列图像
函数: void dft(InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0)
仅支持CV_32F和CV_64F, flags确定输出形式
DFT_COMPLEX_OUTPUT输出复数, DFT_REAL_OUTPUT只输出实数, DFT_INVERSE逆变换, DFT_SCALE是否除以MN, DFT_ROWS输入矩阵每行进行变换或逆变换
src单通道代表实矩阵, 双通道代表复矩阵, 输出矩阵的通道由flags形式指定
flags可+连接组合使用, 常用组合为DFT_INVERSE+DFT_SCALE+COMPLEX_OUTPUT
傅里叶变换的复杂度为O((MN)^2), 当M和N都是2的幂时, 快速傅里叶变换算法可以将复杂度降至O(MNlog(MN))
OpenCV的fft针对的是行数和列数均为2^px3^qx5^r的情况
对于不是这个尺寸的图就需要扩充行列(右侧下侧补0)
函数: getOptimalDFTSize(int vecsize) 返回一个不小于vecsize, 且可分解为以上形式的整数
fft算法已集成在了dft函数中, 使用时先用getOptimalDFTSize算出高宽的目标大小, 用copyMakeBorder函数在右侧和下侧扩充却少的行列, 之后直接调用dft函数即可加速
在逆变换后为得到原图还需用Rect取roi裁剪
void magnitude(cv::InputArray x, cv::InputArray y, cv::OutputArray magnitude)
该函数计算两矩阵对应位置的平方和的平方根
使用时先对图像傅里叶变换, 输出双通道矩阵, split函数分离通道后调用此函数求出幅度
显示时可以log后normalize再convertTo乘255
void phase(InputArray x, InputArray y, OutputArray angle, bool angleInDegrees = false)
计算两矩阵的arctan(y/x)得到相角, angleInDegree表示是否把相角转到(-180, 180)
中心化后相角图较亮区域与图像主要梯度同向(与边缘走向垂直)
1. 求图像fft得到变换复矩阵F
2. 傅里叶变换的幅度谱的灰度级
3. 计算相位谱, 根据相位谱计算对应的正弦谱和余弦谱
4. 对第二步计算的灰度级进行均值平滑, 记为f_mean(graySpectrum)
5. 计算谱残差, 即第二步得到的幅度谱的灰度级减去第四步得到的均值平滑结果
6. 对谱残差进行幂指数运算
7. 幂指数运算作为新的"幅度谱", 仍然使用原图的相位谱, 根据新的"幅度谱"和相位谱进行傅里叶逆变换
8. 对于第七步的复数矩阵, 计算该矩阵的实部和虚部平方和的开方, 然后高斯平滑, 最后灰度级转换得到显著性
时域卷积就是频域相乘, 图像与卷积核卷积就是图像和卷积核分别做傅里叶变换(对图像和卷积核都先填充到卷积结果大小)后相乘, 再逆变换
函数: void mulSpectrums(InputArray a, InputArray b, OutputArray c, int flags, bool conjB = false)
把两个双通道矩阵按照复矩阵计算点积, conjB表示是否对b取共轭
same卷积, 先对图像进行扩充, 上下左右分别扩充(m-1)/2行和(n-1)/2列(扩充方式同卷积, 最好镜像), 卷积核右和下扩充0到和图像一样大
分别计算两者的傅里叶变换, 对变换结果求点积, 计算点积结果的傅里叶变换, 只取实部, 得到full卷积结果, 裁剪为same卷积结果
当卷积核较小时没有明显优势, 当卷积核较大时利用傅里叶变换算卷积才会有明显优势
频率域滤波的过程和用傅里叶变换计算卷积相似, 设计和图像同大小的滤波器(做傅里叶变换), 和图像傅里叶变换结果点乘后逆变换
低频信息表示图像灰度变化缓慢区域, 高频则为变化快的部分如边缘
低通滤波滤去高频保留低频, 高通滤波则相反
假设图像宽高WH, 傅里叶频谱的最大值(中心点)的位置在(maxR, maxC), radius表示截断频率, D(r, c)表示到中心位置的距离
D(r, c) = √((r-maxR)^2+(c-maxC)^2)
1. 理想低通滤波
ilpFilter(r, c) = 1, D(r, c) <= radius
0, D(r, c) > radius
2. 巴特沃斯低通滤波
blpFilter(r, c) = 1/(1+[D(r,c)/radius]^2n) n代表阶数
3. 高斯低通滤波
glpFilter(r, c) = e^(-D^2(r,c)/2radius^2)
低通滤波留下图像平滑部分, 去噪, 通常高斯低通效果较好
4. 理想高通滤波
ihpFilter(r, c) = 0, D(r, c) <= radius
1, D(r, c) > radius
5. 巴特沃斯高通滤波
bhpFilter(r, c) = 1 - 1/(1+[D(r,c)/radius]^2n) n代表阶数
6. 高斯高通滤波
ghpFilter(r, c) = 1 - e^(-D^2(r,c)/2radius^2)
高通滤波保留图像变化剧烈部分, 与原图叠加可锐化图像
hpFilter = 1 - lpFilter
带通保留特定频率带, 带阻滤去特定频率带
BW代表带宽, D0代表带宽的径向中心
1. 理想带通
ibpFilter(r, c) = 1, D0-BW/2 <= D(r, c) <= D0+BW/2
0, else
2. 巴特沃斯带通
bbpFilter(r, c) = 1 - 1/(1+(DxBW/(D(r,c)^2-D0^2))^2n)
3. 高斯带通
gbpFilter(r, c) = exp(-(D(r,c)^2 - D0^2)/(D(r,c)xBW)^2)
4. 理想带阻
ibrFilter(r, c) = 0, D0-BW/2 <= D(r, c) <= D0+BW/2
1, else
5. 巴特沃斯带通
bbrFilter(r, c) = 1/(1+(DxBW/(D(r,c)^2-D0^2))^2n)
6. 高斯带通
gbrFilter(r, c) = 1 - exp(-(D(r,c)^2 - D0^2)/(D(r,c)xBW)^2)
brFilter = 1 - bpFilter
同态滤波与频率域滤波的不同处是在最开始对图像做对数运算, 在最后一步做对数的逆运算
一. RGB空间, 红绿蓝, 源于阴极射线管的彩色电视, RGB加色原理混合处其他颜色
二. HSV空间, 色调饱和度亮度, 与人类感知对应, RGB的三个量对颜色不是独立的关系, HSV有单独的色调通道利于色彩分离提取, 单独亮度通道的存在也让改变彩色图像亮度变得方便
三. HLS(HSI)空间, 色调明度饱和度, 类似HSV, 纯色的亮度等于白色的亮度, 纯色的明度等于中等灰的明度
四. Lab, YUV等空间, 参阅官方文档
函数: void cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0)
code表转换空间输入输出类型例如COLOR_BGR2HSV, 其余参阅头文件