⚠️ 这是一个非官方翻译网站,与 ImageMagick Studio LLC 无关。准确信息请参阅原文(https://usage.imagemagick.org/mapping/index.html)

ImageMagick 示例 -- 图像映射效果

ImageMagick 示例前言与索引
图像映射简介
使用图像映射扭曲图像
绝对畸变查找映射

使用某种辅助的“映射”图像来控制图像的扭曲或修改过程。无论是替换颜色、按位置改变图像模糊程度,还是通过绝对或相对方式指定源坐标来扭曲图像,都是如此。


简介

正如你在前面关于合成简单变形畸变的章节中看到的,可以用许多不同方式修改图像。不过,这些方式都受限于 ImageMagick 已经内建的具体方法。你甚至可以使用 'FX' DIY 运算符“自己动手”制作图像畸变,或者用 EvaluateFunction 之类的运算符,甚至各种 Level 运算符直接修改图像值。但畸变需要大量计算(也需要时间)才能完成任务;如果你打算对多张图像执行同一任务,让 IM 一遍遍重复这些计算就是明显的时间浪费。另一个问题是,很难以自由形式限制畸变效果。你不能简单地编辑或修改想要应用的畸变,控制能力有限。图像映射则不同。你使用一张额外的“映射”图像,控制图像的哪些部分要被修改、修改多少,或者以什么方式修改。它既不必修改整幅图像,也不必按某种预定义或预编程的方式修改图像。你可以创建一张“映射”,以_任意_可能的方式修改图像,不受限制。你也可以_编辑_或进一步修改这张映射,以调整或限制其效果;可以通过合并不同映射让效果更复杂,也可以只是平滑或模糊这种效果。最后,你还可以_保存_这张映射,稍后再次使用。结果由这张“映射”图像控制。由于修改由“映射”控制,ImageMagick 通常只需执行很少的计算,因此一般来说“图像映射”非常快。它也_可重复_,因为你可以把同一张非常复杂的映射应用到任意数量的图像上,得到完全相同的修改。也就是说,你可以很快地把它应用到整个图像目录。从本质上说,_图像映射_把某个特定效果中缓慢而复杂的数学计算,从具体图像转移到更通用的“映射”图像上。一旦生成了这张“映射”,就能非常快速地应用到大量实际图像。

什么是图像映射

映射图像基本上就是“查找表”(Look Up Table,LUT),它定义某个特定效果应如何逐像素地应用到图像上。也就是说,效果是否应用以及应用到什么程度,完全由图像映射控制。本质上,图像就是一组值,而这些值代表什么,取决于正在应用的映射过程。它们可以表示……

  • 直接替换值(颜色查找),
  • 某种颜色应来自哪幅图像(图像遮罩),
  • 某个像素应被提亮或变暗多少(高亮),
  • 指定源坐标(畸变),
  • 或相对于当前位置的位置(位移)。
  • 此位置的像素应模糊多少

其中许多内容我们已经在图像合成中见过;从某种意义上说,图像映射只是把多幅图像合并到一起的另一种方式。事实上,许多图像映射技术只是作为专门的 compose 方法实现的!请记住,真正的图像合成本质上只是把两幅真实的彩色图像以各种方式叠放在一起(具体来说就是 Duff-Porter Alpha 合成方法)。更一般地说,图像映射会使用特殊图像,以特定方式修改另一幅图像。图像映射最难的部分,是为某个特定效果生成对应的“映射”。本页涉及的大量工作、技巧和方法,正是围绕这一点展开的。不过一旦有了映射,就可以把它快速地反复用于许多不同图像。


使用图像映射扭曲图像

在前面 IM 示例章节中描述的各种畸变运算符(例如简单图像变形通用图像畸变)中,你通常只能使用已经编入 IM 图形库的各种畸变类型,而且它们一般基于特定的数学方程和公式。然而,有时你会想以更自由、数学味更少的方式设计自己的畸变。例如,为了生成更复杂的畸变,把图像映射到某个特定形状上,或者实现某种复杂的镜头效果;这些效果画出来比用数学定义更容易。有时你只是想把某个畸变重复应用到大量图像上,避免一遍遍重新计算畸变。解决办法是预先计算畸变,并把它保存成一张特殊的灰度图像形式的查找表(LUT)。也就是说,对于每个输出像素,我们先查找 LUT,再用该值去源图像中查找颜色。换句话说,需要三个步骤。

  1. 在 LUT 中查找每个目标像素
  2. 将 LUT 值映射到源图像位置(两种方法)
  3. 从源图像中查找颜色

由于使用图像作为畸变的“查找表”,你可以用图像编辑器(如 'Gimp' 或 'PhotoShop')创建或修改畸变映射,从而自由地做出非常花哨、复杂的畸变。不过必须记住,和我们见过的所有其他畸变方法一样,这里的查找也是作为反向像素映射来应用的。也就是说,对于目标图像中的每个像素,我们使用正在应用的畸变方法,从源图像中查找该像素的颜色。在这个例子里,方法就是从提供的查找表图像中查找源坐标。
现在,用图像映射决定应在源图像的哪个位置查找颜色有两种方式……绝对相对。使用绝对坐标查找时,畸变映射会把 LUT 颜色值直接转换为源图像中的一个坐标,再从那里查找要使用的颜色。LUT 中该颜色位于哪里并不重要,每种颜色都指向要使用的确切查找点。畸变 LUT 图像会包含颜色渐变,但对这个渐变做任何变形或畸变,在应用映射时都会产生同样效果。使用相对坐标查找时,位移映射会用颜色值偏移当前坐标,从而确定应在源图像哪里查找颜色。这意味着要使用一张纯灰 LUT 图像,用较亮和较暗区域定义像素应如何被映射移动或位移,而不管它们在映射中的位置如何。你会看到,这两种方法各有优缺点。


绝对畸变查找映射

创建绝对畸变 LUT 映射是两种方法中更容易理解、创建对应畸变 LUT 映射并加以应用的一种。不过,正如你将看到的,它有非常严重的缺点,因此实用性不如相对位移映射。在“畸变映射”中任意特定点的颜色,会直接指向源图像中的一个位置。也就是说,横跨“映射”的灰度渐变定义了要放置在该位置的“纹理”。现在假设这张映射图像其实是一件 T 恤之类复杂物体的图像,上面有复杂的褶皱和波纹。如果这件衣服上有一段渐变,你就能把任意平面图像映射到这件衣服上。这就是绝对畸变映射的力量。LUT 图像中任何“黑色”像素(颜色值 0)都会被视为源图像最左侧像素,或源图像的 '0' X 坐标;而 LUT 中任何“白色”(值 1)都会被视为最右侧像素(源图像宽度)。请注意,这个 LUT 只会查找源图像中颜色的 X 坐标或水平位置。它不会改变颜色的高度或 Y 位置。下面先用一个简单、普通的灰度水平渐变作为 LUT 来试试。

  magick koala.gif \( -size 75x75 gradient: -rotate 90 \) \
          -fx 'p{v*w,j}'      distort_noop.gif

[IM 输出] [IM 输出] [IM 输出]

请注意,这并没有真正改变从源图像到目标图像的映射。原因是,我们从畸变映射中查到的 X 坐标,正好就是我们要为其查找颜色的位置。只要把渐变翻转过来,像素查找也会被翻转,从而创建镜像图像。也就是说,白色在左边,“黑色”在右边,形成横跨图像的水平渐变。

    magick koala.gif \( -size 75x75 gradient: -rotate -90 \) \
          -fx 'p{v*w,j}'      distort_mirror_x.gif

[IM 输出] [IM 输出] [IM 输出]

如果取原始渐变,并用对比度增强运算符压缩它,就能得到有用得多的畸变。

  magick -size 75x75 gradient: -rotate 90 \
          -sigmoidal-contrast 8,50%      map_compress.gif
  magick koala.gif  map_compress.gif -fx 'p{v*w,j}'  distort_compress.gif

[IM 输出] [IM 输出] [IM 输出]

注意,畸变的两侧被拉伸,而中心被压缩。我们可以用 2 张畸变映射把它扩展到二维,一张调整 X 坐标,另一张调整 Y 坐标。

  magick map_compress.gif -rotate 90 map_compress_y.gif
  magick koala.gif  map_compress.gif map_compress_y.gif \
          -fx 'p{u[1]*w,u[2]*h}'   distort_compress_2D.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

如你所见,上面重新创建了 implosion 方法的一个变体,不过只是沿 X 和 Y 轴(同时)压缩图像,而不是像 Implode 运算符那样按径向压缩。这里的关键在于:你对绝对畸变映射做了什么,就会对应用它的最终图像做同样的事。这就是畸变映射的力量。

Composite 'Distort' 方法

到目前为止,我们一直使用 FX,通用 DIY 运算符来应用绝对畸变映射。这提供了一种精确调整和微调当前操作的方法,但速度也非常慢。合成运算符 "Distort" 编码了一个与我们上面使用的非常相似的公式。不过它的实现方式也让它与稍后在相对位移映射中要看的 "Displace" 合成运算符稍微更兼容。

下面用 "Distort" 合成重复最后那个“implode”示例。

  magick koala.gif  map_compress.gif map_compress_y.gif \
          -compose Distort  -define compose:args=37.5x37.5 -composite \
          distort_compose.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

请注意上面使用了 "Define Setting" "compose:args"。这个值是针对所用 LUT 渐变的乘数(以完美灰色为中心)。这里使用的值 '37.5' 是图像宽度和高度的一半(75 像素)。你可以更改这个乘数,以放大或缩小畸变的整体尺度。如果没有定义 "compose:args" 值,它会默认为正确值。如果某个值设为零,则该方向不会应用畸变。如果想自动设置合成参数,可以用下面等价的 Set "option:" 方法来计算…… |

  magick koala.gif  map_compress.gif map_compress_y.gif \
          -set option:compose:args '%[fx:w/2]x%[fx:h/2]' \
          -compose Distort  -composite \
          distort_compose_set.gif

[IM 输出]
或者取消定义,让 IM 计算正确值(用于 2D Distort)…… |

  magick koala.gif  map_compress.gif map_compress_y.gif \
          -compose Distort -define compose:args='' -composite \
          distort_compose_default.gif

[IM 输出]

No-op 畸变映射

在继续之前,我想先退一步,再看看上面的 'noop' 示例。它实际上会让图像略微模糊,因为我概述的公式并不完全正确。得到原图的 'no-op' 副本,是检验你的畸变数学是否正确的好方法。

也就是说,给定一个完美渐变时,你可以把源图像中的每个像素映射到目标图像。换言之,LUT 的“白色”(或 1.0)值会精确映射到目标中的最右侧(或最底部)像素。

测试 no-op 畸变时,我们使用一张“像素棋盘格图像”(例如 "pattern:gray50"),因为它会暴露任何畸变,也就能暴露所用数学中的任何问题。下面试着把 no-op 畸变应用到目前用过的方法上…… |

  magick -size 75x75 pattern:gray50 \
          \( gradient: -rotate 90 \) \( gradient: -flip \) \
          -fx 'p{u[1]*w,u[2]*h}'    distort_fx_check.gif

[IM 输出]
|

  magick -size 75x75 pattern:gray50 \
          \( gradient: -rotate 90 \) \( gradient: -flip \) \
          -set option:compose:args '%[fx:w/2] x %[fx:h/2]' \
          -compose Distort  -composite  distort_compose_check.gif

[IM 输出]
如你所见,两种方法都未能重现“像素检查”图像。不过由于坐标计算方式不同,它们失败的表现略有差异。发生的事情是,从颜色查找到像素坐标的缩放因子偏了 1 个像素。关于为什么会这样,请参见畸变,图像坐标与像素坐标。 | _FX 畸变以右上角(像素位置 0,0)为中心,并沿底部和右侧边缘生成重复的虚拟像素。也就是说,它并没有尝试改变从查找颜色到实际查找所用图像坐标的缩放中心。因此,即使缩放是错的,黑色像素仍以像素 0,0 为中心。

compose "Distort" 运算符会在应用缩放之前平移坐标,让零点位于图像中心。它这样做,是因为后面会看到的“位移映射”缩放也需要这一步。因此,不准确的缩放会把图像边缘沿每条边向内拉进 1/2 个像素,同时保持图像中心正确。_
---|---
下面是绝对畸变映射中修正后的“完美 no-op”版本;在计算颜色与坐标之间的缩放因子时,它们本质上使用图像坐标(宽度和高度各减一)。 |

  magick -size 75x75 pattern:gray50 \
          \( gradient: -rotate 90 \) \( gradient: -flip \) \
          -fx 'p{u[1]*(w-1),u[2]*(h-1)}'    distort_fx_check_correct.gif

[IM 输出]
|

  magick -size 75x75 pattern:gray50 \
          \( gradient: -rotate 90 \) \( gradient: -flip \) \
          -set option:compose:args '%[fx:(w-1)/2] x %[fx:(h-1)/2]' \
          -compose Distort  -composite  distort_compose_check_correct.gif

[IM 输出]
事实上,如果未定义 "compose:args",其默认值就会使用正确的缩放值。 |

  magick -size 75x75 pattern:gray50 \
          \( gradient: -rotate 90 \) \( gradient: -flip \) \
          -compose Distort -define compose:args='' -composite \
          distort_compose_default_check.gif

[IM 输出]
不过需要指出的是,使用畸变时这些细微不准确通常并不重要,所以一般会忽略这些小差异。只要把它记在心里,在真正重要时知道这一点就行。

畸变映射的问题

我们继续图像畸变,尝试做一次旋转。为此生成旋转后的映射会有点棘手,但可以做到……

  magick -size 75x75 gradient: -background black -rotate 45 \
          -gravity center -crop 75x75+0+0 +repage  map_rot45_x.png
  magick map_rot45_x.png  -rotate 90              map_rot45_y.png
  magick koala.gif  map_rot45_x.png   map_rot45_y.png \
          -compose Distort  -composite    distort_rot45.gif

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

现在我们又有了一种旋转任意图像的方法。这项技术最大的问题是,在用 rotate 创建畸变映射时,我们在对角边缘的两侧引入了一些颜色奇怪的像素。在上一个示例中,这导致图像右下角沿一条线添加了一些随机像素。这些“随机”颜色是 rotate 为了生成“更好”图像而引入的抗锯齿值。但对畸变映射来说,经过抗锯齿处理的边缘像素可能造成真正的问题。现在我们可以尝试更好地定义旋转后的 LUT 图像边缘颜色。在这个例子里,我们可以生成一张更大的渐变图像,再把旋转结果裁剪到正确大小。

  magick -size 100x20 xc:white xc:black -size 115x75 gradient: \
          +swap -append   -rotate 45 \
          -gravity center -crop 75x75+0+0 +repage   map_rot45b_x.png
  magick map_rot45b_x.png  -rotate 90              map_rot45b_y.png
  magick koala.gif  map_rot45b_x.png   map_rot45b_y.png \
          -compose Distort  -composite     distort_rot45_better.png

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

这样,LUT 中的所有像素现在都被正确定义了,没有抗锯齿。不过这又显示出一个稍微不同的问题。最终图像中的所有像素都被正确定义,但有些像素本不应该属于最终图像。它们在结果图像中没有实际意义。这代表了使用 LUT 指定从源图像获取的绝对坐标时最大的问题:你无法指定 IM 在这些未定义区域中应该做什么。

使用遮罩设置未定义像素

解决“未定义像素”问题的一种更通用方式,是定义一张映射,指出哪些像素实际上是畸变中有效的已定义结果。换句话说,就是一张遮罩图像。例如……

  magick -size 75x75 xc:white -background black -rotate 45 \
          -gravity center -crop 75x75+0+0 +repage  map_rot45b_m.png
  magick distort_rot45_better.png map_rot45b_m.png \
          -alpha off -compose CopyOpacity -composite   distort_rot45_masked.png

[IM 输出] [IM 输出] [IM 输出]

现在,畸变映射涉及三张图像,结果确实变得复杂了。当然,在典型情况下你大概不需要做到这一步,但在一般情形下是需要的。

统一畸变图像

不过你可能已经注意到,这三张映射全都是灰度图像。这意味着,把所有映射合并为一张单独的畸变映射图像是很合理的。例如,我们把“X 畸变映射”映射到 'red' 通道,把“Y 映射”映射到 'green',再把遮罩映射到 'alpha' 或透明度通道,这样更容易处理。

  magick map_rot45b_x.png map_rot45b_y.png \( map_rot45b_m.png -negate \) \
          -alpha off -channel RGA -background black -combine  map_rot45u.png

[IM 输出] [IM 输出] [IM 输出] [IM 输出]

| 合并通道图像中的 'blue' 通道未定义,因此会从当前的 "-background" 颜色取得其值;上面我预先把它设为 'black',也就是零值。
---|---
现在把这张统一畸变映射应用到我们的 koala 图像上。遗憾的是,这需要两个图像处理步骤:一个用于畸变图像,另一个用于遮罩结果。

  magick koala.gif -alpha set   map_rot45u.png \
          \( -clone 0,1  -fx 'p{v.r*w,v.g*h}' \
             +clone -compose Dst_In -composite \) -delete 0,1 \
          distort_rot45_unified.png

[IM 输出] [IM 输出] [IM 输出]

也可以直接使用 "Distort" 合成方法来处理这张统一畸变映射图像…… |

  magick koala.gif -alpha set   map_rot45u.png \
          -compose Distort -define compose:args='' -composite \
          distort_rot45_compose.gif

[IM 输出]
“统一畸变映射”图像中仍有一个未使用的通道(blue)。一个合理用途是用它为畸变后的图像添加高光和阴影。(见 Overlay 高光)。你可以在下面的球面畸变映射示例中看到这项技术又向前推进了一步。

沙漏畸变映射

这里我想要一张一维畸变映射,根据每一行的高度对图像的每一行使用不同缩放。效果有点像真正的游乐场哈哈镜畸变,让胖人看起来很瘦。换句话说,就是一种沙漏畸变。这是一张相当复杂的 LUT 图像,经过很多试调之后,我得出了下面这个表达式,用来生成随高度变化、但水平方向保持线性渐变的映射。 |

  magick -size 100x100 xc:  -channel G \
          -fx 'sc=.15; (i/w-.5)/(1+sc*cos(j*pi*2/h)-sc)+.5' \
          -separate  map_hourglass.png

[IM 输出]
| 生成灰度渐变时,只要求 -fx 运算符只生成一个颜色通道,例如上面示例中的 'G' 或绿色通道,就能让它快 3 倍。随后可以将该通道分离出来,形成所需的灰度图像。尤其在使用非常复杂的 "[-fx](https://imagemagick.org/command-line-options/#fx)" 公式时,这可以带来非常大的速度提升。
---|---
'sc' 是沙漏的缩放因子(取值范围 0 到 0.5),可以用来调整畸变幅度。现在把这张映射应用到内置的 "rose:" 图像上。请注意,100x100 像素的映射与 70x46 像素的图像并不匹配。这会让事情变复杂,因为我们需要按适当比例缩放源图像中的当前像素,使其匹配给定的畸变映射,再查找该像素颜色的位置。 |

  magick rose:  map_hourglass.png \
          -fx 'p{ v.p{i*v.w/w,j*v.h/h}*w,  j}'  distort_hourglass.png

[IM 输出]
仔细看这里,像素的 X 坐标 'i' 会乘以畸变映射图像的宽度 'v.w',再除以原图宽度 'w',得到 'i*v.w/w。像素的 Y 坐标也同样处理,得到 'j*v.h/h'。这会重新缩放目标图像中的像素坐标,使其匹配畸变 LUT 图像。查得的坐标随后会用 LUT 值乘以源图像宽度来缩放,成为颜色查找所用的 X 坐标。如果同时有 X 和 Y 畸变映射,那么还必须对 Y 映射重复这种缩放查找。当然,我们仍有前面见过的同样“边缘”畸变,所以把虚拟像素设置改成透明。 |

  magick rose: -alpha set  -virtual-pixel transparent -channel RGBA \
          map_hourglass.png  -fx 'p{ v.p{i*v.w/w,j*v.h/h}.g*w, j}' \
          distort_hourglass2.png

[IM 输出]
请注意这里使用了 "-channel" 设置,以确保 "[-fx](https://imagemagick.org/command-line-options/#fx)" 会处理并返回源图像中的 alpha 通道(透明)值,具体来说就是透明虚拟像素。还要注意,在查找畸变映射时,我们只从绿色通道查找(使用 'v.p{}.g')。如果不这样做,就会使用与源图像当前正在处理的通道相同的通道,而对这张映射来说,'alpha' 未定义。通过使用非线性渐变,可以让这张畸变映射做得更好,使图像保持矩形,并让边缘比中间产生更多畸变,从而得到更“圆”或“圆柱形”的外观。有人想试试吗?请给我来信

球面畸变映射

在前面的沙漏畸变映射示例中,我生成了一个按余弦曲线在水平方向缩放的渐变。再多做一点工作,就可以改为生成球面形状…… |

  magick -size 100x100 xc:  -channel R \
          -fx 'yy=(j+.5)/h-.5; (i/w-.5)/(sqrt(1-4*yy^2))+.5' \
          -separate  +channel     sphere_lut.png

[IM 输出]
不过请注意,上面的做法并不严格准确。被压缩的渐变仍然是线性渐变,只是压缩到一个圆内。更准确的表示大概需要创建非线性渐变。用绝对位置来说,它会是一个 'arccos()' 函数。现在,这个映射中还有一些大片区域应归类为无效,因此需要某种遮罩来定义最终图像中哪些像素有效、哪些无效。在这个例子里,一个简单的圆就够了。 |

  magick -size 100x100 xc:black -fill white \
          -draw 'circle 49.5,49.5 49.5,0'    sphere_mask.png

[IM 输出]
为了完成效果,我们还需要一个明暗高光图,例如Overlay 高光中开发的那种,用于 OverlayHardlight 合成…… |

  magick sphere_mask.png \
          \( +clone -blur 0x20 -shade 110x21.7 -contrast-stretch 0% \
             +sigmoidal-contrast 6x50% -fill grey50 -colorize 10%  \) \
          -composite sphere_overlay.png

[IM 输出]
请记住,上面的明暗只会在球体对象范围内起作用,所以阴影溢出边界并不重要。事实上,如果你想尝试做出更好的球面明暗,让它产生更像球体的图像,我很乐意看到。现在把三张图像:X 坐标 LUT、Overlay 明暗和透明度遮罩,应用到一张大小合适的实际图像上(为简单起见)。

  magick lena_orig.png -resize 100x100   sphere_lut.png   -fx 'p{ v*w, j }' \
          sphere_overlay.png   -compose HardLight  -composite \
          sphere_mask.png -alpha off -compose CopyOpacity -composite \
          sphere_lena.png

[IM 输出] [IM 输出]

这个例子展示了绝对畸变图最强大的一面。你可以在任何自由形态对象上定义渐变(不一定要用数学方式严格定义),于是任何图像都可以映射到那个对象上,不管它有曲线、皱褶、折叠还是其他形状。简单地说,只要对象映射做好了,你就能把任意图像映射到它的表面。然后为了让它看起来更真实,可以叠加第二张映射,用来添加高光、阴影、边缘和其他特征。当然,由于这三张图都是灰度图,你可以把它们合并成一张统一畸变图,便于保存。在这个例子中,我还会复用 X 坐标畸变 LUT 作为 Y 坐标,让它形成更接近球面的畸变。 |

  magick sphere_lut.png   \( +clone -transpose \) \
          sphere_overlay.png   \( sphere_mask.png -negate \) \
          -channel RGBA  -combine    spherical_unified.png

[IM 输出]
这是一张相当漂亮的映射图。不过如果要解释它,请记住:“红色”和“绿色”通道是 X 与 Y 坐标 LUT,“蓝色”是高光和阴影效果的叠加层,而透明度通道保存最终图像的无效像素蒙版。现在用 “Distort” 合成方法应用它。

  magick mandrill_grid_sm.jpg   spherical_unified.png  \
          \( -clone 0,1 -alpha set -compose Distort -composite \) \
          \( -clone 1   -channel B -separate +channel \) \
          \( -clone 2,3 -compose HardLight -composite \) \
          \( -clone 4,1 -compose DstIn -composite \) \
          -delete 0--2  spherical_mandrill.png

[IM 输出] [IM 输出]

按顺序来看……

  • 应用畸变图(其中包含蒙版)
  • 从统一图像映射中提取明暗贴图
  • 把明暗贴图应用到已畸变的图像
  • 恢复明暗处理时丢失的蒙版
  • 删除最终图像以外的所有图像并保存

这里之所以复杂,完全是因为需要提取明暗蒙版,并恢复明暗处理移除的 alpha 蒙版。

圆弧畸变图

为了展示位置畸变图真正能做到什么,这里给出一张绝对畸变 LUT,类似上面 “Arc”畸变方法提供的效果。基本思路是,不再为每张被畸变图像中的每一个像素逐一计算坐标映射,而是把这些计算好的坐标保存到两张 X 与 Y 坐标灰度 LUT 映射图中。也就是说,我们把整个畸变预先计算成一张更简单的查找表图像,从而可以一次又一次地应用它,而不需要再计算平方根或三角函数。

  magick -pointsize 30 -font Candice label:Anthony -trim +repage \
          -gravity center -resize 95x95 -crop 100x100+0+0\! \
          -flatten text_image.jpg
  magick -size 100x100 xc: -channel G  -fx 'atan(j/(i+.5))*2/pi' \
          -separate   -flip -flop       map_p_angle.png
  magick -size 100x100 xc: -channel G  -fx '1-hypot(i,j)/(w*1.6)' \
          -separate   -transverse       map_p_radius.png
  magick text_image.jpg   map_p_angle.png map_p_radius.png \
              -fx 'p{u[1]*w,u[2]*h}'    distort_p_curved.jpg

[IM 输出]
颜色源 |
| [IM 输出]
角度 - X 映射 |
| [IM 输出]
半径 - Y 映射 |
| [IM 输出]
弯曲文字
---|---|---|---|---|---|---

当然,生成这张畸变图很困难,但只要用你喜欢的任何方式完成一次(甚至可以用像 “Gimp” 这样的图像编辑器以艺术方式绘制),之后就能把它复用于大量图像。

极坐标畸变图

有时为了让事情正确工作,你可能需要由畸变图来定义目标图像,而不是由源图像来定义。例如,如果想把一些文字映射成圆形(也称为极坐标变换),你确实需要使用一张宽度大约是高度 3 到 4 倍的图像(高宽高比),否则结果会很难阅读。为此,我们把畸变图放在颜色源图像之前,这样第一张(X 映射)图像就会用于设定最终结果的尺寸,而不是使用输入源图像的尺寸。

  magick -size 100x100 xc:  -channel G \
          -fx 'atan2(i-w/2,h/2-j)/pi/2 + .5' \
          -separate  map_p_angular.png
  magick -size 100x100 xc:  -channel G \
          -fx 'rr=hypot(i-w/2,j-h/2); (.5-rr/70)*1.2+.5' \
          -separate  map_p_radial.png
  magick -font Candice -gravity center -size 200x50 \
                                label:'Around  the  World'    text.jpg
  magick map_p_angular.png map_p_radial.png text.jpg \
                 -fx 'u[2].p{ u*u[2].w, v*u[2].h }' distort_p_circle.jpg

[IM 输出]
角度 - X 映射 |
| [IM 输出]
径向 - Y 映射 |
| [IM 输出]
颜色源 |
| [IM 输出]
环形文字
---|---|---|---|---|---|---

本质上,颜色源图像现在可以是任意尺寸或宽高比,并且都会被正确处理,不过你可能需要调整畸变图的生成方式,才能正确处理源图像的宽高比。在生成上面的映射图时,值 “70” 控制圆的最终大小,中线会沿着这个圆放置。另一方面,值 “1.2” 控制图像映射到圆内时的垂直缩放,让你可以调整畸变文字的高度。 请记住,这个 “-fx” 表达式要求先给出畸变图,再把颜色源作为第三张(索引 2)图像给出。不过这也意味着,存储在源图像中的任何元数据都会丢失。
_这张畸变图的问题在于,“X 映射” 中存在非常尖锐的颜色断裂(由数学中的渐近线造成)。当你执行任何颜色查找,或调整映射图尺寸以生成更大图像时,这条线都必须保持锐利。也就是说,你需要确保对这张映射图进行任何调整尺寸或插值查找时,都不会沿这条渐近线生成灰色查找颜色。

如果沿这条线生成了灰色查找,最终结果中就会出现一条彩色像素线(从图像中部查找得到)。

因此建议始终按最终图像所需的尺寸直接生成这张畸变图,绝不要使用前面展示过的任何缩放技术。_
---|---
你也可以把它用于其他效果,比如圆形棋盘格…… |

  magick map_p_angular.png map_p_radial.png \
          -size 150x90 pattern:checkerboard \
          -fx 'u[2].p{ u*u[2].w, v*u[2].h }'   distort_check_circle.gif

[IM 输出]
试试 IM 提供的其他内置图案,配合上面的做法可以得到其他有趣效果。 | _上图清楚展示了使用 “-fx” 进行图像畸变的限制。在图像中心附近,由于大面积像素没有合并为单个像素,径向线开始出现混叠。另一方面,在图像边缘,尤其是角落处,径向线出现了相应的模糊。

原因在于 “-fx”(以及多数较旧的畸变方法)只会对源图像颜色做简单的未缩放插值查找。这意味着当图像缩小时,源图像像素不会合并在一起,为目标像素生成正确颜色。

这对放大区域(例如角落)不是问题,只会在极端压缩区域(中心)成为问题。因此一种解决办法是使用超采样,但它只是把问题推迟到更高压缩级别才出现。_
---|---
| _畸变图中同一条渐近线(突变,从图像中心到底部)也会在上例中沿该线产生锐利的颜色变化。把这条线与其他径向线比较一下(例如从中心到图像顶部的线),后者由于前面提到的插值查找,在接近图像边缘时会变得非常模糊。

当使用可平铺图像(如上例)生成圆形图案时,这可能成为问题,可能需要特殊处理,以避免图像这部分出现可见差异。

为避免这一点,最好把图像上半部分和下半部分分别畸变,以避开渐近区域。

_
---|---

打乱行

这个例子做一件稍微不寻常的事……随机打乱图像的行。首先创建一张映射图,其中 X(红色通道)是渐变,Y(绿色通道)是随机噪声图像。

  magick rose: \
          \( -size 46x70 gradient: -rotate -90 \) \
          \( -size 1x46 gradient: -spread 23 -scale 70x46\! \) \
          -compose Distort -define compose:args='' -composite \
          rose_row_shuffle.png

[IM 输出] [IM 输出]

遗憾的是,“-spread” 在选择要交换的像素时似乎会包含虚拟像素,这意味着有些行变成了重复行,而另一些行则完全丢失。换句话说,这张“打乱”图像映射并不完全正确。你有更好的像素打乱方案吗?


相对查找位移贴图

如你所见,创建和使用绝对畸变图相当容易。不过当畸变存在“未定义”区域,或畸变会超出源图像正常边界时,它会有严重问题。更严重的问题是,你始终在处理渐变,而这些渐变定义的是颜色查找的绝对坐标。映射图中没有任何部分是简单、干净、容易手工修改或编辑的。它的创建和使用需要特殊技术与数学。这通常意味着其中很少有“艺术化”发挥空间。不过还有另一种使用查找表的方法,用来指定获取最终颜色的坐标:使用相对位移贴图。它不是让“映射图”定义从源图像查找每个像素颜色的精确坐标,而是定义相对于当前位置的偏移或_位移_。偏移可以是正值也可以是负值,而负值要编码进颜色值里需要一点技巧。因此做法是把“纯灰”定义为坐标的 0 位移(没有变化)。然后让“黑色”表示最大负位移,“白色”表示最大正位移。这可能不太好描述,所以先看一个例子。首先创建一张要“位移”的测试图像。 |

  magick -font Candice -gravity center -size 150x50 \
                                           label:'Anthony'    label.jpg

[IM 输出]
现在我会用一点 “magick” 创建一张带有“纯白”和“纯黑”区域的“纯灰”图像。 |

  echo "P2 5 1 255\n 127 0 127 255 127" |\
                magick - -scale 150x50\! -alpha off   displace_map.jpg

[IM 输出]
现在要把这张图用作“位移贴图”,我们从位移贴图取得“灰度值”,并把它加到 X 或 Y 坐标(或两者)上。也就是说,根据位移贴图的“灰度”,把查找点从当前位置相对位移一段距离。这个“值”会用特殊方式处理,因此“纯灰”表示查找点零位移(这个例子中只是 Y 坐标),而“最大位移”会用于“白色”(正)或“黑色”(负)值。例如,把这张位移贴图应用到我们的 “label” 图像。 |

  magick label.jpg  displace_map.jpg  -virtual-pixel Gray \
          -fx 'dy=10*(2*v-1); p{i,j+dy}'   displaced.jpg

[IM 输出]
如你所见,图像的各个部分看起来会根据位移贴图的颜色“移动”。“白色”区域会把给定的“位移值”加到查找点上,因此在该区域中,每个像素都会查找源图像向“南”10 个像素(正 Y 方向)的位置。结果看起来就像源图像向上移动了。请记住,被位移的是查找点,而不是实际图像本身,这也是为什么白色会让图像看起来向上或负方向移动。对于“黑色”位移区域也能看到类似效果。源图像看起来向下移动,因为查找位移是在负方向完成的。请仔细想想这一点。你还会注意到,“被位移的查找”实际上可以越过正常图像边界,让你能够使用虚拟像素设置控制这些越界像素。上面我只是要求返回一个灰色像素。上例中的“最大位移”值 “10” 非常重要,它是映射图中“纯白”或“纯黑”位移值能让源图像任意部分看起来移动的最大相对距离。查找点以及输入图像不能位移得比这个值更远。介于最大白色或黑色值与中央无位移 50% 灰色值之间的其他灰度,会按相应幅度位移查找点。因此 25% 灰会让查找点沿负方向位移该位移值的一半,而 75% 灰会沿正方向位移该值的一半。这个值是绝对畸变图相对位移贴图之间的关键差异。只需改变位移值,而完全不必修改位移贴图,就可以增大或减小相对位移,让图像畸变得更多或更少。另外,由于“零位移”贴图只是一张纯 50% 或纯灰图像,而不是复杂渐变,你可以从一张简单灰色图像开始,用艺术方式把局部区域调亮或调暗,以生成所需位移。你只需绘制形状或区域即可,不必依赖复杂而精确的数学公式。最后,由于所有位移都是相对的,边缘效应产生的极端值不会生成杂乱或随机的像素颜色。事实上,正如你将看到的,平滑或模糊位移贴图其实是一件好事,因为它会移除上例中可见的断裂或不连续“切割”效果。总结来说,位移贴图更易控制,也更适合艺术化处理;它不需要复杂而苛刻的数学,就能提供局部位移,并且对错误、边缘效应,甚至位移贴图模糊都非常宽容。它非常适合简单的“位移”类畸变,例如生成水面、波浪、变形镜、光线弯折、类似镜头的效果,或磨砂玻璃、气泡玻璃效果。另一方面,高度数学化的畸变,如“极坐标”、旋转、“透视”畸变,或其他真实世界 3D 类型的映射,则不容易实现。并不是说不可能,因为稍后我们会展示你实际上可以在这两种映射图风格之间转换,只是会更困难。

合成位移方法

我们前面用 DIY FX 运算符做了位移映射,这样你可以看到实际发生了什么。但这是一种很慢的技术。不过有一个等价的内置合成运算符:“Displace”。用法如下……

    magick {_image_} {_displacement_map_} \
            -compose Displace   -define compose:args={_X_}x{_Y_} \
            -composite   {_result_ +}

    magick {_image_} {_displacement_map_} \
            -compose Displace   -set option:compose:args {_X_}x{_Y_} \
            -composite   {_result_ +}

    magick composite {_displacement_map_} {_image_} \
              -displace {_X_}x{_Y_}    {_result_ +}

注意顺序,尤其是在 “magick composite” 命令中。

使用 “-set” 而不是 define,还允许你在参数中使用百分号转义。'X ' 和 'Y ' 值定义方向以及“最大位移”,该最大位移会用于给定位移贴图中的“白色”和“黑色”。你可以只定义其中一个值,也可以两个都定义,以便向任意特定方向位移。也就是说,通常位移贴图会提供一个沿某个随机方向的线性位移,其最大强度由 'X ' 和 'Y ' 值控制。“映射图像”随后设置从负最大值(黑色)到正最大值(白色)之间实际应用多少,而完美灰色表示该像素的查找不发生位移。例如,下面是和前面相同的 Y 位移例子…… |

  magick label.jpg  displace_map.jpg  -virtual-pixel Gray \
          -compose Displace -define compose:args=0x10 -composite \
          displaced_y.jpg

[IM 输出]
你还可以使用其他设置,例如 “[-geometry](https://imagemagick.org/command-line-options/#geometry)” 和 “[-gravity](https://imagemagick.org/command-line-options/#gravity)” 设置,用来调整位移贴图叠加到图像上的区域。由位移贴图产生的像素查找仍然可以引用图像中叠加区域之外的部分,并把它们复制到叠加区域内。

简单位移示例

由原始色块组成、没有平滑过渡的位移贴图,通常会在结果图像的不同区域之间产生断裂(不连续)的位移,正如你在上面看到的那样。实际上,你可以用这种技术生成一种仿佛在破裂镜子中观看图像的“碎裂”位移贴图。例子见下面的破裂镜子。如果颜色能从一个区域平滑流向另一个区域,就能得到更漂亮、更平滑的结果。例如,通过模糊位移贴图,可以在被位移区域之间生成类似波浪的过渡…… |

  magick displace_map.jpg  -blur 0x10   dismap_wave.jpg
  magick label.jpg  dismap_wave.jpg  -virtual-pixel Gray \
          -compose Displace -define compose:args=0x10 -composite \
          displaced_wave_y.jpg

[IM 输出]
[IM 输出]
除了沿 Y 方向位移图像,你也可以使用映射图沿 X 方向位移图像,从而得到一种压缩波效果。 |

  magick label.jpg  dismap_wave.jpg  -virtual-pixel Gray \
          -compose Displace -define compose:args=10x0 -composite \
          displaced_wave_x.jpg

[IM 输出]
对 X 和 Y 方向使用同一张位移贴图,就可以同时加入压缩波和振幅波。 |

  magick label.jpg  dismap_wave.jpg  -virtual-pixel Gray \
          -compose Displace -define compose:args=10x10 -composite \
          displaced_wave_xy.jpg

[IM 输出]
请注意,图像仍然沿单一线性方向被位移,导致上图在下坡处被拉伸,在上坡处被挤压到一起。也就是说,畸变是在某个角度或“矢量”上执行的,同时具有水平和垂直分量。你可以看到,这种效果非常像在水下,图像被水面轻柔的波纹所扭曲。不过畸变图也可以包含原始图像的多个副本,就像在反射或折射图像中看到的那样…… |

  echo "P2 3 1 255\n 255 127 0 " | magick - -scale 150x50\! dismap_copy.jpg
  magick label.jpg  dismap_copy.jpg \
          -compose Displace -define compose:args=66x0 -composite \
          displaced_copy.jpg

[IM 输出]
[IM 输出]
你也可以通过渐变来创建图像局部的镜像上下翻转或左右翻转。例如,这里可以使用一张线性位移贴图,把像素从图像的一侧复制到另一侧。 |

  magick -size 50x150 gradient: -rotate -90  -alpha off  dismap_mirror.png
  magick label.jpg  dismap_mirror.png \
          -compose Displace -define compose:args=150x0 -composite \
          displaced_mirror.jpg

[IM 输出]
[IM 输出]
能看出这张位移贴图是如何工作的吗?提示一下,先弄清最左边和最右边边缘分别具有什么位移,再看看图像其余部分如何配合。不过由于这里再次使用了渐变图像,你会失去位移贴图的简单性。因此镜像操作最好直接对图像使用翻转操作,或改用绝对畸变图。注意,把渐变反过来会缩小图像。 |

  magick -size 50x150 gradient: -rotate 90  -alpha off  dismap_shrink.png
  magick label.jpg  dismap_shrink.png \
          -compose Displace -define compose:args=150x0 -composite \
          displaced_shrink.jpg

[IM 输出]
[IM 输出]
上例还展示了位移贴图的一个特殊问题。当图像的某个区域(或全部区域)被压缩超过 50% 时,就会开始产生混叠伪影。这在清晰可见的阶梯状“混叠”边缘上尤其明显。正如前面讨论过的,一个解决办法是对生成每个输出像素时使用的像素数量进行超采样。为此,我们同时放大图像和位移贴图,然后再把结果图像调整回更正常的尺寸。这样会让更多像素参与结果中特定像素的设定,从而生成更好的图像。例如…… |

  magick label.jpg  dismap_shrink.png  -resize 200% \
          -compose Displace -define compose:args=400x0 -composite \
          -resize 50%    displaced_resize.jpg

[IM 输出]
结果平滑得多,也好得多,不过可能也有一点发虚。 绘制渐变图 从上面的例子直接引出了一个想法:通过对一条简单线段做 Y 位移,可以生成位移贴图颜色的图形。例如,这里我生成一个数学 sinc() 函数(定义为 “sin(x)/x”),并把该渐变作为位移贴图绘制成图……

  magick -size 121x100 xc: -fx 'sin(i*24/w-12)/(i*24/w-12)/1.3+.2' \
                                                      gradient_sinc.gif
  magick -size 121x100 xc: -draw 'line 0,50 120,50'     graph_source.gif
  magick graph_source.gif gradient_sinc.gif \
          -compose Displace -define compose:args=0x49 -composite \
          displace_graph.gif

[IM 输出] [IM 输出] [IM 输出]

如你所见,它确实能工作,不过我不太愿意把它用于数学绘图。最好还是使用合适的绘图软件包。不过这种技术很适合作为一种粗略方法,用来绘制图像中某一行或某一列像素的强度。它也展示了位移差异很大时,很容易产生不连续或不平滑的结果。基本上,由于“graph source”中的每个单独像素都是逐个查看的,没有平均处理,因此相邻两个像素的位移查找如果差异很大,就会在结果中产生很大的颜色变化。要点是,位移不仅更适合平滑的位移贴图,也更适合位移那些包含大面积区域或色调的图像。它对尖锐细线的效果并不好。当然,你可以再次通过对畸变图进行超采样来改善效果…… |

  magick graph_source.gif gradient_sinc.gif  -resize 400% \
          -compose Displace -define compose:args=0x196 -composite \
          -resize 25%   displace_graph_2.gif

[IM 输出]
结果好了很多,虽然仍不如使用绘图软件包能达到的效果。不过它的生成只用了 ImageMagick。下面是同一张图的另一个版本,但这次使用纯色区域,它比位移细线效果好得多。 |

  magick -size 121x50 xc:white xc:black -append \
          gradient_sinc.gif  -resize 400% \
          -compose Displace -define compose:args=0x196 -composite \
          -resize 25%   displace_graph_3.gif

[IM 输出]

区域位移(线性)

让我们试一个更符合逻辑的位移问题:把图像中的一个区域沿直线从一个位置移动到另一个位置。正如我们已经看到的,“纯灰”图像不会造成任何位移,而“白色”会造成来自源图像的正向查找位移。例如,先创建这样一张图像…… |

  magick -size 75x75 xc:gray50 -fill white \
          -draw 'circle 37,37 37,20'  dismap_spot.jpg

[IM 输出]
现在应用这张图时,标记区域中的内容应该会得到给定位移值方向上的任何内容的副本。那么试试 X +10 和 Y +10,也就是 “10x10” 的位移值……

  magick koala.gif dismap_spot.jpg \
          -compose Displace -define compose:args=10x10 -composite \
          displace_spot.png

[IM 输出] [IM 输出] [IM 输出]

如你所见,标记区域中的内容现在包含了东南方向 +10,+10 像素处图像的副本。基本上就是 koala “尾巴”的一幅图像。换句话说,在圆内,图像被向东北方向位移了,也就是 -10,-10 像素。请记住,位移发生在查找上,因此由于反向像素映射,源图像会按负量移动。图像会向相反方向位移!还要注意,移动的是标记区域内的图像。你不是在移动被标记的图像,而是把图像位移_进_标记区域。最后还要注意圆边缘处的锐利不连续。标记区域内的部分会移动,而区域外的部分完全保持原样。这些都是事实,所以值得重复一遍。

位移会让图像朝与数值相反的方向移动。
只有被标记为非灰色的区域才会被位移。
锐利的颜色变化会产生锐利的图像不连续。

那么试试更实际一点的做法。把 koala 鼻子和眼睛之间的中心点(位于 “32,22”)移动到白色(完全正位移)圆的中心 “37,37”。这需要一个 “-5,-15” 的位移值(请记住方向是反的)…… |

  magick koala.gif dismap_spot.jpg \
          -compose Displace -define compose:args=-5x-15 -composite \
          displace_head.png

[IM 输出]
这样就得到了 koala 头部中央部分的一个漂亮居中副本。但图像仍然是“断裂”的,而且使用负值也不太理想。解决办法是改用黑色斑点,同时模糊该斑点的边缘。还可以把它做得更大,以包含更多 koala 的头部。因此下面就是我们的“正向移动斑点”图像…… |

  magick -size 75x75 xc:gray50 -fill black \
          -draw 'circle 37,37 37,17'  -blur 0x5  dismap_area.jpg

[IM 输出]
你不希望把图像模糊得太厉害,否则斑点中心就不再是平坦的黑色了。另一种做法是直接对图像进行归一化反向色阶调整,以确保绘制区域为黑色,周围部分是完美灰色。你会在后面的示例中经常看到这种做法。现在用我们的黑色“模糊斑点”位移贴图重复刚才的“头部”位移。 |

  magick koala.gif dismap_area.jpg \
          -compose Displace -define compose:args=5x15 -composite \
          displace_area.png

[IM 输出]
如你所见,我们把图像 +5,+15 移入“模糊”区域,但这一次区域边界更平滑,并与图像其余部分连接在一起。当然,圆边缘处的耳朵被模糊边缘畸变了,koala 的身体也被压缩了,但这仍然比之前好得多。为了防止在拖尾一侧看到图像“撕裂”,或留下被位移部分的副本,你需要扩大这个斑点,或者制作一张更复杂的渐变型位移图像。例如,假设你想把 koala 的头从起始位置 “32,22” 移到图像中心 “37,37”,也就是移动 +5,+15 像素,但希望整张图像都适应这种变化,以得到更平滑的效果。为此,你需要在 “37,37” 处让黑色达到最大位移(正向图像位移),并以 +5,+15 的值进行位移。但你还需要确保图像其余部分保持完整,把四角“钉”在 50% 灰色上。也就是说,这正适合使用 Shepard 插值稀疏渐变

  magick -size 75x75 xc:  -sparse-color  Shepards \
          '37,37 black   0,0 gray50  74,74 gray50  0,74 gray50  74,0 gray50' \
          dismap_move.jpg
  magick koala.gif dismap_move.jpg \
          -compose Displace -define compose:args=5x15 -composite \
          displace_move.png

[IM 输出] [IM 输出] [IM 输出]

如你所见,你得到的是一个更大的位移区域,并且该区域扩散到整张图像上。结果比之前使用的较紧凑“斑点”方法变化平滑得多。事实上,这与 Shepard 畸变完全等价,只不过只有一个移动控制点。它也正是 Fred Weinhaus 脚本 “[shapemorph](http://www.fmwconcepts.com/imagemagick/shapemorph/)” 所使用的方法,只是加上了一些动画。总结来说:对于小范围局部位移,可以使用“模糊斑点”位移。但对于距离更长、幅度更大的位移,应使用范围更大的平滑渐变位移贴图,以避免撕裂或复制源图像。

建设中

简单位移变形

修改位移向量的大小


两幅图像变形


随机 1D 位移

涟漪水面反射

如前所述,位移贴图特别适合生成类似水面和玻璃的扭曲。[IM 输出] 在这个例子中,我通过裁剪一张花朵图像生成了一张小图。现在我想让它看起来像放在带涟漪的水面上。为了生成涟漪,我需要一张同尺寸的正弦波渐变,可以用Evaluate Sin 函数生成。数字 '8' 表示要加入渐变中的“波”的数量。 |

  magick -size 150x80 gradient:  -evaluate sin 8  wave_gradient.png

[IM 输出]
现在用一个带角度的位移向量来扭曲这张图像,而不是只做简单的垂直或水平扭曲,这样效果会更明显。 |

  magick composite wave_gradient.png  flower.jpg -displace 5x5 flower_waves.png

[IM 输出]
现在看起来还不太有意思,但如果把图像翻转、垂直压缩,然后追加到原图后面呢…… |

  magick flower_waves.png -flip \
          flower.jpg  +swap -append  flower_waves_2.png

[IM 输出]
遗憾的是,它看起来仍然相当人工。原因是反射在图像顶部和底部看起来一样,没有“深度”感。反射的亮度也和原图相同,而现实中很少如此。为了更逼真,你需要使用强度会变化的涟漪图案。下面使用一些花哨的渐变数学来“衰减”我们上面使用的波形渐变。也就是说,让波纹图案从顶部到底部线性变小。这个处理确保波纹在图像底部结束于纯灰色或“无位移”颜色(之后会被翻转)。 |

  magick -size 150x80 gradient: \
          \( wave_gradient.png \
             +clone -compose multiply -composite \) \
          \( -clone 0 -negate -evaluate divide 2 \
             -clone 1 -compose plus -composite \) \
          -delete 0-1      waves_decreasing.png

[IM 输出]
现在把这个渐变应用上去,生成花朵的新反射。我还稍微调暗了反射图像,用来表现一部分光线被水体吸收,让它看起来更像水面反射。 |

  magick flower.jpg  waves_decreasing.png  \
          -compose Displace -define compose:args=8x8 -composite \
          -flip   +level 0,80% \
          flower.jpg  +swap -append   flower_in_water.png

[IM 输出]
注意,扭曲后的图像被翻转后形成反射。此外,在“水面”顶部,也就是最接近与原图连接处的位置,图像的“涟漪”会比底部少。这让扭曲带有离观察者远近的感觉。你还可以通过轻微旋转、弧形变换,或只用“随机”位移来扭曲波形位移贴图,使它更真实。这样波浪会更自然。不过最好在它被“衰减”之前做这些处理,这样之后再加入“深度”。试试看,多做实验,也欢迎告诉我你做出了什么。

未来的动画波纹 -
  使用 -function Sinusoid 并改变相位

二维位移映射

到目前为止,所有相对位移贴图都只在一个方向上移动图像。通过设置合适的 '_X_ x _Y_' 位移值或“向量”,这个方向可以设为任意角度。不过,如果使用两个独立的位移,你可以生成复杂得多的位移,让图像按任意方向、任意量移动。为此我们需要创建两张位移贴图,分别用于 X 和 Y 方向。下面是可以使用的命令……

magick {_image_} {_X displacement_} {_Y displacement_} \
        -compose Displace   -define compose:args={_X_}x{_Y_} \
        -composite   {_result_ +}

magick {_image_} {_X displacement_} {_Y displacement_} \
        -compose Displace   -set option:compose:args {_X_}x{_Y_} \
        -composite   {_result_ +}

composite {_X displacement_} {_image_} {_Y displacement_} \
          -displace {_X_}x{_Y_}    {_result_ +}

请注意 "magick composite" 命令中的输入图像顺序。这种奇怪的顺序是因为需要借用 "magick composite" 的选项处理方式,也有历史原因。这里必须写对。因此我建议使用 "magick" 命令,而不是 "magick composite"。 在 IM v6.4.4 之前,使用两张独立的位移贴图分别进行 X 和 Y 位移时,结果并不稳定。有时可用,有时不可用。不建议在早于这个版本的 IM 上尝试使用这种方式。
另外,和统一畸变映射图一样,你也可以使用单张“统一位移贴图”。如果只提供一张位移图像,那么 X 位移会从“red”通道查找,Y 位移会从“green”通道查找,任何 alpha 蒙版也会从位移贴图转移到最终图像。“blue”通道会被忽略。 _在内部,"magick" 和 "magick composite" 实际上都会先合并这两张图像(如果提供了两张),从而生成一张“统一位移贴图”,然后再把它传给内部 API。

这并不影响前面看过的“线性位移”,因为给出的位移贴图是灰度图像,所以“red”和“green”通道完全相同。_
---|---

圆柱位移

IM 论坛里多次有人问,如何把一张图像映射到圆柱体上,例如覆盖到咖啡杯或软饮料罐上。下面就是解决方案…… |

  magick rose: -background black -gravity south -splice 0x8 \
          \( +clone -sparse-color barycentric '0,0 black 69,0 white' \) \
          \( -clone 1 -function arcsin 0.5 \) \
          \( -clone 1 -level 25%,75% \
                 -function polynomial -4,4,0 -gamma 2 \
                 +level 50%,0 \) \
          -delete 1 \
          -virtual-pixel black  -define compose:args=17x7 \
          -compose Displace  -composite   rose_cylinder.png

[IM 输出]
上面的命令非常复杂,但本质上同时使用了两个独立位移:X 方向上的 arcsin() 压缩,以及 Y 方向上的圆弧位移。下面说明这条命令在做什么……

  • 载入 "rose" 图像,并为垂直位移添加一些空间
  • 创建一个水平数学渐变,供后续数学函数使用
  • 用一份渐变副本生成压缩位移贴图
  • 用另一份副本生成垂直椭圆弧位移
  • 移除线性渐变
  • 准备并执行位移

结果……一朵玫瑰被正确包裹成圆柱体的 30 度等距视图。可以拆开上面的命令,保存并查看各个位移贴图。需要记住的关键是,两张位移贴图会同时查找 X 和 Y 值,以确定哪个像素最终应该落在查找位置。请记住,这个位移并不是实际移动源图像,而是移动对源图像的查找位置。这种位移畸变方法已经被 Fred Wienhaus 内置到 "[cylinderize](http://www.fmwconcepts.com/imagemagick/cylinderize/)" 脚本中。

破碎镜面

通过生成随机的 X 和 Y 位移区域,可以让图像呈现“破碎镜面”的外观。

  magick dragon_sm.gif -sparse-color voronoi '  \
                  %[fx:rand()*w],%[fx:rand()*h]  red
                  %[fx:rand()*w],%[fx:rand()*h]  lime
                  %[fx:rand()*w],%[fx:rand()*h]  black
                  %[fx:rand()*w],%[fx:rand()*h]  yellow
               ' -interpolate integer -implode 1     mirror_areas.gif
  magick  mirror_areas.gif -channel R  -separate   mirror_dismap_x.gif
  magick  mirror_areas.gif -channel G  -separate   mirror_dismap_y.gif

  magick composite mirror_dismap_x.gif  dragon_sm.gif  mirror_dismap_y.gif -alpha off \
            -background white -virtual-pixel background -displace 7 \
                                                        mirror_displaced.gif

  magick  mirror_areas.gif -edge 1 -threshold 20% \
            -evaluate multiply .7 -negate               mirror_cracks.gif
  magick composite mirror_displaced.gif  mirror_cracks.gif -compose multiply \
                                                        mirror_cracked.gif

[IM 输出] | [IM 输出] [IM 输出] [IM 输出] |
---|---|---
| [IM 输出] [IM 输出] | [IM 输出]

使用随机化的 Voronoi 稀疏颜色 图像生成四个随机位移区域。然后对它应用内爆畸变,把这些区域向图像中心弯曲。由于四个有色区域都保持为纯色,每个区域都会包含原始图像的一份未扭曲但发生位移的副本。不过,每个区域移动图像的方式都不同,就像破碎镜子的每块碎片一样。最后,为了完成镜面效果,使用边缘检测描出各区域边缘,从而呈现结果图像的破碎特性。也就是说,裂缝也被显示出来。 严格说,我并不需要把生成的彩色随机位移贴图中的“red”和“green”通道分离出来。我本可以直接使用它们,因为 X 位移会从“red”通道查找,Y 位移会从“green”通道查找。也就是说,我可以直接把 "mirror_areas.gif" 图像用作“统一位移贴图”。

建设中

Shepards 位移

随机位移

镜头效果

毛玻璃效果

色散效果(旋转位移)

带随机位移的色散效果

未来:其他可能的 distort / displace 映射示例

  • 将渐变光线追踪到 3D 对象上,这样之后任何图像都可以映射到这些对象上。
    • X 和 Y 渐变映射图像
    • 用于颜色、高光和阴影的纯灰图像

可变模糊映射

ImageMagick 6.5.4-0 中加入的 "[-compose](https://imagemagick.org/command-line-options/#compose)" 方法 'Blur',提供了一种根据映射图像,用相邻像素的椭圆高斯平均值(模糊)替换每个单独像素的方法。

    magick composite -blur {_Xscale_}[x{_Yscale_}[+{_angle_}]]          blur_map  image   result

    magick image  blur_map \
        -define compose:args='{_Xscale_}[x{_Yscale_}[+{_angle_}]]' \
        -compose blur -composite   result

    magick image  blur_map \
        -set option:compose:args '{_Xscale_}[x{_Yscale_}[+{_angle_}]]' \
        -compose blur  -composite   result

注意,这种图像合成需要使用一个操作参数,可以用多种方式设置。更多细节参见全局定义的 Artifact。通过使用可变映射来控制模糊,你可以模糊图像的一部分,同时让另一部分完全不受影响;也可以生成类似移轴效果的效果,让现实世界图像看起来更像小型人工模型。

例如,这里我模糊 koala 图像的一半,而另一半完全不模糊……

  magick -size 37x75 xc:black -size 38x75 xc:white +append  blur_map_bool.gif
  magick koala.gif blur_map_bool.gif \
          -compose blur -define compose:args=3 -composite \
          blur_koala_bool.gif

[IM 输出] [IM 输出] [IM 输出]

如你所见,'blur_map' 图像上为“white”的任何像素,都会使用给定的 'sigma ' 最大值进行模糊,而为“black”的部分则完全不模糊。换句话说,这是一个非常简单的遮罩模糊。当然,这可以用许多其他方式实现,但那并不能说明模糊映射的威力在哪里。模糊映射之所以灵活,是因为它可以在图像范围内变化。也就是说,如果模糊映射颜色是灰色,那么该像素会得到相应较小的模糊结果,使用较小的“邻域”。而黑色不模糊,白色则按给定值进行最大模糊。需要注意的一点是,只有被模糊的区域才会花费额外时间。未模糊的像素不需要这种额外处理。这让上面的做法比使用遮罩合成快得多,后者等同于模糊整张图像再合并结果。在处理图像中很小区域的大范围模糊时,这种节省的时间会更加重要。例如,让 koala 越接近脚部越模糊……

  magick -size 75x75 gradient:black-white blur_map_gradient.gif
  magick koala.gif blur_map_gradient.gif \
          -compose blur -define compose:args=3 -composite \
          blur_koala_gradient.gif

[IM 输出] [IM 输出] [IM 输出]

下面还是同一个模糊,但展示模糊如何随高度变化。

  magick blur_map_bool.gif blur_map_gradient.gif \
          -compose blur -define compose:args=15 -composite \
         blur_edge_gradient.gif

[IM 输出] [IM 输出] [IM 输出]

想看可变映射模糊的实际例子,可以看看照片移轴效果距离模糊阴影字体。注意,用于为每个像素生成“模糊颜色”的,是该像素周围的邻域。这意味着,即使你指定图像某一部分不被模糊,来自这个未模糊区域的颜色仍可能作为周围像素模糊的一部分被使用。也就是说,某个区域不模糊,并不意味着该区域的颜色不会参与其他模糊像素的结果。换句话说,未模糊区域的颜色可能会“渗漏”到周围的模糊区域中。若要在模糊背景时不包含前景像素,需要使用读取遮罩技术,防止它们作为模糊操作的一部分被读取。

椭圆模糊

'Blur' 合成设置使用与普通模糊或高斯模糊运算符不同的技术,因为它是用高斯椭圆区域重采样算法实现的;该算法原本是作为通用畸变运算符的一部分,为缩放图像重采样开发的。用于邻域重采样的椭圆区域,也让这种模糊方法比 "[-blur](https://imagemagick.org/command-line-options/#blur)" 和 "[-gaussian-blur](https://imagemagick.org/command-line-options/#gaussian-blur)" 运算符提供的普通均匀“圆形”模糊更灵活。椭圆本身由模糊区域 sigma 的 'width '、'height ' 定义。椭圆还可以按给定的 'angle '(顺时针)从正交对齐状态旋转。例如,在下面的图中,我们展示单个像素的模糊颜色如何基于给定 sigma 值,从一个旋转的椭圆区域中取得颜色。该区域中的像素随后会按照高斯滤镜(使用椭圆距离公式)进行加权平均,生成模糊颜色。

  magick koala.gif -compose blur -define compose:args=5x1-30 -composite \) \
             elliptical_blur.gif
  # ... other commands to create diagram of blur effect ...

[IM 输出]

如前所述,这与通用畸变运算符为其畸变图像生成颜色时使用的颜色查找方法完全相同,因为它允许把源图像的一个区域按比例缩放(并滤波)后合并为一个像素,尤其适用于查看远处地平线这样的极端畸变。关于这个过程的更多细节,请参见区域重采样重采样滤镜

作为可变模糊映射中可用椭圆控制的例子,我们仍使用之前同一张渐变模糊图,并使用一个黑点。但这次我们会缩放一个细长的水平椭圆 '30x0',而不是圆。'x0' 看起来可能有些奇怪,但基本意思是不要看到垂直方向的模糊,只需要一个高度尽可能小、但足以生成好结果的椭圆。

  magick -size 75x75 xc: -draw 'circle 36,36 36,8'  black_circle.gif
  magick black_circle.gif blur_map_gradient.gif \
          -compose blur -define compose:args=15x0 -composite \
          blur_horizontal.gif

[IM 输出] [IM 输出] [IM 输出]

如你所见,模糊量仍然随提供的映射图像而变化,使图像顶部几乎没有模糊,底部则有大量模糊。但还要注意,底部边缘在水平方向上向两边等量模糊,而垂直方向没有模糊,因此垂直方向形成了锐利截断。你可以通过给出第三个 angle 参数旋转这个细长椭圆,或者直接定义一个垂直椭圆,让图像只在垂直方向模糊…… |

  magick black_circle.gif blur_map_gradient.gif \
          -compose blur -define compose:args=0x15 -composite \
          blur_vertical.gif

[IM 输出]
不过请注意,模糊并没有均匀应用!上半部分看起来比下半部分模糊得少,因为“映射图像”就是这样指定的。反过来,这会扭曲图像,使它看起来像被模糊效果稍微截断了。最后我们再做一次,不过这次把水平椭圆按固定的 45 度角旋转。 |

  magick black_circle.gif blur_map_gradient.gif \
          -compose blur -define compose:args=15x0+45 -composite \
          blur_angle.gif

[IM 输出]
图像可能显得很奇怪,这是因为可变模糊图是垂直的,而模糊本身带有角度;由于椭圆角度和模糊图角度没有对齐,就产生了这种怪异效果。 | 注意,像这样使用细长椭圆实际上比使用单个大圆要快得多。事实上,"[-blur](https://imagemagick.org/command-line-options/#blur)" 运算符通过使用两个独立的水平和垂直模糊来获得速度,而 "[-gaussian](https://imagemagick.org/command-line-options/#gaussian)" 模糊运算符则以比刚才描述的 'Blur' 合成方法更简单的方式,执行完整的二维卷积
---|---

可变宽高比模糊

到目前为止,我们一直使用“模糊图”来改变用于模糊的椭圆区域大小。不过,虽然椭圆大小甚至角度都可以旋转,它的形状和角度仍然是固定的。现在,“模糊图”是一幅由红、绿、蓝三个颜色通道组成的图像。由于我们使用的是灰度图像,三个颜色通道的值都相同。但在内部,椭圆的宽度只按红色通道值缩放,高度则按绿色通道值缩放。蓝色通道值通常会被忽略,只有一个稍后会看到的特殊情况例外。这意味着可以为红色和绿色通道分别使用不同的映射,从而改变椭圆形状或它的“宽高比”。和普通模糊图一样,零值(或仅该通道中的“black”)会得到最小宽度或高度,而最大值(或“white”)会得到给定的模糊量。例如,这里我可以把图像分成几块,让图像的两个四分之一区域水平模糊(红色通道为最大值),而其他区域垂直模糊(绿色通道为最大值)。在这个例子中,我先分别生成宽度和高度映射,再把它们组合成一张单独且带有颜色的“模糊图”。实际使用时,可以用任何你喜欢的方式创建映射,甚至使用预先准备好的映射来实现特定模糊效果。

  magick -size 2x2 pattern:gray50 -sample 75x75! blur_map_r.gif
  magick blur_map_r.gif -negate blur_map_g.gif
  magick blur_map_r.gif blur_map_g.gif -background black \
          -channel RG -combine blur_map_aspect.gif
  magick black_circle.gif blur_map_aspect.gif \
          -compose blur -define compose:args=10x10 -composite \
          blur_aspect.gif

[IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出]

当然,你仍然可以为椭圆设置一个固定角度。 |

  magick black_circle.gif blur_map_aspect.gif \
          -compose blur -define compose:args=15x15+45 -composite \
          blur_aspect_angle.gif

[IM 输出]
| 在 IM 6.5.8-8 版本之前,带角度的垂直椭圆模糊处理存在一个 bug。
---|---

可变角度模糊

到目前为止,用于模糊图像的椭圆角度在整幅图像上都是一个固定角度。也就是说,即便可以通过修改模糊图的红色和绿色通道来改变椭圆的宽高比,用于模糊的椭圆仍始终保持同一角度。从 IM v6.5.8-8 开始,你可以使用模糊映射图像的蓝色通道为模糊提供可变角度。做法是在模糊参数中给出两个角度。第一个角度参数用于定义蓝色通道为零值('0' 或 'black')时的角度,第二个角度用于定义蓝色通道为最大值('QuantumRange' 或 'white')时的角度。如果只给出一个角度值,那么这个角度会同时用于零值和最大“blue”通道值;这基本上意味着无论“模糊图”的蓝色通道中是什么值,角度都会变成固定值。这就是前面示例中角度保持恒定的原因。例如,这里我使用一个水平模糊的椭圆,但随后利用蓝色通道,让椭圆角度在图像中心周围从 '+0' 到 '+360' 的角度范围内变化。映射生成使用极坐标渐变,详情可在畸变渐变中找到。注意,当把该渐变放入蓝色通道时,我将 "[-background](https://imagemagick.org/command-line-options/#background)" 颜色设置与组合运算符一起使用,以确保红色和绿色通道都设置为最大('white')值,这样它就不会缩放带角度的椭圆。当然,这意味着在最终映射图像中,白色表示使用最大角度,而黄色(或零蓝色通道值)表示最小角度。

  magick -size 100x300 gradient: -rotate 90 \
          +distort Polar '36.5,0,.5,.5' +repage -flop gradient_polar.jpg
  magick gradient_polar.jpg -background white \
          -channel B -combine blur_map_angle.jpg
  magick koala.gif blur_map_angle.jpg \
          -compose blur -define compose:args=5x0+0+360 -composite \
          blur_rotated.jpg

[IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出]

结果如你所见,是一张旋转模糊图像。可以把结果与所用的模糊映射进行比较。在图像顶部,角度渐变是白色或黑色;结合所用参数,这意味着椭圆角度为 0 或 360,因此椭圆保持水平。在底部,渐变是纯灰色,所以使用给定范围中间的角度,也就是 180 度。这意味着椭圆同样又是水平的。但在图像两侧,渐变是 25% 或 75% 灰度,因此角度是 90 或 270 度,使椭圆旋转到垂直方向。其他角度依次过渡,使椭圆围绕图像平滑旋转。不过,结果图像的中心被模糊得非常奇怪!这是因为椭圆大小保持不变,并没有朝图像中心适当地变小。解决办法是同时使用红色和绿色通道设置椭圆大小。例如:

  magick -size 106x106 radial-gradient: -negate \
          -gravity center -crop 75x75+0+0 +repage gradient_radial.jpg
  magick gradient_radial.jpg gradient_radial.jpg gradient_polar.jpg \
          -channel RGB -combine blur_map_polar.jpg
  magick koala.gif blur_map_polar.jpg \
          -compose blur -define compose:args=10x0+0+360 -composite \
          blur_polar.jpg

[IM 输出] [IM 输出] [IM 输出] [IM 输出]
[IM 输出] [IM 输出] [IM 输出]

好多了。不过请注意,虽然结果看起来不错,模糊椭圆并没有像真正旋转模糊图像中应有的那样沿弧线正确弯曲。因此,上面的做法只是真正旋转模糊的近似。不过对于较小的模糊距离(相当于模糊角度)来说,效果相当好。更好的旋转模糊做法,是使用特殊的Polar-Depolar 畸变技术,或者当前命名并不准确的径向模糊运算符。通过改变椭圆角度使用的角度范围(蓝色通道),你可以很容易把上面的处理转换成径向模糊,使其随离中心距离增加而更模糊。 |

  magick koala.gif blur_map_polar.jpg \
          -compose blur -define compose:args=5x0+90+450 -composite \
          blur_radial.jpg

[IM 输出]
但你还能做的远不止这些径向/旋转模糊,因为你可以在整幅图像的任何位置,以任意量旋转和缩放模糊。你拥有完全控制。例如,你可以使用不同的角度范围,做出两者非常奇怪的混合,使模糊椭圆的角度与围绕图像中心的角度不匹配。 |

  magick koala.gif blur_map_polar.jpg \
          -compose blur -define compose:args=10x0+0+180 -composite \
          blur_weird.jpg

[IM 输出]
基本上,现在你已经可以完全控制图像中哪些部分会如何被模糊。借助模板,你还可以创建一整套模糊效果库。