数字图像处理-实验5

实验5:图像滤波

实验5.1 快速均值滤波

基于积分图实现图像的快速均值滤波

- 滤波窗口大小通过参数来指定:

  void meanFilter(const Mat &input, Mat &output, int windowSize);

- 采用积分图进行加速,实现与滤波窗口大小无关的效率;

与OpenCV的boxFilter函数比较计算速度,并分析导致速度差异的原因。

 

首先写出的代码如下:

cv::Mat integral_image(const cv::Mat &src){
    cv::Mat res=cv::Mat::zeros(src.rows+1,src.cols+1,src.type());
    for(int i=1;i<=src.rows;++i){
        for(int j=1;j<=src.cols;++j){
            auto val=src.at<cv::Vec3b>(i-1,j-1);
            res.at<cv::Vec3b>(i,j)=res.at<cv::Vec3b>(i-1,j)+res.at<cv::Vec3b>(i,j-1)-res.at<cv::Vec3b>(i-1,j-1)+val;

        }
    }
    return res;
}
cv::Vec3b get_sum(const cv::Mat &sum,int x1,int y1,int x2,int y2){
    if(x1>x2||y1>y2) std::swap(x1,x2),std::swap(y1,y2);
    ++x1; ++x2; ++y1; ++y2;
    return sum.at<cv::Vec3b>(x2,y2)
           -sum.at<cv::Vec3b>(x1-1,y2)
           -sum.at<cv::Vec3b>(x2,y1-1)
           +sum.at<cv::Vec3b>(x1-1,y1-1);
}
void meanFilter(const cv::Mat &src,cv::Mat &dst,int window_size){
    cv::Mat integral=integral_image(src);
    dst=cv::Mat::zeros(src.rows,src.cols,src.type());
    for(int i=0;i<src.rows;++i){
        for(int j=0;j<src.cols;++j){
            int mid=window_size>>1;
            int x1=std::max(i-mid,0),y1=std::max(j-mid,0);
            int x2=std::min(i+mid,src.rows-1),y2=std::min(j+mid,src.cols-1);
            auto sum=get_sum(integral,x1,y1,x2,y2);
            int pixels=(x2-x1+1)*(y2-y1+1);
            dst.at<cv::Vec3b>(i,j)=sum/pixels;
        }
    }
}

int main(int argc,char *argv[]){
    cv::Mat image=cv::imread("bg.jpg");
    if(image.empty()){
        std::cout<<"Open File Fail!"; return 0;
    }
    cv::namedWindow("test");
    cv::createTrackbar("size","test",nullptr,std::min(image.rows,image.cols));
    while(1){
        static cv::Mat res;
        int window_size=cv::getTrackbarPos("size","test");
        meanFilter(image,res,window_size);
        cv::imshow("test",res);
        cv::waitKey(27);
    }
    cv::destroyAllWindows();
    return 0;
}

但是程序的运行结果明显不对:

排查原因发现我在积分图中使用了cv::Vec3b类型保存了像素的和,这显然会导致像素值的溢出,结果自然也不对了。

修改后的代码使用了下面的结构体来保存积分图,像素值使用std::tuple<int,int,int>存储,从而避免了溢出的问题。

struct IntegralImage{
    std::vector<std::tuple<int,int,int>> v;
    int width,height;
    IntegralImage(int width,int height){
        this->width=width; this->height=height;
        v.resize((width+1)*(height+1),{0,0,0});
    }
    std::tuple<int,int,int>& at(int i,int j){
        return v[j*width+i];
    }
};

除此之外,程序还重载了下面的运算符以方便计算:

std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const cv::Vec3b &b){
    auto [a1,a2,a3]=a;
    int b1=b[0],b2=b[1],b3=b[2];
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator-(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1-b1,a2-b2,a3-b3);
}
std::tuple<int,int,int> operator/(const std::tuple<int,int,int> &a,int d){
    auto [a1,a2,a3]=a;
    return std::make_tuple(a1/d,a2/d,a3/d);
}

其他部分的代码则大同小异,完整的代码如下:

查看代码
 struct IntegralImage{
    std::vector<std::tuple<int,int,int>> v;
    int width,height;
    IntegralImage(int width,int height){
        this->width=width; this->height=height;
        v.resize((width+1)*(height+1),{0,0,0});
    }
    std::tuple<int,int,int>& at(int i,int j){
        return v[j*width+i];
    }
};
std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const cv::Vec3b &b){
    auto [a1,a2,a3]=a;
    int b1=b[0],b2=b[1],b3=b[2];
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator-(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1-b1,a2-b2,a3-b3);
}
std::tuple<int,int,int> operator/(const std::tuple<int,int,int> &a,int d){
    auto [a1,a2,a3]=a;
    return std::make_tuple(a1/d,a2/d,a3/d);
}
void integral_image(const cv::Mat &src,IntegralImage &res){
    for(int i=1;i<=src.rows;++i){
        for(int j=1;j<=src.cols;++j){
            auto val=src.at<cv::Vec3b>(i-1,j-1);
            res.at(i,j)=res.at(i-1,j)+res.at(i,j-1)-res.at(i-1,j-1)+val;
        }
    }
}
std::tuple<int,int,int> get_sum(IntegralImage &sum,int x1,int y1,int x2,int y2){
    if(x1>x2||y1>y2) std::swap(x1,x2),std::swap(y1,y2);
    ++x1; ++x2; ++y1; ++y2;
    return sum.at(x2,y2)-sum.at(x1-1,y2)-sum.at(x2,y1-1)+sum.at(x1-1,y1-1);
}
void meanFilter(const cv::Mat &src,cv::Mat &dst,int window_size){
    IntegralImage integral(src.rows,src.cols);
    integral_image(src,integral);
    dst=cv::Mat::zeros(src.rows,src.cols,src.type());
    for(int i=0;i<src.rows;++i){
        for(int j=0;j<src.cols;++j){
            int mid=window_size>>1;
            int x1=std::max(i-mid,0),y1=std::max(j-mid,0);
            int x2=std::min(i+mid,src.rows-1),y2=std::min(j+mid,src.cols-1);
            auto sum=get_sum(integral,x1,y1,x2,y2);
            int pixels=(x2-x1+1)*(y2-y1+1);
            auto result=sum/pixels;
            dst.at<cv::Vec3b>(i,j)={(uchar)std::get<0>(result),(uchar)std::get<1>(result),(uchar)std::get<2>(result)};
        }
    }
}
int main(int argc,char *argv[]){
    cv::Mat image=cv::imread("bg.jpg");
    if(image.empty()){
        std::cout<<"Open File Fail!"; return 0;
    }
    cv::namedWindow("test");
    cv::createTrackbar("size","test",nullptr,std::min(image.rows,image.cols));
    while(1){
        static cv::Mat res;
        int window_size=cv::getTrackbarPos("size","test");
        meanFilter(image,res,window_size);
        cv::imshow("test",res);
        cv::waitKey(27);
    }
    cv::destroyAllWindows();
    return 0;
}

这次图像能够正常展示了:

然而此时又出现了新的问题,当修改滤波窗口的大小时,图像的上半部分会出现彩色的区域,并且随着窗口大小的增大彩色区域的部分也会变大。

通过打印中间变量,发现彩色区域部分的像素值变成了负数,最终经过排查后找到了问题所在:定义积分图的时候为了防止越界,将积分图的行、列相比原图都增大了1,然而在根据行号、列号获取积分图中的元素时使用的还是原图的行列数,因此将类IntegralImage中的.at()函数从:

std::tuple<int,int,int>& at(int i,int j){
    return v[j*width+i];
}

改为:return v[j*(width+1)+i];就正确了。

完整代码如下:

查看代码
 struct IntegralImage{
    std::vector<std::tuple<int,int,int>> v;
    int width,height;
    IntegralImage(int width,int height){
        this->width=width; this->height=height;
        v.resize((width+1)*(height+1),{0,0,0});
    }
    std::tuple<int,int,int>& at(int i,int j){
        return v[j*(width+1)+i];
    }
};
std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator+(const std::tuple<int,int,int> &a,const cv::Vec3b &b){
    auto [a1,a2,a3]=a;
    int b1=b[0],b2=b[1],b3=b[2];
    return std::make_tuple(a1+b1,a2+b2,a3+b3);
}
std::tuple<int,int,int> operator-(const std::tuple<int,int,int> &a,const std::tuple<int,int,int> &b){
    auto [a1,a2,a3]=a; auto [b1,b2,b3]=b;
    return std::make_tuple(a1-b1,a2-b2,a3-b3);
}
std::tuple<int,int,int> operator/(const std::tuple<int,int,int> &a,int d){
    auto [a1,a2,a3]=a;
    return std::make_tuple(a1/d,a2/d,a3/d);
}
void integral_image(const cv::Mat &src,IntegralImage &res){
    for(int i=1;i<=src.rows;++i){
        for(int j=1;j<=src.cols;++j){
            auto val=src.at<cv::Vec3b>(i-1,j-1);
            res.at(i,j)=res.at(i-1,j)+res.at(i,j-1)-res.at(i-1,j-1)+val;
        }
    }
}
std::tuple<int,int,int> get_sum(IntegralImage &sum,int x1,int y1,int x2,int y2){
    if(x1>x2||y1>y2) std::swap(x1,x2),std::swap(y1,y2);
    ++x1; ++x2; ++y1; ++y2;
    return sum.at(x2,y2)-sum.at(x1-1,y2)-sum.at(x2,y1-1)+sum.at(x1-1,y1-1);
}
void meanFilter(const cv::Mat &src,cv::Mat &dst,int window_size){
    IntegralImage integral(src.rows,src.cols);
    integral_image(src,integral);
    dst=cv::Mat::zeros(src.rows,src.cols,src.type());
    for(int i=0;i<src.rows;++i){
        for(int j=0;j<src.cols;++j){
            int mid=window_size>>1;
            int x1=std::max(i-mid,0),y1=std::max(j-mid,0);
            int x2=std::min(i+mid,src.rows-1),y2=std::min(j+mid,src.cols-1);
            auto sum=get_sum(integral,x1,y1,x2,y2);
            int pixels=(x2-x1+1)*(y2-y1+1); //像素数量
            auto result=sum/pixels;
            dst.at<cv::Vec3b>(i,j)={(uchar)std::get<0>(result),(uchar)std::get<1>(result),(uchar)std::get<2>(result)};
        }
    }
}
int main(int argc,char *argv[]){
    cv::Mat image=cv::imread("bg.jpg");
    if(image.empty()){
        std::cout<<"Open File Fail!"; return 0;
    }
    cv::namedWindow("test");
    cv::createTrackbar("size","test",nullptr,std::min(image.rows,image.cols));
    while(1){
        static cv::Mat res;
        int window_size=cv::getTrackbarPos("size","test");
        meanFilter(image,res,window_size);
        cv::imshow("test",res);
        if(cv::waitKey(50)==27) break;
    }
    cv::destroyAllWindows();
    return 0;
}

效率比较

使用下面的代码,将滤波窗口大小设置为50,执行1000次,比较 meanFilter() 和 boxFilter() 两个函数的总耗时:

// 定义滤波窗口大小
int window_size=50;
int times=1000;
// 1. 基于积分图的均值滤波
auto startIntegral=QDateTime::currentMSecsSinceEpoch();
for(int i=1;i<=times;++i){
    Mat outputIntegral;
    meanFilter(img,outputIntegral,window_size);
}
auto durationIntegral=QDateTime::currentMSecsSinceEpoch()-startIntegral;
qDebug()<<"基于积分图的均值滤波 总耗时: "<<durationIntegral<<"ms";
// 2. OpenCV自带的boxFilter
auto startBoxFilter=QDateTime::currentMSecsSinceEpoch();
for(int i=1;i<=times;++i){
    Mat outputBoxFilter;
    boxFilter(img,outputBoxFilter,-1,Size(window_size, window_size));
}
auto durationBoxFilter=QDateTime::currentMSecsSinceEpoch()-startBoxFilter;
qDebug()<<"OpenCV boxFilter 总耗时: "<<durationBoxFilter<<"ms";

执行结果如下:

可以看出 OpenCV 的 boxFilter() 函数的效率要远高于我自己写的函数,通过查询资料得知,OpenCV 的 boxFilter() 函数是高度优化的,使用了多线程、SIMD 指令集和硬件加速等技术,充分利用了 CPU 资源,提高了执行的效率。

posted @   Nartsam  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示