ImageMagick 示例 -- 图像扭曲
在看过 IM 从早期就提供的一组简单内置图像包裹和扭曲运算符之后,这里进一步深入,看看图像扭曲的内部机制以及更复杂的数学扭曲。基于这种更深入的理解,接下来再看一个更通用的图像扭曲运算符。这包括从复杂的旋转、缩放和切变,到透视或 3D 扭曲,到圆弧之间的变换、相机镜头扭曲,最后到更接近变形的通用扭曲。
通用扭曲技术
既然已经介绍过 IM 提供的简单扭曲运算符,我们先退一步看看细节,了解图像扭曲实际如何工作,以及怎样改进它们的用法。后面我们还会进入更复杂的图像扭曲方式,其中包括并未直接内置到 ImageMagick 中的方法。图像处理器扭曲图像的基本方式只有少数几种。例如,简单扭曲 运算符通过 像素交换 实现。也就是说,只是把单个像素,甚至整行、整列像素调换位置,从而对图像进行 Flip、Roll、Transpose,甚至 矩形旋转。颜色不会改变,像素数量也保持不变。下一种扭曲图像的方法是将像素的列和行沿水平或垂直方向 平移或切变,例如 IM 在 图像切变 和上面的 波浪扭曲 中所做的那样。切变反过来也提供了一种以任意给定角度 旋转图像 的方法,而且应该相当快。不过,像素平移方法仅限于这些基本扭曲。例如,它不能把图像缩放到另一个尺寸。对于结果图像中未被原始源图像覆盖的区域如何处理,你也几乎没有控制权。在上述函数中,IM 只是把缺失区域设置为当前背景色。要以更通用的方式扭曲图像,需要使用一种更通用的扭曲技术,称为 反向像素映射。例如,更复杂的 圆形扭曲,如图像 内爆 和 旋涡,就使用了这种方法。
正向或直接像素映射
尝试扭曲一幅图像时,人们首先想到的通常是取源图像中的每个像素,并把它直接移动到目标图像中的新位置。事实上,简单扭曲、图像裁剪,甚至矢量图像的扭曲,大致就是这样做的。每个像素(或坐标)只是被移动到最终图像中的新位置。遗憾的是,只要不是简单扭曲,这样做就会出现问题。例如,这里我取一幅小图像的枚举像素列表,只改变每个像素的位置,让它旋转到新的位置。 |
# Rotate by 17 degrees -- get the Sine and Cosine of this angle
sin=`magick xc: -format "%[fx:sin( 17 *pi/180)]" info:`
cos=`magick xc: -format "%[fx:cos( 17 *pi/180)]" info:`
# For each Pixel, rotate that pixels coordinates
magick koala.gif txt:- | sed '2,$ s/,/:/' |\
gawk -F: 'NR == 1 { print }
NR > 1 { x = $1-32; y = $2-32;
nx = int( c*x - s*y + 32 );
ny = int( s*x + c*y + 32 );
printf( "%d,%d: %s\n", nx, ny, $3 );
}' s=$sin c=$cos - |\
magick -background black txt:- koala_rotated_direct.gif
![[IM 输出]](../static/img/distorts/koala_rotated_direct.gif)
这个扭曲只是 17 度的简单旋转,但结果一点也不好看。首先,每个新的像素位置都是浮点值,而像素只能存在于整数网格上,因此上面的做法只是丢弃了结果中的非整数部分。第二个问题是,结果里到处都是没有任何像素落入的空洞。这又引出了第三个问题。你可能看不出来,但结果图像中的每个空洞,都会对应另一个放入了两个像素的位置。也就是说,同一个位置上有多个像素。那应该使用哪个像素值呢?在上面的例子中,IM 只是使用了某个位置最后定义的像素。换句话说,结果图像是不完整的;目标中的每个像素并不一定在它应在的位置,而且可能有多个像素,或者根本没有像素。这些都是严重问题,当把像素从源图像正向映射到目标图像时,很难轻易解决。也不是说它完全不能工作,许多研究论文讨论了一种称为“splatting ”的技术。基本上,它们取每个输入像素,变换它的位置,然后在新位置以适当的扩散和像素颜色混合来绘制它。在处理现实世界物体的 3D 数字化时,这种技术尤其有用。这里有一团已知颜色的表面点“云”。用户能看到的任意点都会被简单地“splatted”到屏幕上,从而形成最终图像。只要点足够多,图像看起来就会完整。配合交互式 3D 控制,它效果很好,而且速度很快。不过,对三维点进行 splatting 已经超出了 IM 处理 2D 栅格图像的范围。
反向像素映射
与其试图把像素映射到最终图像中,不如把目标图像中每个像素的坐标映射到源图像中的对应位置,再从源图像查找该像素应包含的颜色。这称为 反向像素映射,几乎所有图像扭曲程序都是这样做的。由于会处理目标图像中的每一个像素,我们可以确保目标中的每个像素都恰好得到一种颜色。因此,只要能够算出每个目标像素对应的“源”位置,就可以使用任何你能想到的数学公式,把源图像扭曲成目标图像。
![[示意图]](../static/img/img_diagrams/mapping.gif)
总之,扭曲映射(反向映射)会执行以下步骤。
For each pixel (I,J) in the destination or output image
Map the I,J pixel position to a X,Y pixel position in the original image
Look up the Color of the original image at position X,Y
Using color interpolation, work out the appropriate color.
Or the virtual-pixel setting, if it misses the actual source image.
Set the destination images color for pixel I,J
注意,上面我使用变量名“I,J”和“X,Y”,是因为这些变量会对应到你在 FX DIY 运算符 中通常使用的变量名。例如,这里我模拟前面尝试过的同一个 17 度旋转,不过这次使用“[-fx](https://imagemagick.org/command-line-options/#fx)”运算符,在源图像中查找离该位置最近的像素。 |
# Rotate by 17 degrees -- get the Sine and Cosine of this angle
sin=`magick xc: -format "%[fx:sin( 17 *pi/180)]" info:`
cos=`magick xc: -format "%[fx:cos( 17 *pi/180)]" info:`
cx=37; cy=37; # center of rotation
magick -size 75x75 xc: koala.gif \
-virtual-pixel Black -interpolate NearestNeighbor \
-fx "ii = i - $cx; jj = j - $cy;
xx = $cos*ii +$sin*jj + $cx;
yy = -$sin*ii +$cos*jj + $cy;
v.p{xx,yy}" \
koala_rotated_fx.gif
![[IM 输出]](../static/img/distorts/koala_rotated_fx.gif)
关于上面的 DIY 示例,可以在 DIY 仿射扭曲映射 小节中看到更多细节。可以看到,因为会为目标中的每一个像素查找颜色,我们的图像中不再有“空洞”。它看起来仍然不算很好,但那是精确调整每个像素应放入什么颜色的问题。也就是说,反向像素映射既不会产生空洞,也不会产生重叠像素。每个像素都有明确定义的颜色,从而生成完整图像。
正向映射和反向映射之间的区别很重要,因为大多数数学变换都定义为正向映射,也就是把单个源 (X,Y) 位置映射到目标 (I,J) 位置。事实上,“正向映射”非常适合矢量图形和画线,因为你只需映射线段的两个端点并绘制即可。对于旋转这类线性变换尤其如此,因为直线仍保持为直线。实际上,postscript 和 SVG 等所有基于矢量的语言都是这样做的。但对于普通栅格图像,必须使用“反向映射”来扭曲图像,这样才能确保目标图像的所有像素都被“填上”。例如,如果看一下上面两个例子中用于映射坐标的数学式,你会发现它们几乎完全相同。“旋转”的反向映射是另一个“旋转”,只是方向相反。仔细看会发现,和正向映射版本相比,'sin' 常量取了相反符号,这就足以反转旋转方向。这个细节非常重要,也很关键。问题在于,并不是所有正向映射变换都能很好地作为反向变换来使用。事实上,有些正向映射没有简单直接的反向映射。这并不是说做不到,只是不简单。另一方面,有些图像变换作为反向映射非常好用,却没有简单的正向映射。因此,从数学角度看,使用反向映射方法既有好处,也有坏处。
顺便说一下,下面是使用 通用扭曲 SRT 方法得到的更快等价形式,它执行与上面完全相同的图像旋转,并产生完全相同的结果,只是速度更快。同样,通过使用“point”插值,颜色查找被限制为映射位置最近像素的颜色。这意味着图像中不会加入新的颜色(除非“错过”了源图像),但也会出现严重的锯齿效果。 |
magick koala.gif -virtual-pixel Black -interpolate NearestNeighbor \
-filter point -distort SRT 17 koala_rotated_srt.gif
![[IM 输出]](../static/img/distorts/koala_rotated_srt.gif)
| 关于扭曲变换的另一种讨论,请参见 Leptonica, Affine Implementation,尤其是其中对“point-wise”方法的讨论。另一种方法“sequential”,本质上就是 IM 过去实现其 Rotate 和 Shear 扭曲运算符的方式。
---|---
名称里有什么? 在研究过程中,我发现这种图像处理方法并没有一个真正清晰的命名。实际的算法过程称为“反向像素映射 ”,而数学方程的使用称为“几何变换 ”。如果扭曲由多个控制点的移动来控制,通常称为“图像变形 ”或“橡皮布变形 ”。定义特定点、通常用于在两幅或多幅图像之间寻找对应点的过程称为“图像配准 ”。图像也可以被细分成更小、更简单的单元,再使用“网格化 ”(四边形)和“三角网格 ”(三角形)等技术分别扭曲。通过小幅渐进扭曲并混合两幅图像的颜色,可以生成电影和音乐视频中常见的动画“图像变形 ”。如果不使用即时数学查找,而是使用预先准备好的映射图像,就得到“绝对扭曲映射 ”;如果查找是相对位移(50% 灰色表示无位移或查找坐标不变),就得到“位移映射 ”。如果映射只是稍微修改颜色(阴影),而不是执行查找扭曲,就得到相关但不同的“凹凸表面映射 ”。在 3D 建模和 3D 电脑游戏中,相同技术也用于给平面和曲面赋予某种彩色图案,这种方法称为“纹理映射 ”。它可能涉及把图像细分成接近单个像素的网格和网片。还有一种情况是,使用“点溅射 ”技术查看由数百万个单点定义的对象,不过这通常是通过正向映射扭曲来应用的。上述所有内容都关系密切,最基本的做法大多都是根据把最终目标坐标映射到源图像(或对象)来查找像素颜色。换句话说,就是从 目标到源 的映射。该用哪个术语……任选其一吧。
像素颜色查找
上面的 反向像素映射 技术仍有几个问题。首先,当从目标中的固定整数位置映射一个像素时,可能会落到源图像中的非整数位置。也就是说,该位置位于源图像各个像素之间的某处。为了确定应该返回什么颜色,会使用称为 插值 的过程,通过混合周围像素的颜色来确定该实数位置的最终颜色。插值 设置还会处理扭曲图像的一部分被“拉伸”的情况,即单个源像素被涂抹到目标图像的一大片区域。不过,简单的插值方法并不能很好地处理相反情况,这就需要下面将看到的其他技术。例如,这里我们再次旋转考拉,但这次使用“[-interpolate](https://imagemagick.org/command-line-options/#interpolate) [Mesh](misc.html#mesh)”设置混合附近四个像素,从查找中产生更好、更正确的颜色。 |
magick koala.gif -virtual-pixel Black -interpolate Mesh \
-filter point -distort SRT 17 koala_rotated_mesh.gif
![[IM 输出]](../static/img/distorts/koala_rotated_mesh.gif)
可以看到,只是简单合并非整数查找点周围最近的相邻颜色,就能大幅改善扭曲图像的外观。但还有其他问题……例如,当映射位置完全“错过”源图像时该怎么办?在这种情况下,应返回什么颜色由 虚拟像素 设置决定。该设置会选择一种颜色,例如源图像最近边缘的颜色;假装源图像在平面上无限平铺(或镜像平铺);或者使用某种特定颜色,如“white”、“black”或“transparent”,也可以使用用户定义的背景色。还有一种可能是,被映射的特定目标位置根本没有数学上有效的坐标。例如,像素看向透视“平面”的“天空”(见 查看远处地平线),因此甚至看不到源图像所在的“平面”。在这种情况下,虚拟像素 没有用,因为它并没有在 N 维空间中“命中”源图像平面,因此该目标像素完全无效!此时 IM 会使用当前“[-alpha set](https://imagemagick.org/command-line-options/#alpha)”设置作为该像素颜色。如果它是“近似错过”,IM 会在知道如何处理时,将这种无效颜色与图像平面的邻近颜色进行抗锯齿混合。对于透视扭曲,它会这样做。
超采样
插值对于简单的图像扭曲效果很好。但如果源图像的一部分被压缩到小得多的区域,每个目标像素实际上可能需要合并源图像中大得多的一片区域。请记住,像素并不是真正的点,而是表示真实图像中的一个矩形区域。这意味着在某些情况下,我们确实应该尝试把源图像的大块区域压缩到单个目标像素中。发生这种情况时,简单的 像素查找 会失败,因为它只查找源图像中单个“点”的颜色(使用周围像素邻域),并不会合并和组合输入图像中可能必须压缩到该单个像素里的所有颜色。结果是,目标像素可能得到源图像中本质上随机的颜色,而不是所有相关颜色的平均值。这本身并不坏,但当某个区域里的所有像素都这样做时,就会得到看似随机的孤立像素、莫尔纹效果,以及带锯齿的“阶梯”效果。细线也开始更像点线和虚线(参见 Sample 运算符 的示例),或者可能完全消失。所有这些效果统称为 锯齿伪影。一种解决办法是,对目标中的每一个像素,从源图像执行更多颜色查找,尝试为目标图像中的每个像素确定更正确的颜色。最简单的解决方案通常称为 超采样,或 过采样。参见 Wikipedia 上关于 Super-Sampling 的条目。通过在会映射到每个目标像素的区域内,从源图像取更多样本,该像素的最终颜色会更准确地表示该点处的扭曲图像。颜色样本越多,最终颜色越准确,也会生成更平滑、更真实的外观,不过扭曲速度会变慢。请记住,这种技术真正改善的只是源图像被压缩超过 50% 的区域中目标图像的整体外观。在扭曲放大源图像或保持大致相同比例的区域中,对源图像执行一次 插值查找,通常只需一次查找就能得到不错的结果。在 图像内爆 的变形示例中(以及 IM 示例中的许多其他示例),我简要提到过最简单的“超采样”方法。基本上就是放大输出图像的尺寸(或者在这个例子中只是放大输入图像),然后执行扭曲。扭曲完成后,再把图像调整回正常尺寸,这会合并生成的所有额外“样本”。例如……
magick -size 94x94 xc:red -bordercolor white -border 3 \
-virtual-pixel tile -implode 4 \
implode_tiled_box.gif
magick -size 94x94 xc:red -bordercolor white -border 3 \
-virtual-pixel tile -resize 400% -implode 4 -resize 25% \
implode_tiled_ss.gif
| 当然,与其放大输入图像,也可以从质量更高(更大)的源图像开始,或者在之前的某个处理步骤中生成一个,如果有的话。这在旋转文本时尤其有用,因为文本通常有非常细的细节,需要均匀保留下来,才能确保最终图像看起来质量良好。相关示例见 Polaroid 变换。 | 从 IM v6.4.2-6 起,通用扭曲运算符 可以直接生成放大的输出图像,你可以再将它缩放(或调整尺寸)回去,以合并并超采样结果像素。参见 扭曲缩放设置,以及下一个示例。 |
|---|---|
| 这只是 超采样 的一种方法(称为“网格”方法),不过这种方法还有许多其他变体。最终这些方法也许会在 ImageMagick 中更直接地实现,但目前简单地放大和缩放图像已经很好用,不需要额外编码。最后再提醒一句。超采样受最终图像中每个像素使用的样本数限制,因此也受最终调整尺寸时使用的缩放量限制。这决定了扭曲图像最终的“质量”。但使用更大的缩放因子时,扭曲图像的生成当然会慢得多。即便追求更高质量,也有其极限。在极端情况下,超采样无法处理任何涉及无穷大的图像扭曲(例如内爆图像的中心)。这种情况下需要完全不同的技术,例如下面介绍的 区域重采样。总之,超采样可以改善仅有轻微扭曲的图像外观,例如旋转、切变、仿射和简单透视。但它能改善的扭曲类型是有限的。自适应超采样 超采样技术还可以进一步扩展。不是为每个像素使用固定数量的颜色查找,而是检查源图像中各查找点之间的距离,或者检查低级采样返回的颜色彼此有多接近,以判断是否应该为该特定像素进行更多采样。也就是说,在不了解扭曲本身细节的情况下,也可以让超采样数量响应扭曲的需要。这称为 自适应超采样。这种技术在光线追踪器中实际上非常常见,因为几乎不可能确定结果图像在某个特定点有多复杂。在这种情况下,通常会限制为使用特定位置周围的“颜色差异”来判断何时需要更多样本。如果某个像素与邻近像素差异很大,就会在该区域使用更多样本,以细化很可能是某个三维对象边缘的部分。IM 目前不支持自适应超采样。不过,将替代采样方法加入 通用扭曲运算符(见下文)是很有可能的。这需要对代码做一些功能重排,因此短期内可能不会加入。超采样总结 超采样的难点在于确定到底需要多少“点样本”,以及这些样本应如何排列在子像素边界内。还要决定应应用怎样的“权重”。参见 Wikipedia 上关于 Super-Sampling 的条目。 |
区域重采样,用于更好的扭曲
替代超采样方法的最佳选择之一是 区域重采样。它不是扭曲一幅更大的图像再通过调整尺寸来平均结果,那只是从图像中取更多样本并求平均;而是实际确定为了生成每个特定输出像素,源图像中究竟应合并多少像素(基于该点处扭曲的“缩放”)。也就是说,算出每个输出像素在源图像中代表的大致“区域”,并按照重采样滤镜合并(滤波)该区域内的所有像素。事实上,这正是 ImageMagick Resize 运算符(现实中是一种非常特定的图像扭曲)能生成如此好结果的原因。不过对于 resize,你只需为整幅图像计算一次每个像素需要采样的区域尺度。它需要“采样”的区域是源图像中固定大小的矩形(窗口),这让重采样过程更容易,也为扭曲过程提供了一条捷径。对扭曲图像进行区域重采样时,要从中取样的像素区域(窗口)不仅会改变位置,也会改变大小。因此,目标中的某个像素可能只需要合并少量源图像颜色,甚至只需一次单独的插值颜色查找(如在放大时)。而目标图像中其他位置的另一个像素,可能需要采样非常非常多的像素,才能生成正确的最终颜色。靠近无穷大处时,它甚至可能必须把源图像中的所有像素都纳入采样过程。此外,目标像素在源图像中代表的区域,可能不是简单的正方形、圆形,甚至不是椭圆,而是会根据所用扭曲变成高度变形的形状。计算和处理这种棘手形状可能非常耗时,甚至几乎不可能实现。
使用源图像的椭圆区域来计算每个目标像素颜色的方法,称为椭圆加权平均(EWA)重采样,并由 Paul Heckbert(他还编写了“zoom”程序,几乎所有图像调整尺寸算法都源自该程序)在 PDF 研究论文 “Fundamentals of Texture Mapping and Image Warping” 中概述。随后它被用于定义新的 通用扭曲运算符(见下文)。椭圆对于 仿射扭曲 或 透视扭曲 都是完美形状。它尤其适合极端缩小(见下面示例)。虽然对其他扭曲并不完美,但通常也能较合理地适配许多其他扭曲,例如 圆弧和极坐标扭曲(但不包括它们的反向),以及像 桶形扭曲 这样的径向扭曲。但它并不适合 De-Polar 和 Shepards 扭曲 这类非线性扭曲映射,因此不会用于这些扭曲。超采样没有这种形状问题,因为每个“样本”都会反向映射到目标上。因此在这类情况下,它会成为更好的采样方法。不过如前所述,它可能没有采样所有需要的像素,也可能采样过多像素。
区域采样与超采样
下面是 IM 当前提供的三种采样方法,应用到一幅极端的无限平铺透视图像上。关于这种扭曲的细节,请参见下面的 查看远处地平线。
# input image: special checkerboard with a gold outline.
magick -size 90x90 pattern:checkerboard -normalize -fill none \
-stroke gold -strokewidth 3 -draw 'rectangle 0,0 89,89' \
-fill red -draw 'color 20,20 floodfill' \
-fill lime -draw 'color 40,70 floodfill' \
-fill dodgerblue -draw 'color 70,40 floodfill' \
checks.png
# Using Interpolated Lookup
magick checks.png -filter point \
-virtual-pixel tile -mattecolor DodgerBlue \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
horizon_tile_point.png
# Using Grid Super Sampling
magick checks.png -filter point -set option:distort:scale 10 \
-virtual-pixel tile -mattecolor DodgerBlue \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
-scale 10% horizon_tile_super.png
# Using Area Resampling (default)
magick checks.png -virtual-pixel tile -mattecolor DodgerBlue \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
horizon_tile.png
![[IM 输出]](../static/img/distorts/checks.png)
棋盘格图像 | | ![[IM 输出]](../static/img/distorts/horizon_tile_point.png)
插值
查找 | ![[IM 输出]](../static/img/distorts/horizon_tile_super.png)
超采样
x10 | ![[IM 输出]](../static/img/distorts/horizon_tile.png)
椭圆加权区域
(EWA) 重采样
---|---|---|---|---
所有图像都是完全相同的扭曲,只是使用了不同的“重采样”技术。上面最后一张图使用了 通用扭曲运算符 的默认 EWA 设置,可以看到它生成了极高质量的结果。不过生成这幅图用了 4.6 秒;虽然有点慢(因为涉及异常极端的情况),但还不算太糟。第一张图通过“[-filter](https://imagemagick.org/command-line-options/#filter) point”设置关闭了默认 EWA 重采样。这会强制每个像素使用 直接插值查找。因此这张图相对生成得非常快(0.51 秒),但可以看到,随着“距离”增加,“缩小”(下采样)加剧,结果非常糟糕。中间的图与第一张类似,但先将扭曲输出图像放大 10 倍,再缩回(网格重采样)以匹配其他图像。也就是说,对于每个目标像素,会查找并平均超过 100 个像素,从而对结果进行 超采样。它生成得相当快(1.2 秒),虽然总体上改善了图像质量,但改善有限。上例中使用的 ×10 非常重,远远超过多数超采样用途中更典型的 3 倍或 4 倍缩放。结果之间最大的差异在于,超采样只是在整幅图像上均匀地做总体质量改善。随着扭曲变得更严重,它就开始失效。结果是在中景处清晰可见的 重采样伪影,更具体地说,是地平线前方一串严重的莫尔纹效果。当每像素横向 10 个样本几乎匹配图像的棋盘格图案时,就会产生莫尔纹效果,并生成扭曲的颜色效果。另一方面,区域重采样更集中在靠近地平线的问题像素上(几乎所有时间都花在那里),而不是前景像素;在前景像素上,它实际上胜过超采样。基本上,上面是非常极端的扭曲,EWA 查找所需时间也与之相称。更常见的情况是,它会比单次插值查找生成好得多的结果,因为它会高效地查看所有相关像素,同时不会像超采样那样在不需要的区域使用太多样本。总结…… 使用简单椭圆(EWA 重采样)或矩形(Resize)进行“区域重采样”确实能得到好结果,因为参与缩放、仿射或透视扭曲的所有源像素都会被合并,以生成单个目标像素的最终颜色。在非常非线性的扭曲中,例如 DePolar 扭曲,或不确定的扭曲,例如 Shepard's 扭曲,甚至光线追踪中,寻找正确“区域”来重采样所有所需源像素会变得代价过高,超采样才是改善结果的最佳方法。但对于直接平铺、放大和未缩放旋转,一个非常快的单次“point”插值查找大概就足够了,甚至可能推荐使用,以确保完美的 no-op(无变化)扭曲(见下文)。不过请记住,所有重采样技术都只是确定每个单独像素颜色的方法。除了与目标和源之间(或可能的话反向)的位置映射有关之外,它实际上并不是图像如何被扭曲的一部分。
通用扭曲运算符
在生成这些示例、IM 论坛 随后的讨论,以及用户多次请求更简单、更快速地执行透视和其他扭曲的方法之后,IM v6.3.5-1 添加了一个新运算符,使我们能够更容易地加入许多不同类型的图像扭曲。这个 通用扭曲运算符 称为“[-distort](https://imagemagick.org/command-line-options/#distort)”,你可以使用“[-list](https://imagemagick.org/command-line-options/#list) Distort”查看你的 IM 版本可用的扭曲方法。
magick -list distort
“[-distort](https://imagemagick.org/command-line-options/#distort)”运算符接受两个参数:一个是上面给出的扭曲 方法 之一,另一个是由逗号或空格分隔的浮点值列表组成的字符串参数,用于控制具体的扭曲方法。
magick ... -distort {_method_} "{_list_of_floating_point_values_}" ...
不过,给出的浮点值数量高度依赖所使用的扭曲方法,而它们的含义也不仅依赖所选 方法,还可能依赖某个特定方法所需的控制点或属性的确切数量。对于“[Scale-Rotate-Translate](#srt)”(简称“[SRT](#srt)”)扭曲尤其如此,它实际上把三个独立的“[Affine](#affine)”扭曲组合成一个扭曲。许多扭曲 方法 会接受一组控制点(以 图像坐标 表示),这些控制点通常以 坐标对 形式给出,用来控制扭曲应如何修改图像。这些坐标对会在后面的 使用控制点的扭曲 中更详细说明。
扭曲选项、控制和设置
Best Fit +Distort 标志
默认情况下,“[-distort](https://imagemagick.org/command-line-options/#distort)”通常会把源图像扭曲到与原图像相同大小的图像中。也有例外,例如“[Arc](#arc)”扭曲(极坐标映射的一种变体),在图像的扭曲形态中,输入源图像大小其实没有太大意义(详情见下面的 Arc 扭曲)。该运算符的另一种形式“[+distort](https://imagemagick.org/command-line-options/#distort)”(IM v6.3.5-7 中加入)会尝试调整扭曲图像的大小,使其包含整个输入图像(如果可能),很像旧的 旋转和切变 运算符所做的那样。不过,这种特定“模式”还会进一步设置结果图像的 虚拟画布偏移(page)。这样稍后就可以按照控制点对应的正确位置,将该图像 合并图层 到另一幅图像上,并使用合适的 Alpha 合成(基本示例见 使用仿射分层的 3d 立方体)。此外(取决于扭曲方法),“[+distort](https://imagemagick.org/command-line-options/#distort)”会尝试考虑源图像中可能已有的 虚拟画布偏移,并把它作为扭曲过程的一部分。请参见各个扭曲方法的说明。因此,在使用通用扭曲运算符的“best-fit”形式 之前,你可能需要谨慎使用“[+repage](https://imagemagick.org/command-line-options/#repage)”属性设置运算符来清除或调整该偏移,也就是在使用“[+distort](https://imagemagick.org/command-line-options/#distort)”之前。如果不需要虚拟画布和偏移,也可能需要在 之后 使用它。另见 移除画布/Page 几何信息。普通的“[-distort](https://imagemagick.org/command-line-options/#distort)”就扭曲本身而言会忽略源图像中已有的任何偏移,但会将该偏移原样复制到扭曲后的图像。总结一下……使用“[-distort](https://imagemagick.org/command-line-options/#distort)”可把结果映射到同样大小的图像中。使用“[+distort](https://imagemagick.org/command-line-options/#distort)”可尝试自动设置输出图像尺寸,但也会使用并生成虚拟画布偏移(page 属性)。如果想覆盖这种通用视口选择,并精确控制想在结果中看到扭曲图像的哪种尺寸和哪一部分,也请参见下面的 扭曲视口。
注意……“[+distort](https://imagemagick.org/command-line-options/#distort)”生成的 best-fit 视口比用户通常预期的大 2 个像素。原因是这些像素包含区域重采样滤镜产生的半透明像素,而这些像素对于正确的“边缘拼接”和扭曲图像叠放非常重要。从技术上讲,添加的像素数应取决于 重采样滤镜支持范围 的输出缩放。也就是说,取决于像素区域会因重采样滤镜“扩散”多少。不过,由于每个像素的缩放都可能变化,计算绝对正确的额外像素数量非常棘手,而且通常不值得费力。因此添加 2 个像素是一种“折中”,因为扭曲很少会放大图像并导致像素“扩散”更多。此外,大多数标准重采样滤镜的支持范围为 2 个单位,所以添加 2 个像素也算合理。并且由于这种添加是“固定”的,如果用户愿意,也可以简单地通过各种方式 裁剪图像大小。在放大图像时,2 个像素的“折中”显然会太小。但这类扭曲相当少见,如果这是问题,用户可以定义自己的 视口(见下文)。虚拟画布上扭曲图像的虚拟偏移会进行调整,以计入这 2 个额外像素,因此扭曲图像对于叠放是正确的,虽然对简单合成并不正确。但要注意,Crop、Trim 会保留分层图像位置,而 Shave 和 Chop 会相对于此偏移移动图层图像。
扭曲时的像素颜色确定
如上面 反向像素映射 中所述,结果图像中的每个点,首先会根据所选扭曲方法,把该像素在目标图像中的位置映射到源图像中等价的(反向扭曲后的)位置。不过,像素最终颜色的确定并不这么简单,因为它受大量因素影响。
虚拟像素和平铺
扭曲映射后的点可能没有命中实际源图像,而是在它旁边某处,甚至离实际图像很远。解决办法是假装源图像被一个“无限”或“虚拟”的表面包围,该表面由当前“[-virtual-pixel](https://imagemagick.org/command-line-options/#virtual-pixel)”设置定义。关于此设置效果的详情和示例,请参见 虚拟像素 示例。这对于生成源图像的扭曲甚至未扭曲的平铺图案非常有用。相关技术在 虚拟像素 一节本身(未扭曲)以及下面的 仿射平铺 和 查看远处地平线 中展示。
无效扭曲像素
有时目标像素的扭曲甚至不会“命中”虚拟平铺图像!这通常发生在使用某种三维空间扭曲方法扭曲图像时,像素“向量”甚至没有命中图像所在的源平面。基本上,扭曲结果在数学上变为“未定义”。这种情况下,颜色将由“[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)”设置决定。例如,当你在 透视扭曲 中看到“天空”时(例如参见 查看远处地平线),用于确定源图像位置的数学变为“未定义”(实际上它有定义,但从用户的正向观看视角看并无效)。因此,“天空”会输出“[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)”。实际上,透视扭曲算法还会设法为靠近地平线的像素包含一些“抗锯齿”信息,虽然这种情况并不常见。
EWA 重采样和滤镜
一旦知道目标像素在源图像中“命中”哪里,就需要使用源图像中该“命中”点附近的像素,确定目标像素应采用的颜色。通常 扭曲运算符 会使用 区域重采样 方法 EWA(椭圆加权平均),对源图像中较大的区域求平均,从而算出该像素的正确颜色。可以使用“[-filter](https://imagemagick.org/command-line-options/#resize)”设置改变 EWA 重采样使用的滤镜。更多细节请参见 重采样滤镜,更具体地说,请参见 圆柱滤镜。最初,EWA 重采样使用 圆柱 Gaussian 滤镜,因为这是 EWA 重采样原始研究论文中定义的滤镜。但它往往会产生非常模糊的结果,尽管也不会产生锯齿效果。它曾经是默认滤镜,并且在该版本之前还伴随一个会导致极端模糊的实现错误(现已修复)。从 IM v6.6.5-0 起,在与 Laurentian University 数学教授 Nicolas Robidoux 进行了大量讨论后,图像扭曲的默认滤镜被替换为“**Robidoux**”滤镜,这是一个非常类似“Mitchell”的三次滤镜,专门为 EWA 重采样设计。关于它以及其他圆柱滤镜的信息,请参见 圆柱滤镜。不过请注意,任何底层的 Windowed Sinc 滤波函数都会替换为更圆形的 Windowed Jinc 滤波函数。因此,选择“Lanczos”滤镜会返回一个“Jinc windowed Jinc”滤镜,而不是“Sinc windowed Sinc”滤镜。更多细节见 Windowed Jinc 圆柱滤镜。旁注:“Sinc”函数实际上并不适合作为圆柱函数,因为该函数与网格上径向距离的相互作用,会在使用偶数个“lobes”时使滤波权重形成倾向于自我抵消的形式(权重和为零)。这又会导致它在像素级棋盘“hash”图案上使用时,试图生成近乎无限的颜色。基本上,EWA 使用重采样滤镜,很像 Resize 运算符,因此也可以使用特殊的 专家滤镜选项 来修改滤镜。例如,“Gaussian”及类似 Gaussian 的滤镜的模糊程度可以通过 滤镜模糊设置 控制。同样,可以使用 Lobes 支持设置 控制 Windowed Jinc 滤镜的大小和强度,例如“Lanczos”滤镜。 | _有若干极端扭曲方法会自动关闭 EWA 重采样,只使用更直接的 插值查找。
例如,Depolar 扭曲会生成圆弧形的重采样区域,并不太适合“椭圆”(EWA)重采样。其他扭曲,例如 Shepards,会让“缩放因子”的计算变得极其困难,不过未来对 distort 运算符的改进也许能使其可行)。
对于这些扭曲方法,建议使用 超采样 技术,以防结果中图像压缩(下采样)区域产生严重的 锯齿伪影。
重采样失败
在某些特殊情况下,EWA 重采样椭圆可能无法真正“命中”任何实际像素,从而无法创建加权平均。基本上,椭圆太小或太细,完全落在图像中每个像素之间。没有任何像素颜色时,它就无法为该点的输出图像生成颜色。这是一种极端情况,除非你在调试 专家滤镜设置,否则一般不可能达到。但万一没有命中任何像素,或者滤波权重加起来为零,重采样就会失败。此时 IM 会回退到简单的直接插值查找,就像关闭 EWA 滤波时得到的那样(见下一节)。如果想检查这种情况是否发生,可以使用特殊的 背景插值,并配合一种不寻常的背景色(如“red”),以突出显示此类重采样失败。例如,这里我故意把 box 滤镜的支持范围设得太小,从而让重采样椭圆非常小。我还大幅放大图像,这样你就能看到哪些部分“命中了像素”,哪些没有。 |
magick \( xc:red xc:white xc:black +append \) \
\( xc:blue xc:lime xc:white +append \) \
\( xc:black xc:red xc:blue +append \) -append \
-filter Box -define filter:support=0.4 \
+distort SRT 30,0 bad_box_distort.png
![[IM 输出]](../static/img/distorts/bad_box_distort.png)
在大幅放大的图像中,重采样圆要么只命中一个像素(生成一个纯色的锯齿圆),要么由于圆形采样区域完全落在像素之间而无法匹配任何像素,于是滤镜会回退到插值的颜色渐变(默认是 双线性插值),以便至少为结果图像得到某种较合理的有效颜色。这里是同一个示例,但把插值方法替换为特殊的(通常没什么用的)背景插值(只返回背景色,此处设置为“gray”)。 |
magick \( xc:red xc:white xc:black +append \) \
\( xc:blue xc:lime xc:white +append \) \
\( xc:black xc:red xc:blue +append \) -append \
-filter Box -define filter:support=0.4 \
-interpolate background -background Gray \
+distort SRT 30,0 bad_box_distort_gray.png
![[IM 输出]](../static/img/distorts/bad_box_distort_gray.png)
为了实现完整覆盖(也就是总能找到至少一个像素),圆柱重采样滤镜需要至少约 0.707(sqrt(2)/2)的“support”(box 滤镜的默认值)。所有滤镜通常都远大于这个最小 support 大小。这方面的示例见 圆柱滤镜 一节。 | _角落里的小色点是由虚拟像素的重采样优化导致的(在采样纯色 VP 区域时中止昂贵的 EWA 重采样)。更换“[-virtual-pixel](https://imagemagick.org/command-line-options/#virtual-pixel)”设置后,它们会消失或改变。
通常这不是问题,这里之所以能看到,只是因为 distort 使用了比原图略大的“Best Fit 视口”,因此边缘周围包含了几个额外像素,而本例中这些像素采样到了虚拟像素。
插值,或直接颜色查找
可以使用“[-filter](https://imagemagick.org/command-line-options/#filter) point”关闭滤波,从而关闭 EWA 重采样。这样做时,ImageMagick 会把颜色查找切换为快速且更简单的 像素插值。也就是说,它只使用对源图像的“单点”引用来查找颜色,没有任何“重采样区域”。结果像素的颜色会使用仅基于该点最近邻的插值颜色。
插值通常会在图像发生任何形式的缩小或下采样时,
导致严重的锯齿效果。
但对于只包含最小扭曲的图像,例如旋转、平铺,或者 图像放大(放大或上采样),它效果极好。超采样 技术可以与插值结合使用,以改善强压缩、缩小或下采样区域中的结果。关于使用超采样解决插值锯齿的示例,请参见 Depolar-Polar 循环问题(一种不能使用 EWA 重采样的扭曲)。
详细扭曲摘要
通过先设置“[-verbose](https://imagemagick.org/command-line-options/#verbose)”,再运行“[-distort](https://imagemagick.org/command-line-options/#distort)”(再次关闭可使用“[+verbose](https://imagemagick.org/command-line-options/#verbose)”),distort 会向标准错误通道输出有关算法和内部系数的信息;这些系数是它按指定方式扭曲给定图像时计算并使用的。你可以利用这些信息查看并理解扭曲如何工作以及如何应用。它也是调试工具,可用于找出哪里出了问题,并作为实现新扭曲过程的一部分。
magick koala.gif -verbose -distort SRT 0 +verbose koala_noop.gif
| | | ![[IM 文本]](../static/img/distorts/distort_verbose.txt.gif)
| 注意:结果图像几乎与输入图像相同,但并非完全一样(见下一节“no-op 扭曲”)。
---|---
详细输出列出了给定扭曲的两种替代扭曲技术。一种是“[AffineProjection](#affineprojection)”扭曲,另一种展示了 DIY FX 运算符 替代写法,详细说明如何把输出图像中的给定像素 (i,j) 映射到输入图像中的插值查找点 (xx,yy),从而变换图像。它会执行使用复杂数学(特征值)推导的重采样椭圆计算;这里则只计算源图像中未缩放的插值查找点,以确定 (i,j) 像素的颜色。二者都提供了扭曲过程的信息,并可用于提取额外信息,供同类型的其他扭曲使用。关于使用这些信息的更复杂示例,请参见下面的 透视内部机制 和 双线性内部机制。另外,关于使用 FX 命令进行图像扭曲的示例,请参见 FX 图像调整尺寸。上面额外的“0.5”加减,是把“像素坐标”转换为“图像坐标”所必需的,也是在数学上正确处理图像扭曲所必需的。参见下面的 图像坐标与像素坐标。
No-Op 扭曲
上面的示例展示了执行 no-op 扭曲的结果。也就是说,让图像经过 distort(为了某种副作用),但并没有实际扭曲参与其中(只是像素的一对一映射)。EWA 重采样滤镜不会精确再现与原图完全相同的颜色,而会把单个像素与其邻近像素轻微模糊。这是由于使用了二维滤镜,虽然颜色模糊很小,却无法完全消除。因此,要执行真正的“no-op”,也必须关闭 EWA 滤波,并使用 插值,或直接颜色查找(见上文)。 |
magick koala.gif -filter point -distort SRT 0 koala_noop_perfect.gif
![[IM 输出]](../static/img/distorts/koala_noop_perfect.gif)
几乎所有 插值设置 在被精确引用时,通常都会提取源像素的精确副本。不过出于谨慎,也可以指定 最近邻 插值,以提高速度,并确保无论 distort 可能产生什么浮点误差,都只返回精确匹配的颜色。 |
magick koala.gif -filter point -interpolate nearest \
-distort SRT 0 koala_noop_perfect_2.gif
![[IM 输出]](../static/img/distorts/koala_noop_perfect_2.gif)
这看起来可能适得其反,但它可以成为一种非常有用的方法:在不实际调整原始图像数据尺寸的情况下,扩大图像区域,或平铺图像(使用 虚拟像素方法)。相关示例见 通过 Distort 使用虚拟像素进行平铺。也就是说,使用 扭曲运算符 的副作用,例如多图像虚拟像素平铺、扩大或裁剪图像大小、添加边框,甚至平移(整数或子像素量)。这些都不真正要求图像被“扭曲”,只是以某种“编程”的方式被“修改”。
视口,Distort 查看哪里
如上所述,使用“[-distort](https://imagemagick.org/command-line-options/#distort)”或“[+distort](https://imagemagick.org/command-line-options/#distort)”会把结果“目标图像”的大小和位置分别变为:与源图像相同(忽略任何虚拟画布设置),或者对扭曲后的源图像做 best fit 计算(如果可能)。这两者基本上定义了目标图像能看到结果“扭曲空间”的哪一部分。另一种理解方式是,目标图像是一个查看结果扭曲图像的“窗口”,也就是进入扭曲空间的“视口”。“distort:viewport”设置会覆盖这两个默认值,并允许你直接指定想看到扭曲空间的哪一部分……
[-define](https://imagemagick.org/command-line-options/#define) distort:viewport=WxH+X+Y [-set](https://imagemagick.org/command-line-options/#set) option:distort:viewport WxH+X+Y
这些是在 IM v6.3.6-1 中加入的。它不会放大或缩放扭曲图像,只是指定扭曲图像空间中被查看的位置和区域(视口)。这可用于创建特定大小的目标图像,或将视图移动到扭曲图像空间中的特定区域。它非常类似于对无限大小(由虚拟像素定义)的扭曲图像使用“视口裁剪”。例如,这里我们把输出裁剪为只有考拉头部(使用 no-op 扭曲)。换句话说,就是对原始未扭曲图像进行直接“视口裁剪”。 |
magick koala.gif -define distort:viewport=44x44+15+0 \
-filter point -distort SRT 0 +repage koala_viewport.gif
![[IM 输出]](../static/img/distorts/koala_viewport.gif)
这里我们扩大视图,查看扭曲图像周围的额外空间,并展示 虚拟像素 设置对原始源图像周围无限空间的影响。 |
magick koala.gif -define distort:viewport=125x125-25-25 \
-filter point -distort SRT 0 +repage koala_viewport_2.gif
![[IM 输出]](../static/img/distorts/koala_viewport_2.gif)
在这种情况下,它更像是使用 Extent 运算符 来扩大图像。不过 distort 不是简单地用背景色填充,而是用 虚拟像素设置 填充新增区域。此处使用默认的“[Edge](misc.html#edge)”虚拟像素设置,结果是水平和垂直像素线从原始图像边缘的像素复制而来。你可能想为 虚拟像素设置 做一个更好的选择。例如,使用“[Background](misc.html#background)”设置会让这个 no-op 扭曲几乎完全像 Extent 运算符 那样工作。对于这幅图像,“[White](misc.html#white)”虚拟像素设置可能是更好的选择。 |
magick koala.gif -define distort:viewport=125x125-25-25 \
-virtual-pixel White -distort SRT 0 +repage koala_viewport_3.gif
![[IM 输出]](../static/img/distorts/koala_viewport_3.gif)
前面示例最后的“[+repage](https://imagemagick.org/command-line-options/#repage)”是必要的,用来移除在使用视口设置时“[-distort](https://imagemagick.org/command-line-options/#distort)”会留下的视口虚拟画布偏移。本例中不需要这些信息。在其他情况下,例如分层叠放扭曲图像时,你会需要这些偏移信息。视口选项与“[Tile](misc.html#tile)”甚至“[Mirror](misc.html#mirror)”虚拟像素设置配合时特别有用,可以生成任意大小、不同样式的平铺图像。甚至可以使用 distort 扭曲这些平铺图像,例如下面 仿射平铺 中的示例。 |
magick koala.gif -define distort:viewport=125x125-25-25 \
-virtual-pixel Mirror -distort SRT 0 +repage koala_viewport_4.gif
居中正方形裁剪
如果使用“[-set](https://imagemagick.org/command-line-options/#set)”选项来设置结果图像的“viewport”,可以在赋值中包含 百分号转义。更具体地说,可以包含能够进行数学计算的 FX 百分号转义。这意味着“viewport”可以通过计算得到,同时利用内存中当前图像大小等属性,来指定结果图像的最终大小。这是什么意思?也就是说,可以使用“viewport”生成特殊类型的 裁剪,而通常要做到这一点,需要对图像进行一次或多次预读(或使用更高级的 API 编程接口),并进行外部计算。例如,你可以在事先不知道原图大小或方向的情况下,裁剪出图像的“居中正方形”。这比较复杂,所以我把视口表达式放入变量中,以便更容易阅读、编写和调试,虽然它其实只是一个常量(固定)表达式。
size='%[fx: w>h ? h : w ]'
offset_x='%[fx: w>h ? (w-h)/2 : 0 ]'
offset_y='%[fx: w>h ? 0 : (h-w)/2 ]'
viewport="${size}x${size}+${offset_x}+${offset_y}"
magick worldmap_sm.jpg -set option:distort:viewport "$viewport" \
-filter point -distort SRT 0 +repage viewport_square.gif
结果图像是可以从任何输入源图像中提取出的最大居中正方形,无论该图像大小如何。distort 本身实际上并没有扭曲图像,只是复制视口覆盖的区域。注意,为了生成“居中正方形裁剪”,需要计算全部四个数字,因为所有值都依赖图像方向。因此每个表达式都使用“w>h ? ... : ...”形式的“图像方向”测试,让结果值取决于图像方向。下面是另一种形式,使用“min()”和“max()”函数,而不是图像方向测试。 |
magick worldmap_sm.jpg -set option:distort:viewport \
"%[fx:min(w,h)]x%[fx:min(w,h)]+%[fx:max((w-h)/2,0)]+%[fx:max((h-w)/2,0)]" \
-filter point -distort SRT 0 +repage viewport_square_2.gif
![[IM 输出]](../static/img/distorts/viewport_square_2.gif)
承蒙 Fred Weinhaus 的 Tidbits 页面 提供。使用多种图像处理技术完成同一件事的方法见 缩略图、正方形填充和裁剪。
宽高比裁剪
这个技术可以扩展为将图像居中裁剪到适合给定宽高比。另见论坛讨论 裁剪到宽高比。
其他视口示例
关于使用视口控制结果中可见扭曲空间部分的其他示例,也请参见下面的 旋转图像的方法。
输出缩放和超采样
[-define](https://imagemagick.org/command-line-options/#define) distort:scale=N [-set](https://imagemagick.org/command-line-options/#set) option:distort:scale N
这是在 IM v6.4.2-6 中作为通用输出图像缩放因子加入的。它会按给定因子放大输出图像,因此“[-distort](https://imagemagick.org/command-line-options/#distort)”需要生成多出 N2 个扭曲查找“样本”。这个数值通常是整数,但也可以是浮点放大因子。注意,许多扭曲也允许你“缩放”结果扭曲图像的大小,但结果图像尺寸不会受该缩放影响(除非使用了“best-fit”“[+distort](https://imagemagick.org/command-line-options/#distort)”)。不过,这个“scale”设置完全不会改变结果图像的内容,只是放大或缩小结果输出图像。例如,它可以与合适的“viewport”配合,生成一幅可以轻松“[-resize](https://imagemagick.org/command-line-options/#resize)”到特定大小的图像,让你在不损失质量的情况下,对扭曲图像进行受控“缩放”。例如,我们“缩放”到考拉的头部。 |
magick koala.gif -set option:distort:scale 2.5 \
-set option:distort:viewport 44x44+15+0 \
-distort SRT 0 +repage koala_zoom.gif
![[IM 输出]](../static/img/distorts/koala_zoom.gif)
注意,虽然请求的视口是 44x44 像素,但实际输出图像已经缩放到 110x110 像素。更常见的是,它被用作对扭曲操作进行“超采样”(见上文)的简单手段。为此会使用整数“超采样”缩放因子,并在扭曲图像后再缩放回原始大小,以合并额外样本并产生更高质量的结果。 |
magick koala.gif -filter point -set option:distort:scale 10 \
-distort SRT 0 -scale 10% koala_super.gif
![[IM 输出]](../static/img/distorts/koala_super.gif)
此外,当使用“区域重采样”并不需要、而是用“超采样”改善图像质量时(区域重采样只会拖慢速度),通常会通过“[-filter](https://imagemagick.org/command-line-options/#filter) point”选项将其关闭(见上一节)。
扭曲方法简介
Scale-Rotate-Translate (SRT) 扭曲
最简单但可能也是用途最广的扭曲之一,是“SRT”或“Scale-Rotate-Translate”扭曲。(SRT 只是一个快速简写)你已经在上面的示例中看到过这种扭曲的“no-op”示例,其中图像被处理,但没有对图像施加任何实际扭曲,不过它仍会被滤波,这可能引入非常轻微的模糊。这里重复一下上面“no-op”扭曲的结果……
magick koala.gif -distort SRT 0 koala_noop.gif
| _请注意,由于使用面积重采样,图像会产生非常轻微的模糊。不过,IM 的重采样滤镜是有意设计成尽量减小 No-Op 扭曲中的这种模糊,而且正常使用时也需要它。
如果为了特殊用途需要完美的 'no-op' 扭曲,就关闭 EWA 重采样。也就是在 distort 运算符之前,为上面的命令指定 'no-op' 滤镜 "-filter Point"。
---|---
'SRT 扭曲实际上是在一个扭曲方法中包含三个独立的扭曲,因此它被称为 'Scale-Rotate-Translate' 扭曲。除了 _angle 旋转之外,所有参数都是可选的;这使得参数形式非常灵活,具体取决于你给出多少个以逗号或空格分隔的参数,最多可达 7 个浮点数。 **-distort SRT "** |
Angle
| **"** | -> 居中旋转
---|---|---|---
Scale Angle
| -> 居中缩放并旋转
X,Y Angle
| -> 围绕给定坐标旋转
X,Y Scale Angle
| -> 围绕坐标缩放并旋转
X,Y ScaleX,ScaleY Angle
| -> 同上
X,Y Scale Angle NewX,NewY
| -> 缩放、旋转并平移坐标
X,Y ScaleX,ScaleY Angle NewX,NewY
| -> 同上
它所做的,是接收你选定的一幅图像,以及一个可选的控制点。如果没有给出控制点,就使用输入源图像的精确中心。围绕这个点,扭曲会依次……对图像执行 缩放、旋转,然后 平移,也就是把选定的控制点移动到新的位置。这就是这个扭曲名称的由来。上面显示的参数顺序反映了实际应用到图像上的操作顺序:用 X,Y 将变换的“中心”平移到原点,用 ScaleX,ScaleY 缩放图像,用 Angle 旋转图像,然后用 NewX,NewY 将“中心”平移到这些坐标。也就是说,这个运算符实际上表示 4 个内部扭曲操作,它们作为一个扭曲同时应用。虽然对我们人类来说,只涉及 3 种不同的扭曲。于是我们用 'koala' 图像看一个简单例子……只给一个参数,就是围绕图像中心做简单旋转,基本上会产生与较旧的 Rotate 运算符 类似的结果,但不会增加图像尺寸。 |
magick koala.gif -background skyblue -virtual-pixel background \
-distort ScaleRotateTranslate -110 koala_srt_rotate.png
![[IM 输出]](../static/img/distorts/koala_srt_rotate.png)
请注意,默认情况下输入图像的尺寸也会用于输出图像,因此旋转后的图像可能会被裁切。无论图像的像素数是奇数还是偶数,它也都会精确居中。使用 "+distort" 的“加号”形式,并清理产生的虚拟画布偏移,我们就能生成一个与普通 Rotate 运算符 非常相似的结果。 |
magick koala.gif -background skyblue -virtual-pixel background \
+distort ScaleRotateTranslate -110 +repage koala_srt_rotate2.png
![[IM 输出]](../static/img/distorts/koala_srt_rotate2.png)
| 从 IM 6.7.3-4 起,Rotate 运算符 已改为使用 Distort SRT 扭曲。在此之前它使用的是 Shear 操作,结果并不理想。
---|---
我们也把它缩小 30%,但使用透明背景。 |
magick koala.gif -alpha set -virtual-pixel transparent \
+distort ScaleRotateTranslate '.7,-110' +repage koala_srt_scale.png
![[IM 输出]](../static/img/distorts/koala_srt_scale.png)
下一组参数将指定图像围绕其旋转和缩放的“中心”。这个点称为图像中的“控制点”或“手柄”,是用来控制扭曲的位置。由于这个扭曲使用的是一个特定点,我们就不使用 'best-fit' 模式,以避免“虚拟偏移”的复杂性。例如,让我们围绕考拉的“鼻子”旋转并缩放它,这个点在源图像中的位置是 28,24。同时,我们也让 X 和 Y 的缩放比例不同。 |
magick koala.gif -background skyblue -virtual-pixel background \
-distort ScaleRotateTranslate '28,24 .4,.8 -110' \
koala_srt_center.png
![[IM 输出]](../static/img/distorts/koala_srt_center.png)
最后再举一个例子,我们还把“鼻子”移到图像底部附近,并把背景设置为匹配的白色背景。 |
magick koala.gif -virtual-pixel white \
-distort ScaleRotateTranslate '28,24 .4,.8 -110 37.5,60' \
koala_srt_trans.png
![[IM 输出]](../static/img/distorts/koala_srt_trans.png)
请注意,最终位置也是浮点值。事实上,所有参数都可以是浮点值,扭曲会正确处理。记住,Scale、Rotate 和 Translate 这些操作会按这个顺序执行。可以看到,这个扭曲非常灵活;虽然你可以把它理解成用三种不同方法依次扭曲图像,但实际上它是同时应用这三种扭曲来生成所示结果。这比执行多个单独的运算符更快,而且通常会产生更好的最终结果。上面的例子还演示了不同 Virtual Pixel 设置的用法,用来定义引用实际源图像之外区域时使用的颜色。若要查看 Interpolation 对旋转的影响,请参见旋转线条和边缘的插值。这个扭曲专门用于接收一幅图像,并根据该对象的移动和旋转生成动画。例如,这里我创建一艘风格化的飞船,然后用很粗略的方式为它制作动画。飞船停在它的基座 20,75 上(用于初始的“下蹲”缩放),而用于移动和旋转的普通“手柄”是原始图像中位于 20,60 的飞船中心。这些点表示控制点,借助它们就可以用简单方式为对象制作动画。
magick -size 80x80 xc:skyblue -fill yellow -stroke black \
-draw 'path "M 15,75 20,45 25,75 Z M 10,55 30,55" ' \
spaceship.gif
magick spaceship.gif \
\( -clone 0 -distort SRT '20,75 1.0,0.6 0' \) \
\( -clone 0 -distort SRT '20,60 1 0 20,49' \) \
\( -clone 0 -distort SRT '20,60 0.9 20 27,35' \) \
\( -clone 0 -distort SRT '20,60 0.8 45 40,23' \) \
\( -clone 0 -distort SRT '20,60 0.5 70 55,15' \) \
\( -clone 0 -distort SRT '20,60 0.3 75 72,11' \) \
\( -clone 0 -distort SRT '20,60 0.1 80 100,8' \) \
-set delay 50 -loop 0 spaceship_launch.gif
当然,这只是一个非常粗略的例子,说明如何用 '[SRT](#srt)' 扭曲让静态图像动起来,但你应该能明白其思路。你可以添加更多帧,也许再加一些火焰和烟雾来进一步改进它(欢迎投稿,最佳结果会连同你的名字一起添加到这里)。
旋转图像的方法
图像可以用许多方式旋转。但单纯的旋转未必是你想要的。旋转图像而不改变尺寸…… |
magick rose: -virtual-pixel black -distort SRT '20' rotate_normal.png
magick rose: -virtual-pixel black +distort SRT '20' rotate_noclip.png
![[IM 输出]](../static/img/distorts/rotate_noclip.png)
不过通常你并不想看到围绕图像主体的 'black' Virtual_Pixel(或任何其他非图像颜色)。一种解决办法是把图像裁剪到相同宽高比下最大的矩形(使用扭曲视口设置),使它只包含旋转产生的真实图像像素。不过计算这个矩形相当棘手,并且曾在 ImageMagick 论坛 上借助 Math Help Forum 中找到的一些方程式进行过大量讨论。这里我们旋转图像,并执行尽可能接近原始宽高比的内部裁剪。 |
angle=20
ratio=`magick rose: -format \
"%[fx:aa=$angle*pi/180; min(w,h)/(w*abs(sin(aa))+h*abs(cos(aa)))]" \
info:`
crop="%[fx:floor(w*$ratio)]x%[fx:floor(h*$ratio)]"
crop="$crop+%[fx:ceil((w-w*$ratio)/2)]+%[fx:ceil((h-h*$ratio)/2)]"
magick rose: -set option:distort:viewport "$crop" \
+distort SRT $angle +repage rotate_internal.png
![[IM 输出]](../static/img/distorts/rotate_internal.png)
这看起来复杂,是因为它实际上必须计算 4 个独立的值来定义视口设置:宽度、高度,以及原始图像中的偏移。另一个办法是不仅旋转,还把图像稍微放大,以便“填满”原始图像的边界。 |
angle=20
magick rose: -distort SRT \
"%[fx:aa=$angle*pi/180;(w*abs(sin(aa))+h*abs(cos(aa)))/min(w,h)], $angle" \
rotate_correction.png
![[IM 输出]](../static/img/distorts/rotate_correction.png)
最后这种方法非常适合照片的轻微旋转校正,因为它会保留图像原始尺寸。这个方法之所以更简单,只是因为只需要计算一个“缩放”值,因此可以“内联”完成。
使用控制点的扭曲
虽然 '[SRT](#srt)' 扭曲方法是通过指定旋转角度和缩放因子来定义的,但大多数扭曲都是通过移动源图像上的“点”,并把它们移动到结果图像中的新位置来定义的。这有点像在定义 '[SRT](#srt)' 平移时“中心”点的移动。这些点称为控制点,更常见的定义方式是为每个单独控制点给出 4 个浮点值(两对坐标)。因此,扭曲常常由多组 4 个值来定义。例如……
X1,Y1 I1,J1 X2,Y2 I2,J2 X3,Y3 I3,J3 X4,Y4 I4,J4 . . . .
| 其中,源图像中的控制点 Xi,Yi(相对于它的虚拟画布)会映射到扭曲后目标图像上的 Ii,Ji。 | 不过,由于扭曲运算符 实际上是把目标坐标映射到源坐标(参见反向像素映射),所以上述内容在内部的使用方式,是把 I,J 坐标映射到 X,Y 坐标。结果应该相同,只是思考方式不同。 |
|---|---|
| 在最初引入扭曲运算符 的 IM 6.3.6-0 版本之前,控制点的坐标顺序定义为先列出所有源坐标,再列出所有目标坐标。但这样很难判断哪些源坐标和目标坐标彼此对应,也不便于简单追加更多控制点来进一步细化扭曲。 | |
| --- | --- |
之所以这样定义,是为了让每个单独控制点的移动在以逗号(或空格)分隔的浮点值列表中保持在一起。这也允许将来使用外部的“控制点文件”。使用控制点的最简单扭曲是 '[Affine](#affine)' 扭曲,虽然你稍后会看到,它通常按三个点来定义,但你也可以只使用一个或两个控制点的移动。事实上,'[SRT](#srt)' 只是 '[Affine](#affine)' 扭曲的二点或一点子集。例如,这里我们把考拉图像中位于 '28,24' 的“鼻子”移动到新位置 '45,40'(红色箭头所示),结果就是图像位置的简单“平移”。 |
magick koala.gif -virtual-pixel white \
-distort Affine '28,24 45,40' koala_one_point.png
有两个点时,'[Affine](#affine)' 扭曲不仅可以平移图像,还可以缩放和旋转它(涵盖 '[SRT](#srt)' 扭曲的全部范围)。例如,这里我把考拉的“耳朵”(从 '30,11' 到 '48,29' 的红线)映射到更大的水平位置(从 '15,15' 到 '60,15' 的蓝线),这就要求图像经过缩放、旋转和平移,使控制点移动到这个新位置。
magick koala.gif -virtual-pixel white \
-distort Affine '30,11 15,15 48,29 60,15' koala_two_point.png
当然,'[SRT](#srt)' 扭曲也可以重现上面的二点 '[Affine](#affine)' 扭曲,只是这里我们用不同方式定义了扭曲。该使用哪种形式取决于你想达到什么目的。
图像坐标与像素坐标
在一般情况下使用控制点很直接,但当你需要把扭曲后的图像与另一幅图像或绘制的结构对齐时,事情会变得更困难。原因是,IM 中大多数运算符都按“像素位置 ”处理坐标(例如裁剪、绘制等),而扭曲则按数学上的“图像坐标 ”处理坐标。你需要记住的是,图像中的像素不是一个“点”,而实际上是一个大小为 1 个像素单位的“区域”。也就是说,位于 10,10 的像素定义了一个颜色正方形区域,它从向下/横向 10 个单位延伸到向下/横向 11 个单位。以 图像坐标 来看,“像素”的中心实际上位于 10.5,10.5。也就是说,当你扭曲图像并想把一个“像素”的中心移动到特定位置时,需要加上 0.5。因此,要重新定位图像角上的“像素”,你需要按照位于 0.5,0.5 和 _Width_ -0.5,_Height_ -0.5 的像素来移动图像。另一方面,如果要按照图像实际的“边缘”重新定位图像,就只需使用坐标 0.0,0.0 和 _Width_ ,_Height_。你只需要想清楚自己真正想定位的是什么:图像“像素”的中心,还是图像的“边缘”。或者这对你的具体问题是否真的重要。请记住,如果你想把其他元素绘制到扭曲后的图像上,就需要按“像素位置 ”给出绘制位置。没错,"-draw" 运算符可以用浮点值绘制线条、圆和其他形状。同样,对象的描边宽度和/或半径也可以用浮点值给出。
![[IM 输出]](../static/img/distorts/koala_two_point.png)
| _小于 1.0 的绘制描边宽度效果不好(参见绘制线条)。此外,区域填充会在填充区域的边缘额外加上 0.5(与描边宽度的增加相匹配)(参见 Draw 填充边界)。无论实际使用的描边宽度是多少,都会这样做。
更多信息请参见 Draw 填充边界。我认为这是一个 bug。_
使用百分号转义的控制点
你也可以在扭曲参数中使用百分号转义。例如,你可以提取一幅图像的属性,然后用它们把另一幅图像调整尺寸到与第一幅图像匹配。这里我取得 "rose:" 内置图像的尺寸,然后使用 '[Affine](#affine)' 扭曲把较大的 "logo:" 图像调整到相同尺寸(不保持宽高比)。 |
magick rose: -set option:rw %w -set option:rh %h +delete \
logo: -alpha set -virtual-pixel transparent \
+distort Affine '0,0 0,0 %w,0 %[rw],0 0,%h 0,%[rh]' \
+repage logo_sized_as_rose.png
![[IM 输出]](../static/img/distorts/logo_sized_as_rose.png)
请注意,distort 会在虚拟画布上生成稍大的“图层图像”(包括负偏移),所以我在上面的例子中需要包含 "[+repage](https://imagemagick.org/command-line-options/#repage)"。它的边缘也会发虚,因为 distort 生成的是图像的精确扭曲或真实扭曲,而不是经过规整的正交调整尺寸后的图像。关于使用扭曲运算符 像 Resize 运算符 那样精确调整图像尺寸的更高级示例,请参见 Distort vs Resize,以及下面的 Distort Resize 方法。你还可以使用百分号转义,根据图像在当前图像列表中的位置来计算扭曲。Animated Distorts 中给出了这类示例。
控制点最小二乘拟合
如果你为 '[Affine](#affine)' 扭曲提供超过 3 个控制点,或为 '[Perspective](#perspective)' 或 '[Bilinear](#bilinear)' 扭曲提供超过 4 个点,ImageMagick 会对给出的所有点执行最小二乘平均,以找到这些扭曲的“平均”表示。这意味着,如果你试图把一幅图像与另一幅图像匹配起来(这种技术称为“图像配准 ”),就可以定义超过最低要求数量的点,使结果成为更精确的扭曲。当然,如果其中一个或多个点与其他点“拟合”得不好,那么由于 IM 会尝试使用给出的所有控制点(包括坏点)寻找最佳拟合,结果会被这个“异常”点带偏。在某些情况下,可能需要进行检查,以找出并移除“坏坐标对”。
来自文件的控制点
扭曲所需的数字(参数)列表也可以通过 '@filename' 语法从文件读取,就像你可以为 "[-annotate](https://imagemagick.org/command-line-options/#annotate)" 和 "[label:](text.html#label)" 之类输入文本一样(参见文本参数中的转义字符)。例如,你可以像这样指定一个扭曲……
magick input.png -distort Perspective '@file_of_coords.txt' output.png
文件名也可以只是 '@-',表示从标准输入读取文件。文件本身会被作为字符串读入,并被视为相关扭曲所需的坐标(参数)列表。由于数字可以用逗号或空白分隔,这意味着坐标对可以整齐地按每行一对坐标的形式排列……
X1 Y1 I1 J1
X2 Y2 I2 J2
X3 Y3 I3 J3
X4 Y4 I4 J4
....
这与最小二乘拟合 结合起来,使图像配准的使用非常实用。由于文件只是每行四个数字的列表,你可以使用 "cut"、"paste"、"column" 等其他文本处理脚本工具,以及 "sed"、"awk"、"perl" 等更高级的文本处理脚本工具来操作坐标。随着扭曲变得更高级,坐标和扭曲参数文件的使用会更加重要,例如 '[Shepards](#shepards)' 扭曲,以及计划中的未来扭曲 'Grid' 和 "Mesh',其中可能涉及数百个坐标对。
Affine(三点)扭曲方法
Affine 扭曲
上面展示的 '[SRT](#srt)' 扭曲,以及 'Affine' 扭曲的一点和二点形式,实际上都是完整三点形式 'Affine' 扭曲的简化。事实上,如果你查看任意扭曲的 "-verbose" 输出,尤其是任何 '[SRT](#srt)' 扭曲(示例见 verbose 扭曲设置),就会发现它在内部实际上确实是一个 '[AffineProjection](#affine_projection)' 扭曲(见下文)。上述方法唯一不能完整处理的扭曲效果,是类似 Shear 运算符 所提供的“切变”。为此你需要使用三点 affine 扭曲。你可以把它想成一个三点扭曲:将第一个坐标映射想象成“原点”,另外两个坐标映射则是从这个原点出发的矢量。例如,这里我绘制一些文本,并叠加一条红色和一条蓝色“矢量”,用来定义相对于该文本的三个控制点。现在,通过移动这两条线的坐标(作为图像坐标),我们可以平移、旋转、缩放和切变这幅文本图像,使它适合这些线的新位置。
magick -background lightblue -fill Gray -font Candice \
-size 100x100 -gravity center label:Affine\! \
-draw 'fill blue stroke blue path "M 3,60 32,60 M 27,58 27,62 32,60 Z"' \
-draw 'fill red stroke red path "M 3,60 3,30 M 1,35 5,35 3,30 Z"' \
label_axis.png
magick label_axis.png \
-distort Affine ' 3.5,60.5 3.5,60.5
32.5,60.5 32.5,60.5
3.5,30.5 33.5,20.5' label_axis_distort_shear.png
magick label_axis.png \
-distort Affine ' 3.5,60.5 3.5,60.5
32.5,60.5 27.5,85.5
3.5,30.5 27.5,35.5' label_axis_distort_rotate.png
magick label_axis.png \
-distort Affine ' 3.5,60.5 30.5,50.5
32.5,60.5 60.5,80.5
3.5,30.5 30.5,5.5' label_axis_distort_affine.png
在第一个示例中,只修改了第三个坐标(用于垂直红线),导致图像被切变,并沿 Y 轴拉伸。当然,它并不局限于 Y 轴。后面的例子会对图像做出更大的改变,包括旋转和平移。当然,Annotate Text 运算符也可以用同样方式倾斜实际文本,不过它只能改变角度。该运算符不会在某个特定方向上缩放或放大文本。也就是说,它可以旋转一个“矢量”,但不能把它拉长或缩短。示例表请参见 Annotate 参数用法。Affine 扭曲可以对任何图像执行这种扭曲,而不只是绘制出的文本。使用少于或多于三对坐标的 Affine 如果只提供 1 或 2 对控制点,IM 会使用更受限的 affine 扭曲形式来匹配这些较少点的移动。例如,只有 1 对坐标时,它会限制为图像的无缩放平移。有 2 个点时,它会限制为 '[Scale-Rotate-Translation](#srt)' 扭曲(无切变)。示例请参见前面对使用控制点的扭曲的讨论。如果给 '[Affine](#affine)' 扭曲提供超过 3 个控制点,那么 IM 会使用最小二乘拟合 找到与给出的 所有 坐标对相匹配的最佳“三点” affine 扭曲。这意味着源图像控制点可能不会精确映射到目标图像控制点,而是给定所有点的最佳拟合“平均”。例如,如果你有一份文档扫描件,可以定位并映射文档的全部 4 个角,用 affine 扭曲校正文档的旋转和缩放。这样就能基于 4 个点而不是 3 个点得到更好的“平均”拟合。请注意,虽然更多坐标可以产生更好、更精确的扭曲,但如果某一对坐标非常糟糕,最小二乘拟合可能完全得不到很好的拟合。可能需要某种检查来排除“坏坐标对”。 Future: Add some code to IM to report how 'accurate' each input coordinate pair is relative to the others to help determine what 'bad points' should be eliminated by the user.
Affine Projection 扭曲
正如我已经提到的,'[SRT](#srt)' 扭曲的各种参数以及 '[Affine](#affine)' 扭曲的控制点,会在数学上转换为 6 个特殊数字,它们表示 'Affine Projection' 的“系数”。Affine Projection 中的这些数字,是用于把源图像中的点正向映射到目标图像的系数。也就是说,它们是把源图像 x,y 映射到目标图像 i,j 所用的数学值。6 个浮点参数为(按给出的顺序)……
sx, rx, ry, sy, tx, ty
这些又组成扭曲表达式…… Xd = | sx*Xs + ry*Ys + tx | , | Yd = | rx*Xs + sy*Ys + ty |
---|---|---|---|---|---
其中 "Xs,Ys" 是源图像坐标,"Xd,Yd" 是目标图像坐标。在内部,ImageMagick Distort 会反转上述方程,以便执行合适的像素映射,把 "Xd,Yd" 坐标映射为在源图像中查找 "Xs,Ys" 处的颜色。关于各种 Affine Projection Matrix 值如何影响图像的更多信息,请参见 Affine Matrix Transforms 子页面。如果你已经预先计算好了这些系数(比如从 distort 的 Verbose Output 中提取,或使用其他方法根据其他形式的输入参数自行计算),那么可以直接把它们提供给 IM 来扭曲图像。例如,这里我用角度来计算系数,而不是用控制点的移动,从而“切变”图像。 |
angle=-20
tan=`magick xc: -format "%[fx:tan( $angle *pi/180)]" info:`
magick koala.gif -alpha set -virtual-pixel Transparent \
+distort AffineProjection "1,$tan,0,1,0,0" +repage \
koala_affine_proj.png
![[IM 输出]](../static/img/distorts/koala_affine_proj.png)
在 ImageMagick 中执行这种扭曲的旧方法,是使用 "-affine" 和 "-transform" 这一对操作。不过从 IM v6.4.2-8 起,这只是通过扭曲运算符 的“加号”或 'bestfit' 形式对 'AffineProjection' 的一次简单调用。更多细节请参见 Affine Matrix Transforms 子页面。
Affine 扭曲示例
Affine 平铺
到目前为止,我们看到的上述三种 affine 类扭曲方法,也都提供了有趣的方式,可以基于扭曲后的图像生成各种平铺图案。
magick checks.png -alpha set -virtual-pixel tile \
-distort ScaleRotateTranslate '20,20 .5 30' \
checks_srt_tile.png
magick checks.png -alpha set -virtual-pixel tile \
-distort Affine '0,0 10,10 0,89 10,50 89,0 50,0' \
checks_affine_tile.png
magick checks.png -alpha set -virtual-pixel tile \
-distort AffineProjection '0.9,0.3,-0.2,0.7,20,15' \
checks_amatrix_tile.png
以这种方式使用扭曲映射,实际上就是 3D 图形库和游戏中“纹理映射”的工作方式。唯一的区别是,它们会把表面的三维坐标映射回二维图像。即使是 'no-op' 扭曲("-distort SRT 0"),配合适当的扭曲视口,也提供了一种有用方式,可以平铺一整串图像,例如动画闪光平铺块。
magick glitter_blue.gif -virtual-pixel tile \
-filter point -set option:distort:viewport 100x100 -distort SRT 0 \
glitter_blue_tiled.gif
请注意,我还使用了 "[-filter](https://imagemagick.org/command-line-options/#filter) point" 来关闭 EWA 重采样,从而加快操作,并确保得到源图像像素的完美(未采样)副本。扭曲视口 也可以指定偏移,从而在结果图像上“滚动”平铺图像。
使用 Affine 分层的 3d 立方体
'[Affine](#affine)' 扭曲及其控制点非常适合从三幅图像生成正投影和等轴测立方体(定义请参见 Wikipedia 的 Orthographic Projection 和 Isometric Projection)。你只需要确定目标图像上的四个控制点。由于我们将使用图像分层技术,这些点甚至可以是负值,并允许 IM 根据生成的扭曲图像相应调整最终图像尺寸。在这个例子中,我选择控制点 '0,0' 作为立方体中心,并选择围绕该中心点等距分布的三个点:'-87,-50'、'87,-50' 和 '0,100'。接下来我只需要把三幅(最好是正方形)图像的相应角映射到这些控制点。 |
magick \
\( lena_orig.png -alpha set -virtual-pixel transparent \
+distort Affine '0,512 0,0 0,0 -87,-50 512,512 87,-50' \) \
\( mandrill_orig.png -alpha set -virtual-pixel transparent \
+distort Affine '512,0 0,0 0,0 -87,-50 512,512 0,100' \) \
\( pagoda_sm.jpg -alpha set -virtual-pixel transparent \
+distort Affine ' 0,0 0,0 0,320 0,100 320,0 87,-50' \) \
\
-background none -compose plus -layers merge +repage \
-bordercolor black -compose over -border 5x2 isometric_cube.png
![[IM 输出]](../static/img/distorts/isometric_cube.png)
请注意,在扭曲图像时,我使用的是图像实际边缘的坐标。这意味着从数学上说,这些图像应该精确贴合在一起。还要注意,我并没有简单地把图像“合成”在一起(使用默认的 Over Alpha Composition)。如果那样做,图像之间会出现略带透明的“缝隙”。正确方式(如图所示)是使用 Plus Alpha Composition 来连接“边缘相接”的片段,这会得到没有透明缝隙的完美接合。更多信息请参见对齐两个带蒙版的图像。之后我添加了额外边框,并移除了所有透明度。这并不是必需的,你也可以很容易地使用任何背景(或 "none"),但这样做会突出显示图像中可能存在的任何“缝隙”。
右侧显示的是图像中这样一个接合处的放大,显示沿接合处没有任何“black 填充”的缝隙。另一种创建等轴测立方体的方法不使用 "-distort",见使用 Shears 的等轴测立方体。不过,这种技术不允许使用亚像素坐标(虽然上面我没有用,但本可以使用),而是限制为使用整像素(整数)坐标来定位图像。
使用 Affine Shears 的 3d 阴影
上面使用的同一种分层方法,也可以用来为奇特形状生成很酷的三维阴影。也就是给任何直立的“扁平”形状添加阴影。例如,先创建一个有平坦底边、因此可能直立的形状。 |
magick -background None -virtual-pixel Transparent -fill DodgerBlue \
-pointsize 72 -font Ravie label:A -trim +repage \
-gravity South -chop 0x5 standing_shape.png
![[IM 输出]](../static/img/distorts/standing_shape.png)
请注意,这个“形状”有一个平坦的底边,也就是图像的最后一行。这很重要,因为我们会沿着这一行扭曲该形状,使阴影沿这一行连接到直立形状。下面是从这个“直立形状”生成 3-D 阴影的命令 |
magick standing_shape.png -flip +distort SRT '0,0 1,-1 0' \
\( +clone -background Black -shadow 60x5+0+0 \
-virtual-pixel Transparent \
+distort Affine '0,0 0,0 100,0 100,0 0,100 100,50' \
\) +swap -background white -layers merge \
-fuzz 2% -trim +repage standing_shadow.jpg
![[IM 输出]](../static/img/distorts/standing_shadow.jpg)
上面为得到所示结果执行了相当多的步骤。不过最棘手的是第一行。它先翻转图像,然后再用一次“distort 翻转”翻回来。这样做的结果是,底部那一行现在被放置到虚拟画布上 Y=0 的位置。也就是说,整幅图像被赋予了负偏移,使底部行穿过虚拟画布的原点。通过这个“技巧”,我们就可以对提取出的“阴影”使用一个非常简单的“affine shear”来扭曲它。因此,我们不需要知道形状图像的尺寸就能扭曲阴影,同时仍能让所有内容保持“对齐”,因为它们都沿原始图像底部(Y=0)行保持同步。你可以只通过调整“affine shear”的最终坐标('100,50')来改变阴影落下的方向和长度。前两个“坐标对”不应修改,因为它们会把阴影沿底部行“锁定”到原始图像上。不过请注意,直到最后一步之前,所有图像都包含负的虚拟画布偏移;如果你打算查看或保存中间处理图像,需要格外小心。这个阴影效果的唯一问题是它是“统一模糊”。也就是说,阴影并不真实。真实情况下,阴影在连接到“直立形状”的地方应该清晰,而离得越远越模糊。不过这可以使用可变模糊映射来实现,例如距离模糊阴影字体中使用的方法。
使用透视压缩的 3D 阴影
这是另一种为阴影添加可变模糊的方法,虽然我并不真正推荐它,但实现起来相当简单。这个例子是在 Variable Blur Mapping 加入 ImageMagick 之前开发的。基本上,你先使用透视扭曲(下面会详细介绍)扭曲初始阴影形状,从而强烈压缩阴影的“远端部分”并使其模糊,然后再把这个压缩结果扭曲到上面使用的最终“Affine Shear”位置,以展开这种压缩。 |
magick standing_shape.png -flip +distort SRT '0,0 1,-1 0' \
\( +clone -virtual-pixel Transparent -mattecolor None \
+distort Perspective \
'0,0 0,0 100,0 100,0 0,-100 45,-100 100,-100 60,-100' \
-fuzz 2% -trim -background Black -shadow 60x3+0+0 \
+distort Perspective \
'0,0 0,0 100,0 100,0 45,-100 -100,-50 60,-100 0,-50' \
\) +swap -background white -layers merge \
-fuzz 2% -trim +repage standing_shadow_var.jpg
![[IM 输出]](../static/img/distorts/standing_shadow_var.jpg)
这几乎与原始的 3D 阴影示例 完全相同,只是多了一些步骤。原始形状先被扭曲成梯形,然后裁掉多余空间以加快下一步。随后我们从扭曲后的形状中提取一个模糊阴影。阴影图像从扭曲图像创建出来之后,再使用相同控制点将阴影图像反扭曲,并把它移动到作为 Affine Shear 的位置。关键在于,阴影模糊发生在一幅扭曲后的图像上,而这幅图像随后又被反扭曲(在本例中同时进行了 Affine Shear)。结果,模糊也会被扭曲并展开,从而在阴影顶部周围模糊更多,而沿基线处少得多。由于透视模糊,我们得到一种可变模糊,它的峰值应当出现在离地面基线约 100 像素的位置。这由初始透视模糊控制点定义。
使用 Distort 调整图像尺寸
Distort 和 Resize 实际上在许多方面非常相似。它们都是图像扭曲运算符,也都使用反向像素映射来创建结果图像。它们还都使用 "[-filter](https://imagemagick.org/command-line-options/#resize)" 设置及其专家控制来确定颜色,不过使用方式非常不同。Resize 是一种简化的(也常见得多的)图像扭曲操作,允许进行许多优化。它是正交对齐的,因此在 resize 中可以使用两遍正交图像滤波方法。也就是说,先借助一个中间临时图像在一个维度上调整尺寸,再在另一个维度上调整尺寸。此外,由于缩放因子在整个目标图像上是恒定的,边缘又对齐到整像素(整数)尺寸,算法可以大大简化其处理过程以及所用滤镜的缓存需求。所有这些限制带来了各种优化,使它相比 distort 需要完成的工作要快得多。Distort 也可以调整图像尺寸,但它以单遍完成,直接从原始图像转换到新的结果图像。它不需要把边缘对齐到整数像素位置,并且可以旋转和缩放每个像素位置。换句话说,它是一个通用得多的运算符,需要对最终结果中的每个像素执行大量额外处理,优化空间更少。要让 Distort 生成与 Resize 等价的图像,它必须遵循完全相同的限制,并使用一些复杂的图像处理技巧。这曾在 IM 论坛的 Correct Resize (using distorts) 中讨论,并产生了一种基于 Affine 扭曲方法 的等价 distort resize 技术。由此得到的 'Resize ' 扭曲方法是在 ImageMagick 6.6.9-2 版本中加入的。这个扭曲的命令行界面(CLI)版本会接受完全相同的 Geometry Argument,也就是 Resize 所接受的内容,包括两个维度缩放因子之间的细微差异,使其成为直接的 resize 替代方案。 |
magick logo: -distort Resize 150x logo_resized.png
![[IM 输出]](../static/img/distorts/logo_resized.png)
| _面向 'Resize ' 扭曲方法的其他 API 接口只会接受两个数字作为参数,并把它们视为结果图像的最终整数尺寸。目前它们不会接受带有各种 resize 控制标志的实际 geometry 参数,也就是那些会修改最终图像尺寸的标志。也就是说,百分比、只在更大/更小时调整尺寸,甚至保持宽高比等标志都不可用。
是否为这种特殊图像扭曲方法添加此类支持,留给这些 API 的维护者。_
---|---
上面的 Distort Resize 与普通 Resize 运算符 的真正区别在于,distort 版本使用慢得多的单遍圆柱(椭圆)滤镜,来确定每一个像素的最终颜色。换句话说,它提供了两遍正交滤镜(resize)与单遍但二维的圆柱滤镜(distort resize)之间的直接比较。这样的比较示例请参见 Distort vs Resize。
Distort Resize 内部机制
下面是上述 Distort Resize 在内部执行的等价操作。
magick logo: -alpha set -virtual-pixel transparent \
+distort Affine '0,0 0,0 %w,0 150,0 0,%h 0,113' \
-alpha off -crop 150x113+0+0 +repage distort_resize.png
值 '150' 和 '113'(在两处使用)是最终图像四舍五入到最接近整数后的期望尺寸。它的计算目标是在保持最终整数尺寸限制的同时,尽可能保持图像宽高比。通常它们由 ImageMagick 根据给定的 resize Geometry Argument,通过一个单独的 API 函数计算出来。随后它启用透明度和透明的 Virtual Pixels,使外部的“虚拟像素”不参与最终像素颜色的计算。扭曲完成后,透明度会再次被移除(关闭),而 distort 添加的“缓冲”像素会通过图像裁剪移除。由于使用透明像素,上面的命令只会对不含任何透明度的图像正确工作,例如上面示例中的 "logo:" 内置图像。下面是复杂得多的版本,用来把 Virtual Pixels 的效果与图像中可能已有的透明度分离开来。
magick logo: -alpha set -virtual-pixel transparent \
\( +clone -alpha extract -alpha opaque \) \
+distort Affine '0,0 0,0 %w,0 150,0 0,%h 0,113' \
-alpha off -crop 150x113+0+0 +repage \
-compose CopyOpacity -composite distort_resize_trans.png
这会执行两次扭曲:先扭曲图像,然后单独扭曲 alpha(透明度)通道;每种情况下都使用 transparent 来移除 virtual-pixel 效果。因此,它至少比原始图像没有透明度时的方法慢两倍。这两种技术都由 Distort Resize 方法 在内部实现。因此,这个“方法”实际上只是给用户使用的便利“宏”,而不是一个真正的扭曲方法;真正的扭曲是“affine”扭曲。
四点扭曲方法
透视扭曲
最常被要求的扭曲类型,大概就是快速的透视扭曲操作。这是一种4点扭曲,因此至少需要4组控制点对,也就是16个浮点值。例如,这里有一张建筑物图像。我从这张图像中手动找出了4个点的位置(红色)。我还定义了这些点在最终图像中要变换到的最终位置(蓝色),用来把建筑物正面“拉直”或“校正”。
magick building.jpg \
-draw 'fill none stroke red polygon 7,40 4,124, 85,122, 85,2' \
building_before.jpg
magick building.jpg \
-draw 'fill none stroke blue polygon 4,30 4,123, 100,123, 100,30' \
building_after.jpg
要实际执行图像扭曲,只需把这些坐标送入 'perspective' 方法,也就是 "-distort" 的该方法。
magick building.jpg -alpha set -virtual-pixel transparent \
-distort Perspective \
'7,40 4,30 4,124 4,123 85,122 100,123 85,2 100,30' \
building_pers.png
注意右上角的空白区域,那里是扭曲没有命中源图像中的像素数据。IM 在这种情况下的行为由 "-virtual-pixel" 设置控制(参见虚拟像素)。不太显眼的是,原图最左边缘也有一小部分因为同样的原因“丢失”了。顺便再把每个映射对的坐标互换,反向执行一次扭曲。这样可以看到图像被扭曲操作劣化了多少。
magick building_pers.png -alpha set -virtual-pixel transparent \
-distort Perspective \
'4,30 7,40 4,123 4,124 100,123 85,122 100,30 85,2' \
building_pers_rev.png
还不错。确实有不少“模糊”,但这是难以避免的。请注意,图像右侧被压缩得最多,因此那里的“模糊”更严重。所有扭曲都会受到这种压缩问题影响,所以应尽量从原始图像开始扭曲,而不要再去扭曲已经扭曲过的图像。下面是使用这个变换的另一个例子,使用前面创建的特殊棋盘测试图像,先扭曲再反向扭曲。
magick checks.png -alpha set -virtual-pixel transparent \
-distort Perspective '0,0,0,0 0,90,0,90 90,0,90,25 90,90,90,65' \
checks_pers.png
magick checks_pers.png -alpha set -virtual-pixel transparent \
-distort Perspective '0,0,0,0 0,90,0,90 90,25,90,0 90,65,90,90' \
checks_pers_rev.png
可以看到图像压缩造成的轻微模糊,但图像基本恢复了。实际发生的是,IM 使用给出的所有控制点对来计算 '[Perspective Projection](#perspectiveprojection)'(见下一节)的适当系数。如果加入 Verbose 设置,就能看到这些系数,以及 IM 内部用于执行此扭曲的 DIY FX 等价式。如果只提供3组或更少的控制点对,IM 会自动退回到更简单的 '[Affine](#affine)' 扭曲。而超过4个点(用于 '图像配准')时,会通过最小二乘拟合为所有给定控制点寻找最匹配的扭曲。未来:另一种方案。这四个坐标也可以表示一个三角形和一个中心点。可以固定三角形并移动中心点,或者固定中心点并移动另外三个坐标,从而生成透视视图。 如果想更详细地了解扭曲的工作方式,请看下面的透视内部机制。也可以查看 Gernot Hoffmann 在 PDF 论文透视校正中给出的 Postscript 实现。另请参阅 Leptonica 仿射和透视变换。
观察远处的地平线
如果调整坐标,让“消失点”落在图像边界之内,就可以用透视扭曲产生一些非常特别的效果。 |
magick checks.png -mattecolor DodgerBlue \
-virtual-pixel background -background Green \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
checks_horizon.png
![[IM 输出]](../static/img/distorts/checks_horizon.png)
这里我们为“包围”原始图像的虚拟像素使用了 'Green',并通过虚拟像素背景设置启用了它。不过更有意思的是,通过 "[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)" 设置定义的 'blue' 颜色出现了。这个 'blue' 表示由扭曲生成的像素为 无效 的区域,在这些区域中,"-distort" 运算符只会输出 "[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)" 设置的颜色。对于透视扭曲,结果图像中落到“天空”里的任何像素都会被归类为无效。它还把“天空”定义为“地平线”上源图像不会出现的那一侧。只有当结果图像被扭曲强烈缩短时,“天空”才会出现在透视扭曲图像中。如果不希望最终图像结果中出现“天空”,最好的做法是把 "[-background](https://imagemagick.org/command-line-options/#background)" 和 "[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)" 设置为同一种颜色。透视扭曲在使用某种特殊的无限平铺虚拟像素设置时会更有意思。例如,这里我们使用 '[tile](misc.html#tile)' 设置生成了一个无限平铺的平面。 |
magick checks.png -virtual-pixel tile -mattecolor DodgerBlue \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
horizon_tile.png
![[IM 输出]](../static/img/distorts/horizon_tile.png)
关于这张图像需要提醒一句。要求生成无限平铺图像会 非常慢。图像越大,速度越慢。可以监视 "[-distort](https://imagemagick.org/command-line-options/#distort)"(或任何其他耗时图像处理任务)的进度,方法是使用 "[-monitor](https://imagemagick.org/command-line-options/#monitor)" 操作控制设置。基本上,对于一个靠近地平线的像素,ImageMagick 需要平均原始图像中的大量像素,才能确定合适的颜色。这可能会花很长时间。ImageMagick 会尝试通过缓存信息,并利用内置的各种虚拟像素设置知识,限制处理这些近地平线像素所花的时间,但它仍然可能很慢。关于这种方法的更多细节,请参见上面的区域重采样。还可以使用随机虚拟像素设置生成另一种无限平铺的透视图像…… |
magick checks.png -virtual-pixel random -mattecolor DodgerBlue \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
horizon_random.png
![[IM 输出]](../static/img/distorts/horizon_random.png)
这里发生的是,图像周围的所有虚拟像素都只是从图像本身内部随机挑选的任意像素。结果是一片由随机噪声组成的地面,越看向图像的地平线就越平滑、越模糊。它带来一种自然的深度感,而没有任何特定的重复图案。这里我重复了上面的操作,但使用纯黑白源图像。不过我感兴趣的不是实际扭曲后的图像,而只是生成的虚拟像素 'random' 图案,所以我用特殊的 '-set option:distort:viewport' 设置改变了自己查看“扭曲图像空间”的哪一部分。这个设置会覆盖被查看的扭曲空间区域的正常大小和位置。在本例中,该区域只包含虚拟像素,而不包含扭曲后的图像。 |
magick -size 90x90 pattern:gray50 -alpha set \
-virtual-pixel random -mattecolor none \
-set option:distort:viewport 120x120+100-15 \
-distort Perspective '0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
+repage -size 120x50 gradient:dodgerblue-tomato \
-compose DstOver -composite sunset_horizon.png
![[IM 输出]](../static/img/distorts/sunset_horizon.png)
为了完成图像,我移除了视口偏移(使用 "[+repage](https://imagemagick.org/command-line-options/#repage)"),并把一条夕阳色渐变衬在下面或 DstOver 到透明的“天空”中(通过 "[alpha setmattecolor](https://imagemagick.org/command-line-options/#mattecolor)" 设置)。这是一张很有意思的图像,可以作为其他图像处理工作的背景。可以调整扭曲参数来改变地平线的高度和斜率。下面是一个更传统的平铺透视扭曲测试。 |
magick pattern:checkerboard -scale 120x120 -normalize \
-virtual-pixel tile -distort Perspective \
'0,0 10,61 119,0 60,60 0,119 5,114 119,119 125,110' \
checkered_plain.gif
![[IM 输出]](../static/img/distorts/checkered_plain.gif)
在我的研究中,我发现上面的测试会造成误导,因为它并没有真正说明区域重采样技术在图像接近1倍缩放时(前景区域,而不是远处区域)的质量。也就是说,它更像是在近距离观察重采样伪影中描述的那类重采样问题。最后这张图还显示了靠近地平线的一个“截断”点,在那里 ImageMagick 认为(考虑当前虚拟像素设置后)不值得再尝试为某个像素确定适当颜色,而是短路 EWA 算法,使用整张图像的平均颜色。它之所以在这张图中可见,只是因为图像中存在大尺度的对角色彩图案。图像的平均颜色在每次扭曲操作中只计算一次,并且只在第一次需要时计算。借助它,ImageMagick 在计算靠近地平线的颜色时节省了大量时间,因为通常结果会是图像的平均颜色。当椭圆被拉得过长以至超过浮点限制,或者采样像素数(椭圆的边界平行四边形)变成输入源图像的4倍以上时,就会发生这种情况。当前用户不能设置这一行为。
3D 盒子,透视图层叠放
"+distort" 的 'plus' 形式会确保整个扭曲后的图像保存在位置正确的图层(或“虚拟画布”)中。它的设计目标是:如果用相同的“控制点”来扭曲图像,这些点会在“虚拟空间”中对齐。这意味着如果这些图像被图层合并到一起,它们也会按照控制点对齐。例如,这里我们生成两张图像,一张“正面”图像和一张“书脊”图像,让两条边上的控制点彼此对齐,从而形成盒子的书脊。
# Generate a Spine Image
magick -size 200x40 xc:skyblue \
-pointsize 20 -gravity north -annotate +5+0 'IM Examples' \
-pointsize 10 -gravity south -annotate +0+0 'ImageMagick' \
-stroke blue -strokewidth 2 -draw 'line 30,0 30,40' \
-rotate -90 box_spine.jpg
# generate the front cover
magick -size 150x200 xc:skyblue \
-fill black -pointsize 20 -gravity north -annotate +0+5 'IM Examples' \
-fill blue -pointsize 15 -gravity northeast -annotate +5+28 'Box Set' \
-fill black -pointsize 15 -gravity south -annotate +0+5 'ImageMagick' \
-stroke blue -strokewidth 2 -draw 'line 0,169 150,169' \
\( logo.gif -resize 100x100 \) \
-gravity center -compose multiply -composite box_front.jpg
# Distort both images and merge using common points.
magick \
\( box_spine.jpg -alpha set -virtual-pixel transparent \
+distort Perspective \
'0,0 -30,20 0,200 -30,179 40,200 0,200 40,0 0,0' \) \
\( box_front.jpg -alpha set -virtual-pixel transparent \
+distort Perspective \
'0,0 0,0 0,200 0,200 150,200 100,156 150,0 100,30' \) \
\
-background black -compose plus -layers merge +repage \
-bordercolor black -compose over -border 15x2 box_set.jpg
还要注意这里使用了 Plus Alpha 合成来连接“边缘相接”的部件。这是为了防止两张图像之间产生“半透明缝隙”。更多信息请参见上面的 3D 立方体示例,以及对齐两张带蒙版的图像。使用这样的定位意味着,几乎整个“书脊”图像实际上都被扭曲到了负的 'x' 位置。因此结果图像在虚拟画布上带有负偏移。使用这个运算符的图层版 "+distort" 时,IM 处理这种情况没有问题。图层合并运算符也被设计为能够处理带负偏移的图层图像,并把两张图像干净地“缝合”在一起。它们“合并”之后,我仍然需要最后使用一次 "+repage",从最终图像中移除那个负偏移。否则,Web 浏览器之类的其他程序可能无法理解这种负偏移,并产生未定义的效果。上面的示例也已放在 shell 脚本 "[box_set_example](../static/img/scripts/box_set_example) " 中,方便下载后试验。还可以进一步添加“盒子”在其所在表面上的镜像反射,不过为了让它更真实,可能还需要以某种方式重新着色或调暗该图像。关于这类镜像技术,请参见反射。
另一个 PHP 示例是在讨论如何把“照片”包到无边框画布框上时开发的。更多细节见画布包裹变换。
最后,这里有一个 Jean-François Hren 为 www.animecoversfan.com 制作的精彩示例,它曾在 IM 讨论论坛上被大量讨论。
![[图示]](../static/img/img_diagrams/video_box_example.png)
这张图像的制作方式是:取一张动漫视频盒封面的艺术图像,把封面分成3段('cover'、'spine' 和 'back'),分别扭曲成分层图像,再加入第四张 'disk' 图像并合并到一起。随后通过添加高光和阴影效果(使用 HardLight 图像合成),以及添加边框和半透明阴影效果(使用 CopyOpacity)完成图像。更令人惊叹的是,整个过程都是由输入图像开始,用一条 "magick" 命令完成的。它很好地展示了 IM 能做到什么,以及如何生成复杂命令脚本的过程。我建议阅读这个论坛讨论,其中包含许多提示、技巧和通用调试方法。(欢迎提供更多贡献示例)
透视投影扭曲
正如可以直接给出数学系数来处理 '[Affine](#affine)' 扭曲,也就是使用 '[Affine Projection](#affine_projection)' 一样,'[Perspective](#perspective)' 也可以通过 'Perspective Projection' 扭曲的8个系数来处理。和前面一样,这些数字表示用于把源图像中的点正向映射到目标图像的系数。也就是说,它们是把源图像 x,y 映射到目标图像 i,j 的数学值。8个浮点参数为(按给定顺序)…… sx, ry, tx,
rx, sy, ty,
px, py
这些系数值又组成下面的表达式…… Xd = | sx*Xs + ry*Ys + tx | , | Yd = | rx*Xs + sy*Ys + ty |
---|---|---|---|---|---
|
px*Xs + py*Ys + 1.0 | px*Xs + py*Ys + 1.0
其中 "Xs,Ys" 是源图像坐标,"Xd,Yd" 是目标图像坐标。在内部,ImageMagick Distort 会反转上述方程,以执行适当的反向像素映射,把 "Xd,Yd" 坐标映射为在源图像 "Xs,Ys" 处查找颜色。'Perspective Projection' 的前6个值实际上与 '[Affine Projection](#affine_projection)' 的系数相同,只是为了更合乎逻辑而稍作重排(用“矩阵数学”的说法,前6个元素做了对角转置)。额外的两个参数 px,py 会为整个扭曲形成一个缩放除数,使图像按照给定值在特定方向上看起来更小,从而给扭曲后的图像带来透视的“距离”效果。如果这两个值设为零,'Perspective Projection' 扭曲就等价于 'Affine Projection'。例如…… |
magick rose: -alpha set -virtual-pixel transparent \
-distort Perspective-Projection \
'1.40, 0.25, 3.0 0.15, 1.30, 0.0 0.007, 0.009' \
perspective_projection_rose.png
![[IM 输出]](../static/img/distorts/perspective_projection_rose.png)
请记住,你给出的矩阵是正向投影矩阵,它会把源图像坐标映射到目标图像坐标。在内部,ImageMagick 会反转该矩阵,以便把目标图像坐标映射到源图像坐标。如果想查看这些值,可以使用 Verbose 扭曲选项,让 IM 把其内部系数作为 FX 运算符表达式输出(见下一节)。
透视内部机制
如果在 Perspective 扭曲之前加上 "-verbose"(参见上面的详细扭曲概要),IM 会输出两个运算符,它们应当近似等价于 "-distort" 运算符的替代形式。一个是非常慢的 "-fx" 版本(参见 FX DIY 运算符)。另一个是正向映射的 Perspective_Projection 矩阵。例如…… |
magick rose: -alpha set -virtual-pixel transparent -verbose \
-distort Perspective "0,0,3,0 0,46,10,46 70,0,70,7 70,46,60,40" \
+verbose perspective_rose.png
第一部分 Perspective Projection 可用于把源坐标映射到目标坐标。公式如上所示。
i = ( 1.430099*x +0.246650*y +3 )/( 0.006757*x + 0.009448*y +1 )
j = ( 0.147296*x +1.434591*y +0 )/( 0.006757*x + 0.009448*y +1 )
在定位扭曲后的图层图像最后一组示例中,可以看到提取并使用这些值的例子。另一方面,第二个 FX 等价部分使用另一组不同的8个系数,执行图像扭曲实际需要应用的反向像素映射。也就是……
x = ( 0.711858*i -0.108326*j -2.135575 )/(-0.004119*i -0.005877*j +1 )
y = (-0.073090*i +0.699571*j +0.219269 )/(-0.004119*i -0.005877*j +1 )
注意,在输出的 FX equivelent 公式中,除数系数会先使用,因为它们同时被 X 和 Y 两个坐标方程共用。请记住,你给出的所有坐标都是图像坐标,而不是像素坐标,详见图像坐标与像素坐标。因此,在应用上述公式之前,任何像素位置都需要先给输入像素坐标加上 0.5,然后从最终坐标中减去 0.5,才能把它变回像素(绘制)坐标。可以在上面的 FX equivalent 代码中看到这一点。FX 等价式中、查找源图像之前的最后一个测试,用于处理无效的“天空”像素,也就是目标无法正确映射到源图像的位置。不过它只会为这些像素替换为 'blue',而不是 "-mattecolor",并且不提供透视扭曲内部算法提供的任何地平线抗锯齿。
透视正向映射示例…… 这些映射可以把一张图像中的特定坐标变换到另一张图像中的位置(任意方向)。例如,源玫瑰图像中心的一个暗点位于像素坐标 '39,20'。把它映射到图像坐标时,加上 ½ 得到 '39.5,20.5'。现在可以使用 x,y 到 i,j 的方程,把它映射到目标图像坐标 '44.2,24.1'。最后再减去 ½ 得到“绘制”像素坐标,最终位置为 '43.7,23.6'。这里我用圆在输入和输出图像上都标出了该坐标。
magick rose: -fill none -stroke black \
-draw 'circle 39,20 39,24' rose_marked.png
magick perspective_rose.png -fill none -stroke black \
-draw 'circle 43.7,23.6 43.7,26.6' perspective_rose_marked.png
可以看到,透视扭曲图像中的同一个点已在两张图像中正确定位(甚至精确到子像素级别)!
双线性扭曲
'Bilinear' 扭曲方法实现了另一种4点扭曲。不过它不像上面看到的 '[Perspective](#perspective)' 扭曲那样直接。但正如接下来会看到的,它是一种非常有用的替代扭曲。
正向双线性扭曲
例如,取一张叠加了网格的特殊山魈测试图像,并分别用透视和双线性来扭曲它。
magick mandrill_grid.jpg -alpha set -virtual-pixel black \
-distort Perspective \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_pers.jpg
magick mandrill_grid.jpg -alpha set -virtual-pixel black -interpolate Spline \
-distort BilinearForward \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_blin.jpg
![[IM 输出]](../static/img/img_photos/mandrill_grid.jpg)
原始图像 | | ![[IM 输出]](../static/img/distorts/mandrill_pers.jpg)
透视 | ![[IM 输出]](../static/img/distorts/mandrill_blin.jpg)
双线性
---|---|---|---
首先应注意,两种扭曲都正确地把图像从一组控制点映射到了另一组点。源图像中的所有水平线和垂直线,在两种扭曲中也都保持为直线。不过相似之处到此为止。透视会缩小线与线之间的间距,使对角线也保持直线。这样方格面积会变小,从而让右上角产生真实的“远处”效果。另一方面,双线性不会让图像的一侧看起来“更远”,也不会试图保持所有线为直线。它要做的是保持各条线之间的间距不变,但这会让对角线变成曲线。也就是说,它会保留任意给定线上的距离比例。换言之,即使整条线本身可能弯折、弯曲或整体缩短,每个线段的相对长度在线的全长上仍保持相同。这意味着上例中的网格间距在整个图像上保持同一比例,右上角扭曲后的方格仍然与左下角扭曲后的方格大小差不多。图像仍然看起来“平坦”,只是被扭曲成了另一种形状。请注意,(正向)双线性确实会保证原始图像中的任何水平线或垂直线在最终图像中仍保持直线。也就是说,它会把一个正交对齐的矩形变换为指定四边形,使原矩形的每条边在整条线上以恒定缩放保持直线。正是扭曲的这一点,使 'BilinearForward' 扭曲在更复杂的“网格”扭曲中非常有用。因为即使两个相邻“四边形”被非常不同地扭曲,它们仍会正确地边对边对齐。下面是 '[Perspective](#perspective)' 与 '[BilinearForward](#bilinear_forward)' 的另一组比较,使用内置 rose 图像进行非常强烈的扭曲……
magick rose: -alpha set -virtual-pixel transparent \
-distort Perspective "0,0,3,0 0,46,10,46 70,0,70,7 70,46,60,40" \
perspective_rose.png
magick rose: -alpha set -virtual-pixel transparent -interpolate Spline \
-distort BilinearForward "0,0,3,0 0,46,10,46 70,0,70,7 70,46,60,40" \
bilinear_rose.png
![[IM 输出]](../static/img/images/rose.png)
原始图像 | | ![[IM 输出]](../static/img/distorts/perspective_rose.png)
透视 | ![[IM 输出]](../static/img/distorts/bilinear_rose.png)
双线性
---|---|---|---
为了达到目标(保留所有直线),透视扭曲似乎把几乎整张图像都“吸”到了右侧较小的区域中,而双线性扭曲则让居中的玫瑰在结果中仍保持居中。它同样保留了距离比例,让玫瑰在左右边缘之间保持等距。它所做的只是沿图像长度方向线性地垂直压缩图像高度。'BilinearForward' 扭曲的这一特性也使它被称为“梯形”扭曲。也就是说,当只有一个方向被缩放时,它只是把图像沿一个方向线性压缩。这个压缩方向甚至可以倾斜,而不必沿某个轴对齐。 | _请注意,由于执行 'BilinearForward' 扭曲所需的反向像素映射很复杂,区域重采样当前被关闭。
因此,极端压缩区域(超过2倍)很可能会出现一些锯齿效应(见上面示例中线条边缘)。不过可以使用超采样或 '-interpolate Spline' 来改善最终图像质量。
---|---
| _在 IM v6.5.7-0 之前,'BilinearForward' 扭曲仍处于开发中,并且在特定“退化”情形下存在问题,某些情况下可能产生一张 'black' 错误图像。
---|---
反向双线性扭曲
由于只有水平线和垂直线会保持直线,因此不能使用 'BilinearForward 扭曲来反转该扭曲。因为变换后图像中的网格线不再是水平或垂直的,它们在结果图像中也就不会再保持直线!例如,交换坐标对并重新应用“正向”扭曲(就像上面对 '[Perspective](#perspective)' 扭曲所做的那样)将无法恢复原始图像。
magick mandrill_blin.jpg -alpha set -virtual-pixel black \
-distort BilinearForward \
'26,0 0,0 114,23 128,0 128,100 128,128 0,123 0,128' \
mandrill_blin_back.jpg
请注意,实际指定的坐标本身确实定位正确,但图像扭曲并没有被反转。总之,'[BilinearForward](#bilinear_forward)' 扭曲并不是它自身的逆变换。要恢复图像,需要使用一种略有不同但密切相关的扭曲。“几何变换”的数学逆变换已实现为 'BilinearReverse' 扭曲。例如……
magick mandrill_blin.jpg -alpha set -virtual-pixel black \
-distort BilinearReverse \
'26,0 0,0 114,23 128,0 128,100 128,128 0,123 0,128' \
mandrill_blin_rev.jpg
| 如前所述,由于 '[BilinearForward](#bilinear_forward)' 扭曲很复杂,区域重采样当前被关闭,这在上例中造成了严重的锯齿效应。
---|---
'[BilinearReverse](#bilinear_reverse)' 具有与 'BilinearFoward' 相同的距离比例保持特性,但会把任意四边形变换为一个正交对齐的矩形,确保四边形的边在映射到垂直和水平对齐时保持直线。正如上面所见。 | 在 IM v6.5.1-2 之前,'BilinearReverse' 扭曲只是实现为 'Bilinear'。
---|---
双线性扭曲的一些实现(包括较旧版本的 IM 和 Leptonica 库)只实现了上面这种更简单的(反向)双线性扭曲版本。不过,这种扭曲并不太适合对矩形图像做“正向映射”。例如,这里我尝试把 '[BilinearReverse](#bilinear_reverse)' 用在一个本来可能应该使用 '[BilinearForward](#bilinear_forward)' 扭曲的场景中。
magick mandrill_grid.jpg -alpha set -virtual-pixel black \
-distort BilinearReverse \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_blin_rev2.jpg
可以看到,由于目标四边形不是正交矩形,图像被严重扭曲,产生了大量向内弯曲的线条。
平铺双线性扭曲
虽然 '[BilinearReverse](#bilinear_reverse)' 会从矩形产生“弯曲”的图像,但这种效果确实会生成有趣的平铺图案,看起来像是产生了弯曲的三维表面。例如,应用上面观察远处的地平线中使用的同一变换,可以得到这个有趣的结果。 |
magick checks.png -virtual-pixel tile -mattecolor DodgerBlue \
-distort BilinearReverse \
'0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
bilinear_rev_tile.png
![[IM 输出]](../static/img/distorts/bilinear_rev_tile.png)
实际上,'[BilinearReverse](#bilinear_reverse)' 永远不会产生“地平线”(无效像素)。另一方面,使用 '[BilinearForward](#bilinear_forward)' 往往相当容易产生“天空”或“无效像素”(用当前 "[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)" 填充)。事实上,平铺图案往往会变得相当失控…… |
magick checks.png -virtual-pixel tile -mattecolor DodgerBlue \
-interpolate Spline -distort BilinearForward \
'0,0 20,60 90,0 70,63 0,90 5,83 90,90 85,88' \
bilinear_fwd_tile.png
![[IM 输出]](../static/img/distorts/bilinear_fwd_tile.png)
| 如前所述,由于 '[BilinearForward](#bilinear_forward)' 扭曲很复杂,区域重采样当前被关闭,这在上例中造成了严重的锯齿效应。
---|---
因此,我不建议使用平铺形式的 '[BilinearForward](#bilinear_forward)'。不过在使用正向扭曲时,我确实建议定义一个合适的 "[-mattecolor](https://imagemagick.org/command-line-options/#mattecolor)",以防出现意外的“天空”灰色块。
双线性内部机制
使用正向映射双线性扭曲将源图像中的坐标映射到目标图像的实际公式为…… Xd = |
C0*Xs + C1*Ys + C2*Xs*Ys + C3 |
, | Yd = |
C4*Xs + C5*Ys + C6*Xs*Ys + C7 |
|---|---|---|---|---|
| 不过,由于 IM 使用反向像素映射技术实现扭曲,上面的公式需要反转。这是一个复杂过程,需要求解二次方程、计算平方根,以及整整一页代数。如果要求 IM 详细输出 FX 等价式,就能看到这种复杂性。例如,使用前面创建的 checks 图像…… |
magick checks.png -alpha set -virtual-pixel transparent -mattecolor none \
-interpolate Spline -verbose -distort BilinearForward \
'0,0,0,0 0,90,0,90 90,0,60,30 90,90,90,90' \
+verbose bilinear_checks.png
最后一行 'FX equivalent ' 中的 '(rt > 0 ) ? red :' 检查,是为了避免无效的负平方根。这个检查会产生前面示例中看到的“天空”效果。另一方面,由于反向双线性扭曲简单得多,可以直接应用更简单的多项式方程来反转前面的扭曲…… |
magick bilinear_checks.png -virtual-pixel transparent \
-verbose -distort BilinearReverse \
'0,0,0,0 0,90,0,90 60,30,90,0 90,90,90,90' \
+verbose bilinear_checks_rev.png
| 可以看到,得到的方程非常简单,因为我们现在是在应用它,把目标坐标反向像素映射到源图像坐标。 | 上面看到的锯齿效应是由 '[BilinearForward](#bilinear_forward)' 引起的,而不是由 '[BilinearReverse](#bilinear_reverse)' 扭曲引起的。这是因为当前“正向”映射版本由于复杂性而关闭了区域重采样。 |
|---|---|
| 若想进一步阅读,我建议参阅 Leptonica 仿射和透视变换。 |
组合双线性扭曲
施工中
两种双线性扭曲方法结合使用,可以在保持四边形各边为直线的同时,把任意四边形直接扭曲成另一个任意四边形。基本上,可以先把一个四边形“Reverse”扭曲成矩形图像,再把该矩形“Forward”扭曲成最终四边形。这类扭曲也意味着,可以把任意矩形坐标网格扭曲到另一组矩形坐标网格。这称为“Grid”扭曲。该技术是图像变形的主要基础:在两张图像上定义一个矩形线网格,并用它们把图像合并成中间 composite,甚至生成一段动画,使一张图像正确变形为另一张图像。不过这尚未实现,虽然它是计划中的新增功能。
多项式扭曲(使用多项式拟合进行扭曲)
'Polynomial' 扭曲和前面大多数扭曲方法一样,也会映射控制点对,但它使用标准多项式方程。这意味着在给出控制点之前,还需要一个额外参数。
Order X1,Y1 I1,J1 X2,Y2 I2,J2 X3,Y3 I3,J3 X4,Y4 I4,J4 . . . .
'Order ' 参数通常是从 '1' 开始的整数,不过也可以使用特殊值 '1.5'。它定义将要应用的二维数学方程(同时使用 'x' 和 'y')的 'order',也就是复杂度。例如,order '1' 的多项式会拟合如下形式的方程…… Xd = | C2x*Xs + C1x*Ys + C0x | , | Yd = | C2y*Xs + C1y*Ys + C0y |
---|---|---|---|---|---
如果与仿射投影所用的方程比较,就会发现它们是等价的。由于每个 X 和 Y 公式都需要3个常数,因此还需要提供至少3组 X,Y 坐标对。更多坐标会让方程对给定坐标做最小二乘拟合。下一个 'order',也就是 '1.5',等价于 '[BilinearReverse](#bilinear_reverse)'(请记住,该方程用于把目标坐标映射到源图像)。 Xd = | C3x*Xs*Ys + C2x*Xs + C1x*Ys + C0x | , | Yd = | C3x*Xs*Ys + C2y*Xs + C1y*Ys + C0y |
---|---|---|---|---|---
与 '[BilinearReverse](#bilinear_reverse)' 扭曲一样,它至少需要4个坐标。例如……基本上,这与 order '1' 的方程完全相同,只是在多项式方程中增加了1个额外项。也就是说,由于现在每个方程在每个轴上都有4项、4个常数,因此至少需要4组坐标对,IM 才能确定这些常数。
magick mandrill_grid.jpg -alpha set -virtual-pixel black \
-distort Polynomial \
'1.5 0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_poly_1.5.jpg
当 order 为 '2' 时,多项式方程会进一步扩展为完整的二次拟合,至少需要6组坐标对。 Xd = |
C5x*Xs2 + C4x*Xs*Ys + C3x*Ys2 + C2x*Xs + C1x*Ys + C0x |
|---|---|
Yd = |
C5y*Xs2 + C4y*Xs*Ys + C3y*Ys2 + C2y*Xs + C1y*Ys + C0y |
基本上,这与 order '1' 的方程完全相同,只是在多项式方程前面加上3个额外项(order 2 + 1)。也就是说,由于现在每个方程有6项、6个常数,因此至少需要6个坐标,IM 才能确定这些常数。此后的每一个更高 order 多项式,都会给这一对方程中的每一个再增加 'order'+1 项。因此,order '3' 的三次拟合多项式至少需要10组坐标对才能完全定义,而 order '4' 的五次拟合多项式需要15组坐标对。可以使用详细扭曲概要查看多项式扭曲对指定坐标拟合出的结果方程。作为一个更大的例子,我有一张网格图像。还有一大组坐标(保存在文件 "[grid16_control_points.txt](../static/img/images/grid16_control_points.txt)" 中),说明我想如何扭曲该网格。随后我要求 IM 生成一个三次多项式,以“best-fit”输入坐标。 |
# warp image
magick grid16.png -virtual-pixel gray \
-distort polynomial "3 $(cat grid16_control_points.txt)" \
grid16_polynomial.png
# reverse image coordinate order
awk '{print $3, $4, $1, $2}' grid16_control_points.txt \
> grid16_cp_inverse.txt
# warp image back again
magick grid16_polynomial.png -virtual-pixel gray \
-distort polynomial "3 $(cat grid16_cp_inverse.txt)" \
grid16_restored.png
小的 "awk" 脚本会取得原始的 X,Y 控制点对集合并反转其顺序,这样我们就可以用新文件尝试“撤销”该扭曲。 | _控制点文件 "[grid16_control_points.txt](../static/img/images/grid16_control_points.txt)" 中的坐标是图像坐标,也就是说每个数字都指向其对应像素的中心。没有额外的 0.5 时,这些值就是整数的“像素坐标”。请参见上面的 图像坐标与像素坐标。
这些值完全是用图像查看器手工查找确定的,因此实际上并不十分精确。这可能是某些反向扭曲伪影的来源,不过多项式方程的功能性“最佳拟合”应该已经降低了整体扭曲影响。
---|---
这表明多项式扭曲虽然可用,而且效果不错,但它并不是精确或可逆的扭曲。本质上,81 个坐标会被一起“平均”,以生成输入坐标的数学“最佳拟合”。由于提供的控制点(81 个)多于所需的最小数量(10 个),所以不能保证任何控制点都与请求的坐标完全匹配。不过在这个具体示例中,坐标接近期望的扭曲结果,因此应该相当接近。多项式函数通常在图像边缘,尤其是角落处误差最大。这不仅影响像素位置,也影响边缘处的采样区域(EWA)。这是所用近似方法的自然结果。也可以使用更高阶的多项式,但在这里不会带来多大改进。对这个具体情况而言,多项式实际上是在尝试拟合一个非多项式的三角函数。由于这些函数的性质,第二次扭曲会比第一次更不准确。这个示例实际上与下面要看的径向 桶形扭曲 方法非常接近。不过要注意,被映射的坐标并不一定要排列成网格,也可以是任意一组坐标映射。因此,地理工作者常用它来把航拍照片与地球物理地图对齐(并叠加),以城镇、十字路口、山峰和其他地标的已知位置作为控制点。 | _由于多项式扭曲通常不可逆,IM 无法针对给定源图像计算目标图像视口的“最佳拟合”。因此,该运算符的 "[+distort](#distort_bestfit)" 形式不起作用,会退回为普通的 "[-distort](https://imagemagick.org/command-line-options/#distort)" 操作。不过你仍然可以使用扭曲视口选项来定义目标图像的视口。
---|---
圆形和径向扭曲方法
Arc 扭曲(将图像弯成圆弧)
'Arc' 扭曲(自 IM v6.3.5-5 起)是更复杂的极坐标扭曲(见下文)的一个简单变体。默认情况下,它会按给定角度把指定图像弯成一个完美的圆弧;如果没有其他参数,它会尽可能保留图像水平中心线的缩放以及图像的宽高比。为此它最多接受四个参数。
_arc_angle rotate_angle top_radius bottom_radius_
不过只有 "_arc_angle_" 是必需的,其他参数都是可选的,可以按给定顺序在需要时添加。例如,把一张图像按 60 度进行 “Arc” 处理…… |
magick rose: -virtual-pixel White -distort Arc 60 arc_rose.jpg
![[IM 输出]](../static/img/distorts/arc_rose.jpg)
| _请注意,与其他图像扭曲运算符不同,'Arc' 扭曲总是会设置结果图像的大小,使完整的源图像都包含在内。这包括任何抗锯齿边缘像素。因此,结果图像很少会与输入图像大小一致。
只有视口扭曲选项允许你为某个特定扭曲改变结果图像大小。_
---|---
添加第二个参数 "_rotate_agle_" 可以让图像绕圆旋转。例如旋转 90 度。 |
magick rose: -virtual-pixel White -distort Arc '60 90' arc_rose_rot.jpg
![[IM 输出]](../static/img/distorts/arc_rose_rot.jpg)
由于没有提到具体的半径参数,'Arc' 扭曲方法会尽力确保原始图像的缩放尽可能保持不变。为此,图像的水平中心线会被设为源图像宽度和给定 "_arc_angle_" 所对应的“理想半径”。这意味着如果你以更大的 "_arc_angle_" 弯曲图像,所用中心线的半径也会按同一比例缩小。因此中心线半径会变得更小、更紧。 |
magick rose: -virtual-pixel White -distort Arc 120 arc_rose_3.jpg
![[IM 输出]](../static/img/distorts/arc_rose_3.jpg)
注意图像现在会装进一个更小的圆,但图像底边仍然是一个更小的圆!如果把图像弯曲的角度设得更大,底边会触及扭曲中心并越过它,结果源图像的下半部分会消失得无影无踪。
magick rose: -virtual-pixel White -distort Arc 60 arc_rose_1.jpg
magick rose: -virtual-pixel White -distort Arc 90 arc_rose_2.jpg
magick rose: -virtual-pixel White -distort Arc 120 arc_rose_3.jpg
magick rose: -virtual-pixel White -distort Arc 180 arc_rose_4.jpg
magick rose: -virtual-pixel White -distort Arc 240 arc_rose_5.jpg
magick rose: -virtual-pixel White -distort Arc 300 arc_rose_6.jpg
magick rose: -virtual-pixel White -distort Arc 360 arc_rose_7.jpg
Arc 成完整圆环
较长的图像在很大角度上进行 'Arc' 扭曲时效果会好得多。例如,可以把较长的图像(如文本消息)包成圆环。为了让你真正看到这里发生了什么,我设置了不同的虚拟像素背景色,这样就能看见原始图像的边界。
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-distort Arc 60 arc_circle_1.jpg
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-distort Arc 120 arc_circle_2.jpg
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-distort Arc 180 arc_circle_3.jpg
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-distort Arc 270 arc_circle_4.jpg
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-distort Arc 360 arc_circle_5.jpg
就这样,我们已经把标签图像 'arc' 成了一个完整圆形。如果仔细看完整圆形图像的接合处,你可能会看到一小行像素,说明接合并不完全闭合。这是由周围的 'SkyBlue' 虚拟像素背景造成的,因为我们实际上是在接合图像的两条边。生成完整圆形时,需要使用一种能够正确“接合”这两条边的虚拟像素方法。通常会使用某种平铺型虚拟像素方法,例如 Tile。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Tile -background SkyBlue \
-distort Arc 360 arc_circle_tile.jpg
![[IM 输出]](../static/img/distorts/arc_circle_tile.jpg)
不幸的是,如你所见,这不仅会把图像正确接合起来,还会在主圆环内外生成重复的图像线条。效果不好。从 IM v6.4.2-6 起,新的虚拟像素方法 HorizontalTile 解决了这个问题。该方法只在横向平铺图像,因此能为圆形图像形成良好的接合,同时用当前背景色填充平铺块上方和下方的区域,生成完美的文字圆环。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel HorizontalTile -background SkyBlue \
-distort Arc 360 arc_circle.jpg
![[IM 输出]](../static/img/distorts/arc_circle.jpg)
如果在对图像执行 'arc' 之前先把输入图像旋转成上下颠倒,就可以把图像原来的“顶部”放在圆的内侧边缘。当然,之后你可能想把结果再“旋转”回正,但这个能力已经内置在 'Arc' 扭曲方法中了。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel Background -background SkyBlue \
-rotate 180 -distort Arc '270 180' arc_flip.jpg
![[IM 输出]](../static/img/distorts/arc_flip.jpg)
第三个参数 "_top_radius_" 会覆盖计算出的“理想”中心线半径,使图像顶部变成给定半径的圆。这样会生成一个宽 100 像素的圆环,不过为了容纳抗锯齿效果,承载图像的宽度是 102 像素。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel HorizontalTile -background SkyBlue \
-distort Arc '360 0 50' arc_radius.jpg
![[IM 输出]](../static/img/distorts/arc_radius.jpg)
图像仍保持相同的宽高比,因此上面的结果本质上与之前相同,只是缩放到适合请求半径的圆。请记住,半径可以是浮点数,但圆弧中心总会对齐到像素“角点”,所以结果图像的宽度仍然会是偶数像素。如果提供第四个 "_bottom_radius_" 参数,就可以完全控制圆环宽度,或称它的“径向高度”。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel HorizontalTile -background SkyBlue \
-distort Arc '360 0 45 30' arc_inner.jpg
![[IM 输出]](../static/img/distorts/arc_inner.jpg)
这会扭曲图像的径向缩放,并实际上把径向缩放与结果图像的“圆弧宽度”或角度分离开来。换句话说,原始图像的宽高比将不再保持。你甚至可以强制它完全填满圆的内部,把输入图像的底边包到扭曲中心,也就是“极点”处。 |
magick -font Candice -pointsize 20 label:' Around the World ' \
-virtual-pixel HorizontalTile -background SkyBlue \
-distort Arc '360 0 45 0' arc_fill.jpg
Arc 扭曲示例
可以用 Arc 扭曲生成有趣的效果,例如把一段较长的棋盘格图案弯入圆环(使用虚拟像素设置 '[HorizontalTile](misc.html#horizontal_tile)')会得到…… |
magick -size 210x30 pattern:checkerboard -alpha set \
-virtual-pixel HorizontalTile -background SkyBlue \
-distort Arc 360 arc_checks.png
![[IM 输出]](../static/img/distorts/arc_checks.png)
使用默认虚拟像素设置 '[Edge](misc.html#edge)' 可以产生更有趣的效果。 |
magick -size 210x30 pattern:checkerboard -virtual-pixel Edge \
-distort Arc 360 arc_checks_edge.png
![[IM 输出]](../static/img/distorts/arc_checks_edge.png)
当然,'[Tile](misc.html#tile)' 设置也会生成有趣的“径向”效果,让你生成圆形棋盘格图案。 |
magick -size 210x30 pattern:checkerboard -virtual-pixel Tile \
-distort Arc 360 arc_checks_tile.png
![[IM 输出]](../static/img/distorts/arc_checks_tile.png)
通过控制结果图像的顶部和底部半径,还可以进一步细化上面的效果。这里还有一些 '[Arc](#arc)' 扭曲示例,不过我让你自己试着弄清它们如何工作。你能做出什么? |
magick -size 90x1 pattern:gray50 -scale 900x100 -normalize \
-virtual-pixel Tile -set option:distort:viewport 100x100-50-50 \
-distort Arc 360 +repage arc_radii.gif
magick -size 400x100 pattern:hs_diagcross \
-virtual-pixel Tile -set option:distort:viewport 100x100-50-50 \
-distort Arc '360 0 80 0' +repage arc_cross.gif
magick -size 360x80 xc: -draw "fill none stroke black line 0,5 360,80" \
-virtual-pixel White -distort Arc '360 0 50 0' arc_spiral.gif
|
magick tree.gif -set option:distort:viewport 120x60-60-60 \
-virtual-pixel Dither +distort Arc '180 0 25 0' \
+repage arc_rays.gif
![[IM 输出]](../static/img/distorts/arc_rays.gif)
最后这个示例中的“光线”是伪随机 '[Dither](misc.html#dither)' 虚拟像素设置的副产物,结果来自原始图像左上角“太阳”颜色的奇特像素图案。同样的抖动效果还会生成围绕“树”图像的一圈“短划线”。如果使用 '[Edge](misc.html#edge)' 设置,并配合一张经过修改、加入了有趣边缘像素的图像,就能实现类似但更可控的效果。
Arc 中心点放置
默认情况下,'[Arc](#arc)' 会完全忽略图像可能具有的任何虚拟画布偏移,甚至不会报告图像被 arc 时围绕的“中心”位置。然而,知道“中心点”的位置非常有用。如果不用 "-distort",而是使用特殊的加号形式 "+distort",图像会被赋予一个虚拟画布,使中心位于虚拟画布原点。换句话说,图像的 '0,0' 点会被设为圆弧的“中心”。这对于定位小于完整圆的 arc 图像特别有用,因为此时圆弧“中心”并不是图像中心。例如…… |
magick logo: -resize x150 -gravity NorthEast -crop 100x100+10+0! \
\( -background none label:'IM Examples' \
-virtual-pixel Background +distort Arc '270 50 20' \
-repage +75+21\! \) -flatten arc_overlay.jpg
![[IM 输出]](../static/img/distorts/arc_overlay.jpg)
这里我创建了一个文本标签,并把它以 '[Arc](#arc)' 扭曲成一个不完整的圆,使用的是运算符的加号形式 "+distort"。IM 会利用图像的虚拟画布偏移小心地保留圆弧“中心”。这意味着只需用 "-repage" 和 '!' 标志对偏移做相对调整,就可以把生成的文字圆环放到任何想要的位置!例如上例中坐标为 75,21 的巫师帽尖。遗憾的是,由于使用虚拟偏移来定位图像,精确定位受限于整数像素大小。如果不再执行第二次扭曲,就无法把 Arc 扭曲定位到亚像素定义的位置。不过你可以对极坐标扭曲这样做(见下一节)。
Polar 扭曲(完整圆形扭曲)
'Polar' 扭曲(IM v6.4.2-6 加入)是上面 '[Arc](#arc)' 扭曲的更低层版本。但它不会自动执行“bestfit”,也不会尝试保持图像的宽高比。6 个可选浮点参数是…… Radius_Max Radius_Min Center_X,Center_Y Start_Angle,End_Angle
所有参数在对应位置上都是可选的。默认情况下,'CenterX,Y ' 会取输入图像区域的正中央。然后会生成一个完整圆形的极坐标图像,使整条顶边成为中心,而底边完全包到圆的外侧。左右边缘会在中心点上方相接,对应图像中的角度 '-180' 到 '+180'。由于必须给出 'Radius_Max ',它应该是某个正值。不过如果给出 '0',它会被设为中心到最近边缘的距离;因此如果未给出其他值(使用默认值),整个输入图像会被映射到图像中间的一个圆内。例如,让我们用所有默认值把世界地图 magick 成极坐标视图。当然,在生成完整圆形极坐标映射时,应指定虚拟像素设置 '[HorizontalTile](misc.html#horizontal_tile)'……
magick worldmap_sm.jpg -virtual-pixel HorizontalTile \
-background Black -distort Polar 0 polar_arctic.jpg
当然,这会严重扭曲南半球,把南极洲完全包到“圆盘世界”的圆周上。通过旋转源图像,并裁剪到只显示极冠,我们可以生成一张不错的南极大陆地图。我还指定了更大的输出半径,让它更容易看清,并使用扭曲运算符的“加号”形式,让 IM 将输出图像“适配”到这个大小。 |
magick worldmap_md.jpg -rotate 180 -crop 100%x25%+0+0 +repage \
-virtual-pixel HorizontalTile -background Black \
+distort Polar 80 +repage polar_antarctica.jpg
![[IM 输出]](../static/img/distorts/polar_antarctica.jpg)
请注意,上面的图并不是严格正确的地球视图,因为笛卡尔地图是球体的表示,并不是极坐标中的图像。如果使用一个精确为 '-1' 的特殊 'Radius_Max ' 值,扭曲图像的半径会被设为从中心到最远角(对角线)的距离。这是为了给接下来要看的完整图像 '[DePolar](#depolar)' 扭曲提供理想的“反向”操作。(使用示例见下面的 (De)Polar 技巧)。 | 请记住,与 '[Arc](#arc)' 扭曲不同,'[Polar](#polar)'(也称为“笛卡尔到极坐标”扭曲)不会尝试保持源图像的“理想”宽高比。使用时应谨慎。
---|---
'CenterX,Y ' 参数最有用的地方,是把结果图像的中心放到亚像素偏移处。也就是说,中心是在像素边界(整数)上,还是在像素中心(带 0.5 偏移)。当然,它也决定虚拟画布“图层”的位置。不过默认情况下,它会被赋为图像中间(对于使用输入图像作为视口的 "-distort")或 0,0(对于 "+distort" 图层图像)。接下来的参数 'Start_Angle,End_Angle ' 较少使用,用来限制输入图像覆盖的角度,默认值为 -180 到 180 度(0 为正下方)。和 '[Arc](#arc)' 扭曲一样,你可以用它旋转结果极坐标图像。但它也可用于生成“圆弧”。例如……
magick worldmap_sm.jpg -virtual-pixel Black -background Black \
+distort Polar '60,20 0,0 -60,60' +repage polar_arc.jpg
请注意,目前 IM 不会缩小结果图层图像的尺寸;它会按请求对齐,使虚拟图像的原点位于坐标 0,0。除了参数风格之外,这是 '[Arc](#arc)' 和 '[Polar](#polar)' 扭曲之间最大的区别。还要注意左边缘(角度 -60)在左侧。考虑到 'Y' 轴向下(与所有图像旋转相同),这在数学上是正确的。当然,和 Arc 一样,你可以使用虚拟像素平铺效果生成重复图案。例如,下面与上一个示例完全相同,只是使用了 '[HorizontalTileEdge](misc.html#horizontal_tile)' 设置…… |
magick worldmap_sm.jpg -virtual-pixel HorizontalTile -background Black \
+distort Polar '60,20 0,0 -60,60' +repage polar_arc_tiled.jpg
DePolar 扭曲(极坐标到笛卡尔)
这本质上是 '[Polar](#polar)' 扭曲的反向操作,并且拥有完全相同的一组选项参数。6 个可选浮点参数是……
Radius_Max Radius_Min Center_X,Center_Y Start_Angle,End_Angle
同样,如果把 'Radius_Max ' 设为 '0',就会使用 'CenterX,Y ' 到最近边缘的距离,这意味着最大完整圆内的所有内容都会被映射进一张与输入图像大小相同的图像中。例如,让我们把前面的“圆盘世界”反向变回笛卡尔地图。
magick polar_arctic.jpg -distort DePolar 0 world_restored.jpg
| 由于输入图像大小在两次扭曲过程中始终保持不变,上面的结果基本上与原始地图完全相同。当然,由于图像在顶部“极点”和半径方向都被压缩,输出会比你预期的模糊得多。 | 实际上情况更糟,因为区域重采样算法(EWA)无法沿圆弧采样像素。因此,区域重采样会针对 "[DePolar](#depolar)" 扭曲关闭。建议改用某种超采样技术,例如下一节所示。 |
|---|---|
如果允许 IM 使用“bestfit”(使用运算符的 "+distort" 形式),它会调整输出图像大小,使 'Radius_Max ' 保持单位缩放,并把宽度设为 'Radius_Max ' 和 'Radius_Min ' 中间半径的圆周距离。这本质上是在尽量保持极坐标图像的宽高比,不过可能会生成比预期更长、更细的图像。例如。 |
magick polar_arctic.jpg +distort DePolar 0 world_restored_2.jpg
(De)Polar 循环技巧(径向/角向模糊)
如上所见,将 'Radius_Max ' 设为 '0',会确保在使用 '[Polar](#polar)'(笛卡尔到极坐标)扭曲时,整个图像被映射到一个圆内;同样的设置会通过 '[DePolar](#depolar)'(极坐标到笛卡尔)把该圆映射回矩形图像。不过,如果你想先对矩形图像执行 '[DePolar](#depolar)',再用 '[Polar](#polar)' 反向恢复扭曲,这个设置效果并不好。例如,让我们取一张花朵图像,先 de-polar,再用特殊的 'Radius_Max ' 值 '0'(半径 = 最近边缘)恢复它。
magick flower_sm.jpg -virtual-pixel Black \
-distort DePolar 0 flower_depolar.jpg
magick flower_depolar.jpg \
-virtual-pixel HorizontalTile -background black \
-distort Polar 0 flower_circle.jpg
现在图像没有被正确恢复,因为第一次 '[DePolar](#depolar)' 扭曲把它裁掉了。即便如此,这本身也是一种有用技术,可用于为现有图像生成完美的圆形蒙版,而且尺寸可以完全独立于给定输入图像。要正确使用这个 '[DePolar](#depolar)'-'[Polar](#polar)' 循环技术,我们需要使用从中心到最远角的距离作为半径。特殊的 'Radius_Max ' 值 '-1' 会要求 IM 计算并使用离“中心点”最远的角作为半径。
magick flower_sm.jpg -virtual-pixel Black \
-distort DePolar -1 flower_depolar-1.jpg
magick flower_depolar-1.jpg \
-virtual-pixel HorizontalTile -background black \
-distort Polar -1 flower_restored.jpg
恢复后的图像略微模糊,这是因为在 '[DePolar](#depolar)' 操作期间,为保留整张图像而需要压缩半径。不过可以通过合适的超采样技术来修正(见下一组示例)。但为什么要把图像 magick 成这种形式再变回来呢?因为对中间的 'DePolar' 图像版本应用其他扭曲,就能很容易生成一些非常漂亮的径向或角向效果。例如,滚动中间图像会旋转输出图像,不过角落可能会有一些裁剪…… |
magick flower_sm.jpg -virtual-pixel Black -distort DePolar -1 \
-roll +15+0 \
-virtual-pixel HorizontalTile -background Black \
-distort Polar -1 flower_polar_rotate.jpg
Depolar-Polar 循环问题
在上面的图像旋转中,你可能已经注意到旋转后图像边缘有一些类似“阶梯”的扭曲。这是一个众所周知的问题,原因是图像的大圆周被压缩进输入图像较小的“宽度”中。例如,这里我取棋盘测试图像,不做任何修改,只让它通过普通的 Depolar-Polar 循环。
magick checks.png -virtual-pixel Transparent \
-distort DePolar -1 checks_depolar.png
magick checks_depolar.png -virtual-pixel HorizontalTile -background None \
-distort Polar -1 checks_cycled.png
可以清楚看到,中间图像的尖点处出现了由图像压缩造成的锯齿效应。这个问题还会因为最初转换输入图像时没有使用普通区域重采样,也就是那个 '[Depolar](#depolar)' 转换,而进一步加剧。解决这个问题的最佳方法是使用扭曲输出缩放,既放大中间图像,然后再缩小最终图像。这样会得到一个超采样结果,去除上面看到的压缩伪影。例如,下面是在一个命令中完成的更好的 “no-op” depolar-polar 循环…… |
magick checks.png -virtual-pixel Background -background None \
-set option:distort:scale 4 -distort DePolar -1 \
-noop \
-virtual-pixel HorizontalTile -background None \
-set option:distort:scale .25 -distort Polar -1 \
checks_cycled_ss.png
![[IM 输出]](../static/img/distorts/checks_cycled_ss.png)
如你所见,糟糕的锯齿效应几乎已经消失。不过要注意,非常高而窄的图像可能会让问题重新出现。最好的做法是把它限制在“横向”或较宽的图像上,并配合上面所示的超采样。现在你只需要把 "-noop" 运算符替换为合适的命令,以生成你想要的径向和旋转效果。
Depolar-Polar 效果示例
下面再次展示更好的图像 Polar 旋转,这次使用超采样。不过请注意,由于中间图像大了 4 倍,图像滚动的量也需要大 4 倍。 |
magick flower_sm.jpg -virtual-pixel Black \
-set option:distort:scale 4 -distort DePolar -1 \
-roll +60+0 \
-virtual-pixel HorizontalTile -background Black \
-set option:distort:scale .25 -distort Polar -1 \
flower_polar_rotate_ss.jpg
![[IM 输出]](../static/img/distorts/flower_polar_rotate_ss.jpg)
如你所见,边缘上的“阶梯”效果已被去除,得到的图像质量高得多。
或者,你可以对中间图像应用简单的线性模糊(例如通过压缩图像再重新放大来实现)。 |
magick flower_sm.jpg -virtual-pixel Black \
-set option:distort:scale 4 -distort DePolar -1 \
-scale 10%x100%\! -filter Gaussian -resize 1000%x100%\! +filter \
-virtual-pixel HorizontalTile -background Black \
-set option:distort:scale .25 -distort Polar -1 \
flower_angular_blur.jpg
![[IM 输出]](../static/img/distorts/flower_angular_blur.jpg)
结果非常类似于图像的“旋转模糊”。这与命名不太准确的径向模糊运算符相似,但并不完全相同。实际上,其结果质量高于那种专门的模糊方法。请注意,在所应用的各种虚拟像素设置形式中使用 'black' 颜色,会导致边缘稍微变暗,但在上面的情况中并不太糟。去除“black”边缘效果的一种方法,是改用 'transparency' 颜色,然后在完成时完全关闭 alpha 通道,只留下 IM 计算出的实际颜色。另一种方法是使用两个 'edge' 虚拟像素方法('[Edge](misc.html#edge)' 和 '[HorizontalTileEdge](misc.html#horizontal_edge)'),它们会把图像边缘延伸到未定义的虚拟画布空间中。 |
magick flower_sm.jpg -virtual-pixel Edge \
-set option:distort:scale 4 -distort DePolar -1 \
-scale 10%x100%\! -filter Gaussian -resize 1000%x100%\! +filter \
-virtual-pixel HorizontalTileEdge -background Black \
-set option:distort:scale .25 -distort Polar -1 \
flower_angular_blur_edge.jpg
![[IM 输出]](../static/img/distorts/flower_angular_blur_edge.jpg)
这显示边缘附近的结果好得多。
通过垂直模糊图像的极坐标版本,这次使用运动模糊运算符而不是调整尺寸造成的压缩,你可以生成从图像中心向外移动的 径向条纹…… |
magick flower_sm.jpg -virtual-pixel Black \
-set option:distort:scale 4 -distort DePolar -1 \
-virtual-pixel Edge -motion-blur 0x28-90 \
-virtual-pixel HorizontalTile -background Black \
-set option:distort:scale .25 -distort Polar -1 \
flower_radial_blur.jpg
![[IM 输出]](../static/img/distorts/flower_radial_blur.jpg)
为了让结果只模糊图像中的高光(花瓣),可以使用 Lighten 将它与原始图像合成,这样只有经过模糊的较亮颜色保持可见,较暗颜色不会模糊进较亮区域,也不会破坏花朵中央的黄色斑点。 |
magick flower_sm.jpg flower_radial_blur.jpg \
-compose Lighten -composite flower_radial_blur_lighten.jpg
![[IM 输出]](../static/img/distorts/flower_radial_blur_lighten.jpg)
另一个完成这类处理的例子可参见星星和彗星,不过那里是在应用 '[Polar](#polar)' 扭曲之前,直接生成中间的 'DePolar' 图像。特别感谢 Fred Weinhaus 对 DePolar-Polar 循环的特殊用法,以及他坚持要求我确保这些扭曲对矩形图像完全可逆。他在自己的许多 ImageMagick Scripts 中很好地运用了这一技术,包括 "[bump](http://www.fmwconcepts.com/imagemagick/bump/index.php)"、"[ripples](http://www.fmwconcepts.com/imagemagick/ripples/index.php)" 和 "[striations](http://www.fmwconcepts.com/imagemagick/striations/index.php)"。
Barrel 扭曲(校正镜头扭曲)
Barrel 扭曲(加入于 IM v6.4.2-4)是专门为校正照片中由相机镜头造成的球面扭曲而设计的。也就是桶形和枕形这类扭曲,它们实际上互为反向。关于将这个运算符实际用于镜头扭曲校正,请参见镜头校正一节。该扭曲基于一组 4 个系数值实现,称为 A、B、C 和 D,由 Helmut Dersch 教授在一个后来消失的网站中定义。你可以看到该网站保存在 Wayback Machine 的归档:Correcting Barrel Distortion。这些值基本上构成如下扭曲方程...
_R src_ = r * ( A*_r 3_ + B*_r 2_ + C*_r_ + D )
其中 "_r_" 是目标半径,"_R src_" 是要从中取得像素颜色的源像素。半径经过归一化,使输入图像的最小宽度或高度的一半对应 radius = '1.0'。这看起来也许像是反过来了,但这是因为使用了反向像素映射技术,以确保结果图像被完整覆盖。四个系数(A、B、C 和 D)对于任一特定的相机、镜头和变焦组合都是固定的。这三者通常会随图像一起存储在 EXIF 配置文件中。这一点很重要,因为它意味着一旦取得某台相机的这些值,就可以用它们去除该相机与镜头组合拍摄的所有照片中存在的球面镜头扭曲。'**Barrel**' 扭曲方法需要的参数如下。通常只提供 3 或 4 个值...
_A B C_ [ _D_ [ _X_ , _Y_ ] ]
可选的 X、Y 参数为径向扭曲提供一个可选的“中心”;否则默认使用给定图像的精确中心(不管它的虚拟偏移是多少)。这些系数被设计成:如果 A 到 D 的四个值相加为 '1.0',图像的最小宽度/高度就不会改变。因此,如果没有提供 D(它控制图像的整体缩放),会把它设置为让四个值合计为 '1.0'。使用参数 '0.0 0.0 0.0'(等价于 _A_ =_B_ =_C_ =0.0 且 _D_ =1.0)不会改变输入图像,是这个扭曲的 'no-op' 参数。下面是原网站中的一个例子,使用了拍摄这张照片的相机所提供的系数。
magick barrel_distorted.jpg -virtual-pixel black \
-distort Barrel "0.0 0.0 -0.075 1.1" \
barrel_distorted_fixed.jpg
注意图像中的扭曲如何被校正,使建筑物的柱子变直了。不过,由于 4 个系数相加后的值大于 1.0,图像被略微缩小,在上边缘和下边缘的中部产生了小块黑色区域(取决于给定的虚拟像素设置)。下面是给每个输入系数都加上 0.2 的效果;这些值同样相加后大于 1.0,因此得到的扭曲图像会更小。
magick checks.png -virtual-pixel gray \
-distort Barrel "0.2 0.0 0.0 1.0" barrel_checks_A.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 0.2 0.0 1.0" barrel_checks_B.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 0.0 0.2 1.0" barrel_checks_C.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 0.0 0.0 1.2" barrel_checks_D.png
减去 0.2 会产生相反的效果,不过我用较大的 'D ' 值(用来缩小图像)抵消了这种效果,这样能更清楚地看到结果。
magick checks.png -virtual-pixel gray \
-distort Barrel "-0.2 0.0 0.0 1.3" barrel_checks-A.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 -0.2 0.0 1.3" barrel_checks-B.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 0.0 -0.2 1.3" barrel_checks-C.png
magick checks.png -virtual-pixel gray \
-distort Barrel "0.0 0.0 0.0 1.3" barrel_checks-D.png
注意 A 的效果比 B 更大,B 的效果又比 C 更大,而 D 提供结果的整体缩放。这样就可以用各个系数调整图像:在外边缘校正一种扭曲,在中间附近校正另一种扭曲,可以一处是枕形、另一处是桶形。非常灵活。上面的系数(A、B、C 和 D)是为“归一化”半径而设计的,这个半径是图像最小宽度或高度的一半(类似极坐标扭曲的 '0' 半径设置)。也就是说,它们与图像尺寸无关。因此,对于某台特定相机生成的任意图像,无论它的质量尺寸(相机设置)如何,或图像是否被调整得更小,都可以使用同一组值。也可以对每个系数使用适当的乘数/除数,将系数值调整为使用其他“归一化”半径值,例如最大宽度/高度的一半,或对角线半径。 | _Helmut Dersch 还指出,照片扭曲校正应考虑使用 LAB 色彩空间,因为它会产生更好的颜色插值。这一点实际上可能适用于所有扭曲(包括Resize)。
测试表明,LAB 空间和 sRGB 一样是非线性的,但能避免极端值被裁剪时可能出现的颜色扭曲。请参见人类色彩感知,以及使用色彩空间校正调整尺寸中的实际示例。_
---|---
你也可以为 x 轴和 y 轴声明不同的一组系数,从而生成一些不同寻常的扭曲。
_A x Bx Cx Dx Ay By Cy Dy_ [ _X_ , _Y_ ]
分离的 X 和 Y 参数的用法最早在 Fred Weinhaus 的 pinbarrel 脚本中做过原型,不过他的参数顺序相反,D 在最前,A 在最后。只给 'y' 那组系数使用正的 C 值,并配合适当的 D 值,就可以把图像扭曲成中间向垂直方向鼓起。 |
magick rose: -alpha set -virtual-pixel transparent \
-distort Barrel "0.0 0.0 0.0 1.0 0.0 0.0 0.5 0.5" \
barrel_bulge.png
![[IM 输出]](../static/img/distorts/barrel_bulge.png)
类似地,使用负的 C 值可以在图像中间形成“捏紧”效果。 |
magick rose: -alpha set -virtual-pixel transparent \
-distort Barrel "0.0 0.0 0.0 1.0 0.0 0.0 -0.5 1.9" \
barrel_pinch.png
![[IM 输出]](../static/img/distorts/barrel_pinch.png)
或者给 X 系数添加相反的效果,让图像看起来像被手指挤压,两侧向外鼓起。 |
magick rose: -alpha set -virtual-pixel transparent \
-distort Barrel "0.0 0.0 0.5 0.5 0.0 0.0 -0.5 1.9" \
barrel_pinch_2.png
BarrelInverse 扭曲(另一种桶形扭曲)
'BarrelInverse' 扭曲方法与前面的 Barrel 扭曲 方法非常相似,实际上也接受同一组参数。不过应用的公式略有不同,方程的主体部分改为用半径相除。也就是说,这个方程被反转了。
_R src_ = r / ( A*_r 3_ + B*_r 2_ + C*_r_ + D )
| 这个方程不会产生 'Barrel' 扭曲的“反向”效果。不能用它来“撤销”前一种扭曲。
---|---
其结果是,你会使用 A、B、C 的“负”形式,并在 D 中做相应调整,以得到相似但略有不同的结果。一些来源,例如研究论文镜头扭曲校正方法(PDF),认为这种形式的镜头校正扭曲可以获得更好的结果。例如,下面是使用这种扭曲形式实现的、与上一个 'Pinch' 示例等价的结果。 |
magick rose: -alpha set -virtual-pixel transparent \
-distort BarrelInverse "0.0 0.0 -0.5 1.5 0.0 0.0 0.3 0.5" \
barrel_inv_pinch.png
投影扭曲
这些扭曲用于把存在于一个表面上的图像投影或映射到另一个表面上。投影线可以是平行的,也可以从某个特定位置放射出去。严格说来,Affine 和 Perspective 扭曲 也属于“投影”,因为它们把一个平面上的图像“投影”到另一个平面上(分别使用平行投影和径向投影),但它们太常见了,所以上面的专门示例区域已经更详细地介绍了它们。这里列出的是一些较少见的“投影扭曲”,通常是在 IM Discussion Forum 的帮助下实现的。
Cylinder 2 Plane
'Cylinder2Plane' 扭曲是一种径向投影扭曲,从圆柱中心的一个点投影到与该圆柱相切的平面上。
![[示意图]](../static/img/distorts/cylinder2plane.png)
这种布置常见于一种特殊的针孔相机,即 P.90 相机。这种相机会把 90 度弧面的照片记录在相机内部形成圆柱面的胶片上。下面是这类相机拍摄的一张示例照片...
![[照片]](../static/img/img_photos/p90_orig.jpg)
问题在于,由于胶片的物理布置,得到的图像会发生扭曲,直线会变成弯曲的弧线。本质上就是一个曲面围绕“点”投影源(针孔)包裹起来。注意,普通针孔相机没有这个问题,因为它是从点源投影到平面上。'Cylinder2Plane' 扭曲通过把图像从圆柱布置投影到平面上来修正这个问题。它接受以下参数...
_fov_angle center_x,y fov_output dest_center_x,y_
只有第一个参数,也就是相机视场角,是必需的。对于 P.90 相机,它使用 90mm(径向)焦距和标准 57mm 宽胶片,因此产生的“视场”为 90 / 57 * 180/pi,即 90.467 度。例如,下面我把一张 P90 照片投影到平面上,让图像看起来更“正常”,并让直线重新变直。
magick p90_orig.jpg -virtual-pixel Gray \
+distort Cylinder2Plane 90.467 p90_plane.png
注意,图像的宽度和高度都发生了变化,因为这里使用的是扭曲的“plus”版本,以显示原图中所有扭曲后的像素。由于投影,图像变宽了;而沿垂直中心线的实际图像高度没有变化。这张照片和算法来自 IM 讨论 Correcting for a curved film plane。该讨论也延伸到另一篇关于针孔相机的算法式暗角校正的讨论。
特殊的 'fov_output ' 如果给出,会缩放得到的输出图像,使输出图像的宽度(通常是视口尺寸)精确匹配这个角度。如果没有给出视口,则会启用最佳匹配,在仍然把图像边缘对齐到整数的同时,尽可能近似图像的 1:1 缩放。扭曲的 center-point 参数对应输入中的切点和地平点。最后一个“中心”参数控制结果在视口图像“图层”中的精确浮点定位(即亚像素平移)。它与 'center_x,y ' 参数一起,可以从较大的图像中提取部分。例如,从较大的 360 度全景图中提取一个小的 90 度视图。未来:从 360 全景中提取较小的平面“观看”图像,以及慢慢平移浏览这 360 度的动画。
Plane 2 Cylinder
'Plane2Cylinder' 扭曲是上面投影的反向形式,接受以下参数...
_fov_angle center_x,y_
例如,下面会撤销前面的 P.90 相机示例。
magick p90_plane.png -virtual-pixel Black \
+distort Plane2Cylinder 90.467 p90_restored.png
结果仍然包含之前添加的多余像素,而且还会再添加更多。这些像素应当从上面的结果中裁剪掉。这里我用这个扭曲生成一段“胶片条”动画,并带有完整的胶片齿孔边缘。
magick -size 12x12 xc: -draw 'circle 6,6 6,2' -negate \
-duplicate 5 +append +duplicate \
rose: +swap -background black -append \
-duplicate 3 +append \
-virtual-pixel HorizontalTile -background SkyBlue \
-duplicate 19 -distort SRT '%[fx:72*t/n],0 1 0 0,0' \
-distort Plane2cylinder 115 \
-bordercolor Skyblue -border 0x3 -set delay 5 \
film_strip_anim.gif
说明:
- 首先绘制一个孔洞图像,将它复制并追加成 6 个孔洞的序列。然后再复制一次。
- 接着加入内置的 rose 图像,把它夹在这两份齿孔之间,并一起追加(用黑色填充),生成最终胶片条中的单帧。
- 将这一帧复制,生成由 4 帧组成的胶片条,用来定义最终图像的长度。
- 使用 SRT “平移”扭曲创建 20 帧动画。参见扭曲动画。
- 然后把这 20 帧中的每一帧扭曲到一段 115 度弧的圆柱上,并使用虚拟像素设置生成胶片条的无限水平平铺
- 这里的角度只针对输入图像的宽度;最终图像的宽度超过 180 度,因为我没有使用扭曲的“plus”版本。因此,虽然扭曲后的宽度在宽度方向上缩小了,输出图像本身并没有缩小。
- 最后添加边框,并应用动画设置。
多点与自由形态扭曲
Shepard's 扭曲(类似太妃糖的扭曲)
Shepard 方法(加入于 IM v6.4.2-4)利用给定控制点的移动,以“局部”效果的方式扭曲图像。你可以把它想象成:源图像是一块厚厚的“太妃糖”,把图钉钉进去,然后移动这些图钉。更技术地说,它按照反平方距离插值来移动点。如果只使用一个控制点,那么自然会移动(平移)整张图像,就像一个点的 '[Affine](#affine)' 扭曲那样。不是很有意思。所以让我们试着移动两个控制点。例如,拉扯 'koala' 的耳朵(位于 '30,11' 和 '48,29')来折腾一下它...
magick koala.gif -virtual-pixel Black \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull.png
可以看到,由于控制点移动,两个控制点之间的图像部分被拉伸了。不过图像的其他部分基本保持不变,包括控制点附近的图像、图像底部等等。可能不太明显的是,控制点远侧的部分也被压缩了,因此离得越远,控制点对结果的影响就越小。也就是说,这种扭曲会生成“局部化”的扭曲。让我们扩大视图(使用扭曲视口),以便看得更清楚... |
magick koala.gif -virtual-pixel Black \
-set option:distort:viewport 115x115-20-20 \
-distort Shepards '30,11 15,11 48,29 58,29' \
+repage koala_ear_pull_2.png
![[IM 输出]](../static/img/distorts/koala_ear_pull_2.png)
可以看到,为了容纳被拉伸的 koala “头部”,图像的形状也发生了扭曲。为了避免这种效果,更常见的做法是同时“钉住”图像的四角,可能还会钉住一些边缘,让它们不要移动。 |
magick koala.gif -virtual-pixel Black \
-set option:distort:viewport 115x115-20-20 \
-distort Shepards '30,11 15,11 48,29 58,29
0,0 0,0 0,74 0,74 74,0 74,0 74,74 74,74' \
+repage koala_ear_pull_3.png
![[IM 输出]](../static/img/distorts/koala_ear_pull_3.png)
即使只移动一个点,同时钉住其他点(这里仅钉住四角),也可能很有用。例如,把 koala 的鼻子(位于 '28,24')移动到图像中央。 |
magick koala.gif -virtual-pixel Black \
-distort Shepards '28,24 37,37
0,0 0,0 0,74 0,74 74,0 74,0 74,74 74,74' \
+repage koala_move_nose.png
![[IM 输出]](../static/img/distorts/koala_move_nose.png)
这个具体示例比较特殊,因为 Fred Weinhaus 在他的单点“动画变形”脚本 "[shapemorph](http://www.fmwconcepts.com/imagemagick/shapemorph/)" 中使用的就是这种扭曲。不过他的原始脚本使用了较慢的 DIY FX 运算符,因为当时 Shepards 扭曲 还没有加入 IM。这个脚本实际上也是 Shepards 扭曲 最初想法的来源。
移动图像中的区域
你甚至可以通过一起移动某个区域周围的一组点,来移动图像中的整个区域。例如,使用头部周围的点(红线)把 koala 的头向侧面移动,同时也钉住我们不想移动的图像部分(绿线)。
magick koala.gif -virtual-pixel Black -distort Shepards \
'19,8, 29,8 19,27 29,27 26,34 36,34
33,37 43,37 36,37 46,37 53,37 63,37 58,25 68,25
13,20 13,20 17,28 17,28 25,36 25,36
35,39 35,39 46,40 46,40 50,43 50,43 ' \
+repage koala_head_move.png
注意,虽然头部被移动了,但头部边缘确实发生了严重扭曲。原因是该扭曲移动的是点,而不是区域。如果这些标记边缘的点相距太远,图像就会在这些点之间有点像太妃糖或果冻一样下垂、渗开或弯曲。(旁注:实际术语是拉伸“橡胶膜”或“气球”)。另外,如果两个控制点彼此很近,但移动方向或移动量不同,图像可能会在它们周围局部旋转和弯折。控制点最终仍会落在正确位置,但为了达到这个目标,它们周围的其他所有内容都会被严重扭曲。头部边缘发生的正是这种情况。那么边缘标记点应该多密?基本上,至少应是到任何其他以不同方式移动的点的距离的一半。因此,要么添加更多边缘点,要么在固定点和移动点之间留出一些额外距离。这样可以更好地定义图像可以被拉伸和压缩的空间。还要注意,整张图像总体上也跟着头部向左移动了。只有被固定或移动到特定目标位置的控制点,才保证会被正确放置。离所有控制点都较远的图像部分,也会根据所有控制点移动的大致平均值而移动。因此,更好的做法是在图像各处布置更多“固定”点,甚至在图像外侧一定距离处放置一些反向移动的点,以抵消总体平均移动。也可以复制或加倍控制点(列出两次),让特定点对该区域的扭曲有更大的影响或“力量”。下面是“向侧面移动头部”的另一个版本,不过这次我在移动点(红)和固定点(绿)之间给了更多分隔。我还添加了更多固定点,以减少扭曲图像的平均整体移动。
magick koala.gif -virtual-pixel Black -distort Shepards \
'15,15, 25,15 19,27 29,27 26,34 36,34
33,37 43,37 36,37 46,37 53,37 63,37
10,2 10,2 2,10 2,10 4,55 4,55 14,47 14,47
25,47 25,47 45,51 45,51 55,45 55,45
5,70 5,70 15,60 15,60 55,60 55,60 70,70 70,70' \
+repage koala_head_move_2.png
上面最后要做的,只是设置一个更合适的 "[-virtual-pixel](https://imagemagick.org/command-line-options/#virtual-pixel)" 设置,用来指定上方未定义的黑色区域应是什么颜色。
Shepards 与图像旋转
这种扭曲的一个特点是,它不喜欢任何形式的旋转!例如,下面重复了 Perspective 和 BilinearForward 的例子,并把它们与 Shepards 扭曲 放在一起比较。
magick mandrill_grid.jpg -alpha set -virtual-pixel black \
-distort Perspective \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_pers.jpg
magick mandrill_grid.jpg -alpha set -virtual-pixel black -interpolate Spline \
-distort BilinearForward \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_blin.jpg
magick mandrill_grid.jpg -alpha set -virtual-pixel black -interpolate Spline \
-distort Shepards \
'0,0 26,0 128,0 114,23 128,128 128,100 0,128 0,123' \
mandrill_shep.jpg
![[IM 输出]](../static/img/img_photos/mandrill_grid.jpg)
原图 | | ![[IM 输出]](../static/img/distorts/mandrill_pers.jpg)
Perspective | ![[IM 输出]](../static/img/distorts/mandrill_blin.jpg)
Bilinear | ![[IM 输出]](../static/img/distorts/mandrill_shep.jpg)
Shepards
---|---|---|---|---
注意,与另外两种扭曲方法相比,Shepards 扭曲 生成的图像非常弯曲。这是因为它试图在靠近给定控制点的区域精确保留图像,其中也包括图像的旋转。由于这种“保留”,网格会发生弯曲,使它在实际控制点处仍保持“正交”。这有点像控制点处的那些“钉子”并不是真正的圆钉,而是“十字”,迫使承载图像的“果冻”或“橡胶膜”也保留图像的旋转。这也是这种扭曲能够产生许多“旋涡”效果的来源。例如,如果取图像中的两个点,并把它们互相推过对方,图像会旋涡化,而不是旋转。比如我们试着把 koala 的耳朵彼此推近,而不是拉开。
magick koala.gif -virtual-pixel Black \
-distort Shepards '30,11 40,11 48,29 38,29' \
koala_ear_push.png
Shepard's power 因子
通常,shepard 的 IWD(Inverse Weighted Distance,反加权距离)的距离权重遵循反平方律(1/r2),不过从 IM v6.8.0-10 开始,可以使用专家级 define 'shepards:power' 来控制全局权重的“power-level”。如果未定义,它的值为 2.0;定义得更小,就能让移动控制点周围相对于扭曲图像整体平均位移产生更局部化的扭曲。使用更大的值会在控制点周围生成更大的影响区域。例如,下面重复“拉 koala 耳朵”的例子,并对扭曲权重应用不同的 power 级别。
magick koala.gif -virtual-pixel Black -define shepards:power=0.5 \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull_pow0.5.png
magick koala.gif -virtual-pixel Black -define shepards:power=1.0 \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull_pow1.png
magick koala.gif -virtual-pixel Black \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull.png
magick koala.gif -virtual-pixel Black -define shepards:power=3.0 \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull_pow3.png
magick koala.gif -virtual-pixel Black -define shepards:power=8.0 \
-distort Shepards '30,11 20,11 48,29 58,29' \
koala_ear_pull_pow8.png
![[IM 输出]](../static/img/distorts/koala_ears.png)
原图
和动作 | | ![[IM 输出]](../static/img/distorts/koala_ear_pull_pow0.5.png)
power 0.5 | ![[IM 输出]](../static/img/distorts/koala_ear_pull_pow1.png)
power 1.0 | ![[IM 输出]](../static/img/distorts/koala_ear_pull.png)
power 2.0
(默认) | ![[IM 输出]](../static/img/distorts/koala_ear_pull_pow3.png)
power 3.0 | ![[IM 输出]](../static/img/distorts/koala_ear_pull_pow8.png)
power 8.0
---|---|---|---|---|---|---
上面所有图像结果都使用完全相同的一组控制点移动。唯一的区别是这些控制点周围的影响区域。较小的 power 会把移动“局部化”到非常接近控制点的区域,而较大的 power 会把控制点周围更多的图像一起拖动。在非常大的 power 下,这种拉动往往会沿着控制点之间的中线把图像“撕裂”为独立区域。你甚至可以使用大得多的 power,它会简单地把源控制点周围的区域平移到目标控制点周围的区域。这些区域会形成“Voronoi 区域”的图案,并且可能包含源图像的重复副本。例如,这里我把 koala 鼻子周围的区域(坐标 28,24)映射到六边形图案中的 7 个不同区域,以非常高效的方式产生类似“昆虫眼睛”的效果。 |
magick koala.gif -virtual-pixel Black -define shepards:power=25 \
-distort Shepards '28,24 35,35 \
28,24 20,10 28,24 50,10 \
28,24 20,60 28,24 50,60 \
28,24 10,35 28,24 60,35' koala_hexagonal.png
![[IM 输出]](../static/img/distorts/koala_hexagonal.png)
请记住,Shepards 扭曲 实际上等价于使用与 Shepards, Sparse Color 运算符 相同的技术生成的位移贴图(把目标像素映射到源图像),而它同样会受到这个 power 因子 define 的影响。
Shepard's 扭曲,总结
| Shepards 扭曲 是一种非常灵活的自由形态方法,会把扭曲限制在由给定点的移动或不移动所标记的区域。它的扭曲会根据相邻控制点之间的距离局部化并受到限制,不过所有点仍然会产生平均化的全局影响。只要记住,这种扭曲由点驱动,而不是由线或区域驱动,因此当移动方式不同的控制点放得太近时,点之间的部分可能会意外鼓起或旋涡化。它会让控制点之间的图像旋涡、拉伸和压缩,但会努力避免在控制点附近旋转或缩放图像。最后,它还可能在远离任何控制点的地方产生图像的整体平均平移。不过,如果一组控制点可以在保持其大致相对位置的同时一起移动,它确实提供了一种实现通用而非常简单的点驱动“Image Morphing”技术的方法。Fred Weinhaus 的脚本 'shapemorph2' 使用 Shepards 扭曲 提供了一个很好的通用“Animated Image Morphing”程序。 | 在内部,这种扭曲等价于使用Shepards Sparse Color渐变生成器创建两个相对位移贴图来扭曲图像。这也是 Fred Weinhaus 原始 "[shapemorph](http://www.fmwconcepts.com/imagemagick/shapemorph/)" 脚本所做的事情,并且是这种扭曲技术的来源。 |
|---|---|
由于使用Shepards 扭曲时所需计算很复杂,IM 不提供使用运算符的 plus "[+distort](#distort_bestfit)" 形式得到的任何“best-fit”目标视口。不过你仍然可以使用扭曲视口选项定义更大的输出图像。 |
|
| --- | --- |
| 出于同样的原因,区域重采样被关闭。因此,极端压缩区域(超过 2 倍)很可能会显示出一些混叠效果。例如,请看最后一个示例中六边形图案的边缘。不过可以使用超采样来提高结果的最终图像质量,并减少这类混叠效果。 | |
| --- | --- |
![[IM 输出]](../static/img/distorts/implode_tiled_box.gif)
![[IM 输出]](../static/img/distorts/implode_tiled_ss.gif)
![[IM 输出]](../static/img/distorts/koala_noop.gif)
![[IM 输出]](../static/img/distorts/koala_viewport_4.gif)
![[IM 输出]](../static/img/img_photos/worldmap_sm.jpg)
![[IM 输出]](../static/img/distorts/viewport_square.gif)
![[IM 输出]](../static/img/distorts/spaceship.gif)
![[IM 输出]](../static/img/distorts/spaceship_launch.gif)
![[IM 输出]](../static/img/distorts/rotate_normal.png)
![[IM 输出]](../static/img/distorts/koala_arrow.png)
![[IM 输出]](../static/img/distorts/koala_one_point.png)
![[IM 输出]](../static/img/distorts/label_axis.png)
![[IM 输出]](../static/img/distorts/label_axis_distort_shear.png)
![[IM 输出]](../static/img/distorts/label_axis_distort_rotate.png)
![[IM 输出]](../static/img/distorts/label_axis_distort_affine.png)
![[IM 输出]](../static/img/distorts/checks_srt_tile.png)
![[IM 输出]](../static/img/distorts/checks_affine_tile.png)
![[IM 输出]](../static/img/distorts/checks_amatrix_tile.png)
![[IM 输出]](../static/img/images/glitter_blue.gif)
![[IM 输出]](../static/img/distorts/glitter_blue_tiled.gif)
![[IM 输出]](../static/img/img_photos/building.jpg)
![[IM 输出]](../static/img/distorts/building_before.jpg)
![[IM 输出]](../static/img/distorts/building_after.jpg)
![[IM 输出]](../static/img/distorts/building_pers.png)
![[IM 输出]](../static/img/distorts/building_pers_rev.png)
![[IM 输出]](../static/img/distorts/checks_pers.png)
![[IM 输出]](../static/img/distorts/checks_pers_rev.png)
![[IM 输出]](../static/img/distorts/box_spine.jpg)
![[IM 输出]](../static/img/distorts/box_front.jpg)
![[IM 输出]](../static/img/distorts/box_set.jpg)
![[IM 文本]](../static/img/distorts/perspective_verbose.txt.gif)
![[IM 输出]](../static/img/distorts/rose_marked.png)
![[IM 输出]](../static/img/distorts/perspective_rose_marked.png)
![[IM 输出]](../static/img/distorts/mandrill_blin_back.jpg)
![[IM 输出]](../static/img/distorts/mandrill_blin_rev.jpg)
![[IM 输出]](../static/img/distorts/mandrill_blin_rev2.jpg)
![[IM 输出]](../static/img/distorts/bilinear_checks.png)
![[IM 文本]](../static/img/distorts/bilinear_verbose.txt.gif)
![[IM 输出]](../static/img/distorts/bilinear_checks_rev.png)
![[IM 文本]](../static/img/distorts/bilinear_rev_verbose.txt.gif)
![[IM 输出]](../static/img/distorts/mandrill_poly_1.5.jpg)
![[IM 输出]](../static/img/images/grid16.png)
![[IM 输出]](../static/img/distorts/grid16_polynomial.png)
![[IM 输出]](../static/img/distorts/grid16_restored.png)
![[IM 输出]](../static/img/distorts/arc_rose_1.jpg)
![[IM 输出]](../static/img/distorts/arc_rose_2.jpg)
![[IM 输出]](../static/img/distorts/arc_rose_4.jpg)
![[IM 输出]](../static/img/distorts/arc_rose_5.jpg)
![[IM 输出]](../static/img/distorts/arc_rose_6.jpg)
![[IM 输出]](../static/img/distorts/arc_rose_7.jpg)
![[IM 输出]](../static/img/distorts/arc_circle_1.jpg)
![[IM 输出]](../static/img/distorts/arc_circle_2.jpg)
![[IM 输出]](../static/img/distorts/arc_circle_3.jpg)
![[IM 输出]](../static/img/distorts/arc_circle_4.jpg)
![[IM 输出]](../static/img/distorts/arc_circle_5.jpg)
![[IM 输出]](../static/img/distorts/arc_fill.jpg)
![[IM 输出]](../static/img/distorts/arc_radii.gif)
![[IM 输出]](../static/img/distorts/arc_cross.gif)
![[IM 输出]](../static/img/distorts/arc_spiral.gif)
![[IM 输出]](../static/img/distorts/polar_arctic.jpg)
![[IM 输出]](../static/img/distorts/polar_arc.jpg)
![[IM 输出]](../static/img/distorts/polar_arc_tiled.jpg)
![[IM 输出]](../static/img/distorts/world_restored.jpg)
![[IM 输出]](../static/img/distorts/world_restored_2.jpg)
![[IM 输出]](../static/img/distorts/flower_sm.jpg)
![[IM 输出]](../static/img/distorts/flower_depolar.jpg)
![[IM 输出]](../static/img/distorts/flower_circle.jpg)
![[IM 输出]](../static/img/distorts/flower_depolar-1.jpg)
![[IM 输出]](../static/img/distorts/flower_restored.jpg)
![[IM 输出]](../static/img/distorts/flower_polar_rotate.jpg)
![[IM 输出]](../static/img/distorts/checks_depolar.png)
![[IM 输出]](../static/img/distorts/checks_cycled.png)
![[IM 输出]](../static/img/img_photos/barrel_distorted.jpg)
![[IM 输出]](../static/img/distorts/barrel_distorted_fixed.jpg)
![[IM 输出]](../static/img/distorts/barrel_checks_A.png)
![[IM 输出]](../static/img/distorts/barrel_checks_B.png)
![[IM 输出]](../static/img/distorts/barrel_checks_C.png)
![[IM 输出]](../static/img/distorts/barrel_checks_D.png)
![[IM 输出]](../static/img/distorts/barrel_checks-A.png)
![[IM 输出]](../static/img/distorts/barrel_checks-B.png)
![[IM 输出]](../static/img/distorts/barrel_checks-C.png)
![[IM 输出]](../static/img/distorts/barrel_checks-D.png)
![[IM 输出]](../static/img/distorts/barrel_pinch_2.png)
![[IM 输出]](../static/img/distorts/barrel_inv_pinch.png)
![[IM 输出]](../static/img/distorts/p90_plane.png)
![[IM 输出]](../static/img/distorts/p90_restored.png)
![[IM 输出]](../static/img/distorts/film_strip_anim.gif)
![[IM 输出]](../static/img/distorts/koala_head.png)
![[IM 输出]](../static/img/distorts/koala_head_move.png)
![[IM 输出]](../static/img/distorts/koala_head_2.png)
![[IM 输出]](../static/img/distorts/koala_head_move_2.png)
![[IM 输出]](../static/img/distorts/koala_push.png)
![[IM 输出]](../static/img/distorts/koala_ear_push.png)