opencv(C++)处理图像颜色

文章目录

介绍

  • 使用策略设计模式比较颜色
  • 使用GrabCut算法分割图像
  • 转换颜色表示法
  • 使用色调、饱和度和亮度表示颜色

使用策略设计模式比较颜色

假设需要构建一个简单的算法,用于识别图像中所有具有指定颜色的像素。为实现这一目标,该算法需要接受一张图像和一种颜色作为输入,并返回一张二值图像,显示哪些像素具有指定的颜色。此外,还需要在运行算法之前指定一个容差值(tolerance),以决定接受某种颜色的宽松程度。

为了实现这个目标,将使用策略设计模式(Strategy Design Pattern)。这是一种面向对象的设计模式,非常适合将算法封装到类中。通过这种方式,可以轻松替换某个算法,或将多个算法串联在一起以构建更复杂的过程。此外,这种模式通过隐藏尽可能多的复杂性,为算法提供了一个直观的编程接口,从而简化了算法的部署和使用。

实现方案

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

class ColorDetector 
{
public:
    void setTargetColor(int r, int g, int b);     // 设置目标颜色
    void setColorDistanceThreshold(int distance);  // 设置颜色距离阈值
    
    cv::Mat process(const cv::Mat &image);     // 处理图像并返回结果
    
private:
    bool matchColor(const cv::Vec3b& pixel) const;  // 比较颜色
    
private:
    cv::Vec3b targetColor;  // 目标颜色
    int tolerance{30};     // 容差,默认值
};

void ColorDetector::setTargetColor(int r, int g, int b) 
{
    targetColor = cv::Vec3b(b, g, r);  // 注意OpenCV中的颜色顺序是BGR
}

 void setColorDistanceThreshold(int distance) 
 {
        if (distance < 0) 
        	distance = 0;
        tolerance= distance;
 }

bool ColorDetector::matchColor(const cv::Vec3b& pixel) const 
{
    return std::abs(pixel[0] - targetColor[0]) <= tolerance &&
           std::abs(pixel[1] - targetColor[1]) <= tolerance &&
           std::abs(pixel[2] - targetColor[2]) <= tolerance;
}

cv::Mat ColorDetector::process(const cv::Mat &image) 
{
    cv::Mat result(image.size(), CV_8UC1);  // 输出的二值图像
    for (int i = 0; i < image.rows; ++i) 
    {
        for (int j = 0; j < image.cols; ++j) 
        {
            if (matchColor(image.at<cv::Vec3b>(i, j))) 
            {
                result.at<uchar>(i, j) = 255;  // 白色表示匹配
            } 
            else 
            {
                result.at<uchar>(i, j) = 0;   // 黑色表示不匹配
            }
        }
    }
    return result;
}
cpp 复制代码
#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"

int main() 
{
    // 1. 创建图像处理器对象
    ColorDetector cdetect;

    // 2. 读取输入图像
    cv::Mat image = cv::imread(IMAGE_1);
    if (image.empty()) 
    	return 0;
	
    // 3. 设置输入参数
    cdetect.setTargetColor(230, 190, 130);  // 这里假设要检测蓝色天空

    // 4. 处理图像并显示结果
    cv::namedWindow("result");
    cv::Mat result = cdetect.process(image);
    cv::imshow("result", result);
	cv::imshow("image", image);
    cv::waitKey();

    return 0;
}

OpenCV提供了一些内置函数可以完成类似的任务,例如提取具有特定颜色的连通区域。此外,策略设计模式的实现还可以通过函数对象进一步完善。最后,OpenCV定义了一个基类 cv::Algorithm,它实现了策略设计模式的核心概念。

计算两个颜色向量之间的距离

1. 简单方法:曼哈顿距离计算(Manhattan Distance)

计算方式是将每个颜色通道的绝对差值相加。

优点是实现简单且计算效率高,但在某些情况下可能无法准确反映颜色之间的感知差异。

cpp 复制代码
return abs(color[0] - target[0]) +
       abs(color[1] - target[1]) +
       abs(color[2] - target[2]);
2.使用 OpenCV 的 cv::norm 函数

OpenCV 提供了一个名为 cv::norm 的函数,可以用来计算向量的欧几里得范数(Euclidean Norm)。我们可以使用它来计算颜色向量之间的距离:

cpp 复制代码
return static_cast<int>(
    cv::norm(cv::Vec3i(color[0] - target[0],
                       color[1] - target[1],
                       color[2] - target[2])));

这里使用了 cv::Vec3i(一个包含三个整数值的向量),因为颜色通道相减的结果可能是负数,而 cv::Vec3b 是无符号类型,无法正确表示负数

通过这种方式,可以获得更精确的结果。需要注意的是,cv::norm 默认计算的是欧几里得距离。

3.使用 OpenCV 的 cv::absdiff 函数
错误示例
cpp 复制代码
return static_cast<int>(cv::norm<uchar, 3>(color - target)); // 错误!

OpenCV 的算术运算符会调用 saturate_cast 函数来确保结果始终在输入类型的范围内(在这里是 uchar 类型)。如果目标值大于对应的颜色值,结果会被截断为 0,而不是预期的负值。这会导致距离计算不准确。

为了避免上述问题,可以使用 OpenCV 提供的 cv::absdiff 函数来计算两个向量之间的绝对差值:

cpp 复制代码
// cv::absdiff 会逐元素地计算两个向量之间的绝对差值。
// cv::sum 函数会对结果向量的所有元素求和,返回一个标量值。
cv::Vec3b dist;
cv::absdiff(color, target, dist);
return cv::sum(dist)[0];

这种方法可以正确计算距离,但它需要两次函数调用(cv::absdiff 和 cv::sum),因此效率较低。

尽管 cv::absdiff 和 cv::sum 提供了一种正确且通用的解决方案,但对于大规模图像处理任务来说,效率可能是一个问题。在这种情况下,手动实现距离计算通常是更好的选择(使用方法1)

使用 OpenCV 函数实现颜色检测

上文展示了一种使用 OpenCV 函数实现颜色检测的方法。与手动编写循环相比,这种方法可以更快速地构建复杂的应用程序,并且通常具有更高的效率(得益于 OpenCV 的优化)。需要注意的是,当涉及多个中间步骤时,可能会消耗更多的内存。

实现方案
cpp 复制代码
cv::Mat ColorDetector::process(const cv::Mat &image) 
{
    cv::Mat output;

    // 1. 计算图像与目标颜色之间的绝对差值
    cv::absdiff(image, cv::Scalar(target), output);

    // 2. 将通道分离为三个独立的图像
    // 将 RGB 图像的三个通道分离出来,以便后续对每个通道单独处理
    std::vector<cv::Mat> images;
    cv::split(output, images);

    // 3. 将三个通道相加(可能发生饱和)
    // OpenCV 默认会对结果应用饱和操作(saturate_cast),因此如果某个像素的总和超过 255,它会被截断为 255。
    output = images[0] + images[1] + images[2];

    // 4. 应用阈值处理生成二值图像
    cv::threshold(output,               // 输入/输出图像
                  output,               // 输出图像
                  maxDist,              // 阈值(必须小于 256)
                  255,                  // 最大值
                  cv::THRESH_BINARY_INV // 阈值模式
    );

    return output;
}
阈值处理:cv::threshold
cv::THRESH_BINARY_INV 模式:
  • 像素值 ≤ 阈值时,设置为 255(白色)。
  • 像素值 > 阈值时,设置为 0(黑色)。
其他有用的模式
  • cv::THRESH_TOZERO:保留像素值大于阈值的部分,其余部分设为 0。
  • cv::THRESH_TOZERO_INV:保留像素值小于或等于阈值的部分,其余部分设为 0。

使用 cv::floodFill 函数

上述案例中,使用了逐像素比较的方式来识别图像中颜色接近目标颜色的像素。

OpenCV 提供了一个功能类似的函数 cv::floodFill。cv::floodFill 的核心思想是基于连通区域进行颜色提取,而不仅仅是逐像素判断。

cv::floodFill 的工作原理

1.种子点(Seed Point)

  • 需要指定一个起始像素位置(称为种子点),该点的颜色将作为参考颜色。
  • 从种子点开始,算法会检查其邻居像素,并根据容差参数决定是否接受这些邻居像素。

2.连通性(Connectivity)

  • 如果某个邻居像素被接受,则继续检查该像素的邻居,依此类推。
  • 这样,算法可以提取出一个连通的颜色区域。

3.颜色容差(Tolerance Parameters)

  • 容差参数决定了颜色相似度的标准。用户可以为高于和低于参考颜色的值分别设置不同的阈值。
  • 在固定范围模式(cv::FLOODFILL_FIXED_RANGE)下,所有测试的像素都会与种子点的颜色进行比较。
  • 在默认模式下,每个测试像素会与其邻居的颜色进行比较。

4.重新着色(Repainting)

  • 被接受的像素会被重新着色为指定的颜色(通过第三个参数指定)。
实现方案
cpp 复制代码
cv::Mat image = cv::imread("image.jpg");

// 使用 floodFill 提取天空区域
cv::floodFill(image,                       // 输入/输出图像
              cv::Point(100, 50),          // 种子点位置
              cv::Scalar(255, 255, 255),   // 重新着色的颜色(白色)
              (cv::Rect*)0,                // 返回的边界矩形(可选) 如果需要获取被填充区域的边界,可以传递一个非空指针
              cv::Scalar(35, 35, 35),      // 颜色下限容差
              cv::Scalar(35, 35, 35),      // 颜色上限容差
              cv::FLOODFILL_FIXED_RANGE);  // 固定范围模式
              
              // cv::FLOODFILL_FIXED_RANGE:表示所有测试的像素都与种子点的颜色进行比较。
              // 默认模式(未指定 cv::FLOODFILL_FIXED_RANGE):表示每个测试像素与邻居的颜色进行比较
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"

int main() 
{
    cv::Mat image = cv::imread(IMAGE_1);
    if (image.empty()) 
    	return 0;

	cv::floodFill(image,             // 输入/输出图像
		cv::Point(100, 50),          // 种子点位置
		cv::Scalar(255, 255, 255),   // 重新着色的颜色(白色)
		(cv::Rect*)0,                // 返回的边界矩形(可选) 
		cv::Scalar(35, 35, 35),      // 颜色下限容差
		cv::Scalar(35, 35, 35),      // 颜色上限容差
		cv::FLOODFILL_FIXED_RANGE);  // 固定范围模式
    
	cv::imshow("image", image);
    cv::waitKey();

    return 0;
}

算法会从种子点开始,提取出一个连通的蓝色区域,并将其重新着色为白色。即使图像中其他地方存在颜色相似的像素,只要它们没有区域连通,就不会被识别。

应用场景
  • 连通区域提取: 提取图像中特定颜色的连通区域(如天空、水面等)。
  • 对象分割:将图像中的某个对象(如前景或背景)分割出来。
  • 图像修复:填充图像中的小区域或孔洞。
  • 交互式图像编辑:用户可以通过点击图像中的某个点来选择并操作特定区域。

使用函数对象

在 C++ 中,通过重载 operator() 运算符,可以创建一个类,使其实例像函数一样被调用。这种技术被称为函数对象(Functor) 。

优点是它们结合了函数的简洁性和类的状态管理能力,因此非常适合用于需要保存状态的算法。

实现方案
cpp 复制代码
class ColorDetector 
{
public:
    // 构造函数
    ColorDetector(uchar blue, uchar green, uchar red, int maxDist = 100)
        : maxDist(maxDist) 
    {
        setTargetColor(blue, green, red);
    }

    // 设置目标颜色
    void setTargetColor(uchar blue, uchar green, uchar red) 
    {
        targetColor = cv::Vec3b(blue, green, red);
    }

    // 获取最大距离
    int getMaxDist() const 
    {
        return maxDist;
    }

    // 设置最大距离
    void setMaxDist(int dist) 
    {
        maxDist = dist;
    }

    // 重载 operator(),实现函数对象行为
    cv::Mat operator()(const cv::Mat &image) 
    {
        cv::Mat output;
        // 计算绝对差值
        cv::absdiff(image, cv::Scalar(targetColor), output);

        // 分离通道并相加
        std::vector<cv::Mat> channels;
        cv::split(output, channels);
        output = channels[0] + channels[1] + channels[2];

        // 应用阈值处理
        cv::threshold(output, output, maxDist, 255, cv::THRESH_BINARY_INV);

        return output;
    }

private:
    cv::Vec3b targetColor; // 目标颜色
    int maxDist;           // 最大距离(容差)
};
cpp 复制代码
// 创建 ColorDetector 实例,并初始化目标颜色和容差
ColorDetector colordetector(230, 190, 130, 100);

// 使用函数对象检测颜色
cv::Mat image = cv::imread("image.jpg");
cv::Mat result = colordetector(image); // 像调用函数一样调用对象
适用场景
  • 需要保存状态的算法。
  • 动态配置参数的场景。
  • 与 STL 算法结合使用的场景。

OpenCV 的算法基类:cv::Algorithm

cv::Algorithm 的核心功能

1. 动态创建算法实例

所有继承自 cv::Algorithm 的算法都可以通过静态方法动态创建。

确保算法在创建时总是处于有效状态(即未指定的参数会被赋予默认值)。

例如,对于 cv::ORB(一种用于检测兴趣点的算法),可以通过以下方式创建实例:

cpp 复制代码
cv::Ptr<cv::ORB> ptrORB = cv::ORB::create(); // 默认状态

2. 读取和写入算法状态

cv::Algorithm 提供了通用的 read 和 write 方法,用于加载或存储算法的状态。

这使得算法可以轻松地保存到文件中,并在需要时重新加载。

cpp 复制代码
cv::FileStorage fs("orb_state.yml", cv::FileStorage::WRITE);
ptrORB->write(fs); // 将算法状态写入文件
fs.release();

cv::Ptr<cv::ORB> newORB = cv::ORB::create();
cv::FileStorage fs2("orb_state.yml", cv::FileStorage::READ);
newORB->read(fs2.root()); // 从文件中读取算法状态
fs2.release();

3.专用方法

每个算法都有其特定的功能方法。例如,cv::ORB 提供了 detect 和 compute 方法,用于检测特征点并计算描述符。

cpp 复制代码
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
ptrORB->detectAndCompute(image, cv::Mat(), keypoints, descriptors);

这些方法是算法的核心计算单元,用户可以直接调用它们来执行特定任务。
4.参数设置与获取

算法通常包含多个内部参数,用户可以通过专用的 setter 和 getter 方法来调整这些参数。

cpp 复制代码
ptrORB->setMaxFeatures(500); // 设置最大特征点数量
int maxFeatures = ptrORB->getMaxFeatures(); // 获取当前设置
cv::Algorithm 的多态性

如果将算法实例声明为 cv::Ptrcv::Algorithm 类型,则无法直接调用其专用方法(如 detect 或 compute)。这是因为 cv::Algorithm 是一个通用基类,它并不知道子类的具体方法。

cpp 复制代码
cv::Ptr<cv::Algorithm> algo = cv::ORB::create();
// algo->detect(...) // 错误!detect 方法不可用

使用具体的子类类型(如 cv::Ptrcv::ORB)来声明指针,以便访问其专用方法。

cpp 复制代码
cv::Ptr<cv::ORB> ptrORB = cv::ORB::create();
ptrORB->detectAndCompute(image, cv::Mat(), keypoints, descriptors); // 正确!
优点
  • 统一的接口,便于管理和使用。
  • 动态创建机制确保算法始终处于有效状态。
  • 支持灵活的参数调整和状态管理。
  • 易于扩展,适合开发新的算法。
适用场景
  • 需要动态加载或保存算法状态的场景。
  • 需要灵活调整算法参数的任务。
  • 开发新的算法并希望与 OpenCV 现有框架无缝集成。

使用GrabCut算法分割图像

GrabCut 是一种流行的图像分割算法,特别适用于从静态图像中提取前景对象(例如,从一张图片中剪切并粘贴一个对象到另一张图片)。尽管它是一种复杂且计算成本较高的算法,但它通常能产生非常精确的结果。

图像分割实现步骤

1.定义输入图像和矩形区域

定义一个矩形区域来包含前景对象。所有在这个矩形之外的像素将被标记为背景。

cpp 复制代码
// 定义包围矩形
cv::Rect rectangle(5, 70, 260, 120);

2.初始化 GrabCut 所需的数据结构

创建几个矩阵来存储分割结果和算法内部使用的模型。

cpp 复制代码
cv::Mat result; // 分割结果(4种可能值)          每个像素可以有四种状态之一
cv::Mat bgModel, fgModel; // 算法内部使用的模型   用于存储算法生成的背景和前景模型

3.调用 cv::grabCut 函数

cpp 复制代码
// GrabCut 分割
cv::grabCut(image,        // 输入图像
            result,       // 分割结果
            rectangle,    // 包含前景对象的矩形
            bgModel,      // 背景模型
            fgModel,      // 前景模型
            5,            // 迭代次数
            cv::GC_INIT_WITH_RECT); // 使用矩形模式  使用定义的矩形来指定前景区域

4.解释分割结果

分割结果图像中的每个像素可以有以下四种状态之一:

  • cv::GC_BGD:背景像素(例如,在我们的例子中,矩形外的所有像素)。
  • cv::GC_FGD:前景像素(在我们的例子中没有)。
  • cv::GC_PR_BGD:可能是背景的像素。
  • cv::GC_PR_FGD:可能是前景的像素(即,初始状态下矩形内的所有像素)。

为了生成二值分割图像,我们需要提取那些具有 cv::GC_PR_FGD 值的像素:

cpp 复制代码
// 获取可能属于前景的像素
cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);

// 生成输出图像
cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));
image.copyTo(foreground, // 只复制前景像素
             result);

5.提取所有前景像素

如果要提取所有前景像素(包括 cv::GC_PR_FGD 和 cv::GC_FGD),可以通过检查第一个比特位来实现:

cpp 复制代码
// 检查第一个比特位
result = result & 1; // 如果是前景,则值为1

因为 cv::GC_FGD 和 cv::GC_PR_FGD 的值分别为 1 和 3,而其他两个常量 cv::GC_BGD 和 cv::GC_PR_BGD 的值分别为 0 和 2。因此,通过与操作可以有效地提取前景像素。

实现方案

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

#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"

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

    // 定义包含前景对象的矩形区域
    cv::Rect rectangle(50, 50, 300, 200); // 根据实际情况调整矩形位置和大小

    // 创建用于存储分割结果和模型的矩阵
    cv::Mat result;       // 分割结果(4种可能值)
    cv::Mat bgModel, fgModel; // 背景和前景模型

    // 调用 GrabCut 算法
    cv::grabCut(image,          // 输入图像
                result,         // 分割结果
                rectangle,      // 包含前景的矩形
                bgModel,        // 背景模型
                fgModel,        // 前景模型
                5,              // 迭代次数
                cv::GC_INIT_WITH_RECT); // 使用矩形模式

    // 提取可能属于前景的像素
    cv::compare(result, cv::GC_PR_FGD, result, cv::CMP_EQ);

    // 创建一个空白背景图像(白色背景)
    cv::Mat foreground(image.size(), CV_8UC3, cv::Scalar(255, 255, 255));

    // 将原图中的前景像素复制到空白背景图像中
    image.copyTo(foreground, result);

    // 显示结果
    cv::imshow("Original Image", image);
    cv::imshow("Foreground", foreground);

    // 保存结果
    cv::imwrite("foreground.jpg", foreground);

    // 等待用户按键退出
    cv::waitKey(0);

    return 0;
}

转换颜色

RGB 颜色空间

RGB 颜色空间基于红(Red)、绿(Green)、蓝(Blue)三种加色原理。

在数字图像中,RGB 是默认的颜色空间,因为彩色图像是通过红、绿、蓝滤镜捕捉的。

RGB 的每个通道(红、绿、蓝)都经过归一化处理:当三个通道值相等时,会生成灰度颜色,从黑色(0,0,0)到白色(255,255,255)。然而,它并不适合用来直接衡量两种颜色之间的相似性。

RGB 的局限性

RGB 不是一个感知均匀的颜色空间。

在 RGB 空间中,两个颜色之间的距离可能无法准确反映它们在人眼中的视觉差异。例如:

  • 两个颜色之间的距离相同,但它们看起来可能非常相似。
  • 另外两个颜色之间的距离相同,但它们看起来却可能截然不同。

这种不一致性使得 RGB 颜色空间不适合用于需要精确比较颜色相似性的任务。

CIE Lab* 颜色空间

1.感知均匀性

  • 在 CIE Lab* 中,两个颜色之间的欧几里得距离可以很好地反映它们在人眼中的视觉相似性。
  • 距离越小,颜色看起来越相似;距离越大,颜色看起来差异越明显。

2.分量解释

  • L* 表示亮度(Lightness),从黑到白的变化。
  • a* 表示从绿色到红色的变化。
  • b* 表示从蓝色到黄色的变化。

3.适用性

  • 将图像从 RGB 转换到 CIE Lab* 后,可以直接用欧几里得距离来衡量颜色的相似性,而无需担心 RGB 空间的非均匀性问题。

颜色转换的步骤

1. 将图像从 BGR 转换到 CIE Lab 颜色空间

cpp 复制代码
// 输入图像(假设为BGR格式)
cv::Mat inputImage = cv::imread("input.jpg");

// 创建一个矩阵存储转换后的图像
cv::Mat labImage;

// 使用 cv::cvtColor 进行颜色空间转换
cv::cvtColor(inputImage, labImage, cv::COLOR_BGR2Lab);

cv::COLOR_BGR2Lab 是 BGR 到 CIE Lab* 的转换代码。

转换后,labImage 包含了 Lab* 表示的图像数据。
2. 将目标颜色从 RGB 转换到 CIE Lab

cpp 复制代码
void setTargetColor(unsigned char red, unsigned char green, unsigned char blue) 
{
    // 创建一个单像素的临时图像
    cv::Mat tmp(1, 1, CV_8UC3);
    tmp.at<cv::Vec3b>(0, 0) = cv::Vec3b(blue, green, red); // 注意顺序:BGR

    // 转换到 CIE L*a*b*
    cv::cvtColor(tmp, tmp, cv::COLOR_BGR2Lab);

    // 提取转换后的颜色值
    cv::Vec3b labColor = tmp.at<cv::Vec3b>(0, 0);
}

tmp 是一个单像素图像,用于存储目标颜色。

转换后,labColor 包含了目标颜色的 CIE Lab* 值。
3. 在 CIE Lab 空间中比较颜色

在 CIE Lab* 空间中,颜色的距离可以直接用欧几里得距离计算:

cpp 复制代码
// 计算两个颜色之间的欧几里得距离
double colorDistance(const cv::Vec3b &color1, const cv::Vec3b &color2) 
{
    double deltaL = color1[0] - color2[0];
    double deltaA = color1[1] - color2[1];
    double deltaB = color1[2] - color2[2];
    return std::sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB);
}

color1 和 color2 是两个 CIE Lab* 颜色。

返回值是两个颜色之间的感知距离。

其他常用颜色空间转换

转换类型 代码 描述
BGR → 灰度 cv::COLOR_BGR2GRAY 将彩色图像转换为灰度图像
BGR → HSV cv::COLOR_BGR2HSV 转换到 HSV 颜色空间
BGR → YCrCb cv::COLOR_BGR2YCrCb 转换到 JPEG 压缩使用的 YCrCb 空间
BGR → CIE XYZ cv::COLOR_BGR2XYZ 转换到设备无关的 CIE XYZ 空间
BGR → CIE Luv* cv::COLOR_BGR2Luv 转换到另一种感知均匀颜色空间
cpp 复制代码
// 将彩色图像转换为灰度图像
cv::Mat grayImage;
cv::cvtColor(colorImage, grayImage, cv::COLOR_BGR2GRAY);

工作原理

线性与非线性变换

  • RGB ↔ XYZ 是线性变换。
  • RGB ↔ CIE Lab* 或 CIE Luv* 是非线性变换(为了实现感知均匀性)。

通道范围

  • CIE Lab*:
    1. L 通道:亮度,范围 [0, 100],在 8 位图像中映射为 [0, 255]。
    2. a 和 b 通道:色度,范围 [-127, 127],在 8 位图像中偏移为 [0, 255]。
  • 灰度图像:单通道,范围 [0, 255]。

精度损失

8 位图像在转换时会引入舍入误差,因此某些转换不可完全逆向。

用色调、饱和度和亮度表示颜色

最初考虑的是 RGB 颜色空间,尽管它在电子成像系统中是一种有效的颜色捕获和显示方式,但并不直观。

人们通常用色调、亮度或色彩浓度(即颜色是鲜艳还是柔和)来描述颜色。

因此,基于色调、饱和度和亮度的颜色空间被引入,以帮助用户通过更直观的属性来指定颜色。

颜色空间转换及可视化HSV各分量

HSV颜色空间简介

HSB 颜色空间通常用一个锥形来表示,其中内部的每个点对应一种特定的颜色。角度位置对应于颜色的色调,饱和度是与中心轴的距离,而亮度则由高度表示。锥形的顶点对应黑色,此时色调和饱和度是未定义的。

HSV(色调、饱和度、亮度)颜色空间是基于人类对颜色的自然感知而设计的。它将颜色分为三个直观属性:

色调 (Hue)

表示颜色的类型,例如红色、绿色、蓝色等。

在 HSV 中,色调用角度表示,范围为 [0, 360] 度。在 OpenCV 中,为了适应 8 位图像,范围被压缩到 [0, 180]。

饱和度 (Saturation)

表示颜色的纯度,即颜色中灰色的比例。

高饱和度对应鲜艳的颜色,低饱和度对应接近灰色的颜色。

范围为 [0, 1] 或 [0, 255](对于 8 位图像)。

亮度 (Value)

表示颜色的明暗程度。

在 OpenCV 中,亮度定义为 BGR 通道的最大值。

OpenCV 中的 HSV 实现

OpenCV 提供了两种主要的感知颜色空间:HSV 和 HLS。以下是它们的计算方式和实现细节:

  • 亮度 (Value):
    定义为 BGR 通道的最大值。
  • 饱和度 (Saturation):
    如果颜色为灰度(R=G=B),则饱和度为 0。
    计算公式基于 BGR 的最大值和最小值:

S = (max(R, G, B) - min(R, G, B))/ max(R, G, B)

  • 色调 (Hue):
    基于 BGR 的最大值和最小值,通过三角函数计算角度。
    红色对应 0 度,绿色对应 120 度,蓝色对应 240 度。
生成 HSV 色彩表
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() 
{
    // 创建一个 128x360 的三通道图像
    cv::Mat hs(128, 360, CV_8UC3);

    for (int h = 0; h < 360; h++) 
    {        
        // 遍历所有色调值
        for (int s = 0; s < 128; s++) 
        {     
        	// 遍历所有饱和度值
            // 设置每个像素的 HSV 值
            hs.at<cv::Vec3b>(s, h)[0] = h / 2;      // 色调 (0-180)
            hs.at<cv::Vec3b>(s, h)[1] = 255 - s * 2; // 饱和度 (从高到低)
            hs.at<cv::Vec3b>(s, h)[2] = 255;        // 恒定亮度
        }
    }

    // 将 HSV 图像转换为 BGR 格式以显示
    cv::Mat hsvToBgr;
    cv::cvtColor(hs, hsvToBgr, cv::COLOR_HSV2BGR);

    // 显示结果
    cv::imshow("HSV Color Table", hsvToBgr);
    cv::waitKey(0);

    return 0;
}
修改图像亮度
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"

int main() 
{
    // 读取输入图像
    cv::Mat image = cv::imread(IMAGE_1);
    if (image.empty()) 
    {
        std::cerr << "无法加载图像,请检查路径!" << std::endl;
        return -1;
    }

    // 转换为 HSV 颜色空间
    cv::Mat hsvImage;
    cv::cvtColor(image, hsvImage, cv::COLOR_BGR2HSV);

    // 分离 HSV 通道
    std::vector<cv::Mat> channels;
    cv::split(hsvImage, channels);

    // 将亮度通道设置为 255
    channels[2] = 255;

    // 合并通道
    cv::merge(channels, hsvImage);

    // 转换回 BGR 颜色空间
    cv::Mat newImage;
    cv::cvtColor(hsvImage, newImage, cv::COLOR_HSV2BGR);

    // 显示结果
    cv::imshow("Original Image", image);
    cv::imshow("Modified Image", newImage);
    cv::waitKey(0);

    return 0;
}
将BGR图像转换为HSV颜色空间

要将一个BGR格式的图像转换到HSV(色调Hue、饱和度Saturation、亮度Value)颜色空间,可以使用如下方法:

cpp 复制代码
// 假设'image'是你的输入BGR图像
cv::Mat hsv;
cv::cvtColor(image, hsv, CV_BGR2HSV);

这里的CV_BGR2HSV是转换代码,它将BGR图像转换为HSV图像。

hsv变量现在存储了转换后的图像数据。

从HSV转回BGR颜色空间
cpp 复制代码
cv::Mat bgr;
cv::cvtColor(hsv, bgr, CV_HSV2BGR);
分离并可视化HSV各通道

为了更好地理解HSV颜色空间中的每个组成部分,我们可以将HSV图像的三个通道拆分成独立的图像:

cpp 复制代码
// 创建一个容器来保存三个通道
std::vector<cv::Mat> channels;
// 拆分HSV图像的三个通道
cv::split(hsv, channels);

// 'channels[0]' 是色调(Hue)
// 'channels[1]' 是饱和度(Saturation)
// 'channels[2]' 是亮度(Value)

由于我们处理的是8位图像,OpenCV将这些通道的值调整到了0到255的范围(除了色调外,它的范围是0到180)。这使得我们可以像显示灰度图像一样显示这些通道。

实现案例

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

#define IMAGE_1 "1.jpeg"
#define IMAGE_LOGO "logo.jpg"
#define IMAGE_LOGO_2 "logo_2.jpeg"

int main() 
{
    // 读取输入图像
    cv::Mat image = cv::imread(IMAGE_1);
    if (image.empty()) {
        std::cerr << "无法加载图像,请检查路径!" << std::endl;
        return -1;
    }

    // 转换为HSV颜色空间
    cv::Mat hsvImage;
    cv::cvtColor(image, hsvImage, cv::COLOR_BGR2HSV);

    // 分离HSV的三个通道
    std::vector<cv::Mat> channels;
    cv::split(hsvImage, channels);

    // 提取各通道
    cv::Mat hue = channels[0];         // 色调 (Hue)
    cv::Mat saturation = channels[1]; // 饱和度 (Saturation)
    cv::Mat value = channels[2];      // 亮度 (Value)

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

    // 显示色调通道 (Hue)
    cv::imshow("Hue Channel", hue);

    // 显示饱和度通道 (Saturation)
    cv::imshow("Saturation Channel", saturation);

    // 显示亮度通道 (Value)
    cv::imshow("Value Channel", value);

    // 等待用户按键退出
    cv::waitKey(0);

    return 0;
}


使用颜色进行肤色检测

颜色信息可以用于特定对象的初步检测。

驾驶员辅助系统中,可以通过标准路标的颜色快速识别潜在的路标候选区域。

肤色检测中,可以用来判断图像中是否存在人类,并且常用于手势识别中通过肤色检测来定位手的位置。

使用颜色检测对象,通常需要以下步骤:
收集样本数据

  • 收集大量包含目标对象的图像样本,这些样本应从不同的视角和光照条件下捕获。
  • 这些样本将用于定义分类器的参数。

选择颜色表示方式

  • 对于肤色检测,研究表明不同种族的肤色在色调(Hue)和饱和度(Saturation)空间中具有良好的聚类特性。因此,我们将使用色调和饱和度值来识别肤色
案例实现
cpp 复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

#define IMG_WOMAN "princess.jpeg"
#define IMG_OLD_MAN "old_man.jpeg"

// 定义肤色检测函数
void detectHScolor(const cv::Mat& image,        // 输入图像
                   double minHue, double maxHue, // 色调区间
                   double minSat, double maxSat, // 饱和度区间
                   cv::Mat& mask)              // 输出掩码
{
    // 将图像转换为 HSV 空间
    cv::Mat hsv;
    cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV);

    // 分离 HSV 通道
    std::vector<cv::Mat> channels;
    cv::split(hsv, channels);

    // channels[0] 是色调 (Hue)
    // channels[1] 是饱和度 (Saturation)
    // channels[2] 是亮度 (Value)

    // 色调掩码
    cv::Mat mask1; // 色调小于 maxHue 的部分
    cv::threshold(channels[0], mask1, maxHue, 255, cv::THRESH_BINARY_INV);

    cv::Mat mask2; // 色调大于 minHue 的部分
    cv::threshold(channels[0], mask2, minHue, 255, cv::THRESH_BINARY);

    cv::Mat hueMask; // 色调掩码
    if (minHue < maxHue)
        hueMask = mask1 & mask2; // 如果区间未跨越零度轴
    else
        hueMask = mask1 | mask2; // 如果区间跨越零度轴

    // 饱和度掩码
    cv::Mat satMask; // 饱和度掩码
    cv::inRange(channels[1], minSat, maxSat, satMask);

    // 组合掩码
    mask = hueMask & satMask;
}

int main() 
{
    // 读取输入图像
    cv::Mat image = cv::imread(IMG_OLD_MAN);
    if (image.empty()) 
    {
        std::cerr << "无法加载图像,请检查路径!" << std::endl;
        return -1;
    }

    // 定义肤色检测的色调和饱和度区间
    cv::Mat mask;
    detectHScolor(image, 
                  160, 10, // 色调范围:320 度到 20 度(OpenCV 中缩放为 0-180)
                  25, 166, // 饱和度范围:~0.1 到 ~0.65
                  mask);

    // 显示检测结果
    cv::Mat detected(image.size(), CV_8UC3, cv::Scalar(0, 0, 0)); // 创建黑色背景
    image.copyTo(detected, mask); // 将检测到的区域复制到黑色背景上

    // 显示原始图像和检测结果
    cv::imshow("Original Image", image);
    cv::imshow("Detected Skin", detected);
    cv::waitKey(0);

    return 0;
}


相关推荐
磊叔的技术博客几秒前
A2A 与 MCP:智能体协作的新纪元与AI工程化的思考
人工智能·开源·mcp
小爷毛毛_卓寿杰5 分钟前
【Dify(v1.2) 核心源码深入解析】Agent 模块
人工智能·后端·python
六bring个六34 分钟前
C++双链表介绍及实现
开发语言·数据结构·c++
北京青翼科技36 分钟前
【PCIE736-0】基于 PCIE X16 总线架构的 4 路 QSFP28 100G 光纤通道处理平台
图像处理·人工智能·fpga开发·信号处理
IT古董1 小时前
【漫话机器学习系列】206.稀疏性(Sparsity)
人工智能
AI小码1 小时前
期待的 A2A 和 MCP 的对比,谷歌与Anthropic联手打造的AI协作新时代,你准备好了吗?
人工智能·mcp
视觉AI1 小时前
PyTorch 模型转换为 TensorRT 引擎的通用方法
人工智能·pytorch·python
风筝超冷1 小时前
面试篇 - 位置编码
人工智能·深度学习
pen-ai1 小时前
【NLP】 21. Transformer整体流程概述 Encoder 与 Decoder架构对比
人工智能·自然语言处理·transformer
蹦蹦跳跳真可爱5891 小时前
Python----机器学习(基于PyTorch的垃圾邮件逻辑回归)
人工智能·pytorch·python·机器学习·逻辑回归