OpenCV学习笔记(二)访问和修改图像像素

问题提出

之前做开题报告需要画流程图,于是我在网上用了某某免费流程图在线制作,美滋滋地画完后发现——不!能!保!存!。原来只能免费做图,不能免费保存啊。那我就QQ截图呗,可截图有自带的网格背景,如下:
在这里插入图片描述
如果放到报告中,与白色的纸张背景非常的不搭啊。那怎么把背景的灰色网格去掉呢?有的人选择用PS或者画图的橡皮擦擦除,有的人选择换个软件重新画。如果你会使用OpenCV修改图像像素值,或许就多出了一种行之有效的办法。

基础知识

RGB表色模型

在这里插入图片描述

三基色模型

根据人眼的生理结构,几乎所有颜色(色觉)都可看作是三个基本颜色(单色光)——红、绿和蓝按不同比例的混合。为建立标准,CIE在1931年规定了三种基本色的波长,λ=700.0nm的红光,λ=546.1nm的绿光,λ=435.8nm的蓝光作为三基色。实验表明,白光可由红、绿、蓝三基色按光通量以下述比例混合而成:
φ R ∶ φ G ∶ φ B = 1 ∶ 4.5907 ∶ 0.0601 φ_R ∶ φ_G ∶ φ_B=1∶ 4.5907∶ 0.0601 φRφGφB=14.59070.0601
如果以上述的比例值作为三基色单位当量,分别记为[R]、[G]、[B],那么白光是由各为一个单位当量的三基色混合而成:
1 [ R ] + 1 [ G ] + 1 [ B ] = 白 光 1[R]+1[G]+1[B]=白光 1[R]+1[G]+1[B]=
对彩光F,其配色方程为:
F = R [ R ] + G [ G ] + B [ B ] F=R[R]+G[G]+B[B] F=R[R]+G[G]+B[B]
其中,R、G、B称为色系数。不同比例的三基色可以混出不同的颜色。

图像深度

图像深度是指存储每个像素所用的位数,也用于量度图像的色彩分辨率。比如一幅单色图像,若每个像素有8位 ,则最大灰度数目为2的8次方,即256。我们有时会看到构造函数如Mat src = Mat(600,400,CV_8UC3),意思就是说这幅彩色图像,每个像素为8位(unit),通道数(channel)为3,色彩分辨率计算为 2 8 × 2 8 × 2 8 = 16777216 2^8×2^8×2^8=16777216 28×28×28=16777216。也就是说这幅图一共可以表示出1600多万个颜色。在后文中我们可以看到,用uchar类型(无符号字符型,0-255,8位)指针指向灰度图像的像素值,代表了图像深度为8位。
很容易看出,颜色深度越高,图像色彩越鲜艳逼真。

图像的Mat结构

早期的《Learning OpenCV》一书使用了IplImage*,这完全是C风格的写法。每次退出前还需要release一遍,否则会造成内存泄漏。而OpenCV2.0以后,Mat类型作为主打,避免了释放内存的问题。
Mat是一个类,由两个数据部分组成,矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个指向存储所有像素值的矩阵的指针。
顾名思义,Mat指的是矩阵(Matrix)的缩写。我们知道,图像在计算机中就是以二维矩阵的形式进行存储,图像的深度代表着色彩分辨率。对于灰度图像,矩阵中每个像素元素的值即为灰度值。
在这里插入图片描述
对于多通道(彩色)图像,矩阵中会有多个子列,子列个数与通道数相同。如RGB图像,就有红绿蓝三个通道。
在这里插入图片描述

图片均来自毛星云编著《OpenCV3编程入门》

注意一点,在OpenCV中,子列的顺序不是RGB,而是BGR(蓝绿红)。

访问图像像素的语句和方法

当我们明白数字图像就是矩阵时,可以很好地根据C++访问二维数组的形式访问像素。目前比较流行的三种方法如下:

  1. 指针访问像素
    指针访问像素的优点是执行速度快,代码简单;缺点是容易越界。
    用for循环遍历图像每一行每一列后,定义uchar *data = src.ptr<uchar>(i, j);指向第i行第j列的像素,访问和修改data地址中数据的值即可。

  2. 迭代器访问像素
    迭代器相较指针,不会发生越界,使用比较安全。
    用迭代器操作像素,需要获得图像矩阵的begin和end,然后增加迭代直至从begin到end,将*操作符添加在迭代指针前,即可访问当前内容。

//把蓝色通道设为255
Mat_<Vec3b>::iterator it = dst.begin<Vec3b>();
Mat_<Vec3b>::iterator itend = dst.end<Vec3b>();
for ( ; it < itend; it++) {
	        (*it)[0] = 255;                               
}
  1. 动态地址访问像素
    动态地址的计算配合at方法,运行速度比较慢。但是比较符合人们的直观认识。
    用for循环遍历图像每一行每一列后,代码表示为dst.at<Vec3b>(i, j)

注意:Vec3b针对的是彩色图像的BGR三通道,是由三个unsigned char类型组成的向量。对于灰度图像,需要把Vec3b换成uchar。

实例:消除背景网格

对灰度图像的处理

我们通过观察最上方的输入图像,可以看出,需要消除的背景为灰色,亮度较暗。而主要的流程图和文字部分由蓝色和颜色较深的黑色构成。如果转化成灰度图像,只需要设定一个灰度阈值,大于该阈值的默认为白色(255),就可以得到去除网格的灰度图像了!
在程序中,我们可以定义一个滑动模块调节阈值大小,从而找到消除背景的最佳阈值。

#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int thresh = 230; //设定的灰度阈值
int maxthresh = 255; //最大的灰度阈值
void CallBack_Demo(int, void*);
Mat src, dst;
char *winname1 = "Chart image"; char *winname2 = "Output image";

int main(int argc,char **argv){
	char *filename = "D://Sources//chart.png";
	src = imread(filename,IMREAD_GRAYSCALE);
	if (src.empty()) {
		cout << "cannot load image!" << endl;
		return -1;
	}
	src.copyTo(dst);
	
	namedWindow(winname1, WINDOW_AUTOSIZE);
	namedWindow(winname2, WINDOW_AUTOSIZE);
	imshow(winname1, src);
	createTrackbar("Treshold", winname2, &thresh, maxthresh, CallBack_Demo);
	CallBack_Demo(0, 0);
	waitKey(0);
	return 0;
}
void CallBack_Demo(int, void*) {
	int rows = src.rows; int cols = src.cols;
	int i, j;
	//指针访问像素法
	for (i = 0; i < rows; i++) {
		for (j = 0; j < cols; j++) {
			uchar* data = dst.ptr<uchar>(i, j);
			if (data[0] > thresh) {
				data[0] = 255;
			}
		}
	}
	/*
	//迭代器法
	Mat_<uchar>::iterator it = dst.begin<uchar>();
	Mat_<uchar>::iterator itend = dst.end<uchar>();
	for (; it < itend; it++) {
	        if ((*it) > thresh) {
	            (*it) = 255;
	        }
	}
	//动态地址法
	for (i = 0; i < rows; i++) {
	    for (j = 0; j < cols; j++) {
	         if (dst.at<uchar>(i,j) > thresh) {
	         dst.at<uchar>(i, j) = 255;
	         }
	     }
	}
	*/
	imshow(winname2, dst);
	return;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过调整滑块可以看出,阈值在230左右消除效果较好。阈值过小会腐蚀掉需要的内容,阈值过大会使消除不明显。

对彩色图像的处理

当然,只用灰度的方法处理背景肯定是不完美的,因为导出来的图片始终是灰度图像,彩色部分产生了损失。其实,如果我们知道灰色背景网格的RGB值,也是可以通过RGB三色的阈值进行消除的。
首先,我们使用图像处理工具,我使用的是PS,获取背景灰色的RGB值。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用吸管工具可以看到,灰色的R=G=B(参照灰度图像只有灰度一个属性可以理解)。最浅的为249,最深为236。那么我们设定阈值为230,编写代码。(或者通过上面灰度图像处理的结论使用230的阈值)。这里只使用指针访问。

#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;

int main(int argc, char **argv) {
	char *filename = "D://Sources//chart.png";
	Mat src,dst;
	src = imread(filename);
	if (src.empty()) {
		cout << "cannot load image!" << endl;
		return -1;
	}
	src.copyTo(dst);
	int rows = dst.rows; int cols = dst.cols;
	int i, j;
	for (i = 0; i < rows; i++) {
		for (j = 0; j < cols; j++) {
			uchar *data = dst.ptr<uchar>(i, j);
			if (data[0]>230 && data[1]>230 && data[2]>230) {
				data[0] = 255;
				data[1] = 255;
				data[2] = 255;
			}
		}
	}
	char *winname1 = "Chart image"; char *winname2 = "Output image";
	char *savename = "D://Sources//refine.jpg";
	namedWindow(winname1, WINDOW_AUTOSIZE);
	namedWindow(winname2, WINDOW_AUTOSIZE);
	imshow(winname1, src);
	imshow(winname2, dst);
	imwrite(savename, dst);
	waitKey(0);
	return 0;
}

效果如下:
在这里插入图片描述

结语

本次笔记介绍了色彩空间及Mat存储的知识,记录了OpenCV访问像素的三大主流方法:指针法、迭代器法及动态地址法。最后通过自己的亲身实例使用阈值法,去除了不必要的背景网格。

参考文献:
[1]毛星云编著.OpenCV3编程入门[M].2015

[2]孙即祥编著.图像处理[M].2009

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值