像素缓存 • 图像属性和配置文件 • 多光谱图像 • 大型图像支持 • 像素流 • 执行线程 • 异构分布式处理 • 自定义图像编码器 • 自定义图像过滤器
奥兹的公民对他们无所不能的巫师恩人感到非常满意。他们接受了他的智慧和仁慈,从不质疑他力量的来源、目的和方式。就像奥兹的公民一样,如果你觉得 ImageMagick 可以帮助你转换、编辑或合成图像,而无需了解幕后发生的事情,请随意跳过本节。但是,如果你想了解更多关于 ImageMagick 背后的软件和算法,请继续阅读。为了充分利用本讨论,你应该熟悉图像术语并熟悉计算机编程。
架构概述
图像通常由一个矩形的像素区域和元数据组成。为了有效地转换、编辑或合成图像,我们需要方便地访问区域内(有时也包括区域外)的任何像素。在图像序列的情况下,我们需要访问序列中任何图像的任何区域的任何像素。但是,有数百种图像格式,例如 JPEG、TIFF、PNG、GIF 等,这使得难以按需访问像素。在这些格式中,我们发现了以下方面的差异
- 色彩空间(例如 sRGB、线性 RGB、线性 GRAY、CMYK、YUV、Lab 等)
- 位深度(例如 1、4、8、12、16 等)
- 存储格式(例如 无符号、有符号、浮点、双精度等)
- 压缩(例如 无压缩、RLE、Zip、BZip 等)
- 方向(即 从上到下、从右到左等)
- 布局(例如 原始、与操作码交织等)
此外,一些图像像素可能需要衰减,一些格式允许多个帧,一些格式包含必须首先光栅化(从矢量转换为像素)的矢量图形。
图像处理算法的有效实现可能需要我们获取或设置
- 一次一个像素(例如 位置 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 和其他处理器组成的异构平台上协同执行带来的加速
- 与每个通道相关的特征,以指定像素通道是复制、更新还是混合
- 定义哪些像素有资格更新的掩码
- 对用户有益但其他方面不受 ImageMagick 图像处理算法影响的额外通道
鉴于各种图像格式和图像处理要求,我们实现了 ImageMagick 像素缓存,以便按需方便地顺序或并行访问图像区域内的任何像素(即 真实像素)以及序列中任何图像的任何像素。此外,像素缓存允许访问图像边界之外的像素(即 虚拟像素)。
除了像素外,图像还有大量 图像属性和配置文件。属性包括众所周知的属性,如宽度、高度、深度和色彩空间。图像可能具有可选属性,这些属性可能包括图像作者、注释、创建日期等。一些图像还包括用于色彩管理的配置文件,或 EXIF、IPTC、8BIM 或 XMP 信息配置文件。ImageMagick 提供命令行选项和编程方法来获取、设置或查看图像属性或配置文件,或应用配置文件。
ImageMagick 由近 50 万行 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.1-38 2024-05-05 Q16 https://imagemagick.org.cn
如你所见,像素缓存的便利性有时会带来存储方面的权衡(例如,将 1 位单色图像存储为 16 位是一种浪费),以及速度方面的权衡(即,将整个图像存储在内存中通常比一次访问一行像素慢)。在大多数情况下,像素缓存的好处通常超过任何缺点。
访问像素缓存
一旦像素缓存与图像相关联,你通常想要获取、更新或将像素放入其中。我们将图像区域内的像素称为 真实像素,将区域外的像素称为 虚拟像素。使用以下方法访问缓存中的像素
- 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() 确保任何更新的像素都被推送到像素缓存中。
你可以将任意内容与每个像素相关联,称为元内容。使用 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() 要求你请求的区域在图像区域的范围内。对于 640x480 的图像,你可以获取第 479 行的 640 个像素的扫描线,但是如果你请求第 480 行的扫描线,则会返回一个异常(行从 0 开始编号)。GetVirtualPixels() 没有这个约束。例如,
p=GetVirtualPixels(source,-3,-3,source->columns+3,6,exception);
会毫不犹豫地给你你请求的像素,即使其中一些不在图像区域的范围内。
虚拟像素
有许多图像处理算法需要围绕感兴趣像素的像素邻域。该算法通常包括一个关于如何处理图像边界周围的像素的注意事项,这些像素被称为边缘像素。使用虚拟像素,你无需担心特殊的边缘处理,只需选择最适合你的算法的虚拟像素方法即可。
对虚拟像素的访问由 MagickCore API 中的 SetImageVirtualPixelMethod() 方法或命令行中的 -virtual-pixel 选项控制。这些方法包括
背景 | 图像周围的区域是背景色 |
黑色 | 图像周围的区域是黑色 |
棋盘格 | 图像和背景色交替的方块 |
抖动 | 非随机 32x32 抖动模式 |
边缘 | 将边缘像素向无穷大方向扩展(默认) |
灰色 | 图像周围的区域是灰色 |
水平平铺 | 水平平铺图像,背景色在上方/下方 |
水平平铺边缘 | 水平平铺图像并复制侧面边缘像素 |
镜像 | 镜像平铺图像 |
随机 | 从图像中选择一个随机像素 |
平铺 | 平铺图像 |
透明 | 图像周围的区域为透明黑色 |
垂直平铺 | 垂直平铺图像,两侧为背景色 |
垂直平铺边缘 | 垂直平铺图像并复制侧边边缘像素 |
白色 | 图像周围的区域为白色 |
缓存存储和资源需求
回想一下,ImageMagick 像素缓存这种简单而优雅的设计在存储和处理速度方面是有代价的。像素缓存存储需求随着图像面积和像素分量的比特深度而变化。例如,如果我们有一个 640 x 480 的图像,并且我们使用 ImageMagick 的非 HDRI Q16 版本,像素缓存将消耗图像 宽度 * 高度 * 比特深度 / 8 * 通道 字节,或者大约 2.3 兆字节(即 640 * 480 * 2 * 4)。不算太差,但如果你的图像有 25000 x 25000 像素呢?像素缓存需要大约 4.7 吉字节的存储空间。哎哟。ImageMagick 通过将大型图像缓存到磁盘而不是内存来解决可能的巨大存储需求。通常情况下,像素缓存使用堆内存存储在内存中。如果堆内存耗尽,我们会在磁盘上创建像素缓存并尝试将其内存映射。如果内存映射内存耗尽,我们只使用标准磁盘 I/O。磁盘存储空间充足且廉价,但也很慢——比访问内存中的像素慢 1000 倍以上。如果我们对基于磁盘的缓存进行内存映射,我们可以获得一些速度提升,最多 5 倍。像素缓存管理器与操作系统协商,自动地 决定这些存储决策。但是,您可以使用 缓存资源限制 影响像素缓存管理器如何分配像素缓存。限制包括
宽度 | 图像的最大宽度。超过此限制将抛出异常,操作将中止。 |
高度 | 图像的最大高度。超过此限制将抛出异常,操作将中止。 |
面积 | 可以驻留在像素缓存内存中的任何一个图像的最大字节数。如果超过此限制,图像将自动地 缓存到磁盘,并可选地进行内存映射。 |
内存 | 从堆中分配给像素缓存的最大字节数。 |
映射 | 分配给像素缓存的最大内存映射字节数。 |
磁盘 | 像素缓存允许使用的最大磁盘空间字节数。如果超过此限制,将抛出致命异常,所有处理将停止。 |
文件 | 打开的像素缓存文件的最大数量。当超过此限制时,任何随后缓存到磁盘的像素都会被关闭,并在需要时重新打开。这种行为允许在磁盘上同时访问大量图像,而不会降低速度,因为它减少了像素缓存打开/关闭系统调用的数量。 |
线程 | 允许并行运行的最大线程数。您的系统可以选择小于此值的线程数。ImageMagick 默认选择最佳线程数,通常是主机上的核心数。将此值设置为 1,所有并行区域将由一个线程执行。 |
时间 | 允许进程执行的最大秒数。超过此限制将抛出异常,处理将停止。 |
注意,这些限制与 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 的在线 Web 界面 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.cn/script/security-policy.php,
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 开始,您可以阻止使用任何委托或所有委托(将模式设置为“*”。注意,在此版本之前,使用“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]
此命令使用内存中的像素缓存。徽标消耗了 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() 请求图像边界之外的像素,但是,在图像区域范围内请求像素效率更高。
虽然您可以使用适当的资源限制将像素缓存强制到磁盘,但磁盘访问速度可能会比内存访问速度慢 1000 倍以上。为了快速有效地访问像素缓存,请尝试将像素缓存保留在堆内存中。
ImageMagick 的 Q16 版本允许您在不缩放的情况下读取和写入 16 位图像,但像素缓存消耗的资源是 Q8 版本的两倍。如果您的系统内存或磁盘资源有限,请考虑使用 ImageMagick 的 Q8 版本。此外,Q8 版本的执行速度通常比 Q16 版本快。
大多数图像格式和算法将像素值限制在从 0 到某个最大值的固定范围内,例如,ImageMagick 的 Q16 版本允许强度值从 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() 方法支持工件。工件是隐形属性,不会导出到图像格式(例如 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 个是元通道。请注意,如果您使用配置脚本 --enable-64bit-channel-masks 选项构建 ImageMagick,则可以处理多达 52 个元通道的 62 个波段多光谱图像。
如果您有一个目前未被图像格式支持的用例,请将其发布到 讨论论坛。很有可能,我们将在 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 像素。在这里,我们将一个图像调整为 25 万像素的正方形
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 的许多内部算法都是多线程的,以利用多核处理器芯片提供的加速功能。但是,您可以在您的执行线程中使用 ImageMagick 算法,除了 MagickCore 的 GetVirtualPixels()、GetAuthenticPixels()、QueueAuthenticPixels() 或 SyncAuthenticPixels() 像素缓存方法之外。这些方法仅供一个执行线程使用,OpenMP 并行节除外。要使用多个执行线程访问像素缓存,请使用缓存视图。例如,我们在 CompositeImage() 方法中执行此操作。假设我们希望在每个执行线程中将一个源图像合成到不同的目标图像上。如果我们使用 GetVirtualPixels(),则结果将是不可预测的,因为多个线程可能会同时请求像素缓存的不同区域。相反,我们使用 GetCacheViewVirtualPixels(),它为每个执行线程创建一个唯一的视图,确保我们的程序无论调用多少线程都能正常运行。其他程序接口(如 MagickWand API)是完全线程安全的,因此执行线程没有特殊的注意事项。
以下是一个 MagickCore 代码片段,它利用了 OpenMP 编程范式中的执行线程
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 中实现的相同 S 形对比度算法。
在大多数情况下,默认线程数设置为系统上处理器核心的数量,以实现最佳性能。但是,如果您的系统启用了超线程,或者您在虚拟主机上运行并且只有部分处理器可用于您的服务器实例,则您可以通过设置线程 策略 或 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 支持。将 --disable-openmp 选项添加到配置脚本命令行,然后重新构建并重新安装 ImageMagick。
您可以使用 tcmalloc 内存分配库来减少锁争用,从而进一步提高性能。要启用,请在构建 ImageMagick 时将 --with-tcmalloc 添加到 configure 命令行。
线程性能
在并行环境中预测行为可能很困难。性能可能取决于许多因素,包括编译器、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 包含对 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 超时检测和恢复)的问题。TDR 的作用是通过使用执行时间阈值来检测导致 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 编码器套件 来帮助您开始编写自己的自定义编码器。
在构建之前,如果 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 滤镜套件 来帮助您开始编写自己的自定义图像滤镜。
在构建之前,如果 ImageMagick 不在您的默认系统路径中,请设置 PKG_CONFIG_PATH 环境变量
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig