///|
/// Canny edge detector: the full classic pipeline — Gaussian smoothing,
/// Sobel gradients, non-maximum suppression along the quantized gradient
/// direction, then double thresholding with hysteresis edge tracking.
/// Output is a binary map (255 = edge, 0 = background).
///|
/// Runs Canny edge detection with the given hysteresis thresholds
/// (`low < high`, both compared against the L1 gradient magnitude
/// `|Gx| + |Gy|`). Pixels above `high` seed edges; pixels above `low`
/// survive only when 8-connected to a seed. Alpha is preserved.
pub fn Image::canny(self : Image, low : Int, high : Int) -> Image {
let w = self.width
let h = self.height
let n = w * h
// 1) Smooth luma to suppress noise before differentiation.
let gray = self.grayscale().blur()
// 2) Sobel gradients kept as raw signed values for direction info.
let gx : Array[Int] = Array::make(n, 0)
let gy : Array[Int] = Array::make(n, 0)
let mag : Array[Int] = Array::make(n, 0)
let kx = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
let ky = [-1, -2, -1, 0, 0, 0, 1, 2, 1]
for y in 0.. compare left/right
dy = 0
} else if ax <= 0.41421356 * ay {
dx = 0 // vertical gradient -> compare up/down
dy = 1
} else if gx[p] * gy[p] > 0 {
dx = 1 // 45 deg diagonal
dy = 1
} else {
dx = 1 // 135 deg diagonal
dy = -1
}
let q1 = clamp_coord(y + dy, h) * w + clamp_coord(x + dx, w)
let q2 = clamp_coord(y - dy, h) * w + clamp_coord(x - dx, w)
if m >= mag[q1] && m >= mag[q2] {
thin[p] = m
}
}
}
// 4) Double threshold + hysteresis: flood from strong pixels through
// weak ones using an explicit stack and 8-connectivity.
// state: 0 = none, 1 = weak candidate, 2 = confirmed edge.
let state : Array[Int] = Array::make(n, 0)
let stack : Array[Int] = []
for p in 0..= high {
state[p] = 2
stack.push(p)
} else if thin[p] >= low {
state[p] = 1
}
}
while stack.length() > 0 {
let p = stack.unsafe_pop()
let px = p % w
let py = p / w
for dy in -1..<2 {
for dx in -1..<2 {
let nx = px + dx
let ny = py + dy
if nx < 0 || nx >= w || ny < 0 || ny >= h {
continue
}
let q = ny * w + nx
if state[q] == 1 {
state[q] = 2
stack.push(q)
}
}
}
}
let out = Image::new(w, h)
for p in 0..