PixelMath is another tool that has been in need of some renewal for a long time. I have revised it deeply in PixInsight 1.8.0 to make it more powerful, faster, and more sophisticated. After all, this is one of the tools that better reflect
the PixInsight way of understanding image processing, and as such it deserves special attention. So this is just the first of a series of programmed improvements that we'll release during the 1.8 cycle.
New Expression Editor WindowOn the user interface side, the new version of PixelMath has a redesigned Expression Editor. The structure remains similar to previous versions, but has more functionality and is easier to use.
The main difference is that all PixelMath expressions and symbols are now grouped on a single Expression Editor dialog, so you can edit all your expressions and parse them for verification without needing to close and open several dialogs.
The information given for functions, operators and symbol definition function (new in this version) has been revised, corrected and expanded. Just select one item on the right panel and you'll get the corresponding description on the left-central box.
Rescale Result is Now Disabled By DefaultYes, you read it well: d.i.s.a.b.l.e.d. I am sure that many of you will be extremely happy with this change. The fact that this option was enabled by default in previous versions of PixelMath had become a very annoying "feature"; many users—me included—were getting sick of turning it off.
The default enabled state of this option came from the old days of PixInsight LE, where PixelMath was mainly intended to subtract or divide an image by its DBE-generated background model, and this task requires rescaling.
New Symbol Definition FunctionsBefore describing this new feature, it will be convenient to refresh your PixelMath concepts with a brief introduction to symbols. Symbols allow you to reserve some words or letters as variables or constants that you're going to use in your expressions. If you don't declare a word as a symbol, it will be interpreted as the identifier of an existing image by the PixelMath parser, and if the image in question doesn't exist upon execution, you'll get a runtime error.
There are two types of symbols in PixelMath: variables and constants. A variable can be assigned new values as PixelMath expressions are executed, while the value of a constant is defined at the beginning of the process and remains, well, constant.
Variables allow you to simplify and optimize your expressions. For example, consider the following expression:
iif( (x() < w()/4 || x() > w()/4 + 120) && (y() < h()/2 || y() > h()/2 + 260), $T, ~$T )This expression inverts a cross section of the target image; for example:
The expression above is full of function calls, which make it difficult to understand and maintain. With variables we can simplify and organize it much better:
x = x();
y = y();
x0 = w()/4;
y0 = h()/2;
iif( (x < x0 || x > x0+120) && (y < y0 || y > y0+260), $T, ~$T )This requires four symbols to be declared as variables: x, y, x0 and y0. Note that there's no problem in using a function's name as a symbol; PixelMath's parser knows when you are referring to a function without ambiguity, thanks to the function call operator "()".
Now if you want to vary the width and height of the cross section being inverted with the above expression, you have to change the numbers 120 and 260. Instead of doing this directly on the expression, you can declare two symbols as constants, namely:
W = 120, H = 260and edit the expression as follows:
x = x();
y = y();
x0 = w()/4;
y0 = h()/2;
iif( (x < x0 || x > x0+W) && (y < y0 || y > y0+H), $T, ~$T )so each time you want to change these dimensions you can do it much more easily by just assigning new values to W and H. Note that the PixelMath language is case-sensitive, so 'w' is different from 'W'.
Now that you know how PixelMath symbols work, let me point out an important limitation that symbols have had in previous versions of PixInsight: limited initialization capabilities. On one hand, variables could only be declared but not explicitly initialized, so they always had an initial value of zero. On the other hand, constants could only be initialized with literal numeric values. These limitations have been overcome to a large extent in the latest version.
Symbols can now be initialized with
symbol definition functions. This extends their usability and performance considerably. Formally, a symbol definition function isn't very different from a common function used in PixelMath expressions:
symbol = function( parameters )
where
symbol is the identifier of a symbol being initialized,
function is the function's identifier, and
parameters is a comma-separated list of function parameters that can often be empty (that is, optional). Below is a complete list of the symbol initialization functions supported by the current version of PixelMath included in PixInsight 1.8.0. In all the formal descriptions below:
* Items between square brackets are [optional].
* Items written in italics are
metasymbols, or formal syntax elements.
*
image function parameters are identifiers of existing images. If the identifier of a nonexistent image is specified, a runtime error occurs. When we write "
image=$T", that means that by default, if no image is specified, the target image (that is, the image where PixelMath is being executed) will be used.
*
channel function parameters are valid image channel indexes. Valid channel indexes are integers in the range from zero to the number of channels in the image minus one. Specifying a nonexistent channel always causes a runtime error upon execution.
symbol = keyword_value( [image=$T,] keyword )The symbol will be assigned the value of a numeric or Boolean FITS header keyword in the specified
image.
keyword is the name of the FITS keyword whose value will be retrieved. Boolean keywords generate 0 (false) or 1 (true) symbol values. FITS keyword names are case-insensitive. If the specified keyword is not defined for the image, or if it is defined with a non-numeric and non-Boolean value, a runtime error will occur.
symbol = keyword_defined( [image=$T,] keyword )The symbol value will be either one, if the specified FITS header
keyword is defined in the image, or zero if the keyword is not defined. FITS keyword names are case-insensitive.
symbol = width( [image=$T] )The symbol will be initialized with the width in pixels of the specified image.
symbol = height( [image=$T] )The symbol will be initialized with the height in pixels of the specified image.
symbol = area( [image=$T] )The symbol value is the area of the specified image in square pixels.
symbol = invarea( [image=$T] )The symbol value is the reciprocal of the area of the specified image in square pixels.
symbol = iscolor( [image=$T] )The result is one if the specified image is in the RGB color space; zero if it is a grayscale monochrome image.
symbol = maximum( [image=$T[, channel]] )If no channel index is specified, the symbol value is the maximum pixel value in the specified image. If a valid channel index is specified, the value will be the maximum pixel sample value in the specified image channel.
symbol = minimum( [image=$T[, channel]] )If no channel index is specified, the symbol value is the minimum pixel value in the specified image. If a valid channel index is specified, the value will be the minimum pixel sample value in the specified image channel.
symbol = median( [image=$T[, channel]] )The symbol is initialized with the median pixel value in the specified image, or the median pixel sample value if a valid channel index is specified.
symbol = mdev( [image=$T[, channel]] )The symbol is initialized with the median absolute deviation from the median (MAD) of the specified image, or the median absolute deviation of pixel sample values if a valid channel index is specified.
symbol = adev( [image=$T[, channel]] )The symbol is initialized with the average absolute deviation from the median of the specified image, or the average absolute deviation of pixel sample values if a valid channel index is specified.
symbol = sdev( [image=$T[, channel]] )The symbol is initialized with the standard deviation from the mean of the specified image, or the standard deviation of pixel sample values if a valid channel index is specified.
symbol = mean( [image=$T[, channel]] )The symbol value is the arithmetic mean of the specified image, or the mean of pixel sample values if a valid channel index is specified.
symbol = modulus( [image=$T[, channel]] )The symbol value is the modulus (sum of absolute values) of the specified image, or the modulus of pixel sample values if a valid channel index is specified.
symbol = ssqr( [image=$T[, channel]] )The symbol value is the sum of square pixel values of the specified image, or the sum of square pixel sample values if a valid channel index is specified.
symbol = asqr( [image=$T[, channel]] )The symbol value is the mean of square pixel values of the specified image, or the mean of square pixel sample values if a valid channel index is specified.
symbol = pixel( [image=$T,] x, y )The symbol is initialized with the pixel value of an image at the specified coordinates. Out-of-range coordinates are legal and generate zero symbol values.
symbol = pixel( image, x, y, channel )The symbol is initialized with the pixel sample value of an image at the specified coordinates, for the specified channel. Out-of-range coordinates are legal and generate zero symbol values. Nonexistent channel indices cause runtime errors, as usual.
symbol = init( value )Variable initialization. The symbol will be declared as a thread-local variable (this is explained in the next section) with the specified initial value, which must be a literal numeric expression. By default, if no explicit initial value is specified, thread-local variables are initialized to zero.
symbol = global( op[, value] )Global variable initialization. The symbol will be declared as a global variable with the specified global operator and initial value. The mandatory
op parameter is a global operator specification, which can be one of + or -. For example:
n = global(+)will declare a global additive variable
n with a default initial zero value, while
x = global(*,3.21)declares a global multiplicative variable
x whose initial value is 3.21. Global variables can only play
lvalue roles in expressions. In practice this means that they can only occur at the left hand of assignment operators. For example:
x = $T + nis illegal if
n has been declared as above. Furthermore, global variables are specialized for additive or multiplicative operations. For example, if
n has been declared as above then it is additive, and an expression such as:
n *= $Tis illegal because an additive global variable cannot be involved in multiplicative expressions. However,
n -= $T < 0.01is valid because addition and subtraction are both additive operations. Of course, the same happens with multiplication and division for multiplicative global variables.
By default (if no explicit initial value is specified), additive global variables are initialized to zero and multiplicative global variables are initialized to one. The final values of all global variables are always reported on the console after PixelMath execution.
Global VariablesAs described above, the
global symbol definition function, namely:
symbol = global( op[, value] )allows us to declare special global variables whose final values are always reported on the console at the end of the PixelMath process. Global variables make it possible to perform image analysis tasks that were impossible with previous versions of the PixelMath tool.
For example, suppose you want to know how many pixels have values in the interval from 0.1 to 0.25 in a given image. With previous versions of PixelMath, the aswer is simple: you cannot. A normal variable (which we now call a
thread-local variable) does not work because its value is evaluated for each pixel and then discarded; there's no way to accumulate it.
With PixInsight 1.8.0, this task is really easy with the following expression:
n += $T >= 0.25 && $T <= 0.75and the following symbol declaration:
n = global(+)After running this PixelMath instance for the rose image shown above, the console shows the following:
PixelMath: Processing view: IMG_1472
Executing PixelMath expression: combined RGB/K channels: n += $T >= 0.25 && $T <= 0.75: done
* Global variables:
n(+) = { 15612530, 7279422, 7906687 }The reported components of the
n global variable correspond to the red, green and blue channels of the target image, respectively.
New Generate Output OptionSo far the PixelMath process has been a pure
pixel generator: it either modified the pixels of its target image—when executed directly on an image—, or generated the pixels of a newly created image—when executed globally, or with the
create new image option enabled.
This is not necessarily true anymore. The new
generate output option can be disabled to prevent generation of output pixel data. This can be useful in cases where we are only interested in the
side effects of PixelMath's execution, not in the pixel values resulting from expression evaluation. A good example is the pixel counting operation that we have described in the previous section. Obviously, when you enable this option that's because you are using global variables to accumulate some results, and you only want to get the final variable values, without altering the target image. Of course, this option is disabled by default.
New Single Threaded OptionPixelMath is a fully parallelized process. It will use all available processor cores (unless you explicitly limit parallel execution via global preferences) for maximum performance. That's great for sure, but is this what we
always want to happen?
The answer would invariably be yes, except for the fact that there are some important tasks that are incompatible with parallel execution. These tasks were possible with previous versions of PixelMath, but they required you to disable parallel execution through a global preferences option (or, equivalently, using the
parallel command from the console). However, with this approach those PixelMath instances implementing non-parallelizable tasks depend on a particular global configuration, which is very problematic. Now you can disable parallel execution for specific PixelMath instances, which makes perfectly feasible the implementation of these tasks.
A good example of non-parallelizable task is calculation of integral images. In an integral image, each pixel at coordinates {x,y} is the sum of all pixels at coordinates {0 <= i <= x, 0 <= j <= y} in the original image. The PixelMath expression to compute an integral image is therefore:
x += $Twhere x must be declared as a variable without initialization (that is, initialized to zero by default). If we apply this instance with parallel execution enabled the result is funny:
Do you guess what's this? Here's a hint: I have run this PixelMath example on a last-generation four-core iMac computer, which thanks to hyperthreading is seen as an eight-core machine by the operating system and hence by PixInsight. So what we are seeing on the screenshot is just the result of eight partial results of the evaluated expression, one for each running thread. This happens because variables have been implemented as thread-local data in PixelMath. With the new
single threaded option enabled, we get the correct integral image:
Note the enabled
rescale result option in this case. Always remember that now this option is disabled by default.
New Statistical FunctionsThe following functions are now part of the standard PixelMath set. Some of them already existed in previous versions, but their formal descriptions have changed in the new version.
For each of these functions there are two versions: one that takes a set of two or more arguments, and another that takes a single image argument. In the latter case, the function is an
invariant subexpression, whose value is computed before PixelMath execution and works as a constant during the whole task.
adev( a, b[, ...] )
adev( image )Average absolute deviation from the median.
asqr( a, b[, ...] )
asqr( image )Mean of squares.
bwmv( a, b[, ...] )
bwmv( image )Biweight midvariance.
mdev( a, b[, ...] )
mdev( image )Median absolute deviation (MAD) from the median.
mean( a, b[, ...] )
mean( image )Arithmetic mean.
med( a, b, c[, ...] )
med( image )Median value.
min( a, b[, ...] )
min( image )Minimum value.
mod( a, b[, ...] )
mod( image )Modulus (sum of absolute values).
norm( a, b[, ...] )
norm( image )Norm (sum of values).
pbmv( a, b[, ...] )
pbmv( image )Percentage bend midvariance.
Qn( a, b[, ...] )
Qn( image )Qn scale estimate of Rousseeuw and Croux.
sdev( a, b[, ...] )
sdev( image )Standard deviation from the mean.
Sn( a, b[, ...] )
Sn( image )Sn scale estimate of Rousseeuw and Croux.
ssqr( a, b[, ...] )
ssqr( image )Sum of squares.
New Geometrical FunctionsThese functions allow to perform basic geometrical operations.
d2line( x1, y1, x2, y2 )Returns the distance from the current position to the straight line passing through two points {x1,y1} and {x2,y2}.
d2seg( x1, y1, x2, y2 )Returns the distance from the current position to the straight line segment defined by its two ending points {x1,y1} and {x2,y2}. Here's an example:
with the following expression:
d2seg( 500, 500, 750, 200 ) < 16 || d2seg( 600, 900, 50, 75 ) < 40inellipse( xc, yc, rx, ry )Returns one if the current coordinates are included in the specified ellipse with center at {xc,yc} and semi-axes rx and ry. Returns zero if the current coordinates are exterior to the specified ellipse.
inrect( x0, y0, width, height )Returns one if the current coordinates are inside the specified rectangular region with top left corner at {x0,y0} and the specified width and height in pixels. Returns zero if the current coordinates are exterior to the specified rectangle.
maxd2rect( x0, y0, width, height )Returns the maximum distance in pixels from the current coordinates to the specified rectangle, when the current position is interior to the rectangular region. Returns -1 if the current position is exterior to the specified rectangular region.
mind2rect( x0, y0, width, height )Returns the minimum distance in pixels from the current coordinates to the specified rectangle, when the current position is interior to the rectangular region. Returns -1 if the current position is exterior to the specified rectangular region.
pangle( [xc, yc] )Current polar angle in radians, with respect to an arbitrary center point {xc,yc}. If not specified, the default center is the center of the central pixel of the target image.
rdist( [xc, yc] )Current radial distance in pixels, with respect to an arbitrary center point {xc,yc}. If not specified, the default center is the center of the central pixel of the target image.
xperp2line( x1, y1, x2, y2 )Returns the X-coordinate of the intersection of the line through the current position perpendicular to the line defined by two points {x1,y1} {x2,y2}. This function is useful to select pixels located at one side of a line with arbitrary slope. For example, this expression:
xperp2line( 0, 0, w()-250, h() ) > 300produces the following result on a 1024x1024 grayscale image:
yperp2line( x1, y1, x2, y2 )Returns the Y-coordinate of the intersection of the line through the current position perpendicular to the line defined by two points {x1,y1} {x2,y2}.