第3章 处理图像的颜色
3.2 用策略设计模式比较颜色
1、基本原理
假设我们要构建一个简单的算法,用来识别图像中具有某种颜色的所有像素。这个算法必须输入一幅图像和一个颜色,并且返回一个二值图像,显示具有指定颜色的像素。在运行算法前,还需要指定一个参数,即能够接受的颜色公差。
2、代码实现
/**
* 文件名:example_colorDetector.cpp
* 日期:2021/03/28
* 作者:南风无良
*/
#include <iostream>
#include <opencv2/opencv.hpp>
class ColorDetector {
private:
int maxDist; // 允许的最小差距
cv::Vec3b target; // 目标颜色
cv::Mat result; // 存储二值映射结果的图像
public:
ColorDetector() : maxDist(0.0), target(0, 0, 0) {}
ColorDetector(uchar blue, uchar green, uchar red, int mxDist);
void setColorDistanceThreshold(int distance);
int getColorDistanceThreshold() const;
void setTargetColor(uchar blue, uchar green, uchar red);
void setTargetColor(cv::Vec3b color);
cv::Vec3b getTargetColor() const;
int getDistance2TargetColor(const cv::Vec3b& color) const;
int getColorDistance(const cv::Vec3b& color, const cv::Vec3b& target) const;
cv::Mat process(const cv::Mat& image);
cv::Mat process(const std::string imageFile);
};
ColorDetector::ColorDetector(uchar blue, uchar green, uchar red, int mxDist)
{
this->target = cv::Vec3b(blue, green, red);
this->maxDist = mxDist;
}
// 设置颜色差距的阈值,阈值必须是正数,否则就设定为0
void ColorDetector::setColorDistanceThreshold(int distance)
{
if (distance < 0)
{
distance = 0;
}
this->maxDist = distance;
}
// 获取颜色差距的阈值
int ColorDetector::getColorDistanceThreshold() const
{
return this->maxDist;
}
// 设置需要检测的颜色 - 重载
void ColorDetector::setTargetColor(uchar blue, uchar green, uchar red)
{
this->target = cv::Vec3b(blue, green, red); // OpenCV颜色序列是BGR
}
void ColorDetector::setTargetColor(cv::Vec3b color)
{
this->target = color;
}
// 获取需要检测的颜色
cv::Vec3b ColorDetector::getTargetColor() const
{
return this->target;
}
// 计算与目标颜色之间的距离
int ColorDetector::getDistance2TargetColor(const cv::Vec3b& color) const
{
return getColorDistance(color, this->target);
}
// 计算两个颜色之间的城市距离
int ColorDetector::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::Mat ColorDetector::process(const cv::Mat& image)
{
cv::Mat result;
result.create(image.size(), CV_8U);
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>();
// 处理每一个像素
for (; it != itend; ++it, ++itout)
{
if (this->getDistance2TargetColor(*it) <= this->maxDist)
{
*itout = 255;
}
else
{
*itout = 0;
}
}
return result;
}
cv::Mat ColorDetector::process(const std::string imageFile)
{
cv::Mat image = cv::imread(imageFile);
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);
return image;
}
// main函数
int main(int argc, char* argv[])
{
// 加载目标图像
cv::Mat image = cv::imread("images/cow.jpg");
cv::imshow("Image", image);
std::cout << "cow: " << image.size() << std::endl;
cv::Mat result;
ColorDetector cold(30, 190, 30, 158);
// 颜色比较
const int64 start = cv::getTickCount();
result = cold.process(image);
double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
// 显示结果,并保存
cv::imshow("colorDetector", result);
cv::imwrite("output/colorDetector.jpg", result);
cv::waitKey(0);
std::cout << "wave: " << duration << "s" << std::endl;
return 0;
}
3、验证结果
原图像中的背景以绿色和蓝色偏多,可以将阈值BGR分量中B与G设置的更高,而R分量降低。
(1)原图像
(2)结果图
3.3 用GrabCut算法分割图像
1、基本原理
利用输入信息,GrabCut算法通过以下步骤进行背景/前景分割。首先,把所有未标记的像素临时标为前景(cv::GC_PR_FGD)。基于当前的分类情况,算法把像素划分为多个颜色相似的组(即K个背景组和K个前景组)。下一步是通过引入前景和背景像素之间的边缘,确定背景/前景的分割,这将通过一个优化过程来实现。在此过程中,将试图连接具有相似标记的像素,并且避免边缘出现在强度相对均匀的区域。使用Graph Cuts算法可以高效地解决这个优化问题,它寻找最优解决方案的方法是:把问题表示成一幅连通的图形,然后在图形上进行切割,以形成最优的形态。分割完成后,像素会有新的标记。然后重复这个分组过程,找到新的最优分割方案,如此反复。因此,GrabCut算法是一个逐步改进分割结果的迭代过程。根据场景的复杂程度,找到最佳方案所需的迭代次数各不相同(如果情况简单,迭代一次就足够了)。
2、代码实现
/**
* 文件名:example_GrabCut.cpp
* 日期:2021/03/29
* 作者:南风无良
*/
#include <iostream>
#include <opencv2/opencv.hpp>
void testGrabCut(const std::string imageFile, cv::Mat& result)
{
// 输入图像
cv::Mat image = cv::imread(imageFile);
cv::Mat imageBk;
image.copyTo(imageBk);
// 绘制矩形框
cv::Rect rect(80, 100, 540, 315);
cv::rectangle(imageBk, rect, cv::Scalar(255, 0, 0), 1, 8, 0);
cv::namedWindow("rectangle");
cv::imshow("rectangle", imageBk);
cv::imwrite("output/GrabCut-rectangle.jpg", imageBk);
// GrabCut分割
cv::Mat bgModel1, fgModel1;
cv::grabCut(image, result, rect, bgModel1, fgModel1, 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::namedWindow("GrabCut");
cv::imshow("GrabCut", result);
cv::imwrite("output/GrabCut-mask.jpg", result);
// 显示目标图像,需要注意的是结果图像result是二值图像
cv::Mat object;//(result.rows, result.cols, CV_8UC3, cv::Scalar(0, 0, 0));
image.copyTo(object);
cv::Mat_<uchar>::const_iterator it = result.begin<uchar>();
cv::Mat_<uchar>::const_iterator itend = result.end<uchar>();
cv::Mat_<cv::Vec3b>::iterator itin = image.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itout = object.begin<cv::Vec3b>();
for (; it != itend; it++, itin++, itout++)
{
if ((*it) != 0)
{
(*itout)[0] = (*itin)[0];
(*itout)[1] = (*itin)[1];
(*itout)[2] = (*itin)[2];
}
else
{
(*itout)[0] = 0;
(*itout)[1] = 0;
(*itout)[2] = 0;
}
}
cv::namedWindow("GrabCutOutput");
cv::imshow("GrabCutOutput", object);
cv::imwrite("output/GrabCut-output.jpg", object);
}
// main函数
int main(int argc, char* argv[])
{
std::string imageFile = "images/cow.jpg";
cv::Mat result;
// GrabCut
const int64 start = cv::getTickCount();
testGrabCut(imageFile, result);
double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
// 输出函数执行的时间
std::cout << "wave: " << duration << "s" << std::endl;
return 0;
}
3、验证结果
(1)原图像
(2)前景框
(3)掩膜
(4)分割结果
3.4 转换颜色表示法
1、基本原理
RGB色彩空间的基础是对加色法三原色(红、绿、蓝)的应用,由于人类视锥细胞对这三原色感知能力的差异,因此,RGB色彩空间并不是感知均匀的色彩空间。为了解决这个问题,引入了CIE Lab*色彩模型,把图像转换到这种表示法后,就可以真正地使用图像像素与目标颜色之间的欧几里得距离,来度量颜色之间的视觉相似度。
2、代码实现
/**
* 文件名:example_colorDetectorLab.cpp
* 日期:2021/03/29
* 作者:南风无良
*/
#include <iostream>
#include <opencv2/opencv.hpp>
class ColorDetector {
private:
int maxDist; // 允许的最小差距
cv::Vec3b target; // 目标颜色
cv::Mat result; // 存储二值映射结果的图像
cv::Mat converted; //颜色转换后图像
public:
ColorDetector() : maxDist(0.0), target(0, 0, 0) {}
ColorDetector(uchar blue, uchar green, uchar red, int mxDist);
void setColorDistanceThreshold(int distance);
int getColorDistanceThreshold() const;
void setTargetColor(cv::Vec3b color);
void setTargetColor(uchar blue, uchar green, uchar red);
cv::Vec3b getTargetColor() const;
int getDistance2TargetColor(const cv::Vec3b& color) const;
int getColorDistance(const cv::Vec3b& color, const cv::Vec3b& target) const;
cv::Mat process(const cv::Mat& image);
cv::Mat process(const std::string imageFile);
};
ColorDetector::ColorDetector(uchar blue, uchar green, uchar red, int mxDist)
{
this->target = cv::Vec3b(blue, green, red);
this->maxDist = mxDist;
}
// 设置颜色差距的阈值,阈值必须是正数,否则就设定为0
void ColorDetector::setColorDistanceThreshold(int distance)
{
if (distance < 0)
{
distance = 0;
}
this->maxDist = distance;
}
// 获取颜色差距的阈值
int ColorDetector::getColorDistanceThreshold() const
{
return this->maxDist;
}
// 设置需要检测的颜色 - 重载
void ColorDetector::setTargetColor(cv::Vec3b color)
{
this->target = color;
}
void ColorDetector::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); // OpenCV颜色序列是BGR
cv::cvtColor(tmp, tmp, CV_BGR2Lab);
this->target = tmp.at<cv::Vec3b>(0,0);
}
// 获取需要检测的颜色
cv::Vec3b ColorDetector::getTargetColor() const
{
return this->target;
}
// 计算与目标颜色之间的距离
int ColorDetector::getDistance2TargetColor(const cv::Vec3b& color) const
{
return getColorDistance(color, this->target);
}
// 计算两个颜色之间的城市距离
int ColorDetector::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::Mat ColorDetector::process(const cv::Mat& image)
{
cv::Mat result;
result.create(image.size(), CV_8U); // 使用单通道
cv::cvtColor(image, this->converted, CV_BGR2Lab);
cv::Mat_<cv::Vec3b>::iterator it = this->converted.begin<cv::Vec3b>();
cv::Mat_<cv::Vec3b>::iterator itend = this->converted.end<cv::Vec3b>();
cv::Mat_<uchar>::iterator itout = result.begin<uchar>();
// 处理每一个像素
for (; it != itend; ++it, ++itout)
{
if (this->getDistance2TargetColor(*it) <= this->maxDist)
{
*itout = 255;
}
else
{
*itout = 0;
}
}
return result;
}
cv::Mat ColorDetector::process(const std::string imageFile)
{
cv::Mat image = cv::imread(imageFile);
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);
return image;
}
// main函数
int main(int argc, char* argv[])
{
// 加载目标图像
cv::Mat image = cv::imread("images/cow.jpg");
cv::imshow("Image", image);
std::cout << "cow: " << image.size() << std::endl;
cv::Mat result;
ColorDetector cold(30, 190, 30, 158);
// 颜色比较
const int64 start = cv::getTickCount();
result = cold.process(image);
double duration = (cv::getTickCount() - start) / cv::getTickFrequency();
// 显示结果,并保存
cv::imshow("colorDetectorLab", result);
cv::imwrite("output/colorDetectorLab.jpg", result);
cv::waitKey(0);
std::cout << "wave: " << duration << "s" << std::endl;
return 0;
}
3、验证结果
从原始图像中,可以观察到目标图像的颜色以棕黄色为主,可以适当提高黄色的颜色分量数值。
(1)原始图
(2)效果图
3.5 用色调、饱和度和亮度表示颜色
1、基本原理
引入色调、饱和度和亮度的色彩空间,更加方便地评价图像的品质。其中,色调(Hue)表示主色,我们使用的颜色名称(例如绿色、黄色和红色)就对应了不同的色调值;饱和度(Saturation)表示颜色的鲜艳程度,柔和的颜色饱和度较低,而彩虹的颜色饱和度就很高;最后,亮度(Brightness)是一个主观的属性,表示某种颜色的光亮程度。OpenCV建议的两种直觉色彩空间的实现是HSV和HLS色彩空间。同时,注意感知均匀的Lab和Luv色彩空间。
2、代码实现
/**
* 文件名:example_bgr2hsv.cpp
* 日期:2021/03/29
* 作者:南风无良
*/
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
// main函数
int main(int argc, char* argv[])
{
// 加载目标图像
Mat image = imread("images/cow.jpg");
// 转换色彩空间
Mat hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
imshow("Image", hsv);
imwrite("output/hsv.jpg", hsv);
// 分离色彩空间中的三个颜色通道
vector<Mat> channels;
split(hsv, channels);
// 色调
imshow("Value", channels[0]);
imwrite("output/value.jpg", channels[0]);
// 饱和度
imshow("Saturation", channels[1]);
imwrite("output/saturation.jpg", channels[1]);
// 亮度(灰度)
imshow("Hue", channels[2]);
imwrite("output/hue.jpg", channels[2]);
// 色调、饱和度组合,亮度不变
Mat hs(128, 360, CV_8UC3);
for (int h = 0; h < 360; h++)
{
for (int s = 0; s < 128; s++)
{
hs.at<Vec3b>(s, h)[0] = h / 2; // 所有色调角度
hs.at<Vec3b>(s, h)[1] = 255 - s * 2; // 饱和度从高到低
hs.at<Vec3b>(s, h)[2] = 255;
}
}
imshow("Hue/Saturation", hs);
imwrite("output/hs.jpg", hs);
// 色彩空间的相互转换
channels[2] = 255; // 将所有像素的亮度通道赋值为255
merge(channels, hsv);
Mat newImage;
cvtColor(hsv, newImage, COLOR_HSV2BGR);
imshow("newBGRImage", newImage);
imwrite("output/newBGRImage.jpg", newImage);
waitKey(0);
return 0;
}
3、验证结果
(1)原图像
(2)BGR2HSV
(3)色调
(4)饱和度
(5)亮度
(6)色调、饱和度组合,亮度不变
(7)色彩空间的相互转换
4、扩展
(1)实现过程
/**
* 文件名:example_skin.cpp
* 日期:2021/03/29
* 作者:南风无良
*/
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
void detectHScolor(const Mat& image, double minHue, double maxHue, double minSat, double maxSat, Mat& mask);
// main函数
int main(int argc, char* argv[])
{
// 加载目标图像
Mat image = imread("images/cow.jpg");
imshow("image", image);
// 检测皮肤
Mat mask;
detectHScolor(image, 170, 10, 35, 166, mask);
Mat detected(image.size(), CV_8UC3, Scalar(0, 0, 0));
image.copyTo(detected, mask);
imshow("detected", detected);
imwrite("output/skin.jpg", detected);
waitKey(0);
return 0;
}
void detectHScolor(const Mat& image, double minHue, double maxHue, double minSat, double maxSat, Mat& mask)
{
// 转换至HSV空间
Mat hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
vector<Mat> channels;
split(hsv, channels);
// 构造色调掩膜
Mat mask1;
threshold(channels[0], mask1, maxHue, 255, THRESH_BINARY_INV);
Mat mask2;
threshold(channels[0], mask2, minHue, 255, THRESH_BINARY);
Mat hueMask;
if (minHue < maxHue)
{
hueMask = mask1 & mask2;
}
else
{
hueMask = mask1 | mask2;
}
// 构造饱和度掩膜
Mat satMask;
inRange(channels[1], minSat, maxSat, satMask);
mask = hueMask & satMask;
}
(2)结果图像
需要不断调节阈值,以期获取图像效果最优的结果图。
引用
[1]《OpenCV计算机视觉编程攻略(第3版)》