Exploring Convolution Matrices with JIMP

In this post I will explore image editing using convolution matrices in JavaScript using the JIMP library.

I will again be using the JIMP nmp package. The full documentation for JIMP can be found at https://www.npmjs.com/package/jimp.

Convolution matrices sound a bit scary if you are not familiar with the topic but are actually very simple to understand and extremely powerful.The process is best explained with a diagram. Here we see four 3x3 grids and the process used to obtain the final value for the centre pixel from its original value and those of the 8 pixels surrounding it.

convolution matrix description

I have used greyscale for simplicity but RGB images work in the same way, just with three times as many values. To obtain the new value for the centre pixel, which is currently 128, we multiply each of the values in the grid by its corresponding value in the kernel matrix. The results are shown in the Products grid. These nine values are then added to give a final value, and the process repeated for every pixel in the image. If the result is outside the 0-255 range then 0 or 255 are used.

Each kernel has a certain effect on the image, for example sharpening or blurring. I will show a few well-known ones in the code.

The JIMP implementation is very rudimentary and I am working on a more flexible implementation for a project I am working on. However, you can still carry out some useful enhancements with the JIMP version and it is also ideal for learning the topic.

The source code can be downloaded as a ZIP, or you can clone the Github repository if you prefer.

Source Code Links

ZIP File
GitHub

This is the source code.

convolutionmatrix.js

convolution();

async function convolution()
{
    const Jimp = require("jimp");

    const kernels =
    [
        {name: "emboss", kernel: [[-2, -1, 0], [-1, 1, 1], [0, 1, 2]]},
        {name: "edgedetect", kernel: [[0, 1, 0], [1, -4, 1], [0, 1, 0]]},
        {name: "edgeenhance", kernel: [[0, 0, 0], [-1, 1, 0], [0, 0, 0]]},
        {name: "blur", kernel: [[0.0625, 0.125, 0.0625],[0.125, 0.25, 0.125], [0.0625, 0.125, 0.0625]]},
        // equivalent to {name: "blur", kernel: [[1/16, 1/8, 1/16],[1/8, 1/4, 1/8], [1/16, 1/8, 1/16]]},
        {name: "sharpen", kernel: [[0,-1,0], [-1,5,-1], [0,-1,0]]}
    ];

    for(kernel of kernels)
    {
        const photo = await Jimp.read("templestation.jpg");

        console.log(kernel.name);
        console.log(kernel.kernel);

        photo.convolute(kernel.kernel);

        photo.write(`templestation_${kernel.name}.jpg`);
    }
}

Firstly I have created an array of objects, each with a name and a kernel. This array is then iterated in a for/of loop within which we open the original image, log the particular kernel details before applying it with the convolute method, and then save the resulting image.

You might think, as I did, that the convolute method would return a copy. It doesn't! It actually edits the existing image which is why I re-opened it each time.

Now let's run the code. I'll be using this image for testing, which is included with the source code. The resulting images are shown below.

Run

node convolutionmatrix.js

Emboss

convolution matrix emboss

Edge Detect

convolution matrix edge detect

Edge Enhance

convolution matrix edge enhance

Blur

convolution matrix blur

Sharpen

convolution matrix sharpen