【openCV】键盘响应,像素逻辑操作,通道分离合并,抠像

一.键盘响应式操作

cv::waitKey 是 OpenCV 中等待用户按键的函数,主要用于:

让窗口保持显示,否则窗口会一闪而过。

获取用户按下的键盘按键(用于交互控制)。

函数原型

复制代码
int waitKey(int delay = 0);

参数 delay:等待时间,单位毫秒。

  • delay > 0:等待 delay 毫秒,若在此期间有按键则立即返回。
  • delay = 0(默认):无限等待,直到用户按任意键。
  • delay < 0:同 =0(某些版本支持,但建议用 0)。

返回值:int 类型,表示按下的键的 ASCII 码。

如果没有按键(超时),返回 -1。

我们看一个例子

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main() {
    Mat img(300, 400, CV_8UC3, Scalar(255, 255, 255));
    putText(img, "Press any key (ESC to quit)", Point(50, 150), 
            FONT_HERSHEY_SIMPLEX, 0.6, Scalar(0, 0, 0), 2);
    imshow("Window", img);

    while (true) {
        int key = waitKey(0);
        if (key == 27) break;  // ESC 退出
        cout << "Pressed: " << (char)key << " (ASCII " << key << ")" << endl;
    }
    return 0;
}

我们必须先将键盘焦点给到这个图像窗口,而不是控制台

基于这个现象,我们可以来做一个测试

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main() {
    Mat src = imread("Qt.png");
    if (src.empty()) {
        cout << "请放置 Qt.png 在程序目录下" << endl;
        return -1;
    }

    Mat current = src.clone();
    namedWindow("色彩空间转换");

    while (true) {
        imshow("色彩空间转换", current);
        int key = waitKey(0);

        if (key == 'a') {
            cvtColor(src, current, COLOR_BGR2GRAY);
            cout << "当前: 灰度图" << endl;
        }
        else if (key == 'b') {
            cvtColor(src, current, COLOR_BGR2HSV);
            cout << "当前: HSV" << endl;
        }
        else if (key == 'c') {
            cvtColor(src, current, COLOR_BGR2YUV);
            cout << "当前: YUV" << endl;
        }
        else if (key == 27) { // ESC
            break;
        }
        // 按其他键不转换
    }

    destroyAllWindows();
    return 0;
}

我们按下a

我们按下b

我们按下c

二.openCV自带颜色表操作

cv::applyColorMap 是 OpenCV 中一个很实用的图像处理函数,它的作用是把灰度图像 (只有黑白亮度信息)转换成伪彩色图像(用不同颜色表示不同的灰度级别)。

对初学者来说,可以这样理解:原本一幅黑白照片,你可以让程序自动给它"上色"------暗的地方用一种颜色,亮的地方用另一种颜色,中间过渡用渐变颜色,这样人眼更容易分辨出细微的亮度变化。

cpp 复制代码
void cv::applyColorMap(
    cv::InputArray src,    // 输入:灰度图像(8位单通道)
    cv::OutputArray dst,   // 输出:伪彩色图像(8位三通道,BGR顺序)
    int colormap           // 预定义的颜色映射类型,如 cv::COLORMAP_JET
);
  • src:输入图像,通常是 CV_8UC1 类型(8位无符号单通道灰度图)。
  • dst:输出图像,会自动创建为 CV_8UC3 类型(8位三通道彩色图)。
  • colormap:一个整型常量,对应 OpenCV 内置的多种配色方案。

对于这个colormap,OpenCV内部有多个选择方案,下面是最常用的一个

cv::COLORMAP_AUTUMN(编号 0)

颜色从红色渐变到橙色再到黄色,就像秋天的树叶。暗部偏红,亮部偏黄,整体温暖。

cv::COLORMAP_BONE(编号 1)

模拟 X 光底片的观感:从深蓝过渡到浅灰最后到白色,带有轻微的蓝‑绿调,看起来像骨骼或医学影像。

cv::COLORMAP_JET(编号 2)

最经典的"彩虹色"方案。从蓝色(最暗)开始,依次变为青色、绿色、黄色,最后到红色(最亮)。颜色对比强烈,视觉上非常醒目。

cv::COLORMAP_WINTER(编号 3)

从深蓝色渐变到浅绿色再到黄绿色,像冬季的冷色调,暗部偏蓝,亮部偏绿。

cv::COLORMAP_RAINBOW(编号 4)

另一种彩虹色方案,与 JET 相似但饱和度更高、色彩过渡更鲜艳,视觉冲击力更强。

cv::COLORMAP_OCEAN(编号 5)

以蓝色为主调,从深蓝逐渐变成浅蓝,再到绿色和白色,像海洋深处到浅滩的颜色。

cv::COLORMAP_HOT(编号 11)

模拟热成像仪的效果:黑色(最冷)→ 暗红 → 亮红 → 黄色 → 白色(最热)。常用于温度或密度分布的可视化。

你可以把 0、1、2、3、4、5、11 这些数字依次传给 applyColorMap,看看同一张灰度图变成什么样,很快就能记住每种映射的风格。

我们看一个简单的例子

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // 1. 读取图像(如果原图是彩色,先转为灰度)
    cv::Mat src = cv::imread("test.jpg");
    if (src.empty())
    {
        std::cout << "无法读取图像" << std::endl;
        return -1;
    }

    cv::Mat gray;
    if (src.channels() == 3)//三通道就说明不是灰度图
        cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    else
        gray = src.clone();   // 已经是灰度图

    // 2. 应用颜色映射
    cv::Mat colorized;
    cv::applyColorMap(gray, colorized, cv::COLORMAP_JET);
    //最经典的"彩虹色"方案。从蓝色(最暗)开始,依次变为青色、绿色、黄色,最后到红色(最亮)。颜色对比强烈,视觉上非常醒目。

    // 3. 显示原始灰度图和伪彩色图
    cv::imshow("原始灰度图", gray);
    cv::imshow("伪彩色图 (JET)", colorized);
    cv::waitKey(0);

    return 0;
}

这个就很厉害,我们也可以看看cv::COLORMAP_AUTUMN的效果

三.图像像素的逻辑操作

操作名 核心函数 二进制运算规则
与 (AND) cv::bitwise_and() 两者均为 1 时,结果才为 1;否则(有 0)结果为 0。
或 (OR) cv::bitwise_or() 只要有一个为 1,结果就为 1;只有两个都是 0 时结果才为 0。
异或 (XOR) cv::bitwise_xor() 两者不同(一个 1、一个 0)时结果为 1;相同(0,0 或 1,1)时结果为 0。
非 (NOT) cv::bitwise_not() 对单个二进制位取反:1 变 0,0 变 1。

这些运算在 OpenCV 中按像素(逐位)对图像执行,常用于掩膜处理、图像合成等场景。

3.1.按位与(AND)

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // 创建 300x300 的单通道灰度图像(黑色背景)
    cv::Mat rect_img = cv::Mat::zeros(300, 300, CV_8UC1);
    cv::Mat circle_img = cv::Mat::zeros(300, 300, CV_8UC1);

    // 在 rect_img 上画一个白色实心矩形
    cv::rectangle(rect_img, cv::Point(25, 25), cv::Point(275, 275), cv::Scalar(255), -1);
    // 在 circle_img 上画一个白色实心圆形
    cv::circle(circle_img, cv::Point(150, 150), 150, cv::Scalar(255), -1);

    // 显示两个原始形状
    cv::imshow("矩形", rect_img);
    cv::imshow("圆形", circle_img);

    cv::Mat result_and;
    cv::bitwise_and(rect_img, circle_img, result_and);
    cv::imshow("按位与(AND)", result_and);
    cv::waitKey(0);
}
  • 效果:只有矩形和圆形重叠的区域会变成白色,其余为黑色。

3.2.按位或(OR)

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // 创建 300x300 的单通道灰度图像(黑色背景)
    cv::Mat rect_img = cv::Mat::zeros(300, 300, CV_8UC1);
    cv::Mat circle_img = cv::Mat::zeros(300, 300, CV_8UC1);

    // 在 rect_img 上画一个白色实心矩形
    cv::rectangle(rect_img, cv::Point(25, 25), cv::Point(275, 275), cv::Scalar(255), -1);
    // 在 circle_img 上画一个白色实心圆形
    cv::circle(circle_img, cv::Point(150, 150), 150, cv::Scalar(255), -1);

    // 显示两个原始形状
    cv::imshow("矩形", rect_img);
    cv::imshow("圆形", circle_img);

    cv::Mat result_or;
    cv::bitwise_or(rect_img, circle_img, result_or);
    cv::imshow("按位或(OR)", result_or);
    cv::waitKey(0);
}
  • 效果:矩形和圆形覆盖的所有区域都会变成白色(重叠部分只显示一次)。

3.3.按位异或(XOR)

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // 创建 300x300 的单通道灰度图像(黑色背景)
    cv::Mat rect_img = cv::Mat::zeros(300, 300, CV_8UC1);
    cv::Mat circle_img = cv::Mat::zeros(300, 300, CV_8UC1);

    // 在 rect_img 上画一个白色实心矩形
    cv::rectangle(rect_img, cv::Point(25, 25), cv::Point(275, 275), cv::Scalar(255), -1);
    // 在 circle_img 上画一个白色实心圆形
    cv::circle(circle_img, cv::Point(150, 150), 150, cv::Scalar(255), -1);

    // 显示两个原始形状
    cv::imshow("矩形", rect_img);
    cv::imshow("圆形", circle_img);

    cv::Mat result_xor;
    cv::bitwise_xor(rect_img, circle_img, result_xor);
    cv::imshow("按位异或(XOR)", result_xor);
    cv::waitKey(0);
}
  • 效果 :矩形或圆形中 不重叠 的区域变成白色,重叠部分变黑。

3.4.按位非(NOT)

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    // 创建 300x300 的单通道灰度图像(黑色背景)
    cv::Mat rect_img = cv::Mat::zeros(300, 300, CV_8UC1);
    cv::Mat circle_img = cv::Mat::zeros(300, 300, CV_8UC1);

    // 在 rect_img 上画一个白色实心矩形
    cv::rectangle(rect_img, cv::Point(25, 25), cv::Point(275, 275), cv::Scalar(255), -1);
    // 在 circle_img 上画一个白色实心圆形
    cv::circle(circle_img, cv::Point(150, 150), 150, cv::Scalar(255), -1);

    // 显示两个原始形状
    cv::imshow("矩形", rect_img);
    cv::imshow("圆形", circle_img);

    cv::Mat result_not;
    cv::bitwise_not(circle_img, result_not);
    cv::imshow("按位非(NOT) - 圆形取反", result_not);
    cv::waitKey(0);
}
  • 效果:原图像中白色区域变黑,黑色区域变白。

四.通道分离与合并

在OpenCV里,图像通常是用"通道"来存放颜色信息的。

你可以把一张彩色图片想象成一幅由三种颜色叠加在一起的画------也就是红色、绿色和蓝色。

这三种颜色各自独立存在,就像三张透明的薄膜叠在一起,透过它们你才能看到完整的彩色图像。

通道分离,就是把这张叠在一起的彩色图片拆开,还原成三张独立的"半透明胶片":一张只显示红色信息(所以画面看起来是灰白的,越红的地方越亮),一张只显示绿色信息,一张只显示蓝色信息。拆开之后,你就可以单独处理其中一种颜色,比如调整红色的亮度,或者只保留绿色通道来观察画面中的植物。

通道合并则是反过来:当你分别处理完这三个通道之后,再把它们按红、绿、蓝的顺序重新叠在一起,就能恢复出一张完整的彩色图片。这就像把三张原本分离的透明胶片重新对齐、叠好,颜色又回来了。

除了常见的红绿蓝三通道,图像也可能是灰度图(只有一个通道)或者带透明度的图(比如RGBA四通道)。但不管有多少个通道,分离和合并的基本思想都是一样的:拆开成独立的单层,处理完再按顺序叠回去

4.1.通道分离(cv::split 函数)

把一个多通道图像(比如彩色图的 B、G、R 三个通道交织在一起)"拆开",变成几个独立的单通道图像(也就是三张"灰度图")。拆出来的每一个,都对应着原图的一个颜色分量

它的运作机制可以从输入、输出、转换方式三个层面来理解:

  • 输入 (src):任意一个多通道的 Mat 对象(即图像矩阵)。
  • 输出 (mv):需要一个容器(vector<Mat> 数组或列表)来接收分离出的单通道图像。
  • 通道拆分:逐一提取源图像每个位置的像素值,按顺序放入输出容器的每个 Mat 对象中。彩色图像的本质就是由 B、G、R 这 3 个单通道数据合并构成的。

我们这里需要注意一些事情

当你用 cv::imread("test.jpg") 读取一张普通彩色照片时,OpenCV 默认将它以CV_8UC3类型读入内存。

CV_8UC3:8位无符号 3通道(B、G、R)。

然后你用 cv::split 把这个三通道图像拆开,得到三个独立的 cv::Mat 对象。每个对象存放一个颜色通道的数据(比如所有蓝色像素)。因为每个通道的每个像素仍然是一个 0~255 的数值(表示该颜色的强度),所以每个通道的类型自动变为 CV_8UC1------ 8位无符号单通道。

也就是说,channels[0]、channels[1]、channels[2] 的类型都是 CV_8UC1,也就是灰度图

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 读取一张彩色图像(BGR顺序)
    cv::Mat image = cv::imread("test.png");
    if (image.empty()) {
        std::cout << "无法读取图像,请检查路径" << std::endl;
        return -1;
    }

    // 2. 存放分离后通道的容器
    std::vector<cv::Mat> channels;

    // 3. 通道分离:channels[0]=蓝, channels[1]=绿, channels[2]=红
    cv::split(image, channels);

    // 4. 显示原图以及三个独立的通道(均为灰度样式)
    cv::imshow("Original Color Image", image);
    cv::imshow("Blue Channel", channels[0]);
    cv::imshow("Green Channel", channels[1]);
    cv::imshow("Red Channel", channels[2]);

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

4.2.通道合并(cv::merge函数)

  1. cv::merge 的基本任务

当你对一张彩色图像执行 cv::split 后,会得到若干个独立的单通道图像(比如 B、G、R 三张灰度图)。如果你想把这些分散的通道重新"粘"回一张彩色图,就需要用到 cv::merge。

它的核心工作就是:

  • 输入: 一组单通道图像(每个图像大小、数据类型完全相同)
  • 输出: 一张多通道图像,其中每个像素的各个分量分别来自输入通道对应位置的灰度值。
  1. 输入输出细节

输入:通常是一个 std::vector<cv::Mat> 容器,里面按顺序存放着多个单通道 Mat 对象。也可以是数组形式。例如,vector<Mat> channels 中有 3 个通道,那么 channels[0] 会成为目标图像的第1通道,channels[1] 是第2通道,依此类推。

输出:一个 cv::Mat 对象,它的通道数等于输入通道的数量,数据类型与输入通道相同(比如都是 CV_8UC1 的话,输出就是 CV_8UC3)。

合并顺序至关重要:cv::merge 会严格按照输入容器的顺序来排列通道。对于 BGR 图像,分离后通常是**[蓝, 绿, 红]**,那么合并时也必须以相同的顺序传入,才能恢复原样。如果顺序弄错,比如 [红, 绿, 蓝] 合并,得到的彩色图像颜色就会完全错乱。

我们自然而然就会写出下面这样子的程序

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 读取一张彩色图像(BGR顺序)
    cv::Mat image = cv::imread("test.jpg");
    if (image.empty()) {
        std::cout << "无法读取图像,请检查路径" << std::endl;
        return -1;
    }

    // 2. 原始通道分离
    std::vector<cv::Mat> originalChannels;
    cv::split(image, originalChannels);  // [0]=蓝, [1]=绿, [2]=红

    // -------------------- 情况1:去掉蓝色通道 --------------------
    std::vector<cv::Mat> noBlue = originalChannels;   // 拷贝一份
    //分离出来的通道本来就是 CV_8UC1,所以修改它们(比如置零)时,也要用同样类型的矩阵。
    // 这样合并后才能正确恢复成一张 CV_8UC3 的彩色图。
    noBlue[0] = cv::Mat::zeros(image.size(), CV_8UC1); // 蓝色通道置0
    cv::Mat imageNoBlue;
    cv::merge(noBlue, imageNoBlue);

    // -------------------- 情况2:去掉绿色通道 --------------------
    std::vector<cv::Mat> noGreen = originalChannels;  // 拷贝一份
    noGreen[1] = cv::Mat::zeros(image.size(), CV_8UC1); // 绿色通道置0
    cv::Mat imageNoGreen;
    cv::merge(noGreen, imageNoGreen);

    // -------------------- 情况3:去掉红色通道(你原有的) --------------------
    std::vector<cv::Mat> noRed = originalChannels;    // 拷贝一份
    noRed[2] = cv::Mat::zeros(image.size(), CV_8UC1);  // 红色通道置0
    cv::Mat imageNoRed;
    cv::merge(noRed, imageNoRed);

    // 6. 显示所有结果
    cv::imshow("原始图像", image);
    cv::imshow("去掉蓝色 (B=0)", imageNoBlue);
    cv::imshow("去掉绿色 (G=0)", imageNoGreen);
    cv::imshow("去掉红色 (R=0)", imageNoRed);

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

我们发现这个预期和我们想象中的不太一样

我们仔细想想它的情况应该是下面这样子

  • 去掉蓝色,也就是只剩下绿色+红色,混合之后是黄色
  • 去掉蓝色+绿色,也就是只剩下红色,显示就是红色
  • 去掉蓝色绿色红色,剩下黑色

但是这就和我们想象中不一样了,我们想象中的是

  • 去掉蓝色,也就是只剩下绿色+红色,混合之后是黄色
  • 去掉绿色,也就是只剩下红色+蓝色,混合显示就是紫色
  • 去掉红色,也就是只剩下绿色+蓝色,混合显示就是青色

其实这个都是浅拷贝惹的祸!!!

在 OpenCV 中,std::vector<cv::Mat> noBlue = originalChannels; 这样的拷贝,并不会复制每个 Mat 内部的像素数据,而只是复制了每个 Mat 的头部信息(包括指向同一块像素数据的指针)。也就是说,noBlue[0]、noGreen[0]、noRed[0] 以及 originalChannels[0] 这四个对象实际上都指向同一块内存,里面存放着原始图像的蓝色通道数值。绿色通道、红色通道同理。

这是一张共享数据的"引用网":任何通过任何一个 Mat 对象直接修改像素值(例如调用 setTo()、at<uchar>() 赋值等),都会同步影响到所有指向该数据的 Mat。

正确的写法就是使用深拷贝!!!

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 读取一张彩色图像(BGR顺序)
    cv::Mat image = cv::imread("test.jpg");
    if (image.empty()) {
        std::cout << "无法读取图像,请检查路径" << std::endl;
        return -1;
    }

    // 2. 原始通道分离
    std::vector<cv::Mat> originalChannels;
    cv::split(image, originalChannels);  // [0]=蓝, [1]=绿, [2]=红

    // -------------------- 情况1:去掉蓝色通道(深拷贝) --------------------
    std::vector<cv::Mat> noBlue;
    for (const auto& ch : originalChannels) {
        noBlue.push_back(ch.clone());   // 每个通道深拷贝
    }
    noBlue[0] = cv::Mat::zeros(image.size(), CV_8UC1); // 蓝色通道置0
    cv::Mat imageNoBlue;
    cv::merge(noBlue, imageNoBlue);

    // -------------------- 情况2:去掉绿色通道(深拷贝) --------------------
    std::vector<cv::Mat> noGreen;
    for (const auto& ch : originalChannels) {
        noGreen.push_back(ch.clone());   // 深拷贝
    }
    noGreen[1] = cv::Mat::zeros(image.size(), CV_8UC1); // 绿色通道置0
    cv::Mat imageNoGreen;
    cv::merge(noGreen, imageNoGreen);

    // -------------------- 情况3:去掉红色通道(深拷贝) --------------------
    std::vector<cv::Mat> noRed;
    for (const auto& ch : originalChannels) {
        noRed.push_back(ch.clone());     // 深拷贝
    }
    noRed[2] = cv::Mat::zeros(image.size(), CV_8UC1);  // 红色通道置0
    cv::Mat imageNoRed;
    cv::merge(noRed, imageNoRed);

    // 显示所有结果
    cv::imshow("原始图像", image);
    cv::imshow("去掉蓝色 (B=0)", imageNoBlue);
    cv::imshow("去掉绿色 (G=0)", imageNoGreen);
    cv::imshow("去掉红色 (R=0)", imageNoRed);

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

五.图像色彩空间转换

5.1.色彩空间是什么

阳光通过三棱镜会分解成红、橙、黄、绿、蓝、靛、紫------这是连续的物理光谱。但是计算机和人类不可能存储无穷多种颜色,所以我们需要一种有限的、数字化的方式来描述颜色。

色彩空间就是一套规则,它规定了:

  • 用哪几个"基本量"来描述颜色(比如用三个数字)
  • 这三个数字分别代表什么物理含义
  • 所有可能的颜色如何用这三个数字的组合来表示

你可以把色彩空间想象成三维坐标系,就像空间中的点用 (x, y, z) 表示一样,颜色也用三个坐标值来表示。

我们举一个例子来:

  • 例子1:RGB 色彩空间

RGB 是"红(Red)、绿(Green)、蓝(Blue)"的缩写。

原理:通过三种颜色的光按不同强度混合,可以产生几乎所有人眼能看到的颜色。你的手机屏幕、电脑显示器就是通过红、绿、蓝三个小灯来显示所有颜色的。

表示法:每个颜色用一个三元组 (R, G, B) 表示,通常每个分量范围 0~255(0 表示没有该颜色,255 表示该颜色最强)。

  • (255,0,0) 表示纯红
  • (0,255,0) 表示纯绿
  • (0,0,255) 表示纯蓝
  • (255,255,255) 表示白色
  • (0,0,0) 表示黑色
  • (128,128,128) 表示中灰色

所以当你拍一张彩色照片,计算机存储的是每个像素的 (R,G,B) 三个数字。这就是RGB 色彩空间。

  • 例子2:灰度色彩空间

灰度色彩空间只有一个数字:亮度。0 表示黑,255 表示白,中间的数字表示不同深浅的灰色。

所以灰度图只有亮度信息,没有颜色信息。

  • 例子3:HSV 色彩空间

HSV 是色调(Hue)、饱和度(Saturation)、明度(Value)的缩写。

  • 色调(H):表示是什么颜色,用角度 0°~360° 表示,0° 红色,120° 绿色,240° 蓝色等。
  • 饱和度(S):表示颜色浓淡,0% 是灰色,100% 是纯彩色。
  • 明度(V):表示颜色有多亮,0% 是黑色,100% 是最亮。

HSV 更符合人类直觉:比如你想调一个"更红的颜色",你会改变色调;想让它"更鲜艳",就增加饱和度;想让它"更亮",就增加明度。

什么是"转换"?

转换就是:已知某个像素在一种色彩空间下的坐标值(比如 RGB 值),通过一个数学公式,计算出它在另一种色彩空间下的坐标值(比如 HSV 值)。

因为两个色彩空间描述的是同一个颜色,只是用了不同的语言。就像你手里有一杯水:你可以说它的体积是 500 毫升,也可以说它是 0.5 升。数字变了,但水量没变。同样,颜色没变,只是表达方式变了。

举例:RGB 转灰度

有一个很经典的公式:

cpp 复制代码
灰度 = 0.299 × R + 0.587 × G + 0.114 × B

比如一个像素的 RGB 是 (255, 100, 50)(很亮的红橙色)。

计算:0.299×255 ≈ 76.2,0.587×100 = 58.7,0.114×50 = 5.7,总和 ≈ 140.6。

四舍五入后灰度值 141。

所以这个像素在灰度空间里是 141(中等偏亮的灰色)。

这个转换就是色彩空间转换的一个具体例子。

三、为什么要做这种转换?(重点)

既然图像本来已经是 RGB 了,为什么还要转成别的?因为不同的图像处理任务在不同的色彩空间里做起来更简单、更准确。

场景1. 想把红色变得更红,但不改变亮度

在 RGB 空间里,红色是 (255,0,0)。你想让它"更红",只能把红色分量增大------但最大值已经是 255 了。而且你稍微改一下三个数字,亮度也会跟着变,很难控制。

显然我们就是不能在RGB图像色彩空间里面进行调整的,我们需要转换到HSV图像色彩空间去。

HSV 空间里,颜色、饱和度、亮度是三个独立的分量。

  • 色调(H)决定是什么颜色(红、绿、蓝...)

  • 饱和度(S)决定颜色浓不浓

  • 明度(V)决定亮不亮

你只要增加饱和度 S ,颜色就会变得更浓、更鲜艳,而不会改变它是红色还是亮度。如果你想保持亮度只改颜色,那就改 H。分开控制,简单直接。

我们如果想要调整饱和度,就先将图像色彩空间从RGB转换成HSV,调整完饱和度了之后,我们再转换回我们的RGB图像色彩空间。

场景2:想把彩色照片变成黑白效果

  • 不需要手动调整每个像素的RGB值,只需将图像从 RGB 色彩空间转换为灰度(GRAY)色彩空间即可。
  • 很多滤镜就是这样做的。

场景3:改变照片的"色调"而不改变亮度

  • 在 RGB 里,你想把整张照片变得偏红一些,你会把每个像素的 R 分量增加。但这样做同时也会改变亮度(因为红色更亮了)。
  • 在 HSV 里,亮度是单独一个通道 V。你只需要改变 H(色调)通道,保持 S 和 V 不变,就能改变颜色而保持亮度完全不变。这样调色效果更自然。

场景4:颜色识别/物体跟踪

  • 比如你想写一个程序追踪一个红色小球。在 RGB 空间里,"红色"没有一个简单的范围,因为受到光线亮度影响,红色可能看起来像粉色、暗红、橙色等。
  • 但在 HSV 空间里,红色对应 H 值大约 0~10 和 160~180,而且你可以过滤掉太暗(V 太低)或太灰(S 太低)的像素,就能更稳定地找到红色物体。

场景5:图像压缩(视频、JPEG)

  • 视频编码常用 YCbCr 色彩空间:Y 是亮度,Cb/Cr 是蓝色和红色的色差。因为人眼对亮度细节敏感,对颜色细节不敏感,可以大幅压缩颜色信息而不易察觉。
  • 这也是为什么很多格式需要从 RGB 转到 YCbCr。

5.2.OpenCV 中的色彩空间转换( cv::cvtColor() 函数)

OpenCV 提供了 cv::cvtColor() 函数,专门做这件事。

第一个参数:输入图像,cv::Mat类型

第二个参数:输出图像,cv::Mat类型

第三个参数:转换类型(或称为转换代码),是一个整数常量,用来指定从哪种色彩空间转换到哪种色彩空间。OpenCV 在 cv::ColorConversionCodes 枚举中定义了数百种转换代码,最常见的是 COLOR_BGR2GRAY、COLOR_BGR2HSV、COLOR_HSV2BGR、COLOR_BGR2RGB 等。

作用:告诉函数使用哪个数学公式来重新计算每个像素的颜色值。它是整个转换的核心指令。

命名规则:通常形式为 COLOR_<源空间>2<目标空间>,例如:

  • COLOR_BGR2GRAY:BGR 彩色 → 灰度
  • COLOR_GRAY2BGR:灰度 → BGR(输出三个通道,每个通道的值都等于原灰度值)
  • COLOR_BGR2HSV:BGR → HSV
  • COLOR_HSV2BGR:HSV → BGR

BGR 和 RGB 是两种不同的通道顺序,不是同一个东西。不过因为它们在数值上很像(都是三个通道,只是排列顺序不同),所以很容易被误认为是一回事。它们本质的区别

  • RGB:第一个分量是红色(R),第二个是绿色(G),第三个是蓝色(B)。
  • BGR:第一个分量是蓝色(B),第二个是绿色(G),第三个是红色(R)。

对于同一个颜色(比如纯红色),RGB 存为 (255, 0, 0),而 BGR 存为 (0, 0, 255)。数字完全不同,如果不做转换,直接按 RGB 去读 BGR 的数据,红色就会变成蓝色。

在 OpenCV 里,你用 imshow 显示图像时,它自动帮你把 BGR 转成了屏幕能正确显示的颜色,所以你看到的颜色是正常的,感觉不到区别。

示例

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

void colorSpace_Demo(cv::Mat& image)
{
    cv::Mat gray, hsv;
    cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);
    cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
    cv::imshow("HSV", hsv);
    cv::imshow("灰度", gray);
    cv::imwrite("D:/hsv.png", hsv);
    cv::imwrite("D:/gray.png", gray);
}

int main() {
    cv::Mat img = cv::imread("test.jpg", cv::IMREAD_UNCHANGED);
    if (img.empty()) {
        std::cout << "错误:无法加载图片,请检查路径!" << std::endl;
        return -1;
    }
    colorSpace_Demo(img);
    cv::imshow("Window1", img);//注意这里的窗口名称必须与上面的一致
    cv::waitKey(0);  // 等待任意按键后关闭窗口
    return 0;
}

我们看看效果

我们去看看保存的情况,和我们这里显示的是一样的。

5.3.抠像------cv::inRange

cv::inRange 是 OpenCV 中用于将图像像素按数值范围进行二值化的函数。它会检查输入图像的每个像素,如果该像素的所有通道值都落在指定的下限和上限之间(包含边界),则输出图像对应位置设为 255(白色);否则设为 0(黑色)。最终生成一张单通道的二值掩码(mask)图像。

cpp 复制代码
void cv::inRange(
    InputArray src,     // 输入图像,可以是单通道或多通道(如BGR、HSV)
    InputArray lowerb,  // 下界数组,与src同通道数
    InputArray upperb,  // 上界数组,与src同通道数
    OutputArray dst     // 输出二值图像(CV_8UC1)
);

参数说明

  • src:输入图像,通常是 uint8 类型(0~255)的灰度图或多通道图。
  • lowerb / upperb:下界和上界,必须与 src 有相同的通道数。可以是标量(如 Scalar(0, 0, 0))或数组。每个通道独立比较。
  • dst:输出掩码,大小与 src 相同,类型为 CV_8UC1(0 或 255)。

比较规则

  • 对于多通道图像,所有通道的值都必须满足 lowerb[i] <= src[i] <= upperb[i],该像素才被设置为 255。
  • 只要有一个通道超出范围,该像素就是 0。

怎么理解这么一句话呢?

  • 下界 lowerb = (100, 50, 80)

  • 上界 upperb = (200, 150, 180)

  • 条件:R 在 100~200 之间,G 在 50~150 之间,B 在 80~180 之间

    三个通道同时满足时输出 255,否则输出 0。

图像四个像素的通道值

像素 R G B
A 150 100 120
B 180 60 90
C 210 120 150
D 120 200 100

逐像素判断

  1. 像素 A :R=150 (100~200 ✅)

    G=100 (50~150 ✅)

    B=120 (80~180 ✅)

    → 所有通道在范围内 → 输出 255

  2. 像素 B :R=180 (100~200 ✅)

    G=60 (50~150 ✅)

    B=90 (80~180 ✅)

    → 所有通道在范围内 → 输出 255

  3. 像素 C :R=210 → 超过了上界 200 ❌

    (G=120、B=150 即使都在范围内,但 R 已不满足)

    输出 0

  4. 像素 D :R=120 ✅,G=200 → 超过了上界 150 ❌

    输出 0

最终掩码(二值图像)

  • 像素 A:255(白)

  • 像素 B:255(白)

  • 像素 C:0(黑)

  • 像素 D:0(黑)

这个例子清楚表明:只要有一个通道的值落在指定区间之外,整个像素就被判为 0,而不是每个通道独立输出一张掩码。cv::inRange 要求"所有通道同时满足"才保留。

我们直接看例子

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 读取绿幕图像(BGR格式)
    cv::Mat img = cv::imread("test.png");
    if (img.empty()) {
        std::cout << "无法读取图像!" << std::endl;
        return -1;
    }

    // 2. 转换到 HSV 色彩空间
    cv::Mat hsv;
    cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV);

    // 3. 定义绿色范围(典型值:Hue 35~85,Saturation 50~255,Value 50~255)
    //    可根据实际绿幕微调下界/上界
    cv::Scalar lower_green(35, 50, 50);
    cv::Scalar upper_green(85, 255, 255);

    // 4. 生成绿色背景的掩码(背景为白色,前景为黑色)
    cv::Mat green_mask;
    cv::inRange(hsv, lower_green, upper_green, green_mask);

    // 7. 显示结果
    cv::imshow("原图", img);
    cv::imshow("绿色背景掩码(白色为背景)", green_mask);
    cv::waitKey(0);

    return 0;
}

这个是不是就是在我们的预期之内啊!!

那么我们还不仅限于此,我们还想要把这个图像给抠出来

按位与(bitwise AND)的运算规则

对于 8 位图像(像素值 0~255),将像素视为二进制数(例如 255 即 11111111,0 即 00000000):

白色 (255) 的二进制全为 1:

  • 11111111 & 任意二进制数 x = x
  • → 结果就是原像素值本身,颜色完全保留。

黑色 (0) 的二进制全为 0:

  • 00000000 & 任意二进制数 x = 0
  • → 结果一定为 0,颜色完全消失(变黑)。

也就是说白色与任何像素进行逻辑与操作都会得到该颜色,黑色与任何像素进行逻辑与操作都会得到黑色。

那么我们就必须将背景设置成黑色,抠出来的像变成白色,然后再将这个像去和原图像进行逻辑与操作,就能抠出来这个图!!!

那么我们的思路就很简单了

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 读取绿幕图像(BGR格式)
    cv::Mat img = cv::imread("test.png");
    if (img.empty()) {
        std::cout << "无法读取图像!" << std::endl;
        return -1;
    }

    // 2. 转换到 HSV 色彩空间,便于按色相(Hue)分割绿色
    cv::Mat hsv;
    cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV);

    // 3. 定义绿色背景的 HSV 范围(可微调)
    cv::Scalar lower_green(35, 50, 50);
    cv::Scalar upper_green(85, 255, 255);

    // 4. 生成绿色背景掩码:背景(绿色)→ 白色,前景(非绿)→ 黑色
    //    这样背景为白色(255),前景为黑色(0)
    cv::Mat green_mask;
    cv::inRange(hsv, lower_green, upper_green, green_mask);

    // 5. 反转掩码:得到前景掩码
    //    目的:让前景变成白色(255),背景变成黑色(0)
    //    因为只有白色与像素做"按位与"才能保留原色,黑色会使像素变黑
    cv::Mat foreground_mask;
    cv::bitwise_not(green_mask, foreground_mask);

    // 6. 按位与提取前景
    //    规则:白色(255) & 原像素 = 原像素(保留前景物体)
    //         黑色(0)   & 原像素 = 0       (背景被置黑,即抠除)
    cv::Mat result;
    cv::bitwise_and(img, img, result, foreground_mask);

    // 7. 显示各阶段结果
    cv::imshow("原图", img);
    cv::imshow("绿色背景掩码(白=背景)", green_mask);
    cv::imshow("前景掩码(白=要保留的物体)", foreground_mask);
    cv::imshow("抠图结果(背景已变黑)", result);
    cv::waitKey(0);

    return 0;
}

这样子我们就扣出了一张图。

更换背景

但是我们不想要显示在一个黑色背景里面,我们想显示在一个蓝色背景里面,那怎么办,这个其实我们需要学习一个新的函数了

**src.copyTo(dst, mask)**是带掩码的版本,其含义是:

只将 src 中对应 mask 非零(通常为白色)的像素复制到 dst 中,而 dst 中掩码为零的位置保持不变。

也就是下面这样子

cpp 复制代码
for (int i = 0; i < src.rows; ++i) {
    for (int j = 0; j < src.cols; ++j) {
        if (mask.at<uchar>(i, j) != 0) {   // 注意:掩码非零,也就是该像素点不是黑色的时候
            dst.at<Vec3b>(i, j) = src.at<Vec3b>(i, j);//将
        }
        // 否则 dst 的值不变
    }
}

但是我知道大家肯定是还不明白,其实这里涉及到了三张图

  • src:提供像素值的来源(例如你想抠出的物体)。

  • mask:单通道图像,像素值一般是 0(黑色)或 255(白色)。

    • 非零:通常指 255,即白色区域。

    • 为零:即 0,黑色区域。

  • dst :目标图像,操作前已经存在,并且你希望它最终成为结果图。

初始状态

cpp 复制代码
src = [
    [10, 20, 30],
    [40, 50, 60],
    [70, 80, 90]
]

dst = [
    [100, 200, 100],
    [200, 100, 200],
    [100, 200, 100]
]

mask = [
    [255, 0, 255],
    [0, 255, 0],
    [255, 0, 255]
]

执行 src.copyTo(dst, mask) 后,内部循环:

对于每个位置 (i, j):

  • 检查 mask[i][j] 是否 不等于 0。
  • 如果不等于 0(例如 255),则把 src[i][j] 的值赋值给 dst[i][j](覆盖原来的值)。
  • 如果等于 0,则 什么也不做,dst[i][j] 保持原来的值。

逐像素执行:

  • (0,0): mask=255 ≠0 → dst[0][0] = src[0][0] = 10
  • (0,1): mask=0 → 不变 → dst[0][1] 仍是 200
  • (0,2): mask=255 → dst[0][2] = 30
  • (1,0): mask=0 → 不变 → dst[1][0] 仍是 200
  • (1,1): mask=255 → dst[1][1] = 50
  • (1,2): mask=0 → 不变 → dst[1][2] 仍是 200
  • (2,0): mask=255 → dst[2][0] = 70
  • (2,1): mask=0 → 不变 → dst[2][1] 仍是 200
  • (2,2): mask=255 → dst[2][2] = 90

最终 dst:

cpp 复制代码
dst = [
    [10, 200, 30],
    [200, 50, 200],
    [70, 200, 90]
]

观察结果:

  • 凡是 mask 为白色的位置,都被 src 的像素替换了。
  • 凡是 mask 为黑色的位置,dst 保持了原来的值(200 或 100)。

第三步:为什么 dst 一开始不能是空的?

  • 如果 dst 在 copyTo 之前没有初始化(比如刚创建的 Mat 默认数据是随机的或全 0),那么黑色区域就会保留那些未初始化的值(可能是 0 或乱码)。
  • 所以正确用法是:先给 dst 赋予你想要的背景(纯色或另一张图),然后再 copyTo

那么现在我们就能来实现了

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // ------------------ 1. 读取原始图像 ------------------
    // 读取一张绿幕图片(背景是绿色,前景是需要抠出的物体)
    cv::Mat img = cv::imread("test.png");
    if (img.empty()) {
        std::cout << "无法读取图像!" << std::endl;
        return -1;
    }

    // ------------------ 2. 转换色彩空间 ------------------
    // 将 BGR 图像转换为 HSV 空间。
    // 原因:在 HSV 中,绿幕的"绿色"可以用一个连续的色相(Hue)区间来表示,
    // 比 BGR 空间更稳定,不易受光照影响。
    cv::Mat hsv;
    cv::cvtColor(img, hsv, cv::COLOR_BGR2HSV);

    // ------------------ 3. 定义绿色的HSV范围 ------------------
    // 根据经验,纯绿色的 Hue 大约在 40~80 之间。
    // 这里取 35~85 以覆盖可能的偏黄绿或偏青绿。
    // Saturation(饱和度)和 Value(明度)设为 50~255,排除过暗或过灰的区域。
    cv::Scalar lower_green(35, 50, 50);
    cv::Scalar upper_green(85, 255, 255);

    // ------------------ 4. 生成绿色背景掩码 ------------------
    // inRange 会创建一个二值图像:在绿色范围内的像素设为 255(白),否则为 0(黑)。
    // 因此,green_mask 中:白色 = 绿幕背景,黑色 = 前景物体。
    cv::Mat green_mask;
    cv::inRange(hsv, lower_green, upper_green, green_mask);

    // ------------------ 5. 反转得到前景掩码 ------------------
    // bitwise_not 将黑白互换:255 变 0,0 变 255。
    // 现在 foreground_mask 中:白色 = 前景物体,黑色 = 背景。
    // 为什么需要反转?因为后续我们要"保留前景、替换背景",需要白色区域对应要保留的部分。
    cv::Mat foreground_mask;
    cv::bitwise_not(green_mask, foreground_mask);

    // ------------------ 6. 准备新背景并合成 ------------------
    // 6.1 创建一张与 img 同样大小的红色背景图像(BGR: 蓝0,绿0,红255)
    cv::Mat new_bg(img.size(), img.type(), cv::Scalar(0, 0, 255));

    // 6.2 将新背景复制到 result 中(此时 result 全为红色)
    cv::Mat result;
    new_bg.copyTo(result);

    // 6.3 将原图 img 的前景部分(掩码为白色的地方)覆盖到 result 的对应位置
    //     掩码为黑色的地方,result 保留原来的红色背景,不受影响。
    //     这样就实现了"红色背景 + 抠出物体"的合成。
    img.copyTo(result, foreground_mask);

    // ------------------ 7. 显示所有中间结果和最终结果 ------------------
    cv::imshow("原图", img);                     // 原始绿幕图
    cv::imshow("绿色背景掩码(白=背景)", green_mask);   // 绿幕区域为白色
    cv::imshow("前景掩码(白=物体)", foreground_mask);  // 物体区域为白色
    cv::imshow("抠图结果(红背景)", result);           // 最终合成图
    cv::waitKey(0);

    return 0;
}

这个就很有意思了吧!!

相关推荐
一条泥憨鱼13 小时前
让AI从“死记硬背”到“开卷考试”:详解RAG技术的奥秘
人工智能·ai·语言模型·机器人·rag
霍格沃兹测试学院-小舟畅学13 小时前
高质量测试 Skill 编写手册 -- 渐进式披露
人工智能
MediaTea13 小时前
DL:生成对抗网络的基本原理与 PyTorch 实现
人工智能·pytorch·深度学习·神经网络·生成对抗网络
韦胖漫谈IT13 小时前
数据与模型投毒 - 大语言模型 OWASP TOP 10系列
人工智能·语言模型·自然语言处理
wuxinyan12313 小时前
工业级大模型学习之路025:问题解决-检索质量全为0
人工智能·python·学习·langchain
weixin_4080996713 小时前
2026 图片高清化 API 实战:AI超分辨率重建技术详解 + Python/Java/PHP/C#代码示例
图像处理·人工智能·python·超分辨率重建·石榴智能·图片变清晰·图片高清化api
@蔓蔓喜欢你13 小时前
WebRTC 实时通信:构建音视频通话应用
人工智能·ai
上海全爱科技13 小时前
全爱科技诚邀莅临 | 2026 高等教育博览会 携摩尔线程 GPU + 昇腾 NPU全栈 AI 解决方案,共启科教数智新征程
人工智能·科技
song50113 小时前
多模态模型在昇腾上的部署架构
人工智能·分布式·深度学习·架构·transformer·交互