///|
/// A square or rectangular convolution kernel used by the image effects API.
/// Weights and divisor are integer-scaled so results are reproducible on all
/// MoonBit backends.
pub(all) struct ConvolutionKernel {
width : Int
height : Int
weights : Array[Int]
divisor : Int
bias : Int
} derive(Debug)
///|
pub fn ConvolutionKernel::new(
width~ : Int,
height~ : Int,
weights~ : ArrayView[Int],
divisor~ : Int,
bias~ : Int,
) -> ConvolutionKernel {
if width <= 0 ||
height <= 0 ||
width % 2 == 0 ||
height % 2 == 0 ||
weights.length() != width * height ||
divisor == 0 {
panic()
}
let copied : Array[Int] = []
for value in weights {
copied.push(value)
}
{ width, height, weights: copied, divisor, bias }
}
///|
pub fn ConvolutionKernel::width(self : ConvolutionKernel) -> Int {
self.width
}
///|
pub fn ConvolutionKernel::height(self : ConvolutionKernel) -> Int {
self.height
}
///|
pub fn ConvolutionKernel::weights(self : ConvolutionKernel) -> ArrayView[Int] {
self.weights[:]
}
///|
pub fn ConvolutionKernel::divisor(self : ConvolutionKernel) -> Int {
self.divisor
}
///|
pub fn ConvolutionKernel::bias(self : ConvolutionKernel) -> Int {
self.bias
}
///|
pub fn ConvolutionKernel::box_blur(size? : Int = 3) -> ConvolutionKernel {
let safe = if size < 1 { 1 } else if size % 2 == 0 { size + 1 } else { size }
ConvolutionKernel::new(
width=safe,
height=safe,
weights=Array::make(safe * safe, 1)[:],
divisor=safe * safe,
bias=0,
)
}
///|
pub fn ConvolutionKernel::gaussian_3() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[1, 2, 1, 2, 4, 2, 1, 2, 1][:],
divisor=16,
bias=0,
)
}
///|
pub fn ConvolutionKernel::sharpen() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[0, -1, 0, -1, 5, -1, 0, -1, 0][:],
divisor=1,
bias=0,
)
}
///|
pub fn ConvolutionKernel::unsharp() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[-1, -1, -1, -1, 9, -1, -1, -1, -1][:],
divisor=1,
bias=0,
)
}
///|
pub fn ConvolutionKernel::edge() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[-1, -1, -1, -1, 8, -1, -1, -1, -1][:],
divisor=1,
bias=128,
)
}
///|
pub fn ConvolutionKernel::emboss() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[-2, -1, 0, -1, 1, 1, 0, 1, 2][:],
divisor=1,
bias=128,
)
}
///|
pub fn ConvolutionKernel::laplacian() -> ConvolutionKernel {
ConvolutionKernel::new(
width=3,
height=3,
weights=[0, 1, 0, 1, -4, 1, 0, 1, 0][:],
divisor=1,
bias=128,
)
}
///|
fn convolution_sample(canvas : Canvas, x : Int, y : Int) -> Rgba {
let safe_x = if x < 0 {
0
} else if x >= canvas.width {
canvas.width - 1
} else {
x
}
let safe_y = if y < 0 {
0
} else if y >= canvas.height {
canvas.height - 1
} else {
y
}
canvas.pixels[safe_x + safe_y * canvas.width]
}
///|
fn convolution_channel(
canvas : Canvas,
x : Int,
y : Int,
kernel : ConvolutionKernel,
channel : Int,
) -> Int {
let half_x = kernel.width / 2
let half_y = kernel.height / 2
let mut total = kernel.bias
for ky in 0.. color.red().to_int()
1 => color.green().to_int()
2 => color.blue().to_int()
_ => color.alpha().to_int()
}
total += value * kernel.weights[kx + ky * kernel.width]
}
}
total / kernel.divisor
}
///|
/// Apply a convolution kernel with edge-clamped sampling.
pub fn Canvas::convolve(self : Canvas, kernel : ConvolutionKernel) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Canvas {
let size = if radius <= 0 { 1 } else { radius * 2 + 1 }
self.convolve(ConvolutionKernel::box_blur(size~))
}
///|
pub fn Canvas::gaussian_blur(self : Canvas) -> Canvas {
self.convolve(ConvolutionKernel::gaussian_3())
}
///|
pub fn Canvas::sharpen(self : Canvas) -> Canvas {
self.convolve(ConvolutionKernel::sharpen())
}
///|
pub fn Canvas::unsharp_mask(self : Canvas, amount? : Int = 1) -> Canvas {
if amount <= 0 {
self.copy()
} else {
let blurred = self.gaussian_blur()
let result = self.copy()
for i in 0.. Canvas {
self.convolve(ConvolutionKernel::emboss())
}
///|
pub fn Canvas::edge_map(self : Canvas) -> Canvas {
self.convolve(ConvolutionKernel::edge())
}
///|
pub fn Canvas::laplacian(self : Canvas) -> Canvas {
self.convolve(ConvolutionKernel::laplacian())
}
///|
fn luminance_at(canvas : Canvas, x : Int, y : Int) -> Int {
convolution_sample(canvas, x, y).luma().to_int()
}
///|
/// Return the Sobel gradient magnitude as an opaque grayscale canvas.
pub fn Canvas::sobel(self : Canvas, threshold? : Int = 0) -> Canvas {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgb(0, 0, 0),
)
for y in 0.. 65025 {
255
} else {
int_sqrt(gx * gx + gy * gy)
}
let value = if magnitude < threshold { 0 } else { magnitude }
result.pixels[x + y * self.width] = rgb(
value.to_byte(),
value.to_byte(),
value.to_byte(),
)
}
}
result
}
///|
fn int_sqrt(value : Int) -> Int {
if value <= 0 {
0
} else {
let mut guess = if value > 255 * 255 { 255 } else { value }
let mut changed = true
while changed {
let next = (guess + value / guess) / 2
if next < guess {
guess = next
} else {
changed = false
}
}
if guess > 255 {
255
} else {
guess
}
}
}
///|
fn neighborhood_values(
canvas : Canvas,
x : Int,
y : Int,
radius : Int,
channel : Int,
) -> Array[Int] {
let values : Array[Int] = []
for row in (y - radius)..<(y + radius + 1) {
for column in (x - radius)..<(x + radius + 1) {
let color = convolution_sample(canvas, column, row)
values.push(
match channel {
0 => color.red().to_int()
1 => color.green().to_int()
2 => color.blue().to_int()
_ => color.alpha().to_int()
},
)
}
}
values
}
///|
fn median_value(values : Array[Int]) -> Int {
for i in 1.. 0 && values[j - 1] > value {
values[j] = values[j - 1]
j -= 1
}
values[j] = value
}
if values.length() == 0 {
0
} else {
values[values.length() / 2]
}
}
///|
/// Remove isolated salt-and-pepper noise with a median neighborhood filter.
pub fn Canvas::median_filter(self : Canvas, radius? : Int = 1) -> Canvas {
let safe_radius = if radius < 0 { 0 } else if radius > 4 { 4 } else { radius }
if safe_radius == 0 {
self.copy()
} else {
let result = Canvas::new(
width=self.width,
height=self.height,
fill=rgba(0, 0, 0, 0),
)
for y in 0.. Canvas {
if block_size <= 1 {
self.copy()
} else {
let result = self.copy()
for top in 0.. self.width {
self.width
} else {
left + block_size
}
let bottom = if top + block_size > self.height {
self.height
} else {
top + block_size
}
let mut red = 0
let mut green = 0
let mut blue = 0
let mut alpha = 0
let mut count = 0
for y in top.. 0 {
let average = rgba(
(red / count).to_byte(),
(green / count).to_byte(),
(blue / count).to_byte(),
(alpha / count).to_byte(),
)
for y in top..