ImageMagick 使用示例 -- 遮罩
- 内部 Matte 通道
-
Off(或-alpha off)、Set(或-alpha set)、On、
Activate、
Discrete、
Opaque、Transparent、Extract、Copy、
Shape、Remove、Background - 布尔值 Alpha 透明度
- 作为彩色形状的遮罩
- 数学合成
- 使用遮罩的 Alpha 合成
-
对齐两张带遮罩的图像 (制作中)
- 裁剪遮罩与裁剪路径
- 对带边框的对象做遮罩
- 移除已知背景
- 差异图像遮罩与羽化
- 恢复半透明边缘
- 用模糊填充
(制作中) 在这些示例中,我们将探讨透明度的特殊处理、透明度通道、遮罩的使用方式,以及最终如何移除不需要的背景或其他元素,例如标志、文字、垃圾信息。
Alpha 通道
图像的透明度(alpha)通道是完全可选的,通常需要与普通的“颜色”通道分开做特殊处理。参见上文的图像色彩空间。透明度通道的存在还会影响各种运算符处理其他颜色通道的方式,这通常是因为完全透明的颜色往往应该被操作完全忽略。如果不是这样,图像周围就会出现“黑色光晕”,这正是 IM v6 早期版本中出现过的重大 bug,例如缩放光晕 bug,以及带透明度的模糊 bug。更糟糕的是,这个通道有时也被称为图像的“透明度(transparency)”通道或“不透明度(opacity)”通道,甚至被称为图像的“遮罩”。但这些说法指的都是同一个特殊的、图像的第四个通道。
为了说明其中的区别,我们需要一张可用的示例图像,这里我将使用一张“新月”PNG 图像(来自 CopyOpacity 合成 的示例)。如你所见,这张图像有大量完全透明的区域。不仅如此,我还必须用“PNG”图像格式保存该图像,这是为数不多能正确理解并处理透明色与半透明色的图像格式之一。 我可以通过 Alpha 合成,把这张图像叠加到 IM 内置的棋盘格图案上,来演示这种透明度。 |
magick composite -compose Dst_Over -tile pattern:checkerboard \
moon.png moon_background.jpg
内部 Alpha 通道
如今 IM v7 会在内部把透明度信息存放在“alpha”通道中,这与颜色通道一样,只是一张普通的灰度图像,其值从代表完全透明(即“清澈”)的白色,一直到代表完全不透明的黑色。它有点像是你看到原始图像剪影时得到的效果。像“[-level](https://imagemagick.org/command-line-options/#level)”和“[-threshold](https://imagemagick.org/command-line-options/#threshold)”这类底层运算符会把这份数据当作 alpha 来处理。如果不确定,请查阅官方选项参考。
创建时间: 2003 年 12 月 10 日(最初名为 'channels')
更新时间: 2018 年 10 月 2 日
作者: Anthony Thyssen, Anthony.Thyssen@gmail.com
示例所用版本: ![[version image]](../static/img/masking/version.gif)
URL: https://usage.imagemagick.org/masking/
这是一种从图像中提取“alpha”透明度值的非常古老的方法。它把透明度通道保存为一种“alpha”图像文件格式,需要两个独立的步骤,以及用于指定正确图像文件格式的命令。 | |
magick moon.png alpha:moon.matte
magick MIFF:moon.alpha moon_matte2.png
# 你也可以把这两步合并成一条流水线……
magick moon.png alpha:- | magick - moon_matte3.png
![[IM Output]](../static/img/masking/moon_matte3.png)
在 IM v5 使用的年代,这种提取图像“alpha”的技术很常见。基本上,这是当时获取图像透明度信息的唯一方法。如今已经很少使用了。
控制图像的透明度
有两个运算符可以对内存中图像的透明度通道进行底层控制。较新的“[-alpha](https://imagemagick.org/command-line-options/#alpha)”系列方法目前是推荐的控制方式,尽管许多 IM 示例仍在展示并使用较旧的“-matte”运算符。图像不仅可以拥有 alpha 通道数据,还拥有一个“开关”,用来定义该通道数据是否可见(有效)。这意味着图像在 alpha 通道方面可以有三种状态。 开关 |
通道数据 | |
|---|---|---|
| alpha off | 没有 alpha 数据(未分配任何内存) | |
| alpha deactivate | 存在旧的 alpha 数据(但未被使用) | |
| alpha on | 当前正在使用的 alpha 数据 | |
必须记住这一点,因为各种方法的行为取决于图像当时处于上述三种状态中的哪一种。如果“开关”处于关闭状态,运算符将不会触及 alpha 数据,因为它实际上可能根本不存在。在这种情况下,旧的 alpha 数据可能仍然原封不动地保留着,处于过时状态。正如你稍后会看到的,这在某些情况下其实很有用。不过要注意,有些运算符出于某种原因会自动打开或关闭 alpha 开关。例如,“-compose CopyOpacity -composite”总是会在结果图像中打开 alpha 通道,因为该运算符的职责就是把数据复制到 alpha 通道中。因此最终结果中必须存在该通道。但它在输入数据中是否存在,也会带来其他后果。详情参见 Copy_Opacity 合成方法。同样,使用颜色“None”创建画布时,也会自动创建并启用透明度通道,以确保这张空白图像确实是透明的。另一方面,如果使用其他颜色名称创建画布,通常不会创建任何透明度通道,因为图像默认是不透明的。 |
||
下面是各种“[-alpha](https://imagemagick.org/command-line-options/#alpha)”方法,以及它们如何影响图像及其透明度的示例。 |
Alpha Off 或「-alpha off」
这只是图像上的一个简单开关,会关闭透明度对图像产生的所有效果。它实际上并不会删除或破坏图像所附带的 alpha 通道,只是关闭该通道对图像产生的效果。同样,当通道被关闭时,任何运算符都不会影响所附带的 alpha 通道。例如,让我们使用“新月”图像(来自 CopyOpacity 合成 示例),直接把图像的 alpha 通道关闭。
magick moon.png -alpha off alpha_off.png
请注意,关闭透明度后月亮的形状完全消失了,不过实际上这种情况很少见。基本上,即便是“透明”的区域也是有颜色的,只是通常看不见而已。在这个例子中,隐藏的颜色是用来生成该月亮图像的分形画布图像。这种隐藏的颜色可以是任何东西:从 GIF 格式在其调色表中用来表示透明的简单 GIF 透明色,到图像生成过程中残留下来的垃圾颜色(如上例)都有可能。更典型的情况是,对于任何完全透明的像素,透明色往往就是纯黑色。请注意,靠近边缘的像素可能是半透明的,因此可能仍然带有一个只能部分看见的有效颜色。上面的“[-alpha](https://imagemagick.org/command-line-options/#alpha) Off”操作只是简单地“停用”或“关闭”了该通道。透明度数据本身并没有从存储在内存中的图像数据里被清除或移除,它依然存在,只是暂时不可用而已。但是……如果在透明度数据被关闭的状态下保存图像,任何透明度数据都不会被保存进图像文件格式中。因此,被关闭的 alpha 数据不会出现在保存后的图像副本里,尽管它在内存中的版本里仍然存在(只是处于关闭状态)。另外,由于许多文件格式(例如 JPEG)根本不允许使用透明度,这些文件格式在保存图像时会自动执行相当于“[-alpha](https://imagemagick.org/command-line-options/#alpha) Off”的操作(尽管实际上并未真正执行该操作)。这通常会导致保存为 JPEG 图像时,所有透明区域都变成黑色。关于在保存为 JPEG 文件格式之前正确移除透明度的方法,请参见下文的Alpha Remove - 移除透明度。“[+alpha](https://imagemagick.org/command-line-options/#alpha)”运算符是一个较旧的命令,与“-alpha Off”完全相同,也就是只是关闭透明度通道。请注意,在使用灰度遮罩图像配合 CopyOpacity Alpha 合成方法之前,通常都需要先关闭 alpha。如果不这样做,合成运算符会复制已启用的透明度(不透明度通道),而不是使用你想要的灰度颜色。
Alpha Set 或「-alpha set」
「Set」这种 alpha 方法与较旧的「[-alpha on](https://imagemagick.org/command-line-options/#alpha)」选项相同。它确保图像拥有“透明度”即 alpha 通道,但如果该通道此前不存在或已被关闭,则会将其初始化为完全不透明(参见下文的 Alpha Opaque 方法)。不过,如果图像已经存在并启用了 alpha 通道,则不会做任何改动。换句话说,该运算符可以在不改变图像当前在内存中外观的前提下,确保 alpha 通道存在。因此,单独使用该运算符本身不会给图像带来任何可见变化,但与其他运算符组合使用时会产生实际效果。所以,如果你先用 Alpha Off 关闭 alpha 通道,再用 Alpha Set 重新启用它,图像将拥有 alpha 通道,但它会是完全不透明的,也就是发出「Set」请求那一刻图像的外观。 |
magick moon.png -alpha off -alpha set alpha_set.png
![[IM Output]](../static/img/masking/alpha_set.png)
如果将其应用于已经启用了 alpha 通道的图像,则不会产生任何变化。 |
magick moon.png -alpha set alpha_noset.png
![[IM Output]](../static/img/masking/alpha_noset.png)
总而言之,这个运算符在被应用的那一刻,绝不应该改变图像的外观。它只是确保 alpha 通道被设置成使图像保持_原样_。这种方法通常用在读取图像之后,因为这些图像来自未知的图像文件格式或输入源,可能带有也可能不带有 alpha 通道。这个运算符会确保图像确实拥有 alpha 通道(针对 JPEG 这类图像格式),同时对任何已启用且已存在的 alpha 通道(例如 GIF 或 PNG 格式)保持原样。这是在把图像读入内存后,或者更重要的是,在对图像做过处理之后想要重新启用一个干净的 alpha 通道时,确保图像拥有 alpha 通道的推荐方法。
Alpha On
「[-alpha](https://imagemagick.org/command-line-options/#alpha) On」与前面介绍的 Alpha Off 方法正好相反。通常这种方法对你想要达成的目的来说_过于简单_,因此应当极少使用。几乎在所有情况下你都应该使用「[-alpha Set](#alpha_set)」。基本上,「On」方法只是把开关切换回去,使图像的透明度数据重新变得可见。已有的透明度数据不会被修改,所以如果内存中的图像仍然保留着某些旧的 alpha 通道数据,那些数据就会突然重新变得可见。例如,这里我们先把透明度数据“Off”,然后立刻把它切回“On”,从而重现原始图像。 |
magick moon.png -alpha off -alpha on alpha_on.png
![[IM Output]](../static/img/masking/alpha_on.png)
但是,如果图像(目前)还没有任何以前的 alpha 数据,它就会被初始化为完全不透明——这是合乎逻辑的做法。因此,对于刚刚读入内存的新图像来说,这与 Alpha Set 是等效的,但不应该为此目的使用它。Alpha On 唯一应当使用的场合,是你此前出于某种原因_有意关闭了 alpha_,现在希望恢复那份数据。例如,在应用某些非常特殊的运算符(比如「[-shade](https://imagemagick.org/command-line-options/#shade)」)之前,先关闭再打开 alpha 通道可以用来保留 alpha 通道数据。关于这种特殊用法的示例,请参见带阴影的形状图像。
Alpha Activate/Deactivate
分别以持久化的方式启用和禁用 alpha 通道。这类似于 ImageMagick 6 中的 on/off。在 ImageMagick 7 中,-alpha off 会永久移除 alpha 通道,因此 -alpha on 将无法重新启用它。
Alpha Discrete
Alpha Opaque
这个方法不仅确保 alpha 通道处于“激活”状态,还确保它完全不透明——无论图像的透明度此前是“启用/on”还是“停用/off”。例如……
|
magick moon.png -alpha opaque alpha_opaque.png
![[IM Output]](../static/img/masking/alpha_opaque.png)
在较旧版本的 IM 中,这等同于先用「[-alpha off](https://imagemagick.org/command-line-options/#alph)」关闭 alpha 通道,再用「[-alpha on](https://imagemagick.org/command-line-options/#alpha)」把它打开,同时把它重置为不透明。 |
magick moon.png -alpha off -alpha on alpha_opaque_matte.png
![[IM Output]](../static/img/masking/alpha_opaque_matte.png)
经过这个操作后,图像原本的“形状”就再也无法恢复了,因为原始的 alpha 通道数据已被覆盖。当然,这也等同于使用「-alpha off -alpha set」,不过这种情况下你不如直接用「-alpha opaque」。
Alpha Transparent
类似地,这会确保 alpha 通道处于“激活”状态,同时也完全透明。 |
magick moon.png -alpha transparent alpha_transparent.png
![[IM Output]](../static/img/masking/alpha_transparent.png)
图像的颜色数据依然存在,所以之后再关闭透明度,就会重新显示出图像原有的颜色。 |
magick moon.png -alpha transparent -alpha off alpha_transparent_off.png
![[IM Output]](../static/img/masking/alpha_transparent_off.png)
当然,图像原本的“形状”实际上已经被破坏了,所以这个操作之后就再也无法恢复。使图像完全透明的其他方法,参见透明画布。
Alpha Extract
「Extract」方法会直接把图像的“alpha”遮罩以灰度通道遮罩的形式复制出来。
magick moon.png -alpha extract alpha_extract.png
请注意,完全不透明为白色,而完全透明为纯黑色。由于图像沿边缘包含一些半透明像素(用于抗锯齿,使图像形状看起来更平滑),这张图像并非纯粹的黑白,边缘周围也包含一些灰色像素。如果你的 ImageMagick 是较旧的 IMv7 版本,下面这种使用通道提取的方法(近似)等效。
magick moon.png -channel a -separate +channel -negate alpha_extract.png
「[Extract](#alpha_extract)」方法也会把 alpha「[关闭](#alpha_extract)」,但并不会清除它,所以把 alpha 通道再切回「[On](#alpha_extract)」,就会重新生成原始图像的形状遮罩。 |
magick moon.png -alpha extract -alpha on alpha_extract_on.png
![[IM Output]](../static/img/masking/alpha_extract_on.png)
请注意,所有原始颜色都会被替换为白色,边缘周围则呈现出不同深浅的灰色。如果我们用白色背景移除透明度(参见下文的 Alpha Remove 方法),就能看到这一点。 |
magick alpha_extract_on.png -background white -alpha remove alpha_edge.png
![[IM Output]](../static/img/masking/alpha_edge.png)
这些“灰色”像素实际上被巧妙地用在从抗锯齿形状生成边缘轮廓中,用来从图像形状生成平滑的边缘或轮廓。这种保存 alpha 通道的副作用,在使用不理解也不使用图像 alpha 通道的 Shade 运算符时尤其有用。参见子章节带阴影形状的遮罩。
Alpha Copy
「Copy」方法与「[Extract](#alpha_extract)」相反,本质上是对自身执行一次 CopyOpacity。也就是说,它会把一张灰度图像(无论其 alpha 通道是否启用)转换为一张形状遮罩图像。
magick alpha_extract.png -alpha copy alpha_copy.png
图像原本是否有 alpha 通道并不重要,它所做的只是根据图像的灰度值生成图像的透明度。一旦有了形状遮罩,你就可以使用各种颜色着色或 Duff-Porter Alpha 合成方法为它上色。关于使用形状遮罩的示例,参见作为彩色形状的遮罩。
Alpha Shape
为了更方便地使用灰度图像,「Shape」方法不仅会(像 Alpha Extract 那样)生成形状遮罩,还会用当前的背景色为其上色。
magick alpha_extract.png -background Yellow -alpha shape alpha_shape.png
这意味着,你可以先把图像“形状化”,再把它平铺到另一种背景色上,从而非常快速地为一张灰度遮罩上色。 |
magick alpha_extract.png -background Yellow -alpha shape \
-background Blue -alpha remove alpha_colormask.png
![[IM Output]](../static/img/masking/alpha_colormask.png)
| 背景色其实并不是应该用于这种“shape”上色操作的正确颜色。应该用“fill”颜色来设定形状的前景色。因此,具体该用哪种颜色以后可能会改变。之所以现在使用背景色,只是因为内部实现上难以访问当前的 fill 颜色。这项改动很可能会作为 IMv7 的一部分实现。
---|---
当然,把黑白图像直接映射到特定颜色,更快也更好的方法是使用更专门的按颜色的色阶调整。这样就不需要启用或修改图像现有的透明度通道。 |
magick alpha_extract.png +level-colors Blue,Yellow level_color.png
![[IM Output]](../static/img/masking/level_color.png)
| 上面的操作是在线性色彩空间中做颜色映射,可能需要在某个环节转换为 sRGB,才能得到视觉上更正确的颜色渐变。
---|---
Alpha Remove
「[-alpha](https://imagemagick.org/command-line-options/#alpha) **Remove**」方法(IMv6.7.5 起新增)用于借助当前的「[-background](https://imagemagick.org/command-line-options/#background)」,从图像中移除透明度。 |
magick moon.png -background tan -alpha remove alpha_remove.png
![[IM Output]](../static/img/masking/alpha_remove.png)
请注意,虽然透明度被“移除”了,但 alpha 通道仍会保持开启,只是现在完全不透明。如果你不再需要 alpha 通道,可以用 Alpha Off 禁用它。这个操作简单快速,不需要额外的内存开销,也没有其他替代性透明度移除技术可能带来的副作用。因此,它是移除图像透明度的首选方式。关于其他技术,或者如果你的 ImageMagick 版本比 v6.7.5 更旧,请参阅下文更详细的讨论移除图像的透明度。
Alpha Background
自 IM v6.5.2-10 起,新增了一个「Background」方法,可以把完全透明像素的隐藏颜色设置为当前的背景色。通常这种颜色无关紧要,因为只有当 alpha 通道被关闭时才能看到它。然而,完全透明像素的颜色会被保存进 PNG 图像文件格式中,对于大图像来说,含有随机未知的完全透明颜色会显著影响其压缩效果。详情参见压缩效果更好的 PNG以及 IM 论坛讨论消除 alpha 通道垃圾数据。请注意,这里不会进行任何颜色混合,只是对完全透明的颜色做直接的颜色赋值。不过这些像素依然保持完全透明,因此你不会看到图像有任何变化。 举个例子,这里我用它把所有完全透明的像素设置为「HotPink」。 |
magick moon.png -background HotPink -alpha Background moon_hotpink.png
![[IM Output]](../static/img/masking/moon_hotpink.png)
如你所见,这并没有改变图像实际的外观。为了看到变化,我们现在来关闭 alpha 通道。 |
magick moon_hotpink.png -alpha off moon_hotpink_off.png
这与移除透明度不同
形状的边缘会使所有半透明像素变为不透明,结果产生了相当明显的锯齿(阶梯状)边缘效果。请注意,即便是通常只支持不透明的 PNG24 格式,只要所有完全透明的颜色都相同,也依然可以保存布尔值透明度。详情参见PNG 子格式中的示例。这种替换颜色的处理,实际上几乎等同于执行「-channel RGB -fill _color_ -opaque None +channel」。参见直接替换颜色。请注意,许多其他图像处理运算符也会把任何完全透明的像素变为完全透明的黑色(颜色「None」),因为这在数学上相当于零值。下面是一些已知会这样做的图像操作汇总,不过它们都不如这个运算符那样直接或快速。
magick moon.png \( +clone -alpha off \) \
-compose SrcIn -composite moon_black.png
magick moon.png -channel RGBA -blur 1x.000000001 moon_black.png
magick moon.png -channel RGBA -gaussian 1x0 moon_black.png
magick moon.png -fuzz 0% -transparent none moon_black.png
最后一种方法(参见模糊系数与透明色)特别有用,因为你不仅可以把所有透明颜色都设为完全透明的黑色(「None」),还可以只指定一个模糊系数,就把所有接近完全透明的颜色(这些颜色虽然有效,但实际上几乎不可见)也一并处理掉。这会造成一些数据损失,但对于包含大量接近完全透明颜色的图像来说,可能会改善压缩效果。这些几乎完全透明的像素常常带有非常奇怪或错误的颜色,使用这种方法可以在它们造成其他问题之前先行移除。
移除图像的透明度
Alpha Off 只是简单地切换开关来关闭透明度通道,而如果你试图把图像保存为一种不允许使用透明度的文件格式,也会得到同样的效果。例如保存为 JPEG 时……
magick -size 70x60 xc:none -font Candice -pointsize 50 \
-fill Black -annotate +10+45 'A' -channel RGBA -blur 0x5 \
-fill white -stroke black -draw "text 5,40 'A'" a.png
magick a.png a.jpg
请记住,JPEG 文件格式并不保存 alpha(透明度)通道,因此它只是把该通道关闭了。在这种情况下,透明部分只是简单地变成了黑色(这是典型的结果)。但根据图像来源的不同,透明区域也同样可能变成其他随机的、或者不合适的颜色。另外在很多情况下,半透明像素可能带有一些非常奇怪的颜色,而这些颜色通常因为几乎完全透明而不可见。仅仅关闭透明度会让这些像素变得非常显眼,使结果看起来比你预期的还要糟糕。例如可以看看上面“A”左上方的边缘。无论哪种情况,仅仅关闭透明度通常都_不是_你想要的结果。_最好的解决办法_是使用 Alpha Remove 方法,快速简单地用背景色底衬替换透明度…… |
magick a.png -background skyblue -alpha remove -alpha off a_remove.jpg
![[IM Output]](../static/img/masking/a_compose.jpg)
严格来说,在这种情况下 Alpha Off 并不是必需的,因为保存为 JPEG 时会自动执行这一步。
另一种移除透明度的技术,是想办法生成一张新的“背景”或“画布”图像,然后把你的图像 Over 合成到那张背景上,以此替换透明度。最好同时保留原始图像的元数据,例如可能存在的配置文件、标签、说明和注释。生成这种画布的方法在创建同尺寸的图像画布中有示例。以下就是这样一种方法…… |
magick a.png \( +clone -alpha opaque -fill SkyBlue -colorize 100% \) \
+swap -geometry +0+0 -compose Over -composite \
-alpha off a_compose.jpg
![[IM Output]](../static/img/masking/a_compose.jpg)
其他更简单的做法,是使用一种内部会为你创建“克隆的背景画布”的操作,它会作为该运算符正在执行的更大规模图像处理操作的一部分而生成。最常见的方法是对图像做 Flatten(拼合)。这个运算符经常被用于此目的,以至于移除透明度的过程常被误称为“拼合(flattening)”。例如…… |
magick a.png -background skyblue -flatten -alpha off a_flatten.jpg
![[IM Output]](../static/img/masking/a_flatten.jpg)
不过这种方法对「[mogrify](basics.html#mogrify)」或者一系列多张图像不起作用,基本原因是「[-flatten](https://imagemagick.org/command-line-options/#flatten)」运算符本来就是设计用来把多张图像合并成一张图像的。另一种同样适用于多张图像的常见方法,是给图像加上一个尺寸为零、并指定合适「[-bordercolor](https://imagemagick.org/command-line-options/#bordercolor)」的边框。例如…… |
magick a.png -bordercolor skyblue -border 0 -alpha off a_border.jpg
![[IM Output]](../static/img/masking/a_border.jpg)
其他与上述方法密切相关的图像处理运算符,也可以移除图像的透明度,包括:Mosaic、Merge 和 Frame。Extent 运算符也可以使用,它能让你在移除透明度的同时扩展或裁切图像,但前提是你已经知道最终想要的图像尺寸。你并不一定要用纯色替换透明度。如果使用 DIY 合成(如上所示),你可以用任意图像作为替换背景。一个简单的例子是使用「[composite](basics.html#composite)」命令,用 (Dst_Over) 把一张图像平铺在原图像“下方”。这种合成方法能确保原始图像的元数据和尺寸得以保留。 |
magick composite -compose Dst_Over -tile pattern:checkerboard \
a.png a_undertile.jpg
![[IM Output]](../static/img/masking/a_undertile.jpg)
| _上述许多方法在处理过程中,要么会受到图像可能带有的虚拟画布信息的影响,要么可能会破坏这些信息。当涉及虚拟画布时,你可能需要更仔细地研究各个运算符的细节。在许多情况下,虚拟画布带来的影响对你整体的图像处理其实是有用的。
布尔值 Alpha 透明度
对于某些图像文件格式,你不需要完全移除 alpha 通道,而只需允许纯粹的开/关,也就是布尔值透明度。索引(调色板)类图像文件格式,例如 GIF 和 PNG8,就是典型代表。目前相关示例在GIF 布尔值透明度中讨论,但最终应该会移到这里来。
轮廓或光晕透明度
有时你会想在带有透明度的图像周围加上一圈轮廓。一种方法是使用 EdgeOut 形态学,快速获取原始图像所有相邻的像素,为它们上色,然后用 Under (DstOver) 合成把它叠加到原始图像上。
magick knight.png \( +clone \
-channel A -morphology EdgeOut Diamond +channel \
+level-colors red \
\) -compose DstOver -composite knight_outlined.png
这在从含有半透明边缘像素的 PNG 图像生成 GIF 格式图像时特别有用。它只提供最少量的背景色,而让图像的其余部分保持完全透明。关于这个问题的更多内容,参见图案背景上的 GIF。另一种方法是在形状周围生成一圈柔和的半透明光晕。为此我们要先模糊图像并重新上色,然后再次用 Under (DstOver) 合成把它叠加到原始图像上。 |
magick knight.png \( +clone \
-channel A -blur 0x2.5 -level 0,50% +channel \
+level-colors red \
\) -compose DstOver -composite knight_halo.png
![[IM Output]](../static/img/masking/knight_halo.png)
最后这个例子实际上很类似于使用 柔和轮廓复合字体 效果,只不过这里用的是一张带形状的图像,而不是被注释的文字。
在图像中使用遮罩
给图像做遮罩
如前所述,有几种方法可以给图像做遮罩,从而使图像的一部分变得透明。选择哪种方法,取决于你的图像遮罩是灰度遮罩,还是形状遮罩。
编辑图像遮罩
图像的遮罩是非常实用的东西。举例来说,我们可以通过修改原始图像的遮罩,非常轻松地擦除图像的一部分。请记住,「[-draw](https://imagemagick.org/command-line-options/#draw)」运算符无法“画出什么都没有”,目前也没有擦除选项。这里我们先创建一张图像,然后提取并修改它的遮罩,最后再把遮罩还原到原始图像上。
magick -size 100x100 xc:none -stroke black -fill steelblue \
-strokewidth 1 -draw "circle 60,60 35,35" \
-strokewidth 2 -draw "line 10,55 85,10" drawn.png
magick drawn.png -alpha extract mask.png
magick mask.png -fill black -draw "circle 40,80 60,60" mask_bite.png
magick drawn.png mask_bite.png \
-alpha Off -compose CopyOpacity -composite \
drawn_bite.png
请记住,遮罩中的“黑色”代表透明,“白色”代表不透明,所以我们只需要在不想显示的部分上画黑色即可。别忘了上面的“-alpha Off”操作,它对于确保这张灰度图像不含多余的透明度通道至关重要。就这样,我们从原始图像上“咬”掉了一口。我们也可以把移除的部分重新加回来。例如,这里我通过在遮罩上画白色区域,把从原始图像上去掉的“咬痕”的一部分重新加回来。然后再次使用 CopyOpacity 通道合成,把这张遮罩还原到原始图像上。
magick mask_bite.png -fill white \
-draw "circle 50,70 60,60" \
-draw "roundRectangle 78,5 98,25 5,5" \
-alpha off mask_bite2.png
magick composite -compose CopyOpacity mask_bite2.png drawn.png drawn_bite2.png
关于重新加回部分内容,这里有个提醒。通常情况下,ImageMagick 会把任何完全透明的颜色替换为黑色,这一般是因为运算符背后的数学运算本来就是这样工作的。毕竟它是完全透明的,其颜色理应无关紧要。这意味着,如果你让图像中此前从未画过的一部分变得不透明,它通常就会是黑色的,因为那正是图像透明度之下的颜色。不过在上面的例子中,你会注意到 PNG 图像文件格式正确保留了图像原本(被设为透明)的颜色。因此,重新加回部分的颜色仍然保持了原图的“SteelBlue”色。如果图像被保存成了其他文件格式,或者又经过了进一步修改,你就不应该指望这一点了。
下面是擦除图像局部的另一种方法:我们不再提取并修改灰度遮罩,而是改用 DstOut 合成方法,把一张形状遮罩当作一种“橡皮擦”工具来使用。
magick -size 100x100 xc:none -draw "circle 40,80 60,60" mask_shape.png
magick drawn.png mask_shape.png -compose DstOut -composite drawn_bite3.png
如你所见,有时形状遮罩更容易处理,因为可以避免提取和恢复 alpha 通道的麻烦。不过我在这里使用的Duff-Porter Alpha 合成方法,永远无法恢复已经被变为透明的颜色。使用这些方法时,任何被变为透明(因而颜色未定义)的部分都会一直保持透明。实际上,使用 Alpha 合成方法擦除图像的一部分,实际上会破坏完全透明像素底下的颜色,而不会保留它。毕竟,透明色其实根本不算是真正的颜色!
作为彩色形状的遮罩
除了仅仅用遮罩为图像添加或重新添加透明度之外,另一种做法是把遮罩以各种方式直接与图像结合起来。例如,假设我们只是想把遮罩当作一个符号或形状,以各种颜色叠加到某张图像上。为此我们需要一张遮罩,我会从一种特殊的“符号”字体中提取它。 |
magick -font WebDings -pointsize 24 label:Y \
+trim +repage -negate heart_mask.gif
![[IM Output]](../static/img/masking/heart_mask.gif)
请注意,我对标签图像做了取反(negate),使其成为一张正确的遮罩图像:黑色背景(透明)配白色前景(不透明)。自 IM v6.4.3-7 起,把灰度遮罩变成彩色形状最简单的方法,是使用 Alpha Shape 运算符,它与 Alpha Copy 完全相同,只是多了一步为最终形状上色的操作。 |
magick heart_mask.gif -background Red -alpha Shape heart_red.png
![[IM Output]](../static/img/masking/heart_red.png)
| 注意这里生成的形状图像使用的是“PNG”图像格式而不是 GIF,以避免 GIF 布尔值透明度 带来的问题。
---|---
在此之前,最简单的解决办法是先把 alpha 遮罩取反成一张 matte 通道图像,然后用 Combine 生成带形状的图像。 |
magick heart_mask.gif -negate \
-background Gold -channel A -combine heart_gold.png
![[IM Output]](../static/img/masking/heart_gold.png)
在这种情况下,形状遮罩的颜色由「[-background](https://imagemagick.org/command-line-options/#background)」颜色决定,Combine 用它来填充新图像中未定义的通道。一种更老但更复杂的方法,是使用「[CopyOpacity](compose.html#copyopacity)」合成方法,把图像的透明度设置为给定的遮罩,然后用均匀色彩着色为得到的形状上色。这种方法确实可行,长期以来也是最佳做法,但现在已不再推荐使用。 |
magick heart_mask.gif \( +clone \) -alpha off \
-compose CopyOpacity -composite \
-fill HotPink -colorize 100% heart_hotpink.png
![[IM Output]](../static/img/masking/heart_hotpink.png)
现在你已经有了一张“形状化”的图像,接下来只需使用众多图像分层技术和Alpha 合成方法中的一种,把它叠加到任意背景上,例如内置的玫瑰图像。 |
magick rose: -page +2+2 heart_gold.png \
\( +clone -repage +7+29 \) \
\( +clone -repage +52+14 \) \
-flatten rose_with_love.gif
![[IM Output]](../static/img/masking/rose_with_love.gif)
如果我们想让所有符号都用同一种颜色,这样做没问题,但如果想使用多种颜色,就需要多张中间图像,这在要叠加大量不同颜色的符号时并不现实。制作多色叠加图的一种方法,是在读入图像后立即为形状化的图像重新上色。 |
magick rose: \( heart_gold.png -repage +2+2 \) \
\( +clone -fill Red -colorize 100% -repage +7+29 \) \
\( +clone -fill HotPink -colorize 100% -repage +52+14 \) \
-flatten rose_colored_love.gif
![[IM Output]](../static/img/masking/rose_colored_love.gif)
请注意,我们只读入了一张形状化的图像,然后为每一个要叠加的新“图层”重新为该图像的一份克隆上色。关于为基础图像重新上色的更多示例,请参阅颜色修改整章内容。另请参阅绘制符号,了解在图像中标记特定位置的另一种方法,以及地图钉标注示例,了解更自动化的分层技术。
数学合成
除了把遮罩叠加到某种背景上之外,你可能只对用遮罩本身的白色或黑色部分为图像上色感兴趣。这相对简单,只需使用一些数学 Alpha 合成方法,把遮罩的颜色替换成某种颜色、图案或其他图像即可。例如,「[Multiply](compose.html#multiply)」合成方法会用叠加图像替换白色区域(乘法值为 1),而把黑色区域(乘法值为 0)保持为黑色。「[Screen](compose.html#screen)」运算符与「[Multiply](compose.html#multiply)」完全相同,只是图像被取反了,因此它实际上是替换图像的黑色区域。举例来说,让我们用上面那张较大的遮罩图像,去叠加一张由平铺图案生成的更大图像。
magick mask_bite.png -size 100x100 tile:tile_disks.jpg \
-compose Multiply -composite compose_multiply.png
magick mask_bite.png -size 100x100 tile:tile_water.jpg \
-compose Screen -composite compose_screen.png
「[Multiply](compose.html#multiply)」Alpha 合成方法对于替换文字图像(例如:白色背景上的黑色文字)的背景特别有用,比如那些从 Postscript 文档生成的图像。
使用遮罩的 Alpha 合成
特殊的三图像形式的使用遮罩的 Alpha 合成,让你可以用同一张遮罩直接把两张图像合并在一起。 |
magick -size 100x100 tile:tile_water.jpg tile:tile_disks.jpg \
mask_bite.png -composite compose_masked.png
![[IM Output]](../static/img/masking/compose_masked.png)
第一张图像会替换遮罩中黑色的背景部分,第二张图像则替换遮罩中白色的前景部分。遮罩本身作为第三张图像给出。遮罩用于挑选并混合两张不同的图像,生成最终结果。这其实很像是两张图像经过映射之后的 Blend。请记住,结果图像最终的尺寸和元数据来自上述操作中的第一张“背景”图像(黑色部分),因此如果你想反过来,就要交换图像顺序并对遮罩做取反(Negate)。最后请记住,如果你使用「[composite](basics.html#composite)」命令而不是「[convert](basics.html#convert)」,“叠加”图像(白色部分)要放在前面,“背景”图像(黑色部分)放在第二位。换句话说,对于该命令,前两张图像需要互换顺序。
对齐两张带遮罩的图像
制作中
关于对齐两张带遮罩的图像……
如果你的遮罩是纯粹的布尔值遮罩,无论怎样应用都不会有问题。
但是,为了让边缘看起来“平滑”而带有“抗锯齿”、“灰色”或
“半透明”边缘的遮罩,如果处理不当、不够仔细,
可能会带来严重的麻烦。
本节余下的讨论都是关于“抗锯齿”遮罩的。
需要拼接在一起的抗锯齿遮罩有两种类型……
* 一种像拼图碎片那样拼合,或者
像形状吻合的插销插入形状吻合的孔洞(共享边界)
* 一种是打算叠加在一个实心区域之上的遮罩(分层式)
后一种很容易处理,也是你把一个彩色形状叠加到一张
完全不透明的图像上时通常得到的效果。本质上,
你只需用“over”合成来合成该形状即可。
前一种“拼图式”遮罩则更难处理。这类遮罩本来就不打算
相互重叠或彼此欠合。然而,如果你试图用显而易见、
通常使用的“over”合成来拼接它们,就会在“抗锯齿边缘”
合并的地方出现半透明的接缝。
_“拼图遮罩”拼接错误的示例(over)_
正确拼接遮罩以及带形状的“拼图”图像的方法,是使用
**Plus** 合成,以黑色或完全透明的背景,把图像
“相加”起来。
_“拼图遮罩”正确拼接的示例(plus)_
关于使用 'Dst-In'、'Dst-Out' 和 'Plus' 合成进行 DIY 图像拼接的
另一个示例,请参见……
https://usage.imagemagick.org/compose/#dstin
我还在三图像 Alpha 合成的 bug 报告中详细讨论了这种
拼接细节,参见[遮罩合成 bug
——已修复](https://usage.imagemagick.org/bugs/composite_mask/#correct)。
关于 'over' 与 'plus' 区别的更多内容,参见['Blend'(plus)与 'Dissolve'(over)的对比](compose.html#blend_dissolve)
正确拼接边缘对齐碎片的示例,参见
[3d 立方体 - 仿射](distorts.html#cube3d),以及再次出现在[3d 箱体 - 透视](distorts.html#box3d)
以及使用剪切的等距立方体
https://usage.imagemagick.org/warping/#sheared_cube
这些示例中最主要的问题在于,各个部件并不是用
同一张遮罩生成的,而是各自被扭曲到最终位置。
因此它们彼此之间并不能真正精确吻合,只是拼接在一起而已。
这些示例需要更新为使用 'Plus' 合成方法,
以生成改进后的结果,但即便如此,由于遮罩彼此并不精确
“吻合”,结果大概率仍然不会完全“正确”。
生成边缘对齐正确的遮罩
最好的思路是对两个部件都使用同一张遮罩(取反后的版本),
而不是尝试分别画出两张遮罩。否则你就会遇到目前为止
一直看到的那种问题:两张遮罩要么重叠,要么留有缝隙。
正确拼接遮罩的方法……
* 用遮罩为其中一个部件设置透明度
用取反后的遮罩为另一个部件设置透明度
用 'Plus' 把两个部件加在一起。
* 只用遮罩为一个部件添加透明度,然后
用 'Over' 把该部件合成到一张完整的图像上。
* 使用三图像遮罩合成
参见 https://usage.imagemagick.org/compose/#mask
以及 https://usage.imagemagick.org/masking/#masked_compose
它利用遮罩从两张不同的图像中选取结果。
请记住,'Over' 只需要给“源”或“叠加”图像加遮罩,
背景图像不应该带有需要对齐的半透明边缘。
但 'plus' 合成需要给两张图像都加上遮罩,
并且要用彼此精确的反向遮罩对齐拼接边缘。
警告:Draw 目前无法生成能够无缝拼合、
且不重叠的两个形状!!!!
详情参见[Draw 填充边界](draw.html#bounds)。
我还没有检查过 SVG 是否也有同样的问题。
特殊图像遮罩
写入遮罩 - 保护像素不被更改
“写入”遮罩或“裁剪遮罩”是一种特殊的灰度图像,会以相同尺寸添加到已有的图像上。它定义了图像中哪些区域会被大多数图像处理运算符归类为“不可变”或“不可写”。运算符「[-mask](https://imagemagick.org/command-line-options/#mask)」接受一张外部图像,将其链接到内存中的图像。该运算符的“plus”形式「[+mask](https://imagemagick.org/command-line-options/#mask)」会把遮罩从图像中移除。举例来说,这里我使用一张“写入遮罩”来保护背景像素不被写入,同时旋转色相,把前景的红玫瑰重新上色为蓝玫瑰。
magick rose: -mask rose_bg_mask.png \
-modulate 110,100,33.3 +mask rose_blue.png
这张遮罩有点粗糙,但效果不错。只需记住,“写入遮罩”是用来指定需要被保护或保留的部分。请记住,在 IMv7 中……
「[-mask](https://imagemagick.org/command-line-options/#mask)」运算符定义的是一张“写保护”遮罩
关于更高级的示例,参见色度键遮罩,那更多是关于如何生成遮罩,而不是把它当作写入遮罩来应用。写入遮罩或裁剪遮罩的设计目的,是在图像中的像素被直接修改时发挥作用。例如:negate、level、颜色着色、modulate、绘图、composite、形态学、卷积。对于会生成全新图像的运算符(resize、distort、extent 等),它将无法保留原始像素,因为遮罩无法与新的图像尺寸相对应。这类操作还会带来一个副作用,就是移除或取消设置图像的“写入遮罩”。这里再举一个例子……
magick -size 70x70 xc:red red_image.png
magick -size 70x70 xc: -draw 'circle 35,35 30,5' write_mask.png
magick red_image.png -mask write_mask.png \
-fill blue -opaque red +mask masked_color_replace.png
请注意,遮罩和结果图像的边缘都是平滑的(抗锯齿),这是因为该遮罩并不是单纯的布尔值遮罩,而是一张混合遮罩。“写入遮罩”之所以是混合遮罩,是因为遮罩中的“灰色”像素会根据其灰度值的多少,把新像素与旧图像值按比例混合。这会产生非常平滑的边缘,也能让你在被修改区域与未修改区域之间生成渐变。不过,混合遮罩虽然适合单次操作,但用于多次操作时可能效果不佳,因为它的混合效果会被多次叠加应用。这个问题在诸如形态学之类的循环操作中尤其明显——但仅限于你使用非布尔值的混合遮罩时。如果这成了问题,最好的办法可能是先对图像的一份副本执行所有操作,然后再对原始图像使用使用遮罩的 Alpha 合成。请谨慎处理。
这种遮罩用法实际上正是合成遮罩真正的工作方式!不过仅限于合成运算符被应用的这段时间内。
magick -size 70x70 xc:green green_image.png
magick red_image.png green_image.png write_mask.png \
-composite masked_composite.png
(使用取反后的遮罩)等价于…… |
magick write_mask.png -negate -write MPR:mask +delete \
red_image.png -mask MPR:mask \
green_image.png -composite +mask masked_composite_equiv.png
![[IM Output]](../static/img/masking/masked_composite_equiv.png)
也就是说,先对遮罩取反,然后应用到第一张“目标”图像上。接着第二张图像被合成到第一张图像上,只修改原始遮罩图像中“白色”的区域。
三图像的「[-composite](https://imagemagick.org/command-line-options/#composite)」操作使用了一张“写入”遮罩
在形态学中,写入遮罩通常用于生成某种操作的条件形态学或约束形态学形式。IM 讨论论坛中曾讨论过这样一个例子,清理文字周围的噪点,用来限制膨胀(dilation)的效果。注:-crop 本应能够保留各个图像的图像遮罩——通过同时裁切遮罩并把它赋给新图像。但目前尚未实现这一点。
裁剪遮罩与裁剪路径
该运算符的「[-clip-mask](https://imagemagick.org/command-line-options/#clip-mask)」形式,与上述内容几乎完全相同,只是它只提供布尔值(全有或全无)风格的遮罩。因此你无法得到“混合”或平滑的结果。例如…… |
magick red_image.png -clip-mask write_mask.png \
-fill blue -opaque red +clip-mask clipped_modulate.png
![[IM Output]](../static/img/masking/clipped_modulate.png)
如你所见,结果带有强烈的锯齿感(阶梯状边缘),因为「[-clip-mask](https://imagemagick.org/command-line-options/#clip-mask)」不会像「[-mask](https://imagemagick.org/command-line-options/#mask)」那样产生混合结果。它唯一的好处是速度稍快一点(不过也快不了多少)。这个运算符最初是为了处理 TIFF 图像文件中的裁剪路径而提供的,是一个非常古老的运算符(IMv5)。应该改用较新的「[-mask](https://imagemagick.org/command-line-options/#mask)」运算符。 | _在 IMv7 中,“写入遮罩”和“裁剪遮罩”是并行实现的,尽管从技术上讲它们执行的其实是完全相同的功能。因此你可以同时应用这两种遮罩。
不过并不建议同时使用两者,其结果是未定义的。此外,这种“布尔值遮罩”形式已从 IMv7 中移除。
TIFF 图像的裁剪路径
“裁剪路径”是 TIFF 图像文件格式的一部分,它定义了一条矢量路径,用来在 TIFF 图像中定义一个“有形状的区域”。在 IM 中,运算符「[-clip](https://imagemagick.org/command-line-options/#clip)」和「[-clip-path](https://imagemagick.org/command-line-options/#clip-path)」会读取这条“裁剪路径”,并把它转换成一张裁剪遮罩(如上文所述)。因此,它定义的是一张“写入遮罩”,用于保护该形状不被修改。存储在 TIFF 图像中的裁剪路径被定义为一条 SVG 路径绘制,你可以用以下方式从 TIFF 图像文件格式中提取它……
magick identify -format '%[8BIM:1999,2998:#1]' image_clip.tiff
人们常遇到的最大问题,是要把所有未被裁剪的部分都变成透明。这就要求你去写入那些被遮罩写保护起来的区域!这是一种解决方案:先把整张图像转换为透明,然后打开“裁剪路径”,再把现在可写的部分重新变为不透明(可见)。
magick input.tiff -alpha transparent -clip -alpha opaque -strip out.tiff
「[+clip](https://imagemagick.org/command-line-options/#clip)」运算符也会(就像「[+clip_mask](https://imagemagick.org/command-line-options/#clip_mask)」一样)关闭并移除裁剪遮罩。不过,没有任何图像文件格式会把当前的裁剪遮罩随图像一起保存。(至少在 IMv7 中是这样)
读取遮罩 - 忽略像素输入
需要注意的是,写入遮罩只会限制哪些像素会被写入图像,但并不会限制在执行操作、生成要写入的新像素数据的过程中,哪些像素会被“读取”。这基本上意味着,如果你使用某种“区域效果”或“邻域”类型的运算符,例如模糊、形态学或卷积,那么靠近边缘的“可写像素”可能会包含来自被遮罩、不可写区域的颜色值。举例来说,这里我们在模糊图像之前先对前景的玫瑰做写保护。也就是说,我们只想对图像的背景部分做(在这个例子中相当强烈的)模糊。
magick rose: -mask rose_fg_mask.png \
-blur 0x8 +mask rose_bg_blur_fail.png
如你所见,尽管前景色被遮罩保护了起来,但这些颜色仍然被用作模糊玫瑰周围背景的一部分。正因为如此,靠近前景的模糊背景带有一圈明显的红色调,也就是光晕。换个说法,前景色“渗漏”到了周围的背景中。当人们想要模糊图像背景(比如作为镜头对焦效果的一部分)时,这通常不是他们想要的结果。人们真正想要的,是让模糊完全“忽略”前景像素,只让背景的颜色参与模糊处理。也就是说,他们想阻止模糊“读取”前景像素。
IMv7 的读取遮罩解决方案
在 IMv7 中,让某个像素颜色变得不可读的唯一方法,就是让该像素变得透明。根据定义,透明像素是没有颜色的,因此那个“隐藏的颜色”不会参与模糊运算所做的计算。这给了我们一个“取巧”的办法:让前景像素变透明,应用模糊(或其他)操作,再关闭透明度(在这个例子中我们其实并不需要它)。然后我们就可以恢复图像的前景部分。如果这听起来很复杂,那确实很复杂。以下是具体涉及的步骤,同时展示中间图像,尽量让这项技术清晰易懂……
magick rose: rose_bg_mask.png -alpha off \
-compose CopyOpacity -composite +compose rose_bg_only.png
magick rose_bg_only.png -channel RGBA -blur 0x8 rose_bg_blurred.png
magick rose_bg_blurred.png -alpha off rose_bg_blur_opaque.png
magick rose_bg_blur_opaque.png \
rose: rose_fg_mask.png -composite rose_bg_blur_good.png
结果是,此前“渗漏”到模糊背景中的红色光晕效果被消除了。下面是背景模糊的写入遮罩版本与读取遮罩版本的并排对比,让你能清楚看到我们是如何去除前景色向背景“渗漏”的问题的。 ![[IM Output]](../static/img/masking/rose_bg_blur_fail.png)
写入 | ![[IM Output]](../static/img/masking/rose_bg_blur_good.png)
读取
---|---
两种遮罩方法的差异
上面的例子假设原始图像没有 alpha 通道。如果图像本身也含有 alpha 通道,那么你就需要把 alpha 单独分离出来处理,工作量会翻倍。这方面的一个例子出现在一次“distort resize”的讨论中,当时的需求是忽略使用 distort 调整图像大小时周围的虚拟像素。详情参见正确的 Resize(使用 distort)。另请注意,上述内容与一种模糊版的孔洞填充技术密切相关,唯一的区别是这里要保护不被修改的是背景而不是前景,这使得处理起来稍微简单一些。真正的“读取遮罩”应该会在 IMv7 中提供,届时上述操作就可以简化为同时添加一张“读取遮罩”,并可选地再加一张“写入遮罩”。
区域与区域子图像
区域是另一种把操作效果限制在图像较小区域内的方法。举例来说,这里我把整个矩形区域染成红色…… |
magick koala.gif -region 40x33+15+5 -fill red -colorize 50% \
koala_region_red.gif
magick koala.gif -alpha set \
-region 40x33+15+5 -alpha transparent koala_region_trans.gif
![[IM Output]](../static/img/masking/koala_region_trans.gif)
请注意,在让“区域图像”变透明之前,我需要先确保原始图像启用了 alpha 通道。如果不这样做,IM 会让“区域图像”中的透明部分变成透视效果,你就看不到任何变化。详情参见下文的区域的工作原理。 | 在 IM v6.6.9-5 之前,透明度的保留是有 bug 的,区域中透明度的结果总是“透视回原始图像”。因此,即便图像本身允许使用透明度,上面这个操作的结果也不会包含任何透明像素。
---|---
使用区域最大的理由,不仅在于它能把效果限制在一小块区域内,而且它实际上会把图像的那一矩形区域提取出来,并把后面所有的简单操作都应用到这块较小的区域上。这意味着,如果你只是想修改一张非常大的图像中很小的一块区域——比如做红眼消除——你不仅能把操作范围限制在那块区域内,执行速度也会_快得多_,而且提取出来的区域图像本身也更小。总结一下……写入遮罩会在整张图像上执行操作,但限制实际被更改的像素;而区域使用的是一张更小的、提取出来的子图像。请注意,没有任何东西阻止你同时使用这两种方法。不过,如果你把裁剪遮罩应用到一个区域上,该裁剪遮罩的尺寸应该与提取出来的区域图像相匹配。
对局部区域进行扭曲
由于“图像区域”实际上会提取出原始图像的一张“小型子图像”用于处理,你可以借助特殊的“局部化” 圆形扭曲,对原始图像的小块区域进行扭曲。举例来说,这里有一排条纹。
magick -size 600x70 xc:darkred \
-fill white -draw 'roundrectangle 5,5 595,65 5,5' \
-fill black -draw 'rectangle 5,25 595,31' \
-fill red -draw 'rectangle 5,39 595,45' \
lines.gif
现在,通过定义多个区域,我们可以在不同区域用不同方式对这条线做扭曲。
magick lines.gif \
-region 90x70+10+0 -swirl 400 \
-region 90x70+100+0 -swirl 400 \
-region 90x70+190+0 -swirl -400 \
-region 120x70+280+0 -implode 1.5 \
-region 100x70+380+0 -implode -7 \
-region 101x70+480+0 -wave 10x50 -crop 0x70+0+10\! \
+region lines_regions.gif
请注意,「-implode」和「-swirl」与区域的搭配非常契合,因为它们都具有这样的特性:扭曲图像的外缘会与所定义区域之外的其余图像部分完美衔接。也就是说,它们本身就是为了执行“局部化图像扭曲”而设计的。请注意,我在使用 Wave 扭曲时,不得不裁切结果“wave”图像的尺寸,使其能够重新贴合它被提取出来的原始区域。请记住,区域只有在配合简单图像处理运算符使用时才有效。任何其他运算符(包括另一个「-region」运算符)都会在该操作被应用之前取消区域处理。
区域的工作原理及其问题
实际上,区域的工作方式是……
- 根据「
-region」运算符,用给定的区域参数做一次简单裁切,从图像中提取出一张较小的图像。 - 把后续紧跟着的所有简单图像处理运算符应用到这张较小的图像上。
- 当遇到一个非简单的图像运算符,或者又出现一个「
-region」运算符,或者用「+region」关闭了区域功能时,就把提取出的区域按其原来的提取位置叠加回原始图像上。
区域的工作方式与使用图像堆栈运算符有些类似,不过它在这些运算符出现之前很久就已经存在于 ImageMagick 中了。举例来说,它是 IM 版本 5 中不可或缺的一部分。例如,如果你有这样一个区域操作……
... -region WxH+X+Y ...simple-operators... +region ...
其结果等价于(针对单张图像)……
... \( +clone -crop WxH+X+Y ...simple-operators... \
\) -geometry +X+Y -composite ...
或者是这样(针对多张图像)……
... \( -clone 0--1 -crop WxH+X+Y ...simple-operators... \
null: +insert \) -geometry +X+Y -layer composite ...
“区域图像”实际上是怎样被叠加回“原始图像”上的,这一点有点微妙……如果原始图像没有启用透明度通道,“区域图像”就会用 Over 合成来合成。这意味着,区域图像中的透明区域会变成透视效果,让你能看到它背后的原始图像。举例来说,这里我特意关闭了原始图像的透明度,然后对该区域做旋转,从而在角落处产生一些透明区域。 |
magick koala.gif -alpha off -region 30x30+10+10 \
-alpha on -background None -rotate 30 koala_region_rotate_1.gif
![[IM Output]](../static/img/masking/koala_region_rotate_1.gif)
如你所见,旋转区域的四个角(在“区域图像”中本是透明的)显示出了“原始图像”。基本上,由于原始图像无法处理透明度,区域图像只是被简单地叠加上去,四角呈现出透视效果。如果原始图像确实包含处于激活状态的透明度,那么被修改的区域图像中的透明度也可以被一并修改,因此透明度会被直接“复制”过去。 |
magick koala.gif -alpha set -region 30x30+10+10 \
-background None -rotate 30 koala_region_rotate_2.gif
![[IM Output]](../static/img/masking/koala_region_rotate_2.gif)
如你所见,IM 使用的是 Copy 合成,因此区域图像中存在的任何透明度也都会被复制到原始图像上。如果出于某种原因,你希望原始图像保留它本来的透明度,可以先关闭 alpha,等区域图像恢复之后,再把它重新打开,从而把透明度恢复原状。
被放大或缩小的区域图像,可能无法“正好”贴合回原来的位置。举例来说,这里我调整(并重新上色)区域图像的大小,使其变小…… |
magick koala.gif -region 30x30+10+10 \
-resize 75% -fill red -colorize 30% koala_region_shrink.gif
![[IM Output]](../static/img/masking/koala_region_shrink.gif)
如你所见,恢复回来的区域图像并没有覆盖原来的区域。因此,未被覆盖的那部分就没有被替换。反过来,如果区域变大,叠加回去的区域图像就可能覆盖原始图像更多的部分。 |
magick koala.gif -region 30x30+10+10 \
-resize 150% -fill red -colorize 30% koala_region_enlarge.gif
![[IM Output]](../static/img/masking/koala_region_enlarge.gif)
在这两种情况下,区域左上角的偏移都不会移动。你无法简单地缩小区域图像并让它在区域范围内居中,也无法把区域图像放到其他位置。应当谨慎行事,避免区域图像发生尺寸变化。不过在一些特殊情况下,你仍然可以处理被调整过大小的区域,关于这方面的示例,参见上面的“wave 扭曲”示例。 | _与「[mogrify](basics.html#mogrify)」类似,你无法合并多张子图像,因为那需要用到非简单的图像操作。不过你可以使用「[-draw](https://imagemagick.org/command-line-options/#draw)」作为一种替代的合成方法。相关示例参见Mogrify 中的 Alpha 合成。
_
---|---
| _截至本文撰写时,“区域图像”仍然保留着从原始图像中提取时带有的裁切虚拟画布偏移。这究竟算不算 bug,取决于你是否觉得这个信息有用。区域图像被恢复时,目前并不会用到这个偏移量。
如果你不想要这个偏移(因为它会干扰诸如Distort之类的运算符),可以在「[-region](https://imagemagick.org/command-line-options/#region)」选项之后紧跟一个「[+repage](https://imagemagick.org/command-line-options/#repage)」运算符,以移除区域图像的偏移。移除或修改它并不会影响区域图像恢复回原始图像的过程。
_
---|---
背景去除
图像处理中最常见的问题之一,就是要从一张已有的完全不透明图像中生成遮罩。这类图像通常是从万维网上下载的,或者由程序生成,抑或来自不提供任何透明度形式的图像格式。也可能是你手头有某个物体的照片,想要移除背景。请记住,照片本身并不理解透明度的概念,所以你需要自己动手移除不需要的部分。遗憾的是,这个问题并没有通用的解决方案,尤其是当你还想保留图像可能带有的半透明边缘时更是如此。因此,完成这项任务的方法及其变体有成百上千种,具体取决于确切的情况。与图像遮罩密切相关的,是根据图像即将叠加到的背景来调整透明度。这方面的内容,在保存为只允许布尔值透明度的 GIF 图像文件格式 时有详细讨论。
给简单背景做遮罩(漫水填充)
当图像的背景是单一的纯色时,你通常只需做一次图像中的颜色替换,就能生成简单的遮罩(以及背景去除)。举例来说,下面是对一张纯色背景图像直接进行漫水填充遮罩处理的例子。 |
magick cyclops.png -alpha set -channel RGBA \
-fuzz 1% -fill none -floodfill +0+0 white \
cyclops_flood_1.png
![[IM Output]](../static/img/masking/cyclops_flood_1.png)
好吧,这并没有奏效,因为右上角的漫水填充“种子”点实际上并没有覆盖到图像的所有部分!!!解决办法是把图像稍微放大一些,从而给漫水填充提供一条能够到达图像所有外部边缘的路径。不过要做到这一点,你需要知道背景的颜色。 |
magick cyclops.png -bordercolor white -border 1x1 \
-alpha set -channel RGBA -fuzz 1% \
-fill none -floodfill +0+0 white \
-shave 1x1 cyclops_flood_2.png
![[IM Output]](../static/img/masking/cyclops_flood_2.png)
当然,我们没有指定一个很好的模糊系数。这样做的问题在于,图像中的对象周围会出现光晕。这是因为大多数图像沿边缘都含有一些特殊像素,用来让图像的外观更平滑。不过,由于这张图像相对于背景有一圈很明显的黑色边框,使用较大的模糊系数设置,就可以很好地把图像从背景中分离出来。 |
magick cyclops.png -bordercolor white -border 1x1 \
-alpha set -channel RGBA -fuzz 20% \
-fill none -floodfill +0+0 white \
-shave 1x1 cyclops_flood_3.png
![[IM Output]](../static/img/masking/cyclops_flood_3.png)
这种技术有一些问题。首先,它对图像做的是全有或全无式的遮罩处理,生成的边缘带有锯齿、呈阶梯状,往往很难看。这对于功能有限的 GIF 图像文件格式来说没什么问题,但如果你打算把这张图像叠加到另一个背景上,效果就不太好了。而且,要捕捉到每一个抗锯齿边缘像素也非常非常困难。因此,如果我把上面的图像叠加到黑色背景上,你可能会看到一些明显比正常情况白得多的像素。 |
magick cyclops_flood_3.png -background black -flatten \
cyclops_flood_3_over.png
![[IM Output]](../static/img/masking/cyclops_flood_3_over.png)
另外,即便你设法用了足够高的模糊系数,也很可能会遇到边缘像素所剩无几,或者“渗漏”进图像中心的问题。最后,像这样的直接漫水填充,对于不是单一纯色的背景根本不起作用。
切出带边框的对象
已经带有单色边框的图像,对这些背景去除方法来说有明显的优势,因为边框在图像“内部”与“外部”之间提供了一条清晰的边界,进而让我们可以用一种更好的方式来指定背景图像的边界。也就是说,我们不再指定哪些颜色应被视为背景,而是可以改为指定哪些颜色标记出被遮罩对象的边框。此外,由于边框颜色是已知的,图像边缘周围只会混合出两种特定的颜色。也就是说,这两种颜色都是已知的,因此边缘应有多透明也就非常清楚了。
制作中
移除已知背景
把简单背景移除为一张“布尔值”遮罩相对直接,但当背景不那么简单时,事情就会变得更复杂。不过,如果背景本身是已知的,你就可以利用这一点,帮助从其他图像中把它移除。自 IM v6.3.4 起,新增了一种名为「[ChangeMask](compose.html#changemask)」的特殊Alpha 合成方法,可以直接从图像中移除已知的背景。举例来说,这里我们有一张未经改动的背景图像,以及一张被叠加了具有简单布尔值(纯粹开/关)透明度的 GIF 图像。通过使用「[ChangeMask](compose.html#changemask)」,我们可以恢复出那张原始的叠加图像(前提是它与背景差异很大)。
magick overlay_figure.gif overlay_bgnd.gif \
-compose ChangeMask -composite overlay_removed.png
基本上,它所做的是判断一张图像的像素与另一张图像相比“差异”有多大,如果差异小于当前的模糊系数,就把该像素变为透明。只有完全透明的像素才会被添加到图像中,其他情况下,原始图像会保持原样,包括其透明度在内。我们可以用较旧的「[Difference](compose.html#difference)」合成方法生成一张比较差异图像,来模拟这个运算符……
magick composite overlay_figure.gif overlay_bgnd.gif \
-compose Difference overlay_difference.png
如你所见,差异图像在所有未改变的部分都是黑色,而在发生改变的部分则呈现出各种混合颜色。通过分离各个颜色通道并把它们相加,再做阈值处理,我们就能得到一张遮罩,标出两张图像在任意通道上的差异。 |
magick overlay_difference.png -channel RGB -separate +channel \
-evaluate-sequence add -threshold 0 overlay_mask.png
magick overlay_figure.gif overlay_mask.png \
-alpha off -compose CopyOpacity -composite \
overlay_removed.png
![[IM Output]](../static/img/masking/overlay_removed.png)
如你所见,「[ChangeMask](compose.html#changemask)」合成方法让这个过程变得容易多了。不过,它只能提供“on/off”式的背景遮罩,无法实现结果的模糊或抗锯齿边缘,也无法实现透明的羽化效果。
差异图像遮罩与羽化
上述方法还可以进一步用于带有锯齿边缘、以及背景并不简单的图像。举例来说,这里我们有一张白色背景上的“独眼巨人”,我们想把它提取出来。接着,我们生成一张灰度图像,表示这张图像与背景色(由左上角像素定义)之间的差异。 |
magick cyclops.png \( +clone -fx 'p{0,0}' \) \
-compose Difference -composite \
-modulate 100,0 -alpha off difference.png
![[IM Output]](../static/img/masking/difference.png)
当然,这张差异图像直接拿来当遮罩用是不行的。如果你真这么做,实际上会让图像的大部分都变成半透明,而不仅仅是周围的背景。不过,从这张差异图像出发,根据你到底想实现什么效果,可以生成大量各种各样的透明度遮罩。我们可以调整上面的差异图像,生成一张遮罩,标出所有哪怕只与背景色有一丁点差异的像素。 |
magick difference.png -threshold 0 boolean_mask.png
magick cyclops.png boolean_mask.png \
-alpha off -compose CopyOpacity -composite \
cyclops_boolean.png
![[IM Output]](../static/img/masking/cyclops_boolean.png)
如你所见,“任意差异”这种布尔值判定,导致原始背景相当大一部分都被包含了进来。这是因为原始图像要么“抗锯齿”过,要么与背景之间有轻微的模糊(在这个例子中,原因是原始图像是从一张 JPEG 格式图像调整尺寸而来的)。如果原始图像本身就是一张布尔值叠加图像(例如叠加在背景上的 GIF 格式图像),这就不会成为问题——在那种情况下,你的结果会是完美的(参见上面的“ChangeMask”示例)。通过调整「[-threshold](https://imagemagick.org/command-line-options/#threshold)」,你可以给这张布尔值(仅 on/off)遮罩加上一个“模糊系数”,使遮罩更贴近图像本身。 |
magick difference.png -threshold 15% threshold_mask.png
magick cyclops.png threshold_mask.png \
-alpha Off -compose CopyOpacity -composite \
cyclops_threshold.png
![[IM Output]](../static/img/masking/cyclops_threshold.png)
请注意,独眼巨人图像的眼睛现在也被当成了一个透明的孔洞!这个“孔洞”凸显了整个技术最大的缺陷:图像对象中接近背景色、或者更糟糕地与背景色完全一致的部分,会被当成与背景相同。当然,对于像甜甜圈这类本身就带“孔洞”的对象图像来说,这可能是理想效果,但对我们的独眼巨人来说,“带孔的眼睛”绝对是个错误。原本的“光晕”效果,对于某些场景(比如文字)反而是理想的,可以在把文字再次叠加到某种“嘈杂”背景上时提高可读性。你可以在应用遮罩之前先对其做一点模糊处理,来强化光晕效果,使生成的“光晕”随距离逐渐减弱。 |
magick difference.png -bordercolor black -border 5 \
-threshold 10% -blur 0x3 halo_mask.png
magick cyclops.png -bordercolor white -border 5 halo_mask.png \
-alpha Off -compose CopyOpacity -composite cyclops_halo.png
![[IM Output]](../static/img/masking/cyclops_halo.png)
生成的“光晕”效果,还可以通过对遮罩图像使用更多直方图调整来进一步修改,让你能对特定图像的结果进行非常精细的控制。只要参数合适,你可以把周围的光晕调整到几乎不存在的程度,不过要用这种方式彻底消除它是很困难的。更好的技术请参见下一节。在生成阈值遮罩时,其实建议加入少量模糊(例如「-blur 0x0.707」,即 2 的平方根),只是为了让遮罩的边缘平滑一些。当然,这样一来结果就不再是布尔值了,所以不要试图把它保存成 GIF 格式的图像文件。这也是模糊羽化的一个例子。但要提醒的是,它与真正的使用距离对形状做羽化并不完全相同。不过,在处理我们上面生成的这类“位图”或“阈值遮罩”时,先做少量的模糊羽化,再做较大量的距离羽化,通常能得到最好的整体效果。
恢复半透明边缘
我们上面使用的差异遮罩技术,可以与之前的漫水填充遮罩技术结合使用,从而解决我们在更简单的遮罩技术中见到的大多数问题。这里我们来看一种多层遮罩技术,它应该能近乎理想地移除图像的背景,同时保留边缘上的抗锯齿阴影像素。不过,这仅限于处于已知背景之上、且前景像素具有良好对比“边缘”的图像。在这个示例中,我特意选用了一张非常难以分离的图像,它在边缘周围呈现出的阴影像素,比通常出于抗锯齿目的所需要的要多得多——一个带阴影效果的形状。 |
magick -size 70x60 xc:none -font Candice -pointsize 50 -stroke black \
-fill black -annotate +12+42 'A' -channel RGBA -blur 0x3 \
-fill tile_disks.jpg -annotate +10+40 'A' \
tile_water.jpg -compose DstOver -composite letter.png
![[IM Output]](../static/img/masking/letter.png)
首先我们需要生成一张差异图像,幸运的是我们确实知道背景图像是什么样的。当然,只要有足够好的对比度让我们能生成两张遮罩,这个方法同样适用于纯色背景。基本上,通过使用差异图像,我们可以消除背景图像带来的任何影响,并据此生成我们将要使用的遮罩。 |
magick letter.png tile_water.jpg \
-compose Difference -composite \
-modulate 100,0 -channel B -evaluate set 0 \
-alpha Off diff_mask.png
![[IM Output]](../static/img/masking/diff_mask.png)
请注意,这一次我对灰度差异图像做了一点处理,把它限制在红色和绿色通道,同时清空了蓝色通道,生成了一张黑黄色的差异图像。这样做有些讲究,因为它腾出了“蓝色”通道,让我们可以生成一张干净的漫水填充遮罩,与差异图像本身分开。理论上我也可以顺便清空绿色通道,把它用于第二张遮罩,不过我们先别操之过急。现在我们需要两张遮罩:一张是“外部”遮罩,定义所有肯定会是透明的区域;另一张遮罩,则用来定义图像中对象的内部,同时不会产生任何不需要的“孔洞”。那么,让我们用几种不同的模糊系数,把图像从外向内做漫水填充,这样我们就可以从中挑选出要使用的内、外两张遮罩。
for fuzz in 01 03 06 28 32 34; do \
magick diff_mask.png -fill blue -fuzz $fuzz% \
-bordercolor black -border 1x1 -floodfill +0+0 black \
-shave 1x1 diff_mask_$fuzz.png; \
done
![[IM Output]](../static/img/masking/diff_mask_01.png)
-fuzz 1% | ![[IM Output]](../static/img/masking/diff_mask_03.png)
-fuzz 3% | ![[IM Output]](../static/img/masking/diff_mask_06.png)
-fuzz 6% | | ![[IM Output]](../static/img/masking/diff_mask_28.png)
-fuzz 28% | ![[IM Output]](../static/img/masking/diff_mask_32.png)
-fuzz 32% | ![[IM Output]](../static/img/masking/diff_mask_34.png)
-fuzz 34%
---|---|---|---|---|---|---
上面这些图像中的蓝色区域,就是被遮罩标出的区域。请记住,我们正是为此清空了蓝色通道。第一张遮罩应该标出图像中我们确定希望完全透明的区域,也就是我们确定最终图像中一定会完全透明的部分。遮罩内部的区域应该仍然包含图像大部分的黑色光晕阴影。在这个例子中,由于图像本体与背景其余部分之间存在大量相互作用,我选择了「1%」这个仍然包含图像周围大片区域的模糊系数。在更典型的、没有阴影的情况下,这块区域可以更小,甚至可以小到 5 或 10 这样的非百分比数值。第二张遮罩的“模糊”值应该足够大,能够吃掉所有存在的半透明像素——也就是说,一直吃到、最好是稍微吃进图像的边框内部,但又不能完全去掉边框,或者“渗漏”进图像本体(参见上面最后一张图)。这张遮罩取反后,实际上就代表了最终图像中将完全不透明(因而代表内部)的所有像素。这种选择可能很难把握,可能需要大量试错才能找出最佳取值。对于这张图像,选用了非常高的模糊值「32%」,没有出现什么大问题。基本上,你希望尽量把它调高,使最终图像中不再包含任何原始“背景”像素,但又不能让遮罩吃掉(或渗漏进)图像内部。如果周围“边缘”颜色出现了缺口,甚至可能需要一点手工编辑才能把遮罩调整到恰到好处。现在,我们可以用这张遮罩来提取图像的“核心”,也就是内部。这部分我们确信不会与我们正在移除的背景图案产生任何半透明的相互作用。 |
magick diff_mask_32.png -channel blue -separate +channel -negate \
letter.png +swap -alpha Off -compose CopyOpacity -composite \
letter_inside.png
![[IM Output]](../static/img/masking/letter_inside.png)
请注意我是如何从经过漫水填充的遮罩图像中提取蓝色遮罩的。另外,由于漫水填充全有或全无的特性,遮罩在边缘周围会呈现出明显的阶梯状或锯齿效果。这正是第二张遮罩要帮我们解决的问题。请记住,这张图像只包含我们确定不会与原始背景产生相互作用的像素,在最终图像中会保持原样。它不包含任何阴影效果,也不包含我特别想恢复的抗锯齿像素。恢复那些像素,才是真正工作的核心所在。通过对遮罩做取反并相减(相乘),我们可以生成一张新的遮罩,定义出我们想要提取半透明边缘或阴影像素的区域…… |
magick diff_mask_01.png -negate diff_mask_32.png \
-channel blue -separate +channel -compose multiply -composite \
mask_aliasing_area.png
![[IM Output]](../static/img/masking/mask_aliasing_area.png)
接下来,这块区域会被用于从差异遮罩中提取抗锯齿像素,从而定义这些像素应该有多透明。我们对这些像素做归一化处理,得到一个从不透明到透明的平滑过渡。 |
magick diff_mask.png -channel red -separate +channel \
mask_aliasing_area.png -alpha Off -compose CopyOpacity -composite \
-background gray30 -compose Over -flatten -normalize \
mask_antialiased_pixels.png
![]()
上面这张遮罩中颜色越浅,对应的像素就越不透明;同样,颜色越深,就越透明。请注意,我在这里使用了灰色背景,以确保图像中存在的透明色不会干扰图像的归一化处理。如果不这样做,这次归一化就会失败。那块纯灰色本身并不重要,因为它们位于遮罩区域之外,之后会被忽略。既然我们已经得到了正确的透明度级别,接下来就需要知道这些半透明像素应该使用什么颜色。这个颜色通常应该与图像的边缘颜色相同,在这个例子中就是黑色。不过由于原始背景的相互作用,我决定为阴影选用深灰色 | _你需要想办法弄清楚半透明像素应该是什么颜色,这样才能为抗锯齿像素设置正确的颜色。
这可能是_
- 一种固定的边缘颜色(例如:本示例中的近黑色)
- 使用最近的完全不透明边缘像素的颜色(借助形态学。参见作为填充运算符的稀疏颜色)
- 计算得出:一旦你知道了 alpha 和背景色,就可以通过减去背景色来修正像素颜色。参见下文的使用两个背景进行背景去除。
基本上这取决于你的图像。
---|---
既然做到这一步了,不妨也重新给图像做一次遮罩,只留下这些特殊的边缘像素。 |
magick mask_antialiased_pixels.png mask_aliasing_area.png \
-compose multiply -composite -negate \
-background '#444' -channel A -combine letter_edging.png
![[IM Output]](../static/img/masking/letter_edging.png)
现在只需要把图像内部的“核心”与这些半透明的边缘像素叠加成图层。 |
magick letter_inside.png letter_edging.png \
-background none -flatten letter_recovered.png
![[IM Output]](../static/img/masking/letter_recovered.png)
就这样,我们得到了一张移除了背景、生成了完美抗锯齿效果的图像,半透明的边缘与阴影都被正确地恢复了出来。你甚至可以把它叠加到完全不同的背景上。 |
magick letter_recovered.png tile_aqua.jpg \
-background none -compose DstOver -flatten letter_on_aqua.png
![[IM Output]](../static/img/masking/letter_on_aqua.png)
我在这个例子中使用的图像非常棘手,带有大片的“边缘”区域。大多数图像都不会这么糟糕,但这种方法很可能是最好、也是最通用的背景去除技术。现在这已经被整理成了一个叫做「**[bg_removal](../static/img/scripts/bg_removal)**」的 shell 脚本,只需一条命令、不产生任何临时文件,并针对遮罩的具体执行方式提供了不少额外选项。
使用两个背景进行背景去除
前面这些技术的主要问题在于,你实际上并没有足够的信息,能够完整地恢复出前景对象的全部信息。你实际上需要恢复两方面信息:前景对象中每个像素有多透明,以及它原本的颜色是什么。而仅凭一张图像,你无法完美地同时恢复这两方面信息。即便你确切知道背景图像的样子,除非两者的颜色差异很大且都是已知的,否则也无法简单地把它从前景对象中减去。问题在于,你根本无法确定看到的颜色到底是给定的那个(不透明)颜色,还是某种其他颜色与背景混合之后的结果(半透明)。除非你有额外的信息来源,否则无法把原始颜色从所需的 alpha 值中分离出来。唯一能够完整恢复前景对象所有细节的情况,是你拥有两张图像,它们分别包含两种差异非常大、但完全已知的背景色。在这种情况下,你确实拥有足够的信息,可以恢复出前景对象的颜色及其透明度,从而实现完美的背景去除。选择两张图像时,重要的因素是背景色在整幅图像范围内应尽可能不同。也就是说,这两种颜色不仅要互为补色,在各通道的强度上也应该相反。例如……
虽然使用了不同的背景色,但这两张图像包含的其实是完全相同的对象。所展示的对象并不简单,包含大量半透明颜色。你可以从深蓝色背景在图像火焰中的可见程度看出这一点,而这种透明度在较浅的黄色背景中却几乎看不见。通过使用两种颜色,被叠加对象的半透明像素会与两种差异很大的颜色分别混合,因而在两张图像中呈现出略微不同的颜色。通过测量每个像素差异有多大,你就可以精确判断出哪些像素是半透明的,以及透明到什么程度。本质上,这些信息足以让你完美地恢复出被叠加对象的透明度。恢复透明度或者说“遮罩”,当然是第一步,而且实际上是非常直接的一步。生成一张差异图像,然后把每个通道中发现的差异合并起来并取最大值。 |
magick match_navy.gif match_gold.gif \
-compose difference -composite -separate \
-evaluate-sequence max -auto-level -negate \
match_alpha.png
![[IM Output]](../static/img/masking/match_alpha.png)
这张生成的图像,是每个像素透明程度的一张完美地图。它本质上就是源图像中原始对象的“alpha 遮罩”。不过,只有当叠加图像同时包含完全透明区域和完全不透明区域时,这种方法才有效。如果不是这样,你就不能用上面那一步归一化操作(「-evaluate-sequence max -auto-level」),而需要把每个通道除以两种背景色之间的差值。也就是说,除以一个介于 0.0 到 1.0 之间的数值,差值越大越好。如果两种背景色是纯黑和纯白,那就不需要归一化,只需要两张图像的差值即可。接着对差值做取反(Negate),这样最大差值就会产生零 alpha(也就是完全透明),而没有差异就会产生最大 alpha(也就是完全不透明)。接下来的任务更难一些,因为每个半透明像素的颜色都被背景改变过了,你不能只用 alpha 遮罩从其中一张源图像里提取对象。举例来说…… |
magick match_navy.gif match_alpha.png \
-alpha Off -compose Copy_Opacity -composite \
match_bad_colors.png
![[IM Output]](../static/img/masking/match_bad_colors.png)
基本上我们得到的是,图像半透明“火焰”部分出现了一圈难看的背景色光晕。这个结果完全不理想。这被称为“颜色溢出(color spill)”(一个来自色度键遮罩,也就是俗称蓝幕或绿幕技术的术语),这可能是个很大的问题。我们需要做的是把背景色从半透明像素中去除。不过,由于我们已经恢复出了原始图像的 alpha 通道,我们确切知道每个像素需要去除多少颜色,才能恢复出原本叠加上去的颜色。要做到这一点,我们不仅需要其中一张源图像,以及我们刚提取出的 alpha 通道,还需要知道那张源图像中背景的确切颜色。在像这些示例这样使用纯色背景的情况下,这是一个相对容易解决的问题。举例来说,这里我恢复原始颜色…… |
magick match_navy.gif match_alpha.png -alpha Off \
-fx "v==0 ? 0 : u/v - u.p{0,0}/v + u.p{0,0}" \
match_alpha.png -compose Copy_Opacity -composite \
match_recovered.png
![[IM Output]](../static/img/masking/match_recovered.png)
我使用源图像中左上角的像素(FX 公式「u.p{0,0}」)作为要从半透明像素中去除的背景色。如有需要,可以调整这个值,或者直接替换成要去除的颜色。恢复颜色的关键,在于上面这个复杂的 FX 混合减法运算。它会根据 alpha 遮罩(「v」)增强源图像的原始颜色(「u」),然后从最终结果中减去背景色(u.p{0,0},也就是左上角像素)。这个公式并不直观,非常感谢 HugoRune 在 IM 论坛讨论撤销一次 Composite -dissolve中推导出了所需的数学方法。该讨论还进一步说明了所有步骤具体是如何运作、如何推导出来的,甚至说明了如何从任意两种已知但不同的背景图案中提取出叠加对象。以下是把整个流程整合成一条命令的写法。 |
magick match_gold.gif match_navy.gif -alpha off \
\( -clone 0,1 -compose difference -composite \
-separate -evaluate-sequence max -auto-level -negate \) \
\( -clone 0,2 -fx "v==0?0:u/v-u.p{0,0}/v+u.p{0,0}" \) \
-delete 0,1 +swap -compose Copy_Opacity -composite \
match_recovered_2.png
![[IM Output]](../static/img/masking/match_recovered_2.png)
| _在 IM v6.6.8-3 中,如果 FX 用 'p{}' 引用一个透明像素,得到的会是零值,而不是实际的完全透明颜色值!这是一个 bug,已被报告并在 IM v6.6.8-5 中修复。目前尚不清楚这个 bug 是什么时候引入的。
这个问题只有在你决定先把 alpha 图像合并进源图像,再尝试用已知背景色修正半透明或“溢出”颜色时才会出现。_
---|---
这一次颜色提取用的是“gold”背景图像,它是通过第二次「[-clone](https://imagemagick.org/command-line-options/#clone)」操作中的「0」选中的,但实际上两张源图像都可以使用。这里只有一点提醒:上面的做法假定左上角像素就是未经污染的背景色。如果不是这样,你可能需要修改命令,指定某个具体的像素颜色,或者使用第三张确实包含正确背景色信息的图像。如果背景色在整幅图像中并不恒定,后一种方法就非常关键,不过即便存在这种复杂情况,也是可以修正的。下面是针对叠加在纯黑和纯白背景色上的图像的一个更简单的流程。在这种情况下,颜色总是从黑色背景图像中恢复,因为这只是一次简单的除法,因此可以用更快的 Divide 合成,而不必使用极其缓慢的 FX DIY 运算符。
magick match_black.gif match_white.gif -alpha off \
\( -clone 0,1 -compose difference -composite -negate \) \
\( -clone 0,2 +swap -compose divide -composite \) \
-delete 0,1 +swap -compose Copy_Opacity -composite \
match_recovered_3.png
用于背景恢复的影棚照片 理想的背景是哑光(无反光)的黑色,以及简单纯净(无反光)的白色。背景还应该尽可能平滑、色调均匀。如果是专门为背景去除而拍摄照片,使用两种互补色可能效果更好,比如用绿色和洋红色作背景拍摄。基本上,你需要在拍摄第二张照片之前,设法更换背景色屏幕。请注意,两张照片的拍摄顺序在背景去除过程中并不重要,但它们应该尽可能干净、均匀,而且主体对象和相机必须保持完全稳定、固定不动。更好的方法,可能是干脆把一块白色屏幕放在离对象足够远的地方,然后用两种不同颜色的灯均匀照亮那块屏幕,以避免对象产生任何阴影。用这种技术,你可以切换到另一种背景色,而不需要对影棚做任何物理上的改动,就能拍出两张背景不同的照片。这两种双色背景技术应该很适合透明物体,但被拍摄对象产生的反射、以及背景扭曲或“镜头”效果,是这种技术无法记录下来的,它只能记录透明度。另一方面,物体上恒定光源产生的反射则会被保留下来!如果你尝试了这种方法,请告诉我们,并提供你的源照片和结果示例,以便收录到这里。我们会附上指向你网站的链接,标注你的名字,供大家查看。
视频背景恢复 如果你有足够多、背景各不相同且复杂的一系列图像(比如一段视频),你可以尝试取所有图像的最小值和最大值,从而生成一张接近纯黑白的背景图像来使用。图像越多,效果越好。有了这两张图像,就可以提取出任何恒定的标志及其半透明度,然后用同样的技术把它从所有帧中移除。不过,这只对恒定的半透明叠加层有效,对于使用了颜色或色相扭曲的标志,甚至是纯色标志,可能都不起作用。但它至少能让你确定标志的精确形状。对于完全不透明或更棘手的标志,可以再用孔洞填充(见下文)来根据周围颜色填补缺失的细节。更多详情参见 IM 论坛讨论。
孔洞填充
遮罩、添加透明度和背景去除,都提供了处理不想要的元素的一种方式,但结果往往并不是你真正想要的“孔洞”。当然,你可以直接把带孔洞的图像叠加到其他图像上来填补它们,但这未必能得到无缝的结果。要从图像中擦除某些元素,你想要的并不只是把它们剪掉,而是用孔洞周围的颜色、色调和纹理来替换它们。以下是各种用来确定该拿什么去填补孔洞的技术。
创建待填充的孔洞
假设我们有一张带有一些难看文字的图像…… |
magick zelda_tn.gif -gravity Southwest -annotate +8+20 Zelda zelda_text.jpg
其实我们真正想做的,是移除那些文字,最简单的方法就是给它做遮罩,从而在文字原来所在的位置留下一个“孔洞”。这样问题就简化了,因为被移除的具体内容不再重要,我们只是有一个需要填补的孔洞而已。 ![[IM Output]](../static/img/masking/zelda_text.jpg)
不过在这个例子中,我会用一条画出来的线来创建遮罩,覆盖那些“难看的文字”,就好像有个用户匆忙用图像编辑器操作了一下一样。 |
magick -size 120x90 xc:black -stroke white -strokewidth 7 \
-draw 'stroke-linecap round line 9,62 36,63' \
-threshold 10% zelda_text_mask.gif
![[IM Output]](../static/img/masking/zelda_text_mask.gif)
仅仅是创建这个“孔洞”本身,就可能是一件棘手的事,自动化的解决方案可能取决于你到底想移除什么,甚至可能需要比对成百上千张带有相同“文字”或“标志”的图像,才能精确定位它。请注意,孔洞越小,最终效果就越好。能从原始图像中保留下来的信息越多,结果就会越好。一个粗糙且形状不规则的孔洞,也比一个轮廓非常平滑的孔洞要好。所以,花时间去做一个能移除所有不需要效果的最小孔洞,可能会带来很大的差别。 现在让我们用这张遮罩从图像中剪出一个孔洞,这同时也能检查它是否覆盖了所有不需要的部分。 |
magick zelda_text.jpg \( zelda_text_mask.gif -negate \) \
-compose CopyOpacity -composite zelda_text_hole.png
用模糊填充
于是我们有了一个孔洞,需要用某种颜色把它填上——要填得让人看不出我们其实从图像中移除了什么东西。最简单的方法之一,就是直接对图像做模糊处理,让孔洞周围的颜色“扩散”进孔洞里,然后再移除透明度。 |
magick zelda_text_hole.png -blur 0x1 -alpha off zelda_text_fill.png
所使用的模糊量取决于孔洞的大小。 ![[IM Output]](../static/img/masking/zelda_text_fill.png)
现在我们可以把这张模糊后的图像铺在下面,来“填补”我们之前制造出的那个孔洞……
magick zelda_text_hole.png zelda_text_fill.png \
-compose Dst_Over -composite zelda_text_removed.png
于是文字就被移除了。这并不完美,因为那片区域颜色的模糊,会让人一眼看出有东西被移除过。举例来说,如果你仔细看看 Zelda 头部旁边的窗框,就能看到模糊带来的效果。而且那片区域看起来比图像其余部分更“平滑”,这一点在照片中尤其明显。不过,这是一种流传很广、速度也很快的技术,你经常会在视频中看到它——比如有人试图移除某个电视台作为版权保护手段添加的台标时。除了尝试隐藏移除痕迹之外,另一种做法是干脆让实际的移除变得显眼。举例来说,如果你想要保护某人的匿名性。
制作中
其他方法的链接。基于缩放模糊的孔洞填充方法……稀疏颜色,Shepard 方法(速度快)。另请参见 snibgo, Filling holes 仅对边缘像素做模糊……作为填充运算符的稀疏颜色。另请参见 snibgo, 按优先级填充孔洞 我很想用一种形态学运算符,让它在颜色通道中设置颜色的同时,在一个隐藏的背景通道里计算距离。这应该能生成一种非常快速、不会渗漏的类 Shepard 填充方式,称为“颜色扩散(Color Diffusion)”。参见大量使用这种技术的论文Diffusion Curves。关于孔洞填充(文字移除)的一场大规模且历史悠久的讨论,见于 IM 用户论坛的文字移除讨论。较新的讨论是用边界处最近的颜色填充区域,这更多是关于不用模糊来做填充。一些用来擦除图像局部的非 IM“孔洞填充”方法,展示在 Stack Overflow, Remove text from jpeg 中。例如使用 Python Skimage,或者使用 Python OpenCV inpainting
![[IM Output]](../static/img/masking/moon_background.jpg)
![[IM Output]](../static/img/masking/alpha_off.png)
![[IM Output]](../static/img/masking/alpha_extract.png)
![[IM Output]](../static/img/masking/alpha_copy.png)
![[IM Output]](../static/img/masking/alpha_shape.png)
![[IM Output]](../static/img/masking/moon_hotpink_off.png)
![[IM Output]](../static/img/masking/a.png)
![[IM Output]](../static/img/masking/a.jpg)
![[IM Output]](../static/img/images/knight.png)
![[IM Output]](../static/img/masking/knight_outlined.png)
![[IM Output]](../static/img/masking/drawn.png)
![[IM Output]](../static/img/masking/mask.png)
![[IM Output]](../static/img/masking/mask_bite.png)
![[IM Output]](../static/img/masking/drawn_bite.png)
![[IM Output]](../static/img/masking/mask_bite2.png)
![[IM Output]](../static/img/masking/drawn_bite2.png)
![[IM Output]](../static/img/masking/mask_shape.png)
![[IM Output]](../static/img/masking/drawn_bite3.png)
![[IM Output]](../static/img/masking/compose_multiply.png)
![[IM Output]](../static/img/masking/compose_screen.png)
![[IM Output]](../static/img/images/rose.png)
![[IM Output]](../static/img/images/rose_bg_mask.png)
![[IM Output]](../static/img/masking/rose_blue.png)
![[IM Output]](../static/img/masking/red_image.png)
![[IM Output]](../static/img/masking/write_mask.png)
![[IM Output]](../static/img/masking/masked_color_replace.png)
![[IM Output]](../static/img/masking/green_image.png)
![[IM Output]](../static/img/masking/masked_composite.png)
![[IM Output]](../static/img/images/rose_fg_mask.png)
![[IM Output]](../static/img/masking/rose_bg_only.png)
![[IM Output]](../static/img/masking/rose_bg_blurred.png)
![[IM Output]](../static/img/masking/rose_bg_blur_opaque.png)
![[IM Output]](../static/img/masking/koala_region_red.gif)
![[IM Output]](../static/img/masking/lines.gif)
![[IM Output]](../static/img/masking/lines_regions.gif)
![[IM Output]](../static/img/images/overlay_figure.gif)
![[IM Output]](../static/img/images/overlay_bgnd.gif)
![[IM Output]](../static/img/masking/overlay_difference.png)
![[IM Output]](../static/img/masking/overlay_mask.png)
![[IM Input]](../static/img/images/match_navy.gif)
![[IM Input]](../static/img/images/match_gold.gif)
![[IM Input]](../static/img/images/match_black.gif)
![[IM Input]](../static/img/images/match_white.gif)
![[IM Output]](../static/img/masking/match_recovered_3.png)
![[IM Output]](../static/img/masking/zelda_text_hole.png)
![[IM Output]](../static/img/masking/zelda_text_removed.png)