Muonium
- KNLMeansCL 参数介绍
- 从Sobel算子到ringmask2()
- iterLineDarken —— 一种新型迭代式线条加深方法
- 两种新式图像结构迁移方法
- MinBlurMod —— 一种新式去点状晕轮方法
KNLMeansCL()参数初步结论(参数不包括rclip): | |
以下比较基于相同PSNR下的目视质量和方法噪声 | |
样本主要为《灰与幻想的格林姆加尔》和《为美好世界送上祝福》中兼具平面、线条、纹理的动态/静态场景 | |
(我本人比较喜欢线条干净、噪点偏少的画面 对于平面的变化很难发现 因此下面推荐参数带有主观色彩) | |
h:全局降噪力度调整参数(没有结论 自己调力度) | |
wmode:三种权值方程的选择 | |
推荐值:2 | |
理由:(以下测试都是基于wmode=2的)它是三种权值方程中唯一一个权值能到达0的 这样能减少大量不相似区块对降噪的干扰 | |
[越高,为了得到相似PSNR需要增加h] | |
a: 越高,消除的噪声中低频噪声的成分更高,对高频部分影响可以忽略不计 | |
推荐值:1/2 | |
理由:越低对平面【低频】成分杀伤越少(虽然我看不到……码率影响应该也比较小、也快、省电。。。) | |
[越高,为了得到相似PSNR有时需要增加h] | |
s: 越高对噪点源的鲁棒性越高(来自论文) | |
越高线条附近有越多区域没有处理(留下蚊噪) | |
会模糊弱纹理(WZ意见这里与我相反,不确定) | |
如果没有模糊时,越高与源相似程度越高(烂源也就烂结果,而降低一点似乎可以“锐化”部分模糊的弱纹理) | |
影响【低频】到【高频】成分 | |
s=1时可能使线条出现锯齿 | |
推荐值:2/3 | |
这个参数虽然不能保证与源非常相同,但是能够有效“锐化”部分模糊的弱纹理(其他的弱纹理在h适当时也不会像s=4时破坏掉),而且能处理更多的线条附近的区域 | |
[越高,为了得到相似PSNR需要降低h] | |
d:s中提到“越高线条附近有越多区域没有处理(留下蚊噪)”,而静态场景时的d则能为这些没有处理(因为找不到近似区块)的像素提供样本,从而能让滤镜处理这些没有降噪的区域;对于动态场景而a又不够大导致还是找不到相似区块,那就还是没法处理了…… | |
和a一样,主要影响【低频】部分 | |
推荐值:0/1/2 | |
(从画质和码率的角度都不知道开关时域降噪哪个好……关了大概会快很多) | |
(也许可以考虑在d=0时把没处理的区域框出来用其他滤镜(比如Bilateral)处理,因为这些区域通常是强线条中心区域而不怕被抹) | |
[越高,为了得到相似PSNR有时需要增加h] | |
wref:看起来烂源越高越好(完全不知道为什么,我之前以为是越低越好的) | |
推荐值:(暂无) | |
[越高,为了得到相似PSNR需要增加h] | |
目前我单独使用一次KNLMeansCL()的方案是(d=0, a=2, s=2, wmode=2)并调整h |
《从Sobel算子到ringmask2()》 by Muonium | |
一、Sobel算子 | |
先介绍经典的Sobel算子做了什么。不想看、因为我表述不清/知识水平不足而看不懂下方说明的可以直接看(一)的结论。 | |
(定义一个3x3的矩阵便于说明): | |
a b c | |
d e f | |
g h i | |
对于一个大小为3x3的、包含一条线的图像: | |
9 9 9 | |
1 1 1 (数字代表灰度,为了简洁而数值很小) | |
5 5 5 | |
在边缘检测中,我们想要检测出,这个3x3的图像的确包含一条线,并把“这个图像包含一条线”的信息储存在e处。 | |
1 2 1 | |
用 0 0 0 这个Sobel模板对第一个矩阵进行卷积,在e处的结果为 e' = 1a + 2b + 1c + 0d + 0e + 0f + -1g + -2h + -1i ...① | |
-1 -2 -1 | |
套用到第二个矩阵中 e' = 1*9 + 2*9 + 1*9 + 0*1 + 0*1 + 0*1 + -1*5 + -2*5 + -1*5 = 16 ...② | |
5 5 5 | |
而假设Sobel模板对这样一个平面区域进行卷积: 5 5 5 | |
5 5 5 | |
e'' = 1*5 + 2*5 + 1*5 + 0*5 + 0*5 + 0*5 + -1*5 + -2*5 + -1*5 = 0 ...③ | |
从②③中可以看到,Sobel模板确实能对线条有响应(数值不为0)而对平面无响应。(可以验证,对于均匀渐变的区域,Sobel模板也不会有响应) | |
为什么呢?我们可以把用Sobel模板进行卷积的过程①分解: | |
原式: e' = 1a + 2b + 1c + 0d + 0e + 0f + -1g + -2h + -1i ...① | |
e1 = 1a + 2b + 1c | |
分解: e2 = 0d + 0e + 0f = 0 | |
e3 = 1g + 2h + 1i | |
因此原式 e' = e1 + 0 - e3 = e1 - e3 | |
注意到,e1的计算过程,实际上就是把b在其水平方向进行一个平均/blur操作,并把结果储存在b'处 | |
e3同理,把h在其水平方向进行一个平均/blur操作,并把结果储存在h'处 | |
最后,如果b'和h'数值不同,在e'处储存的b'-e',数值就不会为0,也就表示“e'所在的3x3矩阵有线条”。 | |
【总结】:Sobel算子做的事,就是先在3x3范围的不同区域做两次的blur(低通滤波)操作。blur会破坏高频(噪点、线条)信息,但在噪点相比线条来说少得多时,不同的blur对噪点的破坏程度接近,对线条的破坏程度则有显著区别,利用这一信息就能得到对于高频信息的mask,用伪代码表示就是: | |
nr1 = blur1(src) | |
nr2 = blur2(src) | |
diff1 = src - nr1 | |
diff2 = src - nr2 # diff1和diff2有相近的噪点强度、和显著的线条信息差异 | |
mask = diff2 - diff1 | |
简化一下就是: | |
mask = blur1(src) - blur2(src) ...④ | |
二、ringmask2() | |
有了上面④,ringmask2()的原理就比较简单了 | |
源代码: | |
w = src.width | |
h = src.height | |
rx = ry = 1.8 | |
smooth = core.fmtc.resample(src, w / rx, h / ry, kernel='bicubic').fmtc.resample(w, h, kernel='bicubic', a1=1, a2=0) | |
smoother = core.fmtc.resample(src, w / rx, h / ry, kernel='bicubic',fulls=False).fmtc.resample(w, h, kernel='bicubic', a1=1.5, a2=-0.25) | |
ringmask = core.std.Expr([smooth, smoother], 'x y -') | |
简化: | |
ringmask = blur2(src) - blur1(src) # 也就是和④一模一样 | |
三、推广 | |
比如换blur的kernel啊之类的(短时间想不出来了) | |
(说明:本文的表述极为不严谨,比如这里的卷积实际上是“相关”的概念,比如我没说明上面的Sobel模板实际上是经典的两个Sobel中检测水平线条的模板,而用检测竖直线条的模板进行上面的卷积是没有响应的,比如卷积后还应做取绝对值的操作等。由于本人知识水平不足,仅能写出这样的文章,还很有可能因此让读者读得一头雾水,请见谅。) |
iterLineDarken —— 一种新型迭代式线条加深方法 v0.1 | |
by Muonium 2017/8/21 | |
目标:尝试通过迭代,对黑度不均匀的线条进行加深,并使黑度变得均匀。 | |
评价:不太成功。具体见后文说明。 | |
代码: | |
def scale(val, bits): | |
return val * ((1 << bits) - 1) // 255 | |
def getKillLineDiff(clip): | |
kl1 = clip.std.Maximum() | |
kl2 = kl1.std.Maximum().std.Minimum().std.Minimum() | |
kl1 = kl1.std.Minimum() | |
diff1 = core.std.MakeDiff(clip, kl1) | |
diff2 = core.std.MakeDiff(clip, kl2) | |
diff = core.rgvs.Repair(diff1, diff2, 2) | |
return diff | |
def diffDarken2(clip, y1, x1, x2): | |
bits = clip.format.bits_per_sample | |
isGray = clip.format.color_family == vs.GRAY | |
neutral = 1 << (bits - 1) | |
y1 = scale(128-y1, bits) | |
x1 = scale(x1, bits) | |
x2 = scale(x2, bits) | |
expr = '{neutral} x - {x1} < {neutral} {neutral} x - {x2} > {neutral} {y1} ? ?'.format(neutral=neutral, x1=x2, x2=x2, y1=y1) | |
clip = core.std.Expr([clip], [expr] if isGray else [expr, '']) | |
return clip | |
def mergeDarken(clip, diff, dst, str): | |
bits = clip.format.bits_per_sample | |
isGray = clip.format.color_family == vs.GRAY | |
neutral = 1 << (bits - 1) | |
dst = scale(dst, bits) | |
flt = 'x y {neutral} - +'.format(neutral=neutral) | |
flt2 = 'x {str2} * {dst_str} +'.format(str2=1-str, dst_str=dst*str) | |
expr = 'x {dst} < x {flt} {dst} > {flt} {flt2} ? ?'.format(dst=dst, flt=flt, flt2=flt2) | |
clip = core.std.Expr([clip, diff], [expr] if isGray else [expr, '']) | |
return clip | |
def iterLineDarken(clip, dst=17, iter=5, iterdark=4, x1=3.5, x2=48, inflate=1, limit_str=0.95, fast=True): | |
dark = clip | |
if fast: | |
preDiff = getKillLineDiff(dark) | |
preDiff = diffDarken2(preDiff, iterdark, x1, x2) | |
preDiff = haf.mt_inflate_multi(preDiff, inflate) | |
for i in range(iter): | |
if not fast: | |
diff = getKillLineDiff(dark) | |
diff = diffDarken2(diff, iterdark, x1, x2) | |
diff = haf.mt_inflate_multi(diff, inflate) | |
dark = mergeDarken(dark, diff, dst, limit_str) | |
else: | |
dark = mergeDarken(dark, preDiff, dst, limit_str) | |
return dark | |
样例: | |
darken = iterLineDarken(src16y) 或 iterLineDarken(src8y) | |
darken = core.std.ShufflePlanes([darken, src16], ...) | |
参数说明: | |
dst: 加深的目标值(8bit)。如果源比这个浅,则最多能加深到这个值;如果源比这个深,保持不变。 | |
iter: 迭代次数。 | |
iter_dark: 每次迭代加深力度的最大值(8bit)。如果加深未到达dst,加深程度为 iter * iter_dark。一般认为,在加深程度相同的情况下,迭代次数越多(同时每次迭代的加深力度越小),函数输出的线条越均匀。 | |
x1: 可以看成判断线条的mask的下阈值。越低框到的线条越多。 | |
x2: 可以看成判断线条的mask的上阈值。越高框到的线条越多。 | |
inflate: 加深前的收线(作用于diff),避免加深后线条变得太粗。 | |
limit_str: 如果某次加深后,线条比dst还深,用这个参数限制加深力度为 (current_dark - dst) * limit_str。比如说,若 limit_str=1, dst=17,某像素加深前为current_dark=18,加深力度为2,由于current_dark - 2 < dst,因此限制后的加深力度为 (18 - 17) * 1 = 1,加深后变成 18 - 1 = 17。 若本例中 limit_str=0.5, 则加深力度为0.5,加深后变成17.5 | |
fast: 速度相关参数。开启后,特别是在高迭代次数下,线条交界处的加深力度比关闭时小。 | |
方法说明: | |
1. 由于这个只是方法试验,故一些后处理如 FastLineDarkenMOD() 中的 protection / luma_cap / threshold 以及全局线条mask都没写进去。 | |
2. 我现在常用二次加深:第一次迭代次数少,加深力度大,下阈值高;第二次迭代次数多,加深力度小,下阈值低,来进行加深。 | |
3. getKillLineDiff() 是对以前的 | |
killline = core.std.Maximum(src16y).std.Minimum() | |
diff = core.std.MakeDiff(src16y, killine) | |
的改进,能让线条交界处的线条轮廓更清晰。 | |
该函数中还可以用3次 expand/inpand ,如 | |
kl1 = clip.std.Maximum().std.Maximum() | |
kl2 = kl1.std.Maximum().std.Minimum().std.Minimum().std.Minimum() | |
kl1 = kl1.std.Minimum() | |
,让三角区域更清晰。不过我觉得2次在多数番下效果足够了,速度也快一些。 | |
4. 该方法一个重要不足之处在于,getKillLineDiff()在部分极弱线条处“框”线条的能力较差,导致这些弱线条没法被加深。也许把这一块换成传统的edge mask,或者二者结合会好一些。我这个暑假大概没有时间试了,看看开学的时间怎样。 | |
5.该方法的另一个不足之处是,强加深后线条周围会有一些点状瑕疵。我现在常用eedi2清掉瑕疵。ss说可以用AWarpSharp,但我不太会调。 |
① | |
# 无标准化 | |
ydiff = core.std.MakeDiff(y, smooth(y)) | |
smooth_u = smooth(u, s) | |
udiff = core.std.MakeDiff(u, smooth_u) | |
udiff = core.rgvs.Repair(ydiff, udiff, 1) | |
flt_u = core.std.MergeDiff(smooth_u, udiff) | |
#有标准化 | |
ydiff = core.std.MakeDiff(y, smooth(y)) | |
ydiff, ydiff_mean, ydiff_var = muf.LocalStatistics(ydiff) | |
ydiff_normalized = core.std.Expr([ydiff, ydiff_mean, ydiff_var], ['x y - z sqrt 1e-7 + /']) | |
smooth_u = smooth(u, s) | |
udiff = core.std.MakeDiff(u, smooth_u) | |
udiff, udiff_mean, udiff_var = muf.LocalStatistics(udiff) | |
udiff_normalized = core.std.Expr([udiff, udiff_mean, udiff_var], ['x y - z sqrt 1e-7 + /']) | |
min_udiff = core.std.Minimum(udiff_normalized) | |
max_udiff = core.std.Maximum(udiff_normalized) | |
udiff_normalized = core.std.Expr([ydiff_normalized, min_udiff, max_udiff], ['x y max z min']) | |
udiff = core.std.Expr([udiff_normalized, udiff_var, udiff_mean], ['x y sqrt * z +']) | |
flt_u = core.std.MergeDiff(smooth_u, udiff) | |
② | |
#f = 1 | |
f = partial(core.knlm.KNLMeansCL, d=0, a=3, s=1, h=4, wmode=0) | |
flt = muf.LocalStatisticsMatching(src, ref, radius=f) |
Muonium
在基于 Avisynth / VapourSynth 的图像处理算法中, MinBlur 是一种常见的保持边缘的平滑滤波, 被 HQDeringmod 用于去除晕轮噪声. 由于 MinBlur 的破坏力度较大, 因此在 HQDeringmod 中, 作者基于 Sobel 算子, 通过形态学的膨胀和收缩设计了一个 mask 来保护非晕轮区域. 该方法的主要问题在于, 在 mask 的设计中包含许多参数, 这些参数在很大程度上会影响最终的去晕轮效果, 而实际使用中手动调节效率较低.
为了通过设计更精确的 mask 来提高去晕轮方法的质量, 有人提出了 AnimeMask 和 AnimeMask2, 其中前者将梯度检测的方向进行分解, 通过对4方向 Cartoon 算子的输出进行平移得到最终 Mask, 后者则基于 DoG (Difference of Guassian) 方法. 相比原版, 在这两种方法中, 前者提供了更大的手动调整空间, 有时能得到更精确的 mask, 后者则提高了默认参数的泛化性能, 但两者的性能也比较有限.
点状晕轮是近年动画中的常见瑕疵, 通常表现为线条附近的单像素宽的局部overshoot. 有人曾用某去晕轮方法进行处理, 缺点在于该方法的效率较低, 且会产生一些新瑕疵.
下面导出新式 MinBlurMod. 以 MinBlur(1)
为例, 它表现为在去除线条周围的细晕轮的同时, 把线条本身也进行了模糊, 并且对平面和纹理也进行了破坏. 通过将其输出和MinBlur(2)
进行比较, 可以发现两者对线条的模糊程度相近, 而对晕轮, 纹理等的破坏程度不同. 基于这一特性, 可以利用MinBlur(1)
和MinBlur(2)
的输出通过逐点误差设计 mask, 从而对MinBlur(1)
的输出进行选择性保护. 由于 MinBlur 算法复杂度较低, 多次 MinBlur 的性能代价也较小. 算法的伪代码如下:
def MinBlurMod1(clip, r=1, thr=2):
# Not optimized
pre1 = MinBlur(clip, r=r)
pre2 = MinBlur(clip, r=r+1)
dering = Expr([clip, pre1, pre2], f"y z - abs {thr} <= y x ?")
return dering
MinBlurMod1 可以看作对当前半径的 MinBlur 进行优化, 因此若把它本身再代入它的算法中, 就可以得到:
def MinBlurMod2(clip, r=1, thr=2):
# Not optimized
pre1 = MinBlurMod1(clip, r=r, thr=thr)
pre2 = MinBlurMod1(clip, r=r+1, thr=thr)
dering = Expr([clip, pre1, pre2], f"y z - abs {thr} <= y x ?")
return dering
显然这种推广的深度无限, 越深可以看作用更大的感受野减少 MinBlur 的破坏. 实际中深度为2 (即 MinBlurMod2
) 的效果已经足够.
本文提出了一种改进型 MinBlur 方法. 与原版方法相比, 该方法在去除点状晕轮的同时能更大程度上保护主要线条, 其质量和性能均高于笔者已知的方法. 后续研究可以考虑深入分解算法代码, 找出算法的核心.