1use crate::error::ImageError;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum BorderMode {
17 #[default]
19 Zero,
20 Clamp,
22 Reflect,
24 Wrap,
26}
27
28pub fn conv2d(
39 image: &[f32],
40 width: usize,
41 height: usize,
42 kernel: &[f32],
43 kw: usize,
44 kh: usize,
45 border: BorderMode,
46) -> Result<Vec<f32>, ImageError> {
47 validate_conv_args(image, width, height, kernel, kw, kh)?;
48
49 let half_kw = kw / 2;
50 let half_kh = kh / 2;
51 let mut output = vec![0.0f32; width * height];
52
53 for y in 0..height {
54 for x in 0..width {
55 let mut sum = 0.0f64;
56 for ky in 0..kh {
57 for kx in 0..kw {
58 let iy = y as isize + ky as isize - half_kh as isize;
59 let ix = x as isize + kx as isize - half_kw as isize;
60
61 let pixel = sample_border(image, width, height, ix, iy, border);
62 sum += f64::from(pixel) * f64::from(kernel[ky * kw + kx]);
63 }
64 }
65 output[y * width + x] = sum as f32;
66 }
67 }
68
69 Ok(output)
70}
71
72pub fn separable_conv2d(
81 image: &[f32],
82 width: usize,
83 height: usize,
84 h_kernel: &[f32],
85 v_kernel: &[f32],
86 border: BorderMode,
87) -> Result<Vec<f32>, ImageError> {
88 if width == 0 || height == 0 {
89 return Err(ImageError::ZeroDimension { width, height });
90 }
91 if image.len() != width * height {
92 return Err(ImageError::BufferLengthMismatch {
93 expected: width * height,
94 got: image.len(),
95 width,
96 height,
97 });
98 }
99
100 let hk = h_kernel.len();
102 let half_hk = hk / 2;
103 let mut temp = vec![0.0f32; width * height];
104
105 for y in 0..height {
106 for x in 0..width {
107 let mut sum = 0.0f64;
108 for k in 0..hk {
109 let ix = x as isize + k as isize - half_hk as isize;
110 let pixel = sample_border(image, width, height, ix, y as isize, border);
111 sum += f64::from(pixel) * f64::from(h_kernel[k]);
112 }
113 temp[y * width + x] = sum as f32;
114 }
115 }
116
117 let vk = v_kernel.len();
119 let half_vk = vk / 2;
120 let mut output = vec![0.0f32; width * height];
121
122 for y in 0..height {
123 for x in 0..width {
124 let mut sum = 0.0f64;
125 for k in 0..vk {
126 let iy = y as isize + k as isize - half_vk as isize;
127 let pixel = sample_border(&temp, width, height, x as isize, iy, border);
128 sum += f64::from(pixel) * f64::from(v_kernel[k]);
129 }
130 output[y * width + x] = sum as f32;
131 }
132 }
133
134 Ok(output)
135}
136
137pub fn gaussian_blur(
143 image: &[f32],
144 width: usize,
145 height: usize,
146 sigma: f32,
147) -> Result<Vec<f32>, ImageError> {
148 let kernel = gaussian_kernel_1d(sigma);
149 separable_conv2d(image, width, height, &kernel, &kernel, BorderMode::Clamp)
150}
151
152#[allow(clippy::cast_precision_loss)]
156fn gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
157 let radius = ((3.0 * sigma).ceil() as usize).max(1);
158 let size = 2 * radius + 1;
159 let sigma_sq = f64::from(sigma) * f64::from(sigma);
160
161 let mut kernel = vec![0.0f32; size];
162 let mut sum = 0.0f64;
163
164 for i in 0..size {
165 let x = i as f64 - radius as f64;
166 let v = (-x * x / (2.0 * sigma_sq)).exp();
167 kernel[i] = v as f32;
168 sum += v;
169 }
170
171 for k in &mut kernel {
173 *k = (*k as f64 / sum) as f32;
174 }
175
176 kernel
177}
178
179pub fn sobel(
185 image: &[f32],
186 width: usize,
187 height: usize,
188) -> Result<(Vec<f32>, Vec<f32>), ImageError> {
189 if width == 0 || height == 0 {
190 return Err(ImageError::ZeroDimension { width, height });
191 }
192 if image.len() != width * height {
193 return Err(ImageError::BufferLengthMismatch {
194 expected: width * height,
195 got: image.len(),
196 width,
197 height,
198 });
199 }
200
201 let sx = [-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0_f32];
203 let sy = [-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0_f32];
204
205 let gx = conv2d(image, width, height, &sx, 3, 3, BorderMode::Zero)?;
206 let gy = conv2d(image, width, height, &sy, 3, 3, BorderMode::Zero)?;
207
208 Ok((gx, gy))
209}
210
211pub fn gradient_magnitude(gx: &[f32], gy: &[f32]) -> Vec<f32> {
213 gx.iter()
214 .zip(gy.iter())
215 .map(|(&x, &y)| (x * x + y * y).sqrt())
216 .collect()
217}
218
219pub fn canny(
229 image: &[f32],
230 width: usize,
231 height: usize,
232 sigma: f32,
233 low_threshold: f32,
234 high_threshold: f32,
235) -> Result<Vec<f32>, ImageError> {
236 if low_threshold < 0.0 || high_threshold < low_threshold || high_threshold > 1.0 {
237 return Err(ImageError::InvalidThresholds {
238 low: low_threshold,
239 high: high_threshold,
240 });
241 }
242
243 let blurred = gaussian_blur(image, width, height, sigma)?;
245
246 let (gx, gy) = sobel(&blurred, width, height)?;
248 let mag = gradient_magnitude(&gx, &gy);
249
250 let max_mag = mag.iter().copied().fold(0.0f32, f32::max);
252 let mag_norm: Vec<f32> = if max_mag > 0.0 {
253 mag.iter().map(|&m| m / max_mag).collect()
254 } else {
255 mag
256 };
257
258 let nms = non_maximum_suppression(&mag_norm, &gx, &gy, width, height);
260
261 Ok(hysteresis_threshold(
263 &nms,
264 width,
265 height,
266 low_threshold,
267 high_threshold,
268 ))
269}
270
271fn non_maximum_suppression(
273 mag: &[f32],
274 gx: &[f32],
275 gy: &[f32],
276 width: usize,
277 height: usize,
278) -> Vec<f32> {
279 let mut nms = vec![0.0f32; width * height];
280 for y in 1..height.saturating_sub(1) {
281 for x in 1..width.saturating_sub(1) {
282 let idx = y * width + x;
283 let angle = gy[idx].atan2(gx[idx]);
284 let m = mag[idx];
285
286 let dir =
287 ((angle + std::f32::consts::PI) / std::f32::consts::FRAC_PI_4).round() as usize % 4;
288 let (n1, n2) = match dir {
289 0 => (mag[idx - 1], mag[idx + 1]),
290 1 => (mag[(y - 1) * width + x + 1], mag[(y + 1) * width + x - 1]),
291 2 => (mag[(y - 1) * width + x], mag[(y + 1) * width + x]),
292 _ => (mag[(y - 1) * width + x - 1], mag[(y + 1) * width + x + 1]),
293 };
294
295 if m >= n1 && m >= n2 {
296 nms[idx] = m;
297 }
298 }
299 }
300 nms
301}
302
303fn hysteresis_threshold(nms: &[f32], width: usize, height: usize, low: f32, high: f32) -> Vec<f32> {
305 let mut edges = vec![0.0f32; width * height];
306 for y in 1..height.saturating_sub(1) {
307 for x in 1..width.saturating_sub(1) {
308 let idx = y * width + x;
309 if nms[idx] >= high {
310 edges[idx] = 1.0;
311 } else if nms[idx] >= low {
312 let has_strong = [
313 (y - 1, x - 1),
314 (y - 1, x),
315 (y - 1, x + 1),
316 (y, x - 1),
317 (y, x + 1),
318 (y + 1, x - 1),
319 (y + 1, x),
320 (y + 1, x + 1),
321 ]
322 .iter()
323 .any(|&(ny, nx)| nms[ny * width + nx] >= high);
324
325 if has_strong {
326 edges[idx] = 1.0;
327 }
328 }
329 }
330 }
331 edges
332}
333
334fn sample_border(
335 image: &[f32],
336 width: usize,
337 height: usize,
338 x: isize,
339 y: isize,
340 border: BorderMode,
341) -> f32 {
342 match border {
343 BorderMode::Zero => {
344 if x < 0 || y < 0 || x >= width as isize || y >= height as isize {
345 0.0
346 } else {
347 image[y as usize * width + x as usize]
348 }
349 }
350 BorderMode::Clamp => {
351 let cx = x.clamp(0, width as isize - 1) as usize;
352 let cy = y.clamp(0, height as isize - 1) as usize;
353 image[cy * width + cx]
354 }
355 BorderMode::Reflect => {
356 let rx = reflect(x, width);
357 let ry = reflect(y, height);
358 image[ry * width + rx]
359 }
360 BorderMode::Wrap => {
361 let wx = wrap(x, width);
362 let wy = wrap(y, height);
363 image[wy * width + wx]
364 }
365 }
366}
367
368fn wrap(i: isize, size: usize) -> usize {
369 let s = size as isize;
370 ((i % s + s) % s) as usize
371}
372
373fn reflect(i: isize, size: usize) -> usize {
374 if i < 0 {
375 (-i - 1).min(size as isize - 1) as usize
376 } else if i >= size as isize {
377 (2 * size as isize - i - 1).max(0) as usize
378 } else {
379 i as usize
380 }
381}
382
383fn validate_conv_args(
384 image: &[f32],
385 width: usize,
386 height: usize,
387 kernel: &[f32],
388 kw: usize,
389 kh: usize,
390) -> Result<(), ImageError> {
391 if width == 0 || height == 0 {
392 return Err(ImageError::ZeroDimension { width, height });
393 }
394 if kw == 0 || kh == 0 || kw.is_multiple_of(2) || kh.is_multiple_of(2) {
395 return Err(ImageError::InvalidKernelSize { kw, kh });
396 }
397 if image.len() != width * height {
398 return Err(ImageError::BufferLengthMismatch {
399 expected: width * height,
400 got: image.len(),
401 width,
402 height,
403 });
404 }
405 if kernel.len() != kw * kh {
406 return Err(ImageError::InvalidKernelSize { kw, kh });
407 }
408 Ok(())
409}
410
411pub fn canny_rgb(
423 image: &[f32],
424 width: usize,
425 height: usize,
426 channels: usize,
427 sigma: f32,
428 low_threshold: f32,
429 high_threshold: f32,
430) -> Result<Vec<f32>, ImageError> {
431 let expected = width * height * channels;
432 if image.len() != expected {
433 return Err(ImageError::BufferLengthMismatch {
434 expected,
435 got: image.len(),
436 width,
437 height,
438 });
439 }
440
441 let gray = if channels == 1 {
443 image.to_vec()
444 } else {
445 let mut g = Vec::with_capacity(width * height);
446 for i in 0..width * height {
447 let base = i * channels;
448 let r = image[base];
449 let green = if channels > 1 { image[base + 1] } else { 0.0 };
450 let b = if channels > 2 { image[base + 2] } else { 0.0 };
451 g.push(0.299 * r + 0.587 * green + 0.114 * b);
452 }
453 g
454 };
455
456 canny(&gray, width, height, sigma, low_threshold, high_threshold)
457}