【openCV】cv::Mat的创建和赋值,图像像素的读写,算术操作

目录

一.图像对象(cv::Mat)的创建与赋值

1.1.cv::Mat内部的设计

1.2.cv::Mat的克隆方法

1.3.创建空白图像------cv::Mat::zeros函数

[1.3.1.cv::Mat::zeros 的最后一个参数](#1.3.1.cv::Mat::zeros 的最后一个参数)

1.4.ones

1.4.1.cv::Scalar

1.5.演示浅拷贝与深拷贝

1.5.1.浅拷贝(共享数据,只复制头部)

1.5.2.深拷贝(独立数据,完全复制)

二.图像像素的读写操作

2.1.Mat::at (row, col)

2.2.Mat.ptr (row)

三.图像像素的算术操作

3.1.加减乘除函数

3.2.饱和运算(saturate_cast)

3.3.滚动条操作演示

3.4.融合两张图像

[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 里被设计成两个完全独立的部分:

  1. 头部(Header)
  • 是什么 :一个很小的、固定大小的结构体,里面存放的是描述这个矩阵的所有属性,而不是矩阵里的实际数值。

  • 具体包含什么

    • 矩阵的尺寸:行数、列数

    • 数据类型:比如 8 位无符号整数、32 位浮点数等

    • 通道数:1(灰度图)、3(彩色图)等

    • 步长(step):每一行占用的字节数(可能因为内存对齐而比实际像素宽度大)

    • 是否连续存储的标志

    • 引用计数器的地址 :指向一个整数,用来记录有多少个 Mat 对象正在共享同一块数据内存

  • 特点:头部的内存开销极小(通常几十字节),创建和复制非常快。

  1. 数据部分(Data)
  • 是什么:一块真正存储所有矩阵元素(例如图像的像素值)的连续内存区域,通常位于堆上。

  • 如何组织

    • 按行优先顺序存储。

    • 对于多通道图像(如彩色图),每个像素的各通道值是紧挨着存放的(比如 B、G、R、B、G、R......)。

    • 数据可以通过一个指针(data)直接访问。

  • 生命周期 :由引用计数机制自动管理。当最后一个引用该数据的 Mat 对象被销毁时,这块内存会自动释放。

两部分分离带来的关键特性

浅拷贝(共享数据)

  • 当你把一个 Mat 赋值 给另一个 Mat 时,只会复制头部(新头部里的属性可以独立修改,比如只改变行数用来表示 ROI),数据部分不复制,两个对象指向同一块像素内存。
  • 效果:修改其中一个的图像内容,另一个也会跟着变。
  • 引用计数会增加,确保数据不会被提前释放。

深拷贝(独立数据)

  • 如果你需要两个完全独立的矩阵,可以显式要求复制数据。此时会生成一个新的头部,并另外申请一块新的内存,把原数据完整拷贝过去。
  • 效果:两者互不影响。

ROI(感兴趣区域)的高效实现

  • 从一张大图中取出一小块区域时,OpenCV 不会复制像素数据,而是创建一个新的 Mat 头部,其中 data 指针指向原图中那一小块区域的起始地址,同时修改行数、列数和步长。
  • 这是纯头部层面的操作,几乎没有开销。

自动内存管理

  • 你不需要手动 delete 数据内存。每个 Mat 对象在析构时会减少引用计数,当计数降为 0 时自动释放数据部分。
  • 这避免了内存泄漏,也避免了多次释放同一块内存的错误。

1.2.cv::Mat的克隆方法

  1. 浅拷贝(共享数据,只复制头部)
cpp 复制代码
cv::Mat src = cv::imread("test.jpg");

// 方法1:赋值操作符
cv::Mat dst1 = src;          // dst1 与 src 共享同一块像素数据

// 方法2:拷贝构造函数
cv::Mat dst2(src);           // 同样共享数据

修改 dst1 或 dst2 的像素会直接影响 src。

  1. 深拷贝(独立数据,完全复制)
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;
}
相关推荐
南屹川11 小时前
【服务网格】Istio入门:从部署到流量管理实战
人工智能
救救孩子把11 小时前
65-机器学习与大模型开发数学教程-6-1 浮点数精度与数值稳定性
人工智能·机器学习
南屹川11 小时前
【云计算】Kubernetes入门与实践:从部署到运维
人工智能
armwind11 小时前
数字图像处理-9-图像的腐蚀和膨胀
图像处理·计算机视觉
MediaTea11 小时前
AI 术语通俗词典:GRU
人工智能·rnn·深度学习·gru
IT_陈寒11 小时前
Vite踩坑实录:静态资源加载把我搞懵了
前端·人工智能·后端
RSTJ_162511 小时前
PYTHON+AI LLM DAY FIFITY-FIVE
人工智能
jay神11 小时前
垃圾分类识别数据集 | YOLO格式
人工智能·深度学习·目标检测·机器学习·计算机视觉
MobotStone11 小时前
用 AI 写 PRD 的人越来越多,但真正会用的人不到 10%
人工智能