alpha融合简介
alpha融合(alpha compositing)是图像处理中常用技术,常见的公式如下:
C
O
=
α
C
A
+
(
1
−
α
)
C
B
(1)
C_O= \alpha C_A + (1-\alpha)C_B \tag{1}
CO=αCA+(1−α)CB(1)
其中
C
A
C_A
CA,
C
B
C_B
CB表示待融合颜色(RGB),
C
O
C_O
CO表示输出颜色,
α
\alpha
α表示融合因子,取值[0, 1]。
习惯上 C A C_A CA表示前景, C B C_B CB表示背景。 α \alpha α用于表征前景颜色 C A C_A CA在结果中的可见度(透明度), α \alpha α越大,表示 C A C_A CA在结果中占得比重越大。当 α = 1 \alpha=1 α=1时,结果完全等于 C A C_A CA(完全不透明);反之当 α = 0 \alpha=0 α=0时, C A C_A CA在结果中完全不可见(完全透明)。
alpha融合提出之初,用于解决在计算机的辅助下,动画片生产中的素材融合问题,为了处理有交叠的素材之间的透明度关系而发明了这项技术。
最早的alpha融合就是公式(1),由Smith等在1970s提出,也是我们各类教材中最常见的公式。该公式最大的特点就是只有一个
α
\alpha
α,一般我们认为该
α
\alpha
α用于描述
C
A
C_A
CA的透明度,而背景
C
B
C_B
CB的透明度则默认是1,且必须是1。但是,如果待融合的两个颜色各自都有自己的
α
\alpha
α,即存在
α
A
\alpha_A
αA和
α
B
\alpha_B
αB,那么上述公式就不适用了。后来Wallace在1981提出了一个更加通用的公式,用于解决
α
A
\alpha_A
αA和
α
B
\alpha_B
αB同时存在的情况。直到此时,学者们都是在处理素材在自然叠放情况下的透明度关系,也就是所谓的“over”模式,即A over B
。后来Porter 1984年进一步推广了这项技术,提出了alpha预乘(premultiplied alpha)的概念,并发展了如in, out, atop, xor
等其他几种融合模式。
但无论技术本身如何发展,应用alpha融合时必须时刻记住一个大前提:为了获得最终屏幕渲染的结果,必定存在一个完全不透明的“背景”。这个背景可以是诸如ppt中的白色底板,也可以是Photoshop中的灰色格子。因为 α \alpha α并不是一个物理性的东西,而是为了处理透明度关系而发明出来的一个用于辅助计算的概念性的东西。我们的显示器中没有用于显示 α \alpha α的物理元器件,显示器只能显示颜色。因此无论有多少个素材,它们有多么复杂的透明度以及交叠关系,最终必需都得落在一个完全不透明的背景上,得到纯粹的颜色信息,才能送给显示器去上屏显示。
如下图中的灰色格子就是上面所提的“完全不透明的背景”。
认识alpha通道
现在一些图像格式可以支持(R,G,B,A)四个通道,最典型的如png图像,这其中A就是所谓的alpha通道,用于控制颜色信息(R,G,B)的透明度,因此有时我们会把alpha通道称为透明度通道。
如下图是一个简单的素材,左图是RGBA,中间是RGB,右图是alpha(A),alpha通道越亮表示数值越大。
可以看到,该图像的RGB颜色在所有位置都相同,但我们可以通过alpha通道来调整图像各区域相对于背景的透明度情况。
人像抠图一般也是通过alpha通道来实现。下方素材使用开源数据集FFHQ中的00148.png,同样左图是RGBA,中间是RGB,右图是alpha(A)。
现在抠图的mask(即alpha通道)一般通过卷积神经网络分割算法实现,分割算法的输出是一个概率值,一般由sigmoid函数映射得到。mask中白色表示几乎100%可见,黑色表示几乎0%可见。由于sigmoid函数的值域是(0, 1),也就是说除了几乎纯白与纯黑之外,还会有0.2,0.5之类的灰度数值,这些数值分布在纯白与纯黑的过渡区域,天然形成了羽化的边缘,可使得分割结果具有抗锯齿的效果。
在融合时,如果对抠图的alpha做了不正确的处理,常常会在边缘引起一些badcase,如典型的白边黑边问题,锯齿问题。比如,若我们抠像使用的mask是一个二值化,非0即1的结果,那么就会引起上述问题。
如果我们将上图中mask不等于0的地方全部置为1(if mask >0: maks = 1),然后用于抠像,并置于一个绿色背景上面,可以得到下图。
下图中,左图是结果,中间是原图,右图是二值化mask。
放大下面左图可以明显看到锯齿,头发抠图不够细致,存在多余的边缘信息等问题。
我们将二值化的mask与正常mask相减,就可以更清楚地感受到灰度过渡边缘的存在:
下图中,左图是二值化mask,中间是正常mask,右图是二值化减去正常mask得到的灰度边缘。
alpha compositing:over模式
Over模式就是一层一层的叠放,放置于上面的素材会一定程度遮挡放置于下方的素材,遮挡程度由上面素材的透明度决定。
首先定义一个表示方法:如果有素材A( α A \alpha_{A} αA),B( α B \alpha_{B} αB),C( α C \alpha_{C} αC),…,以及背景Z( α Z ≡ 1 \alpha_{Z} \equiv 1 αZ≡1),那么AZ表示将素材A叠放在背景Z之上,ABZ表示将B叠放在Z上面,再将A叠放在B上面,以此类推。
以公式(1)的方式,有:
C
A
B
Z
=
C
A
(
B
Z
)
=
α
A
C
A
+
(
1
−
α
A
)
C
B
Z
=
α
A
C
A
+
(
1
−
α
A
)
(
α
B
C
B
+
(
1
−
α
B
)
C
Z
)
=
α
A
C
A
+
(
1
−
α
A
)
α
B
C
B
+
(
1
−
α
A
)
(
1
−
α
B
)
C
Z
(2)
\begin{aligned} C_{ABZ} &= C_{A(BZ)} \\[2ex] &= \alpha_{A} C_A + (1-\alpha_{A})C_{BZ} \\[2ex] &= \alpha_{A} C_A + (1-\alpha_{A})(\alpha_{B} C_B + (1-\alpha_{B})C_Z) \\[2ex] &= \alpha_{A} C_A + (1-\alpha_{A})\alpha_{B} C_B + (1-\alpha_{A}) (1-\alpha_{B})C_Z \end{aligned} \tag{2}
CABZ=CA(BZ)=αACA+(1−αA)CBZ=αACA+(1−αA)(αBCB+(1−αB)CZ)=αACA+(1−αA)αBCB+(1−αA)(1−αB)CZ(2)
公式(1)有一个限制,那就是,谁处于融合背景的位置,谁的alpha就必须等于1,比如上式中,当BZ融合之后,也就是把B放在一个完全不透明的Z之上,所得结果肯定是完全不透明的,此时再次利用公式(1)将A叠放上去才能得到正确结果。因此在这个限制之下,我们必须从背景开始一层一层向上融合,才能得到正确结果,这就给融合带来了很大的不灵活性。比如我们已有一个素材池,其中素材均带有透明度信息,现在要融合其中多个素材得到一个带透明度信息的新素材,在这个任务根本没有“背景”这个东西,所以此时通过公式(1)根本无法完成任务。再比如,如果我们想把A和B都做相同的旋转,那么根据公式(1),我们必须先把B旋转,然后放置于Z上面,再把A旋转,然后放置于BZ上面,也就是说尽管A和B的旋转是相同的,但我们还是必须做两次旋转操作。
究其原因,是因为公式(1)中只允许一个 α \alpha α不等于1,作为背景的 α \alpha α必须等于1。现在我们希望待融合的两个对象均可以有不等于1的 α \alpha α。
此时融合结果可以表示为
C
A
B
C_{AB}
CAB,其透明度表示为
α
A
B
\alpha_{AB}
αAB,这两个量目前都是未知的,最终结果可以按照如下融合过程来实现:
C
A
B
Z
=
C
(
A
B
)
Z
=
α
A
B
C
A
B
+
(
1
−
α
A
B
)
C
Z
(3)
\begin{aligned} C_{ABZ} &= C_{(AB)Z} \\[2ex] &= \alpha_{AB} C_{AB} + (1-\alpha_{AB})C_{Z} \\[2ex] \end{aligned} \tag{3}
CABZ=C(AB)Z=αABCAB+(1−αAB)CZ(3)
对于任意
C
Z
C_Z
CZ,公式(2)与(3)的结果都必须相等,所以
C
Z
C_Z
CZ的系数相与偏置项必须分别相等,得:
α
A
B
C
A
B
=
α
A
C
A
+
(
1
−
α
A
)
α
B
C
B
(
1
−
α
A
B
)
=
(
1
−
α
A
)
(
1
−
α
B
)
\begin{aligned} \alpha_{AB} C_{AB} &= \alpha_{A} C_A + (1-\alpha_{A})\alpha_{B} C_B\\[2ex] (1-\alpha_{AB}) &= (1-\alpha_{A}) (1-\alpha_{B}) \\[2ex] \end{aligned}
αABCAB(1−αAB)=αACA+(1−αA)αBCB=(1−αA)(1−αB)
解得:
α A B = α A + α B − α A α B = α A + α B ( 1 − α A ) C A B = α A C A + ( 1 − α A ) α B C B α A B (4) \begin{aligned} \alpha_{AB} &= \alpha_{A} + \alpha_{B} - \alpha_{A} \alpha_{B} \\[2ex] &= \alpha_{A} + \alpha_{B}(1-\alpha_{A}) \\[4ex] C_{AB} &=\frac { \alpha_{A} C_A + (1-\alpha_{A})\alpha_{B} C_B}{\alpha_{AB}}\\[2ex] \end{aligned} \tag{4} αABCAB=αA+αB−αAαB=αA+αB(1−αA)=αABαACA+(1−αA)αBCB(4)
在Wallace(1981)的文章中通过反射/透射(Reflectance / Transmittance)的物理概念得到,但是也可以根据上述公式推导而得。
下图是按照公式(1)(2)的方式融合的结果,背景为纯白。
上左:绿色圆圈叠在白色背景上面,上右:红色方块叠在“上左”上面。
下左:红色方块叠在白色背景上面,下右:绿色圆圈叠在“下左”上面。
下图是按照公式(3)(4)的方式计算出的结果,背景同样为纯白。容易看出这种方式计算出的结果与上图结果完全相同。
左图:红色方块叠在绿色圆圈上,再叠在白色背景上。
右图:绿色圆圈叠在红色方块上,再叠在白色背景上。
下图仅利用公式(4)计算两个有透明度信息的素材融合的结果,并把两者的color与alpha分开显示。
左上:红色方块叠在绿色圆圈上。左下:绿色圆圈叠在红色方块上
中间列:color
右列:alpha
PS:上图中间列一定程度上可以看做是在一个不正确的alpha下渲染的结果,此时缺乏透明度信息带来的层次感。
alpha premultiplication(alpha 预乘)
对公式(4)稍作改变可以得到:
α
A
B
C
A
B
=
α
A
C
A
+
(
1
−
α
A
)
α
B
C
B
\alpha_{AB}C_{AB} = \alpha_{A} C_A + (1-\alpha_{A})\alpha_{B} C_B
αABCAB=αACA+(1−αA)αBCB
如果记
C
X
′
=
α
X
C
X
(5)
C'_X=\alpha_{X} C_X \tag{5}
CX′=αXCX(5)
那么有:
α
A
B
=
α
A
+
α
B
(
1
−
α
A
)
C
A
B
′
=
C
A
′
+
C
B
′
(
1
−
α
A
)
(6)
\begin{aligned} \alpha_{AB} &= \alpha_{A} + \alpha_{B}(1-\alpha_{A}) \\[4ex] C'_{AB} &=C'_A + C'_B (1-\alpha_{A}) \end{aligned} \tag{6}
αABCAB′=αA+αB(1−αA)=CA′+CB′(1−αA)(6)
公式(5)就是alpha预乘,即使用原始色彩乘以自身的alpha,公式(6)是alpha预乘下的over模式公式。
我们在阅读文献资料时,如果做了alpha预乘,通常称之为premultiplied alpha,如果没有做,称之为straight alpha。
使用alpha预乘要特别注意:在计算的各个阶段,包括融合背景在内,对alpha预乘的处理要统一,否则得不到正确的渲染结果。
大部分图片浏览软件显示图片都是基于straight alpha,所以我们在保存图片到磁盘上时候要特别注意这一点,以straight alpha格式去写数据。如果我们写的是premultiplied alpha的结果,那么在看图软件里显示时候就会发暗。
下面左图是straight alpha,右图是premultiplied alpha,明显看得出右图发暗。
alpha预乘的优点
计算加速
在alpha预乘情况下,
α
X
\alpha_X
αX的计算和
C
X
′
C'_X
CX′的计算系数相同,所以可以进一步记一个四元数为:
C
X
R
G
B
A
=
(
C
X
′
,
α
X
)
C^{RGBA}_X = (C'_X, \alpha_X)
CXRGBA=(CX′,αX)
那么有:
C
A
B
R
G
B
A
=
C
A
R
G
B
A
+
C
B
R
G
B
A
(
1
−
α
A
)
(7)
C^{RGBA}_{AB} = C^{RGBA}_A + C^{RGBA}_B (1-\alpha_{A}) \tag{7}
CABRGBA=CARGBA+CBRGBA(1−αA)(7)
也就是说公式(6)两个计算式可以合并为公式(7)的一个计算式,这对于OpenGL这种具备四元数定义的语言来讲,可以起到计算加速的作用。
防止颜色渗漏
当我们做一些空间维度操作时,如图像插值,模糊等,如果使用未预乘的alpha,可能会造成颜色渗漏,特别是在alpha接近于0的地方。如下图是没有alpha预乘情况下对图像做模糊,原图像的色彩在alpha=0的地方是蓝色,alpha=1的地方是红色,由于alpha的作用,模糊前渲染结果只能看到红色,但模糊后明显可以看到有蓝色渗漏,这就不太符合直觉。
而如果在alpha预乘的情况下做模糊,最后的结果看起来就正常得多(下图)。
alpha预乘的缺点
色彩精度丢失
目前绝大多数常用色图片或者视频格式对于色彩的记录都是量化的,如SDR的8bit,HDR的10bit等。 alpha预乘后,再转为量化结果进行保存,会导致色彩精度的丢失,alpha数值越小,色彩精度的丢失就越严重,极限情况下,当alpha=0时,原始色彩将完全丢失。
如8bit情况下,alpha = 10,另外有三个原始色彩数值分别为145,150,155。那么三个色彩在相同的alpha下进行预乘(归一化后预乘)后为:
(145 / 255) * (10 / 255) = 0.022299
(150 / 255) * (10 / 255) = 0.023068
(155 / 255) * (10 / 255) = 0.023837
结果仍然要以8bit存储,所以四舍五入转为8bit数值为:
round(0.022299 * 255) = round(5.68) = 6
round(0.023068 * 255) = round(5.88) = 6
round(0.023837 * 255) = round(6.08) = 6
可以看到原本有区分度的色彩在alpha预乘下变为了同一个数字,不再有区分度。
在很多情况下,由于小的alpha会导致透明度提升,即可见度下降,所以颜色损失一些精度不会在视觉观感上带来强烈的差异。但是,如果我们面对的是调色工作,一般不能在alpha预乘的情况下去实施
,因为对于大多数情况,我们想要的都是下式的左边(先调色再alpha预乘),它与右边(先alpha预乘再调色)一般情况下不相等。
α
∗
A
d
j
u
s
t
(
c
o
l
o
r
)
≠
A
d
j
u
s
t
(
α
∗
c
o
l
o
r
)
\alpha*Adjust(color) \ne Adjust(\alpha*color)
α∗Adjust(color)=Adjust(α∗color)
如果我们拿到的输入是做过alpha预乘的,在调色前应当做逆alpha预乘,做法是:
if alpha != 0:
color = color / alpha
else:
color = 0
alpha compositing over模式的结合律
当有多个带透明度通道的素材时,在公式(1)的情况下,只能自底(画布)而上一层一层融合才能得到正确的结果。而在公式(4)或(6)的情况下,只要叠放顺序不变,我们可以任意决定融合的优先级,最后都能得到正确的结果。这就带来了很大的便利性,不仅允许我们使用多个透明度素材通过融合生成新的透明度素材,而且可以节省一些共同计算。比如AB两个素材需要做相同旋转,在公式(1)情况下必需先对B做旋转,融合到画布上,再对A做旋转,再次融合到画布上,此时需要做两次旋转;而如果使用公式(4)(6),可以先融合AB,再做一次旋转,再融合到画布上,此时只需要做一次旋转。
下面用公式(6)做一个简单的结合律推导,对比下面的公式(8)(9)可知
C
A
(
B
C
)
′
=
C
(
A
B
)
C
′
C'_{A(BC)} = C'_{(AB)C}
CA(BC)′=C(AB)C′,即交换ABC素材融合的优先级,结果不变。
α
A
(
B
C
)
=
α
A
+
α
B
C
(
1
−
α
A
)
=
α
A
+
[
α
B
+
α
C
(
1
−
α
B
)
]
(
1
−
α
A
)
=
α
A
+
α
B
(
1
−
α
A
)
+
α
C
(
1
−
α
A
)
(
1
−
α
B
)
C
A
(
B
C
)
′
=
C
A
′
+
C
B
C
′
(
1
−
α
A
)
=
C
A
′
+
[
C
B
′
+
C
C
′
(
1
−
α
B
)
]
(
1
−
α
A
)
=
C
A
′
+
C
B
′
(
1
−
α
A
)
+
C
C
′
(
1
−
α
A
)
(
1
−
α
B
)
(8)
\begin{aligned} \alpha_{A(BC)} &= \alpha_{A} + \alpha_{BC}(1-\alpha_{A}) \\[2ex] &=\alpha_{A} + [ \alpha_{B} + \alpha_{C}(1-\alpha_{B})](1-\alpha_{A}) \\[2ex] &= \alpha_{A} + \alpha_{B} (1-\alpha_{A}) + \alpha_{C}(1-\alpha_{A})(1-\alpha_{B}) \\[4ex] C'_{A(BC)} &= C'_{A} + C'_{BC} (1-\alpha_{A}) \\[2ex] &= C'_{A} + [ C'_{B} + C'_{C} (1-\alpha_{B}) ] (1-\alpha_{A}) \\[2ex] &= C'_{A} + C'_{B} (1-\alpha_{A}) + C'_{C} (1-\alpha_{A})(1-\alpha_{B}) \end{aligned} \tag{8}
αA(BC)CA(BC)′=αA+αBC(1−αA)=αA+[αB+αC(1−αB)](1−αA)=αA+αB(1−αA)+αC(1−αA)(1−αB)=CA′+CBC′(1−αA)=CA′+[CB′+CC′(1−αB)](1−αA)=CA′+CB′(1−αA)+CC′(1−αA)(1−αB)(8)
α ( A B ) C = α A B + α C ( 1 − α A B ) = α A + α B ( 1 − α A ) + α C ( 1 − [ α A + α B ( 1 − α A ) ] ) = α A + α B ( 1 − α A ) + α C ( 1 − α A ) ( 1 − α B ) C ( A B ) C ′ = C A B ′ + C C ′ ( 1 − α A B ) = C A ′ + C B ′ ( 1 − α A ) + C C ′ ( 1 − [ α A + α B ( 1 − α A ) ] ) = C A ′ + C B ′ ( 1 − α A ) + C C ′ ( 1 − α A ) ( 1 − α B ) (9) \begin{aligned} \alpha_{(AB)C} &= \alpha_{AB} + \alpha_{C}(1-\alpha_{AB}) \\[2ex] &= \alpha_{A} + \alpha_{B}(1-\alpha_{A}) + \alpha_{C}(1-[ \alpha_{A} + \alpha_{B}(1-\alpha_{A})]) \\[2ex] &= \alpha_{A} + \alpha_{B} (1-\alpha_{A}) + \alpha_{C}(1-\alpha_{A})(1-\alpha_{B}) \\[4ex] C'_{(AB)C} &= C'_{AB} + C'_{C} (1-\alpha_{AB}) \\[2ex] &= C'_{A} + C'_{B}(1-\alpha_{A}) + C'_{C}(1-[ \alpha_{A} + \alpha_{B}(1-\alpha_{A})]) \\[2ex] &= C'_{A} + C'_{B} (1-\alpha_{A}) + C'_{C} (1-\alpha_{A})(1-\alpha_{B}) \end{aligned} \tag{9} α(AB)CC(AB)C′=αAB+αC(1−αAB)=αA+αB(1−αA)+αC(1−[αA+αB(1−αA)])=αA+αB(1−αA)+αC(1−αA)(1−αB)=CAB′+CC′(1−αAB)=CA′+CB′(1−αA)+CC′(1−[αA+αB(1−αA)])=CA′+CB′(1−αA)+CC′(1−αA)(1−αB)(9)
其他模式的alpha compositing
其他还有四种不太常用的alpha compositing模式,分别是in,out,atop,xor。Porter(1984)根据光的反射和透射概念对上述模式进行了定义。
下图是wikipedia中对各种模式的示例
Wikipedia中没有给计算公式,计算需要参考Porter(1984)的文献,如下:
统一的公式是(straight alpha):
α
A
B
=
α
A
F
A
+
α
B
F
B
C
A
B
=
α
A
F
A
C
A
+
α
B
F
B
C
B
α
A
B
(10)
\begin{aligned} \alpha_{AB} &= \alpha_{A}F_{A} + \alpha_{B}F_{B} \\[4ex] C_{AB} &=\frac { \ \alpha_{A} F_{A} C_{A} + \alpha_{B} F_{B} C_{B}} {\alpha_{AB}}\\[2ex] \end{aligned} \tag{10}
αABCAB=αAFA+αBFB=αAB αAFACA+αBFBCB(10)
F的数值如下:
over:
F
A
=
1
,
F
B
=
1
−
α
A
F_A=1, F_B=1-\alpha_A
FA=1,FB=1−αA
in:
F
A
=
α
B
,
F
B
=
0
F_A = \alpha_B, F_B=0
FA=αB,FB=0
out:
F
A
=
1
−
α
B
,
F
B
=
0
F_A=1-\alpha_B,F_B=0
FA=1−αB,FB=0
atop:
F
A
=
α
B
,
F
B
=
1
−
α
A
F_A=\alpha_B,F_B=1-\alpha_A
FA=αB,FB=1−αA
xor:
F
A
=
1
−
α
B
,
F
B
=
1
−
α
A
F_A=1-\alpha_B,F_B=1-\alpha_A
FA=1−αB,FB=1−αA
下图从左到右分别是in,out,atop,xor的结果,上面一排没有底层画布,下面一排使用了格子画布,方便观察和对比效果。
实验代码
utils.py
# -*- coding: utf-8 -*-
import os
import cv2
import numpy as np
def remove_file(filename):
"""
Remove a file.
"""
if os.path.exists(filename):
os.remove(filename)
def float_to_uint8(image, scale=255.):
return np.uint8(np.clip(np.round(image * scale), 0, 255))
def concat_color_alpha(color, alpha):
alpha = np.squeeze(alpha)[..., np.newaxis]
image = np.concatenate((color, alpha), axis=2)
return image
def alpha_premultiply(image, alpha):
alpha = np.squeeze(alpha)
result = image * alpha[..., np.newaxis]
return result
def mix(color1, color2, alpha):
"""
Naive alpha blending.
"""
if not isinstance(alpha, (int, float)):
alpha = np.squeeze(alpha)[..., np.newaxis]
res = color1 * (1.0 - alpha) + color2 * alpha
return res
def alpha_compositing(fg_color,
fg_alpha,
bg_color,
bg_alpha,
mode='over',
premultiplied_input=False,
premultiplied_output=False):
if not isinstance(fg_alpha, (int, float)):
fg_alpha = np.squeeze(fg_alpha)[..., np.newaxis]
if not isinstance(bg_alpha, (int, float)):
bg_alpha = np.squeeze(bg_alpha)[..., np.newaxis]
if mode == 'over':
fg_fraction = 1.
bg_fraction = 1. - fg_alpha
elif mode == 'in':
fg_fraction = bg_alpha
bg_fraction = 0.
elif mode == 'out':
fg_fraction = 1. - bg_alpha
bg_fraction = 0.
elif mode == 'atop':
fg_fraction = bg_alpha
bg_fraction = 1. - fg_alpha
elif mode == 'xor':
fg_fraction = 1. - bg_alpha
bg_fraction = 1. - fg_alpha
else:
raise ValueError(
"mode must be one of ['over', 'in', 'out', 'atop', 'xor']")
out_alpha = fg_alpha * fg_fraction + bg_alpha * bg_fraction
if premultiplied_input:
out_color = fg_fraction * fg_color + bg_fraction * bg_color
else:
out_color = fg_alpha * fg_fraction * fg_color + \
bg_alpha * bg_fraction * bg_color
if not premultiplied_output:
out_color = out_color / np.maximum(out_alpha, 1e-6)
return out_color, out_alpha
media.py
# -*- coding: utf-8 -*-
import cv2
import numpy as np
from utils import float_to_uint8
from utils import concat_color_alpha
def decomposite_00148():
image_wo_bg = cv2.imread(r'portrait\00148-remove-bg-small.png',
cv2.IMREAD_UNCHANGED)
height, width = image_wo_bg.shape[:2]
ori_image = cv2.imread(r'.\portrait\00148.png', cv2.IMREAD_UNCHANGED)
image = cv2.resize(ori_image, dsize=(width, height), fx=None, fy=None,
interpolation=cv2.INTER_AREA)
color = image_wo_bg[..., :3]
alpha = image_wo_bg[..., -1]
binary_alpha = alpha.copy()
binary_alpha[binary_alpha > 0] = 255
alpha_edge = binary_alpha - alpha
image_small = concat_color_alpha(image, alpha)
cv2.imwrite(r'.\portrait\00148-small.png', image)
cv2.imwrite(r'.\portrait\color.png', color)
cv2.imwrite(r'.\portrait\alpha.png', alpha)
cv2.imwrite(r'.\portrait\binary-alpha.png', binary_alpha)
cv2.imwrite(r'.\portrait\alpha-edge.png', alpha_edge)
cv2.imwrite(r'.\portrait\00148-remove-bg-small-new.png', image_small)
def generate_ellipse_media():
height = 600
width = 800
alpha = np.zeros(shape=[height, width], dtype=np.float_)
half_min_size = int(min(width, height) // 2)
cv2.ellipse(alpha,
center=(width // 2, height // 2),
axes=(int(half_min_size * 0.8), int(half_min_size * 0.6)),
angle=0,
startAngle=0,
endAngle=360,
color=1.0,
thickness=-1,
lineType=cv2.LINE_AA)
result = np.ones(shape=(height, width, 4), dtype=np.float_)
index = alpha > 0.99
result[index, :3] = (0., 0., 1.)
result[~index, :3] = (1., 0., 0.)
result[..., -1] = alpha
cv2.imwrite(r'.\media\ellipse.png', float_to_uint8(result))
def generate_circle_media():
fg_color = (0., 1., 0.)
height = 600
width = 800
half_min_size = int(min(width, height) // 2)
alpha = np.zeros(shape=[height, width], dtype=np.float_)
cv2.circle(alpha,
center=(width // 2 + 50, height // 2 + 80),
radius=int(half_min_size * 0.55),
color=1.0,
thickness=-1,
lineType=cv2.LINE_AA)
radius = int(half_min_size * 0.3) // 2 * 2 + 1
alpha = cv2.GaussianBlur(alpha,
ksize=(radius, radius),
sigmaX=radius / 2.5,
sigmaY=radius / 2.5,
borderType=cv2.BORDER_REFLECT_101)
alpha = alpha / np.max(alpha)
alpha = 0.2 + alpha * 0.4
result = np.ones(shape=(height, width, 4), dtype=np.float_)
result[..., :3] = fg_color
result[..., -1] = alpha
result = float_to_uint8(result)
cv2.imwrite(r'.\media\green-circle.png', result)
cv2.imwrite(r'.\media\green-circle-color.png', result[..., :3])
cv2.imwrite(r'.\media\green-circle-alpha.png', result[..., -1])
result[..., :3] = result[..., :3] * alpha[..., np.newaxis]
cv2.imwrite(r'.\media\green-circle-premultiply.png', result)
cv2.imwrite(r'.\media\green-circle-premultiply-color.png', result[..., :3])
def generate_square_media():
fg_color = (0., 0., 1.)
height = 600
width = 800
half_min_size = int(min(width, height) // 2)
alpha = np.zeros(shape=[height, width], dtype=np.float_)
base_square_points = np.array([
[0, 0],
[0, 1],
[1, 1],
[1, 0]
])
offset = (half_min_size // 2, half_min_size // 4)
edge_size = half_min_size
square_points = base_square_points * edge_size + offset
cv2.fillPoly(alpha, [square_points], 1.0, lineType=cv2.LINE_AA)
radius = int(half_min_size * 0.3) // 2 * 2 + 1
alpha = cv2.boxFilter(alpha, ddepth=-1, ksize=(radius, radius),
borderType=cv2.BORDER_REFLECT_101)
alpha = alpha / np.max(alpha)
alpha = 0.2 + alpha * 0.3
result = np.ones(shape=(height, width, 4), dtype=np.float_)
result[..., :3] = fg_color
result[..., -1] = alpha
result = float_to_uint8(result)
cv2.imwrite(r'.\media\red-square.png', result)
cv2.imwrite(r'.\media\red-square-color.png', result[..., :3])
cv2.imwrite(r'.\media\red-square-alpha.png', result[..., -1])
def read_portrait_media():
scale = 255.
color = cv2.imread(r'.\portrait\color.png', cv2.IMREAD_UNCHANGED)
alpha = cv2.imread(r'.\portrait\alpha.png', cv2.IMREAD_UNCHANGED)
binary_alpha = cv2.imread(r'.\portrait\binary-alpha.png',
cv2.IMREAD_UNCHANGED)
color = color / scale
alpha = alpha / scale
binary_alpha = binary_alpha / scale
return color, alpha, binary_alpha
def read_color_media():
scale = 255.
red_square = cv2.imread(r'.\media\red-square.png', cv2.IMREAD_UNCHANGED)
green_circle = cv2.imread(r'.\media\green-circle.png',
cv2.IMREAD_UNCHANGED)
red_square = red_square / scale
green_circle = green_circle / scale
return red_square, green_circle
def read_ellipse_media():
scale = 255.
image = cv2.imread(r'.\media\ellipse.png', cv2.IMREAD_UNCHANGED)
image = image / scale
return image
main.py
# -*- coding: utf-8 -*-
import cv2
import numpy as np
from utils import float_to_uint8
from utils import alpha_premultiply
from utils import concat_color_alpha
from utils import mix
from utils import alpha_compositing
from media import decomposite_00148
from media import generate_circle_media
from media import generate_square_media
from media import generate_ellipse_media
from media import read_ellipse_media
from media import read_portrait_media
from media import read_color_media
def alpha_premultiply_00148():
color, alpha, binary_alpha = read_portrait_media()
# premultiply alpha
premultiply_alpha = alpha_premultiply(color, alpha)
premultiply_alpha = float_to_uint8(premultiply_alpha)
cv2.imwrite(r'.\portrait\premultiply-alpha.png', premultiply_alpha)
# premultiply binary alpha
premultiply_binary_alpha = alpha_premultiply(color, binary_alpha)
premultiply_binary_alpha = float_to_uint8(premultiply_binary_alpha)
cv2.imwrite(r'.\portrait\premultiply-binary-alpha.png',
premultiply_binary_alpha)
def alpha_premultiply_leak():
image = read_ellipse_media()
height, width = image.shape[:2]
half_min_size = int(min(width, height) // 2)
radius = int(half_min_size * 0.3) // 2 * 2 + 1
straight_blur = cv2.GaussianBlur(image,
ksize=(radius, radius),
sigmaX=radius / 2.5,
sigmaY=radius / 2.5,
borderType=cv2.BORDER_REFLECT_101)
straight_blur = float_to_uint8(straight_blur)
premultiplied_image = image.copy()
premultiplied_image[..., :3] = image[..., :3] * image[..., -1:]
premultiplied_blur = cv2.GaussianBlur(premultiplied_image,
ksize=(radius, radius),
sigmaX=radius / 2.5,
sigmaY=radius / 2.5,
borderType=cv2.BORDER_REFLECT_101)
premultiplied_blur = float_to_uint8(premultiplied_blur)
image = float_to_uint8(image)
premultiplied_image = float_to_uint8(premultiplied_image)
cv2.imwrite(r'.\media\ellipse-straight-color.png', image[..., :3])
cv2.imwrite(r'.\media\ellipse-alpha.png', image[..., -1])
cv2.imwrite(r'.\media\ellipse-straight-blur.png', straight_blur)
cv2.imwrite(r'.\media\ellipse-straight-blur-color.png',
straight_blur[..., :3])
cv2.imwrite(r'.\media\ellipse-blur-alpha.png',
straight_blur[..., -1])
cv2.imwrite(r'.\media\ellipse-premultiplied-color.png',
premultiplied_image[..., :3])
cv2.imwrite(r'.\media\ellipse-premultiplied-blur-color.png',
premultiplied_blur[..., :3])
premultiplied_blur = premultiplied_blur / 255.
index = premultiplied_blur[..., -1] > 0
premultiplied_blur[index, :3] = premultiplied_blur[index, :3] / \
premultiplied_blur[index, -1:]
premultiplied_blur = float_to_uint8(premultiplied_blur)
cv2.imwrite(r'.\media\ellipse-premultiplied-blur.png', premultiplied_blur)
def naive_alpha_blending():
color, alpha, binary_alpha = read_portrait_media()
bg_color = np.array([0, 1., 0.])
# naive alpha blending using normal alpha
alpha_blending = mix(bg_color, color, alpha)
alpha_blending = float_to_uint8(alpha_blending)
cv2.imwrite(r'.\portrait\alpha-blending-with-bg.png', alpha_blending)
# naive alpha blending using binary_alpha
binary_alpha_blending = mix(bg_color, color, binary_alpha)
binary_alpha_blending = float_to_uint8(binary_alpha_blending)
cv2.imwrite(r'.\portrait\binary-alpha-blending-with-bg.png',
binary_alpha_blending)
def alpha_blending_color_media():
red_square, green_circle = read_color_media()
bg_color = (1., 1., 1.)
bg_green = mix(bg_color, green_circle[..., :3], green_circle[..., -1])
bg_green_red = mix(bg_green, red_square[..., :3], red_square[..., -1])
bg_green = float_to_uint8(bg_green)
bg_green_red = float_to_uint8(bg_green_red)
bg_red = mix(bg_color, red_square[..., :3], red_square[..., -1])
bg_red_green = mix(bg_red, green_circle[..., :3], green_circle[..., -1])
bg_red = float_to_uint8(bg_red)
bg_red_green = float_to_uint8(bg_red_green)
cv2.imwrite(r'.\media\alpha-blending-bg-green.png', bg_green)
cv2.imwrite(r'.\media\alpha-blending-bg-green-red.png', bg_green_red)
cv2.imwrite(r'.\media\alpha-blending-bg-red.png', bg_red)
cv2.imwrite(r'.\media\alpha-blending-bg-red-green.png', bg_red_green)
def alpha_compositing_over_portrait():
color, alpha, binary_alpha = read_portrait_media()
bg_color = np.array([0, 1., 0.])
bg_alpha = 1.0
# composite - 0: alpha compositing, no premultiply, bg_alpha = 1.0
composited_color, composited_alpha = alpha_compositing(
color, alpha, bg_color, bg_alpha)
image = concat_color_alpha(composited_color, composited_alpha)
image = float_to_uint8(image)
cv2.imwrite(r'.\portrait\alpha-compositing-over-bg-alpha=1.0.png', image)
# composite - 0: alpha compositing, no premultiply, bg_alpha = 0.5
bg_alpha = 0.5
composited_color, composited_alpha = alpha_compositing(
color, alpha, bg_color, bg_alpha)
image = concat_color_alpha(composited_color, composited_alpha)
image = float_to_uint8(image)
cv2.imwrite(r'.\portrait\alpha-compositing-over-bg-alpha=0.5.png', image)
def alpha_compositing_over_color_media():
red_square, green_circle = read_color_media()
bg_color = (1., 1., 1.)
color, alpha = alpha_compositing(red_square[..., :3],
red_square[..., -1],
green_circle[..., :3],
green_circle[..., -1])
red_over_green = concat_color_alpha(color, alpha)
red_over_green_over_bg, _ = alpha_compositing(red_over_green[..., :3],
red_over_green[..., -1],
bg_color,
1.0)
red_over_green = float_to_uint8(red_over_green)
red_over_green_over_bg = float_to_uint8(red_over_green_over_bg)
cv2.imwrite(r'.\media\alpha-compositing-red-over-green.png',
red_over_green)
cv2.imwrite(r'.\media\alpha-compositing-red-over-green-color.png',
red_over_green[..., :3])
cv2.imwrite(r'.\media\alpha-compositing-red-over-green-alpha.png',
red_over_green[..., -1])
cv2.imwrite(r'.\media\alpha-compositing-red-over-green_over_bg.png',
red_over_green_over_bg)
color, alpha = alpha_compositing(green_circle[..., :3],
green_circle[..., -1],
red_square[..., :3],
red_square[..., -1])
green_over_red = concat_color_alpha(color, alpha)
green_over_red_over_bg, _ = alpha_compositing(green_over_red[..., :3],
green_over_red[..., -1],
bg_color,
1.0)
green_over_red = float_to_uint8(green_over_red)
green_over_red_over_bg = float_to_uint8(green_over_red_over_bg)
cv2.imwrite(r'.\media\alpha-compositing-green-over-red.png',
green_over_red)
cv2.imwrite(r'.\media\alpha-compositing-green-over-red-color.png',
green_over_red[..., :3])
cv2.imwrite(r'.\media\alpha-compositing-green-over-red-alpha.png',
green_over_red[..., -1])
cv2.imwrite(r'.\media\alpha-compositing-green_over_red_over_bg.png',
green_over_red_over_bg)
def alpha_compositing_other_modes():
red_square, green_circle = read_color_media()
color, alpha = alpha_compositing(red_square[..., :3],
red_square[..., -1],
green_circle[..., :3],
green_circle[..., -1],
mode='in')
red_in_green = concat_color_alpha(color, alpha)
red_in_green = float_to_uint8(red_in_green)
color, alpha = alpha_compositing(red_square[..., :3],
red_square[..., -1],
green_circle[..., :3],
green_circle[..., -1],
mode='out')
red_out_green = concat_color_alpha(color, alpha)
red_out_green = float_to_uint8(red_out_green)
color, alpha = alpha_compositing(red_square[..., :3],
red_square[..., -1],
green_circle[..., :3],
green_circle[..., -1],
mode='atop')
red_atop_green = concat_color_alpha(color, alpha)
red_atop_green = float_to_uint8(red_atop_green)
color, alpha = alpha_compositing(red_square[..., :3],
red_square[..., -1],
green_circle[..., :3],
green_circle[..., -1],
mode='xor')
red_xor_green = concat_color_alpha(color, alpha)
red_xor_green = float_to_uint8(red_xor_green)
cv2.imwrite(r'.\media\red_in_green.png', red_in_green)
cv2.imwrite(r'.\media\red_out_green.png', red_out_green)
cv2.imwrite(r'.\media\red_atop_green.png', red_atop_green)
cv2.imwrite(r'.\media\red_xor_green.png', red_xor_green)
if __name__ == '__main__':
decomposite_00148()
generate_circle_media()
generate_square_media()
generate_ellipse_media()
alpha_premultiply_00148()
alpha_premultiply_leak()
naive_alpha_blending()
alpha_blending_color_media()
alpha_compositing_over_portrait()
alpha_compositing_over_color_media()
alpha_compositing_other_modes()
clear.py
# -*- coding: utf-8 -*-
from utils import remove_file
if __name__ == '__main__':
# generated by media.py
remove_file(r'.\portrait\00148-small.png')
remove_file(r'.\portrait\color.png')
remove_file(r'.\portrait\alpha.png')
remove_file(r'.\portrait\binary-alpha.png')
remove_file(r'.\portrait\alpha-edge.png')
remove_file(r'.\portrait\00148-remove-bg-small-new.png')
remove_file(r'.\media\ellipse.png')
remove_file(r'.\media\green-circle.png')
remove_file(r'.\media\green-circle-color.png')
remove_file(r'.\media\green-circle-alpha.png')
remove_file(r'.\media\green-circle-premultiply.png')
remove_file(r'.\media\green-circle-premultiply-color.png')
remove_file(r'.\media\red-square.png')
remove_file(r'.\media\red-square-color.png')
remove_file(r'.\media\red-square-alpha.png')
# generated by main.py
remove_file(r'.\portrait\premultiply-alpha.png')
remove_file(r'.\portrait\premultiply-binary-alpha.png')
remove_file(r'.\media\ellipse-straight-color.png')
remove_file(r'.\media\ellipse-alpha.png')
remove_file(r'.\media\ellipse-straight-blur.png')
remove_file(r'.\media\ellipse-straight-blur-color.png')
remove_file(r'.\media\ellipse-blur-alpha.png')
remove_file(r'.\media\ellipse-premultiplied-color.png')
remove_file(r'.\media\ellipse-premultiplied-blur.png')
remove_file(r'.\media\ellipse-premultiplied-blur-color.png')
remove_file(r'.\portrait\alpha-blending-with-bg.png')
remove_file(r'.\portrait\binary-alpha-blending-with-bg.png')
remove_file(r'.\portrait\alpha-compositing-over-bg-alpha=1.0.png')
remove_file(r'.\portrait\alpha-compositing-over-bg-alpha=0.5.png')
remove_file(r'.\media\alpha-compositing-red-over-green.png')
remove_file(r'.\media\alpha-compositing-red-over-green-color.png')
remove_file(r'.\media\alpha-compositing-red-over-green-alpha.png')
remove_file(r'.\media\alpha-compositing-red-over-green_over_bg.png')
remove_file(r'.\media\alpha-compositing-green-over-red.png')
remove_file(r'.\media\alpha-compositing-green-over-red-color.png')
remove_file(r'.\media\alpha-compositing-green-over-red-alpha.png')
remove_file(r'.\media\alpha-compositing-green_over_red_over_bg.png')
remove_file(r'.\media\alpha-blending-bg-green.png')
remove_file(r'.\media\alpha-blending-bg-green-red.png')
remove_file(r'.\media\alpha-blending-bg-red.png')
remove_file(r'.\media\alpha-blending-bg-red-green.png')
remove_file(r'.\media\red_in_green.png')
remove_file(r'.\media\red_out_green.png')
remove_file(r'.\media\red_atop_green.png')
remove_file(r'.\media\red_xor_green.png')
参考资料
https://en.wikipedia.org/wiki/Alpha_compositing
https://ciechanow.ski/alpha-compositing/
https://graphics.stanford.edu/papers/merging-sig81/
https://graphics.stanford.edu/papers/merging-sig81/wallace-merging-sig81.pdf
https://dl.acm.org/doi/pdf/10.1145/964965.808606