Home | Download | Documents | Tips & Tutorials | Code Library | Help / User Group | Order

Loops and Fractals



This lesson requires FM Beta version 0.4.2 or later.

This lesson shows you how to control the order in which your filter processes image pixels by using FM's ForEveryPixel and ForEveryTile handlers in conjunction with for-loops.

This lesson also introduces floating-point arithmetic and the FM built-in functions abs(), src(), pset(), setCtlRange(), updateProgress(), and testAbort().

Throughout this lesson we assume that the host graphics application is Adobe Photoshop 4.0.  The instructions for other hosts may vary.
 

**PS4** Photoshop 4.0-specific information is flagged with this icon.
**PSP** Information for Paint Shop Pro is flagged with this icon.
**NYI** Features that are not yet implemented are flagged with this icon.
**TIP** Useful tips, techniques, and hints are marked with this icon.
**WARN** This icon indicates a feature that may cause harmful results if improperly used.

 

1. Rolling Your Own Loops

The Adobe Filter Factory introduced a simple yet surprisingly powerful image processing paradigm:  An image is always processed pixel-by-pixel, from left-to-right and top-to-bottom, similar to the way we read a page of text.  For each pixel, the red (R), green (G), blue (B), and possibly the alpha (A) channel is assigned a computed value in turn.

The Filter Designer's sole task is to create a formula for each pixel channel (or plane) that defines the value assigned to each pixel channel as it is processed.

Many image processing algorithms fit naturally into this paradigm, and even more algorithms can be forced to fit (albeit less naturally).

However, there are large classes of algorithms which are difficult to express in this paradigm.  FM gives the Filter Designer much greater freedom by allowing the FD to take control of the image processing at the tile, row, or pixel level.  At each level, the FD can explicitly control the order of processing by coding his own loops using the various control constructs of the FF+ language.

In this lesson we will concentrate on controlling the image processing at the tile or pixel level using for-loops.

 

2. ForEveryPixel

Consider the following program that converts an image to monochrome by assigning to each pixel channel the weighted intensity of the R, G, and B values of that pixel in the input image:
 
%ffp 

Title:"Monochrome" 

R:i 
G:i 
B:i

 
This can be rewritten as a ForEveryPixel handler as follows:
 
%ffp 

Title:"Monochrome" 

ForEveryPixel: 
{ 
    R=i; G=i; B=i; 
}

 
The ForEveryPixel handler is invoked once for each pixel in the input image, using the same left-to-right, top-to-bottom order that Filter Factory uses.  At each invocation, the r, g, b, and a built-in variables are assigned the values of the red, green, blue, and alpha channels, respectively, for the current input pixel, just as in Filter Factory. Most other built-in variables such as i, u, v, m, d, x, and y are also assigned the appropriate values, just as in Filter Factory.  However, the built-in variables c and z are undefined in the ForEveryPixel handler, since we are processing an entire pixel at a time, not an individual channel within a pixel.  The built-in variables R, G, B, and A are initialized to the values of r, g, b, and a, respectively, at the start of every invocation of ForEveryPixel.  The FD is free to (re)assign any desired values to these variables within the handler.  Upon exit from the handler, the values of these variables are clamped to the range [0,255] and assigned to the red, green, blue, and alpha channels of the corresponding pixel in the output image.
 
 Monochrome  

 

3. ForEveryTile

The ForEveryPixel handler is nice, but it doesn't really give us any more power than the Filter Factory R:, G:, B:, and A: "handlers".

At the highest level, FilterMeister processes an image one "tile" at a time, where a tile is a large rectangular section of the input image. The size of each tile is chosen to minimize physical RAM requirements while processing an image.  The tile size also depends on the properties of the image processing algorithm.  Some algorithms are inherently not tileable; for such "untileable" algorithms, the tile size is set equal to the size of the entire input image, and only one such tile is processed per filter application.

By default, FM takes a conservative approach and considers an algorithm to be untileable.  So in the remainder of this lesson, we will assume that each image is processed as a single large tile.  The subtleties of writing "tileable" algorithms will be discussed in a future lesson.

The ForEveryTile handler is invoked once for each tile in the input image (which in the following examples means it is invoked once per filter application, with a single tile containing the entire input image).

Within the ForEveryTile handler, the Filter Designer is free to process the pixels in any desired order (or even not to process some of them). The FF+ for-loop construct is useful for controlling the order in which pixels are processed.

 

4. For-loops

A for-loop has the same syntax as in the C language, and generally has the form:

    for ( <initialization> ; <while-condition> ; <iteration-update> )
    {
        <body-statements>;
    }

where:

Since a for-loop is just another kind of statement, it may appear within the body of some other for-loop.  Such "nested" for-loops are commonly used to iterate through multidimensional data structures, such as a 2-D image.

In the following program, we use nested for-loops to iterate through all the pixels of an image, and all channels within each pixel.  The outer for-loop iterates the x-coordinate (i.e., it iterates left-to-right or column-by-column), the first inner loop iterates the y-coordinate (from top-to-bottom or row-by-row), and the innermost loop iterates through the individual channels of each pixel (the "z" coordinate). Since an inner loop executes many times per iteration of an outer loop, this means that the y-coordinate varies faster than the x-coordinate. Thus, we are processing the image column-by-column rather than row-by-row. This is the transpose of the order in which Filter Factory normally processes an image.
 
%ffp  

Title:"Brightness"  

ctl(5):"Brightness %",val=50,range=(0,100)  

ForEveryTile:  
 

    for (x=0; x < X; x++) {  
        for (y=0; y < Y; y++) {  
            for (z=0; z < Z; z++) {  
                pset(x,y,z,src(x,y,z)*ctl(5)/100);  
            }  
        }  
    }  

    true;  //Done!  

 
**TIP** TIP
Processing an image in column-by-column order rather than row-by-row order may be useful and important for some algorithms. However, since the image is laid out row-by-row in memory, it is generally more efficient to process row-by-row.  Specifically, processing column-by-column may greatly increase the number of page faults for very large images, resulting in slow performance. So if everything else is equal, choose row-by-row rather than column-by-column processing order.
 
The body of the innermost for-loop in the example above is a single statement:

        pset(x, y, z, src(x,y,z)*ctl(5)/100);
 
The FF/FF+ built-in function src(x,y,z) returns the value of channel z for the input pixel at coordinates (x,y).

The FF+ built-in function pset(x,y,z,v) sets channel z of the output pixel at coordinates (x,y) to the value v.  The value of v is clamped to the range [0,255] before making the assignment.

So the effect of the loop body is to take the input pixel, multiply its channels by a percentage between 0 and 100% (determined by control 5), and assign the result to the corresponding output pixel.
 

Brightness
 
Many different algorithms can be implemented simply by replacing the innermost loop body with some other statements in the above "template".

However, the FD is not limited to assigning the output pixel values in any fixed order.  In the following program, we first iterate through all the image pixels in the customary left-to-right, top-to-bottom order, setting the output image to some reduced brightness version of the input image as in the program above.  However, we then use two more for-loops: The first loop iterates from left-to-right across the image, drawing two semi-transparent horizontal red lines; the second for-loop iterates from top-to-bottom, drawing two semi-transparent vertical green lines.  The relative positions of these lines can be controlled by sliders 0 and 1.  This technique can be generalized to implement arbitrary line-drawing, polygon-drawing, circle-drawing, and other 2-D shape-drawing primitives.  Similarly, the technique can be used to implement shape-filling primitives.  A future lesson will introduce FM's built-in set of 2-D drawing functions.
 
%ffp  

Title:"Frame me!"  

ctl(0):"V Inset",val=10,range=(0,Y/2)  
ctl(1):"H inset",val=10,range=(0,X/2)  

ctl(5):"Brightness %",val=50,range=(0,100)  

ForEveryTile:{  

    setCtlRange(0,0,Y/2);  
    setCtlRange(1,0,X/2);  

    for (x=x_start; x < x_end; x++) {  
        for (y=y_start; y < y_end; y++) {  
            for (z= 0; z < Z; z++) {  
                pset(x,y,z,src(x,y,z)*ctl(5)/100);  
            }  
        }  
    }  

    for (x=x_start; x < x_end; x++) {  
        pset(x,ctl(0),0,255);       //set red channel to max 
        pset(x,Y-1-ctl(0),0,255);   //set red channel to max 
    }  

    for (y=y_start; y < y_end; y++) {  
        pset(ctl(1),y,1,255);       //set green channel to max 
        pset(X-1-ctl(1),y,1,255);   //set green channel to max 
    }  

    true;  //done!  

 
Note the following points in the above example:

 
**TIP** TIP
If you set the return value of the ForEveryTile handler to false, FM will call the ForEveryRow and ForEveryPixel handlers in turn. The default ForEveryPixel handler simply copies the input pixels directly to the output pixels, thus overwriting any changes to the output pixels made by the ForEveryTile handler.  Since this is probably not what you want to do, make sure you set the return value of ForEveryTile to true to avoid invoking the default ForEveryPixel handler!
 
 Frame me!

 

5. A Newton Fractal Explorer

Okay, the sample filter programs above have been silly and trivial.  Now let's write a filter that does something more complicated.

The following example is adapted from Chapter 7 of Graphics Programming with Java, by Roger T. Stevens.

Everyone has seen pictures of the space-filling curves termed "fractals" by Benoit Mandelbrot.  One such class of fractals is based on Newton's method for solving polynomial equations.  Without going deeply into the math involved, we simply present the following program that generates fractal images based on solving a 9-th degree polynomial equation using Newton's method.
 

Newton Fractal Explorer 5.0a
 
The template of the ForEveryTile handler is similar to the previous examples.  Two outer for-loops iterate through the y and x coordinates. An inner for-loop is used to solve the equation up to a fixed number of iterations (controlled by slider 3, "Resolution").  The image can be panned left and right (slider 1) or tilted up and down (slider 2).  It can also be zoomed in or out (slider 0).  Hence the title, "Newton Fractal Explorer."
 
**TIP** TIP
The filter can take several seconds to update the proxy preview each time a control is adjusted.  For faster "exploring" of the fractal space, zoom the preview out to a scale of 25% or less while panning and tilting; then zoom the preview back in when you spot something interesting!
 
This program makes extensive use of floating-point arithmetic.  We have not introduced user variable declarations yet, so this example uses the predefined sets of double-precision floating-point variables: x0, x1, ..., x9; y0, y1, ..., y9; and z0, z1, ..., z9.  Many of the FF+ built-in math functions are polymorphic, and accept either integer or floating-point arguments.  For example abs(i0) returns the absolute integer value of integer i0, whereas abs(x0) returns the absolute double-precision floating-point value of variable x0.  Similarly, sin(i0) computes a (scaled) integer sine function, whereas sin(x0) computes the customary floating-point sine value.
 
%FFP  
Category:  "AFH"  
Title:     "Newton Fractal Explorer 5.0a"  
Copyright: "Copyright ©1998, AFH Systems Group"  
Author:    "Alex Hunter <alex@afh.com>"  
Filename:  "afhnewton5.8bf"  

ctl[0]:"- ZOOM +",range=(1,500),val=35  
ctl[1]:"<-- PAN -->", range=(-400,+400),val=0  
ctl[2]:" v-- TILT --^", range=(-300,+300),val=0  
ctl[3]:"Resolution:",  
            range=(1,1024),val=512,pagesize=50,linesize=5  

ForEveryTile:  
{   //Code transliterated from p.94 of 
    //"Graphics Programming with Java"  
    //by Roger T. Stevens.  

    for (y = y_start; y < y_end; y++) {  

        // Update the progress indicator on each new row...  
        updateProgress(y - y_start, y_end - y_start);  

        // Test for user abort request...  
        if (testAbort()) break;  

        for (x = x_start; x < x_end; x++) {  

            x1 = (x - X/2)*100.0/(ctl(0)*X) + ctl(1)/100.0;  
            y1 = (y - Y/2)*75.0/(ctl(0)*Y) - ctl(2)/100.0;  

            for (i0 = 0; i0 < ctl(3); ++i0) {  
                x2 = x1*x1;  
                x3 = x2*x1;  
                x4 = x3*x1;  
                x5 = x4*x1;  
                x6 = x5*x1;  
                x7 = x6*x1;  
                x8 = x7*x1;  
                y2 = y1*y1;  
                y3 = y2*y1;  
                y4 = y3*y1;  
                y5 = y4*y1;  
                y6 = y5*y1;  
                y7 = y6*y1;  
                y8 = y7*y1;  
                z9 = x2 + y2;  
                z9 = z9*z9;  
                z9 = z9*z9;  
                z9 = z9*z9*9.0;  
                y0 = 0.88888888*y1 - (8.0*x7*y1 -  
                                     56.0*x5*y3 +  
                                     56.0*x3*y5 -  
                                      8.0*x1*y7)/z9;  
                x0 = 0.88888888*x1 + (    x8 -  
                                     28.0*x6*y2 +  
                                     70.0*x4*y4 -  
                                     28.0*x2*y6 +  
                                             y8)/z9;  
                if (abs(x0 - x1) < 1e-10 &&  
                    abs(y0 - y1) < 1e-10 ) break;  
                x1 = x0;  
                y1 = y0;  
            }  
            i0 &= 15;  
            if (i0 == 0) {  
               //black  
               pset(x, y, 0, 0);   //R  
               pset(x, y, 1, 0);   //G  
               pset(x, y, 2, 0);   //B  
            }  
            else if (i0 == 1) {  
               //blue  
               pset(x, y, 0, 0);   //R  
               pset(x, y, 1, 0);   //G  
               pset(x, y, 2, 168); //B  
            }  
            else if (i0 == 2) {  
               //green  
               pset(x, y, 0, 0);   //R  
               pset(x, y, 1, 168); //G  
               pset(x, y, 2, 0);   //B  
            }  
            else if (i0 == 3) {  
               //cyan  
               pset(x, y, 0, 0);   //R  
               pset(x, y, 1, 168); //G  
               pset(x, y, 2, 168); //B  
            }  
            else if (i0 == 4) {  
               //red  
               pset(x, y, 0, 168); //R  
               pset(x, y, 1, 0);   //G  
               pset(x, y, 2, 0);   //B  
            }  
            else if (i0 == 5) {  
               //magenta  
               pset(x, y, 0, 168); //R  
               pset(x, y, 1, 0);   //G  
               pset(x, y, 2, 168); //B  
            }  
            else if (i0 == 6) {  
               //brown  
               pset(x, y, 0, 168); //R  
               pset(x, y, 1, 84);  //G  
               pset(x, y, 2, 0);   //B  
            }  
            else if (i0 == 7) {  
               //lightGray  
               pset(x, y, 0, 168); //R  
               pset(x, y, 1, 168); //G  
               pset(x, y, 2, 168); //B  
            }  
            else if (i0 == 8) {  
               //darkGray  
               R = 84;  
               G = 84;  
               B = 84;  
               pset(x, y, 0, 84);  //R  
               pset(x, y, 1, 84);  //G  
               pset(x, y, 2, 84);  //B  
            }  
            else if (i0 == 9) {  
               //lightBlue  
               pset(x, y, 0, 84);  //R  
               pset(x, y, 1, 84);  //G  
               pset(x, y, 2, 255); //B  
            }  
            else if (i0 == 10) {  
               //lightGreen  
               pset(x, y, 0, 84);  //R  
               pset(x, y, 1, 255); //G  
               pset(x, y, 2, 84);  //B  
            }  
            else if (i0 == 11) {  
               //lightCyan  
               pset(x, y, 0, 84);  //R  
               pset(x, y, 1, 255); //G  
               pset(x, y, 2, 255); //B  
            }  
            else if (i0 == 12) {  
               //lightRed  
               pset(x, y, 0, 255); //R  
               pset(x, y, 1, 84);  //G  
               pset(x, y, 2, 84);  //B  
            }  
            else if (i0 == 13) {  
               //lightMagenta  
               pset(x, y, 0, 255); //R  
               pset(x, y, 1, 84);  //G  
               pset(x, y, 2, 255); //B  
            }  
            else if (i0 == 14) {  
               //yellow  
               pset(x, y, 0, 255); //R  
               pset(x, y, 1, 255); //G  
               pset(x, y, 2, 84);  //B  
            }  
            else {  
               //white  
               pset(x, y, 0, 255); //R  
               pset(x, y, 1, 255); //G  
               pset(x, y, 2, 255); //B  
            } //if  
        } //for x  
    } //for y  

    // Reset progress indicator at end of tile.  
    updateProgress(0, 100);  

    true;  //completely handled  
} //ForEveryTile

 
Notice the following points in this example:
                if (abs(x0 - x1) < 1e-10 &&
                    abs(y0 - y1) < 1e-10 ) break;
 
 
**NYI** NOT YET IMPLEMEMENTED
The switch-statement is not yet implemented in release 0.4.4.
   
**NYI** NOT YET IMPLEMEMENTED
 Arrays are not yet implemented in release 0.4.4.
   
**TIP** TIP
The filter progress indicator is normally updated by the default ForEveryRow handler.  However, setting the return value of the ForEveryTile handler to true causes the ForEveryRow handler to be ignored.  So it is necessary to call updateProgress() explicitly within your ForEveryTile handler to provide the user with visual feedback in this case.
   
**NYI** NOT YET IMPLEMEMENTED
The testAbort() function tests for a user-requested abort only during final filter application, not during proxy preview processing.  In a future version of FM, testAbort() will also allow the user to abort processing during a proxy preview update.
 
**TIP** TIP
Since we often want to call testAbort() and updateProgress() at the same place in our filter program, FM provides a shortcut by calling testAbort() implicitly within updateProgress(), and returning the result of testAbort() as the result of updateProgress().  So we can kill two birds with one stone and replace the separate calls to testAbort() and updateProgress() with the following single call: 
  
// Update the progress indicator and check for  
// user abort on each new row... 

if (updateProgress(y - y_start, y_end - y_start)) break; 
 

 
The observant reader may have noted that the fractal image is processed pixel-by-pixel in exactly the same order as performed by the ForEveryPixel handler.  So if you don't want to go to the bother of coding your own for-loops to iterate over the y and x coordinates, you can simplify your code a bit by using a ForEveryPixel handler instead of a ForEveryTile handler, as demonstrated in the following example.  There are a few differences, however:
 
%FFP  
Category:  "AFH"  
Title:     "Newton Fractal Explorer 5.0b"  
Copyright: "Copyright ©1998, AFH Systems Group"  
Author:    "Alex Hunter <alex@afh.com>"  
Filename:  "afhnewton5.8bf"  

ctl[0]:"- ZOOM +",range=(1,500),val=35  
ctl[1]:"<-- PAN -->", range=(-400,+400),val=0  
ctl[2]:" v-- TILT --^", range=(-300,+300),val=0  
ctl[3]:"Resolution:", 
            range=(1,1024),val=512,pagesize=50,linesize=5  

ForEveryPixel 
{   //Code transliterated from p.94 of  
    //"Graphics Programming with Java"  
    //by Roger T. Stevens.  

    x1 = (x - X/2)*100.0/(ctl(0)*X) + ctl(1)/100.0;  
    y1 = (y - Y/2)*75.0/(ctl(0)*Y) - ctl(2)/100.0;  

    for (i0 = 0; i0 < ctl(3); ++i0) {  
        x2 = x1*x1;  
        x3 = x2*x1;  
        x4 = x3*x1;  
        x5 = x4*x1;  
        x6 = x5*x1;  
        x7 = x6*x1;  
        x8 = x7*x1;  
        y2 = y1*y1;  
        y3 = y2*y1;  
        y4 = y3*y1;  
        y5 = y4*y1;  
        y6 = y5*y1;  
        y7 = y6*y1;  
        y8 = y7*y1;  
        z9 = x2 + y2;  
        z9 = z9*z9;  
        z9 = z9*z9;  
        z9 = z9*z9*9.0;  
        y0 = 0.88888888*y1 - (8.0*x7*y1 -  
                             56.0*x5*y3 +  
                             56.0*x3*y5 -  
                              8.0*x1*y7)/z9;  
        x0 = 0.88888888*x1 + (    x8 -  
                             28.0*x6*y2 +  
                             70.0*x4*y4 -  
                             28.0*x2*y6 +  
                                     y8)/z9;  
        if (abs(x0 - x1) < 1e-10 &&  
            abs(y0 - y1) < 1e-10 ) break;  
        x1 = x0;  
        y1 = y0;  
    }  
    i0 &= 15;  
    if (i0 == 0) {  
       //black  
       R = 0;  
       G = 0;  
       B = 0;  
    }  
    else if (i0 == 1) {  
       //blue  
       R = 0;  
       G = 0;  
       B = 168;  
    }  
    else if (i0 == 2) {  
       //green  
       R = 0;  
       G = 168;  
       B = 0;  
    }  
    else if (i0 == 3) {  
       //cyan  
       R = 0;  
       G = 168;  
       B = 168;  
    }  
    else if (i0 == 4) {  
       //red  
       R = 168;  
       G = 0;  
       B = 0;  
    }  
    else if (i0 == 5) {  
       //magenta  
       R = 168;  
       G = 0;  
       B = 168;  
    }  
    else if (i0 == 6) {  
       //brown  
       R = 168;  
       G = 84;  
       B = 0;  
    }  
    else if (i0 == 7) {  
       //lightGray  
       R = 168;  
       G = 168;  
       B = 168;  
    }  
    else if (i0 == 8) {  
       //darkGray  
       R = 84;  
       G = 84;  
       B = 84;  
    }  
    else if (i0 == 9) {  
       //lightBlue  
       R = 84;  
       G = 84;  
       B = 255;  
    }  
    else if (i0 == 10) {  
       //lightGreen  
       R = 84;  
       G = 255;  
       B = 84;  
    }  
    else if (i0 == 11) {  
       //lightCyan  
       R = 84;  
       G = 255;  
       B = 255;  
    }  
    else if (i0 == 12) {  
       //lightRed  
       R = 255;  
       G = 84;  
       B = 84;  
    }  
    else if (i0 == 13) {  
       //lightMagenta  
       R = 255;  
       G = 84;  
       B = 255;  
    }  
    else if (i0 == 14) {  
       //yellow  
       R = 255;  
       G = 255;  
       B = 84;  
    }  
    else {  
       //white  
       R = 255;  
       G = 255;  
       B = 255;  
    }  

} //ForEveryPixel 

 
Final notes:
Newton Fractal Explorer 5.0b
 

Back to the Tutorial Index
 

 

Home | Download | Documents | Tips & Tutorials | Code Library | Help / User Group | Order