1. 颜色空间
1.1 BGR颜色空间
在opencv中,三原色顺序为BGR,每个通道灰度级为0-255,(0,0,0)则说明无颜色混合,为黑色,(255,255,255)为白色。opencv中还有4通道,为BGRA模型,A表示透明度,也叫alpha阿尔法通道。
1.2 HSV颜色空间
H: hue色度,对应颜色,如红绿蓝;
S:saturation饱和度,对应于深浅,类似于灰度级;
V:value亮度,对应于图像的明暗。
HSV更符合人眼感知颜色的方式。
1.3 GRAY灰度颜色空间
0-255层级,由黑----到白的变化。单通道。
1.4 cvtColor()颜色转化
此外还有YUV,Lab等颜色空间,不详细叙述。
cvtColor()颜色空间转换方式为:
COLOR_BGR2GRAY: BGR--->GRAY
COLOR_BGR2HSV: BGR---->HSV
COLOR_HSV2BGR: HSV--->BGR
通俗易懂。
示例程序:
// BGR空间转为HSV
cvtColor(origin_img, new_img, COLOR_BGR2HSV);
1.5 通道颜色分离与合并
2个概念:
- 像素深度:存储每个像素的位数,例如CV_8U为8位;
- 图像深度:实际用于存储像素位数的深度,如定义16位只用到15位,则为15位图像深度。
对于BGR颜色空间,有3个通道。有时只想处理R红色,则需要分离,处理完成后又需要合并B,G,R像素。
opencv提供方法为:分离函数cv::split(),以及合并函数cv::merge()。
【示例】分离图片的通道,修改R的数值,再合并:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main() {
// 读取图像
cv::Mat img = cv::imread("D:/Project_Images/ikun.png");
if (img.empty()) {
std::cout << "无法打开图像" << std::endl;
return -1;
}
// 分离 RGB 三个通道
std::vector<cv::Mat> channels;
cv::split(img, channels);
// 显示分离后的三个通道:B G R
cv::imshow("Blue Channel", channels[0]);
cv::imshow("Green Channel", channels[1]);
cv::imshow("Red Channel", channels[2]);
// 修改 R 通道数值
cv::Mat& r_channel = channels[2];
// 阈值化处理像素值
cv::threshold(r_channel, r_channel, 128, 255, cv::THRESH_BINARY);
// 合并三个通道为一张图像
cv::Mat merged_img;
cv::merge(channels, merged_img);
cv::namedWindow("Split and Merge", cv::WINDOW_NORMAL);
cv::imshow("Split and Merge", merged_img);
cv::waitKey(0);
// 关闭窗口
cv::destroyAllWindows();
return 0;
}
示例效果:
2. 点运算
常用于改变图像的灰度范围及分布,有时也称为对比度增强,对比度拉伸或灰度变换。包括图像灰度变换,直方图处理,伪色彩处理等技术。
2.1 像素点操作和卷积
1. 首先需要学会对像素值访问,在本系列博客讲义之前的Mat数据读取中讲过。
1. 指针法
src.ptr<Vec3b>(i)[j][c] = 255; // 注意() []
ptr 是Mat中定义好的类函数
先索引i行的地址,再找到j的位置
2. at访问
src.at<Vec3b>(i, j)[c] = 0;
2. 像素点亮度处理
示例:
src.at<Vec3b>(i,j)[c] = saturate_cast<uchar>(src.at<Vec3b>(i, j)[c] + 5 )
直接在像素值上加或减即可,分别对应提高和降低亮度
3. 图像卷积:
去噪,滤波,边缘提取都需要使用到图像卷积,目的是使各个目标的差距变得更大。常见的应用为锐化(突出边缘)和边缘提取。
4. 卷积原理和过程:
数字图像是二维的离散信号,卷积操作是将卷积核在图像上滚动,将图像上的像素灰度值和对应的卷积核上的数值相乘,然后将相乘的值相加作为卷积核中心上的值(可正可负)。
5. 卷积核选择规则:
- 大小奇数,例如3*3,5*5,7*7;
- 所有元素之和为1,是为了维持原图像的能量守恒,也有不为1的特殊情况;
- 如果卷积后的矩阵所有元素和>1, 比原图像会更亮,< 1,会比原图像更暗。 = 0, 不会全黑但会很暗。
6. opencv中提供filter2D()实现卷积操作
示例程序:
filter2D(src, dst, src.depth(), kernel);
// 原图像,输出图,图像深度,卷积核
使用二维卷积操作:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat src_image = imread("D:/Project_Images/ikun.png");
imshow("origin image", src_image);
// 创建一个卷积核
Mat kernel = (Mat_<double>(3, 3) <<
-1, 0, 1,
-2, 0, 2,
-1, 0, 1);
Mat dst_image;
filter2D(src_image, dst_image, src_image.depth(), kernel);
imshow("convert image", dst_image);
waitKey(0);
return 0;
}
运行效果:
明显可以看出,ikun的轮廓被突出。
7. 卷积操作调参比较简单,不同卷积操作只需要改变卷积核即可,以下是几种常见的卷积核:
均值滤波核:
1/ 9 | 1/ 9 | 1/ 9 |
1/ 9 | 1/ 9 | 1/ 9 |
1/ 9 | 1/ 9 | 1/ 9 |
高斯滤波核(正态分布)
1/16 | 2/16 | 1/16 |
2/16 | 4/16 | 2/16 |
1/16 | 2/16 | 1/16 |
边缘锐化核:
-1 | -1 | -1 |
-1 | 9 | -1 |
-1 | -1 | -1 |
可以看到,这个卷积核,刻意将边缘像素值与中心点像素值区分开来。有很明显的锐化效果。
2.2 图像像素值反转
适用于增强嵌入在图像暗色区域的白色或灰色细节,简而言之,黑色变白色,白变黑。
示例:遍历像素点反转灰度图的像素值
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat src;
int height, width;
int i, j;
src = imread("D:/Project_Images/ikun_gray.png");
if (!src.data)
{
cout << "no image!" << endl;
return -1;
}
height = src.rows;
width = src.cols * src.channels();
imshow("origin image", src);
// 遍历操作像素值
for (i = 0; i < height; i++)
{
for (j = 0; j < width; j++)
{
src.at<uchar>(i, j) = 255 - src.at<uchar>(i, j);
}
}
imshow("convert image", src);
waitKey(0);
return 0;
}
可以看到,黑白颜色交换了。
也可以使用OpenCV提供的接口方法:
bitwise_not(src, dst):位反转,就是上面的代码操作
bitwise_and(src, dst):位与操作
bitwise_or(src, dst): 或操作
bitwise_xor(src, dst): 异或操作
2.3 像素值对数变换
可以拉伸范围较窄的低灰度值,同时压缩范围较宽的高灰度值,可以用来扩展图像中的暗像素值,同时压缩亮像素值。
示例程序:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
// 将每个像素点的3个数值都变换
void log_transfor(Mat& img, Mat& result)
{
result = img.clone();
int rows = img.rows;
int cols = img.cols;
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
for (int k = 0; k < 3; k++)
{
// 彩色图像对数变换
result.at<Vec3b>(i, j)[k] = \
31 * log2(1 + img.at<Vec3b>(i, j)[k]);
}
}
}
}
int main()
{
Mat img = imread("D:/Project_Images/ikun.png");
Mat result;
log_transfor(img, result);
imshow("origin image", img);
imshow("convert image", result);
waitKey(0);
return 0;
}
运行结果:
2.4 图像灰度化
2个常识:
- 灰度值是形容黑白图像的,亮度值是形容彩色图像的。将彩色图像转换为黑白图像后,亮度值就会转换为灰度值。
- B,G,R混色后会偏向灰色,因此灰度化就是需要BGR同时参与一定比例即可。
图像灰度化有2种方法:
- 平均值法:将B,G,R亮度分量相加除以3;
- 加权平均法:由于人眼对绿色灵敏度高,蓝色灵敏度低,因此加权才更加合理:
gray(x, y) = r(x, y) *0.3 +g(x, y) *0.59 + b(x, y) *0.11
OpenCV给出的接口cvtColor()使用的是加权平均法。
3. 直方图处理
直方图是表示图像中亮度分布(黑白则为灰度分布),给出的是图像中某个亮度下共有几个像素,即统计数量。
- 缺点在于不能反应像素的位置,失去了空间信息。
- 优点在于计算代价小,可以图像平移,旋转,缩放不变性。
常用于灰度图像的阈值分割,基于颜色的图像检索,图像分类。
3.1 标准直方图
直方图就是一副像素统计图像。
以绘制灰度直方图为例,需要使用2个接口:calcHist()计算直方图, normalize()数据归一化;
程序示例如下:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
// 绘制标准直方图
int main()
{
// 1. 读取图像
Mat image = imread("D:/Project_Images/cat2.jpeg", IMREAD_GRAYSCALE); //图片先灰度化
if (image.empty())
{
std::cout << "Failed to load image!" << std::endl;
return -1;
}
// 2. 计算直方图
int histSize = 256; // 箱子数量(横坐标的份数)
float range[] = { 0, 256 }; // 使用数组,定义像素值范围
const float* histRange = range;
// 横坐标间隔均匀,纵坐标不累加个数值
bool uniform = true, accumulate = false;
Mat hist;
// 输入图像,1张图,不指定通道,无掩码,输出图像,1维,...
calcHist(&image, 1, nullptr, Mat(), hist, 1, &histSize, &histRange, \
uniform, accumulate);
// 3. 创建直方图画布
int histWidth = 512, histHeight = 400;
// 除以256个箱子计算每个箱子宽度,cvRound用于取四舍五入的整数
int binWidth = cvRound((double)histWidth / histSize);
Mat histImage(histHeight, histWidth, CV_8UC3, Scalar(0, 0, 0));
// 4. 归一化直方图数据,变换到[0, 行数]的范围,以适应histImage窗口的高度,Mat()为空矩阵
normalize(hist, hist, 0, histImage.rows, NORM_MINMAX, -1, Mat());
// 5. 绘制直方图
for (int i = 1; i < histSize; i++)
{
line(histImage, Point(binWidth * (i - 1), \
histHeight - cvRound(hist.at<float>(i - 1))),\
Point(binWidth * i, histHeight - cvRound(hist.at<float>(i))), \
Scalar(0, 255, 0), 2, LINE_AA);
}
// 显示直方图
namedWindow("Histogram", WINDOW_AUTOSIZE);
imshow("Histogram", histImage);
// 等待按键退出
waitKey(0);
return 0;
}
运行结果:
3.2 直方图均衡化
是为了增强图像的对比度,也叫直方图线性变换。主要思想是将一幅图像的直方图分布变成近似均匀分布,从而增强图像的对比度。基本原理是对图像中像素个数多的灰度值进行展宽,对像素较少的灰度值进行归并,从而加强对比度,使图像清晰。
OpenCV提供的方法为equalizeHist(image, result);
image必须是8位单通道灰度图。
示例:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat src_img, dst_img;
src_img = imread("D:/Project_Images/animal.jpeg", IMREAD_GRAYSCALE);
if (src_img.empty())
{
cout << "no image exist!" << endl;
return -1;
}
imshow("origin image", src_img);
equalizeHist(src_img, dst_img); // 就这一行代码起作用
imshow("equalize histogram", dst_img);
waitKey(0);
destroyAllWindows();
return 0;
}
实现效果:
效果上:增强对比度。
3.3 直方图匹配
简单来讲,将原图的像素值分布转为目标图的风格。
步骤:
- 将原图像的灰度直方图均衡化;
- 对规定的直方图均衡化;
- 建立原图像和目标图像的映射关系。
需要使用映射函数LUT。
示例:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
// 直方图匹配
int main()
{
// 加载原始图像和目标图像
Mat srcImage = imread("D:/Project_Images/animal.jpeg", IMREAD_GRAYSCALE);
Mat targetImage = imread("D:/Project_Images/panda.jpg", IMREAD_GRAYSCALE);
// 计算原始图像和目标图像的直方图
Mat srcHist, targetHist;
int histSize = 256; // 直方图横轴范围是 [0, 255]
float range[] = { 0, 256 };
const float* histRange = { range };
calcHist(&srcImage, 1, 0, Mat(), srcHist, 1, &histSize, &histRange);
calcHist(&targetImage, 1, 0, Mat(), targetHist, 1, &histSize, &histRange);
// 对原始图像进行直方图匹配
Mat lookupTable(1, 256, CV_8U);
float srcSum = sum(srcHist)[0];
float targetSum = sum(targetHist)[0];
float srcAccumulator = 0, targetAccumulator = 0;
for (int i = 0; i < 256; ++i)
{
srcAccumulator += srcHist.at<float>(i) / srcSum;
targetAccumulator += targetHist.at<float>(i) / targetSum;
lookupTable.at<uchar>(i) = cvRound(255 * srcAccumulator / targetAccumulator);
}
// 应用直方图映射到原始图像
Mat matchedImage;
LUT(srcImage, lookupTable, matchedImage);
// 显示原始图像、目标图像和匹配后的图像
imshow("Original Image", srcImage);
imshow("Target Image", targetImage);
imshow("Matched Image", matchedImage);
waitKey(0);
return 0;
}
效果示例:
原图像风格会趋向目标图像。
4. 图像去噪
去除图像中的噪声,减少干扰信息,也叫做滤波过程。
4.1 均值滤波
以3*3的卷积核为例,均值卷积核为9个格子都相等的卷积核,与原图像做卷积操作。
1/ 9 | 1/ 9 | 1/ 9 |
1/ 9 | 1/ 9 | 1/ 9 |
1/ 9 | 1/ 9 | 1/ 9 |
OpenCV提供的函数接口为blur()。
示例:
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
Mat src_img = imread("D:/Project_Images/noise.jpg");
imshow("origin image", src_img);
Mat dst_img;
dst_img.create(src_img.size(), src_img.type());
// 调用接口blur,这里选用6*6的核
blur(src_img, dst_img, Size(6, 6));
imshow("filter image", dst_img);
waitKey(0);
destroyAllWindows();
return 0;
}
实现效果:
4.2 高斯滤波
先了解4个概念:
低频成分:图像中亮度或灰度值变化不大的部分;
高频成分:图像中亮度或灰度值起伏很大的部分,例如黑白交替条纹。
低通滤波:保留低频成分
高通滤波:保留高频成分
高斯滤波:使用高斯卷积核对图像做卷积处理,平滑图像,属于低频滤波。
OpenCV提供的函数为GuassianBlur()。
示例:
GaussianBlur(src_img, dst_img, Size(7, 7), 0, 0);
效果:
对比均值,在边缘部分,信息丢失没有那么严重。
4.3 中值滤波
使用的滤波窗口取中值。例如3*3的滤波窗口(空窗口,非卷积操作),对图像中该窗口中的9个像素值取中值,将9个像素值全部换成中值。然后滑动窗口,重复操作。
OpenCV函数:medianBlur()
示例:
medianBlur(src_img, dst_img, 7);
效果:
效果:对均值和高斯的折中,兼顾滤波和边缘信息保留。
考点:中值滤波适合去除椒盐噪声(黑、白点噪声)。
原因:使用中值替代,黑色、白色像素孤立点被邻域中的中值同化,自然噪声消失。