用指针扫描图像 - sumpig/OpenCV GitHub Wiki
本节和下一节将展示几种实现高效扫描循环的方法,本节将使用 指针运算。
我们来做一个简单的任务:减少图像中颜色的数量。
基本的减色算法很简单。假设 N 是减色因子,将图像中每个像素的值除以 N(这里假定使用整数除法,不保留余数)。然后将结果乘以 N,得到 N 的倍数,并且刚好不超过原始像素值。加上 N / 2,就得到相邻的 N 倍数之间的中间值。对所有 8 位通道值重复这个过程,就会得到 (256 / N) × (256 / N) × (256 / N)种可能的颜色值。
创建一个二重循环遍历所有像素值,代码如下所示:
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;
}
}
}
在彩色图像中,图像数据缓冲区的前 3 字节表示左上角像素的三个通道的值,接下来的 3 字节表示第 1 行的第 2 个像素,以此类推(注意OpenCV 默认的通道次序为BGR)。
出于性能上的考虑,我们会用几个额外的像素来填补行的长度。这些额外的像素既不会显示也不被保存,它们的额外数据会被忽略。
OpenCV 把经过填充的行的长度指定为有效宽度。如果图像没有用额外的像素填充,那么有效宽度就等于实际的图像宽度。用 step 数据属性可得到单位是字节的有效宽度。
通过 elemSize 方法(例如一个三通道短整型的矩阵CV_16SC3,elemSize 会返回6)可以获得像素的大小,通过 channels 方法(灰度图像为1,彩色图像为3)获得图像中通道的数量,最后用 total 方法返回矩阵中的像素(即矩阵的条目)总数。
为了简化指针运算的计算过程,cv::Mat 类提供了 ptr 方法,可以直接访问图像中一行的起始地址。
减色计算也可以使用取模运算符,它可以直接得到div 的倍数,代码如下所示:
data[i] = data[i] – data[i]%div + div/2;
还可以使用位运算符,使用位运算的代码运行效率很高。
如果把减色因子限定为 2 的指数,即 div=pow(2,n),那么把像素值的前 n 位掩码后就能得到最接近的 div 的倍数。可以用简单的位移操作获得掩码,代码如下所示:
// 用来截取像素值的掩码
uchar mask = 0xFF << n; // 如div=16,则 mask = 0xF0
可用下面的代码实现减色运算:
*data &= mask; // 掩码
*data += div>>1; // 加上div/2
// 这里的+也可以改用“按位或”运算符
有的程序不希望对原始图像进行修改,这时就必须在调用函数前备份图像。
对图像进行深复制最简单的方法是使用 clone() 方法,如下面的代码所示:
// 读入图像
image= cv::imread("boldt.jpg");
// 复制图像
cv::Mat imageClone= image.clone();
这里的关键是先检查输出图像,验证它是否分配了一定大小的数据缓冲区,以及像素类型与输入图像是否相符。
当你用新的大小和像素类型重新分配矩阵时,就要调用 create 方法。。如果矩阵已有的大小和类型刚好与指定的大小和类型相同,这个方法就不会执行任何操作,也不会修改实例,而只是直接返回。
因此,函数中首先要调用create 方法,构建一个大小和类型都与输入图像相同的矩阵(如果必要):
result.create(image.rows, image.cols, image.type());
分配的内存块的大小表示为 total()*elemSize()。
扫描过程中使用两个指针:
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;
}
}
用 cv::Mat 的 isContinuous 方法可轻松判断图像有没有被填充。如果图像中没有填充像素,它就返回 true。
我们还能这样测试矩阵的连续性:
// 检查行的长度(字节数)与“列的个数×单个像素”的字节数是否相等
image.step == image.cols*image.elemSize();
在一些特殊的处理算法中,你可以充分利用图像的连续性,在单个(更长)循环中处理图像。处理函数就可以改为:
void colorReduce(cv::Mat image, int div=64) {
int nl= image.rows;
int nc= image.cols * image.channels();
if (image.isContinuous()) {
// 没有填充的像素
nc= nc*nl;
nl= 1; // 它现在成了一个一维数组
}
int n= static_cast<int>(log(static_cast<double>(div))/log(2.0) + 0.5);
uchar mask= 0xFF<<n;
uchar div2 = div >> 1;
// 对于连续图像,这个循环只执行一次
for (int j=0; j<nl; j++) {
uchar* data= image.ptr<uchar>(j);
for (int i=0; i<nc; i++) {
*data &= mask;
*data++ += div2;
}
}
}
如果连续性测试结果表明图像中没有填充像素,我们就把宽度设为 1,高度设为 W×H,从而去除外层的循环。注意,这里还需要用 reshape 方法。本例中需要这样写:
if (image.isContinuous()){
// 没有填充像素
image.reshape(
1, // 新的通道数
1); // 新的行数
}
int nl = image.rows; // 行数
int nc = image.cols * image.channels();
如果是用reshape 方法修改矩阵的维数,就不需要复制内存或重新分配内存了。第一个参数是新的通道数,第二个参数是新的行数。列数会进行相应的修改。
在 cv::Mat 类中,图像数据是存放在无符号字符型的内存块中的。其中 data 属性表示内存块第一个元素的地址,它会返回一个无符号字符型的指针。
如果要从图像的起点开始循环,你可以用如下代码:
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();
尽管这种处理方法在上述例子中能起作用,但是并不推荐使用。