OpenCV学习心得——基础篇——滤波与卷积——阈值化与平滑
FOR THE SIGMA
FOR THE GTINDER
FOR THE ROBOMASTER
这一系列的学习心得第一轮将参考《学习OpenCV3》一书
操作系统版本:Ubuntu16.04(在这里博主在Linux下进行运行的)
http://www.ubuntu.org.cn/download/desktop 桌面版ubuntu16.04 下载
电子版书籍下载地址
暂无资源
自适应阈值是否跟动态阈值一样?
在我们学习完前面(主要是一些core组件的基础)的后基本了解了如何绘制一个图像,并且将这个图像通过新建一个窗口并显示在这个窗口里面。现在我们将继续了解一下更高级的图像处理方法(了解并熟悉imgproc组件),在这里,需要说明的是,这种处理方法不仅仅是对图像的某个像素的值进行简单的改变,在“滤波与卷积”中的图像处理将根据图像像素之间的联系对之进行高级处理。
重要基础点有以下:
第一个是滤波器(核1)在opencv中的具体运用。
第二个是oepncv在图像的边缘以及图像边界之外的区域如何调用滤波器或其他的方法。
滤波器指的是一种由一幅图像 I(x,y)根据像素点x,y附近的区域计算得到一幅新图像 I’(x,y)的算法。其中,模板规定了滤波器的形状以及这个区域内像素的值的组成规律,也称“滤波器”或“核”。在下面的介绍中多采用的是线性核,即 I’(x,y)的像素的值由 I(x,y)及其周围的像素的值的加权相加得来的。可由以下方程表示:
这个方程的含义是:对任意形状的一个核(5x5),我们需要对核内所有的点i,j做加权,然后相加,对于每一对i,j(通常表现为卷积核中的一个点,我们需要将其点乘原图像中相对于像素点x,y做一个偏移i,j之后的像素点的值后相加。矩阵I的形状为“核的支集2”。尽管“卷积”这个术语在计算机视觉里面只是用来概况滤波在实体图像中的应用而被偶尔提及,但是可以确定的是卷积就是基于线性核的滤波。
我们可用数组来可视化一个核
![]() |
![]() |
![]() |
![]() |
---|---|---|---|
5×5盒状核 | 规范化的5×5盒状核 | 3×3的Sobel核 | 5×5规范化高斯核 |
需要注意的是中间被用粗体标记的是锚点。
所谓的锚点就是定义了核与源图像的对齐关系,例如上面表格中的5×5规范化高斯核,用于计算I‘(x,y)的累加项中,第一个项就是41/273与I(x,y)相乘的结果
在处理图像时有别于oepncv中的滤波操作(如cv::blur( ),cv::erode( ),cv::dilate( )等),我们将通过自定义的方式来解决缺少相邻像素点的边缘像素点计算出一个有效的结果。
自定义边框
在处理图像时,重要告诉要调用的函数添加虚拟像素的规则,库函数便会自动创建对应的虚拟像素。
cv;:copyMakeBorder( )就是一个为图像创建边框的函数,通过指定两幅图像(第一幅为原图像,第二幅为扩展后的图像),需要注意的是这个函数会将第一幅原图像填补后的结果保存在第二幅图像中。
void cv::copyMakeBorder(
cv::InputArray src, //输入的源图像
cv::OutputArray dst, //输出的图像
int top, //边框上方的尺寸
int bottom, //边框下方的尺寸
int left, //边框左方的尺寸
int right, //边框右方的尺寸
int borderType, //像素填充的方式,见下表格
const cv::Scalar& value = cv::Scalar()
);
边框类型 | 效果 |
---|---|
cv::BORDER_CONSTANT | 复制指定的常量扩展边界 |
cv::BORDER_WRAP | 复制对边的像素扩展边界 |
cv::BORDER_REPLICATE | 复制边缘的像素扩展边界 |
cv::BORDER_REFLECT | 通过镜像复制扩展边界 |
cv::BORDER_REFLECT_101 | 通过镜像复制扩展边界,边界像素除外 |
cv::BORDER_DEFAULT | cv;:BORDER_REFLECT_101的别名 |
自定义外推
在某些情况下,我们需要计算某一特定像素所参考的像素的位置。比如,给定一幅宽为w、高为h的图像,我们需要知道哪个像素为虚拟像素赋值,可以使用cv::borderInterpolate( );
int cv::borderInterpolate(
int p, //输入的坐标p
int len, //关联图像的实际大小
int borderType //边界类型
);
举个例子:在混合的边界条件下计算一个特定像素的值,在一维中使用BORDER_REFLECT_101,在另一维度中使用BORDER_WRAP:
float val = img.at(
cv::borderInterpolate(100, img.rows, cv::BORDER_REFLECT_101),
cv::borderInterpolate(-5, img.cols, cv::BORDER_WRAP)
)
注意:borderInterpolate函数一次计算一个维度上的外推
在图像处理过程中经常会遇到已经完成了多层处理步骤并需要做出一个最终决定,或是将高于或低于某一值的像素置零同时其他的像素保存不变。
cv::threshold( )
主要作用为对单通道数组进行固定阈值处理,其经典运用为灰度图像进行阈值操作得到二值图像,或是去除噪声,例如过滤很大或者很小像素值的图像点。
其原理是通过对数组每一个值,根据高低于一个特定值来做相应的处理,给定一个数组和阈值。
double cv::threshold(
cv::InputArray src, //输入数组,填单通道,8或32位浮点类型的Mat即可
cv::OutputArray dst, //存放输出的结果
double thresh,
double maxValue,
int thresholdType
};
阈值类型 | 操作 |
---|---|
cv::THRESH_BINARY | DST = (SRC>thresh)?MAXVALUE:0 |
cv::THRESH_BINARY_INV | DST = (SRC>thresh)?0:MAXVALUE |
cv::THRESH_TRUNC | DST = (SRC>thresh)?THRESH:SRC |
cv::THRESH_TOZER0 | DST = (SRC>thresh)?SRC:0 |
cv::THRESH_TOZER0_INV | DST = (SRC>thresh)?0:SRC |
cv::THRESH_OTSU |
#include
#include
using namespace std;
void sum_rgb(const cv::Mat& src, cv::Mat& dst) {
// 分通道
vector planes;
cv::split(src, planes);
//BGR三通道
cv::Mat b = planes[0];
cv::Mat g = planes[1];
cv::Mat r = planes[2];
cv::Mat s;
// 加权融合,防止越界
cv::addWeighted(r, 1. / 3, g, 1. / 3, 0.0, s);
cv::addWeighted(s, 1., r, 1. / 3, 0.0, s);
// 阈值化操作
cv::threshold(s, dst, 100, 100, cv::THRESH_TRUNC);
}
void help() {
cout << "Call: ./ch10_ex10_1 faceScene.jpg" << endl;
cout << "Show use of alpha blending (addWeighted) and threshold" << endl;
}
int main()
{
help();
cv::Mat src = cv::imread("pic.jpg");
cv::Mat dst;
if (src.empty())
{
cout << "can not load the image" << endl;
return -1;
}
sum_rgb(src, dst);
cv::imshow("img", dst);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
可以得出一张灰度图像
Otsu算法
又被称为全局阈值二值化函数或大律法。该方法在类间方差最大的情况下是最佳的,就图像的灰度值而言,OTSU给出最好的类间分离的阈值。
原理就是将所有可能的阈值全部都遍历一次,然后对每个阈值结果的二类像素计算差(即低于阈值和高于阈值的两类像素)
式中,w_1(t)和w_2(t)是根据两类像素的数量计算而来的权重。由于要遍历所有可能的阈值,所以这并不是一个相对高效的过程。
示例:
/* otsu_2d:二维最大类间方差阈值分割的快速迭代算法 吴一全 */
#include
#include
#include
using namespace std;
double TwoDimentionOtsu(IplImage *image);
int main()
{
IplImage* srcImage = cvLoadImage( "pic.jpg",0 );
assert(NULL != srcImage);
cvNamedWindow("src",0);
cvShowImage("src",srcImage);
clock_t start_time=clock();
//计算最佳阈值
double threshold = TwoDimentionOtsu(srcImage);//70,125
clock_t end_time=clock();
cout<< "Running time is: "<(end_time-start_time)/CLOCKS_PER_SEC*1000<<"ms"<width;
int height = image->height;
double dHistogram[256][256]={0.0}; //建立二维灰度直方图
unsigned long sum0 = 0,sum1 = 0; //sum0记录所有的像素值的总和,sum1记录3x3窗口的均值的总和
uchar* data = (uchar*)image->imageData;
for(int i=0; iwidthStep + j];//nData1记录当前点(i,j)的像素值
sum0 += nData1;
unsigned char nData2 = 0; //nData2记录以当前点(i,j)为中心三领域像素值的平均值
int nData3 = 0; //nData3记录以当前点(i,j)为中心三领域像素值之和,注意9个值相加可能超过一个字节
for(int m=i-1; m<=i+1; m++)
{
for(int n=j-1; n<=j+1; n++)
{
if((m>=0)&&(m=0)&&(nwidthStep + n];
}
}
nData2 = (unsigned char)(nData3/9); //对于越界的索引值进行补零,邻域均值
sum1 += nData2;
dHistogram[nData1][nData2]++;
}
}
long N = height*width; //总像素数
t = sum0/N; //图像灰度级均值
s = sum1/N; //邻域平均灰度级的均值
s0 = s;
t0 = t;
for(int j=0; j<256; j++)
for(int i=0; i<256; i++)
{
dHistogram[i][j] = dHistogram[i][j]/N; //得到归一化的概率分布
}
double w0 = 0.0,w1 = 0.0,u0i = 0.0,u1i = 0.0,u0j = 0.0,u1j = 0.0;
do
{
t0 = t;
s0 = s;
w0 = w1 = u0i = u1i = u0j = u1j = 0.0;
for (int i = 0,j; i < 256; i++)
{
for (j = 0; j < s0; j++)
{
w0 += dHistogram[i][j];
u0j += dHistogram[i][j] * j;
}
for (; j < 256; j++)
{
w1 += dHistogram[i][j];
u1j += dHistogram[i][j] * j;
}
}
for (int j = 0,i = 0; j < 256; j++)
{
for (i = 0; i < t0; i++)
u0i += dHistogram[i][j] * i;
for (; i < 256; i++)
u1i += dHistogram[i][j] * i;
}
u0i /= w0;
u1i /= w1 ;
u0j /= w0;
u1j /= w1;
t = (u0i + u1i)/2;
s = (u0j + u1j)/2;
}while ( t != t0);//是否可以用这个做为判断条件,有待考究,请高手指点
return t;//只用t做为阈值,个人也感觉不妥,但没有找到更好的方法,请高手指点
}
该算法由吴一全老师提出了快速迭代优化型,(知网)论文网址:
http://www.cnki.com.cn/Article/CJFDTotal-ZTSX200703014.htm
http://www.cnki.com.cn/Article/CJFDTOTAL-DZIY201103006.htm
自适应阈值
有一种与之前不同的阈值化方法,这种方法中阈值在整个过程中自动产生变化,在opencv中通过cv::adaptiveThreshold来实现:
void cv::adaptiveThreshold(
cv::InputArray src, //源图像 8位单通道浮点型图像
cv::OutputArray dst,
double ,maxValue, //给像素赋的满足条件的非零值
int adaptiveMethod, //使用的自适应阈值算法:有ADAPTIVE_THRESH_MEAN_C 与 ADAPTIVE_THRESH_GAUSSIAN_C
int thresholdType, //阈值类型
int blockSize, //计算阈值大小的一个像素的领域尺寸,取值3\5\7等
double C //减去平均或加权平均值后的参数值
);
该方法是逐个像素地计算自适应阈值T(x, y),具体计算过程是:计算每个像素位置周围的blockSize×blockSize区域的加权平均值,然后减去常数C。求均值时所用权重和adaptiveMethod有关,若是cv::ADAPTIVE_THRESH_MEAN_C,则权重相等,若是cv::ADAPTIVE_THRESH_GAUSSIAN_C,则权重由高斯方差得到。
注:该方法只适应与单通道8位或浮点型图像
实例:
#include
#include
using namespace std;
int main()
{
// 设置参数
double fixed_threshold = 15;
int threshold_type = 1 ? cv::THRESH_BINARY : cv::THRESH_BINARY_INV;
int adaptive_method = 1 ? cv::ADAPTIVE_THRESH_MEAN_C : cv::ADAPTIVE_THRESH_GAUSSIAN_C;
int block_size = 71;
double offset = 15;
// 以灰度图形式加载图片
cv::Mat Igray = cv::imread("pic.jpg", cv::IMREAD_GRAYSCALE);
// 判断图像是否加载成功
if (Igray.empty()) {
cout << "Can not load " << "pic.jpg" << endl;
return -1;
}
// 声明输出矩阵
cv::Mat It, Iat;
// 阈值化操作
cv::threshold(
Igray,
It,
fixed_threshold,
255,
threshold_type);
// 自适应阈值
cv::adaptiveThreshold(
Igray,
Iat,
255,
adaptive_method,
threshold_type,
block_size,
offset
);
// 展示结果图像
cv::imshow("Raw", Igray);
cv::imshow("Threshold", It);
cv::imshow("Adaptive Threshold", Iat);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
参考:https://blog.csdn.net/momo026/article/details/84026075
平滑也称“模糊”,是一种简单而又常用的图像处理操作。平滑图像的目的有很多,但通常都是为了减少噪声和伪影。在降低图像分辨率的时候,平滑也十分重要,可以防止图片出现锯齿状。
opencv中提供了5种不同的平滑操作。
简单模糊与方框型滤波器
void cv::blur(
cv::InputArray src,
cv::OutputArray dst,
cv::Size ksize, //窗口的尺寸
cv::Point anchor = cv::Point(-1,-1), //指定计算时核与原图像的对齐方式(默认为Point(-1,-1) )
int borderType = cv::BORDER_DEFAULT
);
cv::blur( )实现了简单模糊,目标图像中的每个值都是源图像中相对应位置一个窗口(核)中像素的平均值。
void cv::boxFilter(
InputArray src,
OutputArray dst,
cv::Size ksize,
cv::Point anchor=cv::Point(-1,-1),
bool normalize=true;
int borderType=cv::BORDER_DEFAULT
);
方框滤波器是一种矩形的并且滤波器中所有值k_{i,: j}全部相等的滤波器。通常,所有的k_{i,: j}为1或者1/A,其中A是滤波器的面积。后一种滤波器称为“归一化方框型滤波器”,下面所示的是一个5×5的模糊滤波器,也称“归一化方框型滤波器”。
这两个函数前者是一种特殊化的形式,后者则是一般化的形式,区别在于后者可以以非归一化形式调用,并且输出图像深度可以控制
中值滤波
中值滤波器将每个像素替换为围绕这个像素的矩形领域内的中值或“中值”像素(相对于平均像素)。通过均值滤波器对噪声图像,尤其是有较大孤立的异常值非常敏感,少量具有较大偏差的点也会严重影响到均值滤波器。中值滤波器可以采用取其中间点的方式来消除异常值。
void cv::medianBlur(
cv::InputArray src, //源数组
cv::OutputArray dst, //目标数组
cv::Size ksize //内核大小
);
高斯滤波
void cv::GaussianBlur(
cv::InputArray src, //源图像
cv::OutputArray dst, //目标图像
cv::Size ksize, //高斯内核的大小
double sigmaX, //高斯核函数在X方向的的标准偏差
double sigmaY = 0.0 //高斯核函数在Y方向的的标准偏差,若Y为零,设为X,若X和Y都是0,由ksize.width和ksize.height计算出来
int borderType = cv::BORDER_DEFAULT
};
实例:
#include
int main()
{
// 创建两个窗口,分别显示输入和输出的图像
cv::namedWindow("Example2-5_in", cv::WINDOW_AUTOSIZE);
cv::namedWindow("Example2-5_out", cv::WINDOW_AUTOSIZE);
// 读取图像,并用输入的窗口显示输入图像
cv::Mat img = cv::imread("C:\\Users\\Bello\\Desktop\\test.jpg", -1);
cv::imshow("Example2-5_in", img);
// 声明输出矩阵
cv::Mat out;
// 进行平滑操作,可以使用GaussianBlur()、blur()、medianBlur()或bilateralFilter()
// 此处共进行了两次模糊操作
cv::GaussianBlur(img, out, cv::Size(5, 5), 3, 3);
cv::GaussianBlur(out, out, cv::Size(5, 5), 3, 3);
// 在输出窗口显示输出图像
cv::imshow("Example2-5_out", out);
// 等待键盘事件
cv::waitKey(0);
// 关闭窗口并释放相关联的内存空间
cv::destroyAllWindows();
return 0;
}
参考:https://blog.csdn.net/godadream/article/details/81568844
双边滤波器
void cv::bilateralFilter(
cv::InputArray src,
cv::OutputArray dst,
int d, //像素领域的直径
double sigmaColor, //颜色空间滤波器的sigma值
double sigmaSpace, //坐标空间中滤波器的sigma值
int borderType = cv::BORDER_DEFAULT
);
双边滤波器是一种比较大的图像分析算子,也就是边缘保持平滑。高斯平滑的模糊过程是减缓像素在空间上的变化,因此与邻近的关系紧密,而随机噪声在像素间的变化幅度又会非常的大(即噪声不是空间相关的)。基于这种前提高斯平滑很好地减弱了噪声并且保留了小信号,但是却破坏了边缘信息,边缘也模糊了。
和高斯平滑类似,双边滤波对每个像素及其领域内的像素进行了加权平均。其权重由两部分组成,第一部分同高斯平滑,第二部分也是高斯权重,但是它不是基于空间距离而是色彩强度差计算而来的,在多通道(色彩)图像上强度差由各分量的加权累加代替。可将其当做高斯平滑,指示相似程度更高的像素权值更高,边缘更加明显,对比度更高。
一般来说信号处理领域通常倾向称呼为滤波,而在数学领域则称呼为核。 ↩︎
核的支集由核的非零部分组成。 ↩︎