像素缓存 • 图像属性与配置文件 • 多光谱图像 • 大图像支持 • 像素流式处理 • 执行线程 • 启用动态节流以优化性能 • 异构分布式处理 • 自定义图像编解码器 • 自定义图像滤镜
奥兹国的居民对其恩人——全能的魔法师——相当满足。他们接受他的智慧与仁慈,从不追问其力量的谁、为何与何处。像奥兹国的居民一样,如果你满足于 ImageMagick 能帮你转换、编辑或合成图像,而不必知道幕后发生了什么,尽可以跳过本节。然而,如果你想更多地了解 ImageMagick 背后的软件与算法,请继续阅读。要充分受益于本讨论,你应当熟悉图像术语,并对计算机编程有所了解。
架构概述
图像通常由一个矩形的像素区域和元数据组成。要高效地转换、编辑或合成图像,我们需要便捷地访问区域内(有时是区域外)任意位置的任意像素。而在图像序列的情形下,我们需要访问序列中任意图像、任意区域的任意像素。然而,存在数百种图像格式,如 JPEG、TIFF、PNG、GIF 等,这使得按需访问像素变得困难。在这些格式中我们会发现以下差异:
- 色彩空间(例如 sRGB、线性 RGB、线性 GRAY、CMYK、YUV、Lab 等)
- 位深度(例如 1、4、8、12、16 等)
- 存储格式(例如无符号、有符号、float、double 等)
- 压缩(例如未压缩、RLE、Zip、BZip 等)
- 朝向(即从上到下、从右到左 等)
- 布局(例如 raw、与操作码交错 等)
此外,某些图像像素可能需要衰减,某些格式允许多于一帧,而某些格式包含必须先经过栅格化(从矢量转换为像素)的矢量图形。
图像处理算法的高效实现可能要求我们获取或设置:
- 一次一个像素(例如位置 10,3 处的像素)
- 单条扫描线(例如第 4 行的所有像素)
- 一次几条扫描线(例如第 4-7 行的像素)
- 单列或多列像素(例如第 11 列的所有像素)
- 图像中任意的像素区域(例如定义于 10,7 至 10,19 的像素)
- 随机顺序的像素(例如 14,15 和 640,480 处的像素)
- 来自两幅不同图像的像素(例如图像 1 的 5,1 与图像 2 的 5,1)
- 图像边界外的像素(例如 -1,-3 处的像素)
- 无符号(65311)或浮点表示(例如 0.17836)的像素分量
- 可包含负值(例如 -0.0072973525628)以及超出量子深度的值(例如 65931)的高动态范围像素
- 在不同执行线程中同时处理一个或多个像素
- 内存中的所有像素,以利用在由 CPU、GPU 及其他处理器组成的异构平台上协同执行所带来的加速
- 与每个通道关联的特征(trait),用于指定该像素通道是被复制、更新还是混合
- 定义哪些像素有资格被更新的掩码
- 对用户有益但在其他方面不受 ImageMagick 图像处理算法影响的额外通道
鉴于多样的图像格式与图像处理需求,我们实现了 ImageMagick 像素缓存,以便对图像区域内(即真实像素)以及序列中任意图像的任意位置的任意像素提供便捷的顺序或并行的按需访问。此外,像素缓存还允许访问图像所定义边界之外的像素(即虚拟像素)。
除像素之外,图像还有大量的图像属性与配置文件(profile)。属性包括众所周知的特性,如宽度、高度、深度和色彩空间。图像可能具有可选属性,其中可能包括图像作者、注释、创建日期等。某些图像还包含用于色彩管理的配置文件,或 EXIF、IPTC、8BIM、XMP 信息配置文件。ImageMagick 提供命令行选项和编程方法来获取、设置或查看图像属性或配置文件,或应用配置文件。
ImageMagick 由近五十万行 C 代码组成,并可选地依赖依赖库(例如 JPEG、PNG、TIFF 库)中的数百万行代码。鉴于此,人们也许会预期一份庞大的架构文档。然而,绝大部分图像处理只是访问像素及其元数据,而我们简单、优雅且高效的实现使这一切对 ImageMagick 开发者来说变得容易。接下来的几节中,我们将讨论像素缓存的实现,以及图像属性和配置文件的获取与设置。然后,我们讨论在执行线程中使用 ImageMagick。在最后几节中,我们讨论用于读写特定图像格式的图像编解码器,随后简要介绍如何创建滤镜,以根据你的自定义需求访问或更新像素。
像素缓存
ImageMagick 像素缓存是最多可容纳 64 个通道的图像像素的存储库。通道按构建 ImageMagick 时所指定的深度连续存储。通道深度在 Q8 版本的 ImageMagick 中为每个像素分量 8 位,在 Q16 版本中为每个像素分量 16 位,在 Q32 版本中为每个像素分量 32 位。默认情况下,像素分量为 32 位浮点的高动态范围量。通道可以保存任意值,但通常包含红、绿、蓝和 alpha 强度,或青、品红、黄、黑和 alpha 强度。通道可能包含调色板图像的调色板索引,或 CMYK 图像的黑色通道。像素缓存的存储可以是堆内存、磁盘支持的内存映射,或磁盘。像素缓存采用引用计数。克隆缓存时只复制缓存属性。缓存像素只有在你表明更新任意像素的意图时才会随后被复制。
创建像素缓存
像素缓存在图像创建时与其关联,并在你尝试获取或放入像素时被初始化。以下是将像素缓存与图像关联的三种常用方法:
创建一个初始化为背景色的图像画布:
image=AllocateImage(image_info);
if (SetImageExtent(image,640,480) == MagickFalse)
{ /* an exception was thrown */ }
(void) QueryMagickColor("red",&image->background_color,&image->exception);
SetImageBackgroundColor(image);
从磁盘上的 JPEG 图像创建图像:
(void) strcpy(image_info->filename,"image.jpg"):
image=ReadImage(image_info,exception);
if (image == (Image *) NULL)
{ /* an exception was thrown */ }
从基于内存的图像创建图像:
image=BlobToImage(blob_info,blob,extent,exception);
if (image == (Image *) NULL)
{ /* an exception was thrown */ }
在我们对像素缓存的讨论中,我们使用 MagickCore API 来阐明要点,不过,对于 ImageMagick 的其他程序接口,原理是相同的。
当像素缓存被初始化时,像素会从其原始的任何位深度缩放到像素缓存所需的位深度。例如,如果你使用的是 Q8 版本的 ImageMagick,一个 1 通道 1 位的单色 PBM 图像会被缩放为 8 位灰度图像,而对于 Q16 版本则缩放为 16 位 RGBA。你可以用 -version 选项确定你所拥有的版本:
$ identify -version
Version: ImageMagick 7.1.2-25 2026-06-04 Q16-HDRI https://imagemagick.org
如你所见,像素缓存的便利有时伴随着存储(例如将 1 位单色图像存储为 16 位是浪费的)与速度(即将整幅图像存储在内存中通常比一次访问一条扫描线更慢)上的权衡。在大多数情况下,像素缓存的优点通常超过其缺点。
访问像素缓存
一旦像素缓存与图像关联,你通常会想要获取、更新或将像素放入其中。我们将图像区域内的像素称为真实像素(authentic pixels),区域外的称为虚拟像素(virtual pixels)。使用以下方法来访问缓存中的像素:
- GetVirtualPixels():获取你不打算修改的像素,或位于图像区域外的像素(例如 -1,-3 处的像素)
- GetAuthenticPixels():获取你打算修改的像素
- QueueAuthenticPixels():将你打算设置的像素排入队列
- SyncAuthenticPixels():用任何已修改的像素更新像素缓存
以下是一个用于操作像素缓存中像素的典型 MagickCore 代码片段。在我们的示例中,我们将像素从输入图像复制到输出图像,并将强度降低 10%:
const Quantum
*p;
Quantum
*q;
ssize_t
x,
y;
destination=CloneImage(source,source->columns,source->rows,MagickTrue,exception);
if (destination == (Image *) NULL)
{ /* an exception was thrown */ }
for (y=0; y < (ssize_t) source->rows; y++)
{
p=GetVirtualPixels(source,0,y,source->columns,1,exception);
q=GetAuthenticPixels(destination,0,y,destination->columns,1,exception);
if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)
break;
for (x=0; x < (ssize_t) source->columns; x++)
{
SetPixelRed(image,90*p->red/100,q);
SetPixelGreen(image,90*p->green/100,q);
SetPixelBlue(image,90*p->blue/100,q);
SetPixelAlpha(image,90*p->opacity/100,q);
p+=GetPixelChannels(source);
q+=GetPixelChannels(destination);
}
if (SyncAuthenticPixels(destination,exception) == MagickFalse)
break;
}
if (y < (ssize_t) source->rows)
{ /* an exception was thrown */ }
当我们首次通过克隆源图像创建目标图像时,像素缓存的像素并不会被复制。它们只有在你通过调用 GetAuthenticPixels() 或 QueueAuthenticPixels() 表明修改或设置像素缓存的意图时才会被复制。如果你想设置新的像素值而非更新已有的,请使用 QueueAuthenticPixels()。你也可以用 GetAuthenticPixels() 来设置像素值,但使用 QueueAuthenticPixels() 会略微更高效。最后,使用 SyncAuthenticPixels() 确保任何已更新的像素被推送到像素缓存。
你可以将任意内容与每个像素关联,称为 元 内容(meta content)。使用 GetVirtualMetacontent()(读取内容)或 GetAuthenticMetacontent()(更新内容)来访问该内容。例如,要打印元内容,请使用:
const void
*metacontent;
for (y=0; y < (ssize_t) source->rows; y++)
{
p=GetVirtualPixels(source,0,y,source->columns,1);
if (p == (const Quantum *) NULL)
break;
metacontent=GetVirtualMetacontent(source);
/* print meta content here */
}
if (y < (ssize_t) source->rows)
/* an exception was thrown */
像素缓存管理器决定让你直接还是间接访问图像像素。在某些情况下,像素会被暂存到一个中间缓冲区——这就是为什么你必须调用 SyncAuthenticPixels() 来确保该缓冲区被推出到像素缓存,从而保证缓存中相应的像素被更新。出于这个原因,我们建议你一次只读取或更新一条扫描线或几条扫描线的像素。不过,你可以获取所需的任意矩形像素区域。GetAuthenticPixels() 要求你请求的区域位于图像区域的边界之内。对于一幅 640×480 的图像,你可以获取第 479 行的 640 个像素的扫描线,但如果你请求第 480 行的扫描线,则会返回异常(行从 0 开始编号)。GetVirtualPixels() 没有此约束。例如,
p=GetVirtualPixels(source,-3,-3,source->columns+3,6,exception);
会毫无怨言地给你所请求的像素,即便其中有些并不在图像区域的范围之内。
虚拟像素
有大量图像处理算法需要某个目标像素周围的像素邻域。这类算法通常包含关于如何处理图像边界周围像素(即边缘像素)的注意事项。有了虚拟像素,除了选择最适合你算法的虚拟像素方法外,你无需为特殊的边缘处理操心。
对虚拟像素的访问由 MagickCore API 的 SetImageVirtualPixelMethod() 方法或命令行的 -virtual-pixel 选项控制。这些方法包括:
| background | 图像周围的区域为背景色 |
|---|---|
| black | 图像周围的区域为黑色 |
| checker-tile | 以图像与背景色交替的方格 |
| dither | 非随机的 32x32 抖动图案 |
| edge | 将边缘像素向无穷远延伸(默认) |
| gray | 图像周围的区域为灰色 |
| horizontal-tile | 水平平铺图像,上方/下方为背景色 |
| horizontal-tile-edge | 水平平铺图像并复制侧边缘像素 |
| mirror | 镜像平铺图像 |
| random | 从图像中选择一个随机像素 |
| tile | 平铺图像 |
| transparent | 图像周围的区域为透明的黑色 |
| vertical-tile | 垂直平铺图像,两侧为背景色 |
| vertical-tile-edge | 垂直平铺图像并复制侧边缘像素 |
| white | 图像周围的区域为白色 |
缓存存储与资源需求
回想一下,ImageMagick 像素缓存这一简单而优雅的设计是以存储和处理速度为代价的。像素缓存的存储需求随图像面积与像素分量的位深度而增长。例如,如果我们有一幅 640×480 的图像,并使用非 HDRI 的 Q16 版本 ImageMagick,则像素缓存消耗 图像宽度 * 高度 * 位深度 / 8 * 通道数 字节,约为 2.3 mebibyte(即 640 * 480 * 2 * 4)。还不算太糟,但如果你的图像是 25000×25000 像素呢?像素缓存将需要约 4.7 gibibyte 的存储。哎哟。ImageMagick 通过将大图像缓存到磁盘而非内存来应对可能的巨大存储需求。通常像素缓存使用堆内存存储在内存中。如果堆内存耗尽,我们会在磁盘上创建像素缓存并尝试将其内存映射。如果内存映射内存耗尽,我们就直接使用标准的磁盘 I/O。磁盘存储充足又便宜,但它也非常慢——比访问内存中的像素慢上千倍之多。如果我们对基于磁盘的缓存进行内存映射,可以获得一定的速度提升,最高可达 5 倍。这些关于存储的决定由像素缓存管理器与操作系统协商后自动做出。不过,你可以通过缓存资源限制来影响像素缓存管理器如何分配像素缓存。这些限制包括:
| width | 图像的最大宽度。超过此限制将抛出异常,操作中止。 |
|---|---|
| height | 图像的最大高度。超过此限制将抛出异常,操作中止。 |
| area | 可驻留于像素缓存内存中的任意单幅图像的最大字节面积。如果超过此限制,图像会被自动缓存到磁盘,并可选地进行内存映射。 |
| memory | 从堆中为像素缓存分配的最大内存字节数。 |
| map | 为像素缓存分配的最大内存映射字节数。 |
| disk | 允许像素缓存使用的最大磁盘空间字节数。如果超过此限制,将抛出致命异常,所有处理停止。 |
| files | 打开的像素缓存文件的最大数量。超过此限制时,任何后续缓存到磁盘的像素会被关闭并按需重新打开。此行为通过减少像素缓存的打开/关闭系统调用次数,使大量图像能够在磁盘上同时被访问而不损失速度。 |
| thread | 允许并行运行的最大线程数。你的系统可能选择小于此值的线程数。ImageMagick 默认会选择最优的线程数,通常即为你主机上的核心数。将此值设为 1,则所有并行区域都由一个线程执行。 |
| time | 允许进程执行的最大秒数。超过此限制将抛出异常,处理停止。 |
注意,这些限制针对的是 ImageMagick 像素缓存。ImageMagick 内的某些算法不遵守这些限制,任何外部委托库(例如 JPEG、TIFF 等)也不遵守。
要确定这些限制的当前设置,使用此命令:
-> identify -list resource
Resource limits:
Width: 100MP
Height: 100MP
Area: 25.181GB
Memory: 11.726GiB
Map: 23.452GiB
Disk: unlimited
File: 768
Thread: 12
Throttle: 0
Time: unlimited
你可以通过安全策略(参见 policy.xml)、环境变量、-limit 命令行选项,或 SetMagickResourceLimit() MagickCore API 方法来设置这些限制。举例来说,我们的 ImageMagick 在线网页接口 MagickStudio 包含了以下策略限制,以帮助防止拒绝服务攻击:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)*>
<!ATTLIST policymap xmlns CDATA #FIXED "">
<!ELEMENT policy EMPTY>
<!ATTLIST policy xmlns CDATA #FIXED "">
<!ATTLIST policy domain NMTOKEN #REQUIRED>
<!ATTLIST policy name NMTOKEN #IMPLIED>
<!ATTLIST policy pattern CDATA #IMPLIED>
<!ATTLIST policy rights NMTOKEN #IMPLIED>
<!ATTLIST policy stealth NMTOKEN #IMPLIED>
<!ATTLIST policy value CDATA #IMPLIED>
]>
<!--
Creating a security policy that fits your specific local environment
before making use of ImageMagick is highly advised. You can find guidance on
setting up this policy at https://imagemagick.org/security-policy/,
and it's important to verify your policy using the validation tool located
at https://imagemagick-secevaluator.doyensec.com/.
Secure ImageMagick security policy:
This stringent security policy prioritizes the implementation of
rigorous controls and restricted resource utilization to establish a
profoundly secure setting while employing ImageMagick. It deactivates
conceivably hazardous functionalities, including specific coders like
SVG or HTTP. The policy promotes the tailoring of security measures to
harmonize with the requirements of the local environment and the guidelines
of the organization. This protocol encompasses explicit particulars like
limitations on memory consumption, sanctioned pathways for reading and
writing, confines on image sequences, the utmost permissible duration of
workflows, allocation of disk space intended for image data, and even an
undisclosed passphrase for remote connections. By adopting this robust
policy, entities can elevate their overall security stance and alleviate
potential vulnerabilities.
-->
<policymap>
<!-- Set maximum parallel threads. -->
<policy domain="resource" name="thread" value="2"/>
<!-- Set maximum time in seconds. When this limit is exceeded, an exception
is thrown and processing stops. -->
<policy domain="resource" name="time" value="120"/>
<!-- Set maximum number of open pixel cache files. When this limit is
exceeded, any subsequent pixels cached to disk are closed and reopened
on demand. -->
<policy domain="resource" name="file" value="768"/>
<!-- Set maximum amount of memory in bytes to allocate for the pixel cache
from the heap. When this limit is exceeded, the image pixels are cached
to memory-mapped disk. -->
<policy domain="resource" name="memory" value="256MiB"/>
<!-- Set maximum amount of memory map in bytes to allocate for the pixel
cache. When this limit is exceeded, the image pixels are cached to
disk. -->
<policy domain="resource" name="map" value="512MiB"/>
<!-- Set the maximum width * height of an image that can reside in the pixel
cache memory. Images that exceed the area limit are cached to disk. -->
<policy domain="resource" name="area" value="16KP"/>
<!-- Set maximum amount of disk space in bytes permitted for use by the pixel
cache. When this limit is exceeded, the pixel cache is not be created
and an exception is thrown. -->
<policy domain="resource" name="disk" value="1GiB"/>
<!-- Set the maximum length of an image sequence. When this limit is
exceeded, an exception is thrown. -->
<policy domain="resource" name="list-length" value="32"/>
<!-- Set the maximum width of an image. When this limit is exceeded, an
exception is thrown. -->
<policy domain="resource" name="width" value="8KP"/>
<!-- Set the maximum height of an image. When this limit is exceeded, an
exception is thrown. -->
<policy domain="resource" name="height" value="8KP"/>
<!-- Periodically yield the CPU for at least the time specified in
milliseconds. -->
<!-- -->
<!-- Do not create temporary files in the default shared directories, instead
specify a private area to store only ImageMagick temporary files. -->
<!-- -->
<!-- Force memory initialization by memory mapping select memory
allocations. -->
<policy domain="cache" name="memory-map" value="anonymous"/>
<!-- Ensure all image data is fully flushed and synchronized to disk. -->
<policy domain="cache" name="synchronize" value="true"/>
<!-- Replace passphrase for secure distributed processing -->
<!-- -->
<!-- Do not permit any delegates to execute. -->
<policy domain="delegate" rights="none" pattern="*"/>
<!-- Do not permit any image filters to load. -->
<policy domain="filter" rights="none" pattern="*"/>
<!-- Don't read/write from/to stdin/stdout. -->
<policy domain="path" rights="none" pattern="-"/>
<!-- don't read sensitive paths. -->
<policy domain="path" rights="none" pattern="/etc/*"/>
<!-- Indirect reads are not permitted. -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- These image types are security risks on read, but write is fine -->
<policy domain="module" rights="write" pattern="{MSL,MVG,PS,SVG,URL,XPS}"/>
<!-- This policy sets the number of times to replace content of certain
memory buffers and temporary files before they are freed or deleted. -->
<policy domain="system" name="shred" value="1"/>
<!-- Enable the initialization of buffers with zeros, resulting in a minor
performance penalty but with improved security. -->
<policy domain="system" name="memory-map" value="anonymous"/>
<!-- Set the maximum amount of memory in bytes that is permitted for
allocation requests. -->
<policy domain="system" name="max-memory-request" value="256MiB"/>
</policymap>
由于我们要处理多个同时进行的会话,我们不希望任何单个会话耗尽所有可用内存。有了这个策略,大图像会被缓存到磁盘。如果图像太大并超过像素缓存的磁盘限制,程序将退出。此外,我们设置了时间限制,以防止任何失控的处理任务。如果任何单幅图像的宽度或高度超过 8192 像素,将抛出异常,处理停止。从 ImageMagick 7.0.1-8 起,你可以禁止使用任何委托或所有委托(将 pattern 设为 "*")。注意,在此版本之前,应使用 "coder" 域来阻止委托的使用(例如 domain="coder" rights="none" pattern="HTTPS")。该策略还禁止间接读取。例如,如果你想从文件中读取文本(例如 caption:@myCaption.txt),则需要移除此策略。
注意,这些缓存限制对每次调用 ImageMagick 都是全局的,意味着如果你创建多幅图像,则将合计的资源需求与限制进行比较,以确定像素缓存的存储处置方式。
要确定像素缓存消耗了哪种类型以及多少资源,在命令行中添加 -debug cache 选项:
$ magick -debug cache logo: -sharpen 3x2 null:
2016-12-17T13:33:42-05:00 0:00.000 0.000u 7.0.0 Cache magick: cache.c/DestroyPixelCache/1275/Cache
destroy
2016-12-17T13:33:42-05:00 0:00.000 0.000u 7.0.0 Cache magick: cache.c/OpenPixelCache/3834/Cache
open LOGO[0] (Heap Memory, 640x480x4 4.688MiB)
2016-12-17T13:33:42-05:00 0:00.010 0.000u 7.0.0 Cache magick: cache.c/OpenPixelCache/3834/Cache
open LOGO[0] (Heap Memory, 640x480x3 3.516MiB)
2016-12-17T13:33:42-05:00 0:00.010 0.000u 7.0.0 Cache magick: cache.c/ClonePixelCachePixels/1044/Cache
Memory => Memory
2016-12-17T13:33:42-05:00 0:00.020 0.010u 7.0.0 Cache magick: cache.c/ClonePixelCachePixels/1044/Cache
Memory => Memory
2016-12-17T13:33:42-05:00 0:00.020 0.010u 7.0.0 Cache magick: cache.c/OpenPixelCache/3834/Cache
open LOGO[0] (Heap Memory, 640x480x3 3.516MiB)
2016-12-17T13:33:42-05:00 0:00.050 0.100u 7.0.0 Cache magick: cache.c/DestroyPixelCache/1275/Cache
destroy LOGO[0]
2016-12-17T13:33:42-05:00 0:00.050 0.100u 7.0.0 Cache magick: cache.c/DestroyPixelCache/1275/Cache
destroy LOGO[0]
此命令使用内存中的像素缓存。logo 消耗了 4.688MiB,锐化之后为 3.516MiB。
分布式像素缓存
分布式像素缓存是单台主机上传统像素缓存的扩展。分布式像素缓存可以跨越多台服务器,从而在大小与事务处理能力上不断增长,以支持非常大的图像。在一台或多台机器上启动像素缓存服务器。当你读取或操作图像而本地像素缓存资源耗尽时,ImageMagick 会联系一台或多台这些远程像素服务器来存储或检索像素。分布式像素缓存依赖网络带宽在远程服务器之间编排像素。因此,它很可能比使用本地存储(例如内存、磁盘等)的像素缓存慢得多。
magick -distribute-cache 6668 & // start on 192.168.100.50
magick -define registry:cache:hosts=192.168.100.50:6668 myimage.jpg -sharpen 5x2 mimage.png
缓存视图
来自 MagickCore API 的 GetVirtualPixels()、GetAuthenticPixels()、QueueAuthenticPixels() 和 SyncAuthenticPixels(),一次只能处理每幅图像的一个像素缓存区域。假设你想同时访问同一幅图像的第一条和最后一条扫描线?解决方案是使用缓存视图。缓存视图允许你按需同时访问像素缓存中任意多个区域。缓存视图的方法与前述方法类似,只是你必须先打开一个视图,并在用完后关闭它。以下是一段 MagickCore 代码片段,让我们能够同时访问图像的第一行和最后一行像素:
CacheView
*first_row,
*last_row;
first_row=AcquireVirtualCacheView(source,exception);
last_row=AcquireVirtualCacheView(source,exception);
for (y=0; y < (ssize_t) source->rows; y++)
{
const Quantum
*p,
*q;
p=GetCacheViewVirtualPixels(first_row,0,y,source->columns,1,exception);
q=GetCacheViewVirtualPixels(last_row,0,source->rows-y-1,source->columns,1,exception);
if ((p == (const Quantum *) NULL) || (q == (const Quantum *) NULL))
break;
for (x=0; x < (ssize_t) source->columns; x++)
{
/* do something with p & q here */
}
}
last_row=DestroyCacheView(last_row);
first_row=DestroyCacheView(first_row);
if (y < (ssize_t) source->rows)
{ /* an exception was thrown */ }
Magick 像素缓存格式
回想一下,每种图像格式都由 ImageMagick 解码,像素随后被存入像素缓存。如果你写出图像,像素会从像素缓存中读取,并按照你正在写入的格式(例如 GIF、PNG 等)的要求进行编码。Magick 像素缓存(MPC)格式旨在消除在图像格式与像素之间进行解码和编码的开销。MPC 写出两个文件。其一带有扩展名 .mpc,保留与图像或图像序列相关的所有属性(例如宽度、高度、色彩空间等);其二带有扩展名 .cache,是以原生原始格式存储的像素缓存。读取 MPC 图像文件时,ImageMagick 读取图像属性并将磁盘上的像素缓存内存映射,从而无需解码图像像素。代价在于磁盘空间。MPC 在文件大小上通常比大多数其他图像格式更大。
MPC 图像文件最有效的用法是写一次、多次读取的模式。例如,你的工作流需要从源图像中提取随机的像素块。我们不必每次都重新读取并可能解压源图像,而是使用 MPC 并将图像直接映射到内存。
像素缓存推荐做法
尽管你可以用 GetVirtualPixels()、GetAuthenticPixels()、QueueAuthenticPixels、GetCacheViewVirtualPixels()、GetCacheViewAuthenticPixels() 和 QueueCacheViewAuthenticPixels() 方法从像素缓存请求任意像素、任意像素块、任意扫描线、多条扫描线、任意行或多行,但 ImageMagick 针对一次返回少量像素或几行像素进行了优化。如果你一次请求单条扫描线或几条扫描线,还会有额外的优化。这些方法也允许对像素缓存进行随机访问,不过,ImageMagick 是针对顺序访问优化的。尽管你可以从图像的最后一行到第一行顺序访问扫描线,但如果你按顺序从图像的第一行访问到最后一行,可能会获得性能提升。
你可以按行或按列的顺序获取、修改或设置像素。然而,按行而非按列访问像素更高效。
如果你更新了从 GetAuthenticPixels() 或 GetCacheViewAuthenticPixels() 返回的像素,别忘了分别调用 SyncAuthenticPixels() 或 SyncCacheViewAuthenticPixels(),以确保你的更改与像素缓存同步。
如果你要设置初始像素值,请使用 QueueAuthenticPixels() 或 QueueCacheViewAuthenticPixels()。GetAuthenticPixels() 或 GetCacheViewAuthenticPixels() 方法会从缓存读取像素,而如果你只是要设置初始像素值,这次读取是不必要的。别忘了分别调用 SyncAuthenticPixels() 或 SyncCacheViewAuthenticPixels(),将任何像素更改推送到像素缓存。
GetVirtualPixels()、GetAuthenticPixels()、QueueAuthenticPixels() 和 SyncAuthenticPixels() 比它们对应的缓存视图版本略微更高效。然而,如果你需要同时访问图像的多个区域,或有多个执行线程访问图像,则需要使用缓存视图。
你可以用 GetVirtualPixels() 或 GetCacheViewVirtualPixels() 请求图像边界外的像素,不过,请求图像区域范围内的像素更高效。
虽然你可以通过适当的资源限制强制将像素缓存写到磁盘,但磁盘访问可能比内存访问慢上千倍之多。为了快速、高效地访问像素缓存,尽量让像素缓存保留在堆内存中。
Q16 版本的 ImageMagick 允许你在不缩放的情况下读写 16 位图像,但像素缓存消耗的资源是 Q8 版本的两倍。如果你的系统内存或磁盘资源受限,考虑使用 Q8 版本的 ImageMagick。此外,Q8 版本通常比 Q16 版本执行得更快。
绝大多数图像格式与算法将自身限制在从 0 到某个最大值的固定像素值范围内,例如,Q16 版本的 ImageMagick 允许从 0 到 65535 的强度。然而,高动态范围成像(HDRI)允许比标准数字成像技术远大得多的曝光动态范围(即明暗区域之间的巨大差异)。HDRI 准确地表示真实场景中从最明亮的直射阳光到最深最暗的阴影的宽广强度级别范围。在 ImageMagick 构建时启用 HDRI 以处理高动态范围图像,但要注意每个像素分量是一个 32 位浮点值。此外,像素值默认不被钳制,因此某些算法可能因带外像素值而产生与非 HDRI 版本不同的意外结果。
如果你在处理大图像,请确保像素缓存被写到有充足可用空间的磁盘区域。在 Linux 下,这通常是 /tmp,在 Windows 下是 c:/temp。你可以用这些选项告诉 ImageMagick 将像素缓存写到其他位置并节省内存:
magick -limit memory 2GB -limit map 4GB -define registry:temporary-path=/data/tmp ...
在 policy.xml 配置文件中为你的环境设置全局资源限制。
如果你打算多次处理同一幅图像,考虑使用 MPC 格式。读取 MPC 图像几乎是零开销,因为它已是原生像素缓存格式,无需解码图像像素。以下是一个示例:
magick image.tif image.mpc
magick image.mpc -crop 100x100+0+0 +repage 1.png
magick image.mpc -crop 100x100+100+0 +repage 2.png
magick image.mpc -crop 100x100+200+0 +repage 3.png
MPC 非常适合网站。它减少了读写图像的开销。我们在我们的在线图像工作室中专门使用它。
图像属性与配置文件
图像以属性(例如宽度、高度、描述等)和配置文件(例如 EXIF、IPTC、色彩管理)的形式关联有元数据。ImageMagick 提供便捷的方法来获取、设置或更新图像属性,以及获取、设置、更新或应用配置文件。一些较常用的图像属性与 MagickCore API 中的 Image 结构相关联。例如:
(void) printf("image width: %lu, height: %lu\n",image->columns,image->rows);
对于绝大多数图像属性,例如图像注释或描述,我们使用 GetImageProperty() 和 SetImageProperty() 方法。这里我们设置一个属性并立即将其取回:
const char
*comment;
(void) SetImageProperty(image,"comment","This space for rent");
comment=GetImageProperty(image,"comment");
if (comment == (const char *) NULL)
(void) printf("Image comment: %s\n",comment);
ImageMagick 通过 GetImageArtifact() 和 SetImageArtifact() 方法支持构件(artifact)。构件是隐形属性,不会被导出到图像格式(例如 PNG)。
图像配置文件通过 GetImageProfile()、SetImageProfile() 和 ProfileImage() 方法处理。这里我们设置一个配置文件并立即将其取回:
StringInfo
*profile;
profile=AcquireStringInfo(length);
SetStringInfoDatum(profile,my_exif_profile);
(void) SetImageProfile(image,"EXIF",profile);
DestroyStringInfo(profile);
profile=GetImageProfile(image,"EXIF");
if (profile != (StringInfo *) NULL)
(void) PrintStringInfo(stdout,"EXIF",profile);
多光谱图像
ImageMagick 支持多光谱图像,其中所有通道都具有与原始图像相同的尺寸和像素数量。然而,并非所有图像格式都支持多光谱图像。PSD、TIFF、MIFF、MPC 和 FTXT 完全支持最多 31 个波段的多光谱图像,其中 21 个为元通道。注意,如果你使用 configure 脚本的 --enable-64bit-channel-masks 选项构建 ImageMagick,你可以处理多达 62 个波段、最多 52 个元通道的多光谱图像。
如果你有某个用例当前不被某种图像格式支持,请将其发布到讨论论坛。很有可能,我们能在 ImageMagick 的未来版本中支持你的用例。
像素流式处理
ImageMagick 提供在像素从图像读取或写入图像时进行流式处理的能力。相比像素缓存,这有几个优点。像素缓存所消耗的时间和资源随图像的面积而扩展,而像素流的资源随图像的宽度而扩展。缺点是像素必须在流式处理时被消费,因此没有持久性。
在你的 MagickCore 程序中使用带有适当回调方法的 ReadStream() 或 WriteStream(),以便在像素流式传输时消费它们。以下是一个使用 ReadStream 的简略示例:
static size_t StreamPixels(const Image *image,const void *pixels,const size_t columns)
{
register const Quantum
*p;
MyData
*my_data;
my_data=(MyData *) image->client_data;
p=(Quantum *) pixels;
if (p != (const Quantum *) NULL)
{
/* process pixels here */
}
return(columns);
}
...
/* invoke the pixel stream here */
image_info->client_data=(void *) MyData;
image=ReadStream(image_info,&StreamPixels,exception);
我们还提供了一个轻量级工具 stream,可将图像或图像部分的一个或多个像素分量流式输出到你选择的存储格式。它在从输入图像逐行读取像素分量时将其写出,使得 stream 在处理大图像或当你需要原始像素分量时十分理想。大多数图像格式以从左到右、从上到下的方式流式传输像素(红、绿、蓝)。然而,少数格式不支持这种常见的排序(例如 PSD 格式)。
大图像支持
ImageMagick 具备处理从兆像素到太像素图像尺寸的能力,涵盖读取、处理和写入操作。理论上,在 32 位操作系统上图像尺寸可达 3100 万行/列,在 64 位操作系统上则高达惊人的 31 万亿。然而,实际可达到的尺寸要小得多,取决于你主机上可用的资源。必须注意,某些图像格式对图像尺寸施加了限制。例如,Photoshop 图像的宽度或高度被限制在最多 300,000 像素。这里我们将一幅图像调整为二十五万像素见方:
magick logo: -resize 250000x250000 logo.miff
对于大图像,内存资源很可能会耗尽,ImageMagick 将转而在磁盘上创建像素缓存。请确保你有充足的临时磁盘空间。如果你默认的临时磁盘分区太小,告诉 ImageMagick 使用另一个有充足可用空间的分区。例如:
magick -define registry:temporary-path=/data/tmp logo: \
-resize 250000x250000 logo.miff
为确保大图像不会耗尽你系统上的所有内存,用资源限制强制将图像像素写到内存映射磁盘:
magick -define registry:temporary-path=/data/tmp -limit memory 16mb \
logo: -resize 250000x250000 logo.miff
这里我们强制将所有图像像素写到磁盘:
magick -define registry:temporary-path=/data/tmp -limit area 0 \
logo: -resize 250000x250000 logo.miff
将像素缓存到磁盘比内存慢约 1000 倍。用 ImageMagick 在磁盘上处理大图像时,请预期较长的运行时间。你可以用此命令监控进度:
magick -monitor -limit memory 2GiB -limit map 4GiB -define registry:temporary-path=/data/tmp \
logo: -resize 250000x250000 logo.miff
对于真正巨大的图像,或者如果你主机上的资源有限,你可以在一台或多台远程主机上利用分布式像素缓存:
magick -distribute-cache 6668 & // start on 192.168.100.50
magick -distribute-cache 6668 & // start on 192.168.100.51
magick -limit memory 2mb -limit map 2mb -limit disk 2gb \
-define registry:cache:hosts=192.168.100.50:6668,192.168.100.51:6668 \
myhugeimage.jpg -sharpen 5x2 myhugeimage.png
由于网络延迟,预期你的工作流处理会大幅减速。
执行线程
ImageMagick 的许多内部算法都是多线程的,以利用多核处理器芯片所提供的加速。然而,除 MagickCore 的 GetVirtualPixels()、GetAuthenticPixels()、QueueAuthenticPixels() 或 SyncAuthenticPixels() 像素缓存方法之外,欢迎你在自己的执行线程中使用 ImageMagick 算法。这些方法仅适用于单一执行线程,OpenMP 并行区段除外。要用多个执行线程访问像素缓存,请使用缓存视图。例如,我们在 CompositeImage() 方法中就是这么做的。假设我们想在每个执行线程中将单个源图像合成到不同的目标图像上。如果我们使用 GetVirtualPixels(),结果将不可预测,因为多个线程很可能会同时请求像素缓存的不同区域。我们改用 GetCacheViewVirtualPixels(),它为每个执行线程创建一个唯一的视图,确保无论调用多少线程,我们的程序都能正常工作。其他程序接口,例如 MagickWand API,是完全线程安全的,因此对于执行线程无需特别的预防措施。
以下是一个利用 OpenMP 编程范式发挥执行线程优势的 MagickCore 代码片段:
CacheView
*image_view;
MagickBooleanType
status;
ssize_t
y;
/*
Acquire a cache view to enable parallelism.
*/
status=MagickTrue;
image_view=AcquireVirtualCacheView(image,exception);
#pragma omp parallel for schedule(static,4) shared(status)
for (y=0; y < (ssize_t) image->rows; y++)
{
register Quantum
*q;
register ssize_t
x;
register void
*metacontent;
if (status == MagickFalse)
continue;
/*
Get a row of pixels.
*/
q=GetCacheViewAuthenticPixels(image_view,0,y,image->columns,1,exception);
if (q == (Quantum *) NULL)
{
status=MagickFalse;
continue;
}
metacontent=GetCacheViewAuthenticMetacontent(image_view);
for (x=0; x < (ssize_t) image->columns; x++)
{
/*
Set the pixel color.
*/
SetPixelRed(image,...,q);
SetPixelGreen(image,...,q);
SetPixelBlue(image,...,q);
SetPixelAlpha(image,...,q);
if (metacontent != NULL)
metacontent[indexes+x]=...;
q+=GetPixelChannels(image);
}
/*
Sync the updated pixels to the pixel cache.
*/
if (SyncCacheViewAuthenticPixels(image_view,exception) == MagickFalse)
status=MagickFalse;
}
/*
Destroy the cache view.
*/
image_view=DestroyCacheView(image_view);
if (status == MagickFalse)
perror("something went wrong");
这段代码片段将未压缩的 Windows 位图转换为 Magick++ 图像:
#include "Magick++.h"
#include <assert.h>
#include "omp.h"
void ConvertBMPToImage(const BITMAPINFOHEADER *bmp_info,
const unsigned char *restrict pixels,Magick::Image *image)
{
/*
Prepare the image so that we can modify the pixels directly.
*/
assert(bmp_info->biCompression == BI_RGB);
assert(bmp_info->biWidth == image->columns());
assert(abs(bmp_info->biHeight) == image->rows());
image->modifyImage();
if (bmp_info->biBitCount == 24)
image->type(MagickCore::TrueColorType);
else
image->type(MagickCore::TrueColorMatteType);
register unsigned int bytes_per_row=bmp_info->biWidth*bmp_info->biBitCount/8;
if (bytes_per_row % 4 != 0) {
bytes_per_row=bytes_per_row+(4-bytes_per_row % 4); // divisible by 4.
}
/*
Copy all pixel data, row by row.
*/
#pragma omp parallel for
for (int y=0; y < int(image->rows()); y++)
{
int
row;
register const unsigned char
*restrict p;
register MagickCore::Quantum
*restrict q;
row=(bmp_info->biHeight > 0) ? (image->rows()-y-1) : y;
p=pixels+row*bytes_per_row;
q=image->setPixels(0,y,image->columns(),1);
for (int x=0; x < int(image->columns()); x++)
{
SetPixelBlue(image,p[0],q);
SetPixelGreen(image,p[1],q);
SetPixelRed(image,p[2],q);
if (bmp_info->biBitCount == 32) {
SetPixelAlpha(image,p[3],q);
}
q+=GetPixelChannels(image);
p+=bmp_info->biBitCount/8;
}
image->syncPixels(); // sync pixels to pixel cache.
}
return;
}
如果你从启用了 OpenMP 的应用程序调用 ImageMagick API,并打算在后续并行区域中动态增加可用线程数,请务必在调用 API 之前进行增加,否则 ImageMagick 可能会出错。
MagickWand 支持 wand 视图。视图并行地遍历整幅或部分图像,并对每一行像素调用你提供的回调方法。这将你的大部分并行编程活动限制在那一个模块内。MagickCore 中有类似的方法。作为示例,请参见在 MagickWand 和 MagickCore 中均有实现的同一 sigmoidal 对比度算法。
在大多数情况下,为获得最佳性能,默认线程数设为你系统上的处理器核心数。然而,如果你的系统是超线程的,或者你运行在虚拟主机上而只有部分处理器对你的服务器实例可用,你也许能通过设置线程策略或 MAGICK_THREAD_LIMIT 环境变量来获得性能提升。例如,你的虚拟主机有 8 个处理器,但只有 2 个分配给你的服务器实例。默认的 8 个线程可能引发严重的性能问题。一种解决办法是在你的 policy.xml 配置文件中将线程数限制为可用的处理器数:
<policy domain="resource" name="thread" value="2"/>
或者假设你的 12 核超线程计算机默认为 24 个线程。设置 MAGICK_THREAD_LIMIT 环境变量,你很可能会获得改善的性能:
export MAGICK_THREAD_LIMIT=12
OpenMP 委员会尚未定义将 OpenMP 与其他线程模型(如 Posix 线程)混用的行为。然而,使用现代版本的 Linux,OpenMP 与 Posix 线程似乎能毫无怨言地互操作。如果你想从一个调用某个 ImageMagick 应用程序编程接口(例如 MagickCore、MagickWand、Magick++ 等)的程序模块中使用 Posix 线程,并且运行在 Mac OS X 或较旧的 Linux 版本上,你可能需要在 ImageMagick 中禁用 OpenMP 支持。在 configure 脚本命令行中添加 --disable-openmp 选项,然后重新构建并重新安装 ImageMagick。
你可以通过使用 tcmalloc 内存分配库减少锁竞争,从而进一步提升性能。要启用它,在你构建 ImageMagick 时在 configure 命令行中添加 --with-tcmalloc。
线程性能
在并行环境中很难预测行为。性能可能取决于若干因素,包括编译器、OpenMP 库的版本、处理器类型、核心数量、内存量、是否启用超线程、与 ImageMagick 并发执行的应用程序组合,或你所使用的特定图像处理算法。就线程数量而言,确保最佳性能的唯一办法是进行基准测试。ImageMagick 在对命令进行基准测试时包含渐进式线程,并返回一个或多个线程的经过时间与效率。这可以帮助你确定在你的环境中多少线程最为高效。在此基准测试中,我们用 1 到 12 个线程将一幅 1920x1080 的模特图像锐化 10 次:
$ magick -bench 10 model.png -sharpen 5x2 null:
Performance[1]: 10i 1.135ips 1.000e 8.760u 0:08.810
Performance[2]: 10i 2.020ips 0.640e 9.190u 0:04.950
Performance[3]: 10i 2.786ips 0.710e 9.400u 0:03.590
Performance[4]: 10i 3.378ips 0.749e 9.580u 0:02.960
Performance[5]: 10i 4.032ips 0.780e 9.580u 0:02.480
Performance[6]: 10i 4.566ips 0.801e 9.640u 0:02.190
Performance[7]: 10i 3.788ips 0.769e 10.980u 0:02.640
Performance[8]: 10i 4.115ips 0.784e 12.030u 0:02.430
Performance[9]: 10i 4.484ips 0.798e 12.860u 0:02.230
Performance[10]: 10i 4.274ips 0.790e 14.830u 0:02.340
Performance[11]: 10i 4.348ips 0.793e 16.500u 0:02.300
Performance[12]: 10i 4.525ips 0.799e 18.320u 0:02.210
此示例的最佳点是 6 个线程。这说得通,因为有 6 个物理核心。另外 6 个是超线程。看来锐化并不能从超线程中获益。
在某些情况下,将线程数设为 1 或用 MAGICK_THREAD_LIMIT 环境变量、-limit 命令行选项或 policy.xml 配置文件完全禁用 OpenMP,可能是最优的。
启用动态节流以优化性能
另一个有效的选项是在 ImageMagick 中启用动态节流。当此功能被激活时,ImageMagick 会根据你系统当前的平均负载,在处理各条图像扫描线之间自动引入轻微延迟。这有助于防止资源耗尽,并确保更平滑的性能,尤其是在繁重负载下。
要启用动态节流,添加以下策略配置:
<policy domain="resource" name="dynamic-throttle" value="true"/>
异构分布式处理
ImageMagick 通过 OpenCL 框架包含对异构分布式处理的支持。ImageMagick 内的 OpenCL 内核允许图像处理算法在由 CPU、GPU 及其他处理器组成的异构平台上执行。视你的平台而定,加速可能比传统的单一 CPU 快上一个数量级。
首先验证你的 ImageMagick 版本是否包含对 OpenCL 功能的支持:
magick identify -version
Features: DPC Cipher Modules OpenCL OpenMP(4.5)
如果包含,运行此命令可在图像卷积中实现显著加速:
magick image.png -convolve '-1, -1, -1, -1, 9, -1, -1, -1, -1' convolve.png
如果加速器不可用或加速器未能响应,ImageMagick 会回退到非加速的卷积算法。
以下是一个对图像进行卷积的示例 OpenCL 内核:
static inline long ClampToCanvas(const long offset,const ulong range)
{
if (offset < 0L)
return(0L);
if (offset >= range)
return((long) (range-1L));
return(offset);
}
static inline CLQuantum ClampToQuantum(const float value)
{
if (value < 0.0)
return((CLQuantum) 0);
if (value >= (float) QuantumRange)
return((CLQuantum) QuantumRange);
return((CLQuantum) (value+0.5));
}
__kernel void Convolve(const __global CLPixelType *source,__constant float *filter,
const ulong width,const ulong height,__global CLPixelType *destination)
{
const ulong columns = get_global_size(0);
const ulong rows = get_global_size(1);
const long x = get_global_id(0);
const long y = get_global_id(1);
const float scale = (1.0/QuantumRange);
const long mid_width = (width-1)/2;
const long mid_height = (height-1)/2;
float4 sum = { 0.0, 0.0, 0.0, 0.0 };
float gamma = 0.0;
register ulong i = 0;
for (long v=(-mid_height); v <= mid_height; v++)
{
for (long u=(-mid_width); u <= mid_width; u++)
{
register const ulong index=ClampToCanvas(y+v,rows)*columns+ClampToCanvas(x+u,
columns);
const float alpha=scale*(QuantumRange-source[index].w);
sum.x+=alpha*filter[i]*source[index].x;
sum.y+=alpha*filter[i]*source[index].y;
sum.z+=alpha*filter[i]*source[index].z;
sum.w+=filter[i]*source[index].w;
gamma+=alpha*filter[i];
i++;
}
}
gamma=1.0/(fabs(gamma) <= MagickEpsilon ? 1.0 : gamma);
const ulong index=y*columns+x;
destination[index].x=ClampToQuantum(gamma*sum.x);
destination[index].y=ClampToQuantum(gamma*sum.y);
destination[index].z=ClampToQuantum(gamma*sum.z);
destination[index].w=ClampToQuantum(sum.w);
};
完整的 OpenCL 内核图像卷积实现,请参见 MagickCore/accelerate.c。
注意,在 Windows 下,你可能会遇到 TDR(GPU 的超时检测与恢复)问题。它的目的是通过执行时间阈值检测挂起 GPU 的失控任务。对于某些运行 ImageMagick 中 OpenCL 滤镜的较旧低端 GPU,较长的执行时间可能触发 TDR 机制并抢占 GPU 图像滤镜。当这种情况发生时,ImageMagick 会自动回退到 CPU 代码路径并返回预期结果。为避免抢占,增大 TdrDelay 注册表项。
自定义图像编解码器
图像编解码器(即编码器/解码器)负责对一种图像格式(例如 PNG、GIF、JPEG 等)进行注册、可选地分类、可选地读取、可选地写入以及注销。注册图像编解码器会提醒 ImageMagick 某种特定格式可供读取或写入。而注销则告诉 ImageMagick 该格式不再可用。分类方法查看图像的前几个字节,确定图像是否为预期的格式。读取器设置图像尺寸、色彩空间及其他属性,并将像素载入像素缓存。读取器返回单幅图像或图像序列(如果该格式支持每个文件多幅图像),或者如果发生错误,则返回异常和一个空图像。写入器则相反。它取得图像属性,卸载像素缓存,并按图像格式的要求将它们写出。
以下是一个示例自定义编解码器的代码列表。它以 MGK 图像格式读写图像,该格式只是一个 ID,后跟图像宽度和高度,再跟 RGB 像素值。
#include <MagickCore/studio.h>
#include <MagickCore/blob.h>
#include <MagickCore/cache.h>
#include <MagickCore/colorspace.h>
#include <MagickCore/exception.h>
#include <MagickCore/image.h>
#include <MagickCore/list.h>
#include <MagickCore/magick.h>
#include <MagickCore/memory_.h>
#include <MagickCore/monitor.h>
#include <MagickCore/pixel-accessor.h>
#include <MagickCore/string_.h>
#include <MagickCore/module.h>
#include "filter/blob-private.h"
#include "filter/exception-private.h"
#include "filter/image-private.h"
#include "filter/monitor-private.h"
#include "filter/quantum-private.h"
/*
Forward declarations.
*/
static MagickBooleanType
WriteMGKImage(const ImageInfo *,Image *,ExceptionInfo *);
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% I s M G K %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% IsMGK() returns MagickTrue if the image format type, identified by the
% magick string, is MGK.
%
% The format of the IsMGK method is:
%
% MagickBooleanType IsMGK(const unsigned char *magick,const size_t length)
%
% A description of each parameter follows:
%
% o magick: This string is generally the first few bytes of an image file
% or blob.
%
% o length: Specifies the length of the magick string.
%
*/
static MagickBooleanType IsMGK(const unsigned char *magick,const size_t length)
{
if (length < 7)
return(MagickFalse);
if (LocaleNCompare((char *) magick,"id=mgk",7) == 0)
return(MagickTrue);
return(MagickFalse);
}
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% R e a d M G K I m a g e %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% ReadMGKImage() reads a MGK image file and returns it. It allocates the
% memory necessary for the new Image structure and returns a pointer to the
% new image.
%
% The format of the ReadMGKImage method is:
%
% Image *ReadMGKImage(const ImageInfo *image_info,
% ExceptionInfo *exception)
%
% A description of each parameter follows:
%
% o image_info: the image info.
%
% o exception: return any errors or warnings in this structure.
%
*/
static Image *ReadMGKImage(const ImageInfo *image_info,ExceptionInfo *exception)
{
char
buffer[MaxTextExtent];
Image
*image;
long
y;
MagickBooleanType
status;
register long
x;
register Quantum
*q;
register unsigned char
*p;
ssize_t
count;
unsigned char
*pixels;
unsigned long
columns,
rows;
/*
Open image file.
*/
assert(image_info != (const ImageInfo *) NULL);
assert(image_info->signature == MagickCoreSignature);
if (image_info->debug != MagickFalse)
(void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",
image_info->filename);
assert(exception != (ExceptionInfo *) NULL);
assert(exception->signature == MagickCoreSignature);
image=AcquireImage(image_info,exception);
status=OpenBlob(image_info,image,ReadBinaryBlobMode,exception);
if (status == MagickFalse)
{
image=DestroyImageList(image);
return((Image *) NULL);
}
/*
Read MGK image.
*/
(void) ReadBlobString(image,buffer); /* read magic number */
if (IsMGK(buffer,7) == MagickFalse)
ThrowReaderException(CorruptImageError,"ImproperImageHeader");
(void) ReadBlobString(image,buffer);
count=(ssize_t) sscanf(buffer,"%lu %lu\n",&columns,&rows);
if (count <= 0)
ThrowReaderException(CorruptImageError,"ImproperImageHeader");
do
{
/*
Initialize image structure.
*/
image->columns=columns;
image->rows=rows;
image->depth=8;
if ((image_info->ping != MagickFalse) && (image_info->number_scenes != 0))
if (image->scene >= (image_info->scene+image_info->number_scenes-1))
break;
/*
Convert MGK raster image to pixel packets.
*/
if (SetImageExtent(image,image->columns,image->rows,exception) == MagickFalse)
return(DestroyImageList(image));
pixels=(unsigned char *) AcquireQuantumMemory((size_t) image->columns,
3UL*sizeof(*pixels));
if (pixels == (unsigned char *) NULL)
ThrowReaderException(ResourceLimitError,"MemoryAllocationFailed");
for (y=0; y < (long) image->rows; y++)
{
count=(ssize_t) ReadBlob(image,(size_t) (3*image->columns),pixels);
if (count != (ssize_t) (3*image->columns))
ThrowReaderException(CorruptImageError,"UnableToReadImageData");
p=pixels;
q=QueueAuthenticPixels(image,0,y,image->columns,1,exception);
if (q == (Quantum *) NULL)
break;
for (x=0; x < (long) image->columns; x++)
{
SetPixelRed(image,ScaleCharToQuantum(*p++),q);
SetPixelGreen(image,ScaleCharToQuantum(*p++),q);
SetPixelBlue(image,ScaleCharToQuantum(*p++),q);
q+=GetPixelChannels(image);
}
if (SyncAuthenticPixels(image,exception) == MagickFalse)
break;
if (image->previous == (Image *) NULL)
if ((image->progress_monitor != (MagickProgressMonitor) NULL) &&
(QuantumTick(y,image->rows) != MagickFalse))
{
status=image->progress_monitor(LoadImageTag,y,image->rows,
image->client_data);
if (status == MagickFalse)
break;
}
}
pixels=(unsigned char *) RelinquishMagickMemory(pixels);
if (EOFBlob(image) != MagickFalse)
{
ThrowFileException(exception,CorruptImageError,"UnexpectedEndOfFile",
image->filename);
break;
}
/*
Proceed to next image.
*/
if (image_info->number_scenes != 0)
if (image->scene >= (image_info->scene+image_info->number_scenes-1))
break;
*buffer='\0';
(void) ReadBlobString(image,buffer);
count=(ssize_t) sscanf(buffer,"%lu %lu\n",&columns,&rows);
if (count > 0)
{
/*
Allocate next image structure.
*/
AcquireNextImage(image_info,image,exception);
if (GetNextImageInList(image) == (Image *) NULL)
{
image=DestroyImageList(image);
return((Image *) NULL);
}
image=SyncNextImageInList(image);
if (image->progress_monitor != (MagickProgressMonitor) NULL)
{
status=SetImageProgress(image,LoadImageTag,TellBlob(image),
GetBlobSize(image));
if (status == MagickFalse)
break;
}
}
} while (count > 0);
(void) CloseBlob(image);
return(GetFirstImageInList(image));
}
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% R e g i s t e r M G K I m a g e %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% RegisterMGKImage() adds attributes for the MGK image format to
% the list of supported formats. The attributes include the image format
% tag, a method to read and/or write the format, whether the format
% supports the saving of more than one frame to the same file or blob,
% whether the format supports native in-memory I/O, and a brief
% description of the format.
%
% The format of the RegisterMGKImage method is:
%
% unsigned long RegisterMGKImage(void)
%
*/
ModuleExport unsigned long RegisterMGKImage(void)
{
MagickInfo
*entry;
entry=AcquireMagickInfo("MGK","MGK","MGK image");
entry->decoder=(DecodeImageHandler *) ReadMGKImage;
entry->encoder=(EncodeImageHandler *) WriteMGKImage;
entry->magick=(IsImageFormatHandler *) IsMGK;
(void) RegisterMagickInfo(entry);
return(MagickImageCoderSignature);
}
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% U n r e g i s t e r M G K I m a g e %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% UnregisterMGKImage() removes format registrations made by the
% MGK module from the list of supported formats.
%
% The format of the UnregisterMGKImage method is:
%
% UnregisterMGKImage(void)
%
*/
ModuleExport void UnregisterMGKImage(void)
{
(void) UnregisterMagickInfo("MGK");
}
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% W r i t e M G K I m a g e %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% WriteMGKImage() writes an image to a file in red, green, and blue MGK
% rasterfile format.
%
% The format of the WriteMGKImage method is:
%
% MagickBooleanType WriteMGKImage(const ImageInfo *image_info,
% Image *image)
%
% A description of each parameter follows.
%
% o image_info: the image info.
%
% o image: The image.
%
% o exception: return any errors or warnings in this structure.
%
*/
static MagickBooleanType WriteMGKImage(const ImageInfo *image_info,Image *image,
ExceptionInfo *exception)
{
char
buffer[MaxTextExtent];
long
y;
MagickBooleanType
status;
MagickOffsetType
scene;
register const Quantum
*p;
register long
x;
register unsigned char
*q;
unsigned char
*pixels;
/*
Open output image file.
*/
assert(image_info != (const ImageInfo *) NULL);
assert(image_info->signature == MagickCoreSignature);
assert(image != (Image *) NULL);
assert(image->signature == MagickCoreSignature);
if (image->debug != MagickFalse)
(void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
status=OpenBlob(image_info,image,WriteBinaryBlobMode,exception);
if (status == MagickFalse)
return(status);
scene=0;
do
{
/*
Allocate memory for pixels.
*/
if (image->colorspace != RGBColorspace)
(void) SetImageColorspace(image,RGBColorspace,exception);
pixels=(unsigned char *) AcquireQuantumMemory((size_t) image->columns,
3UL*sizeof(*pixels));
if (pixels == (unsigned char *) NULL)
ThrowWriterException(ResourceLimitError,"MemoryAllocationFailed");
/*
Initialize raster file header.
*/
(void) WriteBlobString(image,"id=mgk\n");
(void) FormatLocaleString(buffer,MaxTextExtent,"%lu %lu\n",image->columns,
image->rows);
(void) WriteBlobString(image,buffer);
for (y=0; y < (long) image->rows; y++)
{
p=GetVirtualPixels(image,0,y,image->columns,1,exception);
if (p == (const Quantum *) NULL)
break;
q=pixels;
for (x=0; x < (long) image->columns; x++)
{
*q++=ScaleQuantumToChar(GetPixelRed(image,p));
*q++=ScaleQuantumToChar(GetPixelGreen(image,p));
*q++=ScaleQuantumToChar(GetPixelBlue(image,p));
p+=GetPixelChannels(image);
}
(void) WriteBlob(image,(size_t) (q-pixels),pixels);
if (image->previous == (Image *) NULL)
if ((image->progress_monitor != (MagickProgressMonitor) NULL) &&
(QuantumTick(y,image->rows) != MagickFalse))
{
status=image->progress_monitor(SaveImageTag,y,image->rows,
image->client_data);
if (status == MagickFalse)
break;
}
}
pixels=(unsigned char *) RelinquishMagickMemory(pixels);
if (GetNextImageInList(image) == (Image *) NULL)
break;
image=SyncNextImageInList(image);
status=SetImageProgress(image,SaveImagesTag,scene,
GetImageListLength(image));
if (status == MagickFalse)
break;
scene++;
} while (image_info->adjoin != MagickFalse);
(void) CloseBlob(image);
return(MagickTrue);
}
要从命令行调用此自定义编解码器,使用这些命令:
magick logo: logo.mgk
display logo.mgk
我们提供 Magick Coder Kit 来帮助你开始编写自己的自定义编解码器。
在你构建之前,如果 ImageMagick 不在你默认的系统路径中,请设置 PKG_CONFIG_PATH 环境变量:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
自定义图像滤镜
ImageMagick 提供了一种便捷的机制来添加你自己的自定义图像处理算法。我们称之为图像滤镜,它们可以从命令行用 -process 选项调用,或从 MagickCore API 方法 ExecuteModuleProcess() 调用。
以下是一个示例自定义图像滤镜的代码列表。它计算若干统计量,例如像素亮度与饱和度的均值和标准差。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <math.h>
#include "MagickCore/studio.h"
#include "MagickCore/MagickCore.h"
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% a n a l y z e I m a g e %
% %
% %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% analyzeImage() computes the brightness and saturation mean, standard
% deviation, kurtosis and skewness and stores these values as attributes
% of the image.
%
% The format of the analyzeImage method is:
%
% size_t analyzeImage(Image *images,const int argc,char **argv,
% ExceptionInfo *exception)
%
% A description of each parameter follows:
%
% o image: the address of a structure of type Image.
%
% o argc: Specifies a pointer to an integer describing the number of
% elements in the argument vector.
%
% o argv: Specifies a pointer to a text array containing the command line
% arguments.
%
% o exception: return any errors or warnings in this structure.
%
*/
typedef struct _StatisticsInfo
{
double
area,
brightness,
mean,
standard_deviation,
sum[5],
kurtosis,
skewness;
} StatisticsInfo;
static inline int GetMagickNumberThreads(const Image *source,
const Image *destination,const size_t chunk,int multithreaded)
{
#define MagickMax(x,y) (((x) > (y)) ? (x) : (y))
#define MagickMin(x,y) (((x) < (y)) ? (x) : (y))
/*
Number of threads bounded by the amount of work and any thread resource
limit. The limit is 2 if the pixel cache type is not memory or
memory-mapped.
*/
if (multithreaded == 0)
return(1);
if (((GetImagePixelCacheType(source) != MemoryCache) &&
(GetImagePixelCacheType(source) != MapCache)) ||
((GetImagePixelCacheType(destination) != MemoryCache) &&
(GetImagePixelCacheType(destination) != MapCache)))
return(MagickMax(MagickMin(GetMagickResourceLimit(ThreadResource),2),1));
return(MagickMax(MagickMin((ssize_t) GetMagickResourceLimit(ThreadResource),
(ssize_t) (chunk)/64),1));
}
ModuleExport size_t analyzeImage(Image **images,const int argc,
const char **argv,ExceptionInfo *exception)
{
#define AnalyzeImageFilterTag "Filter/Analyze"
#define magick_number_threads(source,destination,chunk,multithreaded) \
num_threads(GetMagickNumberThreads(source,destination,chunk,multithreaded))
char
text[MagickPathExtent];
Image
*image;
MagickBooleanType
status;
MagickOffsetType
progress;
assert(images != (Image **) NULL);
assert(*images != (Image *) NULL);
assert((*images)->signature == MagickCoreSignature);
(void) argc;
(void) argv;
status=MagickTrue;
progress=0;
for (image=(*images); image != (Image *) NULL; image=GetNextImageInList(image))
{
CacheView
*image_view;
double
area;
ssize_t
y;
StatisticsInfo
brightness,
saturation;
if (status == MagickFalse)
continue;
(void) memset(&brightness,0,sizeof(brightness));
(void) memset(&saturation,0,sizeof(saturation));
status=MagickTrue;
image_view=AcquireVirtualCacheView(image,exception);
#if defined(MAGICKCORE_OPENMP_SUPPORT)
#pragma omp parallel for schedule(static) \
shared(progress,status,brightness,saturation) \
magick_number_threads(image,image,image->rows,1)
#endif
for (y=0; y < (ssize_t) image->rows; y++)
{
const Quantum
*p;
ssize_t
i,
x;
StatisticsInfo
local_brightness,
local_saturation;
if (status == MagickFalse)
continue;
p=GetCacheViewVirtualPixels(image_view,0,y,image->columns,1,exception);
if (p == (const Quantum *) NULL)
{
status=MagickFalse;
continue;
}
(void) memset(&local_brightness,0,sizeof(local_brightness));
(void) memset(&local_saturation,0,sizeof(local_saturation));
for (x=0; x < (ssize_t) image->columns; x++)
{
double
b,
h,
s;
ConvertRGBToHSL(GetPixelRed(image,p),GetPixelGreen(image,p),
GetPixelBlue(image,p),&h,&s,&b);
b*=QuantumRange;
for (i=1; i <= 4; i++)
local_brightness.sum[i]+=pow(b,(double) i);
s*=QuantumRange;
for (i=1; i <= 4; i++)
local_saturation.sum[i]+=pow(s,(double) i);
p+=GetPixelChannels(image);
}
#if defined(MAGICKCORE_OPENMP_SUPPORT)
#pragma omp critical (analyzeImage)
#endif
for (i=1; i <= 4; i++)
{
brightness.sum[i]+=local_brightness.sum[i];
saturation.sum[i]+=local_saturation.sum[i];
}
}
image_view=DestroyCacheView(image_view);
area=(double) image->columns*image->rows;
brightness.mean=brightness.sum[1]/area;
(void) FormatLocaleString(text,MagickPathExtent,"%g",brightness.mean);
(void) SetImageProperty(image,"filter:brightness:mean",text,exception);
brightness.standard_deviation=sqrt(brightness.sum[2]/area-
(brightness.sum[1]/area*brightness.sum[1]/area));
(void) FormatLocaleString(text,MagickPathExtent,"%g",
brightness.standard_deviation);
(void) SetImageProperty(image,"filter:brightness:standard-deviation",text,
exception);
if (fabs(brightness.standard_deviation) >= MagickEpsilon)
brightness.kurtosis=(brightness.sum[4]/area-4.0*brightness.mean*
brightness.sum[3]/area+6.0*brightness.mean*brightness.mean*
brightness.sum[2]/area-3.0*brightness.mean*brightness.mean*
brightness.mean*brightness.mean)/(brightness.standard_deviation*
brightness.standard_deviation*brightness.standard_deviation*
brightness.standard_deviation)-3.0;
(void) FormatLocaleString(text,MagickPathExtent,"%g",brightness.kurtosis);
(void) SetImageProperty(image,"filter:brightness:kurtosis",text,exception);
if (brightness.standard_deviation != 0)
brightness.skewness=(brightness.sum[3]/area-3.0*brightness.mean*
brightness.sum[2]/area+2.0*brightness.mean*brightness.mean*
brightness.mean)/(brightness.standard_deviation*
brightness.standard_deviation*brightness.standard_deviation);
(void) FormatLocaleString(text,MagickPathExtent,"%g",brightness.skewness);
(void) SetImageProperty(image,"filter:brightness:skewness",text,exception);
saturation.mean=saturation.sum[1]/area;
(void) FormatLocaleString(text,MagickPathExtent,"%g",saturation.mean);
(void) SetImageProperty(image,"filter:saturation:mean",text,exception);
saturation.standard_deviation=sqrt(saturation.sum[2]/area-
(saturation.sum[1]/area*saturation.sum[1]/area));
(void) FormatLocaleString(text,MagickPathExtent,"%g",
saturation.standard_deviation);
(void) SetImageProperty(image,"filter:saturation:standard-deviation",text,
exception);
if (fabs(saturation.standard_deviation) >= MagickEpsilon)
saturation.kurtosis=(saturation.sum[4]/area-4.0*saturation.mean*
saturation.sum[3]/area+6.0*saturation.mean*saturation.mean*
saturation.sum[2]/area-3.0*saturation.mean*saturation.mean*
saturation.mean*saturation.mean)/(saturation.standard_deviation*
saturation.standard_deviation*saturation.standard_deviation*
saturation.standard_deviation)-3.0;
(void) FormatLocaleString(text,MagickPathExtent,"%g",saturation.kurtosis);
(void) SetImageProperty(image,"filter:saturation:kurtosis",text,exception);
if (fabs(saturation.standard_deviation) >= MagickEpsilon)
saturation.skewness=(saturation.sum[3]/area-3.0*saturation.mean*
saturation.sum[2]/area+2.0*saturation.mean*saturation.mean*
saturation.mean)/(saturation.standard_deviation*
saturation.standard_deviation*saturation.standard_deviation);
(void) FormatLocaleString(text,MagickPathExtent,"%g",saturation.skewness);
(void) SetImageProperty(image,"filter:saturation:skewness",text,exception);
if (image->progress_monitor != (MagickProgressMonitor) NULL)
{
MagickBooleanType
proceed;
#if defined(MAGICKCORE_OPENMP_SUPPORT)
#pragma omp atomic
#endif
progress++;
proceed=SetImageProgress(image,AnalyzeImageFilterTag,progress,
GetImageListLength(image));
if (proceed == MagickFalse)
status=MagickFalse;
}
}
return(MagickImageFilterSignature);
}
要从命令行调用此自定义滤镜,使用此命令:
magick logo: -process \"analyze\" -verbose info:
Image: logo:
Format: LOGO (ImageMagick Logo)
Class: PseudoClass
Geometry: 640x480
...
filter:brightness:kurtosis: 3.97886
filter:brightness:mean: 58901.3
filter:brightness:skewness: -2.30827
filter:brightness:standard-deviation: 16179.8
filter:saturation:kurtosis: 6.59719
filter:saturation:mean: 5321.05
filter:saturation:skewness: 2.75679
filter:saturation:standard-deviation: 14484.7
我们提供 Magick Filter Kit 来帮助你开始编写自己的自定义图像滤镜。
在你构建之前,如果 ImageMagick 不在你默认的系统路径中,请设置 PKG_CONFIG_PATH 环境变量:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig