机器视觉中图像滤波处理应该是很基础,很重要的,本主题梳理下Laplace滤波的原理与实现;
拉普拉斯算子
数学表示
二阶导数的求值算法
- 假设计算图像中位置的二阶导数,表示图像位置的像素值。
二阶导数的离散表示
-
提示:
- 该公式的推导,见后面附录。
矩阵的哈达玛(Hadamard)积表示
- 矩阵Hadamard积,使用表示:
二阶导数的Hadamard积的和表示
-
提示:
- 到目前为止,完美的使用矩阵运算表示了离散图像的二阶导数表示。
附录一:离散二阶导数的推导
连续微分
- 假设函数为
-
一阶导数表示:
-
一阶导数的极限定义:
-
二阶微分的定义
离散导数
- 对离散的图像函数表示为,其中表示像素位置。
- 因为离散,所有无穷小量取值为1
-
一阶x右导数近似表示:
-
- 为1
-
-
一阶x左导数近似表示:
-
- 为1
-
-
二阶x导数近似表示:
-
- 为1
-
-
二阶y导数近似表示:
-
Laplace算子:
laplace算子的扩展与变形
说明
- 正是通过导数(梯度:变化状态或者变化速度),检测像素的变化强度;从而可以检测到图像的边缘。
- Sobel算子处理结果是像素的变化度效果;
- 二阶导数,就是最值点(严格来说是极值点),这样也可以检测到图像边缘(严肃变化最大处就是边缘);
- 拉普拉斯算子处理结果是像素边缘效果(可能存在误报,比如:图像中的山背,很可能检测为边缘,实际可能不是物体边缘)
一阶导数的图像意义
- 一阶导数表示的梯度,梯度越大,表示图像像素变化越大。
- 周围没有变化的像素的梯度为0。
- 边缘处的像素肯定有比较强烈的变化,所以图像的一阶导数,可以检测到边缘,旦未必都是边缘。
二阶导数的图像意义
- 二阶导数可以衡量像素的变化强度,变化均匀的属于同一区域,变化最大的就应该是边缘,所以二阶导数用来检测边缘。
- 图像中像边缘但不是边缘的部分也会被检测到。
- 在某些无意义的点上,二阶导数也为0的,这样会被认为是边缘。
拉普拉斯算子实现
下面手工实现拉普拉斯算子,看看与OpenCV实现的差异。
-
图像如果想锐化处理,可以增强差异部分,产生更加清晰的效果。
一个完整实现的类
#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_<float>(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_<cv::Vec3f>(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_<float>(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<cv::Vec3f>(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_<cv::Vec3b>(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_<u_char>(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<cv::Vec3b>(y, x) = cv::Vec3b(pixel[0], pixel[1], pixel[2]);
}
}
m_out.copyTo(m_channels);
}
filter2D滤波效果
Laplacian滤波效果
小数计算滤波效果
无符号字节整数计算滤波效果
思考
如果对图像2次一阶差分计算,产生的效果理论上应该是是一样的,但实际因为近似定义的模板,运行结果应该有差异。
锐化图像处理,其中二阶差分的系数在一般图像是否有一个在某个范围内的经验值?是的锐化图像效果最好?异或使用神经网络训练出一个服务于分类的最优值?
由于laplace算法本身的差分理论,所以对图像的噪音像素会产生放大效果。在图像预处理的时候,可能会使用高斯模糊先平滑处理,去掉部分噪音后在使用laplace提取边缘与轮廓。
-
因为颜色的通道尽管不一样,理论上三个通道的像素值的变化激烈度是一样的,所以可以肯定滴说,laplace不管采用什么方法计算,最后肯定是灰度图(三个通道的值一样)。
- 如果是彩色的,则只有两个原因:计算错误或者数据溢出产生的误差。
-
从运行效果来说,OpenCv的Laplacian滤波函数,使用的是4个方向的滤波核。
- 两个方向的滤波核的效果没有那个差异大。
附录:代码结构
- 界面类头文件
#ifndef DLG_OPENCV_H
#define DLG_OPENCV_H
#include <opencv2/opencv.hpp>
#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
- 计算类头文件
#ifndef IMAGE_PROCESS_H
#define IMAGE_PROCESS_H
#include <opencv2/opencv.hpp>
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