目录
1.Opencv简说
从真实世界中获取数字图像有很多方法,比如数码相机、扫描仪、CT或者磁共振成像。无论哪种方法,我们(人类)看到的是图像,而让数字设备来“看“的时候,则是在记录图像中的每一个点的数值,这些数值在Opencv里就是用矩阵的方式来表示的,在Opencv中用图像类Mat来表示的。
说道图像不得不提颜色是怎么表示的:
有很多的颜色系统,各有自身优势:
- RGB是最常见的,这是因为人眼采用相似的工作机制,它也被显示设备所采用。
- HSV和HLS把颜色分解成色调、饱和度和亮度/明度。这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,使算法对输入图像的光照条件不敏感。
- YCrCb在JPEG图像格式中广泛使用。
- CIE L*a*b*是一种在感知上均匀的颜色空间,它适合用来度量两个颜色之间的 距离 。
每个组成元素都有其自己的定义域,取决于其数据类型。如何存储一个元素决定了我们在其定义域上能够控制的精度。最小的数据类型是 char ,占一个字节或者8位,可以是有符号型(0到255之间)或无符号型(-127到+127之间)。尽管使用三个 char 型元素已经可以表示1600万种可能的颜色(使用RGB颜色空间),但若使用float(4字节,32位)或double(8字节,64位)则能给出更加精细的颜色分辨能力。但同时也要切记增加元素的尺寸也会增加了图像所占的内存空间。
2.MAt类
在Opencv里,Mat是我们经常用到储存图像数据的类,这类有很多构造函数。
例如:
Mat M(2,2, CV_8UC3, Scalar(0,0,255));
CV_8UC3 表示使用8位的 unsigned char 型,每个像素由三个元素组成三通道。预先定义的通道数可以多达四个。 Scalar 是个short型vector。指定这个能够使用指定的定制化值来初始化矩阵。当然,如果你需要更多通道数,你可以使用大写的宏并把通道数放在小括号中,如下所示
int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));`3`:这是矩阵的行数。矩阵`L`有3行。
`sz`:这是指向包含矩阵列数数组的指针。在这种情况下,`sz`包含3个值:2, 2, 2。这意味着矩阵`L`有3个列块,每个列块有2列。因此,`L`是一个3x2x2的3D矩阵。
`CV_8UC(1)`:这是矩阵的数据类型和通道数。`CV_8UC1`表示每个元素是8位无符号单通道数据。在这里,`C`代表`CV_8U`(8位无符号整数)的通道数,而`(1)`表示单通道。因此,`L`的每个元素都是一个8位无符号整数,并且它是单通道的。
`Scalar::all(0)`:这是矩阵的初始值。`Scalar::all(0)`表示使用`Scalar`对象来初始化矩阵,其中所有元素都设置为0。因此,矩阵`L`的每个元素都被初始化为0。cv::Mat A = Mat_<double>(3,3);//创建一个3*3的矩阵用于存放double类型数据
更多初始化可以查看源码了解
Mat有哪些常见的属性?
dims:表示矩阵M的维度,如2*3的矩阵为2维,3*4*5的矩阵为3维
data:uchar型的指针,指向内存中存放矩阵数据的一块内存
rows, cols:矩阵的行数、列数
type:表示了矩阵中元素的类型(depth)与矩阵的通道个数(channels);命名规则为CV_ + (位数)+(数据类型)+(通道数)
其中:U(unsigned integer)-- 无符号整数
S(signed integer)-- 有符号整数
F(float)-- 浮点数
例如CV_8UC3,可拆分为:CV_:type的前缀,
8U:8位无符号整数(depth),C3:3通道(channels)
depth:即图像每一个像素的位数(bits);这个值和type是相关的。例如CV_8UC3中depth则是CV_8U。
CV_8U
8位无符号整数
0—255
CV_8S
8位符号整数
-128—127
CV_16U
16位无符号整数
0-65535
CV_16S
16位符号整数
-32768—32767
CV_32S
32位符号整数
-2147483648—2147483647
CV_32F
32位浮点整数
-FLT_MAX—FLT_MAX, INF, NAN
CV_64F
64位浮点整数
-DBL_MAX—DBL_MAX, INF, NAN
channels:通道数量,若图像为RGB、HSV等三通道图像,则channels = 3;若图像为灰度图,则为单通道,则channels = 1,目前C1、C2、C3、C4分别表示单通道(灰度图像)、双通道(特定图像算法通道不常用)、3通道(BGR)和4通道(BGRA其中A代表图像透明度)
elemSize:矩阵中每一个元素的数据大小
elemSize = channels * depth / 8
例如:type是CV_8UC3,elemSize = 3 * 8 / 8 = 3bytes
elemSize1:单通道的矩阵元素占用的数据大小
elemSize1 = depth / 8
例如:type是CV_8UC3,elemSize1 = 8 / 8 = 1bytes
3.拷贝构造
基本上讲 Mat 是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸,存储方法,存储地址等信息)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同矩阵可以是不同的维数)的指针。矩阵头的尺寸是常数值,但矩阵本身的尺寸会依图像的不同而不同,通常比矩阵头的尺寸大数个数量级。因此,当在程序中传递图像并创建拷贝时,大的开销是由矩阵造成的,而不是信息头。因此除非万不得已,我们不应该拷贝 大 的图像,因为这会降低程序速度
OpenCV使用引用计数机制。其思路是让每个 Mat 对象有自己的信息头,但共享同一个矩阵。这通过让矩阵指针指向同一地址而实现。而拷贝构造函数则 只拷贝信息头和矩阵指针 ,而不拷贝矩阵
Mat A, C; // 只创建信息头部分
A = imread(argv[1], CV_LOAD_IMAGE_COLOR); // 这里为矩阵开辟内存
Mat B(A); // 使用拷贝构造函数
C = A; // 赋值运算符以上代码中的所有Mat对象最终都指向同一个也是唯一一个数据矩阵。虽然它们的信息头不同,但通过任何一个对象所做的改变也会影响其它对象 ,如果矩阵属于多个 Mat 对象,通过引用计数机制来实现负责清理。某些时候你仍会想拷贝矩阵本身(不只是信息头和矩阵指针),这时可以使用函数 clone() 或者 copyTo()
4.灰度化
图像本来有色彩为什么灰度化?
因为灰度化只有单一色彩,数据较少,处理起来速度比较快,其次也比较容易处理图片细节以及纹理
{
Mat iamge, rhgMat, hsvMat;
string strPath = "..//Qt_OpenCV_01//res//a.png";
// 读取图片 保存图片就用imwrite
iamge = imread(strPath);
//将图片处理成灰度图片,cvtColor第三个参数有多重颜色,这里不细细展开自己看
cvtColor(iamge, rhgMat, COLOR_BGR2GRAY);
cvtColor(iamge, hsvMat, COLOR_BGR2HSV);
if (rhgMat.empty() || hsvMat.empty())
{
return;
}
imshow("HSV", hsvMat);
imshow("GRAY", rhgMat);
SaveImage("C:/Users/admin/Desktop/GRAY.png", rhgMat);
SaveImage("C:/Users/admin/Desktop/HSV.png", hsvMat);
}
5.高斯模糊
高斯模糊是另一种平滑技术,使用低通滤波器,其权重源自高斯函数,是最常用的计算机视觉应用之一。高斯滤波器的属性使其变得高效,如线性可分性,可近似不可分离滤波器,应用多个连续的高斯核相当于应用单个更大的高斯模糊,主要用于减少图像的噪声和降低细节层次
{
string strPath = "..//Qt_OpenCV_01//res//a.png";
Mat mat = imread(strPath);
if (mat.empty())
{
return;
}
Mat gaussMat;
//参数3,指内核大小一般为基数,参数4/5,表示X/Y方向上的高斯核标准差(模糊程度)
GaussianBlur(mat, gaussMat, Size(3, 3), 3, 0);
imshow("Gaussian", gaussMat);
}
6.边缘检测、膨胀、腐蚀
{
string strPath = "..//Qt_OpenCV_01//res//a.png";
Mat mat = imread(strPath);
if (mat.empty())
{
return;
}
Mat gaussMat, cannyMay, imgDil, imgErode;
//降噪 当值误边缘
GaussianBlur(mat, gaussMat, Size(3, 3), 3, 0);
//边缘检测 参数3/4 处理图像阈值
Canny(gaussMat, cannyMay, 27, 75);
//获取 大小位3X3的结构性元素,并当做下面操作的核
Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
//膨胀操作
dilate(cannyMay, imgDil, kernel);
//侵蚀操作
erode(imgDil, imgErode, kernel);
imshow("Gaussian", cannyMay);
imshow("GaussianDilation", imgDil);
imshow("GaussianErode", imgErode);
}
7.改变通道数据
前面我们提到Mat类,其实他就是个矩阵数据,那我们试着能不能改变图的亮度呢?
{
Mat inputMat = imread("..//Qt_OpenCV_01//res//a.jpg");
// 确保新Mat与输入图像类型相同
Mat newMat = Mat::zeros(inputMat.size(), inputMat.type());
double dAlpha = 2; // 你可以考虑将这个值限制在0到255之间,以避免溢出对比度
int nBeta = 50;//明亮度
//修改点 (row, col) 的 B/G/R 通道数据
for (int y = 0; y < inputMat.rows; y++)
{
for (int x = 0; x < inputMat.cols; x++)
{
for (int c = 0; c < inputMat.channels(); c++) // 使用channels()函数自动处理通道数
{
// 将double转换为uchar
uchar alpha = static_cast<uchar>(dAlpha);
// 同样处理nBeta,尽管在你的代码中nBeta没有被使用
uchar beta = static_cast<uchar>(nBeta);
newMat.at<Vec3b>(y, x)[c] = saturate_cast<uchar>(alpha * inputMat.at<Vec3b>(y, x)[c] + beta); // 确保结果在uchar范围内
}
}
}
namedWindow("Original Image", WINDOW_AUTOSIZE);
namedWindow("New Image", WINDOW_AUTOSIZE);
imshow("Original Image", inputMat);
imshow("New Image", newMat);
waitKey();
}
8.金子塔 放大缩小
{
Mat src, dst, tmp;
const char* window_name = "Sample Demp";
src = imread("..//Qt_OpenCV_01//res//a.jpg");
tmp = src;
dst = tmp;
namedWindow(window_name, WINDOW_AUTOSIZE);
imshow(window_name, dst);
if (!down)
{
pyrUp(tmp, dst, Size(tmp.cols * 2, tmp.rows * 2));
}
else
{
pyrDown(tmp, dst, Size(tmp.cols / 2, tmp.rows / 2));
}
imshow(window_name, dst);
}
9.阈值处理
namespace Threshold
{
int threshold_type = 3;
int threshold_value = 0;
int const max_value = 255;
int const max_type = 4;
int const max_BINARY_value = 255;
Mat src, src_gray, dst;
const char* window_name = "threshold demo";
const char* trackbar_type = "Type: \n 0: Binary \n 1: Binary Inverted \n 2: Truncate \n 3: To Zero \n 4: To Zero Inverted";
const char* trackbar_value = "Value";
void Threshold_demo(int, void*);
}
void Qt_OpenCV_01::Thresholdong()
{
Threshold::src = imread("..//Qt_OpenCV_01//res//q.jpg");
// change color
cvtColor(Threshold::src, Threshold::src_gray, COLOR_RGB2GRAY);
namedWindow(Threshold::window_name, WINDOW_AUTOSIZE);
//创建滑块 参数1滑块名称 参数2窗口名称 参数3滑块的默认值 参数4最大值 参数5回调函数
createTrackbar(Threshold::trackbar_type, Threshold::window_name, &Threshold::threshold_type, Threshold::max_type, Threshold::Threshold_demo);
createTrackbar(Threshold::trackbar_value, Threshold::window_name, &Threshold::threshold_value, Threshold::max_value, Threshold::Threshold_demo);
Threshold::Threshold_demo(0, 0);
waitKey(0);
}
void Threshold::Threshold_demo(int, void*)
{
/*
0 二进制
1 反向二进制
2 截断
3 阈值为零
4 反向阈值为零
*/
threshold(Threshold::src_gray, Threshold::dst, Threshold::threshold_value, Threshold::max_BINARY_value, Threshold::threshold_type);
imshow(Threshold::window_name, Threshold::dst);
}
10.滤波
filter2D
是OpenCV库中的一个函数,用于对二维矩阵(通常是图像)进行卷积运算。其函数原型如下:void cv::filter2D(InputArray src, OutputArray dst, int ddepth, InputArray kernel,Point anchor=Point(-1,-1), double delta=0, int borderType=BORDER_DEFAULT);其中参数的意义如下:
src
:输入矩阵或图像。dst
:输出图像,与src
大小相同、通道数相同。ddepth
:目标图像的所需深度。kernel
:卷积核(或者更确切地说是相关核),单通道浮点矩阵。如果要将不同的内核应用于不同的通道,请使用split
将图像分割为单独颜色平面并单独处理它们。anchor
:内核的锚点,指示内核中过滤点的相对位置。锚点应该位于内核内;默认值(-1,-1)
表示锚点位于内核中心。delta
:在将过滤像素存储到dst
之前添加到过滤像素的可选值。borderType
:像素外推方法,定义了如何处理图像边界外的像素。卷积核的作用是将周围像素的灰度值与自己的灰度值进行加权平均,从而得到新的像素值。不同的卷积核会得到不同的平滑效果。卷积核的形状、大小、权重都需要用户自己定义。
卷积运算是图像处理中的基础且重要的步骤,通常用于图像过滤,例如边缘检测、模糊等。
filter2D
函数允许用户自定义卷积核,从而实现各种图像滤波效果,如均值滤波、高斯滤波等
{
Mat src, dst, kernel, blur_tmp;
Point anchor;
double delta;
int ddepth;
int kernel_size;
const char* window_name = "filder2D demo";
int c;
src = imread("..//Qt_OpenCV_01//res//a.jpg");
namedWindow(window_name, WINDOW_AUTOSIZE);
//核
anchor = Point(-1, -1);
// 卷积过程加到每个像素值
delta = 0;
// 深度
ddepth = -1;
//每 500毫秒用不同的核滤波图像
int ind = 0;
for (;;)
{
c = waitKey(500);
if ((char)c == 27)
{
break;
}
//以归一化块滤波更新核大小
kernel_size = 3 + 2 * (ind % 5);
kernel = Mat::ones(kernel_size, kernel_size, CV_32F) / (float)(kernel_size * kernel_size);
//平均滤波
blur(src, blur_tmp, Size(5, 5));
imshow("blur", blur_tmp);
//滤波
//
filter2D(src, dst, ddepth, kernel, anchor, delta, BORDER_DEFAULT);
imshow(window_name, dst);
ind++;
}
}
11.算子(Scharr、Sobel、Laplacian)
Sobel主要用于获得数字图像的一阶梯度,常见的应用和物理意义是边缘检测。该算子根据图像中每个像素的上下左右四领域的灰度值加权差,在边缘处达到极值从而检测边缘。Sobel算子对于图像中较弱的边缘提取效果可能较差。为了能够有效提取出较弱的边缘,需要将像素间的差距增大,于是引入了Scharr算子
函数的主要参数包括
- dst 代表目标图像。
- src 代表原始图像。
- ddepth 代表输出图像的深度。
- dx 代表 x 方向上的求导阶数。
- dy 代表 y 方向上的求导阶数。
- ksize 代表 Sobel 核的大小。该值为-1 时,则会使用 Scharr 算子进行运算。
- scale 代表计算导数值时所采用的缩放因子,默认情况下该值是 1,是没有缩放的。
- delta 代表加在目标图像 dst 上的值,该值是可选的,默认为 0。
- borderType 代表边界样式。
Scharr通常指的是Scharr算子,它是一种用于图像边缘检测的算法。Scharr算子是Sobel算子的改进版,与Sobel算子在原理上相似,都是使用右边一列减去左边一列来检测边缘,但Scharr算子的系数与Sobel算子不同,因此其运算准确度更高,效果也更好
函数的主要参数包括同上
Laplacian(拉普拉斯算子)是n维欧几里德空间中的一个二阶微分算子,主要用于检测图像的二阶导数,即边缘。它通过对图像进行二阶微分来检测图像的边缘,对于不同方向的边缘都能够进行检测,对边缘的粗细和强度变化也比较敏感
函数的主要参数包括
- src:输入图像,可以是任意通道数的图像,但通常是单通道灰度图像。
- dst:输出图像,与输入图像src具有相同的尺寸和通道数
- ddepth:输出图像的深度,若src为CV_8U,则可取-1/CV_16S/CV_32F/CV_64F;若src为CV_16U/CV_16S,可取-1/CV_32F/CV_64F;若src为CV_32F,可取-1/CV_32F/CV_64F;若src为CV_64F,可取-1/CV_64F。,当赋值为-1时,输出图像的数据类型自动选择。
- ksize:用于计算二阶导数的核尺寸大小,必须为正奇数。
- scale:对导数计算结果进行缩放的缩放因子,默认系数为1,表示不进行缩放。也称对比度
- delta:偏值,在计算结果中加上偏值。也称亮度
- borderType:像素外推法选择标志,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
{
Mat src, src_gray, grad;
const char* window_name = "Sobel Demo - Simple";
int scale = 1;
int delta = 0;
int ddepth = CV_16S;
int kernel_size = 3;
//
src = imread("..//Qt_OpenCV_01//res//a.jpg");
//对原图使用高斯模糊 降噪
GaussianBlur(src, src, Size(3, 3), 0, 0, BORDER_DEFAULT);
//变成灰度
cvtColor(src, src_gray, COLOR_RGB2GRAY);
//
namedWindow(window_name, WINDOW_AUTOSIZE);
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
//倾斜度x
// Scharr(src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT);
Sobel(src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT);
// Laplacian(src_gray, grad_x, ddepth, kernel_size, scale, delta, BORDER_DEFAULT);
//变换绝对值8位图像
convertScaleAbs(grad_x, abs_grad_x);
//倾斜度Y
// Scharr(src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT);
Sobel(src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT);
// Laplacian(src_gray, grad_y, ddepth, kernel_size, scale, delta, BORDER_DEFAULT);
Canny(src_gray, grad, 50, 200, 3);
convertScaleAbs(grad_y, abs_grad_y);
//总倾斜度
addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, grad);
imshow(window_name, grad);
waitKey(0);
}
12.重映射
remap
- 参数一:InputArray类型的src,一般为cv::Mat;
- 参数二:OutputArray类型的dst,目标图像。它的大小与map1相同,类型与src相同。
- 参数三:InputArray类型的map1,它有两种可能的表示对象:表示点(x,y)的第一个映射或者表示CV_16SC2 , CV_32FC1 或CV_32FC2类型的x值。
- 参数四:InputArray类型的map2,它也有两种可能的表示对象,而且他是根据map1来确定表示哪种对象。若map1表示点(x,y)时,这个参数不代表任何值,否则,表示CV_16UC1 , rCV_32FC1类型的y值(第二个值)。
- 参数五:int类型的interpolation,使用的插值方法;
- 参数六:int类型的borderMode,边界处理方式;
- 参数七:Scalar类型的borderValue,重映射后,离群点的背景,需要broderMode设置为BORDER_CONSTRANT时才有效。(离群点:当图片大小为400x300,那么对应的map1和map2范围为0399、0299,小于0或者大于299的则为离散点,使用该颜色填充);
namespace remapping {
Mat src, dst;
Mat map_x, map_y;
const char* remap_window = "remap demo";
int ind = 0;
}
void update_map(void);
void Qt_OpenCV_01::Remapping()
{
remapping::src = imread("..//Qt_OpenCV_01//res//r.png");
remapping::dst.create(remapping::src.size(), remapping::src.type());
remapping::map_x.create(remapping::src.size(), CV_32FC1);
remapping::map_y.create(remapping::src.size(), CV_32FC1);
namedWindow(remapping::remap_window, WINDOW_AUTOSIZE);
for (;;)
{
int c = waitKey(1000);
if ((char)c == 27)
{
break;
}
update_map();
remap(remapping::src, remapping::dst, remapping::map_x, remapping::map_y, CV_INTER_LANCZOS4, BORDER_CONSTANT, Scalar(0, 0, 0));
imshow(remapping::remap_window, remapping::dst);
}
}
void update_map(void)
{
remapping::ind = remapping::ind % 4;
for (int j = 0; j < remapping::src.rows; j++)
{
for (int i = 0; i < remapping::src.cols; i++)
{
switch (remapping::ind)
{
case 0:
if (i > remapping::src.cols * 0.25 && i < remapping::src.cols * 0.75 && j > remapping::src.rows * 0.25 && j < remapping::src.rows * 0.75)
{
remapping::map_x.at<float>(j, i) = 2 * (i - remapping::src.cols * 0.25f) + 0.5f;
remapping::map_y.at<float>(j, i) = 2 * (j - remapping::src.rows * 0.25f) + 0.5f;
}
else
{
remapping::map_x.at<float>(j, i) = 0;
remapping::map_y.at<float>(j, i) = 0;
}
break;
case 1:
remapping::map_x.at<float>(j, i) = (float)i;
remapping::map_y.at<float>(j, i) = (float)(remapping::src.rows - j);
break;
case 2:
remapping::map_x.at<float>(j, i) = (float)(remapping::src.cols - i);
remapping::map_y.at<float>(j, i) = (float)(j);
break;
case 3:
remapping::map_x.at<float>(j, i) = (float)(remapping::src.cols - i);
remapping::map_y.at<float>(j, i) = (float)(remapping::src.rows - j);
break;
}
}
}
remapping::ind++;
}
级联分类器
通常被用于处理复杂的分类任务,其中每个分类器负责识别特定类型的样本或特征。每个分类器在处理样本时,可以根据需要执行不同的特征提取和处理操作。这种级联的结构能够逐步降低分类问题的复杂性,并提高分类的准确性。级联分类器是一种将多个分类器级联起来的机器学习模型。它可以处理复杂的分类任务,并通过逐步降低问题复杂性的方式提高分类的准确性
人脸识别
{
VideoCapture cap(0);
if (!cap.isOpened())
{
return;
}
CascadeClassifier face_detetor;
face_detetor.load("F:\\opencv4.8.0\\opencv\\build\\etc\\haarcascades\\haarcascade_frontalface_default.xml");
namedWindow("face", WINDOW_AUTOSIZE);
while (1)
{
Mat face_mat;
cap >> face_mat;
Mat gra_mat;
cvtColor(face_mat, gra_mat, COLOR_BGR2GRAY);
vector<Rect> faces;
face_detetor.detectMultiScale(gra_mat, faces, 1.1, 2, 0 | CASCADE_SCALE_IMAGE, Size(30, 30));
for (size_t i = 0; i < faces.size(); ++i)
{
rectangle(face_mat, faces[i], Scalar(0, 0, 255), 2, 8, 0);
}
imshow("face", face_mat);
waitKey(5);
}
}
自己做级联分类器
参考