OpenCV实战——OpenCV策略设计模式

Posted 盼小辉丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV实战——OpenCV策略设计模式相关的知识,希望对你有一定的参考价值。

OpenCV实战(6)——OpenCV策略设计模式

0. 前言

良好的计算机视觉程序始于良好的编程实践,构建无错误的应用程序只是一个开始。我们真正想要的是一个能够随着新需求的出现而轻松适应和发展的应用程序。本节将介绍如何充分利用一些面向对象的编程原则以构建高质量的软件程序,我们将学习一些重要的设计模式,帮助我们使用易于测试、维护和可重用的组件构建应用程序。
设计模式是软件工程中的一个常见概念,设计模式是针对软件设计中经常出现的通用问题的可靠的、可重用的解决方案。当前有许多设计模式在软件设计中被引入,我们应该对现有设计模式有所了解。

1. 策略设计模式颜色识别

1.1 颜色比较

假设我们想要构建一个简单的算法来识别图像中具有给定颜色的所有像素。为了达到目的,算法需要接受图像和颜色作为输入,并返回二值图像,其中在输入图像中与指定颜色相同的像素位置值为 1,否则为 0,例如,输入图像中位置 (1,1) 处的像素值与指定颜色相同,则在二值图像的 (1,1) 位置处像素值为 1。同时,函数也可以接受颜色容差作为参数。

1.2 策略设计模式

为了实现颜色比较,本节将使用策略设计模式,这种面向对象的设计模式将算法封装在类中。这比用另一种算法替换给定算法或将几种算法链接在一起以构建更复杂的过程更容易。此外,这种模式通过隐藏尽可能多的复杂性来简化算法的部署。
一旦使用策略设计模式将算法封装在一个类中,就可以通过创建该类的实例来部署它。通常,实例在程序初始化时创建。在构造的时候,类实例会用它们的默认值初始化算法的不同参数,也可以使用合适的方法读取和设置算法的参数值。对于具有 GUI 的应用程序,可以使用不同的小部件(文本、滑块等)来显示和修改这些参数。

1.3 实现颜色比较

接下来,我们将介绍 Strategy 类的结构。在此之前,我们编写一个简单的 main 函数来运行上述颜色检测算法。

(1) 首先,在 main 函数中,我们必须为类 ColorDetector 创建一个实例(对于类的具体代码,我们将在下一节中介绍):

// 创建图像处理器对象
ColorDetector cdetect;

(2) 读取图像进行处理:

// 读取输入图像
cv::Mat image = cv::imread("1.png");

(3)empty() 函数检查我们是否正确加载了图像,如果图像为空,则退出应用程序:

if (image.empty()) return 0;

(4) 使用 ColorDetector 的新实例设置目标颜色(该函数是在类中定义的):

cdetect.setTargetColor(230, 190, 130);

(5) 创建一个窗口显示图像处理结果。因此,我们必须使用 ColorDetector 实例的 process 函数:

// 处理图像并显示结果
cv::namedWindow("Result");
cv::Mat result = cdetect.process(image);
cv::imshow("Result", result);
6. 最后,在退出之前等待用户按键操作:
cv::waitKey();
return 0;

运行此程序,可以得到以下输出:


在上图中,白色像素表示图像中与给定颜色相同的像素,黑色表示图像中与给定颜色不同的像素。我们封装在 ColorDetector 类中的算法比较简单(仅由一个扫描循环和一个容差参数组成)。当要实现的算法更复杂、步骤较多且包含多个参数时,策略设计模式也会变得更加有用。

1.3.1 完整代码

完整代码 (colorDetector.cpp) 如下所示:

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

#include "colordetector.h"
#include <vector>

int main() 
    // 创建图像处理器对象
    ColorDetector cdetect;
    // 读取输入图像
    cv::Mat image = cv::imread("1.png");
    if (image.empty()) return 0;
    cv::namedWindow("Original Image");
    cv::imshow("Original Image", image);
    // 设置输入参数
    cdetect.setTargetColor(230, 190, 130);
    // 处理图像并显示结果
    cv::namedWindow("Result");
    cv::Mat result = cdetect.process(image);
    cv::imshow("Result", result);
    // 或者使用函子
    ColorDetector colordetector(230, 190, 130, 45, true);
    cv::namedWindow("Result (functor)");
    result = colordetector(image);
    cv::imshow("Result (functor)", result);
    // 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::namedWindow("Flood fill result");
    result = colordetector(image);
    cv::imshow("Flood fill result", image);
    // 创建图像,演示颜色空间属性
    cv::Mat colors(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
    cv::Mat range = colors.colRange(0, 100);
    range = range + cv::Scalar(10, 10, 10);
    range = colors.colRange(200, 300);
    range = range + cv::Scalar(-10, -10, -10);
    cv::namedWindow("3 colors");
    cv::imshow("3 colors", colors);
    
    cv::Mat labImage(100, 300, CV_8UC3, cv::Scalar(100, 200, 150));
    cv::cvtColor(labImage, labImage, cv::COLOR_BGR2Lab);
    range = colors.colRange(0, 100);
    range = range + cv::Scalar(10, 10, 10);
    range = colors.colRange(200, 300);
    range = range + cv::Scalar(-10, -10, -10);
    cv::cvtColor(labImage, labImage, cv::COLOR_Lab2BGR);
    cv::namedWindow("3 colors (Lab)");
    cv::imshow("3 colors (Lab)", colors);

    cv::Mat grayLevels(100, 256, CV_8UC3);
    for (int i=0; i<256; i++) 
        grayLevels.col(i) = cv::Scalar(i, i, i);
    
    range = grayLevels.rowRange(50, 100);
    cv::Mat channels[3];
    cv::split(range, channels);
    channels[1] = 128;
    channels[2] = 128;
    cv::merge(channels, 3, range);
    cv::cvtColor(range, range, cv::COLOR_Lab2BGR);
    cv::namedWindow("Luminance vs Brightness");
    cv::imshow("Luminance vs Brightness", grayLevels);
    cv::waitKey();
    return 0;

1.4 ColorDetector 类

颜色比较算法的核心过程很容易实现,通过一个简单的扫描循环,遍历每个像素,将其颜色与给定目标颜色进行比较:

// 迭代器
cv::Mat_<cv::Vec3b>::const_iterator it = image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::const_iterator itend = image.end<cv::Vec3b>();
cv::Mat_<uchar>::iterator itout = result.begin<uchar>();
if (useLab) 
    it = converted.begin<cv::Vec3b>();
    itend = converted.end<cv::Vec3b>();

for (; it != itend; ++it, ++itout) 
    if (getDistanceToTargetColor(*it) < maxDist) 
        *itout = 255;
     else 
        *itout = 0;
    

cv::Mat 变量 image 指输入图像,而 result 是指二值输出图像。因此,第一步需要设置所需的迭代器,使扫描循环易于实现。在每次迭代时评估当前像素颜色和给定目标颜色之间的距离,以检查它们之间的距离是否在 maxDist 定义的容差参数范围内。如果在容差范围内,则将输出图像像素值设为 255 (白色);否则,将其值设为 0 (黑色)。我们可以使用 getDistanceToTargetColor 方法计算当前像素颜色与给定目标颜色之间的距离,OpenCV 中也有许多不同的方法来计算距离。例如,可以计算包含 RGB 颜色值的向量之间的欧几里得距离,或对 RGB 值之间差值的绝对值求和(这也称为城市街区距离)。在现代架构中,浮点欧几里得距离的计算速度比城市街区距离更快。为了获得更高的灵活性,我们根据 getColorDistance 方法编写 getDistanceToTargetColor 方法:

// 计算与目标颜色的距离
int getDistanceToTargetColor(const cv::Vec3b& color) const 
    return getColorDistance(color, target);

// 计算两种颜色之间的城市街区距离
int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const 
    return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);

我们使用 cv::Vec3d 保存表示 RGB 值的三个无符号字符。target 变量是指给定的目标颜色,它在我们定义的类算法中定义为类 (class) 变量。对于类方法 process,其根据提供的输入图像,扫描图像完成后返回结果:

cv::Mat ColorDetector::process(const cv::Mat& image) 
    result.create(image.size(), CV_8U);
// 循环处理过程 
...
    return result;

每次调用此方法时,需要检查包含结果二值图的输出图像是否需要重新分配内存以匹配输入图像的大小,这就是我们使用 cv::Matcreate 方法的原因。需要注意的是,只有在指定的大小和深度(图像深度是指存储每个像素所用的位数)与当前图像结构不对应时,此方法才会进行重新分配。
定义了核心处理方法之后,我们需要继续添加一些额外的类方法来部署算法。由于我们已经确定了算法需要哪些输入和输出数据,因此,我们首先定义保存这些数据的类属性:

class ColorDetector 
    private:
        // 容差
        int maxDist;
        // 目标颜色
        cv::Vec3b target;
        // 结果二值图像
        cv::Mat result;

为了创建封装算法的类的实例(命名为 ColorDetector),我们需要定义一个构造函数,策略设计模式的目标之一是使算法部署尽可能简单。可以定义的最简单的构造函数是空构造函数,它将创建一个有效的类算法实例。然后,我们令构造函数将所有输入参数初始化为其默认值,在此算法中,我们将通常是可接受的容差参数设为 100;此外,我们还需要设置默认的目标颜色。用于确保我们总是从可预测和有效的输入值开始测试算法:

// 空构造函数
// 默认参数初始化
ColorDetector() : maxDist(100), target(0, 0, 0) 

创建类算法实例的后,我们可以使用有效图像调用 process 方法并获得有效输出,这是策略模式的另一个目标,即确保算法始终以有效参数运行。显然,使用这个类时,我们会需要使用自定义参数,这可以通过提供相应的 gettersetter 方法实现。以颜色容差参数为例:

// 设置颜色阈值距离
void setColorDistanceThreshold(int distance) 
    if (distance < 0) distance = 0;
    maxDist = distance;

// 获取颜色阈值距离
int getColorDistanceThreshold() const 
    return maxDist;

需要注意的是,我们首先需要检查输入的有效性,这是为了确保我们的算法永远不会在无效状态下运行。我们也可以用类似的方式设置给定目标颜色参数:

// 设置颜色阈值距离
void setColorDistanceThreshold(int distance) 
    if (distance < 0) distance = 0;
    maxDist = distance;

// 获取颜色阈值距离
int getColorDistanceThreshold() const 
    return maxDist;

在以上代码中,我们提供了 setTargetColor 方法的两个定义。在定义的第一个版本中,三个颜色分量被指定为三个参数,而在第二个版本中,使用 cv::Vec3b 保存颜色值,目标是便于类算法的使用,以方便在使用时选择最适合的 setter
本节介绍了如何使用策略设计模式将算法封装在类中,本节我们使用颜色比较算法识别足够接近指定目标颜色的图像像素。此外,策略设计模式的实现可以通过使用函数对象进行补充。

1.3.1 完整代码

完整代码 (colordetector.h) 如下所示:

#if !defined COLORDETECT
#define COLORDETECT

#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>

class ColorDetector 
    private:
        // 容差
        int maxDist;
        // 目标颜色
        cv::Vec3b target;
        // 转换色彩后的图像
        cv::Mat converted;
        bool useLab;
        // 结果二进制图像
        cv::Mat result;
    public:
        // 空构造函数
        // 默认参数初始化
        ColorDetector() : maxDist(100), target(0, 0, 0), useLab(false) 
        // Lab颜色空间的额外构造函数 
        ColorDetector(bool useLab) : maxDist(100), target(0, 0, 0), useLab(true) 
        // 完整构造函数
        ColorDetector(uchar blue, uchar green, uchar red, int maxDist=100, bool useLab=false) : maxDist(maxDist), useLab(useLab) 
            // 目标颜色
            setTargetColor(blue, green, red);
        
        // 计算与目标颜色的距离
        int getDistanceToTargetColor(const cv::Vec3b& color) const 
            return getColorDistance(color, target);
        
        // 计算两种颜色之间的城市街区距离
        int getColorDistance(const cv::Vec3b& color1, const cv::Vec3b& color2) const 
            return abs(color1[0] - color2[0]) + abs(color1[1] - color2[1]) + abs(color1[2] - color2[2]);
            // 或者
            // return static_cast<int>(cv::norm<int,3>(cv::Vec3i(color1[0]-color2[0],color1[1]-color2[1],color1[2]-color2[2])));
            // 或者
            // cv::Vec3b dist;
            // cv::absdiff(color1, color2, dist);
            // return cv::sum(dist)[0];
        
        // 处理图像,返回单通道二进制图像
        cv::Mat process(const cv::Mat& image);
        
        cv::Mat operator()(const cv::Mat& image) 
            cv::Mat input;
            if (useLab) 
                cv::cvtColor(image, input, cv::COLOR_BGR2Lab);
             else 
                input = image;
            
            cv::Mat output;
            cv::absdiff(input, cv::Scalar(target), output);
            // 分割图像通道
            std::vector<cv::Mat> images;
            cv::split(output, images);
            // 对三个通道进行加法运算
            output = images[0] + images[1] + images[2];
            // 应用阈值
            cv::threshold(output,   // 输入图像
                output,             // 输出图像
                maxDist,            // 阈值
                255,                // 最大值
                cv::THRESH_BINARY_INV  // 阈值运算类型
            );
            return output;
        

        // 设置颜色阈值距离
        void setColorDistanceThreshold(int distance) 
            if (distance < 0) distance = 0;
            maxDist = distance;
        
        // 获取颜色阈值距离
        int getColorDistanceThreshold() const 
            return maxDist;
        
        // 设置BGR颜色空间中给定的待检测颜色
        void setTargetColor(uchar blue, uchar green, uchar red) 
            target = cv::Vec3b(blue, green, red);
            if (useLab) 
                cv::Mat tmp(1, 1, CV_8UC3);
                tmp.at<cv::Vec3b>(0, 0) = cv::Vec3b(blue, green, red);
                // 将目标颜色转换到 Lab 色彩空间
                cv::cvtColor(tmp, tmp, cv::COLOR_BGR2Lab);
                target = tmp.at以上是关于OpenCV实战——OpenCV策略设计模式的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV实战——基于GrabCut算法的图像分割

OpenCV实战——基于GrabCut算法的图像分割

OPENCV学习笔记15_算法设计中使用策略模式

《PyInstaller打包实战指南》第二十三节 单文件模式打包OpenCV-Python

《PyInstaller打包实战指南》第二十三节 单文件模式打包OpenCV-Python

OpenCV与MFC实战之图像处理 样本采集小工具制作 c++MFC课程设计