一.键盘响应式操作
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函数)
- cv::merge 的基本任务
当你对一张彩色图像执行 cv::split 后,会得到若干个独立的单通道图像(比如 B、G、R 三张灰度图)。如果你想把这些分散的通道重新"粘"回一张彩色图,就需要用到 cv::merge。
它的核心工作就是:
- 输入: 一组单通道图像(每个图像大小、数据类型完全相同)
- 输出: 一张多通道图像,其中每个像素的各个分量分别来自输入通道对应位置的灰度值。
- 输入输出细节
输入:通常是一个 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 |
逐像素判断
-
像素 A :R=150 (100~200 ✅)
G=100 (50~150 ✅)
B=120 (80~180 ✅)
→ 所有通道在范围内 → 输出 255
-
像素 B :R=180 (100~200 ✅)
G=60 (50~150 ✅)
B=90 (80~180 ✅)
→ 所有通道在范围内 → 输出 255
-
像素 C :R=210 → 超过了上界 200 ❌
(G=120、B=150 即使都在范围内,但 R 已不满足)
→ 输出 0
-
像素 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;
}

这个就很有意思了吧!!