Opencv 入门
一、环境搭建
Window下环境的搭建分为以下几个步骤:(opencv3.4+vs2017)
- 官网下载opencv压缩包
- 解压之后找到压缩目录(我的是:D:\opencv\),设置环境变量:D:\opencv\build\x64\vc15\bin
- 打开vs新建空白项目,找到:视图-属性管理器
- Debug|x64下的Microsoft.Cpp.x64.user右键选择属性
- 配置属性“VC++目录”,包含目录,添加3个目录,分别为
- D:\opencv\build\include
- D:\opencv\build\include\opencv
- D:\opencv\build\include\opencv2
- 同样是“VC++目录”下,库目录,添加1个目录:D:\opencv\build\x64\vc15\lib
- 链接器,输入,附加依赖项,添加:opencv_world340d.lib
- 确定
测试代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
int main(int argc, char** argv) {
Mat src = imread("D:/dehaze_image/haze0.jpg");
if (src.empty()) {
printf("could't load image..\n");
return -1;
}
namedWindow("test opencv setup", CV_WINDOW_AUTOSIZE);
imshow("test opencv setup", src);
waitKey(0);
return 0;
}
// 解决方案要选择,Debug,x64
二、加载、修改、保存图片
加载:Mat imread( const String& filename, int flags = IMREAD_COLOR )
转换颜色类型:void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 )
保存图片:bool imwrite( const String& filename, InputArray img,const std::vector<int>& params = std::vector<int>())
/*2.图像的输入,修改,保存*/
void section2_io() {
/*
图像输入,第一个参数为图像路径,第二个参数为图像类型IMREAD_XXX
默认情况下,以IMREAD_COLOR加载,表示RGB彩色图像,可以通过该参数制定加载为什么类型,如灰度图等
imread可以加载jpg、png、tif等常见图片格式
*/
Mat src = imread("D:/dehaze_image/lena.jpg",IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return ;
}
// 原图
imshow("original", src);
Mat output;
/*
cvtColor,convert color,图片颜色类型转换函数,用于不同颜色类型的转换
三个参数分别为,input,output,和转换参数CV_XXX2XXX
*/
cvtColor(src, output, CV_BGR2GRAY);
// 转换之后的图
imshow("gray_img", output);
/*
保存转换之后的图片
参数分别为,保存路径,输出图像的array,参数vector
不指定参数时,会根据保存的后缀名自动适配保存类型
*/
imwrite("D:/dehaze_image/lena_gray.jpg", output);
// pause
waitKey(0);
}
三、矩阵的掩膜(mask)操作
0、什么是掩膜操作?(这里涉及部分数字图像处理的原理的自我理解——可能存在问题)
掩膜操作即图像的模板卷积操作,实际就是乘加的操作。卷积定理有:函数卷积的傅里叶变换是函数傅里叶变换的乘积。即,一个域中的卷积相当于另一个域中的乘积,例如时域中的卷积就对应于频域中的乘积,卷积定理表达如下:(代码并没有用到这个,但是卷积和傅里叶具有天然联系,需要了解)
为什么模板操作具有例如平滑、去噪等功能?
- 在时域来讲,模板操作是一种权值分配的计算,不同算子体现了不同的权重思想,相应得取得了不同的效果。
- 在频域来讲,模板卷积,实际相当于频域的滤波(用一个滤波信号,遍历于每一个图块相乘),过滤图块中的特定频率,而图像的频域分析已知,高频部分对应图像的纹理和细节,低频部分对应图像的大体轮廓,故不同的滤波对高频和低频的处理不同,产生不同的效果。
1、像素操作
uchar * rowVecter = Mat.ptr<uchar>(rowIndex);//此处uchar为泛型类型的实类型
Mat提供了获取一个行像素的方法ptr<类型>(行号)。
2、矩阵模型
Mat表示图像的像素矩阵,行表示每一行像素,但列比较特殊,列是每一个像素的各通道值依次排列。
即,col_count == mat.cols * mat.channels()。即,假如图像为3通道,则一个像素占据一行的3个列的空间。在像素处理的时候,需要谨记该存储模型。
3、默认的标准化函数(防溢出)
opencv提供了防止数据溢出的函数:saturate_cast< T >(int value),将value规定在图像范围之内,如[0,255],采用的是越界则取边界值的方法(大于255则取255,小于0则取0)。
4、opencv库里提供的mask操作,filter2D
Mat mat = Mat::zeros(size,type) // 根据大小和类型构建一个图像的像素矩阵,初始化为0
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);// 构建一个特定的模板算子
filter2D(src, dst, src.depth(), kernel);//模板操作,第三个参数可用-1,表示深度和输入图像一致
5、计时器:计算运行时间
两个函数:int64 t = getTickCount()//获取时间
,(t2-t1)/getTickFrequency();//将tickcount转换为具体的时间,单位为秒,因为getTickFrequency的物理意义为每秒有多少个Tick
。
6、代码样例
/*
3.图像掩膜操作,图像的像素级操作
掩膜,即mask操作,模板操作。
此处采用的模板(算子)如下
|0 -1 0 |
|-1 5 -1|
|0 -1 0 |
该算子作用是:提高对比度,中间点的权值高,4邻域为负权值
*/
void section3_1() {
Mat src = imread("D:/dehaze_image/haze0.jpg", IMREAD_COLOR);
if (!src.data) {
printf("could't load image..\n");
return;
}
// 显示原图
namedWindow("original img", CV_WINDOW_AUTOSIZE);
imshow("original img", src);
// 做掩膜操作
// 1.得出行列数:
// 注意1:这里图像矩阵mat的排列时,每一行的各像素的各通道值是依次排列的
// 如RGB图像的矩阵某一行是这样的内容:(i0_b,i0_g,i0_r, i1_b,i1_g,i1_r, i2_b,i2_g,i2_r)
// 注意2:模板操作的时候,需要处理边界,此处采用的处理方案是,边界不进行模板操作,故col和row需要减去边界
// 注意3:cols和rows是从0开始计算的,这个和数组是一样的
int cols_end = (src.cols - 1)*src.channels();
int rows_end = src.rows-1;
int col_offset = src.channels();
// 根据输入图像的大小和类型,初始化一个输出图像的矩阵
Mat out = Mat::zeros(src.size(), src.type());
int64 t1 = getTickCount();
for (int row = 1; row < rows_end; row++) {
// 获取需要处理的三个行向量
const uchar* currentRow = src.ptr<uchar>(row);
const uchar* preRow = src.ptr<uchar>(row - 1);
const uchar* nextRow = src.ptr<uchar>(row + 1);
uchar * outrow = out.ptr<uchar>(row);
for (int col = col_offset; col < cols_end; col ++) {
// 掩膜操作,就是卷积操作,对模板的乘加操作
outrow[col] = saturate_cast<uchar>(5*currentRow[col]-preRow[col]-nextRow[col]-currentRow[col-col_offset]-currentRow[col+col_offset]);
// 此处需要判断outrow[col]的值越界吗?超过255,显然是需要的,opencv提供了该函数,将值定在区间[0,255]内
// outrow[col] = saturate_cast<uchar>(outrow[col]);
// 上行代码是错误的,必须在原计算值上saturate处理
// 原理:outrow的类型根据src.type,故存储值应该是255之内的。若计算值超出,会溢出,
// 默认的处理溢出的策略是取低8位,这就导致了值异常,而正常的处理是超出255的取255,小于0的取0.
// 故,outrow[col] = saturate_cast<uchar>(outrow[col])根本是无效的,outrow已经采取过计算机默认的溢出处理
}
}
int64 t2 = getTickCount();
printf("section3_1 time consume:%.2f\n", (t2 - t1) / getTickFrequency());
// 输出mask之后图像
namedWindow("masked image", CV_WINDOW_AUTOSIZE);
imshow("masked image", out);
waitKey(0);
}
/*opencv库提供的掩膜操作*/
void section3_2() {
Mat src = imread("D:/dehaze_image/haze0.jpg", IMREAD_COLOR);
if (!src.data) {
printf("could't load image..\n");
return;
}
// 显示原图
namedWindow("original img2", CV_WINDOW_AUTOSIZE);
imshow("original img2", src);
// 输出目的
Mat dst = Mat::zeros(src.size(), src.type());
// 模板算子
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
int64 t1 = getTickCount();
// 库提供的模板卷积操作,第三个参数为深度,在不知道的时候可用-1,表示与输入图像相同的深度
filter2D(src, dst, src.depth(), kernel);
int64 t2 = getTickCount();
printf("section3_2 time consume:%.2f\n", (t2 - t1) / getTickFrequency());
// 输出mask之后图像
namedWindow("masked image2", CV_WINDOW_AUTOSIZE);
imshow("masked image2", dst);
waitKey(0);
}
四、Mat对象
- Mat对象和IplImage对象:IplImage是最初的C风格的数据结构,开发者管理内存,容易造成内存泄漏。Mat是后续版本Opencv提供的图像数据结构,自动进行内存管理,面向对象的数据结构。包含头部与数据两个部分。之后的使用都推荐使用Mat对象。
Mat常用方法:
- Mat a; Mat b(a); // 此时拷贝构造,只复制Mat的头和指针部分内容,不会复制数据部分。
- a.clone() or a.copyTo() // 完全复制
输出图像的内存是自动分配的
- 使用opencv的c++接口,不需要考虑内存分配问题
- 复制操作和拷贝构造函数只会复制头部分信息
- clone和copyTo两个函数实现数据完全复制
- 定义数组:
- 高维数组(很少用到):
- 定义小数组(模板,kernel):Mat kernel = (Mat_(rows,cols) << value1,value2….)
- 零矩阵:Mat::zeros()
- 单位矩阵:Mat::eye()
/*Mat的使用*/
void section4_mat()
{
Mat src = imread("D:/dehaze_image/haze1.jpg", IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// Mat dst = Mat(src.size(), src.type()); // 创建一个空的Mat对象
// dst = Scalar(255, 0, 0); // 赋初值,scalar三个参数分别为BGR
// Mat dst = src.clone(); // Clone方法完全复制一个Mat对象
Mat dst;
// src.copyTo(dst);
cvtColor(src, dst, CV_BGR2GRAY);
namedWindow("output", CV_WINDOW_AUTOSIZE);
imshow("output", dst);
// 采用构造: Mat(rols,cols,type,&scaler)
Mat m(3, 3, CV_8UC3, Scalar(0, 255, 0));
cout << "m = " << endl;
cout << m << endl;
// 另一种,create方式
Mat mat;
mat.create(src.size(), src.type());
mat = Scalar(0, 255, 0);
imshow("mat", mat);
// 定义模板、kernel,小的二维数组
Mat k = (Mat_<int>(3, 3) << 0, 0, 0, 1, 1, 1, 0, 0, 0);
cout << k << endl;
// 单位矩阵eye,零矩阵Mat::zeros(rows,cols,type)
Mat e = Mat::eye(3, 3, CV_8UC1);
cout << e << endl;
cout << "src channel:" << src.channels() << ",size:" << src.size() << endl;
cout << "dst channel:" << dst.channels() << ",size:" << dst.size() << endl;
waitKey(0);
}
五、图像操作
- 读写图像:imread、imwrite
- 读写像素 : mat.at、mat.ptr指针操作
/*5.像素操作,单通道*/
void section5_pixel_1c()
{
Mat src, gray_src;
src = imread("D:/dehaze_image/haze2.jpg", IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return;
}
// mat.at<type>(row_index,col_index)方式访问单通道像素
// 由于是单通道,所以需要先转换为灰度图
cvtColor(src, gray_src, CV_BGR2GRAY);
// 输出原灰度图
imshow("original gray", gray_src);
for(int i = 0; i < gray_src.rows; i++) {
for (int j = 0; j < gray_src.cols; j++) {
uchar value = gray_src.at<uchar>(i, j);
// mat.at函数可以原地赋值操作
gray_src.at<uchar>(i, j) = 255 - value; // 反色操作
}
}
// 反色之后的灰度图
// opencv默认在没有namedwindow时候,自动创建显示窗口,当然它会先去寻找同名的窗口(参数一)
imshow("processed gray", gray_src);
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
waitKey(0);
}
/*5.像素操作,多通道*/
void section5_pixel_mc()
{
Mat src, dst;
src = imread("D:/dehaze_image/haze2.jpg", IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return;
}
dst.create(src.size(), src.type());
dst = Scalar(0, 0, 255);
// 输出原图
imshow("original", src);
// mat.at<数组类型>(row_index,col_index)方式访问多通道像素
int channels = src.channels();
for (int i = 0; i < src.rows; i++) {
for (int j = 0; j < src.cols; j++) {
// typedef Vec<uchar, 3> Vec3b;
// 注意左边的类型应该是引用类型&,才能原地操作
// 这里代码并不能通用各种通道,因为Vec3b指定了通道数为3
Vec3b & values = src.at<Vec3b>(i, j);
Vec3b & dst_values = dst.at<Vec3b>(i, j);
// 操作每个通道
for (int c = 0; c < channels; c++) {
// 反色操作,同样可以原地操作,看mat.at返回的&引用,故可以原地操作
dst_values[c] = 255 - values[c];
}
}
}
imshow("processed", dst);
// opencv内置的同样效果的函数,bitwise_not,内部通过位操作实现
Mat cvdst;
bitwise_not(src, cvdst);
imshow("cv processed", cvdst);
waitKey(0);
}
六、图像混合
- 理论-线性混合: g(x)=(1−a)f1(x)+af2(x) g ( x ) = ( 1 − a ) f 1 ( x ) + a f 2 ( x )
- API:addWeighted
- notice:两个图必须大小相同,类型相同
/*6.图像融合*/
void section6_blend()
{
Mat src1, src2, dst;
src1 = imread("D:/dehaze_image/haze2.jpg", IMREAD_COLOR);
src2 = imread("D:/dehaze_image/hazefree2.jpg", IMREAD_COLOR);
imshow("src1", src1);
imshow("src2", src2);
// opencv内置 权重混合函数
if (src1.size == src2.size && src1.type() == src2.type()) {
//addWeighted(src1, 0.5, src2, 0.5, 1, dst);
//imshow("blend", dst);
}
// 像素实现,采用指针访问
if (src1.size == src2.size && src1.type() == src2.type()) {
dst = Mat::zeros(src1.size(), src1.type());
for (int i = 0; i < src1.rows; i++) {
uchar * row_src1_ptr = src1.ptr<uchar>(i);
uchar * row_src2_ptr = src2.ptr<uchar>(i);
uchar * row_dst_ptr = dst.ptr<uchar>(i);
for (int j = 0; j < src1.cols*src1.channels(); j++)
{
row_dst_ptr[j] = row_src1_ptr[j] * 0.5 + row_src2_ptr[j] * 0.5;
}
}
imshow("blend", dst);
}
else {
// 大小类型不同,需要转换
}
waitKey(0);
}
七、图像亮度与对比度
- 理论: g(x)=af(x)+b g ( x ) = a f ( x ) + b , fx是输入图像,b是对输入图像的整体亮度平移,效果是亮度的增大或减少,亮度差也就是对比度不变,而a是对亮度的线性扩大或缩小,会产生亮度值的相对差的改变引起对比度变化。
- api:像素操作,然后通过saturate_cast进行区间限定。
/*7.图像亮度与对比度*/
void section7()
{
Mat src, dst;
src = imread("D:/dehaze_image/haze2.jpg", IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return;
}
imshow("src1", src);
dst = Mat::zeros(src.size(), src.type());
float a = 1.5;
float b = 0;
for (int i = 0; i < src.rows; i++) {
// 这里用指针ptr操作,也可以直接用at函数操作
uchar * row_src_ptr = src.ptr<uchar>(i);
uchar * row_dst_ptr = dst.ptr<uchar>(i);
for (int j = 0; j < src.cols*src.channels(); j++)
{
row_dst_ptr[j] = saturate_cast<uchar>(row_src_ptr[j] * a + b);
}
}
imshow("processed", dst);
waitKey(0);
}
八、绘制图形和文字
cv::Point表示2d平面上的点
cv::Scalar表示四个元素的向量,最后一个元素默认为0,前三个表示三个通道值bgr
绘图API
- cv::line(LINE_4\LINE_8\LINE_AA)
- cv::ellipse,椭圆
- cv::rectangle,矩形,cv::Rect
- cv::circle,画圆
- cv::fillPoly,填充多边形,绘制多边形cv::polylines
/*8.绘制图像和文字*/
void section8()
{
Mat src;
src = imread("D:/workspace/image/haze1.jpg", IMREAD_COLOR);
if (src.empty()) {
printf("could't load image..\n");
return;
}
// 画线
Point p1(30, 50);
Point p2;
p2.x = 300;
p2.y = 400;
Scalar col_red(0, 0, 255);
line(src, p1, p2, col_red, 2, LINE_AA); // line_aa,反锯齿
// rectangle
Rect rect = Rect(p1, p2); // 常用两对角点,或者左上点+长宽构造
Scalar col_blue(255, 0);
rectangle(src, rect, col_blue, 3, LINE_8);
// ellipse,采用这个RotateRect的3个point的构造需要注意三个顶点必须能构成矩形。
// 这里没有用LINE_AA,明显出现锯齿
ellipse(src, RotatedRect( Point(10,20), Point(10,300),Point(200,300)), Scalar(0, 255), 3,LINE_8);
// 参数:图板,中心,长短轴,倾斜角度,开始角度,中止角度(用于画弧),颜色
ellipse(src, Point(200,200),Size(200,150),45,0,180, Scalar(50, 200, 200), 2, LINE_AA);
// circle
circle(src, Point(300, 300), 80, col_red, 3, LINE_AA);
// 多边形
Point pts[1][5] = {Point(0,0),Point(200,120),Point(120,180),Point(180,60),Point(60,50)};
const Point * ppts[] = { pts[0] }; // point指针的数组,显然这个数组元素只有1个
const int ptCount = 5;
fillPoly(src, ppts, &ptCount, 1 ,col_blue, LINE_8 ,0);
// 多边形轮廓,这里文档的第二个参数有点怪怪……
polylines(src, ppts,&ptCount,1, true, col_red, 2, LINE_AA);
// 输出文本框
putText(src, String("Hello World!"), Point(400, 100), CV_FONT_BLACK, 1.0, col_blue, 2, LINE_AA);
// SHOW
namedWindow("draw", CV_WINDOW_AUTOSIZE);
imshow("draw", src);
waitKey(0);
// 随机画线效果
RNG randn = RNG(12345);
Mat m = Mat::zeros(src.size(), src.type());
Point p3, p4;
namedWindow("randomLines", CV_WINDOW_AUTOSIZE);
while (1) {
p3.x = randn.uniform(0, m.cols);
p4.x = randn.uniform(0, m.cols);
p3.y = randn.uniform(0, m.rows);
p4.y = randn.uniform(0, m.rows);
Scalar color(randn.uniform(0, 255), randn.uniform(0, 255), randn.uniform(0,255));
line(m, p3, p4, color, randn.uniform(0, 3), LINE_AA);
imshow("randomLines", m);
if (waitKey(50) > 0) {
break;
}
}
}
九、图像模糊
卷积操作,用一些线性滤波算子进行图像的模糊、平滑。卷积核也叫kernel,卷积模板。
模糊算法
归一化盒子滤波(均值滤波),kernel元素都为1.blur
高斯滤波:各空间位置权值呈高斯分布。GaussianBlur(src,dst,Window,sigmx,sigmy);//sigmx,sigmy是控制高斯函数形状的
由于权值按高斯分布,对原图的细节保存相对较好,所以平滑效果好。
中值滤波:统计排序滤波器,中值对椒盐噪声具有很好抑制效果(取中值会丢弃最大最小的噪声值)。类似的统计还有最小值、最大值滤波、均值滤波。medianBlur
双边滤波:双边滤波是一种具有边缘保持的平滑滤波器,结合对比度增强、锐化算法应用到人脸美化。bilateralFilter()
/*9.图像模糊*/
void section9()
{
Mat src,dst;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// blur,矩形窗口模糊
blur(src, dst, Size(15, 15), Point(-1, -1));
imshow("output-15*15", dst);
// x轴模糊
blur(src, dst, Size(15, 1), Point(-1, -1));
imshow("output-15*1", dst);
// y轴模糊
blur(src, dst, Size(1, 15), Point(-1, -1));
imshow("output-1*15", dst);
// 高斯模糊
GaussianBlur(src, dst, Size(15, 15), 1);
imshow("gaussianBulr", dst);
waitKey(0);
}
/*中值滤波、高斯双边滤波*/
void section9_1()
{
Mat src, dst;
src = imread("D:/workspace/image/lena_noise.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// 中值滤波,对椒盐噪声效果好
medianBlur(src, dst, 3);
imshow("output-median", dst);
waitKey(0);
// 双边滤波和高斯滤波的对比
Mat src2, dst2;
src2 = imread("D:/workspace/image/woman.jpg", IMREAD_COLOR);
imshow("input", src2);
// 高斯模糊
GaussianBlur(src2, dst2, Size(15, 15), 1);
imshow("output-gaussian", dst2);
// 双边滤波,保留边缘,其他部分平滑
bilateralFilter(src2, dst2, 15, 100, 5);
imshow("bilateral", dst2);
// 双边滤波之后配合对比度增强
Mat kernel = (Mat_<int>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
filter2D(dst2, dst2, -1, kernel, Point(-1, -1));
imshow("bilateral-with-edgEnhance", dst2);
waitKey(0);
}
十、腐蚀与膨胀
图像的形态学操作。
getStructuringElement获取结构元素。
TrackBar:动态的调节滑块。createTrackBar,相当于一种回调机制。
- 腐蚀:最小值替换。erode
- 膨胀:最大值替换。dilate
- 开:morphologyEx,CV_MOP_OPEN,先腐蚀再膨胀
- 闭:morphologyEx,CV_MOP_CLOSE,先膨胀再腐蚀
- 形态学梯度,Morphological Gradient,膨胀减去腐蚀。也叫基本梯度。
- tophat:原图与开操作之后的差值,CV_MOP_TOPHAT
- blackhat:闭操作与原图的差值图像,CV_MOP_BLACKHAT
作用:可以去除图像中小的不需要的对象。
/*10.腐蚀与膨胀*/
void section10()
{
// 局部函数声明:TrackBarCallBack函数
void myCallBack(int ,void*);
Mat src;
src = imread("D:/workspace/image/lena.jpg", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
namedWindow("output", CV_WINDOW_AUTOSIZE);
imshow("input", src);
int element_size = 0;
int max_ele_size = 21;
// 最后两个参数都是默认为0的指针类型
// 回调函数的指针,函数原型规定参数必须为(int,void*),第一个为trackBar的当前值,第二个为用户数据地址。注:函数名就是函数的地址
// 用户数据的指针,void*类型,避免全局变量的使用,用这个参数传递到回调的TrackBarCallBack函数中
createTrackbar("Element Size:", "output", &element_size, max_ele_size, myCallBack , &src);
// 调用一次该函数(用作普通函数调用),让图片显示出来,之后操作显示中的TrackBar会循环再触发该回调函数
myCallBack(element_size, &src);
// 这个waitKey放到callBack之外,否则会出问题
waitKey(0);
}
void myCallBack(int element_size, void* src_) {
int e_size = 2 * element_size + 1;
Mat dst;
Mat * src = static_cast<Mat*>(src_);
// 形态学操作,需要先创建结构元素,参数看函数说明
Mat struct_elmt = getStructuringElement(MorphShapes::MORPH_RECT, Size(e_size, e_size), Point(-1, -1));
// 腐蚀
//erode(*src, dst, struct_elmt,Point(-1,-1));
// 膨胀
dilate(*src, dst, struct_elmt, Point(-1, -1));
// show
imshow("output", dst);
}
/*10.1图像形态学操作:开闭操作*/
void section10_1() {
Mat src,dst;
src = imread("D:/workspace/image/morphology.png", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
namedWindow("output", CV_WINDOW_AUTOSIZE);
imshow("input", src);
int s = 15;
Mat kernel = getStructuringElement(MORPH_RECT, Size(s, s));
// 开
morphologyEx(src, dst, CV_MOP_OPEN, kernel);
imshow("output", dst);
waitKey(0);
// 闭
morphologyEx(src, dst, CV_MOP_CLOSE, kernel);
imshow("output", dst);
waitKey(0);
// 形态学梯度
morphologyEx(src, dst, CV_MOP_GRADIENT, kernel);
imshow("output", dst);
waitKey(0);
// 顶帽
morphologyEx(src, dst, CV_MOP_TOPHAT, kernel);
imshow("output", dst);
waitKey(0);
// 黑帽
morphologyEx(src, dst, CV_MOP_BLACKHAT, kernel);
imshow("output", dst);
waitKey(0);
}
十一、形态学应用案例
1.提取特定的线条
- 加载图片,转换成灰度图,cvtColor
- 将灰度图转换成二值图像。得到二值图像有很多算法。这里采用内置的adaptiveThreshold自适应阈值算法。
- 以127为阈值,或者任意指定阈值,显然效果不好
- 以灰度均值作为阈值,Kittle算法
- 最大类间方差法,OTSU
- 直方图法,找到直方图的两个最高的峰值然后在这两个波峰之间找最小的波谷作为阈值点。
- 得到二值图像之后,考虑如何提取线条,以横线为例,分析横线和竖线的特点。
- 图像底色是白色,值取到最大,255.
- 腐蚀操作——用一个结构元素覆盖在图像上,将覆盖面上最小值赋给锚点。
- 膨胀操作——用结构元素覆盖在图像上,将覆盖面上最大值赋值给锚点。
- 综上,考虑提取直线,就是要消失竖线,用背景(白色)替代竖线,用最大值覆盖竖线的值,显然要膨胀操作。而结构元素用直线,这样能保持住原来的横线。但这个操作之后原来的横线两端会被背景吞噬调一些。所以再用腐蚀操作还原,此时已经没有了竖线,不会有负面影响。
- 实际上就是一个闭操作,就提取了横线(若图片背景为黑色,则相反用开操作)。
- 为了效果更佳,再做一个blur
/*10.2.形态学应用——提取直线*/
void section10_2()
{
Mat src;
src = imread("D:/workspace/image/lines.png", IMREAD_COLOR);
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
// 原图太大,这里为了观察方便,降采样
pyrDown(src, src, Size(src.cols/2,src.rows/2));
imshow("input", src);
// 灰度图
Mat gray;
cvtColor(src,gray,CV_BGR2GRAY);
imshow("gray", gray);
// 二值图
Mat binary;
// 这里用adaptiveThreshould方法总是提取到轮廓,而不是填充的二值图,最终采用threshold方法
//adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -1);
// 由于背景是白色,这里手动设置阈值为250
threshold(gray, binary, 250, 255, THRESH_OTSU);
imshow("binary", binary);
// 构造形态学操作结构元素,提取横线,构造一个横线结构元素
Mat dst;
// 这个和shape和size确定了结构元素的样子,这里size的长度(结构元素横向长度)会影响到字母中的横线是否处理干净
// 这里我的size.width取得很大,否则会残留字母A中得横线部分
Mat kernel = getStructuringElement(MORPH_RECT, Size(51, 1));
//dilate(binary, dst, kernel);
//erode(dst, dst, kernel);
// 事实上,以上两个操作就是闭操作
morphologyEx(binary, dst, CV_MOP_CLOSE, kernel);
// 为了结果更好,再平滑一下
blur(dst, dst, Size(3, 3));
imshow("result", dst);
waitKey(0);
}
2.验证码图中去干扰线
分析:
- 要用白色背景值吞噬线条得值,显然是膨胀操作(取结构元素覆盖范围得最大值赋给锚点),那么结构元素如何定?才能去掉线而留下字母?
- 一种办法:发现字母比线条都宽,那么用一个小点得矩形做结构元素应该是可以达到结果得。这里效果最终有残留,原因是绿色的线条与字母线条宽度很接近,所以结构元素的大小不好调整,一种解决思路是,用上下采样,先上采样,将绿色线条与字母线条的差距放大N倍,这样形态学操作就能很好的根据他们宽度的差异去除线条,之后再下采样回来。需要反复试参数。
/*10.3.形态学应用:验证码去干扰线*/
void section10_3()
{
Mat src;
src = imread("D:/workspace/image/checkcode.png", IMREAD_COLOR);
pyrDown(src, src, Size(src.cols/2, src.rows/2));
if (!src.data) {
printf("load error!");
return;
}
namedWindow("input", CV_WINDOW_AUTOSIZE);
imshow("input", src);
// 灰度图
Mat gray;
cvtColor(src, gray, CV_BGR2GRAY);
imshow("gray", gray);
// 二值图
Mat binary;
// 由于背景是白色,这里手动设置阈值为250
threshold(gray, binary, 250, 255, THRESH_OTSU);
imshow("binary", binary);
// 构造形态学操作结构元素
Mat dst;
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
morphologyEx(binary, dst, CV_MOP_CLOSE, kernel);
// 为了结果更好,再平滑一下
blur(dst, dst, Size(3, 3));
imshow("result", dst);
waitKey(0);
}