http://mathling.com/image/convolution  library module

http://mathling.com/image/convolution


Image convolutions:
Edge detection, blurring, sharpening
Also greyscaling of images
Warning: very slow for large matrixes
If you can, you are much better off doing this on the SVG side with
a filter, or precomputing from a base image with ImageMagick, e.g.
convert -edge 1 -grayscale Rec601Luma -compress none ${fn} ${fn/.jpg/_edge.ppm}

Copyright© Mary Holstege 2022-2025
CC-BY (https://creativecommons.org/licenses/by/4.0/)

September 2022
Status: New

Function Index

Imports

http://mathling.com/core/array
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.xqy"
http://mathling.com/colour/space
import module namespace cs="http://mathling.com/colour/space"
       at "../colourspace/colour-space.xqy"
http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/colour/rgb
import module namespace rgb="http://mathling.com/colour/rgb"
       at "../colourspace/rgb.xqy"
http://mathling.com/image/matrix
import module namespace matrix="http://mathling.com/image/matrix"
       at "../image/matrix.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Variables

Variable: $EDGE-KERNEL as 


Kernel for performing edge detection

Variable: $SHARPEN-KERNEL as 


Kernel for image sharpening

Variable: $GAUSSIAN-BLUR-KERNEL as 


Kernel for (approximate) Gaussian blur

Functions

Function: apply-kernel
declare function apply-kernel($greys as map(xs:string,item()*), (: core/array :) $kernel as map(xs:string,item()*)) as map(xs:string,item()*)


apply-kernel()
Apply a kernel matrix to a greyscale image

Params
  • greys as map(xs:string,item()*): a core array containing greyscale values
  • kernel as map(xs:string,item()*): the kernel array to apply
Returns
  • map(xs:string,item()*)
declare function this:apply-kernel(
  $greys as map(xs:string,item()*),  (: core/array :)
  $kernel as map(xs:string,item()*)  (: core/array :)
) as map(xs:string,item()*) (: core/array :)
{
  let $k-rows := arr:rows($kernel)
  let $k-columns := arr:columns($kernel)
  let $y-range := ($k-rows - 1) idiv 2
  let $x-range := ($k-columns - 1) idiv 2
  let $rows := arr:rows($greys)
  let $columns := arr:columns($greys)
  let $expanded :=
    arr:array($rows + 2 * $y-range, $columns + 2 * $x-range,
      (
        for $y-pad in 1 to $y-range return (
          for $x-pad in 1 to $x-range return $greys=>arr:get(1, 1),
          $greys=>arr:row(1),
          for $x-pad in 1 to $x-range return $greys=>arr:get(1, $columns)
        ),
        for $y in 1 to $rows return (
          for $x-pad in 1 to $x-range return $greys=>arr:get($y, 1),
          $greys=>arr:row($y),
          for $x-pad in 1 to $x-range return $greys=>arr:get($y, $columns)
        ),
        for $y-pad in 1 to $y-range return (
          for $x-pad in 1 to $x-range return $greys=>arr:get($rows, 1),
          $greys=>arr:row($rows),
          for $x-pad in 1 to $x-range return $greys=>arr:get($rows, $columns)
        )
      )
    )
  let $value2 :=
    function($y as xs:integer, $x as xs:integer) as xs:double {
      sum(
        for $dy in -$y-range to $y-range
        for $dx in -$x-range to $x-range
        return (
          ($kernel=>arr:get($y-range + 1 + $dy, $x-range + 1 + $dx)) *
          $expanded=>arr:get($y + $y-range - $dy, $x + $x-range - $dx)
        )
      )
    }
  return (
    arr:array($rows, $columns,
      for $y in 1 to $rows
      for $x in 1 to $columns
      return $value2($y, $x)
    )
  )
}

Function: greyscale
declare function greyscale($image as map(xs:string,item()*)) as map(xs:string,item()*)


greyscale()
Convert an image matrix containing RGB colour values to an array of
greyscale values. Conversion is using Rec601Luma:
L = R * 0.298839 + G * 0.586811 + B * 0.114350

Params
  • image as map(xs:string,item()*): the image matrix containing RGB values
Returns
  • map(xs:string,item()*)
declare function this:greyscale(
  $image as map(xs:string,item()*)  (: image/matrix with rgb values :)
) as map(xs:string,item()*) (: core/array with greyscale raw values :)
{
  arr:array(arr:rows($image), arr:columns($image),
    for $val in matrix:data($image)
    return cs:rgb-to-greyscale-value($val)
  )
}

Function: apply-kernel-multi
declare function apply-kernel-multi($image as map(xs:string,item()*), (: image/matrix :) $kernel as map(xs:string,item()*)) as map(xs:string,item()*)


apply-kernel-multi()
Apply the kernel to each of the colour dimensions. Assumes all colours
have the same number of dimensions and uses [1,1] to determine that.
Agnostic as to colour space, although blindly applying the same kernel
to all coordinates in some colour spaces is probably a bad bad idea.

Params
  • image as map(xs:string,item()*)
  • kernel as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:apply-kernel-multi(
  $image as map(xs:string,item()*),  (: image/matrix :)
  $kernel as map(xs:string,item()*)  (: core/array :)
) as map(xs:string,item()*) (: image/matrix :)
{
  let $k-rows := arr:rows($kernel)
  let $k-columns := arr:columns($kernel)
  let $y-range := ($k-rows - 1) idiv 2
  let $x-range := ($k-columns - 1) idiv 2
  let $rows := matrix:rows($image)
  let $columns := matrix:columns($image)
  let $dim := count(point:coordinates($image=>matrix:get(1,1)))
  let $partials :=
    for $d in 1 to $dim
    let $dimension-array :=
      arr:array($rows, $columns,
        for $value in matrix:data($image) return $value=>point:pcoordinate($d)
      )
    let $applied := this:apply-kernel($dimension-array, $kernel)
    return array { $applied=>arr:data() }
  return (
    matrix:array($rows, $columns,
      for $i in 1 to $rows * $columns return (
        rgb:to-rgb(
          for $d in 1 to $dim return util:clamp($partials[$d]($i), 0.0, 1.0)
        )
      )
    )
  )
}

Function: edge-detection
declare function edge-detection($image as map(xs:string,item()*)) as map(xs:string,item()*)


edge-detection()
Perform edge detection on the image by converting it to greyscale and
then running the edge detection kernel over it. Result is an array
containing greyscale values. NOTE: these values may be out of range
for conversion to RGB unless clamped.

Params
  • image as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:edge-detection(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: core/array :)
{
  $image=>this:greyscale()=>this:apply-kernel($this:EDGE-KERNEL)
}

Function: sharpen
declare function sharpen($image as map(xs:string,item()*)) as map(xs:string,item()*)


sharpen()
Perform sharpening on the image. Returns a full image matrix.

Params
  • image as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:sharpen(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: image/matrix :)
{
  $image=>this:apply-kernel-multi($this:SHARPEN-KERNEL)
}

Function: blur
declare function blur($image as map(xs:string,item()*)) as map(xs:string,item()*)


blur()
Perform approximate Gaussian blurring on the image.
Returns a full image matrix.

Params
  • image as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:blur(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: image/matrix :)
{
  $image=>this:apply-kernel-multi($this:GAUSSIAN-BLUR-KERNEL)
}

Original Source Code

xquery version "3.1";
(:~
 : Image convolutions:
 : Edge detection, blurring, sharpening
 : Also greyscaling of images
 : Warning: very slow for large matrixes
 : If you can, you are much better off doing this on the SVG side with
 : a filter, or precomputing from a base image with ImageMagick, e.g.
 :  convert -edge 1 -grayscale Rec601Luma -compress none ${fn} ${fn/.jpg/_edge.ppm}
 :
 : Copyright© Mary Holstege 2022-2025
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since September 2022
 : @custom:Status New
 :)
module namespace this="http://mathling.com/image/convolution";

import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace rgb="http://mathling.com/colour/rgb"
       at "../colourspace/rgb.xqy";
import module namespace cs="http://mathling.com/colour/space"
       at "../colourspace/colour-space.xqy";
import module namespace matrix="http://mathling.com/image/matrix"
       at "../image/matrix.xqy";

declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";

(:~
 : Kernel for performing edge detection
 :)
declare variable $this:EDGE-KERNEL :=
  arr:array(3, 3,
    (
      -1, -1, -1,
      -1, 8, -1,
      -1, -1, -1
    )
  )
;

(:~
 : Kernel for image sharpening
 :)
declare variable $this:SHARPEN-KERNEL :=
  arr:array(3, 3,
    (
      0, -1, 0,
      -1, 5, -1,
      0, -1, 0
    )
  )
;

(:~
 : Kernel for (approximate) Gaussian blur
 :)
declare variable $this:GAUSSIAN-BLUR-KERNEL :=
  arr:array(3, 3,
    (
      1, 2, 1,
      2, 4, 2,
      1, 2, 1
    )!(. div 16)
  )
;

(:~
 : apply-kernel()
 : Apply a kernel matrix to a greyscale image
 :
 : @param $greys: a core array containing greyscale values
 : @param $kernel: the kernel array to apply
 :)
declare function this:apply-kernel(
  $greys as map(xs:string,item()*),  (: core/array :)
  $kernel as map(xs:string,item()*)  (: core/array :)
) as map(xs:string,item()*) (: core/array :)
{
  let $k-rows := arr:rows($kernel)
  let $k-columns := arr:columns($kernel)
  let $y-range := ($k-rows - 1) idiv 2
  let $x-range := ($k-columns - 1) idiv 2
  let $rows := arr:rows($greys)
  let $columns := arr:columns($greys)
  let $expanded :=
    arr:array($rows + 2 * $y-range, $columns + 2 * $x-range,
      (
        for $y-pad in 1 to $y-range return (
          for $x-pad in 1 to $x-range return $greys=>arr:get(1, 1),
          $greys=>arr:row(1),
          for $x-pad in 1 to $x-range return $greys=>arr:get(1, $columns)
        ),
        for $y in 1 to $rows return (
          for $x-pad in 1 to $x-range return $greys=>arr:get($y, 1),
          $greys=>arr:row($y),
          for $x-pad in 1 to $x-range return $greys=>arr:get($y, $columns)
        ),
        for $y-pad in 1 to $y-range return (
          for $x-pad in 1 to $x-range return $greys=>arr:get($rows, 1),
          $greys=>arr:row($rows),
          for $x-pad in 1 to $x-range return $greys=>arr:get($rows, $columns)
        )
      )
    )
  let $value2 :=
    function($y as xs:integer, $x as xs:integer) as xs:double {
      sum(
        for $dy in -$y-range to $y-range
        for $dx in -$x-range to $x-range
        return (
          ($kernel=>arr:get($y-range + 1 + $dy, $x-range + 1 + $dx)) *
          $expanded=>arr:get($y + $y-range - $dy, $x + $x-range - $dx)
        )
      )
    }
  return (
    arr:array($rows, $columns,
      for $y in 1 to $rows
      for $x in 1 to $columns
      return $value2($y, $x)
    )
  )
};

(:~
 : greyscale()
 : Convert an image matrix containing RGB colour values to an array of
 : greyscale values. Conversion is using Rec601Luma:
 : L = R * 0.298839 + G * 0.586811 + B * 0.114350
 :
 : @param $image: the image matrix containing RGB values
 :)
declare function this:greyscale(
  $image as map(xs:string,item()*)  (: image/matrix with rgb values :)
) as map(xs:string,item()*) (: core/array with greyscale raw values :)
{
  arr:array(arr:rows($image), arr:columns($image),
    for $val in matrix:data($image)
    return cs:rgb-to-greyscale-value($val)
  )
};

(:~
 : apply-kernel-multi()
 : Apply the kernel to each of the colour dimensions. Assumes all colours
 : have the same number of dimensions and uses [1,1] to determine that.
 : Agnostic as to colour space, although blindly applying the same kernel
 : to all coordinates in some colour spaces is probably a bad bad idea.
 :)
declare function this:apply-kernel-multi(
  $image as map(xs:string,item()*),  (: image/matrix :)
  $kernel as map(xs:string,item()*)  (: core/array :)
) as map(xs:string,item()*) (: image/matrix :)
{
  let $k-rows := arr:rows($kernel)
  let $k-columns := arr:columns($kernel)
  let $y-range := ($k-rows - 1) idiv 2
  let $x-range := ($k-columns - 1) idiv 2
  let $rows := matrix:rows($image)
  let $columns := matrix:columns($image)
  let $dim := count(point:coordinates($image=>matrix:get(1,1)))
  let $partials :=
    for $d in 1 to $dim
    let $dimension-array :=
      arr:array($rows, $columns,
        for $value in matrix:data($image) return $value=>point:pcoordinate($d)
      )
    let $applied := this:apply-kernel($dimension-array, $kernel)
    return array { $applied=>arr:data() }
  return (
    matrix:array($rows, $columns,
      for $i in 1 to $rows * $columns return (
        rgb:to-rgb(
          for $d in 1 to $dim return util:clamp($partials[$d]($i), 0.0, 1.0)
        )
      )
    )
  )
};

(:~
 : edge-detection()
 : Perform edge detection on the image by converting it to greyscale and
 : then running the edge detection kernel over it. Result is an array
 : containing greyscale values. NOTE: these values may be out of range
 : for conversion to RGB unless clamped.
 :)
declare function this:edge-detection(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: core/array :)
{
  $image=>this:greyscale()=>this:apply-kernel($this:EDGE-KERNEL)
};

(:~
 : sharpen()
 : Perform sharpening on the image. Returns a full image matrix.
 :)
declare function this:sharpen(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: image/matrix :)
{
  $image=>this:apply-kernel-multi($this:SHARPEN-KERNEL)
};

(:~
 : blur()
 : Perform approximate Gaussian blurring on the image.
 : Returns a full image matrix.
 :)
declare function this:blur(
  $image as map(xs:string,item()*)  (: image/matrix :)
) as map(xs:string,item()*) (: image/matrix :)
{
  $image=>this:apply-kernel-multi($this:GAUSSIAN-BLUR-KERNEL)
};