Writing New And Exciting Filters For GIMP


Click picture to see full size

Introduction To Writing The Pencil Shade Filter

This post is the first step towards the next on a similar subject. In that I shall show you how to create professional filters effects for motion pictures using GIMP and Virtual Dub (i.e. for free).

The filter effect I am going to create is a 'pencil shade'. The aim is to make a picture appear as though it has been shaded in by pencil. My time on this project was a little limited, so the end result has a couple of issues, but I think the filter is fairly close to its goal. Anyhow, the issues that are left for it are also interesting to learn from.

I have chosen to write the filter in Scheme. GIMP has built in support for Scheme as part of its Script Fu system. Scheme is idea as a light-weight scripting language for programs like GIMP because it is in its self very simple; however, the simplicity of the language does not limit what can be done with it.

If you do not know any Scheme, do not panic, I will talk through everything that is done here. I have also avoided some of the more hair curlingly challenging concepts that such programming languages sometimes attract!

How The Filter Works

Below are the steps described as simplified GIMP Scheme statements.

  1. gimp-image-add-layer lineLayer
  2. gimp-layer-set-mode lineLayer MULTIPLY-MODE
  3. gimp-desaturate lineLayer
  4. gimp-desaturate shadeLayer
  5. plug-in-apply-canvas shadeLayer
  6. plug-in-dog shadeLayer 35 3
  7. plug-in-convmatrix shadeLayer
  8. plug-in-sharpen shadeLayer 45
  9. plug-in-convmatrix shadeLayer
  10. plug-in-sharpen shadeLayer 85
  11. plug-in-dog lineLayer 3 1
  12. plug-in-unsharp-mask lineLayer 4 4 5
  13. gimp-layer-set-opacity lineLayer 50

Walking through these steps gives us the first action of creating a duplicate layer. Initially, the image is considered to be flat - it has one layer. This layer is called the ShadeLayer. The first steps adds a new layer on top of the ShadeLayer and calls this new layer LineLayer.

The next three steps set the layers up in the way they will be presented in the final image. The upper LineLayer is set to Multiply Mode and both layers are turned to black and white (monochrome/grey scale). This means that the darkness of the top LineLayer will enhance the darkness of the lower ShadeLayer and the lightness of LineLayer will enhance lightness of ShadeLayer.

One problem I found with this filter when I first created it was that the results looked too perfect. The solution I found to this was to 'rough up' the ShadeLayer before the filtering proper started. This is achieved by applying a canvas to the ShadeLayer.

Now comes the really stylistic part. This part relies on the way the human eye works, as does much of pencil shading. We see edges and the transition of brightness around edges must more that we see large homogeneous areas. This means that when pencil shading, we can get away with shading the edges and detail of the subject but ignoring much of the bulk. The GIMP has a filter that produces exactly this effect. It is called the Difference Of Gaussians (DOG) filter. So, to make a nicely shaded background layer, I apply a DOG filter to the ShadeLayer. The DOG is set with a large outer 'radius' so that it takes in the area around details and edges, not just the facet its self.

Now what the ShadeLayer has a more shading like application of dark and light, the filter has to make it look like that shading was done by a pencil. Pencils leave a distinctive striation to the shading they do. It is normal practice to make the striations of the shading all point in the same direction (or else the picture can look furry!). To replicate this effect I have use a general convolution matrix that blurs the ShadeLayer in one direction. However, the general convolution matrix in the GIMP is limited in size and does not produce a strong enough effect. To overcome this I apply the matrix, sharpen and then apply it again with the following code:

        (
            plug-in-convmatrix 
                RUN-NONINTERACTIVE
                image
                shadeLayer
                25
                matrix
                FALSE
                50
                0
                5
                channels
                2
        )

        (
            plug-in-sharpen RUN-NONINTERACTIVE
                           image shadeLayer 45
        )
       
        (
            plug-in-convmatrix 
                RUN-NONINTERACTIVE
                image
                shadeLayer
                25
                matrix
                FALSE
                50
                0
                5
                channels
                2
        )
Then I apply a final sharpen to bring the structure out. This has the problem that so much sharpening produces 'ringing' around edges. This looks like the edge is doubled or tripled up. This is one of the drawbacks of this filter. I suspect that tweaking the way the sharpening is applied could correct it. However, as I said earlier, time was pressing when this was written.

The LineLayer is designed to replicate the effect of putting in the edges and details by as lines with the pencil. It has not of the fancy shading work. However, I do use the DOG method to find the lines. The DOH filter produces very fine blurred lines with the settings used here. To make them into stronger and less burred lines, I use a Unsharp Mask with very aggressive settings.

Nearly there, the last step is to weaken the effect of the LineLayer by making it have an opacity of 50%


Click picture to see full size

The actual Code

Here is the actual code. I placed it in a file called PencilShade.scm and put that in the scripts folder under the GIMP. Where this folder lives depends on which version of the GIMP you are using. Once you find a folder stuffed full of scm files, you know you are in the correct place. After the code list, I have given some pointers as to what it all means.

(
    define (
                script-fu-pencil-shade
                image
           )
    (
        
        let*
        (
            (
                shadeLayer 
                (
                    car (
                            gimp-image-get-active-layer 
                            image
                        )
                )
            )
            (
                lineLayer
                (
                    car (
                            gimp-layer-copy
                            shadeLayer
                            1
                        )
                )
            )
        )
        (
            gimp-image-add-layer image lineLayer -1
        )
        (
            gimp-layer-set-mode lineLayer MULTIPLY-MODE
        )
        (
            gimp-desaturate lineLayer
        )
        
        (
            gimp-desaturate shadeLayer
        )
        
        (
            plug-in-apply-canvas RUN-NONINTERACTIVE
                image
                shadeLayer
                2
                1
        )
        
        (
            plug-in-dog RUN-NONINTERACTIVE
                           image shadeLayer 35 3  TRUE TRUE 
        )
        (set! matrix (cons-array 25 'double))
        (aset matrix 0 16.0)
        (aset matrix 1 -1.0)
        (aset matrix 2 -1.0)
        (aset matrix 3 -1.0)
        (aset matrix 4 -1.0)
        
        (aset matrix 5 -1.0)
        (aset matrix 6 16.0)
        (aset matrix 7 -1.0)
        (aset matrix 8 -1.0)
        (aset matrix 9 -1.0)
        
        (aset matrix 10 -1.0)
        (aset matrix 11 -1.0)
        (aset matrix 12 16.0)
        (aset matrix 13 -1.0)
        (aset matrix 14 -1.0)
        
        (aset matrix 15 -1.0)
        (aset matrix 16 -1.0)
        (aset matrix 17 -1.0)
        (aset matrix 18 16.0)
        (aset matrix 19 -1.0)

        (aset matrix 15 -1.0)
        (aset matrix 16 -1.0)
        (aset matrix 17 -1.0)
        (aset matrix 18 -1.0)
        (aset matrix 19 16.0)
        
        (set! channels (cons-array 5 'long))
        (aset channels 0 1)
        (aset channels 1 1)
        (aset channels 2 1)
        (aset channels 3 1)
        (aset channels 4 0)

        (
            plug-in-convmatrix 
                RUN-NONINTERACTIVE
                image
                shadeLayer
                25
                matrix
                FALSE
                50
                0
                5
                channels
                2
        )

        (
            plug-in-sharpen RUN-NONINTERACTIVE
                           image shadeLayer 45
        )
       
        (
            plug-in-convmatrix 
                RUN-NONINTERACTIVE
                image
                shadeLayer
                25
                matrix
                FALSE
                50
                0
                5
                channels
                2
        )

        (
            plug-in-sharpen RUN-NONINTERACTIVE
                           image shadeLayer 85
        )
        
        (
            plug-in-dog RUN-NONINTERACTIVE
                           image lineLayer 3 1 TRUE TRUE
        )
        (
            plug-in-unsharp-mask RUN-NONINTERACTIVE
                           image lineLayer 4 4 5
        )
        (
            gimp-layer-set-opacity
                           lineLayer 50
        )
    )
)

(script-fu-register      
    "script-fu-pencil-shade"
    "_Pencil Shade"
    "Creates a pencil shaded style"
    "Alex Turner"
    "Alex Turner"
    "26. 12. 2006"
    "RGB RGBA GRAY GRAYA"
    SF-IMAGE "Image" 0
    SF-DRAWABLE "Drawable" 0
)
(script-fu-menu-register "script-fu-pencil-shade"
                         "<Image>/Script-Fu/Alchemy")

The first thing to understand when viewing this code is that everything is wrapped around in brackets (). Then we have the define statement. This simply says the next thing is a 'function' definition which is called script-fu-pencil-shade which takes one argument which is called image.

The next piece is this:

        let*
        (
            (
                shadeLayer 
                (
                    car (
                            gimp-image-get-active-layer 
                            image
                        )
                )
            )
            (
                lineLayer
                (
                    car (
                            gimp-layer-copy
                            shadeLayer
                            1
                        )
                )
            )
        )
The let* is defining names to stand for things, in this case the names stand for the two layers ShadeLayer and LineLayer. The word car takes a list and returns just the first element. You see, in Scheme, most things are defined as list or single-units. Lists being lists of single-units. gimp-image-get-active-layer image returns the currently active layer of the image. But, all functions like this return lists. In this case the list only has in it one single-unit. To get at that single-unit, we use car.

Most of the rest of the code is straight forward once you have figures these first bits out. The matrix stuff is more interesting. This requires the creation of an array. Array creation is done via the (set! matrix (cons-array 25 'double)) code and then setting the values of the array are done via the (aset matrix 0 16.0) statements. Although the array notionally has two dimensions (5x5) it is created as one long array with a single dimension.

The very final part of the code registers it with the GIMP so that you can access it through the menu. All being well, once you place the file in the correct directory and restart GIMP, you will see PencilShade appear under the Script-Fu/Achemy menu option for images.


Click picture to see full size