阅读opencv计算机视觉编程一(像素操作)

1 opencv像素
对灰度图像(黑白图像)而言,像素是8 位无符号数(数据类型为unsigned char),0 表示黑色,255 表示白色
浮点
double
8U 类型的 RGB 彩色图像 (0-255)

2椒盐噪声是一个专门的噪声类型,它随机选择一些像素,把
它们的颜色替换成白色或黑色。如果通信时出错,部分像素的值在传输时丢失,就会产生这种噪
声。这里只是随机选择一些像素,把它们设置为白色
单通道和三通道 Mat生成

void salt(cv::Mat image, int n) {
std::default_random_engine generator;
std::uniform_int_distribution<int>
randomRow(0, image.rows - 1);
std::uniform_int_distribution<int>
randomCol(0, image.cols - 1);
int i,j;
for (int k=0; k<n; k++) {
// 随机生成图形位置
i= randomCol(generator);
j= randomRow(generator);
if (image.type() == CV_8UC1) { // 灰度图像
// 单通道8 位图像
image.at<uchar>(j,i)= 255;
} else if (image.type() == CV_8UC3) { // 彩色图像
// 3 通道图像
image.at<cv::Vec3b>(j,i)[0]= 255;
image.at<cv::Vec3b>(j,i)[1]= 255;
image.at<cv::Vec3b>(j,i)[2]= 255;
}
}
}

cv::Mat 的at(int y,int x)方法可以访问元素,其中x 是
列号,y 是行号。在编译时必须明确方法返回值的类型,因为cv::Mat 可以接受任何类型的元
素,所以程序员需要指定返回值的预期类型。正因为如此,at 方法被实现成一个模板方法。在调
用at 方法时,你必须指定图像元素的类型,例如:
image.at(j,i)= 255;
有一点需要特别注意,程序员必须保证指定的类型与矩阵内的类型是一致的。at 方法不会进
行任何类型转换。

三通道的访问

image.at<cv::Vec3b>(j,i)[channel]= value;
image.at<cv::Vec3b>(j, i) = cv::Vec3b(255, 255, 255);

还有类似的向量类型用来表示二元素向量和四元素向量(cv::Vec2b 和cv::Vec4b)。此
外还有针对其他元素类型的向量。例如,表示二元素浮点数类型的向量就是把类型名称的最后一个字母换成f,即cv::Vec2f。对于短整型,最后的字母换成s;对于整型,最后的字母换成i;
对于双精度浮点数向量,最后的字母换成d。所有这些类型都用cv::Vec<T,N>模板类定义,其
中T 是类型,N 是向量元素的数量。

opencv采用值传递:是因为它们在复制图像时仍共享了同一块图像数据,因此
在需要修改图像内容时,图像参数没必要采用引用传递的方式。顺便说一下,编译器做代码优化
时,用值传递参数的方法通常比较容易实现。
如果知道矩阵的类型,可以使用cv::Mat_

减色函数

void colorReduce(cv::Mat image, int div=64) {
int nl= image.rows; // 行数
// 每行的元素数量
int nc= image.cols * image.channels();
for (int j=0; j<nl; j++) {
// 取得行j 的地址
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
// 处理每个像素 ---------------------
data[i]= data[i]/div*div + div/2;
// 像素处理结束 ----------------
} // 一行结束
}
}

可以用下面的代码片段测试这个函数:
// 读取图像
image= cv::imread("boldt.jpg");
// 处理图像
colorReduce(image,64);
// 显示图像
cv::namedWindow("Image");
cv::imshow("Image",image);

通过上面的代码可以看出所有的像素都是按照一定的顺序排序的,每一行的元素存放的是所在的行的元素x3个数,如果是三通道
每一行中像素值的个数:
int nc= image.cols * image.channels();
uchar* data= image.ptr(j);
处理语句中采用另一种等价的做法,即利用指针运算从一列移到下一
列。因此可以使用下面的代码:
*data++= data/divdiv + div2;

减色功能的实现是利用了整数除法的特性,即取不超过又最接近结果的整数:data[i]= (data[i]/div)*div + div/2;
减色计算也可以使用取模运算符,它可以直接得到div 的倍数,代码如下所示:data[i]= data[i] – data[i]%div + div/2;
使用位运算符。如果把减色因子限定为2 的指数,即div=pow(2,n),那么把像素值的前n 位掩码后就能得到最接近的div 的倍数。可以用简单的位移操作获得掩码,代码
如下所示:
用来截取像素值的掩码:uchar mask= 0xFF<<n;
可用下面的代码实现减色运算:*data &= mask; // 掩码 *data++ += div>>1; // 加上div/2
一般来说,使用位运算的代码运行效率很高,因此在效率为重时,位运算是不二之选

新的大小和像素类型重新分配矩阵,就要调用create方法

cv::Mat result;
result.create(image.rows,image.cols,image.type());
for (int j=0; j<nl; j++) {
// 获得第j 行的输入和输出的地址
const uchar* data_in= image.ptr<uchar>(j);
uchar* data_out= result.ptr<uchar>(j);
for (int i=0; i<nc*nchannels; i++) {
// 处理每个像素 ---------------------
data_out[i]= data_in[i]/div*div + div/2;
// 像素处理结束 ----------------
} // 一行结束
}

,为了提高性能,可以在图像的每行末尾用额外的像素进行填充。有趣的是,在
去掉填充后,图像仍可被看作一个包含W×H 像素的长一维数组。用cv::Mat 的isContinuous
方法可轻松判断图像有没有被填充。如果图像中没有填充像素,它就返回true。我们还能这样
测试矩阵的连续性:
// 检查行的长度(字节数)与“列的个数×单个像素”的字节数是否相等
image.step == image.cols*image.elemSize();

掩码:
mask就是位图,来选择哪个像素允许拷贝,哪个像素不允许拷贝。如果mask像素的值是非0的,我就拷贝它,否则不拷贝。因为我们上面得到的mask中,感兴趣的区域是白色的,
表明感兴趣区域的像素都是非0,而非感兴趣区域都是黑色,表明那些区域的像素都是0。
image.copyTo(img2, mask);
[1,0,1
0,1,0
0,0,0]
将imge中感兴趣的区域mask拷出来,得到感兴趣区域image2

image.copyTo(img3); img3.setTo(0, mask);

将image3中部分填充掩码

如果要从图像的起点开始循环;uchar data= image.data;
利用有效宽度来移动行指针,可以从一行移到下一行,代码如下所示:
data+= image.step; // 下一行
用step 属性可得到一行的总字节数(包括填充像素)。通常可以用下面的方法得到第j 行、
第i 列的像素的地址:
// (j,i)像素的地址,即&image.at(j,i)
data= image.data+j
image.step+i*image.elemSize();
尽管这种处理方法在上述例子中能起作用,但是并不推荐使用。

时间判断:
OpenCV 有一个非常实用的函数可以用来测算函数或代码段的运行时间,它就是cv::get
TickCount(),该函数会返回从最近一次计算机开机到当前的时钟周期数。在代码开始和结
束时记录这个时钟周期数,就可以计算代码的运行时间。若想得到以秒为单位的代码运行时间,
可使用另一个方法cv::getTickFrequency(),它返回每秒的时钟周期数,这里假定CPU
的频率是固定的(对于较新的CPU,频率并不一定是固定的)。为了获得某个函数(或代码段)
的运行时间,通常需使用这样的程序模板:
const int64 start = cv::getTickCount();
colorReduce(image); // 调用函数
// 经过的时间(单位:秒)
double duration = (cv::getTickCount()-start)/
cv::getTickFrequency();

colorReduce 函数有几种实现方式
在这里插入图片描述

对于可以预先计算的数值,要避免在循环中做重复计算,继而浪费时间。例如,这样写减色
函数是很不明智的:

for (int i=0; i<image.cols * image.channels(); i++) {
*data &= mask;
*data++ += div/2;

上面的代码需要反复计算每行的像素数量和div/2 的结果。改进后的代码为:

int nc= image.cols * image.channels();
uchar div2= div>>1;
for (int i=0; i<nc; i++) {
*(data+i) &= mask;
*(data+i) += div2;

一般来说,需要重复计算的代码会比优化后的代码慢10 倍。但是要注意,有些编译器能够
对此类循环进行优化,仍会生成高效的代码。

速度慢:

for (int j=0; j<nl; j++) {
for (int i=0; i<nc; i++) {
image.at<cv::Vec3b>(j,i)[0]=
image.at<cv::Vec3b>(j,i)[0]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[1]=
image.at<cv::Vec3b>(j,i)[1]/div*div + div/2;
image.at<cv::Vec3b>(j,i)[2]=
image.at<cv::Vec3b>(j,i)[2]/div*div + div/2;
} // 一行结束
}

这种方法的运行速度较慢,分别为0.925 ms、0.580 ms 和1.128 ms。该方法应该在需要随机
访问像素的时候使用,绝不要在扫描图像时使用。
即使处理的元素总数相同,使用较短的循环和多条语句通常也要比使用较长的循环和单条语
句的运行效率高。与之类似,如果你要对一个像素执行N 个不同的计算过程,那就在单个循环中
执行全部计算,而不是写N 个连续的循环,每个循环执行一个计算。
我们还做过连续性测试,针对连续图像生成一个循环,而不是对行和列运行常规的二重循环,
使运行速度平均提高了10%。通常情况下,这种策略是非常好的,因为它会使速度明显提高。

提高效率的方法;多线程
OpenMP

进行锐化,锐化的核值
在这里插入图片描述

扫描图像并访问相邻像素:
我们将使用一个锐化图像的处理函数。它基于拉普拉斯算子(将在第6
章讨论)。在图像处理领域有一个众所周知的结论:如果从图像中减去拉普拉斯算子部分,图像
的边缘就会放大,因而图像会变得更加尖锐。
可以用以下方法计算锐化的数值:
sharpened_pixel= 5*current-left-right-up-down;
这里的left 是与当前像素相邻的左侧像素,up 是上一行的相邻像素,以此类推。
这里不能使用就地处理,用户必须提供一个输出图像。图像扫描中使用了三个指针,一个表
示当前行、一个表示上面的行、一个表示下面的行。另外,因为在计算每一个像素时都需要访问
与它相邻的像素,所以有些像素的值是无法计算的,比如第一行、最后一行和第一列、最后一列
的像素。这个循环可以这样写:

void sharpen(const cv::Mat &image, cv::Mat &result) {
// 判断是否需要分配图像数据。如果需要,就分配
result.create(image.size(), image.type());
int nchannels= image.channels(); // 获得通道数

// 处理所有行(除了第一行和最后一行)

for (int j= 1; j<image.rows-1; j++) {
const uchar* previous= image.ptr<const uchar>(j-1); // 上一行
const uchar* current= image.ptr<const uchar>(j); // 当前行
const uchar* next= image.ptr<const uchar>(j+1); // 下一行
uchar* output= result.ptr<uchar>(j); // 输出行
for (int i=nchannels; i<(image.cols-1)*nchannels; i++) {
// 应用锐化算子
*output++= cv::saturate_cast<uchar>(
5*current[i]-current[i-nchannels]-
current[i+nchannels]-previous[i]-next[i]);
}
}
// 把未处理的像素设为0
result.row(0).setTo(cv::Scalar(0));
result.row(result.rows-1).setTo(cv::Scalar(0));
result.col(0).setTo(cv::Scalar(0));
result.col(result.cols-1).setTo(cv::Scalar(0));
}

调用了cv::saturate_cast 模板函数,并传入运算结果。
这是因为计算像素的数学表达式的结果经常超出允许的范围(即小于0 或大于255)

另外的锐化函数:
void sharpen2D(const cv::Mat &image, cv::Mat &result) {
// 构造内核(所有入口都初始化为0)
cv::Mat kernel(3,3,CV_32F,cv::Scalar(0));
// 对内核赋值
kernel.at(1,1)= 5.0;
kernel.at(0,1)= -1.0;
kernel.at(2,1)= -1.0;
kernel.at(1,0)= -1.0;
kernel.at(1,2)= -1.0;
// 对图像滤波
cv::filter2D(image,result,image.depth(),kernel);
}
这种实现方式得到的结果与前面的完全相同(执行效率也相同)。如果处理的是彩色图像,
三个通道可以应用同一个内核。注意,使用大内核的filter2D 函数是特别有利的,因为这时它
使用了更高效的算法。

这里要把两幅图像相加。这种方法可以用于创建特效图或覆盖图像中的信息。我们可以使用
cv::add 函数来实现相加功能,但因为这次是想得到加权和,因此使用更精确的cv::addWeighted
函数:cv::addWeighted(image1,0.7,image2,0.9,0.,result);
加减乘除函数:
cv::add,cv::subtract、cv::absdiff、cv::multiply 和cv::divide
位运算符(对像素的二进制数值进行按位运算)cv::bitwise_and、cv::bitwise_or、
cv::bitwise_xor 和cv::bitwise_not
cv::min 和cv::max 运算符也非常实用,它们能
找到每个元素中最大或最小的像素值。
在所有场合都要使用cv::saturate_cast 函数,以确保结果在预定
的像素值范围之内(避免上溢或下溢)。
这些图像必定有相同的大小和类型(如果与输入图像的大小不匹配,输出图像会重新分配)。
由于运算是逐个元素进行的,因此可以把其中的一个输入图像用作输出图像。
还有运算符使用单个输入图像,它们是cv::sqrt、cv::pow、cv::abs、cv::cuberoot、
cv::exp 和cv::log

重载图像运算符
OpenCV 的大多数运算函数都有对应的重载运算符,因此调用cv::addWeighted 的语句也
可以写成:
result= 0.7image1+0.9image2;
这种代码更加紧凑也更容易阅读。这两种计算加权和的方法是等效的。特别指出,这两种方
法都会调用cv::saturate_cast 函数。
大部分C++运算符都已被重载,其中包括位运算符&、 |、 ^、~和函数min、max、abs。
比较运算符<、 <=、 ==、 !=、>和>=也已被重载,它们返回一个8 位的二值图像。此外还有矩
阵乘法m1*m2(其中m1 和m2 都是cv::Mat 实例)、矩阵求逆m1.inv()、变位m1.t()、行列
式m1.determinant()、求范数v1.norm()、叉乘v1.cross(v2)、点乘v1.dot(v2),等等。
在理解这点后,你就会使用相应的组合赋值符了(例如+=运算符)

image=(image&cv::Scalar(mask,mask,mask))
+cv::Scalar(div/2,div/2,div/2);
由于被操作的是彩色图像,因此使用了cv::Scalar。使用图像运算符可以简化代码、提高
开发效率,因此在大多数场合都应考虑采用。

分割图像通道:
// 创建三幅图像的向量
std::vectorcv::Mat planes;
// 将一个三通道图像分割为三个单通道图像
cv::split(image1,planes);
// 加到蓝色通道上
planes[0]+= image2;
// 将三个单通道图像合并为一个三通道图像
cv::merge(planes,result);
这里的cv::merge 函数执行反向操作,即用三个单通道图像创建一个彩色图像。

图像重映射:
通过
移动像素修改图像的外观。这个过程不会修改像素值,而是把每个像素的位置重新映射到新的位
置。这可用来创建图像特效,或者修正因镜片等原因导致的图像扭曲。
// 重映射图像,创建波浪形效果

void wave(const cv::Mat &image, cv::Mat &result) {
// 映射参数
cv::Mat srcX(image.rows,image.cols,CV_32F);
cv::Mat srcY(image.rows,image.cols,CV_32F);
// 创建映射参数
for (int i=0; i<image.rows; i++) {
for (int j=0; j<image.cols; j++) {
// (i,j)像素的新位置
srcX.at<float>(i,j)= j; // 保持在同一列
// 原来在第i 行的像素,现在根据一个正弦曲线移动
srcY.at<float>(i,j)= i+5*sin(j/10.0);
}
}
// 应用映射参数
cv::remap(image, // 源图像
result, // 目标图像
srcX, // x 映射
srcY, // y 映射
cv::INTER_LINEAR); // 填补方法
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值