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/)
Status: New
Function Index
apply-kernel($greys as map(xs:string,item()*), (: core/array :) $kernel as map(xs:string,item()*)) as map(xs:string,item()*)apply-kernel-multi($image as map(xs:string,item()*), (: image/matrix :) $kernel as map(xs:string,item()*)) as map(xs:string,item()*)blur($image as map(xs:string,item()*)) as map(xs:string,item()*)edge-detection($image as map(xs:string,item()*)) as map(xs:string,item()*)greyscale($image as map(xs:string,item()*)) as map(xs:string,item()*)sharpen($image as map(xs:string,item()*)) as map(xs:string,item()*)
Imports
http://mathling.com/core/arrayimport module namespace arr="http://mathling.com/core/array"
at "../core/array.xqy"http://mathling.com/colour/spaceimport module namespace cs="http://mathling.com/colour/space"
at "../colourspace/colour-space.xqy"http://mathling.com/art/coreimport module namespace core="http://mathling.com/art/core"
at "../art/core.xqy"http://mathling.com/core/utilitiesimport module namespace util="http://mathling.com/core/utilities"
at "../core/utilities.xqy"http://mathling.com/colour/rgbimport module namespace rgb="http://mathling.com/colour/rgb"
at "../colourspace/rgb.xqy"http://mathling.com/image/matriximport module namespace matrix="http://mathling.com/image/matrix"
at "../image/matrix.xqy"http://mathling.com/core/errorsimport module namespace errors="http://mathling.com/core/errors"
at "../core/errors.xqy"http://mathling.com/geometric/pointimport 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()*)
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()*)
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()*)
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()*)
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()*)
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()*)
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)
};