目录
1.3.创建空白图像------cv::Mat::zeros函数
[1.3.1.cv::Mat::zeros 的最后一个参数](#1.3.1.cv::Mat::zeros 的最后一个参数)
[3.4.1.只调节对比度(固定亮度为 0)](#3.4.1.只调节对比度(固定亮度为 0))
[3.4.2.只调节亮度(固定对比度为 1)](#3.4.2.只调节亮度(固定对比度为 1))
一.图像对象(cv::Mat)的创建与赋值
1.1.cv::Mat内部的设计
cv::Mat在 OpenCV 里被设计成两个完全独立的部分:
- 头部(Header)
-
是什么 :一个很小的、固定大小的结构体,里面存放的是描述这个矩阵的所有属性,而不是矩阵里的实际数值。
-
具体包含什么:
-
矩阵的尺寸:行数、列数
-
数据类型:比如 8 位无符号整数、32 位浮点数等
-
通道数:1(灰度图)、3(彩色图)等
-
步长(step):每一行占用的字节数(可能因为内存对齐而比实际像素宽度大)
-
是否连续存储的标志
-
引用计数器的地址 :指向一个整数,用来记录有多少个
Mat对象正在共享同一块数据内存
-
-
特点:头部的内存开销极小(通常几十字节),创建和复制非常快。
- 数据部分(Data)
-
是什么:一块真正存储所有矩阵元素(例如图像的像素值)的连续内存区域,通常位于堆上。
-
如何组织:
-
按行优先顺序存储。
-
对于多通道图像(如彩色图),每个像素的各通道值是紧挨着存放的(比如 B、G、R、B、G、R......)。
-
数据可以通过一个指针(
data)直接访问。
-
-
生命周期 :由引用计数机制自动管理。当最后一个引用该数据的
Mat对象被销毁时,这块内存会自动释放。
两部分分离带来的关键特性
浅拷贝(共享数据)
- 当你把一个 Mat 赋值 给另一个 Mat 时,只会复制头部(新头部里的属性可以独立修改,比如只改变行数用来表示 ROI),数据部分不复制,两个对象指向同一块像素内存。
- 效果:修改其中一个的图像内容,另一个也会跟着变。
- 引用计数会增加,确保数据不会被提前释放。
深拷贝(独立数据)
- 如果你需要两个完全独立的矩阵,可以显式要求复制数据。此时会生成一个新的头部,并另外申请一块新的内存,把原数据完整拷贝过去。
- 效果:两者互不影响。
ROI(感兴趣区域)的高效实现
- 从一张大图中取出一小块区域时,OpenCV 不会复制像素数据,而是创建一个新的 Mat 头部,其中 data 指针指向原图中那一小块区域的起始地址,同时修改行数、列数和步长。
- 这是纯头部层面的操作,几乎没有开销。
自动内存管理
- 你不需要手动 delete 数据内存。每个 Mat 对象在析构时会减少引用计数,当计数降为 0 时自动释放数据部分。
- 这避免了内存泄漏,也避免了多次释放同一块内存的错误。
1.2.cv::Mat的克隆方法
- 浅拷贝(共享数据,只复制头部)
cpp
cv::Mat src = cv::imread("test.jpg");
// 方法1:赋值操作符
cv::Mat dst1 = src; // dst1 与 src 共享同一块像素数据
// 方法2:拷贝构造函数
cv::Mat dst2(src); // 同样共享数据
修改 dst1 或 dst2 的像素会直接影响 src。
- 深拷贝(独立数据,完全复制)
cpp
cv::Mat src = cv::imread("test.jpg");
// 方法1:clone()
cv::Mat dst3 = src.clone(); // 分配新内存,完全拷贝
// 方法2:copyTo()(无掩码)
cv::Mat dst4;
src.copyTo(dst4); // dst4 自动分配内存并拷贝
// 方法3:copyTo() 配合已有的目标矩阵(目标尺寸类型需匹配,否则会自动重新分配)
cv::Mat dst5;
src.copyTo(dst5); // 同上
dst3、dst4 与 src 互不影响。
1.3.创建空白图像------cv::Mat::zeros函数
cv::Mat::zeros 是 OpenCV 中 cv::Mat 类的一个静态成员函数。
它的作用是:创建一个所有元素都为 0 的矩阵(或图像),并返回这个矩阵。
简单理解:你告诉它"我要一个多少行多少列、什么数据类型的全零矩阵",它直接给你造好。
我们直接看一个例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 创建一个 3 行 4 列、单通道 8 位整数的全零矩阵
//方式一
cv::Mat m1 = cv::Mat::zeros(3, 4, CV_8UC1);
std::cout << "m1 = \n" << m1 << std::endl;
//方式二
cv::Mat m2 = cv::Mat::zeros(cv::Size(3, 4), CV_8UC1);
std::cout << "m2 = \n" << m2 << std::endl;
return 0;
}
按照我们的直觉来说,这2个应该是相同的

嗯?m1和m2怎么不一样??
不等价。关键在于参数顺序不同:
- 方式一:cv::Mat::zeros(3, 4, CV_8UC1)表示 3 行,4 列 → 矩阵尺寸为 3×4。
- 方式二:cv::Mat::zeros(cv::Size(3, 4), CV_8UC1)里面的cv::Size(3, 4) 表示 宽度 = 3,高度 = 4 → 对应矩阵 4 行,3 列 → 矩阵尺寸为 4×3。
下面这2种写法才是等价的
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 创建一个 3 行 4 列、单通道 8 位整数的全零矩阵
//方式一
cv::Mat m1 = cv::Mat::zeros(3, 4, CV_8UC1);
std::cout << "m1 = \n" << m1 << std::endl;
//方式二(等价于方式一)
cv::Mat m2 = cv::Mat::zeros(cv::Size(4, 3), CV_8UC1);
std::cout << "m2 = \n" << m2 << std::endl;
return 0;
}

1.3.1.cv::Mat::zeros 的最后一个参数
cv::Mat::zeros 的最后一个参数是 type,它指定了矩阵(或图像)的数据类型和通道数。OpenCV 使用一套统一的宏来定义类型,格式为:
cpp
CV_<位数><数据类型符号>C<通道数>
或者针对浮点数:CV_<位数>F<通道数>(F 表示 float 或 double)。
| 类型宏 | 位数 | 数据类型 | 通道数 | 元素取值范围 | 典型用途 |
|---|---|---|---|---|---|
CV_8UC1 |
8 | unsigned char | 1 | 0~255 | 灰度图、二值掩膜 |
CV_8UC3 |
8 | unsigned char | 3 | 0~255(每个通道) | 彩色图像(BGR 顺序) |
CV_8UC4 |
8 | unsigned char | 4 | 0~255(RGBA/BGRA) | 带透明通道的图像 |
CV_8SC1 |
8 | signed char | 1 | -128~127 | 梯度或带符号的8位数据 |
CV_16UC1 |
16 | unsigned short | 1 | 0~65535 | 深度图、高精度灰度 |
CV_16SC1 |
16 | signed short | 1 | -32768~32767 | 差分图像、光流 |
CV_16SC3 |
16 | signed short | 3 | -32768~32767 | 带符号的彩色数据 |
CV_32SC1 |
32 | signed int | 1 | -2^31~2^31-1 | 标签图、累加器 |
CV_32FC1 |
32 | float | 1 | 约 ±1.18e-38~±3.4e38 | 浮点矩阵、变换矩阵 |
CV_32FC3 |
32 | float | 3 | 浮点范围 | 浮点彩色图像(如 HDR) |
CV_64FC1 |
64 | double | 1 | 约 ±2.2e-308~±1.8e308 | 高精度计算、旋转矩阵 |
注意 :OpenCV 默认彩色图像顺序是 BGR,而不是 RGB。所以
CV_8UC3的三个通道依次是 Blue、Green、Red。
我们可以修改一下
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
// 创建一个 3 行 4 列、三通道 8 位整数的全零矩阵
cv::Mat m1 = cv::Mat::zeros(3, 4, CV_8UC3);
std::cout << "m1 = \n" << m1 << std::endl;
return 0;
}

为什么这里一行有这么多0了?
在 OpenCV 打印 cv::Mat 的输出中,每一个数字对应一个通道的值(上面打印出来的每一个0都代表一个通道)。
我们采用了三通道啊,三个0为一组组成一个像素点,一行就有4个像素点,所以一行就有12个0。
获取行数,列数,通道数
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建一个 3 行 4 列、3 通道(彩色)的矩阵
cv::Mat mat = cv::Mat::zeros(3, 4, CV_8UC3);
// 获取行数、列数、通道数
int rows = mat.rows; // 行数 = 3
int cols = mat.cols; // 列数 = 4
int channels = mat.channels(); // 通道数 = 3
std::cout << "行数: " << rows << std::endl;
std::cout << "列数: " << cols << std::endl;
std::cout << "通道数: " << channels << std::endl;
return 0;
}

回顾
我们回顾一下最后一个参数的选项的格式,我们现在已经知道了这个通道数是啥含义,那么还有一个位数我们需要了解一下
cpp
CV_<位数><数据类型符号>C<通道数>
我们上面说这里面每一个0所在的位置都代表一个通道。

位数"决定了每个通道位置上能存放的数字的范围(也就是"能显示多大")。
-
如果是 8 位 (比如
CV_8UC3),每个通道是一个 0~255 的整数。→ 那个位置上最大只能显示 255,再大就溢出。
-
如果是 16 位 (比如
CV_16UC1),每个通道是 0~65535 的整数。→ 最大能显示 65535。
-
如果是 32 位浮点 (
CV_32FC1),每个通道是一个小数,可以非常大(约 3.4e38),也可以非常小(约 1.2e-38),还可以是负数。
1.4.ones
我们上面使用的是zeros,但是其实也有ones
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat m1 = cv::Mat::ones(3, 4, CV_8UC1);
std::cout << "m1 = \n" << m1 << std::endl;
return 0;
}

但是这个玩意有一个缺陷,就是多通道的时候,只会将第一个通道设置为1
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
cv::Mat m1 = cv::Mat::ones(3, 4, CV_8UC3);//千万不能设置成三通道
std::cout << "m1 = \n" << m1 << std::endl;
return 0;
}


所以就需要特别注意。
当然,赋值的时候,他也是赋值给第一个通道

这里我们去借用一个类型
1.4.1.cv::Scalar
cv::Scalar 是 OpenCV 中非常常用的一个基础类型,主要用于表示颜色或像素值。
cv::Scalar本质上是一个4 维双精度浮点向量,继承自 cv::Vec<double, 4>。它的定义大致如下:
typedef Vec<double, 4> Scalar;
也就是说,它可以同时存储 4 个 double 类型的值,分别对应:
- val[0] -- 第一个通道(B,蓝色)
- val[1] -- 第二个通道(G,绿色)
- val[2] -- 第三个通道(R,红色)
- val[3] -- 第四个通道(Alpha,透明度,默认 0 或不使用)
我们可以使用这个类型来表示颜色(最常用)
OpenCV 中默认的颜色顺序是 BGR(而非 RGB),因此三个通道时,Scalar(b, g, r) 表示一种颜色。第四个通道 alpha 大多在图像透明处理时使用。
例如:
- Scalar(255, 0, 0) → 蓝色(B=255, G=0, R=0)
- Scalar(0, 255, 0) → 绿色
- Scalar(0, 0, 255) → 红色
- Scalar(0, 0, 0) → 黑色
- Scalar(255, 255, 255) → 白色
现在我们就可以借助这个cv::Scalar来给我们的m1进行赋值



还是很容易懂了吧!!!
同时,我们就可以将这个图像给显示出来

同时我们也可以来验证一下
cv::Scalar是不是可以同时存储 4 个 double 类型的值,分别对应:
- val[0] -- 第一个通道(B,蓝色)
- val[1] -- 第二个通道(G,绿色)
- val[2] -- 第三个通道(R,红色)
- val[3] -- 第四个通道(Alpha,透明度,默认 0 或不使用)



可以看到,这个就是BGR!!
1.5.演示浅拷贝与深拷贝
1.5.1.浅拷贝(共享数据,只复制头部)
cpp
cv::Mat src = cv::imread("test.jpg");
// 方法1:赋值操作符
cv::Mat dst1 = src; // dst1 与 src 共享同一块像素数据
// 方法2:拷贝构造函数
cv::Mat dst2(src); // 同样共享数据
修改 dst1 或 dst2 的像素会直接影响 src。
我们先看看这种赋值的

我们打印m1?怎么是一个绿色啊??这就是浅拷贝,我们改变m2就是在改变m1.
当然下面这种情况也是一样的。

1.5.2.深拷贝(独立数据,完全复制)
cpp
cv::Mat src = cv::imread("test.jpg");
// 方法1:clone()
cv::Mat dst3 = src.clone(); // 分配新内存,完全拷贝
// 方法2:copyTo()(无掩码)
cv::Mat dst4;
src.copyTo(dst4); // dst4 自动分配内存并拷贝
dst3、dst4 与 src 互不影响。


完全没有问题。
二.图像像素的读写操作
2.1.Mat::at<Type>(row, col)
Mat::at<Type>(row, col) 是 OpenCV 中 最常用、最安全 的像素访问方法之一。它适用于随机访问(即不连续地读取或修改某个特定位置的像素值),并且提供了运行时类型检查。
2.1 单通道图像
图像类型如
- CV_8UC1(8位无符号单通道) → 使用 uchar
- CV_32FC1(32位浮点单通道) → 使用 float
- CV_64FC1(64位双精度单通道) → 使用 double
示例:
cpp
cv::Mat gray = cv::imread("img.jpg", cv::IMREAD_GRAYSCALE); // CV_8UC1
uchar pixel = gray.at<uchar>(10, 20); // 读取 (10,20) 的灰度值
gray.at<uchar>(10, 20) = 255; // 修改为白色
我们看一个例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建 100x100 的黑色图像(单通道灰度图)
cv::Mat img(100, 100, CV_8UC1, cv::Scalar(0)); // 全黑
// 1. 修改中心像素为白色(最大值 255)
int center_y = img.rows / 2;
int center_x = img.cols / 2;
img.at<uchar>(center_y, center_x) = 255; // 白色
// 2. 读取该像素并打印
uchar pixel = img.at<uchar>(center_y, center_x);
std::cout << "Pixel value: " << (int)pixel << std::endl; // 输出 255
// 3. 修改另一个像素(10,10)为灰色(128)
img.at<uchar>(10, 10) = 128; // 灰色
// 4. 使用 const 版本读取(避免意外修改)
const cv::Mat& const_img = img;
uchar gray_val = const_img.at<uchar>(10, 10); // 只能读不能写
std::cout << "Gray value at (10,10): " << (int)gray_val << std::endl; // 输出 128
return 0;
}

2.2 多通道图像(3通道 BGR 最常用)
图像类型如
- CV_8UC3 → 每个像素由 3 个 uchar 组成 → 使用 cv::Vec3b(长度为 3 的 uchar 向量)
- CV_32FC3 → 使用 cv::Vec3f
- CV_64FC3 → 使用 cv::Vec3d
示例:
cpp
cv::Mat color = cv::imread("img.jpg"); // 默认 CV_8UC3
cv::Vec3b bgr = color.at<cv::Vec3b>(5, 10);
uchar b = bgr[0];
uchar g = bgr[1];
uchar r = bgr[2];
// 直接修改某个通道的值
color.at<cv::Vec3b>(5, 10)[0] = 0; // 将蓝色通道置 0
注意:你不可以使用 color.at<uchar>(row, col) 来访问三通道图像,因为这样会导致类型不匹配,OpenCV 内部会进行错误的字节解释,且如果开启了调试模式会触发断言。
我们看一个例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建 100x100 的黑色图像(3通道 BGR)
cv::Mat img(100, 100, CV_8UC3, cv::Scalar(0, 0, 0));
// 1. 修改中心像素为红色
int center_y = img.rows / 2;
int center_x = img.cols / 2;
img.at<cv::Vec3b>(center_y, center_x) = cv::Vec3b(0, 0, 255); // 注意是BGR
// 2. 读取该像素并打印
cv::Vec3b pixel = img.at<cv::Vec3b>(center_y, center_x);
std::cout << "B: " << (int)pixel[0]
<< " G: " << (int)pixel[1]
<< " R: " << (int)pixel[2] << std::endl;
// 3. 修改特定通道(不改变其他通道)
img.at<cv::Vec3b>(10, 10)[0] = 255; // 设置蓝色通道为 255
// 4. 使用 const 版本读取(避免意外修改)
const cv::Mat& const_img = img;
uchar b_val = const_img.at<cv::Vec3b>(10, 10)[0]; // 只能读不能写
return 0;
}

2.2.Mat.ptr<Type>(row)
单通道
先看看单通道的用法
cpp
cv::Mat gray(480, 640, CV_8UC1); // 480行 × 640列,每个像素是 uchar
// 遍历所有像素,将每个像素赋值为行号(演示用)
for (int y = 0; y < gray.rows; ++y) {
uchar* row_ptr = gray.ptr<uchar>(y); // 获取第 y 行的起始指针
for (int x = 0; x < gray.cols; ++x) {
row_ptr[x] = y; // 等价于 *(row_ptr + x) = y
}
}
// 读取 (y,x) 处的像素值
uchar value = gray.ptr<uchar>(y)[x];
示例:单通道灰度图(3×3,CV_8UC1)
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建 3x3 单通道灰度图像,初始值全为 0
cv::Mat gray(3, 3, CV_8UC1, cv::Scalar(0));
// 使用 ptr 遍历并赋值:每个像素赋值为 (行号*3 + 列号)
for (int y = 0; y < gray.rows; ++y) {
uchar* row = gray.ptr<uchar>(y); // 第 y 行的指针
for (int x = 0; x < gray.cols; ++x) {
row[x] = y * gray.cols + x; // 数值 0,1,2; 3,4,5; 6,7,8
}
}
// 打印所有像素值
std::cout << "单通道灰度图 (3x3) 像素值:" << std::endl;
for (int y = 0; y < gray.rows; ++y) {
uchar* row = gray.ptr<uchar>(y);
for (int x = 0; x < gray.cols; ++x) {
std::cout << (int)row[x] << " ";
}
std::cout << std::endl;
}
return 0;
}

三通道
我们先看看三通道的用法
cpp
cv::Mat color(480, 640, CV_8UC3); // 每个像素由 3 个 uchar 组成
for (int y = 0; y < color.rows; ++y) {
cv::Vec3b* row_ptr = color.ptr<cv::Vec3b>(y); // 每行指针指向 Vec3b
for (int x = 0; x < color.cols; ++x) {
// 方法1:整体赋值
row_ptr[x] = cv::Vec3b(255, 0, 0); // 蓝色像素
// 方法2:单独修改通道
row_ptr[x][0] = 255; // 蓝色通道 B
row_ptr[x][1] = 0; // 绿色通道 G
row_ptr[x][2] = 0; // 红色通道 R
}
}
// 读取 (y,x) 处的蓝色通道值
uchar b = color.ptr<cv::Vec3b>(y)[x][0];//注意是BGR
示例:三通道彩色图(2×2,CV_8UC3,BGR顺序)
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建 2x2 三通道彩色图像,初始值全黑 (B=0,G=0,R=0)
cv::Mat color(2, 2, CV_8UC3, cv::Scalar(0, 0, 0));
// 使用 ptr 遍历并赋值(BGR格式)
for (int y = 0; y < color.rows; ++y) {
cv::Vec3b* row = color.ptr<cv::Vec3b>(y); // 第 y 行的指针,每个元素是 Vec3b
for (int x = 0; x < color.cols; ++x) {
// 给每个像素赋值不同颜色
if (y == 0 && x == 0) row[x] = cv::Vec3b(255, 0, 0); // 蓝色 (B=255)
if (y == 0 && x == 1) row[x] = cv::Vec3b(0, 255, 0); // 绿色 (G=255)
if (y == 1 && x == 0) row[x] = cv::Vec3b(0, 0, 255); // 红色 (R=255)
if (y == 1 && x == 1) row[x] = cv::Vec3b(128, 128, 128); // 灰色 (B=128,G=128,R=128)
}
}
// 打印每个像素的 BGR 值
std::cout << "三通道彩色图 (2x2) 每个像素的 B G R 值:" << std::endl;
for (int y = 0; y < color.rows; ++y) {
cv::Vec3b* row = color.ptr<cv::Vec3b>(y);
for (int x = 0; x < color.cols; ++x) {
std::cout << "(" << (int)row[x][0] << ","
<< (int)row[x][1] << ","
<< (int)row[x][2] << ") ";
}
std::cout << std::endl;
}
return 0;
}

三.图像像素的算术操作
3.1.加减乘除函数
首先我们先看看最基本的加减乘除

当执行 color + cv::Scalar(50, 50, 50) 时,OpenCV 内部会:
**逐像素、逐通道计算和值:新值 = 原值 + 50,**图像整体变亮。
我们看看减法

当执行 color - cv::Scalar(50, 50, 50) 时,OpenCV 内部会:
逐像素、逐通道计算差值:新值 = 原值 - 50,图像整体变暗。

在 color / cv::Scalar(50, 50, 50) 中,OpenCV 内部做了以下操作:
逐像素、逐通道进行除法:新值 = 原值 ÷ 50。
- 由于 color 是 CV_8UC3(整数类型 0~255),OpenCV 不会做整数截断除法,而是先提升为浮点数计算精确商(例如 200.0 / 50.0 = 4.0)。
- 得到浮点结果后,调用 饱和转换 saturate_cast<uchar>(),将浮点数转换为 0~255 范围内的整数。转换规则是四舍五入(接近 0.5 的舍入行为与标准一致),然后裁剪到有效区间。
- 在例子中:
- 原像素值 = 200,除数 = 50 → 200 / 50 = 4.0 → 四舍五入后仍为 4,且在 0~255 内。
- 最终所有像素变为 (4, 4, 4),图像变得非常暗,几乎全黑。
但是,如果我们这样子去使用乘法的话,其实是会报错的!!!!

不能直接去进行使用乘法。
事实上,opencv给我们写好了加减乘除的这些函数,我们只需要调用这些函数即可
这些函数的用法很简单,我们只需要了解下面这种用法即可,后面的我们完全没有必要去了解。
cpp
函数名(图像1, 图像2, 输出图像)
-
图像1 和 图像2:可以是两张图,也可以一张图一个数字(标量)。
-
输出图像:存放结果。
cpp
cv::add(img1, img2, dst); // dst = img1 + img2
cv::subtract(img1, img2, dst); // dst = img1 - img2
cv::multiply(img1, img2, dst); // dst = img1 × img2
cv::divide(img1, img2, dst); // dst = img1 ÷ img2
我们看一下例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
// 创建两个测试图像:全灰 (100) 和 全浅灰 (150)
cv::Mat img1(300, 400, CV_8UC3, cv::Scalar(100, 100, 100));
cv::Mat img2(300, 400, CV_8UC3, cv::Scalar(150, 150, 150));
cv::Mat dst;
// 1. cv::add : 加法 (100+150=250)
cv::add(img1, img2, dst);
cv::imshow("add", dst); // 结果像素值 250
// 2. cv::subtract : 减法 (100-150 = -50 -> 饱和为0)
cv::subtract(img1, img2, dst);
cv::imshow("subtract", dst); // 全黑
// 3. cv::multiply : 逐元素乘法 (100*150=15000 -> 饱和为255)
cv::multiply(img1, img2, dst);
cv::imshow("multiply", dst); // 全白
// 4. cv::divide : 除法 (100/150 ≈ 0.666 -> 四舍五入为1)
cv::divide(img1, img2, dst);
cv::imshow("divide", dst); // 几乎是全黑(值为1)
cv::waitKey(0);
return 0;
}

事实上,上面所有函数都自动执行 饱和运算(saturate_cast)。
3.2.饱和运算(saturate_cast)
saturate_cast 是 OpenCV 中一个非常基础但极其重要的类型转换函数。它的作用概括为一句话:
将一个数值(可能超出目标类型范围)安全地转换为指定类型,超出部分会被"饱和"到该类型的边界值,而不是取模或截断低位。
我们举一个例子:给图像每个像素加 100 亮度
假设我们有一个像素值 200(uchar 类型,范围 0~255),加上 100 后得到 300。
不用 saturate_cast,直接用 C 风格转换:
cpp
uchar pixel = 200;
int temp = pixel + 100; // temp = 300
uchar result = (uchar)temp; // 结果 = 44 ❌ (300 截断低字节)
原本该变亮的地方反而变暗了,完全错误。
用 saturate_cast:
cpp
uchar pixel = 200;
int temp = pixel + 100; // temp = 300
uchar result = cv::saturate_cast<uchar>(temp); // 结果 = 255 ✅ (饱和到最大值)
结果被限制在 255,图像该亮的地方就是最亮,合理。
另一个例子:减法导致负数
像素值 50 减去 100 得 -50。
- 不用 saturate_cast:(uchar)(-50) → 结果 206(因为负数按补码解释)❌
- 用 saturate_cast:cv::saturate_cast<uchar>(-50) → 结果 0(饱和到最小值)✅
3.3.滚动条操作演示
这里使用到了一个新的函数
cv::createTrackbar 是 OpenCV 中用于创建滑动条(Trackbar)的函数。通过滑动条,你可以交互式地调整某个整数变量的值,并实时看到它对图像处理效果的影响(例如调整阈值、亮度、对比度等)。它是 OpenCV 开发 GUI 调试工具最常用的函数之一。
cpp
int createTrackbar(
const String& trackbarname, // 滑动条的名称
const String& winname, // 父窗口的名称
int* value, // 指向整型变量的指针(滑动条的值会存储在这里)
int count, // 滑动条的最大值(最小值固定为 0)
TrackbarCallback onChange = 0, // 回调函数指针(当值改变时调用)
void* userdata = 0 // 传递给回调函数的用户数据(可选)
);
我们来看一个例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
// 全局变量
int blue = 0, green = 0, red = 0;
Mat image;
// 回调函数:当滑动条数值改变时,重新绘制圆
void onTrackbar(int, void*) {
// 创建黑色背景
image = Mat::zeros(400, 400, CV_8UC3);
// 用当前颜色画实心圆
circle(image, Point(200, 200), 100, Scalar(blue, green, red), -1);
imshow("Trackbar Demo", image);
}
int main() {
// 创建窗口
namedWindow("Trackbar Demo");
// 创建三个滑动条:B, G, R
createTrackbar("Blue", "Trackbar Demo", &blue, 255, onTrackbar);
createTrackbar("Green", "Trackbar Demo", &green, 255, onTrackbar);
createTrackbar("Red", "Trackbar Demo", &red, 255, onTrackbar);
// 初始绘制
onTrackbar(0, nullptr);
// 等待按键
waitKey(0);
return 0;
}
这段代码运行后会打开一个名为"Trackbar Demo"的窗口,窗口尺寸为400×400像素,背景为纯黑色。窗口中央有一个半径为100像素的实心圆。同时,窗口上会显示三个滑动条(Trackbar),分别标记为"Blue"(蓝色)、"Green"(绿色)和"Red"(红色),每个滑动条的取值范围都是0到255。
初始时,三个滑动条的值均为0,因此圆呈现黑色(与背景融为一体,看起来就像没有圆)。当用户拖动任意一个滑动条时,圆的颜色会实时发生变化------例如,增加红色滑动条的值,圆会变红;同时调节蓝、绿、红分量的值,则可以混合出任意颜色。整个交互过程会动态更新,直到用户按下任意键退出程序。




非常的厉害。
cv::createTrackbar最后一个参数的含义
我们再看一个例子
cpp
#include <opencv2/opencv.hpp>
using namespace cv;
// 回调函数:通过 userdata 修改外部变量
void on_change(int pos, void* data)
{
// pos: 滑动条的值(输入)
// data: 指向外部变量的指针(可读写),也是createTrackbar最后一个参数传递进来的
int* ptr = (int*)data; // 把 void* 转回 int*
*ptr = pos; // 修改外部的变量
printf("当前值: %d\n", *ptr);
}
int main() {
int myValue = 0; // 要被滑动条控制的值
namedWindow("Test");
createTrackbar("Slide", "Test", &myValue, 100, on_change, &myValue);
waitKey(0);
return 0;
}

3.4.融合两张图像
cv::addWeighted 的作用非常简单:把两张图片按"比例"混合成一张新图片。
你可以想象成调饮料:
- 你有 A 饮料(第一张图)和 B 饮料(第二张图)
- alpha 是 A 饮料要倒多少(比如 0.7)
- beta 是 B 饮料要倒多少(比如 0.3)
- 混合后就是一杯新饮料(混合后的图片)
公式是:
新图片 = 0.7 × A图片 + 0.3 × B图片 + 额外亮度
这个"额外亮度"(gamma)可以让你整体加一点光,让图片更亮或更暗。
cpp
Mat red(100,100,CV_8UC3, Scalar(0,0,255)); // 红色块
Mat blue(100,100,CV_8UC3, Scalar(255,0,0)); // 蓝色块
Mat mix;
addWeighted(red, 0.5, blue, 0.5, 0, mix);
// 结果:紫色块(红+蓝各一半)
- 0.5 和 0.5 表示各占一半
- 0 表示不加额外亮度
- 如果你改成 addWeighted(red, 0.8, blue, 0.2, 0, mix),结果就是 80%红 + 20%蓝,偏红一点的紫色。
现在我们就可以看一个例子了

我们再看一个例子
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main() {
Mat blue(2, 2, CV_8UC3, Scalar(255, 0, 0));
Mat red(2, 2, CV_8UC3, Scalar(0, 0, 255));
Mat mix;
addWeighted(blue, 0.7, red, 0.3, 0, mix);
// 打印混合后第一个像素的 BGR 值
Vec3b pixel = mix.at<Vec3b>(0, 0);
cout << "Blue: " << (int)pixel[0] << " Green: " << (int)pixel[1] << " Red: " << (int)pixel[2] << endl;
// 输出: Blue: 178 Green: 0 Red: 76 (因为 255*0.7 ≈ 178, 255*0.3 ≈ 76)
return 0;
}

3.4.1.只调节对比度(固定亮度为 0)
对比度 指的是图像中亮部和暗部的差异程度。
-
高对比度:亮的地方很亮,暗的地方很暗,黑白分明,图像看起来"鲜艳"、"锐利"、"有层次感"。
-
低对比度:亮的地方不够亮,暗的地方不够暗,整张图灰蒙蒙的,像蒙了一层雾,细节不清晰。
cpp
#include <opencv2/opencv.hpp>
using namespace cv;
Mat src, dst;
int slider = 100; // 0~200 对应 alpha 0~2
void on_trackbar(int, void*) {
double alpha = slider / 100.0; // 范围 0.0 ~ 2.0
addWeighted(src, alpha, src, 0, 0, dst); // beta = 0
imshow("对比度调整", dst);
}
int main() {
src = imread("Qt.png");
if (src.empty()) return -1;
dst = src.clone();
namedWindow("对比度调整");
createTrackbar("对比度 (0~200)", "对比度调整", &slider, 200, on_trackbar);
on_trackbar(0, nullptr);
waitKey(0);
return 0;
}



为什么使用addWeighted(src, alpha, src, 0, 0, dst);就能调节对比度?
因为 addWeighted 的数学公式是:
cpp
dst=α×src1+β×src2+γ
当你写成 addWeighted(src, alpha, src, 0, 0, dst) 时:
第二张图的权重 beta = 0,所以第二张图不起作用。
gamma = 0,没有额外常数加进去。
于是公式简化为:
cpp
dst=α×src
这意味着输出图像的每个像素值都是输入像素值乘以 alpha。
为什么乘以 alpha 就能控制对比度?
对比度本质上是亮部与暗部的差异程度。
假设两个像素原来的亮度是 50(暗)和 150(亮),相差 100。
- 当 alpha = 2:输出变成 100 和 300(截断到 255),差值变成了 155 → 差距变大,对比度提高。
- 当 alpha = 0.5:输出变成 25 和 75,差值只有 50 → 差距变小,对比度降低。
所以:
- alpha > 1 拉伸了整个亮度范围(原来暗的更暗、亮的更亮),增强对比度。
- alpha < 1 压缩了整个亮度范围(原来暗的和亮的都向中间靠拢),减弱对比度。
- alpha = 1 保持原样。
3.4.2.只调节亮度(固定对比度为 1)
只调节亮度的话,其实我们借助这个addWeighted的第5个参数就行了
cpp
#include <opencv2/opencv.hpp>
using namespace cv;
Mat src, dst;
int slider = 100; // 0~200 对应 beta -100~100
void on_trackbar(int, void*) {
int beta = slider - 100; // 范围 -100 ~ 100
addWeighted(src, 1.0, src, 0, beta, dst); // alpha = 1
imshow("亮度调整", dst);
}
int main() {
src = imread("Qt.png");
if (src.empty()) return -1;
dst = src.clone();
namedWindow("亮度调整");
createTrackbar("亮度 (0~200)", "亮度调整", &slider, 200, on_trackbar);
on_trackbar(0, nullptr);
waitKey(0);
return 0;
}

