数字图像处理OpenCV3 学习笔记
Posted 与光同程
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数字图像处理OpenCV3 学习笔记相关的知识,希望对你有一定的参考价值。
1. 加载保存显示图像
Mat imread( const String& filename, int flags = IMREAD_COLOR );
bool imwrite( const String& filename, InputArray img,
const std::vector<int>& params = std::vector<int>());
void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );
2. 操作图像像素
2.1. 方法1:使用 Mat 中对矩阵元素的地址定位的知识
step[0]:一行字节数
step[1]:一个像素字节数
int main()
{
//新建一个uchar类型的单通道矩阵(grayscale image 灰度图)
Mat m(400, 400, CV_8U, Scalar(0));
for (int col = 0; col < 400; col++)
{
for (int row = 195; row < 205; row++)
{
cout << (int)(*(m.data + m.step[0] * row + m.step[1] * col)) << " ==> ";
//获取第[row,col]个像素点的地址并用 * 符号解析
*(m.data + m.step[0] * row + m.step[1] * col) = 255;
cout << (int)(*(m.data + m.step[0] * row + m.step[1] * col)) << endl;
}
}
imshow("canvas", m);
cvWaitKey();
return 0;
}
2.2. 方法二:使用 Mat::at 函数
int main()
{
Mat img = imread("lena.jpg");
imshow("Lena Original", img);
for (int row = 0; row < img.rows; row++)
{
for (int col = 0; col < img.cols; col++)
{
/* 注意 Mat::at 函数是个模板函数, 需要指明参数类型, 因为这张图是具有红蓝绿三通道的图,
所以它的参数类型可以传递一个 Vec3b, 这是一个存放 3 个 uchar 数据的 Vec(向量). 这里
提供了索引重载, [2]表示的是返回第三个通道, 在这里是 Red 通道, 第一个通道(Blue)用[0]返回 */
if(img.at<Vec3b>(row, col)[2] > 128)
img.at<Vec3b>(row, col) = Vec3b(255, 255, 255);
}
}
imshow("Lena Modified", img);
cvWaitKey();
return 0;
}
2.3. 方法三:使用 Mat 的一个模板子类 Mat_
int main()
{
Mat m(400, 400, CV_8UC3, Scalar(255, 255, 255));
// m2 是 Mat_<Vec3b> 类型的, 因为 m 中元素的类型是 CV_8UC3, 可以用 Vec3b 存储 3 个通道的值
// 注意 Mat_<CV_8UC3> 这种写法是错误的, 因为 CV_8UC3 只是一个宏定义
// #define CV_8UC3 CV_MAKETYPE(CV_8U, 3)
Mat_<Vec3b> m2 = m;
// for 循环画一个红色的实心圆
for (int y = 0; y < m.rows; y++)
{
for (int x = 0; x < m.rows; x++)
{
if (pow(double(x-200), 2) + pow(double(y-200), 2) - 10000.0 < 0.00000000001)
{
// Mat_ 模板类实现了对()的重载, 可以定位到一个像素
m2(x, y) = Vec3b(0, 0, 255);
}
}
}
imshow("Image", m);
cvWaitKey();
return 0;
}
2.4. 方法四:使用 Mat::ptr 模板函数
int main()
{
Mat m(400, 400, CV_8UC3, Scalar(226, 46, 166));
imshow("Before", m);
for (int row = 0; row < m.rows; row++)
{
if (row % 5 == 0)
{
// data 是 uchar* 类型的, m.ptr<uchar>(row) 返回第 row 行数据的首地址
// 需要注意的是该行数据是按顺序存放的,也就是对于一个 3 通道的 Mat, 一个像素有
// 有 3 个通道值, [B,G,R][B,G,R][B,G,R]... 所以一行长度为:
// sizeof(uchar) * m.cols * m.channels() 个字节
uchar* data = m.ptr<uchar>(row);
for (int col = 0; col < m.cols; col++)
{
data[col * 3] = 102; //第row行的第col个像素点的第一个通道值 Blue
data[col * 3 + 1] = 217; // Green
data[col * 3 + 2] = 239; // Red
}
}
}
imshow("After", m);
cout << (int)m.at<Vec3b>(0, 0)[0] << ','; //利用 Fn 1 介绍的方法输出一下像素值到控制台
cout << (int)m.at<Vec3b>(0, 0)[1] << ',';
cout << (int)m.at<Vec3b>(0, 0)[2] << endl;
cvWaitKey();
return 0;
}
3. 图像掩模
/*
* @Descripttion:
* @version:
* @Author: Yueyang
* @email: 1700695611@qq.com
* @Date: 2021-05-02 23:45:24
* @LastEditors: Yueyang
* @LastEditTime: 2021-05-03 00:50:40
*/
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
int main()
{
Mat srcImage,dstImage;
srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
//if(!srcImage.data){cout<<"load error"<<endl;}
// int cols=(srcImage.cols-1)*srcImage.channels();
// int rows=srcImage.rows;
// int offset=srcImage.channels();
// dstImage=Mat::zeros(srcImage.size(),srcImage.type());
// for(int row=1;row<rows-1;row++)
// {
// const uchar* priviousptr=srcImage.ptr<uchar>(row-1);
// const uchar* currentptr =srcImage.ptr<uchar>(row);
// const uchar* nextptr =srcImage.ptr<uchar>(row+1);
// uchar* outptr =dstImage.ptr<uchar>(row);
// /**
// * 相当于卷积
// * -1
// * -1 5 -1
// * -1
// */
// for(int col=offset;col<cols;col++)
// {
// outptr[col]=saturate_cast<uchar>( 5*currentptr[col]-currentptr[col-offset]-currentptr[col+offset]-priviousptr[col]-nextptr[col]);
// }
// }
Mat kernel =(Mat_<char>(3,3)<<0,-1,0,-1,5,-1,0,-1,0);
filter2D(srcImage,dstImage,srcImage.depth(),kernel);
imwrite("F:/SDK/OpenCV/HiKivision/picture/mask.jpg",dstImage);
return 0;
}
4. 图像线性变换
4.1. 图像线性混合
g(x,y)=af1(x,y)+bf2(x,y)+gamma
void addWeighted(InputArray src1, double alpha, InputArray src2,
double beta, double gamma, OutputArray dst, int dtype = -1);
/*
* @Descripttion:
* @version:
* @Author: Yueyang
* @email: 1700695611@qq.com
* @Date: 2021-05-03 01:24:41
* @LastEditors: Yueyang
* @LastEditTime: 2021-05-03 01:35:19
*/
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
Mat srcImage1,srcImage2,dstImage;
srcImage1=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
srcImage2=imread("F:/SDK/OpenCV/HiKivision/picture/apple.jpg");
addWeighted(srcImage1,0.5,srcImage2,0.5,0,dstImage);
imwrite("F:/SDK/OpenCV/HiKivision/picture/aadd.jpg",dstImage);
multiply(srcImage1,srcImage1,dstImage,0.5);
imwrite("F:/SDK/OpenCV/HiKivision/picture/muliy.jpg",dstImage);
return 0;
}
4.2. 图像亮度与对比度
g(x,y)=a*f(x,y)+b
上面这个公式可以很好的解释对图像的亮度和对比度操作的原理,第一个参数α必须是大于零,不然则基本上没有意义了。
α能代表什么呢?α能使图像像素成倍数的增长或降低(α<1),改变了是图像的对比度,因为使图像的差值变化了。
那么β作何解释呢?β可为负,也可为正,那么任何一个像素都在(0, 255)之间,加上一个值或减去一个值则会使这个
像素点变大或变小,其实就是向白色或向黑色靠近(0为黑,255为白),所以改变的是图像的亮度。
/*
* @Descripttion:
* @version:
* @Author: Yueyang
* @email: 1700695611@qq.com
* @Date: 2021-05-03 01:42:42
* @LastEditors: Yueyang
* @LastEditTime: 2021-05-03 08:54:29
*/
#include <opencv2/opencv.hpp>
using namespace cv;
int main()
{
Mat srcImage,dstImage;
srcImage=imread("F:/SDK/OpenCV/HiKivision/picture/lena.jpg");
dstImage=Mat::zeros(srcImage.size(),srcImage.type());
int height =srcImage.rows;
int width=srcImage.cols;
//1.2*pix+30
float alpha=1.2;
float beta=50;
for(int row=0;row<width;row++)
{
for(int col=0;col<width;col++)
{
if(srcImage.channels()==3){
float b=srcImage.at<Vec3b>(row,col)[0];
float g=srcImage.at<Vec3b>(row,col)[1];
float r=srcImage.at<Vec3b>(row,col)[2];
dstImage.at<Vec3b>(row,col)[0]=saturate_cast<uchar> (b*alpha+beta);
dstImage.at<Vec3b>(row,col)[1]=saturate_cast<uchar> (g*alpha+beta);
dstImage.at<Vec3b>(row,col)[2]=saturate_cast<uchar> (r*alpha+beta);
}else if(srcImage.channels()==4){
float b=srcImage.at<Vec4b>(row,col)[0];
float g=srcImage.at<Vec4b>(row,col)[1];
float r=srcImage.at<Vec4b>(row,col)[2];
dstImage.at<Vec4b>(row,col)[0]=saturate_cast<uchar> (b*alpha+beta);
dstImage.at<Vec4b>(row,col)[1]=saturate_cast<uchar> (g*alpha+beta);
dstImage.at<Vec4b>(row,col)[2]=saturate_cast<uchar> (r*alpha+beta);
dstImage.at<Vec4b>(row,col)[3]=srcImage.at<Vec4b>(row,col)[3];
}else{
float b=(float)srcImage.at<uchar>(row,col);
dstImage.at<Vec3b>(row,col)=b*alpha+beta;
}
}
}
imwrite("F:/SDK/OpenCV/HiKivision/picture/linghtchange.jpg",dstImage);
return 0;
}
4.3. 线性模糊
4.3.1. 均值模糊
void blur( InputArray src, OutputArray dst,
Size ksize, Point anchor = Point(-1,-1),
int borderType = BORDER_DEFAULT );
4.3.2. 高斯模糊
void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT )
InputArray src:输入的图像
OutputArray dst:输出图像
Size ksize:高斯卷积核的大小,是奇数
double sigmaX, double sigmaY=0, :表示x和y方向的方 差,如果y=0则y方向的方差与x相等
int borderType=BORDER_DEFAULT :边界的处理方式,一般默认
高斯模糊:越靠近卷积核的领域权重越大。
均值模糊:领域权重都为1。
而无论是高斯模糊或者是均值模糊,有一个缺点是他们在模糊的时候并不能很好的保留
边缘信息。因此双边滤波便很好客服了这一缺陷。原理如图:
void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT )
InputArray src: 输入图像,可以是Mat类型,图像必须是8位或浮点型单通道、三通道的图像。
. OutputArray dst: 输出图像,和原图像有相同的尺寸和类型。
. int d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
. double sigmaColor: 颜色空间过滤器的sigma值,这个参数的值月大,表明该像素邻域内有月宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
. double sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace五官,否则d正比于sigmaSpace.
. int borderType=BORDER_DEFAULT: 用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.
5. 形态学操作
形态学元素
Mat getStructuringElement(int shape, Size ksize, Point anchor = Point(-1,-1));
5.1. 腐蚀膨胀
膨胀
void dilate( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
腐蚀
void erode( InputArray src, OutputArray dst, InputArray kernel,
Point anchor = Point(-1,-1), int iterations = 1,
int borderType = BORDER_CONSTANT,
const Scalar& borderValue = morphologyDefaultBorderValue() );
跟卷积操作类似,假设有图像A和结构元素B,结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点的像素,其中B作为结构体可以是任意形状。我们回忆一下中值平滑操作——取每一个位置的矩形领域内值的中值作为该位置的输出灰度值,图像的膨胀操作与中值平滑操作类似,它是取每一个位置的矩形领域内值的最大值作为该位置的输出灰度值。不同的是,这里的领域不再单纯是矩形结构的,也可以是椭圆形结构的、十字交叉形结构的等,其中红色是参考点,也称为锚点(anchor point),如下所示:
因此取每个位置领域内最大值,所以膨胀后输出图像的总体亮度的平均值比起原图会有所升高,图像中比较亮的区域的面积会变大,而较暗物体的尺寸会减小甚至消失
腐蚀操作与膨胀操作类似,只是它取结构元所指定的领域内值的最小值作为该位置的输出灰度值。因为取每个位置领域内最小值,所以腐蚀后输出图像的总体亮度的平均值比起原图会有所降低,图像中比较亮的区域的面积会变小甚至消失,而较暗物体的尺寸会扩大
5.2. 开闭操作
cv2.morphologyEX(
img 输入图像
cv2.MORPH_CLOSE,cv2.MORPH_OPEN 形态学操作
kernel
)
用于对二值化后的图像进行处理,属于形态学操作(morphology)
开操作:消除白色的小点,去除小的干扰块
先对图像腐蚀后膨胀
闭操作:消除黑色的小块,填充闭合区域
先对图像膨胀后腐蚀
5.3. 顶帽黑帽
顶帽
就是开运算去掉的亮点
又称礼帽,是原始图像与进行开运算之后得到的图像的差。
因为开运算到来的结果是放大了裂痕或者局部低亮度的区域,因此,从原图中减去运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域,且这一操作和选择的核的大小相关。
顶帽运算往往用来分离比邻近点亮一些的斑块。当一幅图像具有大幅的背景的时候,而微小物品比较有规律的情况下,可以使用顶帽运算进行背景提取
黑帽
是进行闭运算以后得到的图像与原图像的差。
就是闭运算去掉的黑点
黑帽运算之后的效果图突出了与原图像轮廓周围的区域更暗的区域,且这一操作和选择的核大小相关。所以黑帽运算用来分离比邻近点暗一些的斑块。
5.4. 形态学梯度
形态学梯度是膨胀和腐蚀的差别,结果看上去就像前景物体的轮廓。计算的梯度常见的有三种:
基本梯度:
基本梯度是用膨胀后的图像减去腐蚀后的图像得到差值图像,称为梯度图像,也是OpenCV中支持的计算形态学梯度的方法,而此方法得到的梯度又被称为基本梯度。
内部梯度:
是用原图像减去腐蚀后的图像得到差值图像,称为图像的内部梯度
外部梯度:
图像膨胀后的图像减去原来的图像得到的差值图像,称为图像的外部梯度。
6. 图像金字塔
一个图像金字塔是一系列图像的集合 - 所有图像来源于同一张原始图像 - 通过梯次向下采样获得,直到达到某个终止条件才停止采样。
有两种类型的图像金字塔常常出现在文献和应用中:
高斯金字塔(Gaussian pyramid): 用来向下采样
拉普拉斯金字塔(Laplacian pyramid): 用来从金字塔低层图像重建上层未采样图像
6.1. 高斯金字塔
想想金字塔为一层一层的图像,层级越高,图像越小。
每一层都按从下到上的次序编号, 层级 (i+1) (表示为 G_{i+1} 尺寸小于层级 i (G_{i}))。
为了获取层级为 (i+1) 的金字塔图像,我们采用如下方法:
将 G_{i} 与高斯内核卷积:
将所有偶数行和列去除。
显而易见,结果图像只有原图的四分之一。通过对输入图像 G_{0} (原始图像) 不停迭代以上步骤就会得到整个金字塔。
以上过程描述了对图像的向下采样,如果将图像变大呢?:
首先,将图像在每个方向扩大为原来的两倍,新增的行和列以0填充(0)
使用先前同样的内核(乘以4)与放大后的图像卷积,获得 “新增像素” 的近似值。
这两个步骤(向下和向上采样) 分别通过OpenCV函数 pyrUp 和 pyrDown 实现, 我们将会在下面的示例中演示如何使用这两个函数。
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
using namespace cv;
/// 全局变量
Mat src, dst, tmpOpenCV3学习笔记整理(一期)