CV02-02:Laplace滤波

  机器视觉中图像滤波处理应该是很基础,很重要的,本主题梳理下Laplace滤波的原理与实现;


拉普拉斯算子

数学表示

二阶导数的求值算法

  • 假设计算图像中位置的二阶导数,表示图像位置的像素值。

二阶导数的离散表示

  • 提示:

    • 该公式的推导,见后面附录。

矩阵的哈达玛(Hadamard)积表示

  • 矩阵Hadamard积,使用表示:
    • \begin{aligned} H&= \begin{bmatrix} {0}&{1}&{0}\\{1}&{-4}&{1}\\{0}&{1}&{0}\\\end{bmatrix} \ast \begin{bmatrix} {f(x-1, y-1)}&{f(x, y-1)}&{f(x+1, y-1)}\\{f(x-1, y)}&{f(x,y)}&{f(x+1, y)}\\{f(x-1, y+1)}&{f(x, y+1)}&{f(x+1, y+1)}\\\end{bmatrix} \\ \\ &= \begin{bmatrix} {0}&{f(x, y-1)}&{0}\\{f(x-1, y)}&{-4f(x,y)}&{f(x+1, y)}\\{0}&{f(x, y+1)}&{0}\\\end{bmatrix} \\ \\ &= f(x+1,y) + f(x-1,y) + f(x,y+1) + f(x,y-1) - 4f(x,y) \end{aligned}

二阶导数的Hadamard积的和表示

  • 提示:

    • 到目前为止,完美的使用矩阵运算表示了离散图像的二阶导数表示。

附录一:离散二阶导数的推导

连续微分

  • 假设函数为
  1. 一阶导数表示:

  2. 一阶导数的极限定义:

  3. 二阶微分的定义

离散导数

  • 对离散的图像函数表示为,其中表示像素位置。
    • 因为离散,所有无穷小量取值为1
  1. 一阶x右导数近似表示:

      • 为1
  2. 一阶x左导数近似表示:

      • 为1
  3. 二阶x导数近似表示:

      • 为1
  4. 二阶y导数近似表示:

  5. Laplace算子:

    • \begin{aligned} \Delta^2 f &= \dfrac{\partial^2 f}{\partial x^2} + \dfrac{\partial^2 f}{\partial y^2} \\ &= f(x+1,y) + f(x-1,y) - 2f(x,y) + f(x,y+1) + f(x,y-1) - 2f(x,y) \\ &= (f(x+1,y) + f(x-1,y) + f(x,y+1) + f(x,y-1)) - 4f(x,y) \end{aligned}
  6. laplace算子的扩展与变形

说明

  • 正是通过导数(梯度:变化状态或者变化速度),检测像素的变化强度;从而可以检测到图像的边缘。
    • Sobel算子处理结果是像素的变化度效果;
  • 二阶导数,就是最值点(严格来说是极值点),这样也可以检测到图像边缘(严肃变化最大处就是边缘);
    • 拉普拉斯算子处理结果是像素边缘效果(可能存在误报,比如:图像中的山背,很可能检测为边缘,实际可能不是物体边缘)

一阶导数的图像意义

  • 一阶导数表示的梯度,梯度越大,表示图像像素变化越大。
    • 周围没有变化的像素的梯度为0。
    • 边缘处的像素肯定有比较强烈的变化,所以图像的一阶导数,可以检测到边缘,旦未必都是边缘。

二阶导数的图像意义

  • 二阶导数可以衡量像素的变化强度,变化均匀的属于同一区域,变化最大的就应该是边缘,所以二阶导数用来检测边缘。
    • 图像中像边缘但不是边缘的部分也会被检测到。
    • 在某些无意义的点上,二阶导数也为0的,这样会被认为是边缘。

拉普拉斯算子实现

  • 下面手工实现拉普拉斯算子,看看与OpenCV实现的差异。

  • 图像如果想锐化处理,可以增强差异部分,产生更加清晰的效果。

    • h(x, y) = \begin{cases} f(x, y) + c \ast (\dfrac{\partial ^ 2 f(x, y)}{ \partial ^2 x} +\dfrac{\partial ^ 2 f(x, y)}{ \partial ^2 y} )\\ f(x, y) - c \ast (\dfrac{\partial ^ 2 f(x, y)}{ \partial ^2 x} +\dfrac{\partial ^ 2 f(x, y)}{ \partial ^2 y} ) \end{cases}

一个完整实现的类

#include "imageprocess.h"

ImageProc::ImageProc():
    filename_image(new cv::String("lotus.png")){
    m_src = cv::imread(*filename_image);
}
ImageProc::ImageProc(const char *filename):
    filename_image(new cv::String(filename)){
    m_src = cv::imread(*filename_image);
}
ImageProc::~ImageProc(){
    delete filename_image;
    m_filter2d.release();
    m_laplace.release();
    m_channel.release();
    m_channels.release();
    m_src.release();
}
void ImageProc::filter2D(){
    cv::Mat kernel = (     // 逗号初始化器
        cv::Mat_(3, 3) <<
            1.0,  1.0, 1.0,
            1.0, -8.0, 1.0,
            1.0,  1.0, 1.0 
    );
    cv::filter2D(this->m_src, this->m_filter2d, -1, kernel, cv::Point(-1,-1), 0.0);
}
void ImageProc::laplace(){
    cv::Laplacian(this->m_src, this->m_laplace, -1, 3, 1.0, 0.0);
}
void ImageProc::channel(){
    // 转换为浮点数
    cv::Mat in_img;
    m_src.convertTo(in_img, CV_32FC3);    // 从三通道单字节无符号整数,转换为三通道单精度小数

    // 定义Laplace差分核(二阶导数)
    cv::Mat kernel = (                      // 便于cv::Mat,定义三通道单精度核。
        cv::Mat_(3,3) << 
            cv::Vec3f(1.0, 1.0, 1.0), cv::Vec3f( 1.0,  1.0,  1.0), cv::Vec3f(1.0, 1.0, 1.0),
            cv::Vec3f(1.0, 1.0, 1.0), cv::Vec3f(-8.0, -8.0, -8.0), cv::Vec3f(1.0, 1.0, 1.0),
            cv::Vec3f(1.0, 1.0, 1.0), cv::Vec3f( 1.0,  1.0,  1.0), cv::Vec3f(1.0, 1.0, 1.0)
    );
    // 获取图像行列
    int rows = in_img.rows;
    int cols = in_img.cols;
    // 构造一个Padding图像,Padding补0, padding大小(1,1)
    cv::Mat p_img(rows + 2 , cols + 2, in_img.type(), cv::Scalar_(0.0, 0.0, 0.0));
    // 拷贝图像到padding图像
    in_img.copyTo(p_img(cv::Range(1,rows+1), cv::Range(1, cols+1))); 
    // 定义输出(与源图像同大小与类型)
    cv::Mat out_img(rows, cols, in_img.type());
    // 5. 循环计算没有输出像素
    for(int y = 0; y< rows; y++){
        for(int x = 0; x< cols; x++){
            // 获取padding图像的子图
            cv::Mat sub = p_img(cv::Range(y, y + 3), cv::Range(x, x + 3)); // 子图大小为3
            // 计算矩阵hadamard乘积
            cv::Mat prod = sub.mul(kernel); // 子图与kernel矩阵的乘积 
            // 计算矩阵的和
            cv::Scalar pixel = cv::sum(prod);
            // 赋值结果到输出图像
            out_img.at(y, x) = cv::Vec3f(pixel[0], pixel[1], pixel[2]);
        }
    }
    out_img.convertTo(m_channel, CV_8UC3);
}
void ImageProc::channels(){
    // 卷积核:三通道无符号字节整数
    cv::Mat kernel = (
        cv::Mat_(3,3) << 
            cv::Vec3b(1, 1, 1), cv::Vec3b( 1,  1,  1), cv::Vec3b(1, 1, 1),
            cv::Vec3b(1, 1, 1), cv::Vec3b(-8, -8, -8), cv::Vec3b(1, 1, 1),
            cv::Vec3b(1, 1, 1), cv::Vec3b( 1,  1,  1), cv::Vec3b(1, 1, 1)
    );
    // 获取图像行列
    int rows = m_src.rows;
    int cols = m_src.cols;
    // 构造一个Padding图像,Padding补0
    cv::Mat p_img(rows + 2 , cols + 2, m_src.type(), cv::Scalar_(0, 0, 0));
    // 拷贝图像到padding图像
    m_src.copyTo(p_img(cv::Range(1, rows + 1), cv::Range(1, cols + 1))); 
    // 定义输出(与源图像同大小与类型)
    cv::Mat m_out(rows, cols, m_src.type());
    // 5. 循环计算没有输出像素
    for(int y = 0; y< rows; y++){
        for(int x = 0; x< cols; x++){
            // 获取padding图像的子图
            cv::Mat sub = p_img(cv::Range(y, y + 3), cv::Range(x, x + 3)); // 子图大小为3
            // 计算矩阵hadamard乘积
            cv::Mat prod = sub.mul(kernel); // 子图与kernel矩阵的乘积 
            // 计算矩阵的和
            cv::Scalar pixel = cv::sum(prod);
            // 赋值结果到输出图像
            m_out.at(y, x) = cv::Vec3b(pixel[0], pixel[1], pixel[2]);
        }
    }
    m_out.copyTo(m_channels);
}


filter2D滤波效果

通用卷积计算效果

Laplacian滤波效果

OpenCV的拉普拉斯滤波效果

小数计算滤波效果

使用cv::Mat实现的Laplace滤波

无符号字节整数计算滤波效果

数据溢出的滤波效果

思考

  1. 如果对图像2次一阶差分计算,产生的效果理论上应该是是一样的,但实际因为近似定义的模板,运行结果应该有差异。

  2. 锐化图像处理,其中二阶差分的系数在一般图像是否有一个在某个范围内的经验值?是的锐化图像效果最好?异或使用神经网络训练出一个服务于分类的最优值?

  3. 由于laplace算法本身的差分理论,所以对图像的噪音像素会产生放大效果。在图像预处理的时候,可能会使用高斯模糊先平滑处理,去掉部分噪音后在使用laplace提取边缘与轮廓。

  4. 因为颜色的通道尽管不一样,理论上三个通道的像素值的变化激烈度是一样的,所以可以肯定滴说,laplace不管采用什么方法计算,最后肯定是灰度图(三个通道的值一样)。

    • 如果是彩色的,则只有两个原因:计算错误或者数据溢出产生的误差。
  5. 从运行效果来说,OpenCv的Laplacian滤波函数,使用的是4个方向的滤波核。

    • 两个方向的滤波核的效果没有那个差异大。

附录:代码结构

  1. 界面类头文件
    #ifndef DLG_OPENCV_H
    #define DLG_OPENCV_H
    #include 
    #include "ui_output.h"
    #include "imageprocess.h"

    class DlgLaplace: public QDialog{
    Q_OBJECT    // 使用signal与slot,记得添加这个宏(如果继承QObject类型,则可选)。
    private:
        Ui::ui_output  *dlg;
        ImageProc  *proc;
        void showImage();
        void showImageOut(cv::Mat im);
    public:
        DlgLaplace(QWidget *parent = 0);     // 编程套路
        ~DlgLaplace();
    protected:
        virtual void showEvent(QShowEvent *event);
    private slots:
        void channel();
        void channels();
        void filter2D();
        void laplace();
    };
    #endif

  1. 计算类头文件
#ifndef  IMAGE_PROCESS_H
#define IMAGE_PROCESS_H
#include 
class ImageProc{
private:
    cv::String *filename_image;     // 文件名:std::string

public:
    cv::Mat m_src;                  // 原始图像的数据结构
    cv::Mat m_filter2d;            // 输出图像:cv::filter2D
    cv::Mat m_laplace;            // 输出图像:cv::Laplacian 
    cv::Mat m_channel;           // 输出图像:小数运算通道处理;
    cv::Mat m_channels;          // 输出图像:字节运算通道处理;
public:
    ImageProc();
    ImageProc(const char *filename);
    ~ImageProc();
    void channel();
    void channels();
    void filter2D();
    void laplace();
};
#endif // ! IMAGE_PROCESS_H

你可能感兴趣的:(CV02-02:Laplace滤波)