常用图像滤波算法的实现与原理解析
点击上方“小白学视觉”,选择加"星标"或“置顶”
重磅干货,第一时间送达
导读
图像滤波是一种非常重要的图像处理技术,本文详细介绍了四种常见的图像滤波算法,并附上源码,包括自适应中值滤波、高斯滤波、双边滤波和导向滤波。
前言
本文介绍四种常见的图像滤波算法,并附上源码。图像滤波是一种非常重要的图像处理技术,现在大火的卷积神经网络其实也是滤波的一种,都是用卷积核去提取图像的特征模式。不过,传统的滤波,使用的卷积核是固定的参数,是由经验非常丰富的人去手动设计的,也称为手工特征。而卷积神经网络的卷积核参数初始时未知的,根据不同的任务由数据和神经网络反向传播算法去学习得到的参数,更能适应于不同的任务。
目录
自适应中值滤波
高斯滤波
双边滤波
导向滤波
自适应中值滤波
中值滤波器是一种常用的非线性滤波器,其基本原理是:选择待处理像素的一个邻域中各像素值的中值来代替待处理的像素。主要功能使某像素的灰度值与周围领域内的像素比较接近,从而消除一些孤立的噪声点,所以中值滤波器能够很好的消除椒盐噪声。不仅如此,中值滤波器在消除噪声的同时,还能有效的保护图像的边界信息,不会对图像造成很大的模糊(相比于均值滤波器)。
中值滤波器的效果受滤波窗口尺寸的影响较大,在消除噪声和保护图像的细节存在着矛盾:滤波窗口较小,则能很好的保护图像中的某些细节,但对噪声的过滤效果就不是很好,因为实际中的噪声不可能只占一个像素位置;反之,窗口尺寸较大有较好的噪声过滤效果,但是会对图像造成一定的模糊。另外,根据中值滤波器原理,如果在滤波窗口内的噪声点的个数大于整个窗口内非噪声像素的个数,则中值滤波就不能很好的过滤掉噪声。
自适应中值滤波器
常规的中值滤波器,在噪声的密度不是很大的情况下,效果不错。但是当噪声出现的概率较高时,常规的中值滤波的效果就不是很好了。有一个选择就是增大滤波器的窗口大小,这虽然在一定程度上能解决上述的问题,但是会给图像造成较大的模糊。
常规的中值滤波器的窗口尺寸是固定大小不变的,就不能同时兼顾去噪和保护图像的细节。这时就要寻求一种改变,根据预先设定好的条件,在滤波的过程中,动态的改变滤波器的窗口尺寸大小,这就是自适应中值滤波器 Adaptive Median Filter。在滤波的过程中,自适应中值滤波器会根据预先设定好的条件,改变滤波窗口的尺寸大小,同时还会根据一定的条件判断当前像素是不是噪声,如果是则用邻域中值替换掉当前像素;不是,则不作改变。
自适应中值滤波器有三个目的:
滤除椒盐噪声
平滑其他非脉冲噪声
尽可能的保护图像中细节信息,避免图像边缘的细化或者粗化。
自适应中值滤波算法描述
自适应滤波器不但能够滤除概率较大的椒盐噪声,而且能够更好的保护图像的细节,这是常规的中值滤波器做不到的。自适应的中值滤波器也需要一个矩形的窗口 ,和常规中值滤波器不同的是这个窗口的大小会在滤波处理的过程中进行改变(增大)。需要注意的是,滤波器的输出是一个像素值,该值用来替换点 处的像素值,点 是滤波窗口的中心位置。
在描述自适应中值滤波器时需要用到如下的符号:
窗口中的最小灰度值
窗口中的最大灰度值
窗口中的灰度值的中值
表示坐标 处的灰度值
允许的最大窗口尺寸
自适应中值滤波器有两个处理过程,分别记为:和。
A :
如果A1 > 0 且 A2 < 0,跳转到 B;
否则,增大窗口的尺寸 如果增大后窗口的尺寸 ,则重复A过程。否则,输出 ????????????????
B:
如果B1 > 0 且 B2 <0则输出 ,否则输出
自适应中值滤波原理说明
过程A的目的是确定当前窗口内得到中值 ???????????????? 是否是噪声。如果 ????????????????<????????????????<????????????????,则????????????????不是噪声,这时转到过程B测试当前窗口的中心位置的像素 ???????????? 是否是一个噪声点。如果 ????????????????<????????????<????????????????,则 ???????????? 不是一个噪声,此时输出???????????? ;如果不满足上述条件,则可判定 ???????????? 是噪声,这是输出中值 ???????????????? (在A中已经判断出 ???????????????? 不是噪声)。
如果在过程A中,得到则 ???????????????? 不符合条件 ????????????????<????????????????<???????????????? ,则可判断得到的中值 ???????????????? 是一个噪声。在这种情况下,需要增大滤波器的窗口尺寸,在一个更大的范围内寻找一个非噪声点的中值,直到找到一个非噪声的中值,跳转到B;或者,窗口的尺寸达到了最大值,这时返回找到的中值,退出。
从上面分析可知,噪声出现的概率较低,自适应中值滤波器可以较快的得出结果,不需要去增加窗口的尺寸;反之,噪声的出现的概率较高,则需要增大滤波器的窗口尺寸,这也符合种中值滤波器的特点:噪声点比较多时,需要更大的滤波器窗口尺寸。
算法实现
有了算法的详细描述,借助于OpenCV对图像的读写,自适应中值滤波器实现起来也不是很困难。首先定义滤波器最小的窗口尺寸以及最大的窗口尺寸。要进行滤波处理,首先要扩展图像的边界,以便对图像的边界像素进行处理。copyMakeBorder根据选择的BorderTypes使用不同的值扩充图像的边界像素,具体可参考OpenCV的文档信息。下面就是遍历图像的像素,对每个像素进行滤波处理。需要注意一点,不论滤波器多么的复杂,其每次的滤波过程,都是值返回一个值,来替换掉当前窗口的中心的像素值。函数adpativeProcess就是对当前像素的滤波过程,其代码如下:
uchar adaptiveProcess(const Mat &im, int row,int col,int kernelSize,int maxSize)
{
vector<uchar> pixels;
for (int a = -kernelSize / 2; a <= kernelSize / 2; a++)
for (int b = -kernelSize / 2; b <= kernelSize / 2; b++)
{
pixels.push_back(im.at<uchar>(row + a, col + b));
}
sort(pixels.begin(), pixels.end());
auto min = pixels[0];
auto max = pixels[kernelSize * kernelSize - 1];
auto med = pixels[kernelSize * kernelSize / 2];
auto zxy = im.at<uchar>(row, col);
if (med > min && med < max)
{
// to B
if (zxy > min && zxy < max)
return zxy;
else
return med;
}
else
{
kernelSize += 2;
if (kernelSize <= maxSize)
return adpativeProcess(im, row, col, kernelSize, maxSize); // 增大窗口尺寸,继续A过程。
else
return med;
}
}
有了上面这个函数,剩下的只需要对全部像素做一个遍历即可,更为完整的代码,请见我的Github地址:
https://github.com/zhangqizky/common-image-filteringgithub.com
高斯滤波
高斯滤波也是一种非常常见的滤波方法,其核的形式为:
其中是图像中的点的坐标, 为标准差,高斯模板就是利用这个函数来计算的,x和y都是代表,以核中心点为坐标原点的坐标值。这里想说一下 的作用,当 比较小的时候,生成的高斯模板中心的系数比较大,而周围的系数比较小,这样对图像的平滑效果不明显。而当 比较大时,生成的模板的各个系数相差就不是很大,比较类似于均值模板,对图像的平滑效果比较明显。
高斯滤波没有特别多可说的,最主要的作用是滤除高斯噪声,即符合正态分布的噪声。
实现的方式有两种,第一种是按照公式暴力实现,代码如下:
//O(m * n * ksize^2)
void GaussianFilter(const Mat &src, Mat &dst, int ksize, double sigma)
{
CV_Assert(src.channels() || src.channels() == 3); //只处理3通道或单通道的图片
double **GaussianTemplate = new double *[ksize];
for(int i = 0; i < ksize; i++){
GaussianTemplate[i] = new double [ksize];
}
generateGaussianTemplate(GaussianTemplate, ksize, sigma);
//padding
int border = ksize / 2;
copyMakeBorder(src, dst, border, border, border, border, BORDER_CONSTANT);
int channels = dst.channels();
int rows = dst.rows - border;
int cols = dst.cols - border;
for(int i = border; i < rows; i++){
for(int j = border; j< cols; j++){
double sum[3] = {0};
for(int a = -border; a <= border; a++){
for(int b = -border; b <= border; b++){
if(channels == 1){
sum[0] += GaussianTemplate[border+a][border+b] * dst.at<uchar>(i+a, j+b);
}else if(channels == 3){
Vec3b rgb = dst.at<Vec3b>(i+a, j+b);
auto k = GaussianTemplate[border+a][border+b];
sum[0] += k * rgb[0];
sum[1] += k * rgb[1];
sum[2] += k * rgb[2];
}
}
}
for(int k = 0; k < channels; k++){
if(sum[k] < 0) sum[k] = 0;
else if(sum[k] > 255) sum[k] = 255;
}
if(channels == 1){
dst.at<uchar >(i, j) = static_cast<uchar >(sum[0]);
}else if(channels == 3){
Vec3b rgb = {static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2])};
dst.at<Vec3b>(i, j) = rgb;
}
}
}
for(int i = 0; i < ksize; i++)
delete[] GaussianTemplate[i];
delete[] GaussianTemplate;
}
当核比较大时,高斯滤波会比较费时,此时可以使用分离X和Y通道的形式来实现可分离的高斯滤波。为什么可以这么做?因为高斯函数中没有这样的耦合项,即x和y是相对独立的,此时就可以将两个维度分离开来。
//分离实现高斯滤波
//O(m*n*k)
void separateGaussianFilter(const Mat &src, Mat &dst, int ksize, double sigma){
assert(src.channels()==1 || src.channels() == 3); //只处理单通道或者三通道图像
//生成一维的
double *matrix = new double[ksize];
double sum = 0;
int origin = ksize / 2;
for(int i = 0; i < ksize; i++){
double g = exp(-(i-origin) * (i-origin) / (2 * sigma * sigma));
sum += g;
matrix[i] = g;
}
for(int i = 0; i < ksize; i++) matrix[i] /= sum;
int border = ksize / 2;
copyMakeBorder(src, dst, border, border, border, border, BORDER_CONSTANT);
int channels = dst.channels();
int rows = dst.rows - border;
int cols = dst.cols - border;
//水平方向
for(int i = border; i < rows; i++){
for(int j = border; j < cols; j++){
double sum[3] = {0};
for(int k = -border; k<=border; k++){
if(channels == 1){
sum[0] += matrix[border + k] * dst.at<uchar>(i, j+k);
}else if(channels == 3){
Vec3b rgb = dst.at<Vec3b>(i, j+k);
sum[0] += matrix[border+k] * rgb[0];
sum[1] += matrix[border+k] * rgb[1];
sum[2] += matrix[border+k] * rgb[2];
}
}
for(int k = 0; k < channels; k++){
if(sum[k] < 0) sum[k] = 0;
else if(sum[k] > 255) sum[k] = 255;
}
if(channels == 1)
dst.at<Vec3b>(i, j) = static_cast<uchar>(sum[0]);
else if(channels == 3){
Vec3b rgb = {static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2])};
dst.at<Vec3b>(i, j) = rgb;
}
}
}
//竖直方向
for(int i = border; i < rows; i++){
for(int j = border; j < cols; j++){
double sum[3] = {0};
for(int k = -border; k<=border; k++){
if(channels == 1){
sum[0] += matrix[border + k] * dst.at<uchar>(i+k, j);
}else if(channels == 3){
Vec3b rgb = dst.at<Vec3b>(i+k, j);
sum[0] += matrix[border+k] * rgb[0];
sum[1] += matrix[border+k] * rgb[1];
sum[2] += matrix[border+k] * rgb[2];
}
}
for(int k = 0; k < channels; k++){
if(sum[k] < 0) sum[k] = 0;
else if(sum[k] > 255) sum[k] = 255;
}
if(channels == 1)
dst.at<Vec3b>(i, j) = static_cast<uchar>(sum[0]);
else if(channels == 3){
Vec3b rgb = {static_cast<uchar>(sum[0]), static_cast<uchar>(sum[1]), static_cast<uchar>(sum[2])};
dst.at<Vec3b>(i, j) = rgb;
}
}
}
delete [] matrix;
}
同样,完整的代码请见:
https://github.com/zhangqizky/common-image-filtering/tree/maingithub.com
双边滤波
双边滤波是一种非线性滤波方法,是结合了图像的邻近度和像素值相似度的一种折中,在滤除噪声的同时可以保留原图的边缘信息。整个双边滤波是由两个函数构成:一个函数是由空间距离决定的滤波器系数,另外一个诗由像素差值决定的滤波器系数。整个双边滤波的公式如下:
其中权重系数 取决于定义域核:
和值域核
的乘积。其中定义域核影响的是空间位置,如果把图像看成一个二维函数,那么定义域就是图像的坐标,值域就是该坐标处对应的像素值。定义域核就是普通的高斯核,全局使用一个就可以。但值域核是需要对每个像素点滑动进行计算的。
那么如何理解双边滤波呢
高斯滤波的滤波核的意义是,滤波后的像素值等于窗口内的像素值的加权平均值,权值系数是符合高斯分布,距离该点越近,权值越大。但是没有考虑像素值与当前点的差距。现在加上值域核,意义就在,滤波后当前点的像素值还会受到领域内像素值与自身的像素值差异的影响,不仅仅是距离来决定。这样,在平缓的区域里,由于像素值差异非常小,则值域的权重趋向于1,所以双边滤波就近似为高斯滤波。而在边缘区域中,由于像素值的差异比较大,则值域核趋向于0,权重下降,即当前像素受到领域内像素影响比较小,从而保留了边缘信息。
双边滤波的代码
opencv中提供了bilateralFilter()函数来实现双边滤波操作,其原型如下:
void cv::bilateralFilter(InputArray src,
OutputArray dst,
int d,
double sigmaColor,
double sigmaSpace,
int borderType = BORDER_DEFAULT
)
InputArray src: 输入图像,可以是Mat类型,图像必须是8位整型或浮点型单通道、三通道的图像。
OutputArray dst: 输出图像,和原图像有相同的尺寸和类型。
int d: 表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
double sigmaColor: 颜色空间过滤器的值,这个参数的值月大,表明该像素邻域内有越宽广的颜色会被混合到一起,产生较大的半相等颜色区域。(这个参数可以理解为值域核的 和 )
double sigmaSpace: 坐标空间中滤波器的sigma值,如果该值较大,则意味着越远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace无关,否则d正比于sigmaSpace. (这个参数可以理解为空间域核的 和 )
int borderType=BORDER_DEFAULT: 用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.
具体代码如下:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
//定义全局变量
const int g_ndMaxValue = 100;
const int g_nsigmaColorMaxValue = 200;
const int g_nsigmaSpaceMaxValue = 200;
int g_ndValue;
int g_nsigmaColorValue;
int g_nsigmaSpaceValue;
Mat g_srcImage;
Mat g_dstImage;
//定义回调函数
void on_bilateralFilterTrackbar(int, void*);
int main()
{
g_srcImage = imread("lena.jpg");
//判断图像是否加载成功
if(g_srcImage.empty())
{
cout << "图像加载失败!" << endl;
return -1;
}
else
cout << "图像加载成功!" << endl << endl;
namedWindow("原图像", WINDOW_AUTOSIZE);
imshow("原图像", g_srcImage);
//定义输出图像窗口属性和轨迹条属性
namedWindow("双边滤波图像", WINDOW_AUTOSIZE);
g_ndValue = 10;
g_nsigmaColorValue = 10;
g_nsigmaSpaceValue = 10;
char dName[20];
sprintf(dName, "邻域直径 %d", g_ndMaxValue);
char sigmaColorName[20];
sprintf(sigmaColorName, "sigmaColor %d", g_nsigmaColorMaxValue);
char sigmaSpaceName[20];
sprintf(sigmaSpaceName, "sigmaSpace %d", g_nsigmaSpaceMaxValue);
//创建轨迹条
createTrackbar(dName, "双边滤波图像", &g_ndValue, g_ndMaxValue, on_bilateralFilterTrackbar);
on_bilateralFilterTrackbar(g_ndValue, 0);
createTrackbar(sigmaColorName, "双边滤波图像", &g_nsigmaColorValue,
g_nsigmaColorMaxValue, on_bilateralFilterTrackbar);
on_bilateralFilterTrackbar(g_nsigmaColorValue, 0);
createTrackbar(sigmaSpaceName, "双边滤波图像", &g_nsigmaSpaceValue,
g_nsigmaSpaceMaxValue, on_bilateralFilterTrackbar);
on_bilateralFilterTrackbar(g_nsigmaSpaceValue, 0);
waitKey(0);
return 0;
}
void on_bilateralFilterTrackbar(int, void*)
{
bilateralFilter(g_srcImage, g_dstImage, g_ndValue, g_nsigmaColorValue, g_nsigmaSpaceValue);
imshow("双边滤波图像", g_dstImage);
}
导向滤波
需要有高斯滤波和双边滤波的相关知识背景才能更好的理解导向滤波。在导向滤波中,首先利用了局部线性模型。这个模型认为某函数上一点与其近邻部分的点成线性关系,一个复杂的函数就可以用很多局部的线性函数来表示,当需要求该函数上某一点的值时,只需要计算所有包含该点的线性函数的值并取平均值即可。这种模型,在表示非解析函数上,非常有用。
同理,我们可以认为图像是一个二维函数,并且假设该函数的输出与输入在一个二维窗口内满足线性关系,如下:
其中,是输出像素的值,是输入图像的值,和是像素索引,和是当窗口中心位于k时该线性函数的系数。其实,输入图像不一定是待滤波的图像本身,也可以是其他图像即引导图像,这也是为何称为引导滤波的原因。对上式两边取梯度,可以得到:
即当输入图像有梯度时,输出也有类似的梯度,现在可以解释为什么引导滤波有边缘保持特性了。下一步是求出线性函数的系数,也就是线性回归,即希望拟合函数的输出值与真实值之间的差距最小,也就是让下式最小:
这里只能是待滤波图像,并不像那样可以是其他图像。同时,之前的系数用于防止求得的过大,也是调节滤波器滤波效果的重要参数(相当于L2正则化的权重惩罚)。接下来利用最小二乘法的原理令 和 得到2个二元一次方程,求解得到:
,
其中 是 在窗口的平均值, 是 在窗口 的方差, 是窗口 中的像素个数, 是待滤波图像在窗口 中的均值。在计算每个窗口的线性系数时,我们可以发现一个像素会被多个窗口包含,也就是说,每个像素都由多个线性函数所描述。因此,如之前所说,要具体求某一点的输出值时,只需将所有包含该点的线性函数值平均即可,如下:
这里, 是所有包含像素 的窗口, 是其中心位置。
当把引导滤波用作边缘保持滤波器时,往往有 ,如果 ,显然是为最小值的解,从上式可以看出,这时的滤波器没有任何作用,将输入原封不动的输出。如果 ,在像素强度变化小的区域(或单色区域),有近似于(或等于0,而近似于(或等于) ,即做了一个加权均值滤波;而在变化大的区域,近似于1,近似于0,对图像的滤波效果很弱,有助于保持边缘。而 的作用就是界定什么是变化大,什么是变化小。在窗口大小不变的情况下,随着的增大,滤波效果越明显。
在滤波效果上,引导滤波和双边滤波差不多,然后在一些细节上,引导滤波较好(在PS的磨皮美白中,经过实践,效果更好)。引导滤波最大的优势在于,可以写出时间复杂度与窗口大小无关的算法,因此在使用大窗口处理图片时,其效率更高。
同样,OpenCV中也有导向滤波的接口。具体代码如下:
void cv::ximgproc::guidedFilter ( InputArray guide,
InputArray src,
OutputArray dst,
int radius,
double eps,
int dDepth = -1
)
guide | 引导图像,3通道,如果大于3通道则只有前三个会被用到 |
---|---|
src | 待滤波图像 |
dst | 输出图像 |
radius | 导向滤波的窗口 |
eps | 正则化参数 |
dDepth | 可选,图像的深度参数 |
这边有个基于scipy实现的python代码,可以参考一下:
import numpy as np
import scipy as sp
import scipy.ndimage
def box(img, r):
""" O(1) box filter
img - >= 2d image
r - radius of box filter
"""
(rows, cols) = img.shape[:2]
imDst = np.zeros_like(img)
tile = [1] * img.ndim
tile[0] = r
imCum = np.cumsum(img, 0)
imDst[0:r+1, :, ...] = imCum[r:2*r+1, :, ...]
imDst[r+1:rows-r, :, ...] = imCum[2*r+1:rows, :, ...] - imCum[0:rows-2*r-1, :, ...]
imDst[rows-r:rows, :, ...] = np.tile(imCum[rows-1:rows, :, ...], tile) - imCum[rows-2*r-1:rows-r-1, :, ...]
tile = [1] * img.ndim
tile[1] = r
imCum = np.cumsum(imDst, 1)
imDst[:, 0:r+1, ...] = imCum[:, r:2*r+1, ...]
imDst[:, r+1:cols-r, ...] = imCum[:, 2*r+1 : cols, ...] - imCum[:, 0 : cols-2*r-1, ...]
imDst[:, cols-r: cols, ...] = np.tile(imCum[:, cols-1:cols, ...], tile) - imCum[:, cols-2*r-1 : cols-r-1, ...]
return imDst
def _gf_color(I, p, r, eps, s=None):
""" Color guided filter
I - guide image (rgb)
p - filtering input (single channel)
r - window radius
eps - regularization (roughly, variance of non-edge noise)
s - subsampling factor for fast guided filter
"""
fullI = I
fullP = p
if s is not None:
I = sp.ndimage.zoom(fullI, [1/s, 1/s, 1], order=1)
p = sp.ndimage.zoom(fullP, [1/s, 1/s], order=1)
r = round(r / s)
h, w = p.shape[:2]
N = box(np.ones((h, w)), r)
mI_r = box(I[:,:,0], r) / N
mI_g = box(I[:,:,1], r) / N
mI_b = box(I[:,:,2], r) / N
mP = box(p, r) / N
# mean of I * p
mIp_r = box(I[:,:,0]*p, r) / N
mIp_g = box(I[:,:,1]*p, r) / N
mIp_b = box(I[:,:,2]*p, r) / N
# per-patch covariance of (I, p)
covIp_r = mIp_r - mI_r * mP
covIp_g = mIp_g - mI_g * mP
covIp_b = mIp_b - mI_b * mP
# symmetric covariance matrix of I in each patch:
# rr rg rb
# rg gg gb
# rb gb bb
var_I_rr = box(I[:,:,0] * I[:,:,0], r) / N - mI_r * mI_r;
var_I_rg = box(I[:,:,0] * I[:,:,1], r) / N - mI_r * mI_g;
var_I_rb = box(I[:,:,0] * I[:,:,2], r) / N - mI_r * mI_b;
var_I_gg = box(I[:,:,1] * I[:,:,1], r) / N - mI_g * mI_g;
var_I_gb = box(I[:,:,1] * I[:,:,2], r) / N - mI_g * mI_b;
var_I_bb = box(I[:,:,2] * I[:,:,2], r) / N - mI_b * mI_b;
a = np.zeros((h, w, 3))
for i in range(h):
for j in range(w):
sig = np.array([
[var_I_rr[i,j], var_I_rg[i,j], var_I_rb[i,j]],
[var_I_rg[i,j], var_I_gg[i,j], var_I_gb[i,j]],
[var_I_rb[i,j], var_I_gb[i,j], var_I_bb[i,j]]
])
covIp = np.array([covIp_r[i,j], covIp_g[i,j], covIp_b[i,j]])
a[i,j,:] = np.linalg.solve(sig + eps * np.eye(3), covIp)
b = mP - a[:,:,0] * mI_r - a[:,:,1] * mI_g - a[:,:,2] * mI_b
meanA = box(a, r) / N[...,np.newaxis]
meanB = box(b, r) / N
if s is not None:
meanA = sp.ndimage.zoom(meanA, [s, s, 1], order=1)
meanB = sp.ndimage.zoom(meanB, [s, s], order=1)
q = np.sum(meanA * fullI, axis=2) + meanB
return q
def _gf_gray(I, p, r, eps, s=None):
""" grayscale (fast) guided filter
I - guide image (1 channel)
p - filter input (1 channel)
r - window raidus
eps - regularization (roughly, allowable variance of non-edge noise)
s - subsampling factor for fast guided filter
"""
if s is not None:
Isub = sp.ndimage.zoom(I, 1/s, order=1)
Psub = sp.ndimage.zoom(p, 1/s, order=1)
r = round(r / s)
else:
Isub = I
Psub = p
(rows, cols) = Isub.shape
N = box(np.ones([rows, cols]), r)
meanI = box(Isub, r) / N
meanP = box(Psub, r) / N
corrI = box(Isub * Isub, r) / N
corrIp = box(Isub * Psub, r) / N
varI = corrI - meanI * meanI
covIp = corrIp - meanI * meanP
a = covIp / (varI + eps)
b = meanP - a * meanI
meanA = box(a, r) / N
meanB = box(b, r) / N
if s is not None:
meanA = sp.ndimage.zoom(meanA, s, order=1)
meanB = sp.ndimage.zoom(meanB, s, order=1)
q = meanA * I + meanB
return q
def _gf_colorgray(I, p, r, eps, s=None):
""" automatically choose color or gray guided filter based on I's shape """
if I.ndim == 2 or I.shape[2] == 1:
return _gf_gray(I, p, r, eps, s)
elif I.ndim == 3 and I.shape[2] == 3:
return _gf_color(I, p, r, eps, s)
else:
print("Invalid guide dimensions:", I.shape)
def guided_filter(I, p, r, eps, s=None):
""" run a guided filter per-channel on filtering input p
I - guide image (1 or 3 channel)
p - filter input (n channel)
r - window raidus
eps - regularization (roughly, allowable variance of non-edge noise)
s - subsampling factor for fast guided filter
"""
if p.ndim == 2:
p3 = p[:,:,np.newaxis]
out = np.zeros_like(p3)
for ch in range(p3.shape[2]):
out[:,:,ch] = _gf_colorgray(I, p3[:,:,ch], r, eps, s)
return np.squeeze(out) if p.ndim == 2 else out
def test_gf():
import imageio
cat = imageio.imread('cat.bmp').astype(np.float32) / 255
tulips = imageio.imread('tulips.bmp').astype(np.float32) / 255
r = 8
eps = 0.05
cat_smoothed = guided_filter(cat, cat, r, eps)
cat_detail = cat / cat_smoothed
print(cat_detail.shape)
cat_smoothed_s4 = guided_filter(cat, cat, r, eps, s=4)
imageio.imwrite('cat_smoothed.png', cat_smoothed)
imageio.imwrite('cat_smoothed_s4.png', cat_smoothed_s4)
imageio.imwrite('cat_smoothed_detailed.png',cat_detail)
tulips_smoothed4s = np.zeros_like(tulips)
tulips_detailed = np.zeros_like(tulips)
for i in range(3):
tulips_smoothed4s[:,:,i] = guided_filter(tulips, tulips[:,:,i], r, eps, s=4)
tulips_detailed = tulips / tulips_smoothed4s
imageio.imwrite('tulips_detailed.png',tulips_detailed)
imageio.imwrite('tulips_smoothed4s.png', tulips_smoothed4s)
tulips_smoothed = np.zeros_like(tulips)
for i in range(3):
tulips_smoothed[:,:,i] = guided_filter(tulips, tulips[:,:,i], r, eps)
imageio.imwrite('tulips_smoothed.png', tulips_smoothed)
if __name__ == '__main__':
test_gf()
一副图像,经过mask是图像本身的导向滤波之后,得到一张细节图和一张滤波图。下面从左到右分别是原图,细节图和滤波图。其实这是现在很多low-level领域的预处理步骤。拿到细节图之后可以用卷积神经网络做下面的处理。
下载1:OpenCV-Contrib扩展模块中文版教程
在「小白学视觉」公众号后台回复:扩展模块中文教程,即可下载全网第一份OpenCV扩展模块教程中文版,涵盖扩展模块安装、SFM算法、立体视觉、目标跟踪、生物视觉、超分辨率处理等二十多章内容。
下载2:Python视觉实战项目52讲
在「小白学视觉」公众号后台回复:Python视觉实战项目,即可下载包括图像分割、口罩检测、车道线检测、车辆计数、添加眼线、车牌识别、字符识别、情绪检测、文本内容提取、面部识别等31个视觉实战项目,助力快速学校计算机视觉。
下载3:OpenCV实战项目20讲
在「小白学视觉」公众号后台回复:OpenCV实战项目20讲,即可下载含有20个基于OpenCV实现20个实战项目,实现OpenCV学习进阶。
交流群
欢迎加入公众号读者群一起和同行交流,目前有SLAM、三维视觉、传感器、自动驾驶、计算摄影、检测、分割、识别、医学影像、GAN、算法竞赛等微信群(以后会逐渐细分),请扫描下面微信号加群,备注:”昵称+学校/公司+研究方向“,例如:”张三 + 上海交大 + 视觉SLAM“。请按照格式备注,否则不予通过。添加成功后会根据研究方向邀请进入相关微信群。请勿在群内发送广告,否则会请出群,谢谢理解~
上一篇: 数字图像处理之图像滤波
下一篇: 图像处理滤波技巧概览
推荐阅读
-
跟着我学习 Python 图像处理丨带你掌握傅立叶变换的原理与实现
-
反传销网8月30日发布:视频区块链里的骗子,币里的韭菜,杜子建骂人了!金融大V周召说区块链!——“一小帮骗子玩一大帮小白,被割韭菜,小白还轮流被割,割的就是你!” 什么区块链,统统是骗子 作者:周召(知乎金融领域大V,毕业于上海财经大学,目前任职上海某股权投资基金合伙人) 有人问我,区块链现在这么火,到底是不是骗局? 我的回答是: 是骗局。而且我并不是说数字货币是骗局,而是说所有搞区块链的都是骗局。 -01- 区块链是一种鸡肋技术 人类社会任何技术的发明应用,本质都是为了提高社会的生产效率。而所谓区块链技术本质不过是几种早已成熟的技术的大杂烩,冗余且十分低效,除了提高了洗钱和诈骗的效率以外,对人类社会的进步毫无贡献。 真正意义上的区块链得包含三个要素:分布式系统(包括记账和存储),无法篡改的数据结构,以及共识算法,三者互为基础和因果,就像三体世界一样。看上去挺让人不明觉厉的,而经过几年的瞎折腾,稍微懂点区块链的碰了几次壁后都已经渐渐明白区块链其实并没有什么卵用,区块链技术已经名存实亡,沦为了营销工具和传销组织的画皮。 因为符合上述定义的、以比特币为代表的原教旨区块链技术,是反效率的,从经济学角度来说,不但不是一种帕累托改进,甚至还可以说是一种帕累托倒退。 原教旨区块链技术的效率十分低下,因为要遍历所有节点,只能做非常轻量级的数据应用,一旦涉及到大量的数据传输与更新,区块链就瞎了。 一方面整条链交易速度会极慢,另一方面数据库容量极速膨胀,考虑到人手一份的存储机制,区块链其实是对存储资源和能源的一种极大的浪费。 这里还没有加上为了取得所谓的共识和挖矿消耗的巨大的能源,如果说区块链技术是屎,那么这波区块链投机浪潮可谓人类历史上最大规模的搅屎运动。 区块链也验证不了任何东西。 所谓的智能合约,即不智能,也非合约。我看有人还说,如果有了智能合约,就可以跟老板签一份放区块链上,如果明年销售业绩提升30%,就加薪10%,由于区块链不能篡改,不能抵赖,所以老板必须得执行,说得有板有眼,不懂行的愣一看,好像还真是那么回事。 但仔细一想,问题就来了。首先,在区块链上如何证明你真的达到了30%业绩提升?即便真的达到老板耍赖如何执行? 也就是说,如果区块链真这么厉害,要法院和仲裁干什么。 人类社会真正的符合成本效益原则的是代理制度。之前有人说要用区块链改造注册会计师行业,我不知道他准备怎么设计,我猜想他思路大概是这样的,首先肯定搞去中心化,让所有会计师到链上来,然后一个新人要成为注册会计师就要所有会计师同意并记录在链上。 那我就请问了,我每天上班累死累活,为什么还要花时间去验证一个跟我无关的的人的专业能力?最优做法当然是组织一个委员会,让专门的人来负责,这不就是现在注册会师协会干的事儿吗?区块链的逻辑相当于什么事情都要拿出来公投,这个绝对是扯淡的。 当然这么说都有点抬举区块链了,区块链技术本身根本没有判断是非能力,如果这么高级的人工智能,靠一个无脑分布式记账就能实现的话,我们早就进入共产主义社会了。 虽然EOS等数字货币采用了超级节点,通过再中心化的方式提高效率,有点行业协会的意思,是对区块链原教旨主义的一种修正,但是依然无法突破区块链技术最本质的局限性。有人说,私有链和联盟链是区块链技术的未来,也是扯淡,因为区块链技术没有未来。如果有,说明他是包装成区块链的伪区块链技术。 区块链所涉及的所有底层技术,不管是分布式数据库技术,加密技术,还是点对点传输技术等,基本都是早已存在没什么秘密可言的技术。 比特币系统最重要的特性是封闭性和自洽性,他验证不了任何系统自身以外产生的信息的真实性。 所谓系统自身产生的信息,就是数据库数据的变动信息,有价值的基本上有且只有交易信息。所以说比特币最初不过是中本聪一种炫技的产物,来证明自己对几种技术的掌握,你看我多牛逼,设计出了一个像三体一样的系统。因此,数字货币很有可能是区块链从始至终唯一的杀手应用。 比特币和区块链概念从诞生到今天已经快10年了,很多人说区块链技术在爆发的前夜,但这个前夜好像是不是有点过长了啊朋友,跟三体里的长夜有一拼啊。都说区块链技术像是90年代初的互联网,可是90年代初的互联网在十年发展后,已经出现了一大批伟大的公司,阿里巴巴在99年都成立了,区块链怎么除了币还是币呢? 正规的数字货币未来发展的形式无外乎几种,要么就是论坛币形式,或者类似股票的权益凭证等。问题是论坛币和股票之前,本来也都电子化了,区块链来了到底改变了什么呢? 所有想把TOKEN和应用场景结合起来的人最后都很痛苦,最后他们会发现区块链技术就是脱裤子放屁,自己辛苦搞半天,干嘛不自己作为中心关心门来收钱?最后这些人都产生了价值的虚无感,最终精神崩溃,只能发币疯狂收割韭菜,一边嘴里还说着我是个好人之类的奇怪的话。 因此,之前币圈链圈还泾渭分明,互相瞧不起,但这两年链圈逐渐坐不住了,想着是不是趁着泡沫没彻底破灭之前赶快收割一波,不然可能什么都捞不着了。 前段时间和一个名校毕业的链圈朋友瞎聊天,他说他们“致力于用区块链技术解决数字版权保护问题”,我就问他一个问题,你们如何保证你链的版权所有权声明是真实的,万一盗版者抢先一步把数据放在链上怎么办。他说他们的解决方案是连入国家数字版权保护中心的数据库进行验证…… 所以说区块链技术就是个鸡肋,研究到最后都会落入效率与真实性的黑洞,很多人一头扎进链圈后才发现,真正意义上的区块链技术,其实什么都干不了。 -02- 不是蠢就是坏的区块链媒体 空气币和区块链的造富神话,让区块链自媒体也开始迎风乱扭。一群群根本不知道区块链为何物的妖魔鬼怪纷纷进驻区块链自媒体战场,开始大放厥词胡编乱造。 任何东西,但凡只要和区块,链,分,分布式,记账,加密,验证,可追溯等等这些个关键词沾到哪怕一点点,这些所谓的区块链媒体人就会像狗闻到了屎了一样疯狂地把区块链概念往上套。 这让我想起曾经一度也是热闹非凡的物联网,我曾经去看过江苏一家号称要改变世界的“物联网”企业,过去一看是生产路由器的,我黑人问号脸,对方解释说没有路由器万物怎么互联,我觉得他说得好有道理,竟无言以对。 好,下面让我们进入奇葩共赏析时间,来看看区城链媒体经常有哪些危言耸听的奇谈怪论 区块链(分布式记账)的典型应用是*?? 正如前面所说,真正意义上的区块链分布式记账,不光包括“记”这个动作,还包括分布式存储和共识机制等。而*诞生远远早于区块链这个词的出现,勉强算是“分布式编辑”吧,就被很多区块链媒体拿来强行充当区块链技术应用的典范。 其实事实恰恰相反,*恰恰是去中心化失败的典范,现在如果没有精英和专业人士的编辑和维护,*早就没法看了。 区块链会促进社会分工?? 罗振宇好像就说过类似的话,虽然罗振宇说过很多没有逻辑的话,但这句话绝对是最没逻辑思维的。很多区块链自媒体也常常用这句话来忽悠老百姓,说分工代表效率提高社会进步,而区块链“无疑”会促进分工,他们的理由仅仅是分工和分布式记账都共用一个“分”字,就强行把他们扯到一起。 实际情况恰恰相反,区块链是逆分工的,区块链精神是号召所有人积极地参与到他不擅长也不想掺合的事情里面去。 区块链不能像上帝一样许诺他的子民死后上天国,只能给他们许诺你们是六度人脉中的第一级,我可以赚后面五级人的钱,你处于金字塔的顶端。
-
代币桶算法的原理与实现
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
探究Spring源码:十五节深入解析常用注解的运用与内部实现机制
-
实战教学:C++与CUDA实现零基础LeNet卷积神经网络,深度解析各层算法原理 - 源代码解析第四部分
-
玩转三维图形:变换与投影的奥秘 - 重要原理与核心算法解析
-
玩转微波射频:LC谐振的实证与LC滤波器的工作原理解析
-
深入解析jmap命令的工作原理与实现机制
-
实战讲解:热力图与相关系数图的数据可视化——基于Python的实现与原理解析(第二部分:代码实操)