OpenCV入门学习-图像处理(一)

环境:

  • win11 x64
  • OpenCV 4.9.0
  • VS 2022 (v143)
  • C++ (ISO C++ 14 标准)

1. 绘图基础

本文介绍如何使用OpenCV进行基础图 形绘制,包括直线、椭圆、矩形、圆形和填充多边形的实现方法。所有代码均使用C++语法,并明确标注命名空间。

1.1. 核心数据结构

1.1.1. cv::Point

表示二维坐标系中的点,可通过以下两种方式定义:

1
2
3
4
5
cv::Point pt1;
pt1.x = 10; // x 坐标
pt1.y = 8; // y 坐标

cv::Point pt2(10, 8); // 直接初始化

1.1.2. cv::Scalar

表示四维向量,常用于表示 BGR 颜色值(仅使用前三个参数):

1
cv::Scalar(blue, green, red);  // BGR 颜色模型示例

1.2. 基础绘图函数实现

1.2.1. 创建画布

初始化 400x400 的黑色背景图像:

1
2
cv::Mat atom_image = cv::Mat::zeros(w, w, CV_8UC3);  // 原子结构画布
cv::Mat rook_image = cv::Mat::zeros(w, w, CV_8UC3); // 城堡结构画布

1.2.2. 绘制直线

通过 cv::line 实现直线绘制:

1
2
3
4
5
6
void MyLine(cv::Mat img, cv::Point start, cv::Point end) {
int thickness = 2;
int lineType = cv::LINE_8;
cv::line(img, start, end, cv::Scalar(0, 0, 0), thickness, lineType);
// 参数说明:目标图像、起点、终点、颜色(黑色)、线宽、连线类型。
}

1.2.3. 绘制椭圆

使用 cv::ellipse 绘制旋转椭圆:

1
2
3
4
5
6
7
8
9
10
void MyEllipse(cv::Mat img, double angle) {
cv::ellipse(img,
cv::Point(w/2, w/2), // 中心坐标
cv::Size(w/4, w/16), // 长轴/短轴
angle, // 旋转角度
0, 360, // 起始/结束角度
cv::Scalar(255, 0, 0), // 蓝色椭圆
2, // 线宽
cv::LINE_8);
}

1.2.4. 绘制实心圆

通过 cv::circle 实现填充圆形:

1
2
3
4
5
6
7
8
void MyFilledCircle(cv::Mat img, cv::Point center) {
cv::circle(img,
center, // 圆心坐标
w/32, // 半径
cv::Scalar(0, 0, 255), // 红色填充
cv::FILLED, // 填充模式
cv::LINE_8);
}

1.2.5. 绘制多边形

使用 cv::fillPoly 绘制自定义多边形:

1
2
3
4
5
6
7
8
9
void MyPolygon(cv::Mat img) {
cv::Point rook_points[1][20] = { /* 顶点坐标定义 */ };
const cv::Point* ppt[1] = { rook_points[0] };
int npt[] = { 20 };

cv::fillPoly(img, ppt, npt, 1,
cv::Scalar(255, 255, 255), // 白色填充
cv::LINE_8);
}

1.2.6. 绘制矩形

直接调用 cv::rectangle 函数:

1
2
3
4
5
6
cv::rectangle(rook_image,
cv::Point(0, 7*w/8), // 左上角坐标
cv::Point(w, w), // 右下角坐标
cv::Scalar(0, 255, 255), // 黄色填充
cv::FILLED, // 填充模式
cv::LINE_8);

1.2.7. 添加文本

文字标注需指定字体类型和左下角起点:

1
2
3
4
5
6
7
8
cv::putText(img,
"OpenCV",
cv::Point(10, 500), // 文字起点
cv::FONT_HERSHEY_SIMPLEX, // 字体类型
4, // 字体缩放因子
cv::Scalar(255, 255, 255),
2,
cv::LINE_AA);
  • 字体优化:cv::LINE_AA 抗锯齿模式可提升文字显示质量

1.2.8. 仿射变换

数学描述 [xy]=[abcd][xy]+[k1k2] 通过 cv::getAffineTransformcv::warpAffine 可实现图像的平移、旋转等操作

1.3. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

#define w 400 // 定义画布宽度

using namespace cv;

void MyEllipse(Mat img, double angle); // 声明绘制椭圆函数
void MyFilledCircle(Mat img, Point center); // 声明绘制实心圆函数
void MyPolygon(Mat img); // 声明绘制多边形函数
void MyLine(Mat img, Point start, Point end); // 声明绘制直线函数

int main(void) {
char atom_window[] = "Drawing 1: Atom"; // 定义原子图窗口名称
char rook_window[] = "Drawing 2: Rook"; // 定义城堡图窗口名称

Mat atom_image = Mat::zeros(w, w, CV_8UC3); // 创建原子图,黑色背景
Mat rook_image = Mat::zeros(w, w, CV_8UC3); // 创建城堡图,黑色背景

MyEllipse(atom_image, 90); // 绘制旋转角度90°的椭圆
MyEllipse(atom_image, 0); // 绘制旋转角度0°的椭圆
MyEllipse(atom_image, 45); // 绘制旋转角度45°的椭圆
MyEllipse(atom_image, -45); // 绘制旋转角度-45°的椭圆

MyFilledCircle(atom_image, Point(w / 2, w / 2)); // 绘制原子核实心圆

MyPolygon(rook_image); // 绘制城堡主体多边形

rectangle(rook_image, // 绘制城堡底座矩形
Point(0, 7 * w / 8), // 矩形左上角坐标
Point(w, w), // 矩形右下角坐标
Scalar(0, 255, 255), // 矩形颜色(黄色)
FILLED, // 填充矩形
LINE_8); // 线型

MyLine(rook_image, Point(0, 15 * w / 16), Point(w, 15 * w / 16)); // 绘制底部水平线
MyLine(rook_image, Point(w / 4, 7 * w / 8), Point(w / 4, w)); // 绘制左侧竖线
MyLine(rook_image, Point(w / 2, 7 * w / 8), Point(w / 2, w)); // 绘制中间竖线
MyLine(rook_image, Point(3 * w / 4, 7 * w / 8), Point(3 * w / 4, w)); // 绘制右侧竖线

imshow(atom_window, atom_image); // 显示原子图
moveWindow(atom_window, 0, 200); // 移动原子图窗口位置
imshow(rook_window, rook_image); // 显示城堡图
moveWindow(rook_window, w, 200); // 移动城堡图窗口位置

waitKey(0); // 等待按键
return 0; // 程序结束
}

void MyEllipse(Mat img, double angle) {
int thickness = 2; // 线宽
int lineType = 8; // 线型

ellipse(img, // 绘制椭圆
Point(w / 2, w / 2), // 椭圆中心点
Size(w / 4, w / 16), // 椭圆长轴和短轴
angle, // 椭圆旋转角度
0, // 起始角度
360, // 结束角度
Scalar(255, 0, 0), // 颜色(蓝色)
thickness, // 线宽
lineType); // 线型
}

void MyFilledCircle(Mat img, Point center) {
circle(img, // 绘制实心圆
center, // 圆心坐标
w / 32, // 半径
Scalar(0, 0, 255), // 颜色(红色)
FILLED, // 填充圆形
LINE_8); // 线型
}

void MyPolygon(Mat img) {
int lineType = LINE_8; // 线型

Point rook_points[1][20]; // 定义一个多边形的20个顶点
rook_points[0][0] = Point(w / 4, 7 * w / 8);
rook_points[0][1] = Point(3 * w / 4, 7 * w / 8);
rook_points[0][2] = Point(3 * w / 4, 13 * w / 16);
rook_points[0][3] = Point(11 * w / 16, 13 * w / 16);
rook_points[0][4] = Point(19 * w / 32, 3 * w / 8);
rook_points[0][5] = Point(3 * w / 4, 3 * w / 8);
rook_points[0][6] = Point(3 * w / 4, w / 8);
rook_points[0][7] = Point(26 * w / 40, w / 8);
rook_points[0][8] = Point(26 * w / 40, w / 4);
rook_points[0][9] = Point(22 * w / 40, w / 4);
rook_points[0][10] = Point(22 * w / 40, w / 8);
rook_points[0][11] = Point(18 * w / 40, w / 8);
rook_points[0][12] = Point(18 * w / 40, w / 4);
rook_points[0][13] = Point(14 * w / 40, w / 4);
rook_points[0][14] = Point(14 * w / 40, w / 8);
rook_points[0][15] = Point(w / 4, w / 8);
rook_points[0][16] = Point(w / 4, 3 * w / 8);
rook_points[0][17] = Point(13 * w / 32, 3 * w / 8);
rook_points[0][18] = Point(5 * w / 16, 13 * w / 16);
rook_points[0][19] = Point(w / 4, 13 * w / 16);

const Point* ppt[1] = {rook_points[0]}; // 多边形顶点指针数组
int npt[] = {20}; // 多边形顶点个数

fillPoly(img, // 填充多边形
ppt, // 顶点数组
npt, // 每个多边形的顶点数
1, // 多边形数量
Scalar(255, 255, 255), // 填充颜色(白色)
lineType); // 线型
}

void MyLine(Mat img, Point start, Point end) {
int thickness = 2; // 线宽
int lineType = LINE_8; // 线型

line(img, // 绘制直线
start, // 起点坐标
end, // 终点坐标
Scalar(0, 0, 0), // 直线颜色(黑色)
thickness, // 线宽
lineType); // 线型
}

运行代码将生成两个窗口:


2. 随机数生成与文本绘制

将通过OpenCV的随机数生成器 cv::RNG 和文本绘制功能,演示如何动态生成随机几何图形与文字效果

2.1. 随机数生成器

OpenCV提供 cv::RNG 类生成伪随机数。初始化时需指定种子值,例如:

1
cv::RNG rng(0xFFFFFFFF);  // 使用十六进制种子初始化

通过 uniform(a, b) 方法生成区间 [a,b) 内的 均匀分布 随机 整数

1
int random_value = rng.uniform(0, 100);  // 随机数范围 [0, 100)

若需要浮点数,可显式指定类型:

1
double random_float = rng.uniform(0.0, 1.0);  // 生成 0.0 到 1.0 之间的浮点数

2.2. 绘制随机几何图形

以下示例演示如何生成随机线条:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int Drawing_Random_Lines(cv::Mat image, const std::string& window_name, cv::RNG rng) {
const int NUMBER = 100; // 绘制 100 条随机线
int lineType = cv::LINE_8;
cv::Point pt1, pt2;

for (int i = 0; i < NUMBER; i++) {
pt1.x = rng.uniform(0, image.cols); // x 坐标随机
pt1.y = rng.uniform(0, image.rows); // y 坐标随机
pt2.x = rng.uniform(0, image.cols);
pt2.y = rng.uniform(0, image.rows);

// 随机颜色生成函数
auto randomColor = [&](cv::RNG& rng) {
return cv::Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
};

cv::line(image, pt1, pt2, randomColor(rng), rng.uniform(1, 10), lineType);
cv::imshow(window_name, image);
if (cv::waitKey(10) >= 0) break; // 监听键盘 10ms
}
return 0;
}

类似方法可扩展至其他图形(矩形、椭圆、多边形等),只需调整参数生成逻辑。

2.3. 动态文本绘制

使用 cv::putText 在图像上添加随机文字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int Displaying_Random_Text(cv::Mat image, const std::string& window_name, cv::RNG rng) {
for (int i = 0; i < 50; i++) {
cv::Point org;
org.x = rng.uniform(0, image.cols - 200); // 避免文字越界
org.y = rng.uniform(20, image.rows);

// 随机字体、大小、颜色和粗细
int fontFace = rng.uniform(0, 8);
double fontScale = rng.uniform(1, 3);
cv::Scalar color(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
int thickness = rng.uniform(1, 5);

cv::putText(image, "OpenCV Tutorial", org, fontFace, fontScale, color, thickness);
cv::imshow(window_name, image);
if (cv::waitKey(100) >= 0) break;
}
return 0;
}

2.4. 动态渐变文字效果

以下代码实现文字颜色渐变与图像亮度变化的动画效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int Displaying_Big_End(cv::Mat image, const std::string& window_name) {
// cv::Size cv::getTextSize(const std::string& text, // 要测量的文本串
// int fontFace, // 字体类型
// double fontScale, // 字体的缩放因子
// int thickness, // 文本的线条宽度
// int* baseline = 0); // 计算文本的基线的位置
cv::Size textSize = cv::getTextSize("OpenCV forever!", cv::FONT_HERSHEY_COMPLEX, 3, 5, 0);
cv::Point org((image.cols - textSize.width) / 2, (image.rows + textSize.height) / 2);
cv::Mat image2;

for (int i = 0; i < 255; i += 2) {
image2 = image - cv::Scalar::all(i); // 图像亮度递减
cv::putText(image2, "OpenCV forever!", org, cv::FONT_HERSHEY_COMPLEX, 3,
cv::Scalar(i, i, 255), 5); // 文字颜色渐变
cv::imshow(window_name, image2);
if (cv::waitKey(30) >= 0) break;
}
return 0;
}

2.5. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <iostream>
#include <opencv2/opencv.hpp>

const int NUMBER = 50;

// 随机颜色生成函数
cv::Scalar randomColor(cv::RNG& rng) {
return cv::Scalar(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));
}

// 绘制随机线条
int Drawing_Random_Lines(cv::Mat& image, const std::string& window_name, cv::RNG& rng) {

int lineType = cv::LINE_8;
cv::Point pt1, pt2;

for (int i = 0; i < NUMBER; i++) {
pt1.x = rng.uniform(0, image.cols); // x坐标随机
pt1.y = rng.uniform(0, image.rows); // y坐标随机
pt2.x = rng.uniform(0, image.cols);
pt2.y = rng.uniform(0, image.rows);

cv::line(image, pt1, pt2, randomColor(rng), rng.uniform(1, 10), lineType);
cv::imshow(window_name, image);
if (cv::waitKey(10) >= 0) break;
}
return 0;
}

// 绘制随机矩形
int Drawing_Random_Rectangles(cv::Mat& image, const std::string& window_name, cv::RNG& rng) {

int lineType = cv::LINE_8;
cv::Point pt1, pt2;

for (int i = 0; i < NUMBER; i++) {
pt1.x = rng.uniform(0, image.cols);
pt1.y = rng.uniform(0, image.rows);
pt2.x = rng.uniform(0, image.cols);
pt2.y = rng.uniform(0, image.rows);

cv::rectangle(image, pt1, pt2, randomColor(rng), rng.uniform(-1, 10), lineType);
cv::imshow(window_name, image);
if (cv::waitKey(10) >= 0) break;
}
return 0;
}

// 绘制随机椭圆
int Drawing_Random_Ellipses(cv::Mat& image, const std::string& window_name, cv::RNG& rng) {

int lineType = cv::LINE_8;

for (int i = 0; i < NUMBER; i++) {
cv::Point center(rng.uniform(0, image.cols), rng.uniform(0, image.rows));
cv::Size axes(rng.uniform(0, 200), rng.uniform(0, 200));
double angle = rng.uniform(0, 360);

cv::ellipse(image, center, axes, angle, 0, 360, randomColor(rng), rng.uniform(-1, 10), lineType);
cv::imshow(window_name, image);
if (cv::waitKey(10) >= 0) break;
}
return 0;
}

// 绘制随机多边形
int Drawing_Random_Polylines(cv::Mat& image, const std::string& window_name, cv::RNG& rng) {

int lineType = cv::LINE_8;

for (int i = 0; i < NUMBER; i++) {
cv::Point pts[1][3];
pts[0][0] = cv::Point(rng.uniform(0, image.cols), rng.uniform(0, image.rows));
pts[0][1] = cv::Point(rng.uniform(0, image.cols), rng.uniform(0, image.rows));
pts[0][2] = cv::Point(rng.uniform(0, image.cols), rng.uniform(0, image.rows));

const cv::Point* ppt[1] = {pts[0]};
int npt[] = {3};

cv::polylines(image, ppt, npt, 1, true, randomColor(rng), rng.uniform(1, 10), lineType);
cv::imshow(window_name, image);
if (cv::waitKey(10) >= 0) break;
}
return 0;
}

// 绘制随机文本
int Displaying_Random_Text(cv::Mat& image, const std::string& window_name, cv::RNG& rng) {

int lineType = cv::LINE_8;

for (int i = 0; i < NUMBER; i++) {
cv::Point org;
org.x = rng.uniform(0, image.cols - 200); // 避免文字越界
org.y = rng.uniform(20, image.rows);

int fontFace = rng.uniform(0, 8); // 随机字体
double fontScale = rng.uniform(1, 3); // 随机字体大小
int thickness = rng.uniform(1, 5); // 随机粗细

cv::putText(image, "OpenCV Tutorial", org, fontFace, fontScale, randomColor(rng), thickness, lineType);
cv::imshow(window_name, image);
if (cv::waitKey(100) >= 0) break;
}
return 0;
}

// 动态渐变文字效果
int Displaying_Big_End(cv::Mat& image, const std::string& window_name) {

cv::Size textSize = cv::getTextSize("OpenCV forever!", cv::FONT_HERSHEY_COMPLEX, 3, 5, 0);
cv::Point org((image.cols - textSize.width) / 2, (image.rows + textSize.height) / 2);
cv::Mat image2;

for (int i = 0; i < 255; i += 2) {
image2 = image - cv::Scalar::all(i); // 图像亮度递减
cv::putText(image2, "OpenCV forever!", org, cv::FONT_HERSHEY_COMPLEX, 3,
cv::Scalar(i, i, 255), 5); // 文字颜色渐变
cv::imshow(window_name, image2);
if (cv::waitKey(30) >= 0) break;
}
return 0;
}

int main() {
const int window_width = 800;
const int window_height = 600;
const std::string window_name = "OpenCV Random Drawing";

// 初始化黑底画布
cv::Mat image = cv::Mat::zeros(window_height, window_width, CV_8UC3);
cv::imshow(window_name, image);

// 初始化随机数生成器
cv::RNG rng(0xFFFFFFFF);

// 依次调用绘图函数
Drawing_Random_Lines(image, window_name, rng);
Drawing_Random_Rectangles(image, window_name, rng);
Drawing_Random_Ellipses(image, window_name, rng);
Drawing_Random_Polylines(image, window_name, rng);
Displaying_Random_Text(image, window_name, rng);
Displaying_Big_End(image, window_name);

cv::waitKey(0);
return 0;
}

3. 图像平滑

3.1. 目标

使用 OpenCV 实现图像平滑处理,涵盖四种常用线性滤波器:均值滤波、高斯滤波、中值滤波和双边滤波。掌握 cv::blur()cv::GaussianBlur()cv::medianBlur()cv::bilateralFilter() 等函数的使用方法。


3.2. 理论基础

3.2.1. 均值滤波(Normalized Box Filter)

公式:

K=1KwidthKheight[111111111]

参数说明:

  • KwidthKheight
    表示滤波核的宽度和高度。通常选取奇数(如 3×3、5×5 等),以便确定一个明确的中心像素。

  • 归一化因子 1KwidthKheight
    保证滤波核内所有元素的权重之和为 1,从而在平滑图像时不改变整体亮度。

作用:

  • 均值滤波通过计算邻域内所有像素值的平均值来减少图像噪声,具有简单高效的特点。
  • 但由于对所有邻域像素赋予相同的权重,它在平滑的同时可能会使边缘和细节变得模糊。

3.2.2. 高斯滤波(Gaussian Filter)

公式:

G0(x,y)=Aexp((xμx)22σx2(yμy)22σy2)

参数说明:

  • A
    归一化常数,用于确保整个滤波核的所有权重之和为1。

  • μxμy
    分别为高斯函数在 xy 方向的均值。通常选取核的中心点,即 μx=0, μy=0 (若坐标以核中心为原点),或者直接设置为核的中心像素坐标。

  • σxσy
    分别为高斯函数在 xy 方向的标准差,决定了权重分布的宽度。

    • 较大的 σ 值使得核内权重分布更加平缓,平滑效果更强;
    • 较小的 σ 值使得权重更多集中在中心像素附近,从而在平滑的同时更好地保留局部细节。
    • 当要求各向同性平滑时,可以令 σx=σy=σ

作用:

  • 高斯滤波器能较好地平滑图像,同时在一定程度上保留边缘信息,相比于简单的均值滤波,它更能适应图像的局部特性。
  • 由于权重随距离呈指数衰减,高斯滤波在邻域中心像素附近具有更高的权重,减少了远处像素对结果的影响。

3.2.3. 中值滤波(Median Filter)

公式:

g(i,j)=median{f(i+k,j+l)}

其中, kl 遍历选定的邻域(例如 3×3 或 5×5 窗口)。

参数说明:

  • 窗口大小:
    定义了计算中值时考虑的邻域范围。窗口越大,能够去除的噪声范围就越广,但也可能导致细节损失;窗口较小则保留更多细节,但可能对较大范围的噪声不够鲁棒。

作用:

  • 中值滤波特别适用于去除“椒盐噪声”,因为中值运算能有效抑制孤立的异常值(极大或极小的噪声像素),而不受这些异常值的极端影响。
  • 与均值滤波不同,中值滤波不会产生模糊边缘的现象,能够较好地保留边缘和细节信息。

3.2.4. 双边滤波(Bilateral Filter)

公式:

g(i,j)=k,lf(i+k,j+l)wspace(k,l)wcolor(f(i,j),f(i+k,j+l))k,lwspace(k,l)wcolor(f(i,j),f(i+k,j+l))

参数说明:

  • wspace(k,l)
    空间权重,通常采用高斯函数定义,用于衡量当前像素与邻域像素之间的空间距离影响。
    • 一般形式为:
      wspace(k,l)=exp(k2+l22σs2)
    • 其中, σs 决定了空间距离的衰减速度,较大的 σs 意味着更广的空间范围内的像素会有较高的权重。
  • wcolor(f(i,j),f(i+k,j+l))
    颜色(或灰度)权重,用于衡量中心像素与邻域像素在像素值(颜色或亮度)上的相似程度。
    • 常用形式为:
      wcolor(f(i,j),f(i+k,j+l))=exp((f(i,j)f(i+k,j+l))22σr2)
    • 参数 σr (也称为 σc )控制了对像素值差异的敏感程度。较小的 σr 会使得仅有非常相近像素值的邻域才被赋予较大权重,从而有效保护边缘信息。
  • 窗口大小:
    决定了在空间上参与滤波的邻域范围。通常与 σs 相匹配,确保窗口足够大以包含主要的贡献区域。

作用:

  • 双边滤波在平滑图像噪声的同时,通过结合空间和颜色信息,有效避免了跨边缘的像素混合,从而更好地保留图像的边缘和细节。
  • 适用于需要在降噪的同时保持清晰边缘的图像处理场景,如人像美化和细节增强。

参数说明:

  • src:输入图像
  • dst:输出图像
  • Size(w, h):核大小(需为奇数)
  • Point(-1, -1):锚点位置(默认中心)

3.3. 代码实现

3.3.1. 均值滤波示例

1
2
3
Mat src = imread("lena.jpg", cv::IMREAD_COLOR);
Mat dst;
cv::blur(src, dst, Size(5, 5), cv::Point(-1, -1));

参数说明:

  • src:输入图像
  • dst:输出图像
  • Size(w, h):核大小(需为奇数)
  • Point(-1, -1):锚点位置(默认中心)

3.3.2. 高斯滤波示例

1
cv::GaussianBlur(src, dst, Size(5, 5), 0, 0);

参数 0 表示自动计算标准差。

3.3.3. 中值滤波示例

1
cv::medianBlur(src, dst, 5);

核大小必须为奇数。

3.3.4. 双边滤波示例

1
cv::bilateralFilter(src, dst, 9, 75, 75);

参数说明:

  • 9:邻域直径
  • 75:颜色空间和坐标空间的标准差

3.4. 示例代码

通过循环逐步增大核尺寸,可观察不同滤波器的平滑效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include "opencv2/imgproc.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"

int main() {
// 读取图像
const char* filename = "cat.jpg";
cv::Mat src = cv::imread(cv::samples::findFile(filename), cv::IMREAD_COLOR);
if (src.empty()) {
std::cout << "Error opening image!" << std::endl;
std::cout << "Usage: " << argv[0] << " [image_name -- default lena.jpg]" << std::endl;
return EXIT_FAILURE;
}

// 显示原始图像
cv::namedWindow("Original Image", cv::WINDOW_AUTOSIZE);
cv::imshow("Original Image", src);

// 定义输出图像
cv::Mat dst;

// 均值滤波
for (int i = 1; i < 31; i = i + 2) {
cv::blur(src, dst, cv::Size(i, i), cv::Point(-1, -1));
cv::imshow("Homogeneous Blur", dst);
cv::waitKey(500); // 每500ms更新一次
}

// 高斯滤波
for (int i = 1; i < 31; i = i + 2) {
cv::GaussianBlur(src, dst, cv::Size(i, i), 0, 0);
cv::imshow("Gaussian Blur", dst);
cv::waitKey(500);
}

// 中值滤波
for (int i = 1; i < 31; i = i + 2) {
cv::medianBlur(src, dst, i);
cv::imshow("Median Blur", dst);
cv::waitKey(500);
}

// 双边滤波
for (int i = 1; i < 31; i = i + 2) {
cv::bilateralFilter(src, dst, i, i * 2, i / 2);
cv::imshow("Bilateral Blur", dst);
cv::waitKey(500);
}

// 结束提示
std::cout << "Smoothing process completed!" << std::endl;
cv::waitKey(0);
return 0;
}

3.5. 总结

  • 均值滤波:简单易实现,但容易模糊边缘。适用于噪声较低且对边缘要求不高的场景。
  • 高斯滤波:通过空间加权实现更自然的平滑效果,对细节保留较好。常用于预处理步骤中去除高频噪声。
  • 中值滤波:特别适合去除椒盐噪声,因其中值运算对异常值不敏感,能较好地保护边缘信息。
  • 双边滤波:结合空间和像素值信息,在降噪的同时能较好地保留边缘,适用于高质量图像处理。

实际应用中需根据噪声类型和图像特点选择合适的滤波方法。通过调整核大小和参数,可灵活平衡平滑效果与细节保留。


4. 形态学操作 (morphological operation)

4.1. 腐蚀与膨胀 (erosion and dilation)

形态学操作通过 结构元素(Kernel)对图像进行形状处理,常用于去噪、分离或连接图像元素。
形态学中两种基础操作——腐蚀 (Erosion)膨胀 (Dilation)

  1. 腐蚀:缩小图像中的亮区域,扩大暗区域。
  2. 膨胀:扩大图像中的亮区域,缩小暗区域。

数学表达式如下:

  • 膨胀

dst(x,y)=max(x,y)src(x+x,y+y)

  • 腐蚀

dst(x,y)=min(x,y)src(x+x,y+y)

膨胀和腐蚀和神经网络中的池化操作在操作上有相似之处。

4.1.1. 示例代码

OpenCV提供cv::erodecv::dilate函数实现腐蚀与膨胀。以下代码演示如何通过滑动条动态调整结构元素类型和大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

cv::Mat src, erosion_dst, dilation_dst;
int erosion_elem = 0, dilation_elem = 0; // 结构元素类型 0: 矩形,1: 十字,2: 椭圆
int erosion_size = 0, dilation_size = 0; // 结构元素的大小
const int max_elem = 2, max_kernel_size = 21; // 最大结构元素类型和最大核尺寸

// 函数声明
void Erosion(int, void*);
void Dilation(int, void*);

int main(int argc, char** argv) {
cv::CommandLineParser parser(argc, argv, "{@input | cat.jpg | input image}");
src = cv::imread(cv::samples::findFile(parser.get<cv::String>("@input")), cv::IMREAD_COLOR);
if (src.empty()) {
std::cout << "无法加载图像!" << std::endl;
return -1;
}

// 创建窗口
cv::namedWindow("腐蚀示例", cv::WINDOW_AUTOSIZE);
cv::namedWindow("膨胀示例", cv::WINDOW_AUTOSIZE);
cv::moveWindow("膨胀示例", src.cols, 0);

// 创建滑动条
cv::createTrackbar("结构元素", "腐蚀示例", &erosion_elem, max_elem, Erosion); // 结构元素类型 0: 矩形,1: 十字,2: 椭圆
cv::createTrackbar("核大小", "腐蚀示例", &erosion_size, max_kernel_size, Erosion); // 核尺寸 2*n+1 确保为奇数
cv::createTrackbar("结构元素", "膨胀示例", &dilation_elem, max_elem, Dilation);
cv::createTrackbar("核大小", "膨胀示例", &dilation_size, max_kernel_size, Dilation);

// 初始调用(更新显示效果)
Erosion(0, 0);
Dilation(0, 0);

cv::waitKey(0);
return 0;
}

void Erosion(int, void*) {
// 根据滑动条选择结构元素类型
int erosion_type = cv::MORPH_RECT; // 默认矩形
if (erosion_elem == 1)
erosion_type = cv::MORPH_CROSS; // 十字
else if (erosion_elem == 2)
erosion_type = cv::MORPH_ELLIPSE; // 椭圆

// 获取结构元素(注意使用 cv::getStructuringElement)
cv::Mat element = cv::getStructuringElement(erosion_type,
cv::Size(2 * erosion_size + 1, 2 * erosion_size + 1),
cv::Point(erosion_size, erosion_size));
// 执行腐蚀操作
cv::erode(src, erosion_dst, element);
// 显示结果
cv::imshow("腐蚀示例", erosion_dst);
}

void Dilation(int, void*) {
int dilation_type = cv::MORPH_RECT;
if (dilation_elem == 1)
dilation_type = cv::MORPH_CROSS;
else if (dilation_elem == 2)
dilation_type = cv::MORPH_ELLIPSE;

cv::Mat element = cv::getStructuringElement(dilation_type,
cv::Size(2 * dilation_size + 1, 2 * dilation_size + 1),
cv::Point(dilation_size, dilation_size));
// 执行膨胀操作
cv::dilate(src, dilation_dst, element);
cv::imshow("膨胀示例", dilation_dst);
}

相关函数解释

1. cv::getStructuringElement

功能:生成指定形状和大小的结构元素(Kernel),用于形态学操作。
函数原型

1
cv::Mat cv::getStructuringElement(int shape, cv::Size ksize, cv::Point anchor = cv::Point(-1,-1));

参数说明

  • shape:结构元素的形状,可选值:
    • cv::MORPH_RECT:矩形
    • cv::MORPH_CROSS:十字形
    • cv::MORPH_ELLIPSE:椭圆形
  • ksize:结构元素的大小,通常为奇数(如Size(3, 3))。
  • anchor:锚点位置,默认为中心点(-1, -1)

示例

1
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));

2. cv::erode

功能:对图像进行腐蚀操作,缩小亮区域,扩大暗区域。
函数原型

1
2
3
void cv::erode(cv::InputArray src, cv::OutputArray dst, cv::InputArray kernel, 
cv::Point anchor = cv::Point(-1,-1), int iterations = 1,
int borderType = cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue());

参数说明

  • src:输入图像。
  • dst:输出图像。
  • kernel:结构元素,可通过cv::getStructuringElement生成。
  • anchor:锚点位置,默认为中心点。
  • iterations:腐蚀操作的迭代次数。
  • borderType:边界填充类型。
  • borderValue:边界填充值。

示例

1
2
cv::Mat eroded;
cv::erode(src, eroded, element);

3. cv::dilate

功能:对图像进行膨胀操作,扩大亮区域,缩小暗区域。
函数原型

1
2
3
void cv::dilate(cv::InputArray src, cv::OutputArray dst, cv::InputArray kernel, 
cv::Point anchor = cv::Point(-1,-1), int iterations = 1,
int borderType = cv::BORDER_CONSTANT, const cv::Scalar& borderValue = cv::morphologyDefaultBorderValue());

参数说明

  • 参数与cv::erode相同。

示例

1
2
cv::Mat dilated;
cv::dilate(src, dilated, element);

4. 总结

  • cv::getStructuringElement:生成结构元素,决定腐蚀或膨胀的形状和大小。
  • cv::erode:腐蚀操作,缩小亮区域。
  • cv::dilate:膨胀操作,扩大亮区域。

通过组合这些函数,可以实现图像去噪、边缘检测等形态学操作。


4.2. 高级形态学操作

4.2.1. 开运算 (Opening)

  • 定义:先对图像进行腐蚀,再进行膨胀。
  • 公式
    dst=dilate(erode(src,element))
  • 用途:用于消除图像中较小的亮区域(噪点),同时保留目标的整体形状。

4.2.2. 闭运算 (Closing)

  • 定义:先对图像进行膨胀,再进行腐蚀。
  • 公式
    dst=erode(dilate(src,element))
  • 用途:可填补目标区域内部的小孔洞或暗区域,使整体形态更完整。

4.2.3. 形态学梯度 (Morphological Gradient)

  • 定义:计算膨胀图像与腐蚀图像之间的差值。
  • 公式
    dst=dilate(src,element)erode(src,element)
  • 用途:突出显示图像中物体的边缘信息,有助于轮廓提取。

4.2.4. 顶帽 (Top Hat)

  • 定义:原始图像与其开运算结果之间的差值。
  • 公式
    dst=srcopen(src,element)
  • 用途:用于提取图像中比周围背景亮的小区域或细节特征。

4.2.5. 黑帽 (Black Hat)

  • 定义:闭运算结果与原始图像之间的差值。
  • 公式
    dst=close(src,element)src
  • 用途:可检测出图像中比背景暗的小区域或细微结构。

4.2.6. 击中-不击中 (Hit-or-Miss)

  • 定义:用于在二值图像CV_8UC1)中定位特定像素模式。其核心是通过两个结构元素(Structuring Elements)分别匹配前景和背景的邻域特征,最终通过逻辑与运算确定目标位置。

  • 公式
    AB=(AB1)(AcB2)

    其中:

    • : 腐蚀操作
    • B1:定义前景像素必须匹配的模式
    • B2:定义背景像素必须匹配的模式
    • Ac:输入图像 A 的补集
  • 操作步骤

    1. 使用 B1 对原图 A 进行腐蚀操作
    2. 使用 B2 对补集 Ac 进行腐蚀操作
    3. 将两次腐蚀结果进行逻辑与(AND)

OpenCV 的 MORPH_HITMISS 操作只需要一个结构元素,它会自动将这个结构元素应用到公式中的两个腐蚀操作 B1B2,即假设 B1=B2

用途

  • 形状检测:查找符合特定结构的图案,例如角点、交叉点、细线等。
  • 模式匹配:用于特定形状目标的存在性检测
  • 噪声去除:过滤掉与目标形状不匹配的部分,仅保留符合模板的结构。

4.2.7. 形态学操作对比总结

操作名称 计算方式 主要作用
开运算(Opening) 先腐蚀再膨胀 去除小亮噪点
闭运算(Closing) 先膨胀再腐蚀 填充暗区域小孔洞
梯度(Gradient) 膨胀 - 腐蚀 提取物体边缘
顶帽(Top Hat) 原图 - 开运算 亮细节增强
黑帽(Black Hat) 闭运算 - 原图 暗细节增强
击中-不击中(Hit-or-Miss) 形态学模式匹配 目标形状检测

4.2.8. 示例代码

4.2.8.1. 开运算、闭运算、梯度、顶帽、黑帽

下面的 C++ 示例代码演示了如何利用 OpenCV 实现上述形态学操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <opencv2/opencv.hpp>

int main() {
// 读取图像
cv::Mat src = cv::imread("cat.jpg", cv::IMREAD_COLOR);
if (src.empty()) {
std::cerr << "无法读取图像!请检查路径。" << std::endl;
return -1;
}

// 定义3×3的矩形结构元素
cv::Mat element = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));

// 形态学操作结果
cv::Mat opened, closed, gradient, tophat, blackhat, hit_miss;

// 开运算(去噪点)
cv::morphologyEx(src, opened, cv::MORPH_OPEN, element);

// 闭运算(填充小孔洞)
cv::morphologyEx(src, closed, cv::MORPH_CLOSE, element);

// 形态学梯度(边缘提取)
cv::morphologyEx(src, gradient, cv::MORPH_GRADIENT, element);

// 顶帽(提取比背景亮的区域)
cv::morphologyEx(src, tophat, cv::MORPH_TOPHAT, element);

// 黑帽(提取比背景暗的区域)
cv::morphologyEx(src, blackhat, cv::MORPH_BLACKHAT, element);


// 显示结果
cv::imshow("原图", src);
cv::imshow("开运算", opened);
cv::imshow("闭运算", closed);
cv::imshow("形态学梯度", gradient);
cv::imshow("顶帽", tophat);
cv::imshow("黑帽", blackhat);

cv::waitKey(0);
return 0;
}
4.2.8.2. hit-miss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

int main() {
// 读取图像并转为灰度图
cv::Mat img = cv::imread("leaf.jpg", cv::IMREAD_GRAYSCALE);

if (img.empty()) {
std::cout << "无法打开图像!" << std::endl;
return -1;
}

// 二值化图像
cv::Mat binary_img;
cv::threshold(img, binary_img, 128, 255, cv::THRESH_BINARY);

// 边缘检测 kernel
cv::Mat kernel = (cv::Mat_<int>(3, 3) <<
0,0,0,
1,-1,0,
0,0,0);

// 使用hit-miss操作(这里我们使用morphologyEx来模拟)
cv::Mat hitmiss_img;
cv::morphologyEx(binary_img, hitmiss_img, cv::MORPH_HITMISS, kernel);

// 可视化原始图像和结果
cv::namedWindow("Original Image", cv::WINDOW_NORMAL);
cv::namedWindow("Hit-Miss Result", cv::WINDOW_NORMAL);
cv::imshow("Original Image", binary_img);
cv::imshow("Hit-Miss Result", hitmiss_img);

cv::waitKey(0); // 等待键盘事件
return 0;
}

5. 形态学操作提取水平与垂直线条

在图像处理中,形态学操作是提取特定形状的重要工具。

这里通过从乐谱图像中分离水平线(五线谱)和垂直线(音符符干)并优化边缘效果的例子,对 OpenCV 形态学操作中的膨胀、腐蚀、结构元素等进一步深入了解。

5.1. 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <iostream>

void show_wait_destroy(const char* winname, cv::Mat img);

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
CommandLineParser parser(argc, argv, "{@input | music.jpg | 输入图像}");
Mat src = imread(samples::findFile(parser.get<String>("@input")), IMREAD_COLOR);
if (src.empty()) {
cout << "无法打开或找到图像!\n" << endl;
cout << "用法: " << argv[0] << " <输入图像>" << endl;
return -1;
}

// 显示原始图像
imshow("src", src);

// 如果原始图像不是灰度图,则转换为灰度图
Mat gray;
if (src.channels() == 3) {
cvtColor(src, gray, COLOR_BGR2GRAY);
}
else {
gray = src;
}

// 显示灰度图像
show_wait_destroy("gray", gray);

// 对取反后的灰度图像应用自适应阈值(注意 ~ 符号)
Mat bw;
adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);

// 显示二值图像
show_wait_destroy("binary", bw);

// 创建用于提取水平和垂直线条的图像
Mat horizontal = bw.clone();
Mat vertical = bw.clone();

// 指定水平方向的尺寸
int horizontal_size = horizontal.cols / 30;

// 创建用于形态学操作提取水平线的结构元素
Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));

// 应用形态学操作(腐蚀后膨胀)
erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));

// 显示提取出的水平线
show_wait_destroy("horizontal", horizontal);

// 指定垂直方向的尺寸
int vertical_size = vertical.rows / 30;

// 创建用于形态学操作提取垂直线的结构元素
Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));

// 应用形态学操作(腐蚀后膨胀)
erode(vertical, vertical, verticalStructure, Point(-1, -1));
dilate(vertical, vertical, verticalStructure, Point(-1, -1));

// 显示提取出的垂直线
show_wait_destroy("vertical", vertical);

// 对垂直图像进行反色操作
bitwise_not(vertical, vertical);
show_wait_destroy("vertical_bit", vertical);

// 根据逻辑提取边缘并平滑图像
// 1. 提取边缘
// 2. 膨胀边缘
// 3. 将原图复制到平滑图像中
// 4. 对平滑图像进行模糊处理
// 5. 用平滑图像替换边缘区域的原图

// 第一步:提取边缘
Mat edges;
adaptiveThreshold(vertical, edges, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
show_wait_destroy("edges", edges);

// 第二步:膨胀边缘
Mat kernel = Mat::ones(2, 2, CV_8UC1);
dilate(edges, edges, kernel);
show_wait_destroy("dilate", edges);

// 第三步:将原图复制到平滑图像中
Mat smooth;
vertical.copyTo(smooth);

// 第四步:对平滑图像进行模糊处理
blur(smooth, smooth, Size(2, 2));

// 第五步:用平滑图像替换边缘区域的原图
smooth.copyTo(vertical, edges);

// 显示最终结果
show_wait_destroy("smooth - final", vertical);

return 0;
}

void show_wait_destroy(const char* winname, cv::Mat img) {
imshow(winname, img);
moveWindow(winname, 500, 0);
waitKey(0);
destroyWindow(winname);
}

结果:

结果分析

  • 水平线提取:五线谱的横线被完整保留,音符等小物体被移除;
  • 垂直线提取:音符符干和乐谱小节线被分离,经优化后边缘更平滑;
  • 应用场景:此方法可用于乐谱数字化、表格检测等需要分离规则结构的任务。

5.2. 相关补充

5.2.1. cv::adaptiveThreshold

函数原型:

1
2
3
4
5
6
7
8
9
void cv::adaptiveThreshold(
InputArray src, // 输入图像,必须是8位单通道图像
OutputArray dst, // 输出图像,与输入图像大小相同
double maxValue, // 当像素值满足条件时赋予的最大值(通常为255)
int adaptiveMethod, // 自适应方法
int thresholdType, // 阈值类型
int blockSize, // 邻域区域大小(必须为奇数),用来计算局部阈值
double C // 从计算出的局部阈值中减去的常数,用于微调
);

参数解释

  • src:输入图像,要求是灰度图(8位单通道)。
  • dst:输出图像,二值化后的图像,大小和输入图像相同。
  • maxValue:当像素满足阈值条件时赋予的值,通常设置为255。
  • adaptiveMethod
    • ADAPTIVE_THRESH_MEAN_C:使用邻域内像素的均值作为阈值。
    • ADAPTIVE_THRESH_GAUSSIAN_C:使用邻域内像素的高斯加权和作为阈值,这样会使得靠近中心的像素权重更大。
  • thresholdType
    • THRESH_BINARY:如果像素值大于局部阈值则赋值为 maxValue,否则赋值为0。
    • THRESH_BINARY_INV:与 THRESH_BINARY 相反,即小于局部阈值赋值为 maxValue,大于则赋值为0。
  • blockSize:定义计算局部阈值时所考虑的邻域区域的尺寸(宽度和高度相同),必须为奇数(例如3、5、7...)。
  • C:一个常数,会从计算出的局部阈值中减去,用于调整阈值的效果。正值会使阈值更低,负值则相反。

5.2.2. cv::Mat::copyTo

在 OpenCV 中,cv::Mat::copyTo 有多个重载版本,其中一个带有掩码(mask)的版本,其函数原型如下:

1
void cv::Mat::copyTo(OutputArray dst, InputArray mask) const;
  • 参数
    • OutputArray dst:目标矩阵,用来存放复制后的结果。
    • InputArray mask:掩码矩阵,通常是一个 8 位单通道(CV_8UC1)的图像。掩码中非零值的位置表示对应源图像中的像素会被复制到目标图像中;零值位置则不会发生复制操作。

6. 图像金字塔

图像金字塔是计算机视觉中用于多尺度分析的重要工具,其核心思想是通过 下采样上采样 生成不同分辨率的图像序列。OpenCV提供了高斯金字塔(Gaussian Pyramid)和拉普拉斯金字塔(Laplacian Pyramid)两种实现方式。

6.1. 高斯金字塔

下采样(Downsampling)
i. 对图像进行高斯卷积核滤波,核矩阵为:
1256[1464141624164624362464162416414641]
ii. 去除偶数行和偶数列,得到尺寸为原图1/4的新图像。
iii. 重复上述步骤生成金字塔各层。

上采样(Upsampling)
i. 将图像尺寸扩大两倍,新增行列填充零。
ii. 使用相同的高斯核(乘以4)进行卷积,近似补全缺失像素值。

6.1.1. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/highgui.hpp>

int main(int argc, char** argv) {
const char* window_name = "Pyramids Demo";
const char* filename = argc >=2 ? argv[1] : "chicky_512.png";

// 加载图像
cv::Mat src = cv::imread(cv::samples::findFile(filename));
if (src.empty()) {
std::printf("Error opening image\n");
return EXIT_FAILURE;
}

// 创建窗口并循环处理用户输入
cv::namedWindow(window_name);
for (;;) {
cv::imshow(window_name, src);
char c = (char)cv::waitKey(0);

if (c == 27) { // ESC键退出
break;
} else if (c == 'i') { // 上采样
// 多次上采样会导致图像模糊,因插值过程无法恢复原始高频信息
cv::pyrUp(src, src, cv::Size(src.cols*2, src.rows*2));
std::printf("** Zoom In: Image x2 \n");
} else if (c == 'o') { // 下采样
cv::pyrDown(src, src, cv::Size(src.cols/2, src.rows/2));
std::printf("** Zoom Out: Image /2 \n");
}
}
return EXIT_SUCCESS;
}

效果

  • 下采样示例:对 512×512 图像连续两次下采样,得到 128×128 图像,细节信息逐步丢失。
  • 上采样示例:对下采样后的图像上采样,分辨率恢复但清晰度下降,体现金字塔重建的局限性。

6.2. 拉普拉斯金字塔

拉普拉斯金字塔(Laplacian Pyramid)是高斯金字塔的补充,用于保存图像在不同尺度下的细节信息,从而实现高精度的图像重建。其核心思想是:通过高斯金字塔的下采样与上采样过程,计算相邻层之间的差异,从而捕捉高频细节。

6.2.1. 与高斯金字塔的对应关系

  1. 生成方式
    • 高斯金字塔:通过逐层下采样生成低分辨率图像序列。
    • 拉普拉斯金字塔:通过以下步骤生成:
      • 对高斯金字塔的某一层 Gi+1 进行上采样(使用 cv::pyrUp),得到近似的高分辨率图像 G~i
      • 计算高斯金字塔原层 Gi 与上采样后的近似层 G~i 的差值:Li=GiG~i
      • Li 即为拉普拉斯金字塔的当前层,保存了下采样过程中丢失的高频细节。
  2. 数学意义
    • 拉普拉斯金字塔的每一层 Li 记录了高斯金字塔相邻层之间的残差信息。
    • 通过拉普拉斯金字塔,可以从最低分辨率的高斯层 Gn 逐步向上恢复原始图像:Gi=G~i+Li.
    • 这一特性在图像融合、压缩和超分辨率重建中至关重要。

用一张图来说明图像金字塔

图中拉普拉斯金字塔中的图像实际上对比度很低,所以这里通过 Gamma 校正 使得对比度提高了一些,看的时候方便一些。

6.2.2. 示例代码

以下代码展示了上图的产生过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <opencv2/opencv.hpp>
#include <vector>

void saveGammaImg(cv::String name, cv::Mat& img);

int main() {
cv::Mat src = cv::imread("oiiai.jpg");
if (src.empty()) {
std::cerr << "Error: Could not load image!" << std::endl;
return -1;
}

cv::Mat down1, down2, down3, down4, up1, up2, up3, up4;

cv::pyrDown(src, down1, cv::Size(src.cols / 2, src.rows / 2));
cv::pyrUp(down1, up1, src.size());

cv::pyrDown(down1, down2, cv::Size(down1.cols / 2, down1.rows / 2));
cv::pyrUp(down2, up2, down1.size());

cv::pyrDown(down2, down3, cv::Size(down2.cols / 2, down2.rows / 2));
cv::pyrUp(down3, up3, down2.size());

cv::pyrDown(down3, down4, cv::Size(down3.cols / 2, down3.rows / 2));
cv::pyrUp(down4, up4, down3.size());

cv::Mat L1 = src - up1;
cv::Mat L2 = down1 - up2;
cv::Mat L3 = down2 - up3;
cv::Mat L4 = down3 - up4;

cv::imwrite("oiiai/down1.jpg", down1);
cv::imwrite("oiiai/up1.jpg", up1);
cv::imwrite("oiiai/L1.jpg", L1);

cv::imwrite("oiiai/down2.jpg", down2);
cv::imwrite("oiiai/up2.jpg", up2);
cv::imwrite("oiiai/L2.jpg", L2);

cv::imwrite("oiiai/down3.jpg", down3);
cv::imwrite("oiiai/up3.jpg", up3);
cv::imwrite("oiiai/L3.jpg", L3);

cv::imwrite("oiiai/down4.jpg", down4);
cv::imwrite("oiiai/up4.jpg", up4);
cv::imwrite("oiiai/L4.jpg", L4);

std::vector<cv::String> stringSeed = {"oiiai/L1", "oiiai/L2", "oiiai/L3", "oiiai/L4"};

for (const auto& name : stringSeed) {
cv::Mat img = cv::imread(name + ".jpg", cv::IMREAD_GRAYSCALE);
if (img.empty()) {
std::cerr << "Error: Could not load image " << name << ".jpg" << std::endl;
continue;
}
saveGammaImg(name, img);
}
return 0;
}

void saveGammaImg(cv::String name, cv::Mat& img) {
cv::Mat gamma_corrected;
img.convertTo(gamma_corrected, CV_32F, 1.0 / 255.0);
cv::pow(gamma_corrected, 0.5, gamma_corrected);
gamma_corrected *= 255;
gamma_corrected.convertTo(gamma_corrected, CV_8U);

name += "_gamma.jpg";
cv::imwrite(name, gamma_corrected);
}

高斯下采样:使用 cv::pyrDown 对原始图像下采样,得到低分辨率的高斯层。
高斯上采样:对下采样后的高斯层使用 cv::pyrUp 上采样至原图尺寸。
残差计算:将上采样后的图像与原图相减,得到拉普拉斯金字塔层。
结果分析:拉普拉斯层中亮区域表示高频细节(如边缘、纹理),暗区域表示低频信息已被高斯金字塔保留。

6.2.3. 应用场景

  1. 图像融合
    • 在融合不同曝光的图像时,拉普拉斯金字塔可分别保留各层细节,融合后重建出高质量图像。
    • 例如:将两张图像的拉普拉斯金字塔逐层融合,再通过逆过程重建结果。
  2. 图像压缩
    • 存储拉普拉斯金字塔的残差信息(稀疏矩阵)比直接存储原图更高效。
    • 结合量化技术,可实现高压缩率且保留关键细节。
  3. 图像增强
    • 通过增强拉普拉斯层的高频分量(如边缘锐化),再重建图像以提升清晰度。

6.3. 总结

拉普拉斯金字塔与高斯金字塔共同构成了多尺度图像分析的基础。前者通过残差捕捉细节,后者通过下采样简化结构。在 OpenCV 中,虽然未直接提供拉普拉斯金字塔的函数,但通过组合 cv::pyrUpcv::pyrDown 和矩阵运算即可实现其功能。理解二者的协同作用,将为图像处理任务(如超分辨率、图像融合)提供重要的技术支撑。


7. 基本阈值操作

7.1. 阈值处理基础

阈值处理是图像分割中最简单的方法之一,其核心思想是通过比较像素强度与阈值来区分目标区域与背景。OpenCV 提供的 cv::threshold s函数支持五种阈值操作类型,可将图像转换为二值化或特定处理后的形式。

7.2. 基本阈值操作类型

7.2.1. 二值化阈值(Binary Threshold)

dst(x,y)={maxValif src(x,y)>thresh0otherwise 当像素强度超过阈值时设为最大值(如255),否则设为0。

7.2.2. 反向二值化阈值(Binary Inverted)

dst(x,y)={0if src(x,y)>threshmaxValotherwise 与二值化阈值相反,超过阈值的像素设为0,其余设为最大值。

7.2.3. 截断阈值(Truncate)

dst(x,y)={threshif src(x,y)>threshsrc(x,y)otherwise 超过阈值的像素被截断为阈值,其余保持不变。

7.2.4. 零阈值(Threshold to Zero)

dst(x,y)={src(x,y)if src(x,y)>thresh0otherwise 低于阈值的像素设为0,其余保留原值。

7.2.5. 反向零阈值(Threshold to Zero Inverted)

dst(x,y)={0if src(x,y)>threshsrc(x,y)otherwise 超过阈值的像素设为0,其余保留原值。


7.3. 代码实现与解析

以下是一个完整的阈值处理示例代码,支持通过滑动条动态调整参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "opencv2/highgui.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/imgproc.hpp"
#include <iostream>

using namespace cv;
using std::cout;

int threshold_value = 8;
int threshold_type = 3;
const int max_value = 255;
const int max_type = 4;
const int max_binary_value = 255;

Mat src, src_gray, dst;
const char* window_name = "Threshold Demo";

// 滑动条回调函数
static void Threshold_Demo(int, void*) {
cv::threshold(src_gray, dst, threshold_value, max_binary_value, threshold_type);
cv::imshow(window_name, dst);
}

int main(int argc, char** argv) {
String imageName("iamge.png");
if (argc > 1) imageName = argv[1];

// 读取并转换图像为灰度图
src = cv::imread(cv::samples::findFile(imageName), cv::IMREAD_COLOR);
if (src.empty()) {
std::cout << "无法读取图像: " << imageName << std::endl;
return -1;
}
cv::cvtColor(src, src_gray, cv::COLOR_BGR2GRAY);

// 创建窗口和滑动条
cv::namedWindow(window_name, cv::WINDOW_AUTOSIZE);
cv::createTrackbar("type", window_name, &threshold_type, max_type, Threshold_Demo);
cv::createTrackbar("size", window_name, &threshold_value, max_value, Threshold_Demo);

Threshold_Demo(0, 0); // 初始化
cv::waitKey();
return 0;
}

7.4. 总结

阈值处理是OpenCV中基础的图像分割技术,适用于目标检测、背景分离等场景。通过灵活选择阈值类型和参数,开发者可以快速实现不同需求的分割效果。


8. HSV 通道的阈值操作

8.1. 理论基础

与传统的cv::threshold函数不同,cv::inRange允许通过设定像素值的上下限范围来提取目标区域。HSV(Hue, Saturation, Value)颜色空间因其色相(Hue)通道能独立表示颜色类型,常用于基于颜色的图像分割任务:

  • Hue(色相):表示颜色类型,范围为0-180(OpenCV中常将0-360度映射到此范围)。
  • Saturation(饱和度):从灰度(低饱和度)到纯色(高饱和度)。
  • Value(明度):表示颜色亮度。

颜色空间转换公式(RGB 到 HSV)可通过 cv::cvtColor 实现,具体计算遵循标准转换规则: H=arctan2(3(GB),2RGB)S=max(R,G,B)min(R,G,B)max(R,G,B)V=max(R,G,B)

8.2. 代码实现

以下示例演示如何通过实时视频流进行阈值分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/videoio.hpp"
#include <iostream>

using namespace cv;

// 定义全局变量
const int max_value_H = 180; // Hue范围映射为0-180
const int max_value = 255;
const String window_capture_name = "Video Capture";
const String window_detection_name = "Object Detection";
int low_H = 0, low_S = 0, low_V = 0;
int high_H = max_value_H, high_S = max_value, high_V = max_value;

// Trackbar回调函数,确保阈值范围合法
static void on_low_H_thresh_trackbar(int, void*) {
low_H = std::min(high_H - 1, low_H);
cv::setTrackbarPos("Low H", window_detection_name, low_H);
}

static void on_high_H_thresh_trackbar(int, void*) {
high_H = std::max(high_H, low_H + 1);
cv::setTrackbarPos("High H", window_detection_name, high_H);
}

static void on_low_S_thresh_trackbar(int, void*) {
low_S = std::min(high_S - 1, low_S);
cv::setTrackbarPos("Low S", window_detection_name, low_S);
}

static void on_high_S_thresh_trackbar(int, void*) {
high_S = std::max(high_S, low_S + 1);
cv::setTrackbarPos("High S", window_detection_name, high_S);
}

static void on_low_V_thresh_trackbar(int, void*) {
low_V = std::min(high_V - 1, low_V);
cv::setTrackbarPos("Low V", window_detection_name, low_V);
}

static void on_high_V_thresh_trackbar(int, void*) {
high_V = std::max(high_V, low_V + 1);
cv::setTrackbarPos("High V", window_detection_name, high_V);
}

int main(int argc, char* argv[]) {
// 捕获摄像头画面 或 视频文件
cv::VideoCapture cap;
cap.open(0);
if (!cap.isOpened()) {
std::cerr << "Error: Unable to open video capture!" << std::endl;
return -1;
}

// 创建显示窗口
cv::namedWindow(window_capture_name);
cv::namedWindow(window_detection_name);

// 创建HSV阈值调节Trackbar
cv::createTrackbar("Low H", window_detection_name, &low_H, max_value_H, on_low_H_thresh_trackbar);
cv::createTrackbar("High H", window_detection_name, &high_H, max_value_H, on_high_H_thresh_trackbar);
cv::createTrackbar("Low S", window_detection_name, &low_S, max_value, on_low_S_thresh_trackbar);
cv::createTrackbar("High S", window_detection_name, &high_S, max_value, on_high_S_thresh_trackbar);
cv::createTrackbar("Low V", window_detection_name, &low_V, max_value, on_low_V_thresh_trackbar);
cv::createTrackbar("High V", window_detection_name, &high_V, max_value, on_high_V_thresh_trackbar);

cv::Mat frame, frame_HSV, frame_threshold;
while (true) {
// 捕获视频帧
cap >> frame;
if (frame.empty()) {
std::cerr << "Error: Captured frame is empty!" << std::endl;
break;
}

// 转换到HSV颜色空间
cv::cvtColor(frame, frame_HSV, cv::COLOR_BGR2HSV);

// 应用阈值分割
cv::inRange(frame_HSV, cv::Scalar(low_H, low_S, low_V), cv::Scalar(high_H, high_S, high_V), frame_threshold);

// 显示结果
cv::imshow(window_capture_name, frame);
cv::imshow(window_detection_name, frame_threshold);

// 按'q'或'ESC'键退出
char key = (char)cv::waitKey(30);
if (key == 'q' || key == 27) {
break;
}
}

return 0;
}

8.2.1. 1. 低H(Low H)

  • 作用:设置 色相(Hue)最低阈值
  • 范围:0-180(OpenCV中Hue范围为0-180,而非0-360)。
  • 功能
    通过滑动条调整,选择目标颜色的起始色相值。例如,若要检测红色(Hue约0-10或160-180),可将 Low H 设为0或160。

8.2.2. 2. 高H(High H)

  • 作用:设置 色相(Hue)最高阈值
  • 范围:0-180。
  • 功能
    定义目标颜色的终止色相值。例如,检测红色时,若 Low H=160,则 High H=180 可覆盖红色的色相范围。

8.2.3. 3. 低S(Low S)

  • 作用:设置 饱和度(Saturation)最低阈值
  • 范围:0-255。
  • 功能
    饱和度表示颜色的纯度。低饱和度接近灰色,高饱和度接近纯色。
    通过提高 Low S,可过滤掉灰度区域(如阴影或白色背景)。

8.2.4. 4. 高S(High S)

  • 作用:设置 饱和度(Saturation)最高阈值
  • 范围:0-255。
  • 功能
    限制颜色的最大饱和度。通常保持 High S=255,表示允许所有高饱和度颜色参与检测。

8.2.5. 5. 低V(Low V)

  • 作用:设置 明度(Value)最低阈值
  • 范围:0-255。
  • 功能
    明度表示颜色亮度。通过调整 Low V,可过滤掉过暗区域。例如,Low V=50 会排除亮度低于50的像素。

8.2.6. 6. 高V(High V)

  • 作用:设置 明度(Value)最高阈值
  • 范围:0-255。
  • 功能
    限制颜色的最大亮度。通常保持 High V=255,允许所有亮度区域参与检测。

8.3. 运行结果

程序运行后,两个窗口分别显示原始视频和阈值处理后的二值图像。通过调节滑动条可实时观察不同阈值范围对检测效果的影响,适用于动态环境下的颜色目标跟踪。


9. 参考资料

[1] OpenCV 官方文档

-------------本文结束 感谢您的阅读-------------