双边滤波
双边滤波器也是一种保边滤波器.和导向图滤波器一样,可以达到在平坦区域进行均值(高斯)滤波的效果,在边缘不进行滤波的效果.其原理为一个与空间距离相关的高斯函数与一个灰度距离相关的高斯函数相乘.
其中空间距离指的是当前点与中心点的欧式距离。空间域高斯函数其数学形式为:
(xi,yi)为当前点位置,(xc,yc)为中心点的位置,sigma为空间域标准差 .
灰度距离指的是当前点灰度(像素点值)与中心点灰度的差的绝对值。值域高斯函数其数学形式为
其中gray(xi,yi)为当前点灰度值,gray(xc,yc)为中心点灰度值,sigma为值域标准差 .
对于高斯滤波,仅用空间距离的权值系数核与图像卷积后,确定中心点的灰度值。即认为离中心点越近的点,其权重系数越大。 双边滤波中加入了对灰度信息的权重,即在邻域内,灰度值越接近中心点灰度值的点的权重更大,灰度值相差大的点权重越小。此权重大小,则由值域高斯函数确定。两者权重系数相乘,得到最终的卷积模板。由于双边滤波需要每个中心点邻域的灰度信息来确定其系数,所以其速度与比一般的滤波慢很多,而且计算量增长速度为核大小的平方.
由该图可以看出,和导向图滤波相比,其达到的目的是一致的.
sigma的选取
空间域
对于空间域,可以理解成为高斯滤波.由此可以参考一下高斯滤波中sigma参数的影响,总之就是sigma越大,越偏向于均值滤波.但是其值的选取通常按照如下的方式:其中核大小通常为sigma的6*sigma + 1。因为离中心点3*sigma大小之外的系数与中点的系数只比非常小,可以认为此之外的点与中心点没有任何联系,及权重系数为0. OpenCV中默认的计算公式也是如此,OpenCV参考文档内容如下:“对应高斯参数的 Gaussian sigma (标准差). 如果为零,则标准差由下面的核尺寸计算: sigma = (n/2 - 1)*0.3 + 0.8, 其中 n=param1 对应水平核,n=param2对应垂直核.”
灰度值域
灰度差△g = abs(gray(xi,yi)- gray(xc,yc)),忽略常数的影响,因此其函数可以简化为:
由的图像可知
已知 0≤△g≤255;
1)假设sigma = 255,当△g = 255时,系数为exp(-1) = 0.3679,当△g = 0时,系数为exp(-0)= 1.灰度最大点的系数与相差最小的灰度值系数之比为 0.3679.
2)假设sigma = 122.5,当△g = 255时,系数为exp(-4) = 0.0183,当△g = 0时,系数为exp(-0)= 1.灰度差最大点的系数与相差最小的灰度值系数之比为 0.0183.
结论:因为导数为
,其增长速度为指数增长。
当simga较大时,灰度差最大值与最小值的系数在很小的一个范围之内,其比值较大。及灰度差较大的点,对于中心点也会有相应的较大的权值,此与双边滤波的保留边缘的初衷相违背。
当sigma较小时,灰度差最大值与最小值的系数在较大的一个范围之内,其比值很小,及灰度差较大的点,对应中心点仅有很小的权重,此时达到了保留边缘的目的
综合下来,都可以发现,sigma越大,无论是空间域还是灰度值域,其越偏向于均值滤波(模糊图片),极限情况为simga无穷大,值域系数近似相等(无穷大后,灰度值相差再大,在无穷大面前仍是变化不大的值,可一直该区域平坦.);Sigma越小,边缘越清晰,极限情况为simga无限接近0,值域系数近似相等(接近exp(-∞) = 0),与高斯模板(空间域模板)相乘后,可近似为系数皆相等,等效于源图像.
C代码实现部分引用并简化了一下这里的代码
void myBilateralFilter(const Mat &src, Mat &dst, int ksize, double space_sigma, double color_sigma)
{
int channels = src.channels();
CV_Assert(channels == 1 || channels == 3);
double space_coeff = -0.5 / (space_sigma * space_sigma);
double color_coeff = -0.5 / (color_sigma * color_sigma);
int radius = ksize / 2;
Mat temp;
copyMakeBorder(src, temp, radius, radius, radius, radius, BorderTypes::BORDER_REFLECT);
vector<double> _color_weight(channels * 256); // 存放差值的平方
vector<double> _space_weight(ksize * ksize); // 空间模板系数
vector<int> _space_ofs(ksize * ksize); // 模板窗口的坐标
double *color_weight = &_color_weight[0];
double *space_weight = &_space_weight[0];
int *space_ofs = &_space_ofs[0];
// 使用查表的方式计算灰度值模板系数
for (int i = 0; i < channels * 256; i++)
color_weight[i] = exp(i * i * color_coeff);
// 生成空间模板,距离的模板是二维的,这里使用的方法就i比较巧妙,将其化为了一维
int maxk = 0;
for (int i = -radius; i <= radius; i++)
{
for (int j = -radius; j <= radius; j++)
{
double r = sqrt(i*i + j * j);
if (r > radius)
continue;
space_weight[maxk] = exp(r * r * space_coeff); // 存放模板系数
space_ofs[maxk++] = i * temp.step + j * channels; // 存放模板的位置,和模板系数相对应
}
}
// 滤波过程
for (int i = 0; i < src.rows; i++)
{
const uchar *sptr = temp.data + (i + radius) * temp.step + radius * channels;
uchar *dptr = dst.data + i * dst.step;
if (channels == 1)
{
for (int j = 0; j < src.cols; j++)
{
double sum = 0, wsum = 0;
int val0 = sptr[j]; // 模板中心位置的像素
for (int k = 0; k < maxk; k++)
{
int val = sptr[j + space_ofs[k]];
double w = space_weight[k] * color_weight[abs(val - val0)]; // 模板系数 = 空间系数 * 灰度值系数
sum += val * w;
wsum += w;
}
dptr[j] = (uchar)cvRound(sum / wsum);
}
}
else if (channels == 3)
{
for (int j = 0; j < src.cols * 3; j+=3)
{
double sum_b = 0, sum_g = 0, sum_r = 0, wsum = 0;
int b0 = sptr[j];
int g0 = sptr[j + 1];
int r0 = sptr[j + 2];
for (int k = 0; k < maxk; k++)
{
const uchar *sptr_k = sptr + j + space_ofs[k];
int b = sptr_k[0];
int g = sptr_k[1];
int r = sptr_k[2];
double w = space_weight[k] * color_weight[abs(b - b0) + abs(g - g0) + abs(r - r0)];
sum_b += b * w;
sum_g += g * w;
sum_r += r * w;
wsum += w;
}
wsum = 1.0f / wsum;
b0 = cvRound(sum_b * wsum);
g0 = cvRound(sum_g * wsum);
r0 = cvRound(sum_r * wsum);
dptr[j] = (uchar)b0;
dptr[j + 1] = (uchar)g0;
dptr[j + 2] = (uchar)r0;
}
}
}
}
需要注意图像像素值的获取,首先获取到每行的坐标指针
const uchar *sptr = temp.data + (i + radius) * temp.step + radius * channels;
uchar *dptr = dst.data + i * dst.step;
在滤波循环中,从space_ofs
中取出每个模板位置偏移地址
int val = sptr[j + space_ofs[k]];
这种实现方法,大大的降低滤波的时间复杂度。
实现的结果和OpenCV的实现相差无几。sigma = 80,模板大小为20.
- 联合双边网络
双边滤波可以进行保边滤波,但是其权值不太稳定,容易在边缘出现问题.因此引入了联合双边滤波增加其稳定性以及拓展其方法的应用,使其不只是局限于滤波,也可以对图像进行一些增强的操作.可以参考这里来区分双边滤波的不同
联合双边滤波说的直白一点就是引入一张图,在进行值灰度值域计算的时候,在滤波窗口下使用该引导图替代原图.该引导图可以是原图用被平滑过后的图像,也可以是先下采样后上采样回来的图像.总之其实现目的就是在进行灰度值域计算的时候,梯度没有那么大.因此也可以看出,这里没有涉及到加速的问题.
用公式进行简单的区别可以表达如下:
BF(bilateral filters):
JBF(joint bilateral filters):
f为the spatial filter kernel,就想高斯kernel,是以p为中心。
g 为the range filter kernel,图像中心的像素值为Ip.
Ω is the spatial support of the kernel f
kp is a normalizing factor, the sum of the f · g filter weights
kp为一个window中的所有的f与g相乘的值
I 冒即为联合双边滤波的引导图
有些地方可能会看到cross-bilateral filter,这其实就是joint bilateral filters联合双边滤波另外一种叫法
双边网格
双边网格是从双边滤波一步步发展到现在的,而且是一个团队的人不短的优化才达到目前的状态,首先是通过双边滤波的快速算法的思想,大致通过以下三篇论文:
A Fast Approximation of the Bilateral Filter using a Signal Processing Approach
Real-time Edge-Aware Image Processing with the Bilateral Grid
Bilateral Guided Upsampling
首先一定要吃透第一篇文章的思想,想要详细了解其步骤最终还是要回归到代码,因为有些话语,特别是英文翻译,理解起来可能不是你设想的那样,特别是一种新的算法思想.
A Fast Approximation of the Bilateral Filter using a Signal Processing Approach
这里我详细介绍一下这篇论文中所用到的创新思想,即二维图像非线性卷积变成了三维的线性卷积操作.参考了好多博客,几乎都是对原作者论文进行翻译,然后贴上相应的公式.很少有人举例说明.特别是为什么二维变成三维以后,卷积核成为3D卷积核后,双边滤波中的值域信息就没有了.
我们首先对论文中的Fig.1进行解释.作者是对一维的数据进行升维,由一维卷积变成二维卷积.
这张图应该是比较容易理解的,本来一维度的数据,比如说(近似图上的数据):y=[0.23,0.22,0.23,0.23,0.21,....,0.23,0.80,0.78,0.80,......].升了维度变成2D后,那么变成上图以后就成了传统的x,y坐标图.即x:y=[0:0.23,1:0.22,2:0.23,3:0.23,4:0.21,....,32:0.23,33:0.80,34:0.78,35:0.80,......].对于卷积,就是权重求和/权重和.对于一维,就是自身像素值左右两边像素值的权重求和.二维的即使周边的.
上图的小框即可当成卷积的过程.只是红框的地方都是零,结果也为零.但是绿色框的卷积部分,表示着值的变化不大,那么这里就可以当成是普通的高斯滤波即可.但是在两个蓝色框,如果是普通的一维卷积,我们这里按图上的坐标,该值大概为0.23和0.80.那么按照普通的高斯滤波或者均值滤波,滤波后的值大概在0.5左右.但是按照我们保边滤波的策略,边缘部分信息是要保留的.由于我们提升了维度,这里滤波结果是由二维卷积确定的,然后由于第32个和33个值相差过大,在二维图像上就变成了'距离过大'(上图两个蓝框), 下上两个蓝框卷积核覆盖下的'有效值'没有了关联,用比较高大上的文字描述就是:超过了高斯衰减范围.在第一个蓝框确定的卷积值大概在0.23左右,而上面蓝框确定的值在0.8左右.因此,实现了边缘两边独自进行滤波,达到了保边作用.
如何定义边缘的大小阈值呢,那就是卷积核的大小,如果卷积核过大,上下两个卷积核有了重叠,则表示第32个值和第33个值被同一卷积核覆盖.如果该卷积核是均值滤波影响是相当大的,如果是高斯滤波,那么也同样获取了所谓的'空间'距离(由值域转换,相邻两个值差距越大,在升维度后距离也就越远),这样分配的权重不一样,更加侧重中心.总之,维度变成二维后,二维卷积核就可以直接是普通的高斯滤波(但就剩下空间距离,所谓的值域也变成了空间上的距离,即图中的上下两个蓝框).
论文中也对此进行了标注.黑色部分为0.同样,如果两维度图片进行升维度变成三维,那么卷积操作成了3D卷积.把升维后的图像想象成连绵起伏的山,高度就是像素值.滤波过程就是一个正方体'掠过'整个空间,当掠过'山面'时(注意是山面,山内部不算,'掠过'没意义)才真正的有意义. 在'悬崖'部分(像素值变化大),悬崖上下的山面,一个正方体无法包含.因此,上下部分独立,悬崖上下的山面各自进行的'高斯滤波'无任何关联(超过高斯衰减范围)
上述部分我也只是通过自身的理解进行拟物化描述.可能存在不太好的表达.总之想表达的就是:你会发现值域信息在升维度以后变成了空间信息,原来的双边滤波=值域信息+空间信息,现在都变成了空间信息.当然卷积过程也进行了升维. 因此,卷积核的值不再变化.这样就为加速提供了可能性.
一般图像越小,计算量越小.如果把图像缩小后进行滤波,然后在放大.这样会使整个的图像模糊,因为在放大过程,边缘部分实际上是一种平滑操作.但是我们在放大过程中往往忽略了一直存在的信息,那就是未进行滤波的原图.单一的考虑保边滤波,我们的目的是边缘信息保留下来,平坦区域进行滤波.因此,可以在滤波后的缩小图进行放大时,把原图的信息也参考进去.
该篇论文作者也从未对原图进行滤波处理,而是进行了缩图后进行插值放大.具体可以参考作者公布的代码.
作者之一公布的matlab的代码,我们也可以提前在他的个人主页上找到关于双边网格算法的一些信息.
%
% output = bilateralFilter( data, edge, ...
% edgeMin, edgeMax, ...
% sigmaSpatial, sigmaRange, ...
% samplingSpatial, samplingRange )
%
% Bilateral and Cross-Bilateral Filter using the Bilateral Grid.
%
% Bilaterally filters the image 'data' using the edges in the image 'edge'.
% If 'data' == 'edge', then it the standard bilateral filter.
% Otherwise, it is the 'cross' or 'joint' bilateral filter.
% For convenience, you can also pass in [] for 'edge' for the normal
% bilateral filter.
%
% Note that for the cross bilateral filter, data does not need to be
% defined everywhere. Undefined values can be set to 'NaN'. However, edge
% *does* need to be defined everywhere.
%
% data and edge should be of the greyscale, double-precision floating point
% matrices of the same size (i.e. they should be [ height x width ])
%
% data is the only required argument
%
% edgeMin and edgeMax specifies the min and max values of 'edge' (or 'data'
% for the normal bilateral filter) and is useful when the input is in a
% range that's not between 0 and 1. For instance, if you are filtering the
% L channel of an image that ranges between 0 and 100, set edgeMin to 0 and
% edgeMax to 100.
%
% edgeMin defaults to min( edge( : ) ) and edgeMax defaults to max( edge( : ) ).
% This is probably *not* what you want, since the input may not span the
% entire range.
%
% sigmaSpatial and sigmaRange specifies the standard deviation of the space
% and range gaussians, respectively.
% sigmaSpatial defaults to min( width, height ) / 16
% sigmaRange defaults to ( edgeMax - edgeMin ) / 10.
%
% samplingSpatial and samplingRange specifies the amount of downsampling
% used for the approximation. Higher values use less memory but are also
% less accurate. The default and recommended values are:
%
% samplingSpatial = sigmaSpatial
% samplingRange = sigmaRange
%
function output = bilateralFilter( data, edge, edgeMin, edgeMax, sigmaSpatial, sigmaRange, ...
samplingSpatial, samplingRange )
if( ndims( data ) > 2 ),
error( 'data must be a greyscale image with size [ height, width ]' );
end
if( ~isa( data, 'double' ) ),
error( 'data must be of class "double"' );
end
if ~exist( 'edge', 'var' ),
edge = data;
elseif isempty( edge ),
edge = data;
end
if( ndims( edge ) > 2 ),
error( 'edge must be a greyscale image with size [ height, width ]' );
end
if( ~isa( edge, 'double' ) ),
error( 'edge must be of class "double"' );
end
inputHeight = size( data, 1 );
inputWidth = size( data, 2 );
if ~exist( 'edgeMin', 'var' ),
edgeMin = min( edge( : ) );
warning( 'edgeMin not set! Defaulting to: %f\n', edgeMin );
end
if ~exist( 'edgeMax', 'var' ),
edgeMax = max( edge( : ) );
warning( 'edgeMax not set! Defaulting to: %f\n', edgeMax );
end
edgeDelta = edgeMax - edgeMin;
if ~exist( 'sigmaSpatial', 'var' ),
sigmaSpatial = min( inputWidth, inputHeight ) / 16;
fprintf( 'Using default sigmaSpatial of: %f\n', sigmaSpatial );
end
if ~exist( 'sigmaRange', 'var' ),
sigmaRange = 0.1 * edgeDelta;
fprintf( 'Using default sigmaRange of: %f\n', sigmaRange );
end
if ~exist( 'samplingSpatial', 'var' ),
samplingSpatial = sigmaSpatial;
end
if ~exist( 'samplingRange', 'var' ),
samplingRange = sigmaRange;
end
if size( data ) ~= size( edge ),
error( 'data and edge must be of the same size' );
end
% parameters
derivedSigmaSpatial = sigmaSpatial / samplingSpatial;
derivedSigmaRange = sigmaRange / samplingRange;
paddingXY = floor( 2 * derivedSigmaSpatial ) + 1;
paddingZ = floor( 2 * derivedSigmaRange ) + 1;
% allocate 3D grid
downsampledWidth = floor( ( inputWidth - 1 ) / samplingSpatial ) + 1 + 2 * paddingXY;
downsampledHeight = floor( ( inputHeight - 1 ) / samplingSpatial ) + 1 + 2 * paddingXY;
downsampledDepth = floor( edgeDelta / samplingRange ) + 1 + 2 * paddingZ;
gridData = zeros( downsampledHeight, downsampledWidth, downsampledDepth );
gridWeights = zeros( downsampledHeight, downsampledWidth, downsampledDepth );
% compute downsampled indices
[ jj, ii ] = meshgrid( 0 : inputWidth - 1, 0 : inputHeight - 1 );
% ii =
% 0 0 0 0 0
% 1 1 1 1 1
% 2 2 2 2 2
% jj =
% 0 1 2 3 4
% 0 1 2 3 4
% 0 1 2 3 4
% so when iterating over ii( k ), jj( k )
% get: ( 0, 0 ), ( 1, 0 ), ( 2, 0 ), ... (down columns first)
di = round( ii / samplingSpatial ) + paddingXY + 1;
dj = round( jj / samplingSpatial ) + paddingXY + 1;
dz = round( ( edge - edgeMin ) / samplingRange ) + paddingZ + 1;
% perform scatter (there's probably a faster way than this)
% normally would do downsampledWeights( di, dj, dk ) = 1, but we have to
% perform a summation to do box downsampling
for k = 1 : numel( dz ),
dataZ = data( k ); % traverses the image column wise, same as di( k )
if ~isnan( dataZ ),
dik = di( k );
djk = dj( k );
dzk = dz( k );
gridData( dik, djk, dzk ) = gridData( dik, djk, dzk ) + dataZ;
gridWeights( dik, djk, dzk ) = gridWeights( dik, djk, dzk ) + 1;
end
end
% make gaussian kernel
kernelWidth = 2 * derivedSigmaSpatial + 1;
kernelHeight = kernelWidth;
kernelDepth = 2 * derivedSigmaRange + 1;
halfKernelWidth = floor( kernelWidth / 2 );
halfKernelHeight = floor( kernelHeight / 2 );
halfKernelDepth = floor( kernelDepth / 2 );
[gridX, gridY, gridZ] = meshgrid( 0 : kernelWidth - 1, 0 : kernelHeight - 1, 0 : kernelDepth - 1 );
gridX = gridX - halfKernelWidth;
gridY = gridY - halfKernelHeight;
gridZ = gridZ - halfKernelDepth;
gridRSquared = ( gridX .* gridX + gridY .* gridY ) / ( derivedSigmaSpatial * derivedSigmaSpatial ) + ( gridZ .* gridZ ) / ( derivedSigmaRange * derivedSigmaRange );
kernel = exp( -0.5 * gridRSquared );
% convolve
blurredGridData = convn( gridData, kernel, 'same' );
blurredGridWeights = convn( gridWeights, kernel, 'same' );
% divide
blurredGridWeights( blurredGridWeights == 0 ) = -2; % avoid divide by 0, won't read there anyway
normalizedBlurredGrid = blurredGridData ./ blurredGridWeights;
normalizedBlurredGrid( blurredGridWeights < -1 ) = 0; % put 0s where it's undefined
% for debugging
% blurredGridWeights( blurredGridWeights < -1 ) = 0; % put zeros back
% upsample
[ jj, ii ] = meshgrid( 0 : inputWidth - 1, 0 : inputHeight - 1 ); % meshgrid does x, then y, so output arguments need to be reversed
% no rounding
di = ( ii / samplingSpatial ) + paddingXY + 1;
dj = ( jj / samplingSpatial ) + paddingXY + 1;
dz = ( edge - edgeMin ) / samplingRange + paddingZ + 1;
% interpn takes rows, then cols, etc
% i.e. size(v,1), then size(v,2), ...
output = interpn( normalizedBlurredGrid, di, dj, dz );
另外,还有人通过上诉代码,修改成了Python代码,并且对论文进行了详细中文阐述(此人其他博客写的也想当不错哦,都配有代码).但是还是感觉英文原版论文解释的比较详细.
import numpy as np
import math
import scipy.signal, scipy.interpolate
import cv2
def bilateral_approximation(image, sigmaS, sigmaR, samplingS=None, samplingR=None):
# It is derived from Jiawen Chen's matlab implementation
# The original papers and matlab code are available at http://people.csail.mit.edu/sparis/bf/
# --------------- 原始分辨率 --------------- #
inputHeight = image.shape[0]
inputWidth = image.shape[1]
sigmaS = sigmaS
sigmaR = sigmaR
samplingS = sigmaS if (samplingS is None) else samplingS
samplingR = sigmaR if (samplingR is None) else samplingR
edgeMax = np.amax(image)
edgeMin = np.amin(image)
edgeDelta = edgeMax - edgeMin
# --------------- 下采样 --------------- #
derivedSigmaS = sigmaS / samplingS
derivedSigmaR = sigmaR / samplingR
paddingXY = math.floor(2 * derivedSigmaS) + 1
paddingZ = math.floor(2 * derivedSigmaR) + 1
downsampledWidth = int(round((inputWidth - 1) / samplingS) + 1 + 2 * paddingXY)
downsampledHeight = int(round((inputHeight - 1) / samplingS) + 1 + 2 * paddingXY)
downsampledDepth = int(round(edgeDelta / samplingR) + 1 + 2 * paddingZ)
wi = np.zeros((downsampledHeight, downsampledWidth, downsampledDepth))
w = np.zeros((downsampledHeight, downsampledWidth, downsampledDepth))
# sigmaS=64, sigmaR=32, samplingS=32, samplingR=16
# 下采样索引
(ygrid, xgrid) = np.meshgrid(range(inputWidth), range(inputHeight))
dimx = np.around(xgrid / samplingS) + paddingXY
dimy = np.around(ygrid / samplingS) + paddingXY
dimz = np.around((image - edgeMin) / samplingR) + paddingZ
flat_image = image.flatten()
flatx = dimx.flatten()
flaty = dimy.flatten()
flatz = dimz.flatten()
# 盒式滤波器(平均下采样)
for k in range(dimz.size):
image_k = flat_image[k]
dimx_k = int(flatx[k])
dimy_k = int(flaty[k])
dimz_k = int(flatz[k])
wi[dimx_k, dimy_k, dimz_k] += image_k
w[dimx_k, dimy_k, dimz_k] += 1
# --------------- 三维卷积 --------------- # # 生成卷积核
kernelWidth = 2 * derivedSigmaS + 1
kernelHeight = kernelWidth
kernelDepth = 2 * derivedSigmaR + 1
halfKernelWidth = math.floor(kernelWidth / 2)
halfKernelHeight = math.floor(kernelHeight / 2)
halfKernelDepth = math.floor(kernelDepth / 2)
(gridX, gridY, gridZ) = np.meshgrid(range(int(kernelWidth)), range(int(kernelHeight)), range(int(kernelDepth)))
# 平移,使得中心为0
gridX -= halfKernelWidth
gridY -= halfKernelHeight
gridZ -= halfKernelDepth
gridRSquared = ((gridX * gridX + gridY * gridY) / (derivedSigmaS * derivedSigmaS)) + \
((gridZ * gridZ) / (derivedSigmaR * derivedSigmaR))
kernel = np.exp(-0.5 * gridRSquared)
# 卷积
blurredGridData = scipy.signal.fftconvolve(wi, kernel, mode='same')
blurredGridWeights = scipy.signal.fftconvolve(w, kernel, mode='same')
# --------------- divide --------------- #
blurredGridWeights = np.where(blurredGridWeights == 0, -2, blurredGridWeights)
# avoid divide by 0, won't read there anyway
normalizedBlurredGrid = blurredGridData / blurredGridWeights
normalizedBlurredGrid = np.where(blurredGridWeights < -1, 0, normalizedBlurredGrid) # put 0s where it's undefined
# --------------- 上采样 --------------- #
(ygrid, xgrid) = np.meshgrid(range(inputWidth), range(inputHeight))
# 上采样索引
dimx = (xgrid / samplingS) + paddingXY
dimy = (ygrid / samplingS) + paddingXY
dimz = (image - edgeMin) / samplingR + paddingZ
out_image = scipy.interpolate.interpn((range(normalizedBlurredGrid.shape[0]),
range(normalizedBlurredGrid.shape[1]),
range(normalizedBlurredGrid.shape[2])),
normalizedBlurredGrid,
(dimx, dimy, dimz))
return out_image
if __name__ == "__main__":
image = cv2.imread('image.png', 0)
mean_image = bilateral_approximation(image, sigmaS=64, sigmaR=32, samplingS=32, samplingR=16)
mean_image = np.around(mean_image).astype(np.uint8)
print(image)
print(mean_image)
cv2.imshow('ori', image)
cv2.imshow('filter',mean_image)
cv2.waitKey(0)
以上的代码,无论是matlab或者Python,都应该是非常容易理解的.但是一些变量的维度是应当注意一些的,以Python为例,在下采样索引时,dimx,dimy,dimz的维度依然是图片的长宽(比如说图片是512X512的尺寸).只是里面的值改变了. 另外在上采样维度坐标信息生成时也是同样需要注意的是,这里的dimz是没有round函数进行四舍五入的,因此dimz=(image - edgeMin) / samplingR + paddingZ,由于没有进行round函数操作,这里是保留了原图像的全部信息的,只是相当于进行了一个线性变换.因此,实现了在滤波后的缩小图进行放大时,把原图的信息也参考进去. 整个部分的难点或许在于scipy.interpolate.interpn的用法,可以参考一下这里
Real-time Edge-Aware Image Processing with the Bilateral Grid
该篇论文感觉就是对上面那篇论文的详细解释以及用法的普及,为了让读者明白,这种增维型的数据结构除保边滤波外还可以有更多的工作.文中主要对上篇文章中的Fast Approximation,也就是增加维度后进行缩放加速的过程从数据结构方面进行阐述.将图像增加维度后变成了双边网格结构的拟物化描述,所谓的网格,感觉就是3D图像在各个维度方向进行缩放,可以看成将某个维度砍成几截,三个维度加起来就是块,块内的值进行了求和,这在上面的程序中有所体现.那么每一块就好像是网格一样.
对于增维,我上面的解释如果过于白话,那么我将论文中对双边网格的解释进行翻译.主要是2.1小节中的 A Simple Illustrator.
文中将增维度的过程比喻成一只用于滤波/平滑的笔刷,当你用这只笔刷在图像E 上的某一个位置( x , y )处点击了一下,对应的,3D双边网格的(x,y,E(x,y)位置将出现一个点,这个点即对应你在2D影像E上点击的那个点.并在边缘部分有了感知.可以想象一下悬崖,走到边缘部分画笔在山面上被限制.
edge-aware brush is similar to brushes offered by traditional editing packages except that when the user clicks near an edge, the brush does not paint across the edge :边缘感知笔刷类似于传统编辑软件包提供的画笔,当用户在边缘附近单击时,笔刷不会越过边缘绘制.
The falloff along the two spatial dimensions is the same as in classical 2D brushes, but the range falloff is specific to our brush and ensures that only a limited interval of intensity values is affected:沿两个空间维度的衰减与经典2D笔刷中的衰减相同,但范围衰减是由笔刷指定的,并确保仅影响有限的强度值间隔
这里由专业术语说出的感觉就是显得高大上,即衰减范围就是上篇文章中,用方块的大小来描述.影响的有限强度,我用悬崖的高度来决定等.
In regions where the image E is nearly constant, the edge-aware brush behaves like a classical brush with a smooth spatial falloff. Since the range variations are small, the range falloff has little influence. At edges, E is discontinuous and if only one side has been painted, the range falloff ensures that the other side is unaffected, thereby creating a discontinuity in the painted values V:在图像E的平坦区域中,边缘感知笔刷的表现类似于具有平滑空间衰减的经典笔刷(其实就是高斯滤波)。由于距离变化很小,所以范围衰减的影响很小(这里是rang,可以想想成是图像上的像素值变化范围)。在边缘部分,E是不连续的,如果只绘制了一侧,则范围衰减确保另一侧不受影响,因此,在绘制值V中产生了不连续性.
Although the grid values are smooth, the output value map is piecewise-smooth and respects the strong edges of E because we slice according to E:虽然网格值是平滑的,但是输出值映射是分段平滑的,因为我们根据图像进行切片并且考虑到了图像的强边缘.
这段话的翻译可以这样理解,还是想象成山脉,这个slice切片操作完全可以看成是悬崖的横切面.在这里进行了分段.一段内的数值可以平滑,但是在悬崖这里进行了截断,称之为分段平滑.
除了滤波外,可以用在直方图均衡化,图像的重新绘色等等
Bilateral Guided Upsampling
这篇论文主要是受到了何凯明的快速导向图滤波和联合双边上采样两篇文章的影响.将双边网格算法中的网格的作用拓展.让网格不在只是像素值的表达,而是每个网格作为图片与图片之间的仿射变换的参数.这种对不同区域拥有不同的仿射变换参数特别适合图像增强或者风格转变之类的图像处理.这是因为在这样一类的工作中,在空间与值域相近的区域内,相似输入图像的亮度经仿射变换算子变换后也应该是相似的.并且继承了上面两篇论文的加速思想,使用小分辨率来进行放射变换参数的.
在求解过程中,使用的是坐标和亮度信息取整映射到网格里面,并且每个网格里面的仿射变换使用的是一个3X4的矩阵.
This method works by fitting a 2D array of affine functions between overlapping patches in a guide image and an input image,These affine functions are then applied to the input image
We instead fit a 3D array of affine functions on the image's bilateral-grid representation, meaning that within a single patch, we fit different affine functions per intensity range
The 3D array can be treated as a type of bilateral grid that stores affine models instead of colors
上面两段的英文大概意思就是相比于导向图滤波的快速算法.本文用的算法更加精细话,原来在一个patch下进行放射变换,但是本文在此基础上,每层的像素强度(最终体现在网格)也会有独立的仿射变换参数.
HDRNET
使用神经网络和双边网格算法来进行图像的增强工作,也是结合了神网计算量大但是效果好,双边网格可以加速的优点.
Deep Bilateral Learning for Real-Time Image Enhancement 是这两种算法结合的代表论文.也是由上面几篇论文一步步延伸过来的.和第一篇论文相比,HDRNET是将CNN后的特征图(特征图中融合了局部和全局特征)当做双边网格,双边网格中每个网格为一个仿射变换矩阵(3X4).特征图变成双边网格的目的就在于可以用小分辨率作为输入来进行加速.另外提一句,原图和双边网格进行上采样的过程,称为slicing.在传统的双边网格中,形成初步网格的过程叫做splat,3D滤波的过程可以看过是一个模糊操作:blur
论文的具体步骤就不在详细论述.因为涉及到的只是将两种算法思想相结合,并没有新的单一技术创新.这里提供一下github上关于该论文的代码.谷歌出品或者高星,但是这两个项目的双边网格部分都是C语言实现的库进行链接的.纯tensorflow版本写的双边网格,以及整个项目都是tensorflow代码的地址。
无论用到上面的哪个项目,在运行时你可能会遇到输出图片不正常,训练时无法收敛的问题。这主要是由于tensorflow的tf.matmul的bug引起的,而且如此重大的bug居然等到了tensorflow2.1以后才修复,也有可能意味着tensorflow1.0版本都存在着该bug。但是也有可能是GPU型号以及cuda等引起的,毕竟原始作者训练出来的模型还是比较好的。暂时没有找到更好的解决办法。即使只有三维的情况下,仍然会出现问题。在不升级tensorflow的情况下,规避的方法目前只有强制让matmul在CPU上执行。
with tf.device('/cpu:0'): out_img = tf.matmul(coef, in_img)