我们在前面讲了配置,讲了OpenCV一个很重要的数据结构Mat。那么Mat就是为了图像操作而生的。
试想对于一个二维数组我们是如何遍历的?
我们写嵌套循环,第一个循环是二维数组的行数控制,第二个就是列数控制。
那么这一套也完全可以用到图像遍历中来!
只是我们需要考虑的一个问题就是图像的存储是否是连续的呢?即第一行的最后一列和第二行的第一个之间的地址差值是相邻的么?
好吧,实际上我们不需要通过大串代码去判断,因为有内置API!
那么我们写一个遍历的形式:
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat img = imread(file_path);
// 判断一张图像的存储是否是连续的
cout << boolalpha << img.isContinuous() << endl;
//Mat copy_img = Mat::zeros(img.size(), CV_8UC3);
int row = img.rows;
int col = img.cols;
int channel = img.channels();
// 试想如果是三通道,那么我们的列数就应该乘上3能对每个值进行遍历到
col *= channel;
// 如果是连续的话我们可以用一维的形式来遍历即可,遍历总数就是col * row * channel
if (img.isContinuous()) {
row = 1;
col *= row;
}
uchar* p;
for (int i = 0; i < row; i++) {
p = img.ptr<uchar>(i);
for (int j = 0; j < col; j++) {
cout << p[j] << endl;
}
}
return 0;
}
我们读入一张图片后获得其行数与列数。在列数上值得我们注意,因为不要忘了图片的通道数!上述代码中我们直接将列数与其相乘得到一个新值后嵌套了两层循环,但是也未必一定如此。我们可以嵌套三层循环,令第三层循环被通道数控制即可。我们每遍历一行就先去获得当前行的首地址,即用ptr来获取。
ptr就代表了指针pointer,uchar就是数据类型代表unsigned char,最后的i就是行标。如此形式获得的就是第i行的首地址。
这里就不进行打印输出了,因为实在太长了。我们在进行项目Debug时可能对问题定位要输出,平时的话一张小图也就算了,我这张图太大!
在介绍迭代器遍历时,要先介绍另一种OpenCV数据结构,是Vec3b。
假设我们的一个像素点是三通道的,那么我们可以用一个Vec3b数据结构来装取那个像素点的值,同时用索引“[]”来获得某个通道的具体值。
(要注意Vec3b中的三个值都是uchar类型。)
迭代器相信大家在用STL标准库中很多容器的时候都已经体会到过了,我们只要获得一个头一个尾,然后类似于指针一样不断往后移得到的就完成了,那么步骤实际上一模一样。声明迭代器,获得头和尾,让迭代器从头到尾即可:
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat img = imread(file_path);
MatIterator_<Vec3b> it; // 声明迭代器
for (it = img.begin<Vec3b>(); it < img.end<Vec3b>(); it++) {
//通过API直接获得图头和尾
cout << "B:" << (*it)[0] << endl;
cout << "G:" << (*it)[1] << endl;
cout << "R:" << (*it)[2] << endl;
}
return 0;
}
当然我们也可以声明迭代器时用uchar,但那适用于单通道图。
~~注:我们在打印输出uchar数据时常常获得到一些怪异符号,我们只需要强制类型转换一下变成int就ok!
官教里还有一个关于lookuptable的不是很感兴趣也没咋接触过,就先不用了。
最近的AI热居高不下,可以用obsession来形容。
大部分人对图像滤波的感想应该是,例如“某图秀秀的P图(磨皮?高亮?)”,或者说就是接触到过AI的kernel“滤波核”。大部分时间我们都在感受滤波带来的强大的功能或者调用API直接操作,我们同时也得对实现原理进行一下深入的解析。
首先直接放代码,这样解释起来也会比较方便:
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
void Sharpen(Mat& A, Mat& B);
int main() {
Mat img = imread(file_path);
Mat B;
B.create(img.size(), img.type());
double t = (double)getTickCount();
Sharpen(img, B);
cout << "Time is " << ((double)getTickCount() - t) / getTickFrequency() << endl;
imshow("filter", B);
waitKey(0);
Mat dst = Mat::zeros(img.size(), img.type());
Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
t = (double)getTickCount();
filter2D(img, dst, dst.depth(), kernel);
cout << "Time is " << ((double)getTickCount() - t) / getTickFrequency() << endl;
imshow("Like", dst);
waitKey(0);
return 0;
}
void Sharpen(Mat& A, Mat& B) {
int row_num = A.rows;
int col_num = A.cols;
int chanel = A.channels();
for (int i = 1; i < row_num - 1; i++) {
const uchar* up = A.ptr<uchar>(i - 1);
const uchar* pa = A.ptr<uchar>(i);
const uchar* un = A.ptr<uchar>(i + 1);
uchar* pb = B.ptr<uchar>(i);
for (int j = chanel; j < (col_num - 1) * chanel; j++) {
*pb++ = saturate_cast<uchar>(5 * pa[j] - up[j] - un[j] - pa[j + chanel] - pa[j - chanel]);
}
}
B.row(0).setTo(Scalar(0));
B.row(row_num - 1).setTo(Scalar(0));
B.col(0).setTo(Scalar(0));
B.col(col_num - 1).setTo(Scalar(0));
}
滤波干的一件事其实就是改变原图中的像素值,其核心也就在于如何改变?这样的改变引起的效果是什么?为了达到某种效果我们如何设计滤波核权值矩阵?
同样是以官教为例:
其为我们设计了如下的一个权值矩阵
那么根据此矩阵如何来改变一个像素值呢?
我们要紧盯中心点,这个点实际上就代表了我们正遍历到那个点!
根据这个思路我们发现,我们在遍历到的本身这个点得到了5的权值,其四周得到的都是-1的权值,那么最后此点的值的计算方式就变为:
这样一来就决定了我们遍历那张被操作的图时要从第二行开始到倒数第二行结束,而列数就较为复杂,画一张以三通道为例的图来解释一下:
仔细看,我们的列循环真的是从1开始吗?不是的!是从channel开始,比如上图的三通道我们第一个滤波的像素就是第二列的Blue,那么其序号是几?是3!是cahnnel数!这样一来也就不难理解,列循环的封顶就是 <(col-1)* channel了。
当那么当我们了解了滤波这么个过程就好写这个东西了,我们嵌套的两层循环的条件全已经出来了。同时我们要设置三个常指针,分别指向当前像素位置的上一行,当前行和下一行。同时要注意,上下很好理解,因为处于同一列!但左右不是简单地减去1啊,也是减去channel啊,看看我画的图就知道了,第二列的B要对应到第一列的B啊,于是减去的仍旧是3啊。
最后的最后,我们要为没有滤波的也就是头尾两行和两列变为0。这是官方说的简单的处理方式,我寻思,不处理不是更加简单吗哈哈~~~~
然后又到了我们喜欢的API环节,OpenCV帮我们封装好了滤波函数,就是filter2D。参数对应的就是原图,滤波后的图,图像位深度和滤波核。
那么这么一来就不用我们每次手写了,因为有时候可能滤波矩阵更大,打个比方15 * 15,那么我们就要从第7行开始······确实是解放了勤劳的双手。
**这里插一嘴,虽然基本上都说是filter2D的时间来得快,不见得啊,我没一次测到的是这个快,有问题,有猫腻,我也不明白!! **
最基础的就是要学会读入和写出一张图片,也就是imread函数的使用:
当然最基本的上面代码都已经看到了就是参数加上文件路径名即可。
那么有时候我们想得到的或许仅仅是一张灰度图的话,这样:
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat img = imread(file_path, IMREAD_GRAYSCALE);
imshow("im", img);
waitKey(0);
return 0;
}
得到的就是这样一张图:
实际上读入的可以有那么多选项,没必要一一介绍,自己摸索即可:
读和写是一对并存的操作,这不像OS,或者Database,读给你上一把锁写给你上一把锁,读给你设一个权限,写也给你一个权限。
写函数也很简单比如:
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
const string file_path = "D://C++//kingjames//Debug//favorite.jpg";
int main() {
Mat img = imread(file_path, IMREAD_GRAYSCALE);
imwrite("D://new.png", img);
return 0;
}
你就会发现在D盘下多了一张new.png了。就是那么简单!
!!如果你的D盘下是有权限的,那么不一定可以哦,换一个有权限的地方写就行啦!!
(注:以下部分均直接摘自官网!)
我们已经会图像的遍历了,图像的索引实际上也差不多,对于一张单通道图,我们可以这样来搞:
at函数,指定类型为uchar,y和x就是对应的位置。
Scalar intensity = img.at<uchar>(y, x);
这种方式最需要注意的就是x和y的顺序,如果我们嫌记不住,就得这样写:
Scalar intensity = img.at<uchar>(Point(x, y));
三通道的索引方式就得利用到Vec3b了:
Vec3b intensity = img.at<Vec3b>(y, x);
uchar blue = intensity.val[0];
uchar green = intensity.val[1];
uchar red = intensity.val[2];
这种索引方式也可以用于直接修改其位置对应的值。
有是后我们会选取一块自己感兴趣的值,形状得由自己定,这里展示的最简单的矩形:
Rect的前两个参数固定左上顶点位置,后两个位置分别代表宽和高。
其还有多种初始化的方法,传入两个Point也是可以的······
Rect r(10, 10, 100, 100);
Mat smallImg = img(r);
我们第二节就讲过的色彩域,只需一个cvtColor就行:
一大堆变换不逐一解释,用到哪个说哪个,更何况很多用不到。
接下来就是变化图片的存储方式:
我们很常见的就是8bit unsigned char类型,其实还有很多,而这就借助于我们的convertTo来完成:(反正也是一大截)
那么如何展示我们的图片相信都已经看腻了,就是imshow。
imshow的第一个参数是窗口名,第二个就是Mat要被展示的矩阵。
我们在Debug时需要用一个waitKey命令来使图片停留,否则图片无法显示哦!
基本就是这些。
现在借用郭锥名言,郭Jerry的“好吧,那我现在走了,白白”!