⚠️ Este es un sitio de traducción no oficial, sin relación con ImageMagick Studio LLC. Para información autorizada, consulte la página original (https://imagemagick.org/architecture/).

La caché de píxeles • Propiedades y perfiles de imagen • Imágenes multiespectrales • Soporte de imágenes grandes • Streaming de píxeles • Hilos de ejecución • Habilitar la limitación dinámica para un rendimiento optimizado • Procesamiento distribuido heterogéneo • Codificadores de imagen personalizados • Filtros de imagen personalizados

Los habitantes de Oz estaban bastante satisfechos con su benefactor, el todopoderoso Mago. Aceptaban su sabiduría y benevolencia sin cuestionar jamás el quién, el porqué ni el dónde de su poder. Como los habitantes de Oz, si te sientes cómodo con que ImageMagick pueda ayudarte a convertir, editar o componer tus imágenes sin saber qué ocurre detrás de la cortina, siéntete libre de saltarte esta sección. Sin embargo, si quieres saber más sobre el software y los algoritmos que hay detrás de ImageMagick, sigue leyendo. Para aprovechar plenamente esta discusión, deberías estar cómodo con la nomenclatura de imágenes y familiarizado con la programación de computadoras.

Visión general de la arquitectura

Una imagen consiste normalmente en una región rectangular de píxeles y metadatos. Para convertir, editar o componer una imagen de manera eficiente, necesitamos acceso conveniente a cualquier píxel en cualquier lugar dentro de la región (y a veces fuera de la región). Y en el caso de una secuencia de imágenes, necesitamos acceso a cualquier píxel de cualquier región de cualquier imagen de la secuencia. Sin embargo, existen cientos de formatos de imagen como JPEG, TIFF, PNG, GIF, etc., que dificultan el acceso a los píxeles bajo demanda. Dentro de estos formatos encontramos diferencias en:

  • espacio de color (p. ej. sRGB, RGB lineal, GRAY lineal, CMYK, YUV, Lab, etc.)
  • profundidad de bits (p. ej. 1, 4, 8, 12, 16, etc.)
  • formato de almacenamiento (p. ej. sin signo, con signo, float, double, etc.)
  • compresión (p. ej. sin comprimir, RLE, Zip, BZip, etc.)
  • orientación (es decir, de arriba abajo, de derecha a izquierda, etc.),
  • disposición (p. ej. raw, intercalado con opcodes, etc.)

Además, algunos píxeles de imagen pueden requerir atenuación, algunos formatos permiten más de un fotograma y algunos formatos contienen gráficos vectoriales que primero deben rasterizarse (convertirse de vector a píxeles).

Una implementación eficiente de un algoritmo de procesamiento de imágenes puede requerir que obtengamos o establezcamos:

  • un píxel a la vez (p. ej. el píxel en la ubicación 10,3)
  • una sola línea de escaneo (p. ej. todos los píxeles de la fila 4)
  • unas pocas líneas de escaneo a la vez (p. ej. las filas de píxeles 4-7)
  • una sola columna o columnas de píxeles (p. ej. todos los píxeles de la columna 11)
  • una región arbitraria de píxeles de la imagen (p. ej. los píxeles definidos de 10,7 a 10,19)
  • un píxel en orden aleatorio (p. ej. el píxel en 14,15 y 640,480)
  • píxeles de dos imágenes diferentes (p. ej. el píxel en 5,1 de la imagen 1 y el píxel en 5,1 de la imagen 2)
  • píxeles fuera de los límites de la imagen (p. ej. el píxel en -1,-3)
  • un componente de píxel sin signo (65311) o en representación de punto flotante (p. ej. 0.17836)
  • un píxel de alto rango dinámico que puede incluir valores negativos (p. ej. -0.0072973525628) así como valores que exceden la profundidad cuántica (p. ej. 65931)
  • uno o más píxeles simultáneamente en diferentes hilos de ejecución
  • todos los píxeles en memoria para aprovechar las aceleraciones que ofrece la ejecución conjunta en plataformas heterogéneas compuestas por CPU, GPU y otros procesadores
  • rasgos asociados a cada canal para especificar si el canal del píxel se copia, actualiza o mezcla
  • máscaras que definen qué píxeles son aptos para ser actualizados
  • canales adicionales que benefician al usuario pero que, por lo demás, permanecen intactos por los algoritmos de procesamiento de imágenes de ImageMagick

Dada la variedad de formatos de imagen y de requisitos de procesamiento de imágenes, implementamos la caché de píxeles de ImageMagick para proporcionar acceso secuencial o paralelo conveniente a cualquier píxel bajo demanda en cualquier lugar dentro de la región de la imagen (es decir, píxeles auténticos) y de cualquier imagen de una secuencia. Además, la caché de píxeles permite el acceso a píxeles fuera de los límites definidos por la imagen (es decir, píxeles virtuales).

Además de los píxeles, las imágenes tienen una multitud de propiedades y perfiles de imagen. Las propiedades incluyen los atributos bien conocidos como el ancho, el alto, la profundidad y el espacio de color. Una imagen puede tener propiedades opcionales que podrían incluir el autor de la imagen, un comentario, una fecha de creación y otras. Algunas imágenes también incluyen perfiles para la gestión del color, o perfiles informativos EXIF, IPTC, 8BIM o XMP. ImageMagick proporciona opciones de línea de comandos y métodos de programación para obtener, establecer o ver las propiedades o perfiles de imagen, o para aplicar perfiles.

ImageMagick consta de casi medio millón de líneas de código C y, opcionalmente, depende de varios millones de líneas de código en bibliotecas dependientes (p. ej. las bibliotecas JPEG, PNG, TIFF). Dado esto, uno podría esperar un documento de arquitectura enorme. Sin embargo, la gran mayoría del procesamiento de imágenes consiste simplemente en acceder a los píxeles y a sus metadatos, y nuestra implementación sencilla, elegante y eficiente lo hace fácil para el desarrollador de ImageMagick. Discutimos la implementación de la caché de píxeles y la obtención y el establecimiento de propiedades y perfiles de imagen en las próximas secciones. A continuación, discutimos el uso de ImageMagick dentro de un hilo de ejecución. En las secciones finales, discutimos los codificadores de imagen para leer o escribir un formato de imagen en particular, seguido de unas palabras sobre la creación de un filtro para acceder a los píxeles o actualizarlos según tus requisitos personalizados.

La caché de píxeles

La caché de píxeles de ImageMagick es un repositorio de píxeles de imagen con hasta 64 canales. Los canales se almacenan de forma contigua con la profundidad especificada cuando se compiló ImageMagick. Las profundidades de canal son de 8 bits por componente de píxel para la versión Q8 de ImageMagick, 16 bits por componente de píxel para la versión Q16 y 32 bits por componente de píxel para la versión Q32. De forma predeterminada, los componentes de píxel son cantidades de alto rango dinámico de punto flotante de 32 bits. Los canales pueden contener cualquier valor, pero normalmente contienen las intensidades de rojo, verde, azul y alfa, o las intensidades de cian, magenta, amarillo, negro y alfa. Un canal podría contener los índices del mapa de colores para imágenes con mapa de colores o el canal negro para imágenes CMYK. El almacenamiento de la caché de píxeles puede ser memoria del heap, memoria mapeada respaldada por disco o disco. La caché de píxeles cuenta con conteo de referencias. Solo se copian las propiedades de la caché cuando se clona la caché. Los píxeles de la caché se copian posteriormente solo cuando señalas tu intención de actualizar alguno de los píxeles.

Crear la caché de píxeles

La caché de píxeles se asocia con una imagen cuando esta se crea y se inicializa cuando intentas obtener o colocar píxeles. Aquí hay tres métodos comunes para asociar una caché de píxeles con una imagen:

Crear un lienzo de imagen inicializado con el color de fondo:

image=AllocateImage(image_info);
if (SetImageExtent(image,640,480) == MagickFalse)
  { /* an exception was thrown */ }
(void) QueryMagickColor("red",&image->background_color,&image->exception);
SetImageBackgroundColor(image);

Crear una imagen a partir de una imagen JPEG en disco:

(void) strcpy(image_info->filename,"image.jpg"):
image=ReadImage(image_info,exception);
if (image == (Image *) NULL)
  { /* an exception was thrown */ }

Crear una imagen a partir de una imagen en memoria:

image=BlobToImage(blob_info,blob,extent,exception);
if (image == (Image *) NULL)
  { /* an exception was thrown */ }

En nuestra discusión de la caché de píxeles, usamos la API de MagickCore para ilustrar nuestros puntos; sin embargo, los principios son los mismos para otras interfaces de programación de ImageMagick.

Cuando se inicializa la caché de píxeles, los píxeles se escalan desde cualquiera que sea la profundidad de bits de la que provienen a la requerida por la caché de píxeles. Por ejemplo, una imagen PBM monocromática de 1 canal y 1 bit se escala a una imagen gris de 8 bits, si estás usando la versión Q8 de ImageMagick, y a RGBA de 16 bits para la versión Q16. Puedes determinar qué versión tienes con la opción -version:

$ identify -version
Version: ImageMagick 7.1.2-25 2026-06-04 Q16-HDRI https://imagemagick.org

Como puedes ver, la comodidad de la caché de píxeles a veces conlleva un compromiso en almacenamiento (p. ej. almacenar una imagen monocromática de 1 bit como 16 bits es un desperdicio) y velocidad (es decir, almacenar la imagen entera en memoria es generalmente más lento que acceder a una línea de escaneo de píxeles a la vez). En la mayoría de los casos, los beneficios de la caché de píxeles suelen superar cualquier desventaja.

Acceder a la caché de píxeles

Una vez que la caché de píxeles está asociada con una imagen, normalmente querrás obtener, actualizar o colocar píxeles en ella. Nos referimos a los píxeles dentro de la región de la imagen como píxeles auténticos y a los de fuera de la región como píxeles virtuales. Usa estos métodos para acceder a los píxeles de la caché:

  • GetVirtualPixels(): obtiene píxeles que no pretendes modificar o píxeles que se encuentran fuera de la región de la imagen (p. ej. el píxel @ -1,-3)
  • GetAuthenticPixels(): obtiene píxeles que pretendes modificar
  • QueueAuthenticPixels(): pone en cola los píxeles que pretendes establecer
  • SyncAuthenticPixels(): actualiza la caché de píxeles con cualquier píxel modificado

Aquí hay un fragmento de código típico de MagickCore para manipular píxeles en la caché de píxeles. En nuestro ejemplo, copiamos píxeles de la imagen de entrada a la imagen de salida y disminuimos la intensidad en un 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 */ }

Cuando creamos por primera vez la imagen de destino clonando la imagen de origen, los píxeles de la caché de píxeles no se copian. Solo se copian cuando señalas tu intención de modificar o establecer la caché de píxeles llamando a GetAuthenticPixels() o QueueAuthenticPixels(). Usa QueueAuthenticPixels() si quieres establecer nuevos valores de píxel en lugar de actualizar los existentes. Podrías usar GetAuthenticPixels() para establecer valores de píxel, pero es ligeramente más eficiente usar QueueAuthenticPixels() en su lugar. Por último, usa SyncAuthenticPixels() para asegurarte de que cualquier píxel actualizado se envíe a la caché de píxeles.

Puedes asociar contenido arbitrario con cada píxel, llamado contenido meta. Usa GetVirtualMetacontent() (para leer el contenido) o GetAuthenticMetacontent() (para actualizar el contenido) para acceder a este contenido. Por ejemplo, para imprimir el metacontenido, usa:

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 */

El gestor de la caché de píxeles decide si te da acceso directo o indirecto a los píxeles de la imagen. En algunos casos, los píxeles se preparan en un búfer intermedio, y por eso debes llamar a SyncAuthenticPixels() para asegurarte de que este búfer se envíe a la caché de píxeles y garantizar que los píxeles correspondientes de la caché se actualicen. Por esta razón, recomendamos que solo leas o actualices una línea de escaneo o unas pocas líneas de escaneo de píxeles a la vez. Sin embargo, puedes obtener cualquier región rectangular de píxeles que desees. GetAuthenticPixels() requiere que la región que solicitas esté dentro de los límites del área de la imagen. Para una imagen de 640 por 480, puedes obtener una línea de escaneo de 640 píxeles en la fila 479, pero si pides una línea de escaneo en la fila 480, se devuelve una excepción (las filas se numeran a partir de 0). GetVirtualPixels() no tiene esta restricción. Por ejemplo,

p=GetVirtualPixels(source,-3,-3,source->columns+3,6,exception);

te da los píxeles que pediste sin quejas, aunque algunos no estén dentro de los confines de la región de la imagen.

Píxeles virtuales

Hay una multitud de algoritmos de procesamiento de imágenes que requieren un vecindario de píxeles alrededor de un píxel de interés. El algoritmo normalmente incluye una advertencia sobre cómo manejar los píxeles alrededor de los límites de la imagen, conocidos como píxeles de borde. Con los píxeles virtuales, no necesitas preocuparte por el procesamiento especial de los bordes, salvo elegir qué método de píxel virtual es el más apropiado para tu algoritmo.

El acceso a los píxeles virtuales se controla mediante el método SetImageVirtualPixelMethod() de la API de MagickCore o la opción -virtual-pixel de la línea de comandos. Los métodos incluyen:

background el área que rodea la imagen es el color de fondo
black el área que rodea la imagen es negra
checker-tile cuadrados alternos con la imagen y el color de fondo
dither patrón con tramado no aleatorio de 32x32
edge extiende el píxel de borde hacia el infinito (predeterminado)
gray el área que rodea la imagen es gris
horizontal-tile coloca la imagen en mosaico horizontalmente, color de fondo arriba/abajo
horizontal-tile-edge coloca la imagen en mosaico horizontalmente y replica los píxeles del borde lateral
mirror coloca la imagen en mosaico en espejo
random elige un píxel aleatorio de la imagen
tile coloca la imagen en mosaico
transparent el área que rodea la imagen es negrura transparente
vertical-tile coloca la imagen en mosaico verticalmente, los lados son el color de fondo
vertical-tile-edge coloca la imagen en mosaico verticalmente y replica los píxeles del borde lateral
white el área que rodea la imagen es blanca

Almacenamiento de la caché y requisitos de recursos

Recuerda que este diseño sencillo y elegante de la caché de píxeles de ImageMagick tiene un coste en términos de almacenamiento y velocidad de procesamiento. Los requisitos de almacenamiento de la caché de píxeles escalan con el área de la imagen y la profundidad de bits de los componentes de píxel. Por ejemplo, si tenemos una imagen de 640 por 480 y estamos usando la versión Q16 no-HDRI de ImageMagick, la caché de píxeles consume ancho de la imagen * alto * profundidad de bits / 8 * canales bytes, o aproximadamente 2,3 mebibytes (es decir, 640 * 480 * 2 * 4). No está mal, pero ¿qué pasa si tu imagen es de 25000 por 25000 píxeles? La caché de píxeles requiere aproximadamente 4,7 gibibytes de almacenamiento. Ay. ImageMagick tiene en cuenta los posibles requisitos de almacenamiento enormes almacenando en caché las imágenes grandes en disco en lugar de en memoria. Normalmente, la caché de píxeles se almacena en memoria usando memoria del heap. Si la memoria del heap se agota, creamos la caché de píxeles en disco e intentamos mapearla en memoria. Si la memoria de memoria mapeada se agota, simplemente usamos E/S de disco estándar. El almacenamiento en disco es abundante y barato, pero también es muy lento, hasta 1000 veces más lento que acceder a los píxeles en memoria. Podemos obtener algunas mejoras de velocidad, hasta 5 veces, si mapeamos en memoria la caché basada en disco. Estas decisiones sobre el almacenamiento las toma automáticamente el gestor de la caché de píxeles negociando con el sistema operativo. Sin embargo, puedes influir en cómo el gestor de la caché de píxeles asigna la caché de píxeles con límites de recursos de la caché. Los límites incluyen:

width ancho máximo de una imagen. Si se supera este límite, se lanza una excepción y la operación se interrumpe.
height alto máximo de una imagen. Si se supera este límite, se lanza una excepción y la operación se interrumpe.
area área máxima en bytes de cualquier imagen que pueda residir en la memoria de la caché de píxeles. Si se supera este límite, la imagen se almacena en caché en disco automáticamente y, opcionalmente, se mapea en memoria.
memory cantidad máxima de memoria en bytes que se asignará para la caché de píxeles desde el heap.
map cantidad máxima de memoria mapeada en bytes que se asignará para la caché de píxeles.
disk cantidad máxima de espacio en disco en bytes permitido para uso de la caché de píxeles. Si se supera este límite, se lanza una excepción fatal y todo el procesamiento se detiene.
files número máximo de archivos de caché de píxeles abiertos. Cuando se supera este límite, cualquier píxel posterior almacenado en caché en disco se cierra y se vuelve a abrir bajo demanda. Este comportamiento permite acceder simultáneamente a un gran número de imágenes en disco sin penalización de velocidad, reduciendo el número de llamadas al sistema de apertura/cierre de la caché de píxeles.
thread número máximo de hilos que se permite ejecutar en paralelo. Tu sistema puede elegir un número de hilos menor que este valor. ImageMagick elige un número óptimo de hilos de forma predeterminada, que normalmente es el número de núcleos de tu host. Establece este valor en 1 y todas las regiones paralelas se ejecutarán con un solo hilo.
time número máximo de segundos que se permite ejecutar al proceso. Si se supera este límite, se lanza una excepción y el procesamiento se detiene.

Ten en cuenta que estos límites se refieren a la caché de píxeles de ImageMagick. Ciertos algoritmos dentro de ImageMagick no respetan estos límites, ni tampoco ninguna de las bibliotecas delegadas externas (p. ej. JPEG, TIFF, etc.).

Para determinar la configuración actual de estos límites, usa este comando:

-> 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

Puedes establecer estos límites ya sea como una política de seguridad (véase policy.xml), con una variable de entorno, con la opción de línea de comandos -limit, o con el método de la API de MagickCore SetMagickResourceLimit(). Como ejemplo, nuestra interfaz web en línea de ImageMagick, MagickStudio, incluye estos límites de política para ayudar a prevenir una denegación de servicio:

<?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>

Dado que procesamos múltiples sesiones simultáneas, no queremos que ninguna sesión consuma toda la memoria disponible. Con esta política, las imágenes grandes se almacenan en caché en disco. Si la imagen es demasiado grande y supera el límite de disco de la caché de píxeles, el programa termina. Además, establecemos un límite de tiempo para evitar tareas de procesamiento descontroladas. Si alguna imagen tiene un ancho o un alto que supera los 8192 píxeles, se lanza una excepción y el procesamiento se detiene. A partir de ImageMagick 7.0.1-8, puedes impedir el uso de cualquier delegado o de todos los delegados (establece el patrón en "*"). Ten en cuenta que, antes de esta versión, usa un dominio de "coder" para impedir el uso de delegados (p. ej. domain="coder" rights="none" pattern="HTTPS"). La política también impide las lecturas indirectas. Si quieres, por ejemplo, leer texto de un archivo (p. ej. caption:@myCaption.txt), tendrás que eliminar esta política.

Ten en cuenta que los límites de la caché son globales para cada invocación de ImageMagick, lo que significa que si creas varias imágenes, los requisitos de recursos combinados se comparan con el límite para determinar la disposición del almacenamiento de la caché de píxeles.

Para determinar qué tipo y cuántos recursos consume la caché de píxeles, añade la opción -debug cache a la línea de comandos:

$ 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]

Este comando utiliza una caché de píxeles en memoria. El logo consumió 4.688MiB y, después de aplicarle el enfoque, 3.516MiB.

Caché de píxeles distribuida

Una caché de píxeles distribuida es una extensión de la caché de píxeles tradicional disponible en un solo host. La caché de píxeles distribuida puede abarcar múltiples servidores para que pueda crecer en tamaño y capacidad transaccional con el fin de soportar imágenes muy grandes. Inicia el servidor de la caché de píxeles en una o más máquinas. Cuando lees u operas sobre una imagen y los recursos de la caché de píxeles local se agotan, ImageMagick contacta con uno o más de estos servidores de píxeles remotos para almacenar o recuperar píxeles. La caché de píxeles distribuida depende del ancho de banda de la red para transferir los píxeles hacia y desde el servidor remoto. Como tal, probablemente será significativamente más lenta que una caché de píxeles que utiliza almacenamiento local (p. ej. memoria, disco, etc.).

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

Vistas de caché

GetVirtualPixels(), GetAuthenticPixels(), QueueAuthenticPixels() y SyncAuthenticPixels(), de la API de MagickCore, solo pueden manejar un área de la caché de píxeles por imagen a la vez. ¿Supón que quieres acceder a la primera y a la última línea de escaneo de la misma imagen al mismo tiempo? La solución es usar una vista de caché. Una vista de caché te permite acceder a tantas áreas simultáneamente en la caché de píxeles como necesites. Los métodos de la vista de caché son análogos a los métodos anteriores, salvo que primero debes abrir una vista y cerrarla cuando hayas terminado con ella. Aquí hay un fragmento de código de MagickCore que nos permite acceder a la primera y a la última fila de píxeles de la imagen simultáneamente:

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 */ }

Formato Magick Pixel Cache

Recuerda que cada formato de imagen es decodificado por ImageMagick y los píxeles se depositan en la caché de píxeles. Si escribes una imagen, los píxeles se leen de la caché de píxeles y se codifican según lo requiera el formato que estás escribiendo (p. ej. GIF, PNG, etc.). El formato Magick Pixel Cache (MPC) está diseñado para eliminar la sobrecarga de decodificar y codificar píxeles hacia y desde un formato de imagen. MPC escribe dos archivos. Uno, con la extensión .mpc, conserva todas las propiedades asociadas con la imagen o la secuencia de imágenes (p. ej. ancho, alto, espacio de color, etc.) y el segundo, con la extensión .cache, es la caché de píxeles en el formato raw nativo. Al leer un archivo de imagen MPC, ImageMagick lee las propiedades de la imagen y mapea en memoria la caché de píxeles en disco, eliminando la necesidad de decodificar los píxeles de la imagen. El compromiso está en el espacio en disco. MPC es generalmente más grande en tamaño de archivo que la mayoría de los demás formatos de imagen.

El uso más eficiente de los archivos de imagen MPC es un patrón de escribir-una-vez, leer-muchas-veces. Por ejemplo, tu flujo de trabajo requiere extraer bloques aleatorios de píxeles de la imagen de origen. En lugar de releer y posiblemente descomprimir la imagen de origen cada vez, usamos MPC y mapeamos la imagen directamente a la memoria.

Prácticas recomendadas para la caché de píxeles

Aunque puedes solicitar cualquier píxel de la caché de píxeles, cualquier bloque de píxeles, cualquier línea de escaneo, varias líneas de escaneo, cualquier fila o varias filas con los métodos GetVirtualPixels(), GetAuthenticPixels(), QueueAuthenticPixels, GetCacheViewVirtualPixels(), GetCacheViewAuthenticPixels() y QueueCacheViewAuthenticPixels(), ImageMagick está optimizado para devolver unos pocos píxeles o unas pocas filas de píxeles a la vez. Hay optimizaciones adicionales si solicitas una sola línea de escaneo o unas pocas líneas de escaneo a la vez. Estos métodos también permiten el acceso aleatorio a la caché de píxeles; sin embargo, ImageMagick está optimizado para el acceso secuencial. Aunque puedes acceder a las líneas de escaneo de píxeles secuencialmente desde la última fila de la imagen hasta la primera, puedes obtener un aumento de rendimiento si accedes a las líneas de escaneo desde la primera fila de la imagen hasta la última, en orden secuencial.

Puedes obtener, modificar o establecer píxeles en orden de filas o de columnas. Sin embargo, es más eficiente acceder a los píxeles por fila que por columna.

Si actualizas píxeles devueltos por GetAuthenticPixels() o GetCacheViewAuthenticPixels(), no olvides llamar a SyncAuthenticPixels() o SyncCacheViewAuthenticPixels() respectivamente para asegurarte de que tus cambios se sincronicen con la caché de píxeles.

Usa QueueAuthenticPixels() o QueueCacheViewAuthenticPixels() si estás estableciendo un valor de píxel inicial. El método GetAuthenticPixels() o GetCacheViewAuthenticPixels() lee píxeles de la caché y, si estás estableciendo un valor de píxel inicial, esta lectura es innecesaria. No olvides llamar a SyncAuthenticPixels() o SyncCacheViewAuthenticPixels() respectivamente para enviar cualquier cambio de píxel a la caché de píxeles.

GetVirtualPixels(), GetAuthenticPixels(), QueueAuthenticPixels() y SyncAuthenticPixels() son ligeramente más eficientes que sus equivalentes de vista de caché. Sin embargo, las vistas de caché son necesarias si necesitas acceder a más de una región de la imagen simultáneamente o si más de un hilo de ejecución está accediendo a la imagen.

Puedes solicitar píxeles fuera de los límites de la imagen con GetVirtualPixels() o GetCacheViewVirtualPixels(); sin embargo, es más eficiente solicitar píxeles dentro de los confines de la región de la imagen.

Aunque puedes forzar la caché de píxeles a disco usando límites de recursos apropiados, el acceso a disco puede ser hasta 1000 veces más lento que el acceso a memoria. Para un acceso rápido y eficiente a la caché de píxeles, intenta mantener la caché de píxeles en la memoria del heap.

La versión Q16 de ImageMagick te permite leer y escribir imágenes de 16 bits sin escalado, pero la caché de píxeles consume el doble de recursos que la versión Q8. Si tu sistema tiene recursos de memoria o disco limitados, considera la versión Q8 de ImageMagick. Además, la versión Q8 normalmente se ejecuta más rápido que la versión Q16.

La gran mayoría de los formatos de imagen y los algoritmos se restringen a un rango fijo de valores de píxel de 0 a algún valor máximo; por ejemplo, la versión Q16 de ImageMagick permite intensidades de 0 a 65535. Sin embargo, la imagen de alto rango dinámico (HDRI) permite un rango dinámico de exposiciones mucho mayor (es decir, una gran diferencia entre las áreas claras y oscuras) que las técnicas de imagen digital estándar. HDRI representa con precisión la amplia gama de niveles de intensidad que se encuentran en escenas reales, que van desde la luz solar directa más brillante hasta las sombras más profundas y oscuras. Habilita HDRI en el momento de la compilación de ImageMagick para tratar con imágenes de alto rango dinámico, pero ten en cuenta que cada componente de píxel es un valor de punto flotante de 32 bits. Además, los valores de píxel no se recortan de forma predeterminada, por lo que algunos algoritmos pueden tener resultados inesperados debido a valores de píxel fuera de rango, a diferencia de la versión no-HDRI.

Si trabajas con imágenes grandes, asegúrate de que la caché de píxeles se escriba en un área de disco con mucho espacio libre. En Linux, esto suele ser /tmp y en Windows, c:/temp. Puedes indicarle a ImageMagick que escriba la caché de píxeles en una ubicación alternativa y conserve memoria con estas opciones:

magick -limit memory 2GB -limit map 4GB -define registry:temporary-path=/data/tmp ...

Establece los límites globales de recursos para tu entorno en el archivo de configuración policy.xml.

Si planeas procesar la misma imagen muchas veces, considera el formato MPC. Leer una imagen MPC tiene una sobrecarga casi nula porque está en el formato nativo de la caché de píxeles, eliminando la necesidad de decodificar los píxeles de la imagen. Aquí hay un ejemplo:

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 es ideal para sitios web. Reduce la sobrecarga de leer y escribir una imagen. Lo usamos exclusivamente en nuestro estudio de imágenes en línea.

Propiedades y perfiles de imagen

Las imágenes tienen metadatos asociados en forma de propiedades (p. ej. ancho, alto, descripción, etc.) y perfiles (p. ej. EXIF, IPTC, gestión del color). ImageMagick proporciona métodos convenientes para obtener, establecer o actualizar las propiedades de imagen y para obtener, establecer, actualizar o aplicar perfiles. Algunas de las propiedades de imagen más populares están asociadas con la estructura Image en la API de MagickCore. Por ejemplo:

(void) printf("image width: %lu, height: %lu\n",image->columns,image->rows);

Para la gran mayoría de las propiedades de imagen, como un comentario o una descripción de la imagen, usamos los métodos GetImageProperty() y SetImageProperty(). Aquí establecemos una propiedad y la recuperamos de inmediato:

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 admite artefactos con los métodos GetImageArtifact() y SetImageArtifact(). Los artefactos son propiedades sigilosas que no se exportan a los formatos de imagen (p. ej. PNG).

Los perfiles de imagen se manejan con los métodos GetImageProfile(), SetImageProfile() y ProfileImage(). Aquí establecemos un perfil y lo recuperamos de inmediato:

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);

Imágenes multiespectrales

ImageMagick admite imágenes multiespectrales donde todos los canales tienen las mismas dimensiones y número de píxeles que la imagen original. Sin embargo, no todos los formatos de imagen admiten imágenes multiespectrales. PSD, TIFF, MIFF, MPC y FTXT tienen soporte completo para imágenes multiespectrales de hasta 31 bandas, 21 de ellas canales meta. Ten en cuenta que, si compilas ImageMagick con la opción --enable-64bit-channel-masks del script configure, puedes procesar imágenes multiespectrales de 62 bandas con hasta 52 canales meta.

Si tienes un caso de uso que actualmente no es compatible con un formato de imagen, publícalo en el foro de discusión. Hay una buena probabilidad de que podamos dar soporte a tu caso de uso en una futura versión de ImageMagick.

Streaming de píxeles

ImageMagick permite el streaming de píxeles a medida que se leen de una imagen o se escriben en ella. Esto tiene varias ventajas sobre la caché de píxeles. El tiempo y los recursos consumidos por la caché de píxeles escalan con el área de una imagen, mientras que los recursos del flujo de píxeles escalan con el ancho de una imagen. La desventaja es que los píxeles deben consumirse a medida que se transmiten, por lo que no hay persistencia.

Usa ReadStream() o WriteStream() con un método de callback apropiado en tu programa de MagickCore para consumir los píxeles a medida que se transmiten. Aquí hay un ejemplo abreviado del uso de 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);

También proporcionamos una herramienta ligera, stream, para transmitir uno o más componentes de píxel de la imagen o de una parte de la imagen al formato de almacenamiento que elijas. Escribe los componentes de píxel a medida que se leen de la imagen de entrada, una fila a la vez, lo que hace que stream sea deseable cuando se trabaja con imágenes grandes o cuando se requieren componentes de píxel raw. La mayoría de los formatos de imagen transmiten los píxeles (rojo, verde y azul) de izquierda a derecha y de arriba abajo. Sin embargo, unos pocos formatos no admiten este orden común (p. ej. el formato PSD).

Soporte de imágenes grandes

ImageMagick tiene la capacidad de manejar tamaños de imagen que van desde mega- hasta tera-píxeles, abarcando operaciones de lectura, procesamiento y escritura. En teoría, las dimensiones de la imagen pueden extenderse hasta 31 millones de filas/columnas en un sistema operativo de 32 bits y hasta unos asombrosos 31 billones en un SO de 64 bits. Sin embargo, las dimensiones realmente alcanzables son sustancialmente menores, dependiendo de los recursos disponibles en tu computadora host. Es esencial tener en cuenta que ciertos formatos de imagen imponen limitaciones al tamaño de la imagen. Por ejemplo, las imágenes de Photoshop están limitadas a un máximo de 300.000 píxeles de ancho o alto. Aquí redimensionamos una imagen a un cuarto de millón de píxeles cuadrados:

magick logo: -resize 250000x250000 logo.miff

Para imágenes grandes, es probable que los recursos de memoria se agoten e ImageMagick creará en su lugar una caché de píxeles en disco. Asegúrate de tener mucho espacio temporal en disco. Si tu partición de disco temporal predeterminada es demasiado pequeña, indícale a ImageMagick que use otra partición con mucho espacio libre. Por ejemplo:

magick -define registry:temporary-path=/data/tmp logo:  \   
     -resize 250000x250000 logo.miff

Para asegurarte de que las imágenes grandes no consuman toda la memoria de tu sistema, fuerza los píxeles de la imagen a un disco mapeado en memoria con límites de recursos:

magick -define registry:temporary-path=/data/tmp -limit memory 16mb \
  logo: -resize 250000x250000 logo.miff

Aquí forzamos todos los píxeles de la imagen a disco:

magick -define registry:temporary-path=/data/tmp -limit area 0 \
  logo: -resize 250000x250000 logo.miff

Almacenar píxeles en caché en disco es unas 1000 veces más lento que en memoria. Espera tiempos de ejecución largos al procesar imágenes grandes en disco con ImageMagick. Puedes monitorizar el progreso con este comando:

magick -monitor -limit memory 2GiB -limit map 4GiB -define registry:temporary-path=/data/tmp \
  logo: -resize 250000x250000 logo.miff

Para imágenes realmente grandes, o si hay recursos limitados en tu host, puedes utilizar una caché de píxeles distribuida en uno o más hosts remotos:

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

Debido a la latencia de la red, espera una ralentización sustancial en el procesamiento de tu flujo de trabajo.

Hilos de ejecución

Muchos de los algoritmos internos de ImageMagick están preparados para hilos con el fin de aprovechar las aceleraciones que ofrecen los chips de procesador multinúcleo. Sin embargo, puedes usar los algoritmos de ImageMagick en tus hilos de ejecución, con la excepción de los métodos de la caché de píxeles GetVirtualPixels(), GetAuthenticPixels(), QueueAuthenticPixels() o SyncAuthenticPixels() de MagickCore. Estos métodos están destinados a un solo hilo de ejecución, con la excepción de una sección paralela de OpenMP. Para acceder a la caché de píxeles con más de un hilo de ejecución, usa una vista de caché. Hacemos esto para el método CompositeImage(), por ejemplo. Supón que queremos componer una sola imagen de origen sobre una imagen de destino diferente en cada hilo de ejecución. Si usamos GetVirtualPixels(), los resultados son impredecibles porque es probable que varios hilos soliciten diferentes áreas de la caché de píxeles simultáneamente. En su lugar, usamos GetCacheViewVirtualPixels(), que crea una vista única para cada hilo de ejecución, asegurando que nuestro programa se comporte correctamente sin importar cuántos hilos se invoquen. Las otras interfaces de programación, como la API de MagickWand, son completamente seguras para hilos, por lo que no hay precauciones especiales para los hilos de ejecución.

Aquí hay un fragmento de código de MagickCore que aprovecha los hilos de ejecución con el paradigma de programación 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");

Este fragmento de código convierte un mapa de bits de Windows sin comprimir en una imagen de 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;
}

Si llamas a la API de ImageMagick desde tu aplicación habilitada para OpenMP y pretendes aumentar dinámicamente el número de hilos disponibles en regiones paralelas posteriores, asegúrate de realizar el aumento antes de llamar a la API; de lo contrario, ImageMagick puede fallar.

MagickWand admite vistas de wand. Una vista itera sobre toda la imagen, o una parte de ella, en paralelo y, para cada fila de píxeles, invoca un método de callback que tú proporcionas. Esto limita la mayor parte de tu actividad de programación paralela a ese único módulo. Hay métodos similares en MagickCore. Para ver un ejemplo, consulta el mismo algoritmo de contraste sigmoidal implementado tanto en MagickWand como en MagickCore.

En la mayoría de las circunstancias, el número de hilos predeterminado se establece en el número de núcleos del procesador de tu sistema para un rendimiento óptimo. Sin embargo, si tu sistema tiene hyperthreading o si estás ejecutando en un host virtual y solo un subconjunto de los procesadores está disponible para tu instancia de servidor, podrías obtener un aumento del rendimiento estableciendo la política de hilos o la variable de entorno MAGICK_THREAD_LIMIT. Por ejemplo, tu host virtual tiene 8 procesadores pero solo 2 están asignados a tu instancia de servidor. El valor predeterminado de 8 hilos puede causar graves problemas de rendimiento. Una solución es limitar el número de hilos a los procesadores disponibles en tu archivo de configuración policy.xml:

<policy domain="resource" name="thread" value="2"/>

O supón que tu computadora con hyperthreading de 12 núcleos usa 24 hilos de forma predeterminada. Establece la variable de entorno MAGICK_THREAD_LIMIT y probablemente obtendrás un rendimiento mejorado:

export MAGICK_THREAD_LIMIT=12

El comité de OpenMP no ha definido el comportamiento de mezclar OpenMP con otros modelos de hilos como los hilos Posix. Sin embargo, usando versiones modernas de Linux, OpenMP y los hilos Posix parecen interoperar sin problemas. Si quieres usar hilos Posix desde un módulo de programa que llama a una de las interfaces de programación de aplicaciones de ImageMagick (p. ej. MagickCore, MagickWand, Magick++, etc.) desde Mac OS X o una versión antigua de Linux, es posible que necesites deshabilitar el soporte de OpenMP dentro de ImageMagick. Añade la opción --disable-openmp a la línea de comandos del script configure y vuelve a compilar y reinstalar ImageMagick.

Puedes aumentar aún más el rendimiento reduciendo la contención de bloqueos con la biblioteca de asignación de memoria tcmalloc. Para habilitarla, añade --with-tcmalloc a la línea de comandos de configure cuando compiles ImageMagick.

Rendimiento de los hilos

Puede ser difícil predecir el comportamiento en un entorno paralelo. El rendimiento puede depender de varios factores, incluyendo el compilador, la versión de la biblioteca OpenMP, el tipo de procesador, el número de núcleos, la cantidad de memoria, si el hyperthreading está habilitado, la mezcla de aplicaciones que se ejecutan simultáneamente con ImageMagick, o el algoritmo de procesamiento de imágenes en particular que utilizas. La única forma de estar seguro de un rendimiento óptimo, en términos del número de hilos, es hacer una evaluación comparativa (benchmark). ImageMagick incluye hilos progresivos al hacer un benchmark de un comando y devuelve el tiempo transcurrido y la eficiencia para uno o más hilos. Esto puede ayudarte a identificar cuántos hilos son los más eficientes en tu entorno. Para este benchmark, aplicamos el enfoque a una imagen de 1920x1080 de un modelo 10 veces con 1 a 12 hilos:

$ 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

El punto óptimo para este ejemplo son 6 hilos. Esto tiene sentido, ya que hay 6 núcleos físicos. Los otros 6 son hyperthreads. Parece que el enfoque no se beneficia del hyperthreading.

En ciertos casos, podría ser óptimo establecer el número de hilos en 1 o deshabilitar OpenMP por completo con la variable de entorno MAGICK_THREAD_LIMIT, la opción de línea de comandos -limit, o el archivo de configuración policy.xml.

Habilitar la limitación dinámica para un rendimiento optimizado

Otra opción eficaz es habilitar la limitación dinámica (dynamic throttling) en ImageMagick. Cuando esta función está activada, ImageMagick introduce automáticamente un ligero retardo entre el procesamiento de las líneas de escaneo de la imagen en función de la carga media actual de tu sistema. Esto ayuda a prevenir el agotamiento de recursos y garantiza un rendimiento más fluido, especialmente bajo cargas de trabajo intensas.

Para habilitar la limitación dinámica, añade la siguiente configuración de política:

<policy domain="resource" name="dynamic-throttle" value="true"/>

Procesamiento distribuido heterogéneo

ImageMagick incluye soporte para el procesamiento distribuido heterogéneo con el framework OpenCL. Los kernels de OpenCL dentro de ImageMagick permiten que los algoritmos de procesamiento de imágenes se ejecuten en plataformas heterogéneas compuestas por CPU, GPU y otros procesadores. Según tu plataforma, las aceleraciones pueden ser un orden de magnitud más rápidas que la CPU única tradicional.

Primero verifica que tu versión de ImageMagick incluye soporte para la función OpenCL:

magick identify -version
Features: DPC Cipher Modules OpenCL OpenMP(4.5)

Si es así, ejecuta este comando para lograr una aceleración significativa en la convolución de imágenes:

magick image.png -convolve '-1, -1, -1, -1, 9, -1, -1, -1, -1' convolve.png

Si no hay un acelerador disponible o si el acelerador no responde, ImageMagick recurre al algoritmo de convolución no acelerado.

Aquí hay un ejemplo de kernel de OpenCL que convoluciona una imagen:

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);
};

Consulta MagickCore/accelerate.c para ver una implementación completa de la convolución de imágenes con un kernel de OpenCL.

Ten en cuenta que, en Windows, podrías tener un problema con TDR (Detección y Recuperación de Tiempo de Espera de las GPU). Su propósito es detectar tareas descontroladas que bloquean la GPU usando un umbral de tiempo de ejecución. Para algunas GPU antiguas de gama baja que ejecutan los filtros OpenCL en ImageMagick, los tiempos de ejecución más largos podrían activar el mecanismo TDR y desalojar (preempt) el filtro de imagen de la GPU. Cuando esto ocurre, ImageMagick recurre automáticamente a la ruta de código de la CPU y devuelve los resultados esperados. Para evitar el desalojo, aumenta la clave del registro TdrDelay.

Codificadores de imagen personalizados

Un codificador de imagen (es decir, codificador/decodificador) es responsable de registrar, clasificar opcionalmente, leer opcionalmente, escribir opcionalmente y dar de baja un formato de imagen (p. ej. PNG, GIF, JPEG, etc.). Registrar un codificador de imagen avisa a ImageMagick de que un formato en particular está disponible para leer o escribir. Mientras que dar de baja le indica a ImageMagick que el formato ya no está disponible. El método de clasificación examina los primeros bytes de una imagen y determina si la imagen está en el formato esperado. El lector establece el tamaño de la imagen, el espacio de color y otras propiedades, y carga la caché de píxeles con los píxeles. El lector devuelve una sola imagen o una secuencia de imágenes (si el formato admite múltiples imágenes por archivo), o, si ocurre un error, una excepción y una imagen nula. El escritor hace lo contrario. Toma las propiedades de la imagen, descarga la caché de píxeles y las escribe según lo requiera el formato de imagen.

Aquí hay un listado de un codificador personalizado de muestra. Lee y escribe imágenes en el formato de imagen MGK, que es simplemente un ID seguido del ancho y el alto de la imagen, seguido de los valores de píxel 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);
}

Para invocar el codificador personalizado desde la línea de comandos, usa estos comandos:

magick logo: logo.mgk
display logo.mgk

Proporcionamos el Magick Coder Kit para ayudarte a comenzar a escribir tu propio codificador personalizado.

Antes de compilar, establece la variable de entorno PKG_CONFIG_PATH si ImageMagick no está en la ruta predeterminada de tu sistema:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

Filtros de imagen personalizados

ImageMagick proporciona un mecanismo conveniente para añadir tus propios algoritmos personalizados de procesamiento de imágenes. Los llamamos filtros de imagen y se invocan desde la línea de comandos con la opción -process o desde el método de la API de MagickCore ExecuteModuleProcess().

Aquí hay un listado de un filtro de imagen personalizado de muestra. Calcula algunas estadísticas como la media y la desviación estándar del brillo y la saturación de los píxeles.

#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);
}

Para invocar el filtro personalizado desde la línea de comandos, usa este comando:

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

Proporcionamos el Magick Filter Kit para ayudarte a comenzar a escribir tu propio filtro de imagen personalizado.

Antes de compilar, establece la variable de entorno PKG_CONFIG_PATH si ImageMagick no está en la ruta predeterminada de tu sistema:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig