图像处理基本操作
基本数据类型
Vec
如果想要存储向量 可使用 Vec 类。这是一个列向量。其初始化方式为:
Vec<Typename _Tp, int _cn>
Vec<uchar, 3> vi(1, 2, 3); // 定义一个 列向量 [1, 2, 3]
vi.rows; // 值为 3
// OpenCV 中也有许多别名可用
typedef Vec<uchar, 3> Vec3b; // 类似定义有很多
Mat
OpenCV 中矩阵以 Mat 类型存在,通常用于存储图片数据。其常用构造函数如下:
Mat(Mat image); // 其他 Mat 对象的拷贝 像素不共享
Mat(int rows, int cols, int type); // 构造新的 Mat 对象
Mat(Size(int cols, int rows), int type); // 另一种方式构造 Mat 对象 注意宽高顺序不同
type 代表像素点类型。常用如下:
#define CV_8UC1 // 0-255 表示的单通道灰度图
#define CV_8UC3 // 0-255 表示的三通道彩色图
#define CV_32FC1 // 0-1 浮点数表示的单通道灰度图
#define CV_32FC3 // 0-1 浮点数表示的三通道彩色图
如下可构造两行三列的矩阵:
Mat m = Mat(2, 3, CV_8UC1);
矩阵信息访问方式:
m.rows; // 矩阵行数
m.cols; // 矩阵列数
m.size(); // 矩阵尺寸信息的 Size 对象
m.total(); // 矩阵点数 即 行数*列数 也可使用 m.size().area() 获取
矩阵中的值通常使用两种访问方式 指针访问 和 对象访问
指针访问速度是最快的 不涉及对象生成:
for (int r = 0; r < m.rows; ++r)
{
char *ptr = m.ptr<uchar>(r); // 获取行指针
for (int c = 0; c < m.cols; ++c)
cout << ptr[c] << endl; // 访问像素值
}
如果想要进一步加速可使用 isContinuous() 判断图片是否在内存中连续存储。
at 方式访问速度较慢 但更为直观 如果追求可读性可以考虑使用这种方式:
m.at<uchar>(r, c); // 访问第 r 行 c 列的元素
以上两种方式均为对原像素空间进行访问,可以修改矩阵的值。
通常我们使用 Mat 存储函数返回结果。如果需要赋初值可使用如下方式:
Mat m = Mat::zero(int rows, int cols, int type);
Mat m = Mat::ones(int rows, int cols, int type);
Mat m = Mat::eye(int rows, int cols, int type);
Mat m = (Mat_<uchar>(int rows, int cols) << 1, 2, 3, 4, 5, 6) // 最外层括号不可省略
// , 优先级低于 =
矩阵之间可进行算数运算。但注意 m*n 代表矩阵乘法叉乘,m.mul(n) 为点乘。矩阵除法 m/n 为点除。
图片读写
OpenCV 提供了读写使用的 API,支持多种图片格式,会根据图片格式自动解码。
Mat image = imread("1.bmp"); // 默认方式读取为三通道BGR图 CV_8UC3
Mat image = imread("1.bmp", IMREAD_GRAYSCALE); // 通常使用单通道灰度图 CV_8UC1
imwrite("result.jpg", image); // 指定后缀名即可转为对应的图片格式
几何变换
几何变换即空间坐标变换 主要使用以下三种
仿射变换
仿射变换可实现平面内的变换:缩放、平移、旋转。其运算可由矩阵表示为:
(
x
~
y
~
)
=
(
a
11
a
12
a
13
a
21
a
22
a
23
)
(
x
y
1
)
\begin{pmatrix} \tilde x \\ \tilde y \end{pmatrix} = \begin{pmatrix}a_{11} & a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\end{pmatrix} \begin{pmatrix}x\\y\\1\end{pmatrix}
(x~y~)=(a11a21a12a22a13a23)⎝⎛xy1⎠⎞
可以参照矩阵运算规则直接写出变换矩阵,或使用方程法求解。六个未知数需要三组坐标:
vector<Point> src;
src.push_back(Point(0, 0));
src.push_back(Point(1, 0));
src.push_back(Point(0, 1));
vector<Point> dst;
dst.push_back(Point(0, 0));
dst.push_back(Point(2, 0));
dst.push_back(Point(0, 2));
Mat affine = getAffineTransform(src, dst);
通常在实现时使用逆向方法,根据目标图坐标反推原图坐标,乘上变换矩阵的逆即可。而求得的原图坐标通常不是整数,需要使用邻近像素取插值方式得到像素值。方式有最近邻插值,双线性插值,双三次插值。其中双三次插值效果最好,最近邻速度最快。
投影变换
由于图片拍摄时视角不会在物体正上方,导致图片存在三维空间存在旋转,这种变换称为投影变换:
(
x
~
y
~
z
~
)
=
(
a
11
a
12
a
13
a
21
a
22
a
23
a
31
a
32
a
33
)
(
x
y
z
)
\begin{pmatrix} \tilde x \\ \tilde y\\ \tilde z \end{pmatrix} = \begin{pmatrix}a_{11} & a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\a_{31}&a_{32}&a_{33}\end{pmatrix} \begin{pmatrix}x\\y\\z\end{pmatrix}
⎝⎛x~y~z~⎠⎞=⎝⎛a11a21a31a12a22a32a13a23a33⎠⎞⎝⎛xyz⎠⎞
而图像处理关心的是二维平面下的二维投影变换,存在一些约束条件,上式可简化为:
(
x
~
y
~
1
)
=
(
a
11
a
12
a
13
a
21
a
22
a
23
a
31
a
32
1
)
(
x
y
1
)
\begin{pmatrix} \tilde x \\ \tilde y\\ 1 \end{pmatrix} = \begin{pmatrix}a_{11} & a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\a_{31}&a_{32}&1\end{pmatrix} \begin{pmatrix}x\\y\\1\end{pmatrix}
⎝⎛x~y~1⎠⎞=⎝⎛a11a21a31a12a22a32a13a231⎠⎞⎝⎛xy1⎠⎞
共有八个未知数 需要4组点求解,可使用 API:
Mat src = (Mat_<float>(2, 4) << 0, 0, 1, 0, 0, 1, 1, 1);
Mat dst = (Mat_<float>(2, 4) << 0, 0, 2, 0, 0, 1, 1, 1);
Mat perspective = getPerspectiveTransform(src, dst);
可以看出 仿射变换 是 透视变换 的一个子集。
极坐标变换
即笛卡尔坐标系转换到极坐标系。通常用于处理圆形物体,如仪表盘刻度。对
x
o
y
xoy
xoy 平面上任意一点
(
x
,
y
)
(x, y)
(x,y) 以
(
x
ˉ
,
y
ˉ
)
(\bar x, \bar y)
(xˉ,yˉ) 为中心变换到
θ
o
r
\theta or
θor 平面。变换规则如下:
r
=
(
x
−
x
ˉ
)
2
+
(
y
−
y
ˉ
)
2
r=\sqrt{(x-\bar x)^2+(y-\bar y)^2}
r=(x−xˉ)2+(y−yˉ)2
θ
=
{
2
π
+
arctan
2
(
y
−
y
ˉ
,
x
−
x
ˉ
)
,
y
−
y
ˉ
≤
0
arctan
2
(
y
−
y
ˉ
,
x
−
x
ˉ
)
,
y
−
y
ˉ
>
0
\theta=\left\{\begin{array}{ll}2\pi+\arctan2(y-\bar y,x-\bar x),& y-\bar y\le0\\[4ex]\arctan2(y-\bar y,x-\bar x),&y-\bar y>0\end{array}\right.
θ=⎩⎪⎨⎪⎧2π+arctan2(y−yˉ,x−xˉ),arctan2(y−yˉ,x−xˉ),y−yˉ≤0y−yˉ>0
OpenCV 提供了cartToPolar() 函数可实现这一变换。
对比度增强
通常情况下待处理的图像为灰度图像,增强对比度是提高灰度图图像质量的一个轻量级操作,其主要通过改变灰度直方图分布来实现。
直方图正规化(归一化)
灰度图可以有 0-255 共 256 级灰度,而原始图片的灰度分布不一定能占满整个灰度值域,一个显而易见的增强方式即为拉伸其灰度分布到 [0,255]。OpenCV 提供了 normalize 函数实现正规化,它实现了多种正规化操作,这里仅使用最简单的 NORM_MINMAX 模式:
Mat norm_image; // 用来存储正规化后的图片
normalize(image, norm_image, 255, 0, NORM_MINMAX);
伽马变换
伽马变换需要先将图片归一化到 [0, 1] 范围,然后对每一个像素求其 γ \gamma γ 次方。由此可知。对于 γ = 1 \gamma=1 γ=1 的变换,图片不变。如果 0 < γ < 1 0<\gamma<1 0<γ<1,则较暗的区域所占的灰度范围会被扩展,而 γ > 1 \gamma>1 γ>1 时,较亮的区域所占灰度范围会扩展。即伽马变换是以主观感觉为参考的变换方式,如果感兴趣区域较暗则将 γ \gamma γ 值设为 [0, 1]。其实现使用 pow 函数即可,只是需要转换下数据格式:
Mat float_image;
image.converTo(float_image, CV_32F1, 1.0f / 255, 0); // 转换函数 可给定系数及偏移量(y=a*x+b)
Mat gamma_image;
pow(float_image, 0.5f, gamma_image);
gamma_image.converTo(gamma_image, CV_8UC1, 255, 0); // 如果需要再把格式转换回来
直方图均衡化
如果想要自动对灰度分布进行变换,只能给定一个均衡的目标,对较暗和较亮区域一视同仁,将直方图分布拉成平的。即像素值均匀分布在灰度直方图上,达到自动增强对比度的效果。
实际操作中拉平直方图会导致一些暗区域出现噪声,亮区域则损失信息,于是提出一种改进版本,限制对比度的自适应直方图均衡化。其目的不再是拉平灰度分布,而是仅对灰度分布高出限定值的部分进行削顶。即只将灰度分布集中的部分进行扩展,效果会更温和。并且其实现也不是对全图区域进行均衡化,而是先划分为不重合的区块后各自均衡化。实现为 CLAHE。
Ptr<CLAHE> clahe = createCLAHE(2.0, Size(8, 8)); // 构建 CLAHE 对象 阈值为2 区块大小 8*8
Mat dst; // 用于保存结果
clahe->apply(image, dst); // 自适应均衡化
滤波
图像中通常会包含很多噪点,滤波是必须进行的一项操作。常见的滤波如均值滤波、高斯滤波等线性滤波,在消除噪点的同时也会抹去很多图像细节。我们对另一类在消除噪声的同时能保留图像边界的滤波更感兴趣,这些一般为非线性滤波。
中值平滑
对平滑窗口内像素按值排序后取中值作为结果,这种滤波方式称为中值滤波,对去除椒盐噪声非常有效。椒盐噪声为图像中的孤立白点或黑点,常因图像传输过程中的误码等原因出现,而在一定区域内的灰度排序中,噪声值常位于排序两端,不会被选中。
形态学处理
虽说形态学处理不属于滤波范围,但对于我们感兴趣的结构来说,噪声尺度相对较小,所以可以通过形态学处理消除小尺寸目标保留大尺寸目标来达成滤波的目的。
形态学处理也是基于邻域内像素值排序来做的。其邻域结构不局限于矩形,可以是椭圆、菱形等。若取排序最大值作为输出结果,此操作称为膨胀,即白色区域扩张。而取排序最小值作为输出结果,称之为腐蚀,白色区域缩小。
膨胀和腐蚀作为基本操作单元可以进行不同的组合达成不同的效果:
先腐蚀后膨胀可以消除一些细小的高亮区域,可以在纤细点处分离物体,称之为开运算。
先膨胀后腐蚀可以消除一些细小的黑色区域,可以消除空洞,连接临近物体。称之为闭运算。
如果使用原图减去开运算结果,则我们能得到被开运算消除的高亮区域,这有种重要的作用:矫正不均匀光照,这种操作称为白顶帽变换。
如果使用原图减去闭运算结果,则可以得到图像中较暗的区域,称为黑底帽变换。
另外有一种膨胀结果减去腐蚀结果,可以得到其形态学梯度,类似于边缘检测的效果。膨胀图减去原图或原图减去腐蚀图也可以得到边缘信息。
阈值分割
二值图是最容易处理的图。实现也很方便。给定一个阈值即可。
Mat bin;
threshold(image, bin, 100, 255, THRESH_BINARY); // 给定阈值100
但我们通常希望能自动得到阈值,如果图片中仅存在背景和一个前景,灰度直方图有两个峰,使用 OTSU 效果较好。其目的在于最大化前景和背景的类间差。
Mat bin;
threshold(image, bin, 0, 255, THRESH_BINARY|THRESH_OTSU); // 自动计算 给定阈值无效
而如果前景占比例较小,灰度直方图仅有一个背景峰时(或相反的只有一个前景峰时),选用 TRIANGLE 方法效果较好。
Mat bin;
threshold(image, bin, 0, 255, THRESH_BINARY|THRESH_TRIANGLE); // 自动计算 给定阈值无效
由于拍摄时可能存在光照不均的情况,图片上会带有阴影效果,此时使用全局阈值效果会很不理想。针对这种情况应先去除阴影影响,或直接使用自适应局部阈值化。
Mat bin;
// 参数给定邻域范围为 80*80 取均值作为阈值 邻域范围应足够大 能消除物体影响 得到背景阴影模式的估计
adaptiveThreshold(image, bin, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 81, 0);
边缘检测
最常用的检测方式为 Canny。其结果为二值图,边缘像素为白色。
Mat canny;
Canny(image, canny, 30, 100);
几何形状的检测和拟合
通过一些列处理后,我们可以实现更为实用的功能了,从图片中拿到几何形状。
轮廓提取
使用二值图做轮廓提取获得物体轮廓点集是一种常用手段。可以使用灰度图但二值图更为常见,来源于阈值化或边缘检测的结果。
vector<vector<Point>> contours; // 存储结果
findContours(image, contours, RETR_CCOMP, CHAIN_APPROX_SIMPLE); // 轮廓提取
drawContours(show, contours, -1, Scalar(255)); // 绘制轮廓
获取结果为白色连通域的边界点集,坐标点值为白色,不同连通域相互独立,形式上构成了二维列表。可用 drawContours 查看提取效果。另外有两个 API 获取周长和面积,可以用于初步筛选,剔除不需要的轮廓。
double area = contourArea(contour);
double length = arcLength(contour, true);
通过轮廓提取或其他方式获取到目标物体关键点集后,就可以进一步做检测或拟合了。
拟合
convexHull(points, hull); // 最小凸包
Rect rect = boundingRect(points); // 最小直立矩形
RotatedRect rect = minAreaRect(points); // 最小旋转矩形
minEnclosingCircle(points, ¢er, &radius); // 最小外包圆
double area = minEnclosingTriangle(points, triangle); // 最小三角形
检测
检测常用霍夫直线检测和霍夫圆检测。以直线检测为例,其原理是将所有直线做成一个表格,然后每个点画出可能过这一点的直线,在表格上投票。那么共线的点越多,这条线所得的票数就越高,以此来做直线检测。从原理上来说,这是一种精确检测,而实际中的图片往往带有一些干扰,或者干脆带有弧度导致检测失败。我所希望的是以某种概率大致拟合出直线。这个方向个人还在探索中。
模板匹配
对于特定图像,如定位点标记,更为方便的检测方位为模板匹配。简单直白,在图像上做滑窗,比较窗口内容和给定模板的相似度作为输出结果,选出相似度最高的窗口即为定位点。
Mat match; // 保存匹配结果
matchTemplate(image, template_image, match, TM_SQDIFF_NORMED);