0%

数字图像处理小作业1:RGB转CMY转HCI及简单风格化(附OpenCV安装及基本使用方法)

数字图像处理小作业1

最近软件工程课要我们用博客记录项目。我感觉这样做挺好的,一来可以让项目的开发更规范化,二来可以给博客和github添加一些内容,不让大学生活看起来很空虚。以后我所有项目作业尽量都建立仓库和博客。

代码仓库

https://github.com/SingleZombie/RGB-CMY-HSI-transformer-homework

需求分析

  • 原图像一张RGB(彩色图像)
  • 分别在RGB,CMY,和HSI三个空间通过调整颜色实现一种风格化转换(风格自选)
  • 需要提交源码以及不超过半页的代码说明(说明使用的库和参数)
  • 压缩包内文件命名方式为:
    -图像命名:
    0(此原图像)
    1RGB,2CMY,3HSI(此为处理结果)
    -文档命名:学号_姓名_1
    -代码包命名:学号_姓名_1

以上是老师给的作业要求。据说老师上课的时候还提到,三种通道的风格化结果要看起来相等。

我对风格化的理解是,改变某种彩色表示下某个分量后得到的结果。

经总结,需求有以下内容:

  • 实现RGB到CMY和HSI的转换
  • 在某种表示下改变图片风格,并调整另两种表示下的结果使得图片看起来相同
  • 写说明文档

技术学习

OpenCV入门

OpenCV安装与配置

在本作业中,我打算用OpenCV来读、写、显示图像文件。在一些任务开始前,我要先学OpenCV的用法。

我是在Win10下用VS2017编程,生成的是x86程序。

  • https://opencv.org/releases/ 下载Windows版本的库文件。下载后能得到一个exe文件,把该文件“解压”到一个路径即可。解压的文件夹里有一个opencv子文件夹。记此子文件夹为$(OPENCV)
  • x86的OpenCV库编译(可选)

    1. https://opencv.org/releases/ 下载Sources,这样编译出来的库可以满足当前编译器的配置。(直接安装的话没有x86的文件)
    2. 用cmake配置源代码。注意源代码的路径最好不要包含中文。(我第一次包含了中文,结果vs编译到一半出错了)
    3. 用vs打开cmake生成的目录下的OpenCV.sln,编译源代码。
    4. 编译结束后在目录的lib文件夹中找到编译的结果文件夹(Debug或Release),里面所有.lib文件就是编译的结果,把它们放到上一步的build文件夹里。我按照格式放到了$(OPENCV)\build\x86\vc15\lib文件夹里。
    5. 编译出的bin文件夹需要加入系统环境变量,因为里面的dll会被用到。改完环境变量重启电脑。
  • 在VS中的属性管理器新建配置,添加用户宏OPENCV,宏值就是开始那个解压出的opencv子文件夹。$(OPENCV)现在就表示库目录了。
  • 在属性的包含目录里加上$(OPENCV)\build\include,库目录改成对应版本的库的目录,比如我的就是$(OPENCV)\build\x86\vc15\lib
  • VS链接器-输入-附加依赖项中加入需要的.lib文件。我的第一个程序加入了opencv_highgui420d.lib;opencv_core420d.lib;opencv_imgcodecs420d.lib这三个库
  • 我随便到网上找了下函数,依葫芦画瓢地写了以下代码。如果一切都配置好了,程序会显示main.cpp目录下wallpaper.jpg这张图片。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

const std::string windowName = "window1";

int main()
{
cv::Mat mat = cv::imread("wallpaper.jpg");

cv::namedWindow(windowName);
cv::imshow(windowName, mat);

cv::waitKey();

return 0;
}

OpenCV 像素获取与修改

OpenCV的操作很多都基于Mat的对象,也就是图像矩阵。

1
2
3
4
unsigned char R, G, B;
B = mat.at<cv::Vec3b>(i, j)[0];
G = mat.at<cv::Vec3b>(i, j)[1];
R = mat.at<cv::Vec3b>(i, j)[2];

对矩阵的at操作可以get或set颜色分量的值。

注意RGB是反的。

OpenCV输入输出图像

1
2
cv::Mat mat = cv::imread("in.png");
cv::imwrite("out.png", mat);

输入输出都是基于Mat的对象。

RGB转CMY、HSI

RGB转CMY

看了一下,RGB符合人对颜色的认知。但现实中,为了方便打印出黑色,用CMY表示颜料的颜色更加方便。

CMY就是RGB取反,即:

1
2
3
C = 255 - R;
M = 255 - G;
Y = 255 - B;

RGB转HSI

我对HSI的大致理解是:H是颜色的属性,比如是红色、绿色、蓝色还是其他颜色;S是颜色被稀释的属性,也就是颜色的深浅;I是光强,是颜色向量的模的大小。

https://blog.csdn.net/yangleo1987/article/details/53171623 里有好几个RGB转HSI方法的介绍。其中第一种是上课提到的方法。

风格转换

我问了一下别人,又看了一下书。感觉风格转换就是对某个颜色通道做一个函数映射,使得图片整体风格改变。

结构设计

由于程序过于简单,该程序用结构化的设计方法。

输入经过输入预处理模块,进入处理模块,最后进入输出模块输出。整个结构非常简明而无趣。

1
2
3
//         ******************    ***********************    *******************
// data -> ** input module ** -> ** processing module ** -> ** output module ** -> output
// ****************** *********************** *******************

由于要提交源代码文件,我把代码全部放到一个main.cpp里面了。但在我心中,还是为每个模块各建了一个头文件和cpp文件的。

程序设计

程序概览

代码放在Github上:代码仓库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cv::Mat mat = getInput("0.jpg");

auto matArr = processImage(mat);

outputImage(matArr[0], "1RGB.jpg");
outputImage(matArr[1], "2CMY.jpg");
outputImage(matArr[2], "3HSI.jpg");

cv::imshow("w1", matArr[0]);
cv::imshow("w2", matArr[1]);
cv::imshow("w3", matArr[2]);

cv::waitKey();

return 0;
}

main函数非常简明,getInputoutputImage隐藏了输入输出细节,方便修改。事实上,图像读入时像素是用unsigned char存储的,我在输入的时候把它转换成了float,输出的时候又转了回去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 0; i < mat.rows; i++)
{
for (int j = 0; j < mat.cols; j++)
{
// tranformation
matCMY.at<cv::Vec3f>(i, j) = bgrToYmc(matCMY.at<cv::Vec3f>(i, j));
matHSI.at<cv::Vec3f>(i, j) = bgrToHsi(matHSI.at<cv::Vec3f>(i, j));

// tonal function
// ........


// inverse transformation
matCMY.at<cv::Vec3f>(i, j) = ymcToBgr(matCMY.at<cv::Vec3f>(i, j));
matHSI.at<cv::Vec3f>(i, j) = hsiToBgr(matHSI.at<cv::Vec3f>(i, j));
}
}

processImage的主体是遍历像素,先把原图像做格式转换,再用自己的风格转换函数瞎搞,最后转换回正常的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
inline cv::Vec3f bgrToYmc(const cv::Vec3f& vec)
{
return cv::Vec3f(1.0f - vec[0], 1.0f - vec[1], 1.0f - vec[2]);
}
inline cv::Vec3f ymcToBgr(const cv::Vec3f& vec)
{
return cv::Vec3f(1.0f - vec[0], 1.0f - vec[1], 1.0f - vec[2]);
}
inline cv::Vec3f bgrToHsi(const cv::Vec3f& vec)
{
float R = vec[2], G = vec[1], B = vec[0];
float RG = (R - G), RB = (R - B);
float tmp = sqrt(RG * RG + RB * (G - B)); // 分母不能为0!!!
float theta = tmp != 0 ? acos(0.5f * (RG + RB) / tmp) : 0;
theta = B > G + EPS ? CV_2PI - theta : theta;
return cv::Vec3f(
theta / CV_2PI,
1.0f - 3.0f * std::min(std::min(R, G), B) / (R + G + B),
(R + G + B) / 3.0f
);
}
inline cv::Vec3f hsiToBgr(const cv::Vec3f& vec)
{
float H = vec[0] * CV_2PI, S = vec[1], I = vec[2];
float H2 = fmod(H, CV_2PI / 3);
float v1 = I * (1.0f - S);
float v2 = I * (1.0f + S * cos(H2) / cos(CV_PI / 3 - H2));
float v3 = 3 * I - v1 - v2;
if (H + EPS < CV_2PI / 3.0f)
{
return cv::Vec3f(v1, v3, v2);
}
else if (H + EPS < 2.0f * CV_2PI / 3.0f)
{
return cv::Vec3f(v3, v2, v1);
}
else
{
return cv::Vec3f(v2, v1, v3);
}
}

格式转换函数完全是在照搬公式而已,没有什么价值。

写程序过程中碰到的问题

  1. 为了公式处理方便,我把像素格式转换成了float。我一开始以为直接就能通过mat.at<Vec3f>来访问浮点格式像素,但是发现cv::Mat的机制好像是只能允许像素按一种格式存。要把图像矩阵用.convertTo(mat, CV_32FC3, 1 / 255.0)来转换成浮点表示,最后输出的时候还要用.convertTo(tmpMat, CV_8UC3, 255.0f)转换回去。
  2. 我还好提前看了一下cv::Mat的工作原理。该类只存了图像数据的指针,要复制图像数据的话要调用src.copyTo(dest)
  3. 图片转HSI再转回来后,我发现图像中出现一些绿色方块。经调试发现,H分量在运算的时候变成了nan。计算H的时候一定要判断分母是否为0!
  4. 图像的RGB是倒着存的。我之前测试OpenCV的时候碰到了这个问题,写这个程序的时候就跳过了这个坑。

处理结果

0.jpg

原图

1.jpg

RGB风格转换结果

2.jpg

CMY风格转换结果

3.jpg

HSI风格转换结果

感想

写博客真浪费时间。