snibgo's ImageMagick pages

New FX

Proposal for a replacement faster "-fx".

The old "-fx" applies an arithmetical expression to every selected channel of every pixel of an image. This is a flexible operation, but very slow for large images as much processing is done at every pixel. For example, if the expression contains the text string "0.123456" then that text is parsed and converted to a floating-point binary number at every pixel. Logically, that processing need occur only once. Similarly, keywords such as "hypot" and variable names such as "primes" are re-translated at every pixel.

The new "-fx" does the same overall work as the old "-fx", but generally much faster. It first translates the input string into Reverse Polish Notation object code, with full tokenisation and syntax-checking, then executes that code for each pixel. So the text string "0.123456" is translated once into floating-point binary, and that floating-point element is accessed as required for each pixel at run time.

How much faster? One to two orders of magnitude.

Full error-checking and reporting is expensive when it is repeated at every pixel, so the current "-fx" is weak in this area. This makes writing complex expressions error-prone. The new "-fx2" does this work once, at translation, so performance of error-checking is not a problem.

For clarity, this page refers to the existing operation as "old -fx", and the proposed replacement as "new -fx2". I am not proposing a name change. The replacement operation should have the same name as the old operation, "-fx".

This also replaces the existing "%[fx:...]". The benefit here is not speed, as it is only executed once per image. The benefit is some extra power in the language, especially the ability to incorporate "%[...]" elements in the input string.

This page describes the new "-fx" language in some detail. It has a minimal description of the RPN language, and how translation from "-fx" to RPN is performed.

Windows usage: As usual when using Windows BAT scripts, when IM expects a percent "%" character, it must be doubled in the script to "%%". This is a "feature" of the Windows BAT script interpreter.

See also the companion page New FX tests, which details some black-box tests for validating changes.

References

The operation

-fx loops through all the pixels in the first image, and all the channels selected by "-channel" within each pixel.

The same low-level code evaluates "-fx" and "%[fx:...] and "%[hex:...]" and "%[pixel:...].

"%[fx:...] and "%[hex:...]" and "%[pixel:...] are used in "-format" and "-define" operations. In property.c, FxEvaluateChannelExpression() is called. As expressions are not calculated at every pixel, performance is not critical for these operations.

The difference between "-fx" and the other operations is:

Explaining this in pseudocode, "-fx" does this, for the first image:

for y = 0 to image_rows-1 {
  for x = 0 to image_columns-1 {
    for c = 0 to number_channels-1 {
      evaluate input_string, updating image
    }
  }
}

"%[fx:...]" does this:

for i = 0 to number_images-1 {
  evaluate input_string
}

"%[hex:...]" and "%[pixel:...]" do this:

for i = 0 to number_images-1 {
  for c = 0 to number_channels-1 {
    evaluate input_string
  }
}

This raises an obvious question: is there an equivalent process that loops though all the images, and all the pixels of each image, and all the channels of each image? No. However, within -fx we can loop through all the images (read-only), so this can provide a similar effect.

Objectives

The overall objective of this replacement is to make complex "-fx" processing feasible for large images. Specifically:

A photo from a DSLR camera can have 33 million pixels, with three channels per pixel, so the expression is evaluated 100 million times. To achieve good performance, we need minimal processing when evaluating expressions.

The new "-fx2" should reject all input strings that have incorrect syntax. One consequence is that existing scripts with badly-formed expressions will be rejected by the new fx. For example, the documentation claims:

Assignments to reserved built-ins do not throw an exception and have no effect; e.g. -fx "r=3.0;r" returns the pixel red color value, not 3.0, and does not change the red value.

I regard this as misleading language design. -fx "r=3.0" should either assign the value "3.0" to r, or it should raise an error. The new -fx2 rejects assignment to reserved words. At least, to words that are reserved in the -fx context such as "r" and "abs".

New language features

The "-fx" language has been slightly extended:

  1. The new "-fx2" accepts channel qualifiers ".all" and ".this".
  2. The new "-fx" accepts "%[...]" as an operand. Hence, "%[fx:...]" now also accepts "%[...]" as an operand. For example:

    magick in1.png in2.png -metric RMSE -compare -format "%[fx:%[distortion]>0.1]" info:

    This example tests the difference between two images, measured by RMSE. If the difference is greater than 0.1 it returns 1; otherwise it returns 0.

Language definition and translation

The old "-fx" implementation does some simple tokenisation (such as transforming ">>" to a single character) then re-interprets the input string for every pixel. This gives simplicity and flexibility but at a cost to performance.

The new "-fx2" operation is in two phases:

  1. Translation of the input string to Reverse Polish Notation (RPN), including full syntax checking and tokenisation. Peformance of this phase is not critical. It uses a stack of operators like a shunting-yard algorithm. It is also recursive, to simplify handling of nested expressions and flow control.
  2. Execution of the RPN for every channel at every pixel. This is designed to be fast. It uses a stack of floating-point operands.

We assume an approximately BNF definition of the input string. In this BNF:

input_string ::= statement_list 

statement_list ::= statement { ';' statementstatement ::= expression 

expression ::= { user_symbol '=' } expression |
               for_expression |
               if_expression |
               do_expression |
               while_expression |
               ternary_expression |
               operand { operator operand }

for_expression ::= 'for' '(' statement_list ',' statement_list ',' statement_list) 

if_expression ::= 'if' '(' statement_list ',' statement_list ',' statement_list ) 

do_expression ::= 'do' '(' statement_list ',' statement_list ) 

while_expression ::= 'while' '(' statement_list ',' statement_list ) 

ternary_expression ::= statements '?' statements ':' statements  

operand ::= number [ si_prefix ] |
            '%[' long_form_attribute_escape ']' |
            '(' expression ')' |
            image_artifact |
            named_constant |
            colour_constant |
            function_call |
            pixel_value |
            image_attribute |
            symbol |
            user_symbol |
            user_symbol '++' |
            user_symbol '--' |
            unary_prefix operand 

operator ::= '+' | '-' | '*' | '+=' | '*=' | ...

unary_prefix ::= '+' | '-' | '~' | '!'

si_prefix ::= 'y' | 'z' | 'a' | 'f' | 'p' | 'n' | 'u' | 'm' | 'k' |
              'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' |
              'yi' | 'zi' | 'ai' | 'fi' | 'pi' | 'ni' | 'ui' | 'mi' | 'ki' |
              'Mi' | 'Gi' | 'Ti' | 'Pi' | 'Ei' | 'Zi' | 'Yi'

function_call ::= function_name '(' function_args ) |
                  function_name '[' function_args ']' |
                  function_name '{' function_args '}'

function_args ::= statement_list { ',' statement_list }

long_form_attribute_escape ::= 'bit-depth' |
                               'colors' |
                               'fx:' expression |
                               'EXIF:FNumber' | ...

colour_constant ::= '#' hex_digits |
                    'blue' | 'lime' | 'khaki' | ... |
                    colorspace '(' number_list ')' |
                    'icc-color' '(' colorspace ',' number_list ')' |
                    'device-gray' '(' number ')' |
                    'device-rgb' '(' number_list ')' |
                    'device-cmyk' '(' number_list ')'

named_constant ::= 'epsilon' | 'e' |
                   'nummetas' |
                   'opaque' |
                   'phi' | 'pi' |
                   'quantumrange' | 'quantumscale' |
                   'transparent' | 'MaxRGB'

pixel_value ::= image_spec |
                image_spec '.' coord_spec |
                image_spec '.' channel_spec |
                image_spec '.' coord_spec '.' channel_spec |
                image_spec '.' img_attr_spec |
                image_spec '.' img_attr_spec '.' channel_spec |
                coord_spec |
                coord_spec '.' channel_spec |
                channel_spec |
                'metas' '[' statement_list ']'
                img_attr_spec |
                img_attr_spec '.' channel_spec 

image_spec ::= 'u' | 'v' | 's' |
               'u' '[' statement_list ']'

coord_spec ::= 'p' |
               'p' '{' statement_list ',' statement_list '}' |
               'p' '[' statement_list ',' statement_list ']'  

img_attr_spec ::= 'mean' | 'kurtosis' | 'page.x' | ...  

channel_spec ::= 'r' | 'g' | 'b' |
                 'c' | 'm' | 'y' | 'k' |
                 'a' | 'o' |
                 'meta' | 'meta0' | 'meta1' |...| 'meta99999' |
                 'hue' | 'saturation' | 'lightness' |
                 'intensity' |
                 'all' | 'this

Notes:

For comparison, the C language and the old -fx both permit "xx=12; xx++ *3". C finishes with XX set to 13 and returns 36. However, the old -fx finishes with xx set to 13 but returns 0 (zero).

So the new -fx might process "xx=12; xx++ *3" either like C which could make scripts return different results to the old -fx, or it might process like the old -fx which could confuse programmers accustomed to C and similar languages. I resolve the issue by prohibiting that syntax. We can use parentheses like this "xx=12; (xx++) *3".

If insufficient function arguments are provided, an error is generally raised. However, functions "if", "p", "s" and "u" can have mere place-holder commas, like this:

p[,]
if (1,2,)
if (1,,3)

In those cases, there is an implied 0 (zero) in the missing arguments.

Similarly, the function "channel" can have from zero to five arguments. At translation, if fewer than five arguments are supplied, trailing arguments are given a default value of zero. Hence at runtime, the function always has five arguments.

Beware of chained comparisons. In both old "-fx" and new "-fx2", the expression "10 < xx < 12" has the same effect as "(10 < xx) < 12". The first part "(10 < xx)" evaluates to either one (true) or zero (false). So the next comparison is either "1 < 12" or "0 < 12", and these are both one (true). So the result of "10 < xx < 12" is always one (true), for all values of xx, which was probably not the intention.

What are the channel qualifiers "all" and "this"? If we specify "u.r", we get the red channel of the current image. If we don't specify a channel qualifier, we get the current channel. For an image_spec , the default channel_spec is "this".

However, when we have an img_attr_spec , and we don't specify a channel qualifier, the default is to get the overall attribute, not the attribute of an individual channel. We can use operands such as "mean.this" which will set the output red channel to the mean of just the input red channel, and so on for all the channels. We can use "mean.all" to explicitly state we want all the output channels set to the overall mean of the input channels.

In "%[fx:...]", what do "%[fx:u]", "%[fx:v]", "%[fx:s]" etc with no channel qualifiers refer to? The documentation currently says:

As each image in the sequence is being evaluated, s and t successively refer to the current image and its index, while i and j are set to zero, and the current channel set to red (-channel is ignored). An example:

 $ magick canvas:'rgb(25%,50%,75%)' rose: -colorspace rgb  \
      -format 'Red channel of NW corner of image #%[fx:t] is %[fx:s]\n' info:
    Red channel of NW corner of image #0 is 0.464883
    Red channel of NW corner of image #1 is 0.184582

But this documentation does not describe what IM actually does. We can see this more clearly with a simpler command:

%FXOLD%magick canvas:"rgb(25%%,50%%,75%%)" -format "Red channel of NW corner of image #%%[fx:t] is %%[fx:s] but s.r is %%[fx:s.r] and intensity is %%[fx:s.intensity]\n" info: 
Red channel of NW corner of image #0 is 0.25 but s.r is 0.25 and intensity is 0.4648825

So "%[fx:s]" refers to the intensity, not the red channel, of the NW corner.

I consider this to be a language implementation bug. If users want intensity, they can be explicit: "%[fx:s.intensity]". If they want the red channel, they can also be explicit: "%[fx:s.r]". For consistency with the documentation, the new "%[fx:s]" returns the red channel of the NW corner:

set FXNEW=%IM7DEV%

%FXNEW%magick canvas:"rgb(25%%,50%%,75%%)" xc:#abd -format "Red channel of NW corner of image #%%[fx:t] is %%[fx:s]\n" info: 
Red channel of NW corner of image #0 is 0.25
Red channel of NW corner of image #1 is 0.66666667

Flow control

Taking the for syntax as:

for ( initialize, condition, body_statements )

... each of the three arguments is a "statement_list", so we have three lists of statements. The lists are separated by semi-colons. Within each list, statements are separated by commas. Steps for the for RPN are:

  1. statements for initialize.
  2. statements for condition.
  3. if condition is false (zero), skip over statements to step 6.
  4. body_statements.
  5. goto step 2.
  6. continue.

Each step may translate into many elements of the RPN array, so "goto" a forward reference can only be completed when we have translated intermediate steps.

The "for" function returns a value. For example:

%FXOLD%magick ^
  xc:red -fx "xx=12;aa=1;xx=for(aa=23,aa<34,aa=aa+1)+2;debug(xx)" ^
  txt: >fxn_for.lis 2^>^&1
red[0,0].[0]: xx=2
red[0,0].[1]: xx=2
red[0,0].[2]: xx=2
# ImageMagick pixel enumeration: 1,1,0,4294967295,srgb
0,0: (8589934590,8589934590,8589934590)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(200%,200%,200%)

... sets xx to 36. But why? Because "for" returns 34. But why? I don't know. Shouldn't "for" return the value of the last evaluated expression, which is "aa<34", which is false, ie 0? "for" returns the value of the third expression, "aa=aa+1".

Beware that the meanings in fx of semi-colon ";" and comma "," are reversed compared to the meanings in C.

Similarly for do, which seems to work like this (contrary to the documentation):

do ( condition, body_statements )

The body_statements are always executed at least once.

%FXOLD%magick ^
  xc:#abc -fx "xx=12; do (xx<20,xx++); xx" ^
  txt: 
# ImageMagick pixel enumeration: 1,1,0,4294967295,srgb
0,0: (85899345900,85899345900,85899345900)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(2000%,2000%,2000%)
%FXOLD%magick ^
  xc:#abc -fx "xx=12; do (xx<10,xx++); xx" ^
  txt: 
# ImageMagick pixel enumeration: 1,1,0,4294967295,srgb
0,0: (55834574835,55834574835,55834574835)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(1300%,1300%,1300%)

Steps for the ideal do RPN are:

  1. body_statements.
  2. statements for condition.
  3. if condition is true (non-zero), goto step 1.
  4. continue.

With the inverted order of the statements and condition, this would be difficult to translate. Instead, we can create this RPN:

  1. goto step 4
  2. statements for condition.
  3. if condition is false (zero), goto step 6.
  4. body_statements.
  5. goto step 2
  6. continue.

The while function has the same syntax as do, but the condition is checked first so the body_statements may never be executed:

while ( condition, body_statements )

Steps for the while RPN are simpler than the do:

  1. statements for condition.
  2. if condition is false (zero), goto step 5.
  3. body_statements.
  4. goto step 1.
  5. continue.

Similarly for if:

if ( condition, nonzero_statements, zero_statements )

Steps for the if RPN are:

  1. statements for condition.
  2. if condition is false (zero), skip over statements to step 5.
  3. nonzero_statements.
  4. goto step 6.
  5. nonzero_statements.
  6. continue.

Ternary has the same effect as if:

condition ? true_statements : false_statements

Steps for the ternary RPN are:

  1. statements for condition.
  2. if condition is false (zero), goto step 5.
  3. true_statements.
  4. goto step 6.
  5. false_statements.
  6. continue.

Pixel values

"-fx" loops through every pixel in the first image, and every channel in each pixel, and evaluates the expression for that channel of that pixel. The expression can read values not just from that channel of that pixel, but from any pixel from any location from any image in the current list. For example, "-fx u[2].p{12.3,34.5}.g" reads from the third image (they are counted from zero), coordinates (12.3, 34.5), the green channel. These coordinates are not integers, so they will be interpolated. Each number can be an expression like "3+4.5" or even a statement list such as "thisX = i/(w-1); thatX = (u[2].page.width-1)*thisX"

Instead of u[2] we might have u which means the same as u[0]. Or we might have v which means the same as u[1]. Or we might have s which means (in -fx) the same as u[0].

(In a %[fx:...] expression, which loops through all the images, s means the current image.)

The old "-fx" is not fussy about the number of arguments for functions. For example, -fx "abs(-23)" and -fx "abs()" and -fx "abs(-23,)" and -fx "abs(-23,45)" are all accepted.

For some functions, too few arguments are reasonable. For example, "if (xx==0, aa=4, )" will set aa if and only if xx is zero. The new "-fx2" also accepts this. I don't think too many arguments are ever reasonable, so "-fx2" will raise a fatal error.

When a parenthesis pair denotes arithmetic precedence, such as 2 * (3+4) / 2, the open-parenthesis is treated as an operand but is followed by another operand, the close-parenthesis is treated as an operator but is followed by another operator.

When a parenthesis pair denotes a function list, such as 2 * abs(3+4) / 2, the open-parenthesis is treated as an operand but is followed by another operand, the close-parenthesis is treated as an operator but is followed by another operator.

The documentation for the old "-fx" claims it has implied multiplication. That documentation seems to be wrong. The new "-fx2" does not have implied multiplication.

The old "-fx" does not check function arguments. For example, -fx "max(5,11,3,6)" is accepted as valid, but returns "6".

p can have offsets and be qualified with an image attribute, eg:

%FXOLD%magick xc:#abc toes.png -fx "p[1,1].minima" txt: 
@fxn_p.lis

When it has offsets and an image statistic, the offsets seem to be ignored, so "p[1,1].minima" has the effect is the same as "u.minima". In my view, this is poor language design: a user might reasonably suppose the input string "p[1,1].minima" means the minimum value in the channels at the specified pixel. In the new -fx, p may not be qualified with an image attribute.

"z" seems to return "1". But:

%FXOLD%magick xc:#abc -fx "debug(z)" txt: 
# ImageMagick pixel enumeration: 1,1,0,4294967295,srgb
0,0: (17179869180,17179869180,17179869180)  #FFFFFFFFFFFFFFFFFFFFFFFF  srgb(400%,400%,400%)

Assignments

Here is a sample input string with an assignment:

"xx = 2 + 3"

We call "xx" a UserSymbol, an operand. In many languages this is called a "variable", but I don't because the "-fx" language has many entities with varying values. (And I may add a feature for performance to declare that a UserSymbol is a constant, that is evaluated only once at translation instead of at every pixel.) Syntactically, assignment is an operator with very low precedence. But there is a semantic rule: an assignment operator encountered after any non-assignment operator in the same (possibly nested) expression is invalid. So "xx = yy = 2+3" and xx = 4 + (yy = 2+3)" are valid but "xx = 2 + yy = 3" is invalid. For the input string "xx = 2 + 3", the RPN would be:

2, 3, +, =

The "=" RPN entry is the assignment. At translation, we create an array of UserSymbols. All references to user symbols are translated to an index into the array. At run time, there is a corresponding array of values of user symbols, so the result of "2 3 +" is copied to an element of that array. Each thread has its own array.

The UserSymbol "xx" might be used in another expression like this:

"yy = (6 + xx ) / 7"

The RPN would be:

6, xx, +, 7, /, =

The RPN Element for "xx" will be "CopyFrom", with EleNdx set to the index of the user symbol table that was used for the assignment of "xx". At run time, encountering the "CopyFrom" Element will cause the value to be copied from the assignment element.

Subexpressions:

"while ( xx = 5 , yy = 6 ) + 3"   Expression levels
             +-+      +-+         level 3
        +------+ +------+         level 2
+-----------------------------+   level 1

The RPN, before implementing loops, would be:

5 = 6 = while 3 +
  ^   ^
  |   |
  xx  yy
 ^-------V   ^-----V       if false goto
"3-1 ? 4+5 :   6 ? 7 : 2*3"
"3-1 ? 4+5 : ( 6 ? 7 : 2*3 )"
                    +-------^   goto
          +-----------------^   goto

The result is "9".

So we start a subexpression after ":".

xx = yy
Ideally:
yy CopyTo(xx)

xx = yy = 0

Ideally:
0 CopyTo(yy) CopyTo(xx)

Indexed u takes a statement list, like this:

%FXOLD%magick ^
  -size 5x1 gradient:red-blue ^
  -size 6x1 gradient:black-white ^
  -fx "yy=u[xx=2-1;1];debug(xx);yy" ^
  txt:

In the old -fx, indexed v works like indexed u, so v[0] refers to the first image in the list. The new -fx2 does not permit indexed v or s.

Image attributes

Some numbers are relevant to the overall image, rather than individual pixels. These are the "Image Attributes". Of these, some are statistics that are calculated by each channel, and also overall. These statistics are: kurtosis, maxima, mean, median, minima, skewness, and standard_deviation.

These statistics are very slow in the old "-fx". Perhaps they are re-calculated at every pixel. I doubt that they are normally used in "-fx" operations, but are used in "%[fx:...]" expressions, which are evaluated only once per image.

In the new "-fx2", statistics are calculated only once for each image, and they can be used without performance problems. This can be useful for operations such as "(u-minima)/(maxima-minima)".

Create a simple test image:

%FXOLD%magick xc:#abd fxn_tst1.png

"-verbose info:" shows four values for "mean", three for the RGB channels and an overall mean:

%FXOLD%magick fxn_tst1.png -verbose info: |grep -i mean 
 mean: 0.66666667 (0.66666667)
      mean: 0.73333333 (0.73333333)
      mean: 0.86666667 (0.86666667)
      mean: 0.75555556 (0.75555556)
%FXOLD%magick fxn_tst1.png -fx "mean" txt: 
%FXOLD%magick fxn_tst1.png -fx "u.mean" txt: 
%FXOLD%magick fxn_tst1.png -fx "u.mean.r" txt: 
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (0,193,193)  #00C1C1  srgb(0%,75.555557%,75.555557%)
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (193,193,193)  #C1C1C1  srgb(75.555557%,75.555557%,75.555557%)
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (170,170,170)  #AAAAAA  srgb(170.00001,170.00001,170.00001)

This shows that when there is no channel qualifier, all output channels are set to the overall mean, the "composite" mean. When there is a channel qualifier, all output channels are set to the mean of the qualifier channel. There seems to be no way to set each output channel to the mean of the corresponding input channel.

The same operations with the new "-fx2":

%FXNEW%magick fxn_tst1.png -fx "mean" txt: 
%FXNEW%magick fxn_tst1.png -fx "u.mean" txt: 
%FXNEW%magick fxn_tst1.png -fx "u.mean.r" txt: 
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (0,193,193)  #00C1C1  srgb(0%,75.555557%,75.555557%)
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (193,193,193)  #C1C1C1  srgb(75.555557%,75.555557%,75.555557%)
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (170,170,170)  #AAAAAA  srgb(170.00001,170.00001,170.00001)

With the new "-fx2", we can use the channel qualifier ".this" to set each output channel to the mean of just that channel:

%FXNEW%magick fxn_tst1.png -fx "u.mean.this" txt: 
# ImageMagick pixel enumeration: 1,1,0,255,srgb
0,0: (170,187,221)  #AABBDD  srgb(170.00001,187,221)

Meta channels

The old "-fx" provides no method for accessing meta channels.

The following is a proposal only. I haven't yet written the code to do this.

As from December 2023 [?], fx can read meta channels ("multispectral channels"). They are numbered from zero. The meta channel is specified by "meta" possibly suffixed by an integer such as "meta0", "meta5" or "meta999". "meta" with no suffix means the same as "meta0".

The fx code imposes no limit on the number of meta channels. If the requested meta channel does not exist for the image, an error is raised.

For example, to set the output green channel from meta 3 of the first image multipled by 0.9:

-channel G -fx "meta3 * 0.9" +channel

As with colour channels such as "r", a meta channel can be used in isolation (meaning it is for the first image in an -fx operation), or as a qualifier to an image specifier.

For example, to set the output green channel from meta 3 of image number 2 multipled by 0.9:

-channel G -fx "u[2].meta3 * 0.9" +channel

In addition, meta channels may be accessed as if they were elements of an array by using "metas[n]" where n is a statement list that evaluates to a number. This format is only available in isolation, so only for the first image in an -fx operation, and is not available in a p[...] or p{...} construct.

A constant named "nummetas" has a value that is the number of meta channels in the first image in an -fx operation, or each image in %[fx:...]. The value is an integer from zero upwards. For example, we can create a list of the numbers of meta channels in each image like this:

magick ^
  in1.tiff in2.tiff in3.tiff in4.tiff ^
  -format "%[fx:nummetas]\n" ^
  info:

Example: for each pixel, set the green channel to the mean of all the meta channels:

magick ^
  in.tiff ^
  -channel Green ^
  -fx "metasum=0; for (Ndx=0, Ndx<nummetas, Ndx++; metasum+=metas[Ndx]); metasum/nummetas" ^
  +channel ^
  out.tiff

Example: for each pixel, set the blue channel to the number of the meta channel that contains the largest value:

magick ^
  in.tiff ^
  -channel Blue ^
  -format "%[fx:max=meta0; maxAt=0; for (Ndx=1, Ndx<nummetas, Ndx++; if (max<metas[Ndx], max=metas[Ndx]; maxAt=Ndx); maxAt/QuantumLevel" ^
  +channel ^
  out.tiff

Resource Limits

The fx code regards both TimeResource and ThrottleResource. These are both checked only for loops (do and while and for), and only every 4096 loop iterations within each fx (including the first iteration).

The resources can be limited in policy.xml or with environment variables or -limit options at the command line.

For example, we can prevent infinite loops from causing a denial of service (DOS) by setting a limit of two seconds:

set MAGICK_TIME_LIMIT=2

%IM7DEV%magick xc: -fx "for (val=3, val<10, val+=0); val" NULL: 
magick: time limit exceeded `' @ fatal/fx.c/ExecuteRPN/3942.

I don't usually want a limit, so I unset it:

set MAGICK_TIME_LIMIT=

Performance

The new "-fx2" is faster than the old "-fx" operation. For example, in a test with a 1000x1000 input image, "-fx2 u*1.1" is a factor of 100 faster the old operation.

If an "-fx2" expression has the same effect as a single IM operation, the "-fx2" expression will be slower. For example, "-fx2 u*1.1" is 10% slower than "-evaluate Multiply 1.1".

For best performance, expressions should generally be kept as simple as possible. For example, the operands "r" and "u[0].p[0,0].r" have the same effect, but the second is slower than the first. An exception to this general rule is that using "%[fx:...]" within "-fx" can improve performance. For example, we may need to calculate "(w-1)". If we write that in the "-fx2", the RPN will contain three elements, for the image width, the constant one, and the minus operation. If instead we use "%[fx:w-1]", the RPN will contain just one element, the constant value that is the width minus one.

The translation phase of the new "-fx2", with extensive error-checking but executed only once for all the pixels, is far more complex and does far more work than the old "-fx", which is executed at evey pixel. When the number of pixels is small, the new "-fx2" will take more time than the old "-fx".

Pseudo-channels can be used, such as "u.saturation" and "u.intensity". In "-fx", these are calculated at every channel of every pixel. For good performance, it is better to calculate these channels before the "-fx" with the appropriate IM operation, and then refer to these with "v" or "u[2]" or whatever within the "-fx" expression.

The translation phase of the new "-fx2" is single-threaded. It cannot easily be made multi-threaded.

The run-time phase of the new "-fx2" is multi-threaded.

FX Limits

For simplicity, the new "-fx2" does not implement image attributes for hue, saturation or lightness, so we can't use expressions like "mean.saturation". I doubt that anyone uses HSL statistics. HSL is for most purposes a useless colorspace. We can use pixel HLS values such as "u.saturation".

The old "-fx" did allow expressions like "mean.saturation", but returned the wrong result.

Arithmetic is performed in fxFltType, which is long double, which is normally 128 bits floating-point. However, the code calls other IM functions, and some of those use merely double, which is normally 64 bits.

No limit is set on the length of an expression string.

No limit is set on the length of an RPN array. However, it starts at an initial length of 100 elements, and is extended if required during translation.

Tokens, including UserSymbols, are limited to a length of 100 characters.

No limit is set on the number of UserSymbols. However, the table starts at an initial length of 50 elements, and is extended if required during translation.

No limit is set on the depth of nested expressions. An arbitrary limit, such as 100 or 1000, could be set to prevent runaway bugs eating all available memory.

No limit is set on the size of the translate-time operator stack. However, it starts at an initial length of 50 elements, and is extended if required during translation.

The run-time value stack has a fixed size, which is twice the maximum number of used elements of the translate-time operator stack, or 100, whichever is the larger. A hostile expression could exceed this size, which would raise a fatal exception.

Debug output

If the property fx:debug is true, the functions AcquireFx2Info() and FxImage() will write information to stderr. This is primarily for debugging purposes, to identify bugs in fx input strings as well as internal translation or run-time bugs. It is also to inform users of available features, and provide insight into the generated RPN code to assist the writing of efficient fx input strings.

The debugging output contains:

  1. Symbols and keywords, divided into sections for Operators, Functions, Image attributes, Symbols and Controls. Not all of these are available for use in "-fx" expressions.
  2. UserSymbols. In the example below, there is one UserSymbol, named "xx".
  3. The RPN table in a human-readable format. In the example below, the RPN has four entries (numbered 0 to 3).
  4. After the run-time of "-fx", the final values of each UserSymbol, for each thread. In the example below, the CPU can run eight threads, so each UserSymbol has eight possible values. It looks likely that only three threads have been used.
%FXNEW%magick toes.png -define fx:debug=true -fx "xx=u*0.3" NULL: 

The debugging output is:

 Operators:
  += -= *= /= ++ -- + - * / % + - << >> == != <= >= < > && || ! & | ~ ^ ? : ( ) [ ] { } =
Functions:
   abs acosh acos airy alt asinh asin atanh atan2 atan ceil channel clamp cosh cos debug drc erf exp floor gauss gcd hypot int isnan j0 j1 jinc ln logtwo log max min mod not pow rand round sign sinc sinh sin sqrt squish tanh tan trunc do for if while u u0 up s v p sp vp
Image attributes:
   depth extent kurtosis maxima mean median minima page page.x page.y page.width page.height printsize printsize.x printsize.y quality resolution resolution.x resolution.y skewness standard_deviation h n t w z
Symbols:
   hue intensity lightness luma luminance saturation a b c g i j k m o r y
Controls:
   goto gotochk ifzerogoto ifnotzerogoto copyfrom copyto zerstk 
UserSymbols (1)
  0: 'xx'
DumpRPN:  numElements=100  usedElements=4  maxUsedOprStack=2  ImgListLen=1  NeedStats=no  GotStats=no  NeedHsl=no
EntireImage
  0: Function val=0 'u0' nArgs=0 ndx=0  push
  1: Constant val=0.3 'onull' nArgs=0 ndx=0  push
  2: Operator val=0 '*' nArgs=2 ndx=0  push
  3: Control val=0 'copyto' nArgs=1 ndx=0  push  CopyTo ==> xx
User symbols (1):
th=0 us=0 'xx': 0.13167773
th=1 us=0 'xx': 0.11320211
th=2 us=0 'xx': 0.11654383
th=3 us=0 'xx': 0
th=4 us=0 'xx': 0
th=5 us=0 'xx': 0
th=6 us=0 'xx': 0
th=7 us=0 'xx': 0

The generated RPN has four entries.

  1. Function 'u0': read the pixel value, and push it.
  2. Constant '0.3': push the value.
  3. Operator '*': pop two values, multiply them, and push the result.
  4. Control 'copyto': pop one value, and copy it to xx.

The output shows the final value of each UserSymbol for each thread.

Example usage

Sharpening by any method may push values outside 0 to 100%. A simple -fx shows where this clipping occurs:

%FXNEW%magick ^
  toes.png ^
  -unsharp 0x3+2+0 ^
  -fx "u<=0?1:u>=1?0:u" ^
  fxn_clips.png
fxn_clips.pngjpg

Here is an IM script, named fxn_fxshp.scr, with an expression for a capped unsharp mask. I put it in an IM script so I can split the "-fx" expression into lines with indentation and don't have to fuss with shell line-continuation characters or escapes.

-colorspace RGB
-fx "
  mrad = %[rad];
  blur = 0; 
  lmax = u;
  lmin = u;
  nPix = %[fx:(rad*2+1)*(rad*2+1)];
  for ( dy=-mrad, dy <= mrad,
        for ( dx=-mrad, dx <= mrad,
                pix = u.p[dx,dy]; 
                blur += pix; 
                lmax = max (lmax,pix); 
                lmin = min (lmin,pix); 
                dx++
            );
        dy++
      ); 
  blur /= nPix;
  shp = blur + 4 * (u-blur); 
  shp = max (min (shp, lmax) , lmin)" 
-colorspace sRGB
-set filename:f %[rad]
-write fxn_fxshp_%[filename:f].png
-exit

The "-fx" calculates a blur as the mean value of the nine local pixels, then calculates the sharpened value using a fairly extreme unsharp mask from that blur, but limits this to be within the range of the lowest and highest local values. It operates in linear RGB to give a slightly prettier result than it would in sRGB.

toes.png

toes.pngjpg
%FXNEW%magick ^
  toes.png ^
  -define rad=1 ^
  -script fxn_fxshp.scr
fxn_fxshp_1.pngjpg
%FXNEW%magick ^
  toes.png ^
  -define fx:debug=true ^
  -define rad=3 ^
  -script fxn_fxshp.scr 
fxn_fxshp_3.pngjpg

Some points to note:

Here is the output from "fx:debug". "DumpRPN" occurs twice. The first is the nested "%[fx:...]" expression, which can be summarised as:

3 2 * 1 + 3 2 * 1 + *

By hand, we can calculate the result: 49. The first RPN has calculated this value, and passed it back to its caller, which was the translator for the second RPN.

In the second RPN, at line 8, we see the constant value "49" is pushed. At line 9, there is one argument so "49" is popped, and copied to UserSymbol nPix.

Operators:
  += -= *= /= ++ -- + - * / % + - << >> == != <= >= < > && || ! & | ~ ^ ? : ( ) [ ] { } =
Functions:
   abs acosh acos airy alt asinh asin atanh atan2 atan ceil channel clamp cosh cos debug drc erf exp floor gauss gcd hypot int isnan j0 j1 jinc ln logtwo log max min mod not pow rand round sign sinc sinh sin sqrt squish tanh tan trunc do for if while u u0 up s v p sp vp
Image attributes:
   depth extent kurtosis maxima mean median minima page page.x page.y page.width page.height printsize printsize.x printsize.y quality resolution resolution.x resolution.y skewness standard_deviation h n t w z
Symbols:
   hue intensity lightness luma luminance saturation a b c g i j k m o r y
Controls:
   goto gotochk ifzerogoto ifnotzerogoto copyfrom copyto zerstk 
UserSymbols (0)
DumpRPN:  numElements=100  usedElements=11  maxUsedOprStack=3  ImgListLen=1  NeedStats=no  GotStats=no  NeedHsl=no
CornerOnly
  0: Constant val=3 'onull' nArgs=0 ndx=0  push
  1: Constant val=2 'onull' nArgs=0 ndx=0  push
  2: Operator val=0 '*' nArgs=2 ndx=0  push
  3: Constant val=1 'onull' nArgs=0 ndx=0  push
  4: Operator val=0 '+' nArgs=2 ndx=0  push
  5: Constant val=3 'onull' nArgs=0 ndx=0  push
  6: Constant val=2 'onull' nArgs=0 ndx=0  push
  7: Operator val=0 '*' nArgs=2 ndx=0  push
  8: Constant val=1 'onull' nArgs=0 ndx=0  push
  9: Operator val=0 '+' nArgs=2 ndx=0  push
  10: Operator val=0 '*' nArgs=2 ndx=0  push
Operators:
  += -= *= /= ++ -- + - * / % + - << >> == != <= >= < > && || ! & | ~ ^ ? : ( ) [ ] { } =
Functions:
   abs acosh acos airy alt asinh asin atanh atan2 atan ceil channel clamp cosh cos debug drc erf exp floor gauss gcd hypot int isnan j0 j1 jinc ln logtwo log max min mod not pow rand round sign sinc sinh sin sqrt squish tanh tan trunc do for if while u u0 up s v p sp vp
Image attributes:
   depth extent kurtosis maxima mean median minima page page.x page.y page.width page.height printsize printsize.x printsize.y quality resolution resolution.x resolution.y skewness standard_deviation h n t w z
Symbols:
   hue intensity lightness luma luminance saturation a b c g i j k m o r y
Controls:
   goto gotochk ifzerogoto ifnotzerogoto copyfrom copyto zerstk 
UserSymbols (9)
  0: 'mrad'
  1: 'blur'
  2: 'lmax'
  3: 'lmin'
  4: 'nPix'
  5: 'dy'
  6: 'dx'
  7: 'pix'
  8: 'shp'
DumpRPN:  numElements=100  usedElements=62  maxUsedOprStack=5  ImgListLen=1  NeedStats=no  GotStats=no  NeedHsl=no
EntireImage
  0: Constant val=3 'onull' nArgs=0 ndx=0  push
  1: Control val=0 'copyto' nArgs=1 ndx=0  NO push  CopyTo ==> mrad
  2: Constant val=0 'onull' nArgs=0 ndx=0  push
  3: Control val=0 'copyto' nArgs=1 ndx=1  NO push  CopyTo ==> blur
  4: Function val=0 'u0' nArgs=0 ndx=0  push
  5: Control val=0 'copyto' nArgs=1 ndx=2  NO push  CopyTo ==> lmax
  6: Function val=0 'u0' nArgs=0 ndx=0  push
  7: Control val=0 'copyto' nArgs=1 ndx=3  NO push  CopyTo ==> lmin
  8: Constant val=49 'onull' nArgs=0 ndx=0  push
  9: Control val=0 'copyto' nArgs=1 ndx=4  NO push  CopyTo ==> nPix
  10: Control val=0 'copyfrom' nArgs=0 ndx=0  push  CopyFrom <== mrad
  11: Operator val=0 '-' nArgs=1 ndx=0  push
  12: Control val=0 'copyto' nArgs=1 ndx=5  NO push  CopyTo ==> dy
  13: Control val=0 'copyfrom' nArgs=0 ndx=5  push  CopyFrom <== dy  <==dest(1)
  14: Control val=0 'copyfrom' nArgs=0 ndx=0  push  CopyFrom <== mrad
  15: Operator val=0 '<=' nArgs=2 ndx=0  push
  16: Control val=0 'ifzerogoto' nArgs=1 ndx=45  push
  17: Control val=0 'zerstk' nArgs=0 ndx=-2  NO push
  18: Control val=0 'copyfrom' nArgs=0 ndx=0  push  CopyFrom <== mrad
  19: Operator val=0 '-' nArgs=1 ndx=0  push
  20: Control val=0 'copyto' nArgs=1 ndx=6  NO push  CopyTo ==> dx
  21: Control val=0 'copyfrom' nArgs=0 ndx=6  push  CopyFrom <== dx  <==dest(1)
  22: Control val=0 'copyfrom' nArgs=0 ndx=0  push  CopyFrom <== mrad
  23: Operator val=0 '<=' nArgs=2 ndx=0  push
  24: Control val=0 'ifzerogoto' nArgs=1 ndx=43  push
  25: Control val=0 'zerstk' nArgs=0 ndx=-2  NO push
  26: Constant val=0 'onull' nArgs=0 ndx=0  push
  27: Control val=0 'copyfrom' nArgs=0 ndx=6  push  CopyFrom <== dx
  28: Control val=0 'copyfrom' nArgs=0 ndx=5  push  CopyFrom <== dy
  29: Function val=0 'up[]' nArgs=3 ndx=0  push
  30: Control val=0 'copyto' nArgs=1 ndx=7  NO push  CopyTo ==> pix
  31: Control val=0 'copyfrom' nArgs=0 ndx=7  push  CopyFrom <== pix
  32: Operator val=0 '+=' nArgs=1 ndx=1  NO push  <==> blur
  33: Control val=0 'copyfrom' nArgs=0 ndx=2  push  CopyFrom <== lmax
  34: Control val=0 'copyfrom' nArgs=0 ndx=7  push  CopyFrom <== pix
  35: Function val=0 'max' nArgs=2 ndx=0  push
  36: Control val=0 'copyto' nArgs=1 ndx=2  NO push  CopyTo ==> lmax
  37: Control val=0 'copyfrom' nArgs=0 ndx=3  push  CopyFrom <== lmin
  38: Control val=0 'copyfrom' nArgs=0 ndx=7  push  CopyFrom <== pix
  39: Function val=0 'min' nArgs=2 ndx=0  push
  40: Control val=0 'copyto' nArgs=1 ndx=3  NO push  CopyTo ==> lmin
  41: Operator val=0 '++' nArgs=0 ndx=6  NO push  <==> dx
  42: Control val=0 'gotochk' nArgs=0 ndx=21  NO push
  43: Operator val=0 '++' nArgs=0 ndx=5  NO push  <==> dy  <==dest(1)
  44: Control val=0 'gotochk' nArgs=0 ndx=13  NO push
  45: Control val=0 'zerstk' nArgs=0 ndx=-2  NO push  <==dest(1)
  46: Control val=0 'copyfrom' nArgs=0 ndx=4  push  CopyFrom <== nPix
  47: Operator val=0 '/=' nArgs=1 ndx=1  NO push  <==> blur
  48: Control val=0 'copyfrom' nArgs=0 ndx=1  push  CopyFrom <== blur
  49: Constant val=4 'onull' nArgs=0 ndx=0  push
  50: Function val=0 'u0' nArgs=0 ndx=0  push
  51: Control val=0 'copyfrom' nArgs=0 ndx=1  push  CopyFrom <== blur
  52: Operator val=0 '-' nArgs=2 ndx=0  push
  53: Operator val=0 '*' nArgs=2 ndx=0  push
  54: Operator val=0 '+' nArgs=2 ndx=0  push
  55: Control val=0 'copyto' nArgs=1 ndx=8  NO push  CopyTo ==> shp
  56: Control val=0 'copyfrom' nArgs=0 ndx=8  push  CopyFrom <== shp
  57: Control val=0 'copyfrom' nArgs=0 ndx=2  push  CopyFrom <== lmax
  58: Function val=0 'min' nArgs=2 ndx=0  push
  59: Control val=0 'copyfrom' nArgs=0 ndx=3  push  CopyFrom <== lmin
  60: Function val=0 'max' nArgs=2 ndx=0  push
  61: Control val=0 'copyto' nArgs=1 ndx=8  push  CopyTo ==> shp
User symbols (9):
th=0 us=0 'mrad': 3
th=0 us=1 'blur': 0.13332967
th=0 us=2 'lmax': 0.18012802
th=0 us=3 'lmin': 0.07105587
th=0 us=4 'nPix': 49
th=0 us=5 'dy': 4
th=0 us=6 'dx': 4
th=0 us=7 'pix': 0.16180135
th=0 us=8 'shp': 0.07105587
th=1 us=0 'mrad': 3
th=1 us=1 'blur': 0.13125552
th=1 us=2 'lmax': 0.16180135
th=1 us=3 'lmin': 0.07105587
th=1 us=4 'nPix': 49
th=1 us=5 'dy': 4
th=1 us=6 'dx': 4
th=1 us=7 'pix': 0.16180135
th=1 us=8 'shp': 0.16180135
th=2 us=0 'mrad': 3
th=2 us=1 'blur': 0.13101549
th=2 us=2 'lmax': 0.16283497
th=2 us=3 'lmin': 0.07105587
th=2 us=4 'nPix': 49
th=2 us=5 'dy': 4
th=2 us=6 'dx': 4
th=2 us=7 'pix': 0.16180135
th=2 us=8 'shp': 0.10670083
th=3 us=0 'mrad': 0
th=3 us=1 'blur': 0
th=3 us=2 'lmax': 0
th=3 us=3 'lmin': 0
th=3 us=4 'nPix': 0
th=3 us=5 'dy': 0
th=3 us=6 'dx': 0
th=3 us=7 'pix': 0
th=3 us=8 'shp': 0
th=4 us=0 'mrad': 0
th=4 us=1 'blur': 0
th=4 us=2 'lmax': 0
th=4 us=3 'lmin': 0
th=4 us=4 'nPix': 0
th=4 us=5 'dy': 0
th=4 us=6 'dx': 0
th=4 us=7 'pix': 0
th=4 us=8 'shp': 0
th=5 us=0 'mrad': 0
th=5 us=1 'blur': 0
th=5 us=2 'lmax': 0
th=5 us=3 'lmin': 0
th=5 us=4 'nPix': 0
th=5 us=5 'dy': 0
th=5 us=6 'dx': 0
th=5 us=7 'pix': 0
th=5 us=8 'shp': 0
th=6 us=0 'mrad': 0
th=6 us=1 'blur': 0
th=6 us=2 'lmax': 0
th=6 us=3 'lmin': 0
th=6 us=4 'nPix': 0
th=6 us=5 'dy': 0
th=6 us=6 'dx': 0
th=6 us=7 'pix': 0
th=6 us=8 'shp': 0
th=7 us=0 'mrad': 0
th=7 us=1 'blur': 0
th=7 us=2 'lmax': 0
th=7 us=3 'lmin': 0
th=7 us=4 'nPix': 0
th=7 us=5 'dy': 0
th=7 us=6 'dx': 0
th=7 us=7 'pix': 0
th=7 us=8 'shp': 0

Here is a Julia set, taken from official IM documentation. As an alternative to an IM script as shown above, just the "-fx" is read from text file julia.fx:

Xi=2.4*i/w-1.2;
Yj=2.4*j/h-1.2;
for (pixel=0.0, (hypot(Xi,Yj) < 2.0) && (pixel < 1.0),
  delta=Xi^2-Yj^2;
  Yj=2.0*Xi*Yj+0.2;
  Xi=delta+0.4;
  pixel+=0.00390625
);
pixel == 1.0 ? 0.0 : pixel
%FXNEW%magick ^
  -size 600x600 xc:Black -colorspace Gray ^
  -fx "@julia.fx" -auto-level ^
  fxn_julia.png
fxn_julia.pngjpg

Some more examples, mostly adapted from the official IM documentation:

Create an identity absolute displacement map:

%FXNEW%magick ^
  -size 400x400 xc:#008 ^
  -channel R -fx "i/(w-1)" ^
  -channel G -fx "j/(h-1)" ^
  +channel ^
  fxn_iadm.png
fxn_iadm.png

Create a thing:

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx "sin((i-w/2)*(j-h/2)/w)/2+.5" ^
  fxn_grad.png
fxn_grad.png

Create a cone effect:

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx "rr=hypot(i/w-.5, j/h-.5); 1-rr*1.42" ^
  fxn_cone.png
fxn_cone.png

Create a spherical effect:

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx 'xx=i/w-.5; ^
       yy=j/h-.5; ^
       rr=xx*xx+yy*yy; ^
       1-rr*4' ^
  fxn_spher.png
fxn_spher.png

Repeat previous, offsetting the peak to (0.2, 0.75).
"%[fx:...]" avoids recalculating constants at every pixel.

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx 'px = %%[fx:log(0.5)/log(0.2)]; ^
       py = %%[fx:log(0.5)/log(0.75)]; ^
       xx = pow(i/w,px) - .5; ^
       yy = pow(j/h,py) - .5; ^
       rr = xx*xx+yy*yy; ^
       1-rr*4' ^
  fxn_spher2.png
fxn_spher2.png

Create a rectangular fade-off effect:

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx "(1-(2*i/w-1)^4)*(1-(2*j/h-1)^4)" ^
  fxn_rfo.png
fxn_rfo.png

Create an angular gradient:

%FXNEW%magick ^
  -size 400x400 xc: ^
  -set colorspace Gray ^
  -fx ".5 - atan2(j-h/2,w/2-i)/pi/2" ^
  fxn_angr.png
fxn_angr.png

Create a "gradient circular mean hue":

%FXNEW%magick ^
  -size 400x400 xc: +size xc:red xc:blue xc:lime ^
  -set colorspace sRGB ^
  -colorspace HSB -channel R ^
  -fx 'aa = u[1]*2*pi; ^
       ba = u[2]*2*pi; ^
       ca = u[3]*2*pi; ^
       ar = 1 / max (1, hypot (i-200,j-40 ) ); ^
       br = 1 / max (1, hypot (i-40, j-280) ); ^
       cr = 1 / max (1, hypot (i-360,j-360) ); ^
       nr = ar+br+cr; ^
       mod(atan2( ( sin(aa)*ar + sin(ba)*br + sin(ca)*cr )/nr, ^
                  ( cos(aa)*ar + cos(ba)*br + cos(ca)*cr )/nr ^
                )/(2*pi)+1, 1)' ^
  -channel 1,2 -evaluate set 100%% ^
  +channel ^
  -set colorspace HSB -colorspace sRGB ^
  fxn_gcmh.png
fxn_gcmh.png

Add random noise to an image:

%FXNEW%magick ^
  toes.png ^
  -fx 'iso=32; ^
  rone=rand(); ^
  rtwo=rand(); ^
  myn=sqrt(-2*ln(rone)) * cos(2*Pi*rtwo); ^
  myntwo=sqrt(-2*ln(rtwo)) * cos(2*Pi*rone); ^
  pnoise=sqrt(p)*myn*sqrt(iso) * channel(4.28,3.86,6.68)/255; ^
  max(0,p+pnoise)' ^
  fxn_nse.png
fxn_nse.pngjpg

Future

In principle, the input string could be translated into GPU code. For large images, this might give great performance improvement.

At run-time there is a stack for values. (Actually, one stack per thread.) Values are pushed and popped as required. For example, the input string "3 + 4" will push two values, then pop them both to add them. This might be optimized.

For performance at run-time, all references to variables or locations in the object code are direct indexes into an array. No searches are required at run-time. However, searches are required at translation, for example to test whether a token is a user-defined symbol. For simplicity, these are simple linear searches. If performance of translation is a problem, these could be improved.

For performance reasons, the code will calculate image statistics (such as mean and standard_deviation) only if they are required. It may not know which images in the list need statistics until it is evaluating expressions, because the input string might contain something like:

-fx "myImgNum = complex_expression;
mySD = u[myImgNum].standard_deviation ; ..."

But at that time in a "-fx" operation, multi-threading is occurring, and we can't easily calculate the standard_deviation and make it available to other threads. For simplicity in a "-fx" operation, if any image needs any statistics, the code calculates all statistics for all the images. It does this once only per image. If this causes a performance problem, the algorithm might be enhanced.

For "%[fx:...]" and "%[hex:...]" and "%[pixel:...]", image statistics are handled differently. For these, the fx code is called for every image, but only one pixel is traversed: just the top-left corner instead of the entire image. If that code calculated statistics for all images each time it was called, then a large number of images would cause a performance problem. So the run-time code at each pixel (there is only one) calculates statistics for the relevant image. If the expression requires more than one statistic (for example, mean and standard_deviation) then this code is inefficient because it would call ImageStat() multiple times. In addition, if the expression needs a statistic for a constant image, eg "u.mean" which is always the first image in the list, then at each image in the list the code will laboriously re-calculate the same statistic of that first image. Again, the algorithm could be enhanced for performance, at a cost to complexity.

Infinite loops at run-time could be detected.

Operations for "break" and "continue" could be added, for looping functions such as "for ()". But if we "break" from a loop, what value should be returned by the looping function?

Perhaps also a function "return (statement_list)", to exit the "-fx2" early.

Symbols i and j return the current x and y coordinates. We could also have symbols that return the current image number and channel number.

A cleaner syntax could be devised for specifying the four dimensions of (image_number, x, y, channel_number), each of them being a general expression. This could also allow a loop through channels, and give access to meta channels.

Constant expressions could be optimized, eg "1/3" will laboriously divide one by three at every pixel. This might be automatically optimized, or programatically eg "const OneThird=1/3", where the keyword "const" evaluates the expression at translation instead of at run-time. A technique that already works in the new -fx2 is to use an "%[fx:...]" expression. For example:

OneThird = %[fx:1/3]

At run-time, GetImageDepth() can be repeatedly called. This could be moved to CollectStatistics() that is called only once.

An expression with u is translated into opcodes fU or fU0 or fUP to minimize tests at run-time. This could be extended into specialist opcodes that have no channel qualifier, or no ImgAttr qualifier, etc.

There might also be optimisations for pixel offsets that are integers.

The opcode rZerStk is needed because control-flow functions such as if() and for() push values that may not be needed. If the translation was more intelligent, we wouldn't need that opcode, giving a small performance improvement.

The operator stack is not needed at run-time, so its memory could be released then.

User-defined functions would be useful, especially if they could be read from a file, a library of functions.

New keywords can be easily added. For example, a symbol "widthProportion" could have the same effect as "i/(w-1)" with improved performance as it would have one RPN entry instead of five.

Source code

There is one new structure "FxInfo" and four functions:

... to replace the existing structure and functions.

The language translation, tokenisation, validation and reporting are done in "AcquireFxInfo()". Run-time processing for "-fx" is done in FxImage(), or for "%[fx:...]" and "%[hex:...]" and "%[pixel:...]" in FxEvaluateChannelExpression(). Wrap-up is done in DestroyFxInfo().

The file fx2.zip contains the file fx2.inc, which contains the source code of the structure and four functions, and their dependencies.


All images on this page were created by the commands shown, using:

%FXOLD%magick identify -version
Version: ImageMagick 7.1.1-20 (Beta) Q32-HDRI x86_64 66c30fc22:20231002 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenCL OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig freetype heic jbig jng jpeg lcms ltdl lzma pangocairo png raqm raw rsvg tiff webp wmf x xml zip zlib
Compiler: gcc (11.3)
%FXNEW%magick identify -version
Version: ImageMagick 7.1.1-20 (Beta) Q32-HDRI x86_64 66c30fc22:20231002 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenCL OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig freetype heic jbig jng jpeg lcms ltdl lzma pangocairo png raqm raw rsvg tiff webp wmf x xml zip zlib
Compiler: gcc (11.3)

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

Source file for this web page is fxnew.h1. To re-create this web page, execute "procH1 fxnew".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 8-January-2022.

Page created 04-Nov-2023 14:44:10.

Copyright © 2023 Alan Gibson.