Skip to content

Instantly share code, notes, and snippets.

@WolframRhodium
Last active January 17, 2018 19:42
Show Gist options
  • Save WolframRhodium/1e3ae9276d70aa1ddc93ea833cdce9c6 to your computer and use it in GitHub Desktop.
Save WolframRhodium/1e3ae9276d70aa1ddc93ea833cdce9c6 to your computer and use it in GitHub Desktop.
Publications

Muonium

章节

  1. KNLMeansCL 参数介绍
  2. 从Sobel算子到ringmask2()
  3. iterLineDarken —— 一种新型迭代式线条加深方法
  4. 两种新式图像结构迁移方法
  5. 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)

MinBlurMod —— 一种新式去点状晕轮方法

Muonium

1 引言

在基于 Avisynth / VapourSynth 的图像处理算法中, MinBlur 是一种常见的保持边缘的平滑滤波, 被 HQDeringmod 用于去除晕轮噪声. 由于 MinBlur 的破坏力度较大, 因此在 HQDeringmod 中, 作者基于 Sobel 算子, 通过形态学的膨胀和收缩设计了一个 mask 来保护非晕轮区域. 该方法的主要问题在于, 在 mask 的设计中包含许多参数, 这些参数在很大程度上会影响最终的去晕轮效果, 而实际使用中手动调节效率较低.

为了通过设计更精确的 mask 来提高去晕轮方法的质量, 有人提出了 AnimeMaskAnimeMask2, 其中前者将梯度检测的方向进行分解, 通过对4方向 Cartoon 算子的输出进行平移得到最终 Mask, 后者则基于 DoG (Difference of Guassian) 方法. 相比原版, 在这两种方法中, 前者提供了更大的手动调整空间, 有时能得到更精确的 mask, 后者则提高了默认参数的泛化性能, 但两者的性能也比较有限.

点状晕轮是近年动画中的常见瑕疵, 通常表现为线条附近的单像素宽的局部overshoot. 有人曾用某去晕轮方法进行处理, 缺点在于该方法的效率较低, 且会产生一些新瑕疵.

2 算法

下面导出新式 MinBlurMod. 以 MinBlur(1) 为例, 它表现为在去除线条周围的细晕轮的同时, 把线条本身也进行了模糊, 并且对平面和纹理也进行了破坏. 通过将其输出和MinBlur(2) 进行比较, 可以发现两者对线条的模糊程度相近, 而对晕轮, 纹理等的破坏程度不同. 基于这一特性, 可以利用MinBlur(1)MinBlur(2) 的输出通过逐点误差设计 mask, 从而对MinBlur(1) 的输出进行选择性保护. 由于 MinBlur 算法复杂度较低, 多次 MinBlur 的性能代价也较小. 算法的伪代码如下:

算法 1

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

3 推广

MinBlurMod1 可以看作对当前半径的 MinBlur 进行优化, 因此若把它本身再代入它的算法中, 就可以得到:

算法 2

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) 的效果已经足够.

4 总结

本文提出了一种改进型 MinBlur 方法. 与原版方法相比, 该方法在去除点状晕轮的同时能更大程度上保护主要线条, 其质量和性能均高于笔者已知的方法. 后续研究可以考虑深入分解算法代码, 找出算法的核心.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment