前言
最近因为一些特殊原因,需要写几个图像处理相关的代码。计划手动实现双边滤波,目的是熟悉opencv对图像的存储方式,以及相关的库函数的使用。
在不考虑效率,使用Mat提供的成员函数以及vector等容器实现功能后,100W的像素跑出了7分钟,也着实让我吓了一跳。
最终,进过优化,100W像素跑到3-4秒之间。这个效率,说实话,并不是特别满意。理想状态应该秒开,毫无延迟的感觉。要达到毫无延迟,要么使用快速滤波算法,要么进一步优化代码。而我,就是想将代码写到极致。
优化之前的源代码
双边滤波器原理请点这里
inline void getColorWeightBuffer(double* CWBuffer, double sigma) {
double temp_ = -0.5 / (sigma * sigma);
for (int i = 0; i < 768; i++) {
CWBuffer[i] = exp(i * i * temp_);
}
}
inline void getSpaceWeightBuffer(Mat &mask, double sigma, int ksize) {
double temp_ = -0.5 / (sigma * sigma);
int radius = ksize / 2;
int distance = 0;
for (int i = -radius; i <= radius; i++) {
double* rows = mask.ptr<double>(i+radius);
for (int j = -radius; j <= radius; j++) {
distance = i * i + j * j;
rows[j + radius] = exp(distance * temp_);
}
}
}
void myBilateralFilters(Mat& src, Mat& dst,int ksize , double space_sigma,double color_sigma) {
CV_DbgAssert(src.channels() == 1 || src.channels() == 3);
unique_ptr<double> CWBuffer(new double[256]);
Mat mask(ksize, ksize, CV_64F);
getColorWeightBuffer(CWBuffer.get(), color_sigma);
getSpaceWeightBuffer(mask, space_sigma, ksize);
int radius = ksize / 2;
dst.create(src.size(), src.type());
for (int i = 0; i < src.rows ; i++) { // #1
uchar* ptr = src.ptr<uchar>(i); // #2
for (int j = 0; j < src.cols; j++) { // #3
double sum[3]{ 0 };
double w[3]{ 0 };
double graydiff[3]{ 0 };
for (int k = 0; k < ksize; k++) { // #3
for (int l = 0; l < ksize; l++) {
if (i - radius + k < 0 || j - radius + l < 0 || i -radius + k > src.rows-1 || j - radius + l > src.cols-1) continue; // #4
if (src.channels() == 1) { //#5
w[0] = CWBuffer.get()[abs(ptr[j] - src.at<uchar(i-radius+k,j-radius+k))] * mask.at<double>(k,l);
sum[0] += w[0];
graydiff[0] += src.at<uchar>(i - radius + k, j -radius + k) * w[0];
}
else if (src.channels() == 3) {
w[0] = CWBuffer.get()[abs(ptr[j*src.channels()] - srcat<Vec3b>(i-radius+k,j-radius+l)[0])] * maskat<double>(k,l); // #6
w[1] = CWBuffer.get()[abs(ptr[j*src.channels() + 1] -src.at<Vec3b>(i-radius+k,j-radius+l)[1])] * maskat<double>(k,l);
w[2] = CWBuffer.get()[abs(ptr[j*src.channels() + 2] -src.at<Vec3b>(i-radius+k,j-radius+l)[2])] * maskat<double>(k,l);
sum[0] += w[0];
sum[1] += w[1];
sum[2] += w[2];
graydiff[0] += src.at<Vec3b>(i - radius + k, j -radius + l)[0] * w[0]; // #7
graydiff[1] += src.at<Vec3b>(i - radius + k, j -radius + l)[1] * w[1];
graydiff[2] += src.at<Vec3b>(i - radius + k, j -radius + l)[2] * w[2];
}
}
}
if (dst.channels() == 1) {
dst.at<uchar>(i, j) = graydiff[0] / sum[0];
}
else if (dst.channels() == 3) {
dst.at<Vec3b>(i, j)[0] = graydiff[0] / sum[0]; //#8
dst.at<Vec3b>(i, j)[1] = graydiff[1] / sum[1];
dst.at<Vec3b>(i, j)[2] = graydiff[2] / sum[2];
}
}
}
}
这里实现的双边滤波和原来的还是有点不一样的地方。原版双边滤波器算像素权值,是将三个通道的差加在一起作为计算像素权重的参数,这边是将三个通道单独计算的。从效果上来说,原版的包边性更好。当然,后边优化的时候,为了和原版保持一致,改回和原版一样了。
优化原理
-
调用函数的时间开销,远远大于使用局部变量
-
通过指针修改数据,效率远比通过Mat对象提供的成员函数快
-
减少大量的重复计算。
-
慎用STL模板(效率真的不咋地)
具体方案
-
获取指向图像数据的指针,通过移动指针更改数据。
Mat存数数据的结构看这篇博客,图文结合,非常好理解。
uchar *data = src.data; //访问单通道(i,j)得像素 data[i*cols+j]; //访问三通道图 b = data[((i*cols)+j)*channels] // channels = 3 g = data[((i*cols)+j)*channels + 1] // channels = 3 r = data[((i*cols)+j)*channels + 2] // channels = 3
-
将需要大量使用的参数,赋值给局部变量,避免过多的函数调用
int channels = src.channels(); //这里只是个例子 int rows = src.rows; int cols = src.cols;
这里不得不提一嘴,把循环体中的channels换了之后,效率有了质的飞跃。
优化后代码
void myBilateralFilters2(Mat& src, Mat& dst, int ksize, double space_sigma, double color_sigma) {
CV_DbgAssert(src.channels() == 1 || src.channels() == 3);
int channels = src.channels();
double* CWBuffer = new double[768];
getColorWeightBuffer(CWBuffer, color_sigma);
Mat mask(ksize, ksize, CV_64F);
getSpaceWeightBuffer(mask, space_sigma, ksize);
uchar* umask = mask.data;
int radius = ksize / 2;
dst.create(src.size(), src.type());
uchar* udst = dst.data;
uchar* usrc = src.data;
int rows = src.rows;
int cols = src.cols;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
double sum[3]{ 0 }; //归一化权重
double w[3]{ 0 }; // 权重
double graydiff[3]{ 0 };
int LT = i - radius;
int RT = j - radius;
int LB = i - radius - src.rows + 1;
int RB = j - radius - src.cols + 1;
for (int k = 0; k < ksize; k++) {
for (int l = 0; l < ksize; l++) {
if (LT + k < 0 || RT + l < 0 || LB > -k || RB > -l) continue;
if (channels == 1) {
uchar gray = usrc[(LT + k) * cols + RT + l];
w[0] = CWBuffer[abs(usrc[i*cols+j] - gray)] * umask[k * ksize + l];
sum[0] += w[0];
graydiff[0] += gray * w[0];
}
else if (channels== 3) {
int index = ((LT + k) * cols + (RT + l)) * 3;
int index_gray = (i * cols + j) * 3;
int index_umask = k * ksize + l;
int color_ = abs(usrc[index_gray] - usrc[index]) + abs(usrc[index_gray + 1] - usrc[index + 1]) + abs(usrc[index_gray + 2] - usrc[index + 2]);
w[0] = CWBuffer[color_] * umask[index_umask];
w[1] = CWBuffer[color_] * umask[index_umask];
w[2] = CWBuffer[color_] * umask[index_umask];
/*
w[0] = CWBuffer[abs(usrc[index_gray] - usrc[index])] * umask[index_umask];
w[1] = CWBuffer[abs(usrc[index_gray + 1] - usrc[index + 1])] * umask[index_umask];
w[2] = CWBuffer[abs(usrc[index_gray + 2] - usrc[index + 2])] * umask[index_umask];
*/
sum[0] += w[0];
sum[1] += w[1];
sum[2] += w[2];
graydiff[0] += usrc[index] * w[0];
graydiff[1] += usrc[index + 1] * w[1];
graydiff[2] += usrc[index + 2] * w[2];
}
}
}
if (channels == 1) {
udst[i * cols + j] = graydiff[0] / sum[0];
}
else if (channels == 3) {
udst[(i * dst.cols + j) * 3] = graydiff[0] / sum[0];
udst[(i * dst.cols + j) * 3 + 1] = graydiff[1] / sum[1];
udst[(i * dst.cols + j) * 3 + 2] = graydiff[2] / sum[2];
}
}
}
delete[] CWBuffer;
}
总结
上述优化,完全是根据个人编程经验改的。如果有更好的修改方案,希望各位大佬能够指导。