One class of image digital filters is described by a rectangular matrix of real coefficients called kernel convoluted in a sliding window of image pixels. Usually the kernel is square 




K

k
l




{\displaystyle K_{kl}}

, where k, l are in the range -R,-R+1,..,R-1,R. W=2R+1 is the kernel width. The filter determines the new value of a monochromatic image pixel Pij as a convolution of the image pixels in the window centered in i, j and the kernel values:







P

i
j


=


∑

k
=
−
R


R



∑

l
=
−
R


R



P

i
+
k
 
j
+
l



K

k
l





{\displaystyle P_{ij}=\displaystyle \sum _{k=-R}^{R}\sum _{l=-R}^{R}P_{i+k\ j+l}K_{kl}}




Color images are usually split into the channels which are filtered independently. A color model can be changed as well, i.e. filtration is performed not necessarily in RGB. Common kernels sizes are 3x3 and 5x5. The complexity of filtrating grows quadratically (O(n2)) with the kernel width.

Task: Write a generic convolution 3x3 kernel filter. Optionally show some end user filters that use this generic one.

(You can use, to test the functions below, these input and output solutions.)

import "graphics" for Canvas, Color, ImageData
import "dome" for Window
 
class ArrayData {
    construct new(width, height) {
        _width  = width
        _height = height
        _dataArray = List.filled(width * height, 0)
    }
 
    width  { _width }
    height { _height }
 
    [x, y] { _dataArray[y * _width + x] }
 
    [x, y]=(v) { _dataArray[y * _width + x] = v }
}
 
class ImageConvolution {
    construct new(width, height, image1, image2, kernel, divisor) {
        Window.title = "Image Convolution"
        Window.resize(width, height)
        Canvas.resize(width, height)
        _width  = width
        _height = height
        _image1 = image1
        _image2 = image2
        _kernel = kernel
        _divisor = divisor
    }
 
    init() {
        var dataArrays = getArrayDatasFromImage(_image1)
        for (i in 0...dataArrays.count) {
            dataArrays[i] = convolve(dataArrays[i], _kernel, _divisor)
        }
        writeOutputImage(_image2, dataArrays)
    }
 
    bound(value, endIndex) {
        if (value < 0) return 0
        if (value < endIndex) return value
        return endIndex - 1
    }
 
    convolve(inputData, kernel, kernelDivisor) {
        var inputWidth   = inputData.width
        var inputHeight  = inputData.height
        var kernelWidth  = kernel.width
        var kernelHeight = kernel.height
        if (kernelWidth <= 0 || (kernelWidth & 1) != 1) {
            Fiber.abort("Kernel must have odd width")
        }
        if (kernelHeight <= 0 || (kernelHeight & 1) != 1) {
            Fiber.abort("Kernel must have odd height")
        }
        var kernelWidthRadius  = kernelWidth >> 1
        var kernelHeightRadius = kernelHeight >> 1
 
        var outputData = ArrayData.new(inputWidth, inputHeight)
        for (i in inputWidth - 1..0) {
            for (j in inputHeight - 1..0) {
                var newValue = 0
                for (kw in kernelWidth - 1..0) {
                    for (kh in kernelHeight - 1..0) {
                        newValue = newValue + kernel[kw, kh] * inputData[
                            bound(i + kw - kernelWidthRadius, inputWidth),
                            bound(j + kh - kernelHeightRadius, inputHeight)
                        ]
                        outputData[i, j] = (newValue / kernelDivisor).round
                    }
                }
            }
        }
        return outputData
    }
 
    getArrayDatasFromImage(filename) {
        var inputImage = ImageData.loadFromFile(filename)
        inputImage.draw(0, 0)
        Canvas.print(filename, _width * 1/6,  _height * 5/6, Color.white)
        var width  = inputImage.width
        var height = inputImage.height
        var rgbData = []
        for (y in 0...height) {
            for (x in 0...width) rgbData.add(inputImage.pget(x, y))
        }
        var reds   = ArrayData.new(width, height)
        var greens = ArrayData.new(width, height)
        var blues  = ArrayData.new(width, height)
        for (y in 0...height) {
            for (x in 0...width) {
                var rgbValue = rgbData[y * width + x]
                reds[x, y]   = rgbValue.r
                greens[x,y]  = rgbValue.g
                blues[x, y]  = rgbValue.b
            }
        }
        return [reds, greens, blues]
    }
 
    writeOutputImage(filename, redGreenBlue) {
        var reds   = redGreenBlue[0]
        var greens = redGreenBlue[1]
        var blues  = redGreenBlue[2]
        var outputImage = ImageData.create(filename, reds.width, reds.height)
        for (y in 0...reds.height) {
            for (x in 0...reds.width) {
                var red   = bound(reds[x, y], 256)
                var green = bound(greens[x, y], 256)
                var blue  = bound(blues[x, y], 256)
                var c = Color.new(red, green, blue)
                outputImage.pset(x, y, c)
             }
        }
        outputImage.draw(_width/2, 0)
        Canvas.print(filename, _width * 2/3,  _height * 5/6, Color.white)
        outputImage.saveToFile(filename)
    }
 
    update() {}
 
    draw(alpha) {}
}
 
var k = [
    [1,  4,  7,  4, 1],
    [4, 16, 26, 16, 4],
    [7, 26, 41, 26, 7],
    [4, 16, 26, 16, 4],
    [1,  4, 7,   4, 1]
]
 
var divisor = 273
 
var image1 = "Pentagon.png"
var image2 = "Pentagon2.png"
 
System.print("Input file %(image1), output file %(image2).")
System.print("Kernel size: %(k.count) x %(k[0].count), divisor %(divisor)")
System.print(k.join("\n"))
 
var kernel = ArrayData.new(k.count, k[0].count)
for (y in 0...k[0].count) {
    for (x in 0...k.count) kernel[x, y] = k[x][y]
}
var Game = ImageConvolution.new(700, 300, image1, image2, kernel, divisor)