【ISP图像处理】Tone Mapping基础知识及相关算法(附代码)_durand tone mapping

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注运维)
img

正文

在Tone Mapping中,还有一种最直观的映射曲线,即“S”曲线。如图所示:x曲线是输入高动态范围像素(一般会先转换到log域),y是映射后输出的低动态范围像素,高亮暗部像素都向中间压缩,即实现抑制过曝提亮暗部的目的。

Uncharted 2人工计算一条S曲线进行拟合,得到一个多项式曲线的表达式。这样的表达式应用到渲染结果后,就能在很大程度上自动接近人工调整的结果。

代码如下:

vec3 uncharted2_tonemap_partial(vec3 x)
{
    float A = 0.15f;
    float B = 0.50f;
    float C = 0.10f;
    float D = 0.20f;
    float E = 0.02f;
    float F = 0.30f;
    return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}

vec3 uncharted2_filmic(vec3 v)
{
    float exposure_bias = 2.0f;
    vec3 curr = uncharted2_tonemap_partial(v * exposure_bias);

    vec3 W = vec3(11.2f);
    vec3 white_scale = vec3(1.0f) / uncharted2_tonemap_partial(W);
    return curr * white_scale;
}
(3)ACES Filmic Tone Mapping Curve [Blog]

类似Uncharted 2使用一个多项式进行拟合,ACES提出的多项式则更加简单,S曲线如图所示:

可以看到,亮部和暗部区域像素被压缩范围更加均衡,应用中色彩对比度还原会更加优异。

代码如下:

float3 ACESToneMapping(float3 color, float adapted_lum)
{
	const float A = 2.51f;
	const float B = 0.03f;
	const float C = 2.43f;
	const float D = 0.59f;
	const float E = 0.14f;

	color *= adapted_lum;
	return (color * (A * color + B)) / (color * (C * color + D) + E);
}

方法效果可以参考另一篇博文

(4)直方图均衡 [PDF]

图像直方图描述了一张图片像素值分布的情况,我们知道,例如对于一张sRGB图像,我们可以看作是一个H*W*1的矩阵,矩阵中每个元素的取值范围为[0,255],图像直方图h(g)定义为像素值为g的元素的个数。如图所示,原始HDR图像大部分像素都是偏暗,因此其对应直方图像素值也集中分布在值较小的区间。经过直方图均衡后,图像中个数多的灰度级进行展宽,而对像素个数少的灰度级进行缩减,从而达到提升暗部,抑制高亮区域,清晰图像的目的。

实现代码如下:

using namespace cv;
using namespace std;

int main()
{ 
	Mat image, image_gray, image_enhanced;   //定义输入图像,灰度图像, 直方图
	image = imread("lena.png");  //读取图像;
	if (image.empty())
	{
		cout << "读取错误" << endl;
		return -1;
	}

	cvtColor(image, image_gray, COLOR_BGR2GRAY);  //灰度化
	imshow(" image_gray", image_gray);   //显示灰度图像

	equalizeHist(image_gray, image_enhanced);//直方图均衡化
	imshow(" image_enhanced", image_enhanced);   //显示增强图像

	waitKey(0);  //暂停,保持图像显示,等待按键结束
	return 0;
}

2.2 Local Tone Mapping(局部色调映射,LTM)

在真实场景中,仅靠GTM增强图像会产生局部区域过度/不足增强。LTM提供更细粒度的控制,并有助于突出重点区域细节。

(1)Photographic Tone Reproduction for Digital Images [PDF]

这篇论文除了提出了一个GTM算子,同时还提出了一个LTM算子。作者认为仅仅依赖GTM算子并不足以应对动态范围极广的图像,比如,强光反射区域中的小面积纹理细节容易在被压缩过程中丢失。因此,作者进一步结合dodging-and-burning方法,提出了一个LTM算子,希望做颜色映射时进一步保持对比度。

首先介绍dodging-and-burning方法,该方法的核心思想是对于每一个像素点都考察不同尺度的邻域,以找到使得对比度没有明显突变的最大邻域。如下面示意图所示,尺度过小和过大时,中心区域和环绕区域间的对比度都比较低,而在正确的尺度上,中心区域与环绕区域的对比度就比较大。不同尺度的运用其实是通过使用不同尺度的高斯滤波器对图像滤波来实现的,中心区域与环绕区域也是半径具有一定比例的两个邻域。记中心区域的尺度为s1,环绕区域的对比度为s2,对于每个像素点,比较两个尺度下的滤波结果V1和V2,设置一个可以利用正确尺度下V1和V2的高对比度性质的公式进行尺度甄选即可。

具体计算过程,首先采用高斯滤波器对图像进行卷积:

其中,\alpha_ii有1和2两个取值,分别对应中心区域与环绕区域的尺度缩放系数,论文中取值为\alpha_1=1/2\sqrt{2},\alpha_2=1.6\alpha_1。在获得了每个尺度的V_1V_2后,根据中心环绕函数计算一个对比度指标:

其中\phi默认取值为8.0。分母中前面这一项是为了防止V1趋于0时V取值过大。

如果V1和V2的值接近,则说明这一片区域没有变化强烈的的内容,即对比度低,计算出的V值也趋近于0;反之则V越大。考虑局部区域原则上是给定像素周围没有大对比度变化的最大区域,所以我们需要找到一个大下适中的滤波核,不会因半径过小没有领域信息卷积后变化不明显,也不会因半径过大在高亮区域周围形成暗环。

通过设置阈值,第一个对比度指标大于阈值的小半径滤波核尺度即为所需的卷积半径:

\left | V(x,y,s_m) \right |> \varepsilon

确定该高斯滤波核之后,映射公式可表达为:

由公式可知,在相对明亮的区域,暗像素的亮度会满足L>V1,因此该算子会降低显示亮度Ld,从而增加该像素处的对比度,同样的,一个像素在一个相对较暗的区域将被压缩较少,像素相对于周围区域的对比度增加。

python代码如下:

def reinhard_local(img,eps,refV):
    gamma = 1 / 1.6
    picNum = 2
    ratio = 2.7
    # refV = 1.0
    phi = 8.0   # 8.0
    # eps = 0.05
    alpha = 1 / np.sqrt(8)
    epsilon = 0.00001
    a = 0.54

    # L = 0.27 * img[:, :, 0] + 0.67*  img[:, :, 1] + 0.06 * img[:, :, 2] + epsilon
    L = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2] + epsilon
    R = img[:, :, 0] / L
    G = img[:, :, 1] / L
    B = img[:, :, 2] / L

    L_log = np.log(L + 1e-9)
    temp = np.exp(L_log.mean())
    L = L * a / temp
    H, W = np.shape(L)

    V1 = np.zeros((W, H, picNum + 1))

    for i in range(picNum + 1):
        s = ratio**i
        sigma = alpha * s
        kernelRadius = int(np.ceil(2 * sigma))
        kernelSize = 2 * kernelRadius + 1
        kernel_H = np.multiply(cv2.getGaussianKernel(kernelSize, sigma), (cv2.getGaussianKernel(1, sigma)).T) 
        temp = tmu.conv2(L, kernel_H, 'same')
        kernel_V = np.multiply(cv2.getGaussianKernel(1, sigma), (cv2.getGaussianKernel(kernelSize, sigma)).T)
        V1[:,:,i] = tmu.conv2(temp, kernel_V, 'same')
    
    V = np.zeros((W, H, picNum))
    for i in range(picNum):
        s = ratio ** i
        V[:,:,i]=abs(V1[:,:,i+1]-V1[:,:,i]) / (a * (2 ** phi)/(s**2) + V1[:,:,i])
    
    Vsm = V1[:,:,picNum]
    for i in range(W):
        for j in range(H):
            for k in range(picNum):
                if V[i,j,k] > eps:
                    Vsm[i,j]=V1[i,j,k] 
                    break
    
    Ld = 1 * L / (refV + 0.6*Vsm)
    Ld[Ld > 1] = 1  #亮度通道重映射结果

    out = np.zeros(img.shape)

    out[:,:,0] = R * Ld
    out[:,:,1] = G * Ld
    out[:,:,2] = B * Ld

    return out

matlab代码可以参考这篇博文

(2)基于快速双边滤波的方法 [PDF]

前面说到,如果直接整张图进行压缩,容易使得图像细节丢失。为了保持图像细节,提出基于快速双边滤波方法。该方法的核心思想是对图片进行分层:主要为亮度变化信息的基础层(base),和包含高频纹理的细节层(detail)。所以在压缩的时候,只需要对基础层做对比度压缩,压缩之后的基础层再和原来的细节层相加,得到保留了细节信息的低动态范围图像。

为了很好地将低频基础层和高频细节层分离,文章采用双边滤波算子与原图做卷积,卷积后的图像作为基础层,原图再减去基础层作为细节层。双边滤波的原理可以参看这篇博文

基础层的压缩则通过一个压缩因子实现。首先计算压缩因子:

然后将该压缩因子和基础层相乘则实现压缩过程。需要注意的是,整个过程都是在log域完成的。最终图像获取见如下公式:

完整的流程可总结为:

实现的python代码如下:

def durandanddorsy(img, c):
    height, width = np.shape(img)[:2]
    img = img/img.max()
    epsilon = 0.000001
    # L = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2] + epsilon #Y - grey
    L = 1 / 61 * (20 * img[:, :, 0] + 40 * img[:, :, 1] + img[:, :, 2] + epsilon) 
    R = img[:, :, 0] / L
    G = img[:, :, 1] / L
    B = img[:, :, 2] / L

    L_log = np.log(L) 

    base_log = cv2.bilateralFilter(L_log, 9, 0.3, 0.3)
    detail_log = L_log - base_log
    compressionFactor = np.log(c) / (base_log.max() - base_log.min())
    log_abs_scale = base_log.max() * compressionFactor
    # Ld_log = base_log * compressionFactor + detail_log
    Ld_log = base_log * compressionFactor + detail_log - log_abs_scale
    
    out = np.zeros(img.shape)
    out[:,:,0] = R * np.exp(Ld_log)
    out[:,:,1] = G * np.exp(Ld_log)
    out[:,:,2] = B * np.exp(Ld_log)
    
    outt = np.zeros(out.shape, dtype=np.float32)
    cv2.normalize(out, outt, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)

    return outt

c++代码可以参考这篇博文

(3)Adaptive Local Tone Mapping Based on Retinex for High Dynamic Range Images [PDF]

这篇文章做LTM的思路和Reinhard很像,也是先做全局的色调映射,然后再做局部色调映射。首先介绍GTM算子:

其中N是图像的像素总数,\delta是一个很小的值,为了防止遇到图像中亮度为0的情况;

通过GTM将HDR图像颜色范围压缩到[0, 1]之间后,再进一步应用LTM算子。这篇论文也提到了,在做完GTM后,亮部区域周围可能出现黑边情况,这一问题可以在局部映射中引入保边滤波器解决。

在介绍LTM之前,先介绍Reninex原理。其核心就是类似于人视网膜的视觉观察,我们看到的物体有一部分信息是入射光影响造成的,我们要通过我们看到的图像推算出入射光的影响所造成的图像,然后求二者的差,就是真实的反射物体该有的样子。

基于该原理,Retinex输出可以通过如下公式计算获得:

其中,这篇文章使用引导滤波(原理介绍可见博文)作为F计算获得观测后的图像。我们需要的是实际的反射图像R。

为了进一步防止滤波器造成的平面外观,文章引入了两个因子:\alpha用于提升图像对比度,\beta作为自适应非线性偏移,通过调整对数起点来控制非线性强度。因子计算和LTM过程可用如下公式表达(注意公式(13)把log相减变为了除法,实际还是Retinex公式):

实现的python代码如下:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
变为了除法,实际还是Retinex公式):

实现的python代码如下:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注运维)
[外链图片转存中…(img-qX8tPBkA-1713457515999)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值