opencv-5-图像遍历与图像改变

opencvc++qt

目录

开始

图像的像素点访问与遍历

我们在上一篇文章中已经 大概介绍了 mat 图像的数据格式, 实际上可以理解为一个二维数组的格式, 那么 茴香豆的茴字一共有几种写法 访问一个像素点有几种方式呢

在opencv 的官方文档: How to scan images, lookup tables and time measurement with OpenCV

说了一共有三种, 常用的做法, 也可以参考Opencv访问图像像素的三种方法 这篇文章,

opencv 座标定义

opencv 对于图像数据的 座标是从左上角开始 的,

  • 纵向的座标 y 也称为行 rows
  • 横座标 y 也称为 列 cols

    座标范围: (0,0)-- (rows-1,cols-1) 我们一般使用 (行,列) 的方式进行访问,

也比如 我们使用 设置 行列尺寸的

但是对于 二维点, 实际上是 以 列行做 的尺寸, 此处也要进行注意

下标访问

对于二维数组, 肯定是使用 下标索引访问了

比如我们在上一篇文章中, 使用 lena_rgb.at<cv::Vec3b>(i, j) 进行彩色图像的访问, 使用lena_gray_avg.at<uchar>(i, j) 进行灰度图像的访问.

也就是 Matat() 方法进行图像的访问, 具体还要考虑灰度图像或者 彩色图像, 因为对于灰度图像只有一个值, 彩色图像每一个位置是有3个值的, 我们可以使用 lena_rgb.at<cv::Vec3b>(i, j)[k] 来 访问对应的 BGR 的值,


BGR 图像访问
// 遍历每一个像素进行灰度化
for (int i = 0; i < lena_rgb.rows; i++)
{
for (int j = 0; j < lena_rgb.cols; j++)
{
img_brg.at<cv::Vec3b>(i,j)[0] = 0; // 蓝色通道设为0
img_gray.at<uchar>(i,j) = 0; // 灰度设为 0
}
}

指针访问

图像数据是每行存储存储的, 我们可以每次获取到一行的数据 然后把行数据作为一维数组访问, 使用指针的方式就变得很简单了, 也是目前是最快的访问方式,

对于 灰度图像, 我们可以使用 uchar* pdata = img_gray.ptr<uchar>(i) 访问灰度图像一行的数据, 使用 cv::Vec3b* pdata = img_gray.ptr<cv::Vec3b>(i) 访问一行数据,

cv::Mat img_gray = cv::Mat::zeros(lena_rgb.size(), CV_8UC1);
cv::Mat img_bgr = cv::Mat::zeros(lena_rgb.size(), CV_8UC3);
// 使用指针进行图像访问
for (int i = 0; i < lena_rgb.rows; i++)
{
uchar *p_gray = img_gray.ptr<uchar>(i);
cv::Vec3b *p_bgr = img_bgr.ptr<cv::Vec3b>(i);
for (int j = 0; j < lena_rgb.cols; j++)
{
p_gray[j] = 0;
p_bgr[j][0] = 0;
}
}

我们通过行的索引, 获取到 第 i 行的数据指针, 然后使用作为一维数组的访问方式进行指针数据的访问,

功能强度, 十分快速, 但是可能会由于指针出现访问出错,

迭代器法访问

迭代器是 C++ 11(不确定) 之后的方案, 通过迭代器能够访问不连续的数据, 这样, 我们只需要给出图像的 开始地址与 结束地址就能完成图像的访问, 也是目前最安全的方案, 不会出现越界的错误

对于灰度图像或者 彩色图像, 我们都能够使用迭代器进行访问,

// 使用迭代器访问
for (cv::Mat_<cv::Vec3b>::iterator it = img_bgr.begin<cv::Vec3b>();
it != img_bgr.end<cv::Vec3b>(); it++)
{
(*it)[0] = 0;
}

遍历访问时间对比

在 opencv 的文档中, 给出了一个时间的对比方式, 通过获取 CPU 的运行时间 对比算法,

上面中, 我们给出了访问图像数据的三种方式, 这样我们就能进行一个一个像素的访问数据了,

其实, 我们在每个遍历的前后添加时间 测量程序, 最后得到这样的程序

#include "mainwindow.h"
#include <QApplication>
// 引入 opencv 函数头文件
#include <opencv2/opencv.hpp>
int main(int argc, char *argv[])
{
//QApplication a(argc, argv);
//MainWindow w;
//w.show();
// 设置 要显示的图像路径
//std::string test_pic = "./TestImages/lena.png";
double time_cnt = 0;
double time_s = 0.0; // 读取图像
// cv::Mat lena_rgb = cv::imread(test_pic);
// 声明 彩色图像 和灰度图像 // 设置 10000*10000 尺寸的图像, 避免出错
cv::Mat img_bgr = cv::Mat::zeros(cv::Size(1000, 1000), CV_8UC3); time_cnt = cv::getTickCount();
// 遍历每一个像素进行灰度化
for (int i = 0; i < img_bgr.rows; i++)
{
for (int j = 0; j < img_bgr.cols; j++)
{
img_bgr.at<cv::Vec3b>(i, j)[0] = 0;
}
}
time_s = ((double)cv::getTickCount() - time_cnt) / cv::getTickFrequency();
printf("index scan image time: \t\t %f second \n", time_s); time_cnt = cv::getTickCount();
// 使用指针进行图像访问
for (int i = 0; i < img_bgr.rows; i++)
{
cv::Vec3b *p_bgr = img_bgr.ptr<cv::Vec3b>(i);
for (int j = 0; j < img_bgr.cols; j++)
{
p_bgr[j][0] = 0; // 访问(i,j) 的第一个通道
}
}
time_s = ((double)cv::getTickCount() - time_cnt) / cv::getTickFrequency();
printf("pointer scan image time: \t %f second \n", time_s); time_cnt = cv::getTickCount();
// 使用迭代器访问
for (cv::Mat_<cv::Vec3b>::iterator it = img_bgr.begin<cv::Vec3b>();
it != img_bgr.end<cv::Vec3b>(); it++)
{
(*it)[0] = 0;
}
time_s = ((double)cv::getTickCount() - time_cnt) / cv::getTickFrequency();
printf("iterator scan image time: \t %f second \n", time_s); cv::waitKey(0); return 0;
// return a.exec();
}

最后,运行之后便能够得到我们的运行时间, 指针访问还是最快的方式,

index scan image time:           0.040871 second
pointer scan image time: 0.015297 second
iterator scan image time: 0.561931 second

差别还是有点大的, 使用 指针的方式是最快的, 迭代器是最安全的 , 但是 迭代器在较大尺寸的图的时候 是真的慢, 我们测试 的是 1000*1000 尺寸的图像, 时间差别还是比较大的, 在图像处理的过程中欧能够, 遍历图像还是比较常用的手段的, 所以 可以考虑考虑自己最熟悉的方式进行 图像遍历.. 性能情况下要多使用 指针方式访问, 注意具体的访问越界即可.

图像操作

我们在能够实现图像的像素点访问之后, 会想到干什么呢, opencv的 例程中给出了两个有用的案例

一个是将两幅图像做 混合叠加, 另外一个是处理图像的亮度和对比度

图像叠加

我们找两个等大的图像, 对于每一个点,像素相加除以2 得到平均值, 然后 生成新的图像

代码编写

感觉就是访问两幅图像, 然后叠加就好了, 跟上面讲的一样, 没有太多难度

// 设置 要显示的图像路径
std::string img_panda = "./TestImages/panda.png";
std::string img_lena = "./TestImages/lena.png"; // 读取两幅彩色图像 512*512
cv::Mat panda_bgr = cv::imread(img_panda);
cv::Mat lena_bgr = cv::imread(img_lena);
// 声明结果图像
cv::Mat res_bgr = cv::Mat::zeros(lena_bgr.size(), CV_8UC3); for (int i = 0; i < lena_bgr.rows; i++)
{
for (int j = 0; j < lena_bgr.cols; j++)
{
res_bgr.at<cv::Vec3b>(i, j)[0] = (panda_bgr.at<cv::Vec3b>(i, j)[0] + lena_bgr.at<cv::Vec3b>(i, j)[0]) / 2;
res_bgr.at<cv::Vec3b>(i, j)[1] = (panda_bgr.at<cv::Vec3b>(i, j)[1] + lena_bgr.at<cv::Vec3b>(i, j)[1]) / 2;
res_bgr.at<cv::Vec3b>(i, j)[2] = (panda_bgr.at<cv::Vec3b>(i, j)[2] + lena_bgr.at<cv::Vec3b>(i, j)[2]) / 2;
}
}
cv::imshow("panda_bgr", panda_bgr);
cv::imshow("lena_bgr", lena_bgr);
cv::imshow("res_bgr", res_bgr);
cv::waitKey(0);

执行结果

这种就是单纯像素的叠加, 没有什么深入的点, 理解就好了


执行结果

图像"拼接"

考虑一种拼接, 我们只是 将两幅图像并起来, 不考虑复杂的图像匹配, 我们可以简单的写一下, 也很简单

其实代码也很简单

// 设置 要显示的图像路径
std::string img_panda = "./TestImages/panda.png";
std::string img_lena = "./TestImages/lena.png"; // 读取两幅彩色图像 512*512
cv::Mat panda_bgr = cv::imread(img_panda);
cv::Mat lena_bgr = cv::imread(img_lena);
// 声明结果图像 1020*1020
cv::Mat res_bgr = cv::Mat::zeros(cv::Size(1024,1024), CV_8UC3); for (int i = 0; i < lena_bgr.rows; i++)
{
for (int j = 0; j < lena_bgr.cols; j++)
{
// 复制第一副图像
res_bgr.at<cv::Vec3b>(i, j)[0] = (panda_bgr.at<cv::Vec3b>(i, j)[0]);
res_bgr.at<cv::Vec3b>(i, j)[1] = (panda_bgr.at<cv::Vec3b>(i, j)[1]);
res_bgr.at<cv::Vec3b>(i, j)[2] = (panda_bgr.at<cv::Vec3b>(i, j)[2]); // 在第一副图下面 拼接 反色图像
res_bgr.at<cv::Vec3b>(512+i, j)[0] = (255- panda_bgr.at<cv::Vec3b>(i, j)[0]);
res_bgr.at<cv::Vec3b>(512+i, j)[1] = (255 - panda_bgr.at<cv::Vec3b>(i, j)[1]);
res_bgr.at<cv::Vec3b>(512+i, j)[2] = (255 -panda_bgr.at<cv::Vec3b>(i, j)[2]); // 复制第二幅图像
res_bgr.at<cv::Vec3b>(i, 512+j)[0] = (lena_bgr.at<cv::Vec3b>(i, j)[0]);
res_bgr.at<cv::Vec3b>(i, 512+j)[1] = (lena_bgr.at<cv::Vec3b>(i, j)[1]);
res_bgr.at<cv::Vec3b>(i, 512+j)[2] = (lena_bgr.at<cv::Vec3b>(i, j)[2]); // 在第二副图下面 拼接 反色图像
res_bgr.at<cv::Vec3b>(512 + i, 512+j)[0] = (255 - lena_bgr.at<cv::Vec3b>(i, j)[0]);
res_bgr.at<cv::Vec3b>(512 + i, 512+j)[1] = (255 - lena_bgr.at<cv::Vec3b>(i, j)[1]);
res_bgr.at<cv::Vec3b>(512 + i, 512+j)[2] = (255 - lena_bgr.at<cv::Vec3b>(i, j)[2]); }
}

"拼接" 图像

图像 相减

在考虑一种情况, 我们彩色图像的三个通道值有大有小, 那所有值减去最小值会得到什么呢,

看 代码:

for (int i = 0; i < lena_bgr.rows; i++)
{
for (int j = 0; j < lena_bgr.cols; j++)
{
// 求出最小值
cv::Vec3b tmp_px = lena_bgr.at<cv::Vec3b>(i, j);
int min_c = std::min(std::min(tmp_px[0], tmp_px[1]), tmp_px[2]); // 每个通道减去最小值
res_bgr.at<cv::Vec3b>(i, j)[0] = tmp_px[0] - min_c;
res_bgr.at<cv::Vec3b>(i, j)[1] = tmp_px[1] - min_c;
res_bgr.at<cv::Vec3b>(i, j)[2] = tmp_px[2] - min_c;
}
}

运行结果


图像相减

亮度和对比度操作

上面的两个操作只是玩玩, opencv 的例程中 关于

亮度和对比度的操作还是可以试试的Changing the contrast and brightness of an image!

亮度是指 数字图像的明暗程度

对比度是值 图像最高亮度与最低亮度的差值

锐度: 图像边缘像素的对比度

可以参考文章【数字图像处理系列二】亮度、对比度、饱和度、锐化、分辨率

其实吧, 知道就行了, 具体深究也可以看这篇一次搞懂清晰度、对比度以及锐化的区别, 有很多图片可以查看, 还能通过图像进行对比.

亮度操作

回到正题, 我们要进行 亮度变换 其实就是在进行 图像灰度值的调节过程.

是原始图像 灰度放大倍数 , 是灰度的偏置 bias

我们来实现一下, 看下效果:

参数选择 例程中的 ,

float a = 2.2f, b = 50;
for (int i = 0; i < lena_bgr.rows; i++)
{
for (int j = 0; j < lena_bgr.cols; j++)
{
// 取出原始图像 灰度值
cv::Vec3b tmp_px = lena_bgr.at<cv::Vec3b>(i, j);
// 每个通道减去最小值
res_bgr.at<cv::Vec3b>(i, j)[0] = cv::saturate_cast<uchar>(a * tmp_px[0] + b);
res_bgr.at<cv::Vec3b>(i, j)[1] = cv::saturate_cast<uchar>(a * tmp_px[1] + b);
res_bgr.at<cv::Vec3b>(i, j)[2] = cv::saturate_cast<uchar>(a * tmp_px[2] + b);
}
}

运行结果图:


亮度提升

这里使用的是 cv::saturate_cast<uchar> 进行的结果转换, 都是转换成 uchar 数据, 如果

直接使用 uchar 转换 得到的结果图像会很奇怪, 例如: res_bgr.at<cv::Vec3b>(i, j)[0] = (uchar)(a * tmp_px[0] + b);


直接使用 uchar 转换结果

伽马矫正(Gamma)

线性变化还有很多, 灰度转换, 截取, 反色等等操作, 但是有一种非线性变化, 必须要进行介绍, 那就是 伽马矫正, 用来对于矫正输入图像的亮度值,

具体的公式表示为:

对于不同的 值, 我们绘制输入输出曲线可以得到这个图,


gamma 矫正

我们测试一下代码试试, 例程中使用了 一个 LUT的函数,

因为看上面的变换公式, 涉及到了指数运算, 如果我们每个像素值都计算一次 会比较花时间, 反正对于一个像素值, 计算出来的gamma 值是一样的 , 我提前计算好, 之际查找不就好了吗,

我们在最开始计算出来每个 灰度值的结果表,

// 自定义 gamma 参数
float gamma = 0.4;
// 生成gamma 查找表
uchar table[256] = { 0 };
for (int i = 0; i < 256; i++)
{
table[i] = std::pow(i / 255.0f, gamma) * 255;
} for (int i = 0; i < lena_bgr.rows; i++)
{
for (int j = 0; j < lena_bgr.cols; j++)
{
// 取出原始图像 灰度值
cv::Vec3b tmp_px = lena_bgr.at<cv::Vec3b>(i, j);
// 每个通道减去最小值
res_bgr.at<cv::Vec3b>(i, j)[0] = table[tmp_px[0]];
res_bgr.at<cv::Vec3b>(i, j)[1] = table[tmp_px[1]];
res_bgr.at<cv::Vec3b>(i, j)[2] = table[tmp_px[2]];
}
}

lena 进行 gamma = 0.4 的运算结果

这个效果不是很好, 可以参考 opencv 例程里面的图, 效果真的很不错


opencv 例程 gamma

其他

opencv-5-图像遍历与图像改变的更多相关文章

  1. opencv——图像遍历以及像素操作

    摘要 我们在图像处理时经常会用到遍历图像像素点的方式,在OpenCV中一般有四种图像遍历的方式,在这里我们通过像素变换的点操作来实现对图像亮度和对比度的调整. 补充: 图像变换可以看成 像素变换--点 ...

  2. OpenCV学习笔记:resize函数改变图像的大小

    OpenCV提供了resize函数来改变图像的大小,函数原型如下: , , int interpolation=INTER_LINEAR ); 参数解释: src:输入,原图像,即待改变大小的图像: ...

  3. 第八节,Opencv的基本使用------存取图像、视频功能、简单信息标注工具

    1.存取图像 import cv2 img=cv2.imread('test.jpg') cv2.imwrite('test1.jpg',img) 2.图像的仿射变换 图像的仿射变换涉及图像的形状位置 ...

  4. OpenCV计算机视觉学习(11)——图像空间几何变换(图像缩放,图像旋转,图像翻转,图像平移,仿射变换,镜像变换)

    如果需要处理的原图及代码,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice 图像 ...

  5. 【OpenCV入门教程之三】 图像的载入,显示和输出 一站式完全解析(转)

    本系列文章由@浅墨_毛星云 出品,转载请注明出处. 文章链接:http://blog.csdn.net/poem_qianmo/article/details/20537737 作者:毛星云(浅墨)  ...

  6. 跟我一起学opencv 第四课之图像的基本操作

    1.图像是由像素组成的,所以修改了像素就可以实现图像的改变. 2先看灰度图像(单通道): *****2.获取灰度图像的像素值使用:  int gray = gray_src.at<uchar&g ...

  7. 第十三节,OPenCV学习(二)图像的简单几何变换

    图像的简单几何变换 几何变换不改变图像的像素值,只是在图像平面上进行像素的重新安排 适当的几何变换可以最大程度地消除由于成像角度.透视关系乃至镜头自身原因所造成的几何失真所产生的的负面影响. 一.图像 ...

  8. Python 图像处理 OpenCV (4):图像算数运算以及修改颜色空间

    前文传送门: 「Python 图像处理 OpenCV (1):入门」 「Python 图像处理 OpenCV (2):像素处理与 Numpy 操作以及 Matplotlib 显示图像」 「Python ...

  9. OpenCV计算机视觉学习(1)——图像基本操作(图像视频读取,ROI区域截取,常用cv函数解释)

    1,计算机眼中的图像 我们打开经典的 Lena图片,看看计算机是如何看待图片的: 我们点击图中的一个小格子,发现计算机会将其分为R,G,B三种通道.每个通道分别由一堆0~256之间的数字组成,那Ope ...

随机推荐

  1. python学习第二节 数据类型、字符编码、文件处理

    标准数据类型 Python3 中有六个标准的数据类型: Number(数字) String(字符串) List(列表) Tuple(元组) Sets(集合) Dictionary(字典) 数字 #整型 ...

  2. C 怪兽游戏

    时间限制 : - MS   空间限制 : - KB  评测说明 : 1s,256m 问题描述 何老板在玩一款怪兽游戏.游戏虽然简单,何老板仍旧乐此不疲.游戏一开始有N只怪兽,编号1到N.其中第i只怪兽 ...

  3. C语言把整数转换为字符串

    目录 1.把整数/长整数格式化输出到字符串 2.注意事项 3.版权声明 各位可能在网上看到用以下函数可以将整数转换为字符串: itoa(); //将整型值转换为字符串 ultoa(); // 将无符号 ...

  4. PTA数据结构与算法题目集(中文) 7-36 社交网络图中结点的“重要性”计算 (30 分)

    PTA数据结构与算法题目集(中文)  7-36 社交网络图中结点的“重要性”计算 (30 分) 7-36 社交网络图中结点的“重要性”计算 (30 分)   在社交网络中,个人或单位(结点)之间通过某 ...

  5. Redis 过期时间相关命令

    命令 示例和描述 PERSIST PERSIST key-name —— 移除键的过期时间 TTL TTL key-name —— 查看给定键距离过期还有多少秒 EXPIRE EXPIRE key-n ...

  6. 真没想到,Springboot能这样做全局日期格式化,有点香!

    最近面了一些公司,有一些 Java方面的架构.面试资料,有需要的小伙伴可以在公众号[程序员内点事]里,无套路自行领取 说在前边 最近部门几位同事受了一些委屈相继离职,共事三年临别之际颇有不舍,待一切手 ...

  7. VUE_shop(第十天)项目的打包优化

    项目的打包优化 1.添加页面的加载效果 1.首先安装运行依赖nprocess,在main.js文件中的axios拦截器拦截请求的时候调用Npeocees.start. 在拦截响应的时候调用nproce ...

  8. CDR

    伴随着新经济.独角兽一同被热议的,中国将很快推出存托凭证迎接独角兽回归.中国存托凭证(CDR)已成为当下热门话题.说不清CDR,还能和小伙伴们愉快地聊天吗? CDR到底是什么?它具有哪些优势?能否带来 ...

  9. C语言实现双向链表

    目前我们所学到的链表,无论是动态链表还是静态链表,表中各节点中都只包含一个指针(游标),且都统一指向直接后继节点,通常称这类链表为单向链表(或单链表). 虽然使用单链表能 100% 解决逻辑关系为 & ...

  10. Linq中带有迭代索引的Select扩展方法,为啥知道的人不多呢?

    一:背景 昨天在看C#函数式编程这本书的时候,有一处让我干着急,需求是这样: 给多行文字加上数字列表项. 针对这个需求你会如何快捷高效的给每个项目加上数字编号呢? 我看书中是这样实现的,如下代码 pu ...