1use yscv_tensor::Tensor;
2
3use super::super::ImgProcError;
4use super::super::shape::hwc_shape;
5
6#[allow(unsafe_code)]
11pub fn draw_rect(
12 image: &mut Tensor,
13 x: usize,
14 y: usize,
15 rect_w: usize,
16 rect_h: usize,
17 color: [f32; 3],
18 thickness: usize,
19) -> Result<(), ImgProcError> {
20 let (img_h, img_w, channels) = hwc_shape(image)?;
21 if channels != 3 {
22 return Err(ImgProcError::InvalidChannelCount {
23 expected: 3,
24 got: channels,
25 });
26 }
27
28 let data = image.data_mut();
29
30 let set_pixel = |data: &mut [f32], py: usize, px: usize, color: &[f32; 3]| {
31 if py < img_h && px < img_w {
32 let idx = (py * img_w + px) * 3;
33 data[idx] = color[0];
34 data[idx + 1] = color[1];
35 data[idx + 2] = color[2];
36 }
37 };
38
39 if thickness == 0 {
40 for py in y..std::cmp::min(y + rect_h, img_h) {
42 for px in x..std::cmp::min(x + rect_w, img_w) {
43 set_pixel(data, py, px, &color);
44 }
45 }
46 } else {
47 for t in 0..thickness {
49 for px in x.saturating_sub(t)..std::cmp::min(x + rect_w + t, img_w) {
51 if y >= t {
52 set_pixel(data, y - t, px, &color);
53 }
54 if y + t < img_h {
55 set_pixel(data, y + t, px, &color);
56 }
57 let bot = y + rect_h.saturating_sub(1);
58 if bot >= t {
59 set_pixel(data, bot - t, px, &color);
60 }
61 if bot + t < img_h {
62 set_pixel(data, bot + t, px, &color);
63 }
64 }
65 for py in y.saturating_sub(t)..std::cmp::min(y + rect_h + t, img_h) {
67 if x >= t {
68 set_pixel(data, py, x - t, &color);
69 }
70 if x + t < img_w {
71 set_pixel(data, py, x + t, &color);
72 }
73 let right = x + rect_w.saturating_sub(1);
74 if right >= t {
75 set_pixel(data, py, right - t, &color);
76 }
77 if right + t < img_w {
78 set_pixel(data, py, right + t, &color);
79 }
80 }
81 }
82 }
83
84 Ok(())
85}
86
87const FONT_8X8: [[u8; 8]; 95] = {
90 let mut f = [[0u8; 8]; 95];
91
92 f[0] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
94 f[1] = [0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00];
96 f[2] = [0x6C, 0x6C, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00];
98 f[3] = [0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00];
100 f[4] = [0x18, 0x7E, 0xC0, 0x7C, 0x06, 0xFC, 0x18, 0x00];
102 f[5] = [0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00];
104 f[6] = [0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00];
106 f[7] = [0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00];
108 f[8] = [0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00];
110 f[9] = [0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00];
112 f[10] = [0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00];
114 f[11] = [0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00];
116 f[12] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30];
118 f[13] = [0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00];
120 f[14] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00];
122 f[15] = [0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00];
124 f[16] = [0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0x7C, 0x00];
126 f[17] = [0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x7E, 0x00];
128 f[18] = [0x7C, 0xC6, 0x06, 0x1C, 0x30, 0x66, 0xFE, 0x00];
130 f[19] = [0x7C, 0xC6, 0x06, 0x3C, 0x06, 0xC6, 0x7C, 0x00];
132 f[20] = [0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x1E, 0x00];
134 f[21] = [0xFE, 0xC0, 0xFC, 0x06, 0x06, 0xC6, 0x7C, 0x00];
136 f[22] = [0x38, 0x60, 0xC0, 0xFC, 0xC6, 0xC6, 0x7C, 0x00];
138 f[23] = [0xFE, 0xC6, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00];
140 f[24] = [0x7C, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0x7C, 0x00];
142 f[25] = [0x7C, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0x78, 0x00];
144 f[26] = [0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00];
146 f[27] = [0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30];
148 f[28] = [0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x00];
150 f[29] = [0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00];
152 f[30] = [0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00];
154 f[31] = [0x7C, 0xC6, 0x0C, 0x18, 0x18, 0x00, 0x18, 0x00];
156 f[32] = [0x7C, 0xC6, 0xDE, 0xDE, 0xDC, 0xC0, 0x7C, 0x00];
158 f[33] = [0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x00];
160 f[34] = [0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00];
162 f[35] = [0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00];
164 f[36] = [0xF8, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00];
166 f[37] = [0xFE, 0x62, 0x68, 0x78, 0x68, 0x62, 0xFE, 0x00];
168 f[38] = [0xFE, 0x62, 0x68, 0x78, 0x68, 0x60, 0xF0, 0x00];
170 f[39] = [0x3C, 0x66, 0xC0, 0xC0, 0xCE, 0x66, 0x3E, 0x00];
172 f[40] = [0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0x00];
174 f[41] = [0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
176 f[42] = [0x1E, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00];
178 f[43] = [0xE6, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0xE6, 0x00];
180 f[44] = [0xF0, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00];
182 f[45] = [0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00];
184 f[46] = [0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00];
186 f[47] = [0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
188 f[48] = [0xFC, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00];
190 f[49] = [0x7C, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x06];
192 f[50] = [0xFC, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0xE6, 0x00];
194 f[51] = [0x7C, 0xC6, 0xC0, 0x7C, 0x06, 0xC6, 0x7C, 0x00];
196 f[52] = [0x7E, 0x5A, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
198 f[53] = [0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
200 f[54] = [0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00];
202 f[55] = [0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00];
204 f[56] = [0xC6, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0xC6, 0x00];
206 f[57] = [0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x3C, 0x00];
208 f[58] = [0xFE, 0xC6, 0x8C, 0x18, 0x32, 0x66, 0xFE, 0x00];
210 f[59] = [0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00];
212 f[60] = [0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00];
214 f[61] = [0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00];
216 f[62] = [0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00];
218 f[63] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF];
220 f[64] = [0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00];
222 f[65] = [0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00];
224 f[66] = [0xE0, 0x60, 0x7C, 0x66, 0x66, 0x66, 0xDC, 0x00];
226 f[67] = [0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC6, 0x7C, 0x00];
228 f[68] = [0x1C, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00];
230 f[69] = [0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0x7C, 0x00];
232 f[70] = [0x1C, 0x36, 0x30, 0x78, 0x30, 0x30, 0x78, 0x00];
234 f[71] = [0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x78];
236 f[72] = [0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00];
238 f[73] = [0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00];
240 f[74] = [0x06, 0x00, 0x0E, 0x06, 0x06, 0x66, 0x66, 0x3C];
242 f[75] = [0xE0, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0xE6, 0x00];
244 f[76] = [0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
246 f[77] = [0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xC6, 0xC6, 0x00];
248 f[78] = [0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x00];
250 f[79] = [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
252 f[80] = [0x00, 0x00, 0xDC, 0x66, 0x66, 0x7C, 0x60, 0xF0];
254 f[81] = [0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x1E];
256 f[82] = [0x00, 0x00, 0xDC, 0x76, 0x60, 0x60, 0xF0, 0x00];
258 f[83] = [0x00, 0x00, 0x7C, 0xC0, 0x7C, 0x06, 0xFC, 0x00];
260 f[84] = [0x10, 0x30, 0x7C, 0x30, 0x30, 0x34, 0x18, 0x00];
262 f[85] = [0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00];
264 f[86] = [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00];
266 f[87] = [0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xFE, 0x6C, 0x00];
268 f[88] = [0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00];
270 f[89] = [0x00, 0x00, 0xC6, 0xC6, 0xCE, 0x76, 0x06, 0xFC];
272 f[90] = [0x00, 0x00, 0xFC, 0x98, 0x30, 0x64, 0xFC, 0x00];
274 f[91] = [0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00];
276 f[92] = [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00];
278 f[93] = [0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00];
280 f[94] = [0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
282
283 f
284};
285
286pub fn draw_text(
292 image: &mut Tensor,
293 text: &str,
294 x: usize,
295 y: usize,
296 color: [f32; 3],
297) -> Result<(), ImgProcError> {
298 let (img_h, img_w, channels) = hwc_shape(image)?;
299 if channels != 3 {
300 return Err(ImgProcError::InvalidChannelCount {
301 expected: 3,
302 got: channels,
303 });
304 }
305
306 let data = image.data_mut();
307
308 for (ci, ch) in text.bytes().enumerate() {
309 let glyph_idx = if (32..=126).contains(&ch) {
310 (ch - 32) as usize
311 } else {
312 31 };
314 let glyph = &FONT_8X8[glyph_idx];
315 let cx = x + ci * 8;
316 for row in 0..8 {
317 let py = y + row;
318 if py >= img_h {
319 break;
320 }
321 let bits = glyph[row];
322 for col in 0..8 {
323 let px = cx + col;
324 if px >= img_w {
325 break;
326 }
327 if bits & (0x80 >> col) != 0 {
328 let idx = (py * img_w + px) * 3;
329 data[idx] = color[0];
330 data[idx + 1] = color[1];
331 data[idx + 2] = color[2];
332 }
333 }
334 }
335 }
336
337 Ok(())
338}
339
340pub struct Detection {
342 pub x: usize,
343 pub y: usize,
344 pub width: usize,
345 pub height: usize,
346 pub score: f32,
347 pub class_id: usize,
348}
349
350pub fn draw_detections(
356 image: &mut Tensor,
357 detections: &[Detection],
358 labels: &[&str],
359) -> Result<(), ImgProcError> {
360 const PALETTE: [[f32; 3]; 10] = [
361 [1.0, 0.0, 0.0],
362 [0.0, 1.0, 0.0],
363 [0.0, 0.0, 1.0],
364 [1.0, 1.0, 0.0],
365 [1.0, 0.0, 1.0],
366 [0.0, 1.0, 1.0],
367 [1.0, 0.5, 0.0],
368 [0.5, 0.0, 1.0],
369 [0.0, 0.5, 0.0],
370 [0.5, 0.5, 0.5],
371 ];
372
373 for det in detections {
374 let color = PALETTE[det.class_id % PALETTE.len()];
375 draw_rect(image, det.x, det.y, det.width, det.height, color, 2)?;
376
377 if det.class_id < labels.len() {
378 let label = format!("{} {:.2}", labels[det.class_id], det.score);
379 let text_y = if det.y >= 10 {
380 det.y - 10
381 } else {
382 det.y + det.height + 2
383 };
384 draw_text(image, &label, det.x, text_y, color)?;
385 }
386 }
387
388 Ok(())
389}
390
391#[allow(unsafe_code)]
393pub fn draw_line(
394 image: &mut Tensor,
395 x0: i32,
396 y0: i32,
397 x1: i32,
398 y1: i32,
399 color: [f32; 3],
400 thickness: usize,
401) -> Result<(), ImgProcError> {
402 let (img_h, img_w, channels) = hwc_shape(image)?;
403 if channels != 3 {
404 return Err(ImgProcError::InvalidChannelCount {
405 expected: 3,
406 got: channels,
407 });
408 }
409
410 let data = image.data_mut();
411 let half_t = thickness.saturating_sub(1) / 2;
412
413 let set_thick_pixel = |data: &mut [f32], py: i32, px: i32| {
414 for dy in 0..=half_t {
415 for dx in 0..=half_t {
416 for (sy, sx) in [
417 (py + dy as i32, px + dx as i32),
418 (py + dy as i32, px - dx as i32),
419 (py - (dy as i32), px + dx as i32),
420 (py - (dy as i32), px - dx as i32),
421 ] {
422 if sy >= 0 && (sy as usize) < img_h && sx >= 0 && (sx as usize) < img_w {
423 let idx = (sy as usize * img_w + sx as usize) * 3;
424 data[idx] = color[0];
425 data[idx + 1] = color[1];
426 data[idx + 2] = color[2];
427 }
428 }
429 }
430 }
431 };
432
433 let mut x = x0;
434 let mut y = y0;
435 let dx = (x1 - x0).abs();
436 let dy = -(y1 - y0).abs();
437 let sx = if x0 < x1 { 1 } else { -1 };
438 let sy = if y0 < y1 { 1 } else { -1 };
439 let mut err = dx + dy;
440
441 loop {
442 set_thick_pixel(data, y, x);
443 if x == x1 && y == y1 {
444 break;
445 }
446 let e2 = 2 * err;
447 if e2 >= dy {
448 err += dy;
449 x += sx;
450 }
451 if e2 <= dx {
452 err += dx;
453 y += sy;
454 }
455 }
456
457 Ok(())
458}
459
460#[allow(unsafe_code)]
465pub fn draw_circle(
466 image: &mut Tensor,
467 cx: i32,
468 cy: i32,
469 radius: usize,
470 color: [f32; 3],
471 thickness: usize,
472) -> Result<(), ImgProcError> {
473 let (img_h, img_w, channels) = hwc_shape(image)?;
474 if channels != 3 {
475 return Err(ImgProcError::InvalidChannelCount {
476 expected: 3,
477 got: channels,
478 });
479 }
480
481 let data = image.data_mut();
482 let r = radius as i32;
483
484 if thickness == 0 {
485 for py in (cy - r).max(0)..=(cy + r).min(img_h as i32 - 1) {
487 let dy = py - cy;
488 let half_w = ((r * r - dy * dy) as f32).sqrt() as i32;
489 for px in (cx - half_w).max(0)..=(cx + half_w).min(img_w as i32 - 1) {
490 let idx = (py as usize * img_w + px as usize) * 3;
491 data[idx] = color[0];
492 data[idx + 1] = color[1];
493 data[idx + 2] = color[2];
494 }
495 }
496 } else {
497 let mut x = 0i32;
499 let mut y = r;
500 let mut d = 1 - r;
501
502 let plot = |data: &mut [f32], px: i32, py: i32| {
503 if px >= 0 && (px as usize) < img_w && py >= 0 && (py as usize) < img_h {
504 let idx = (py as usize * img_w + px as usize) * 3;
505 data[idx] = color[0];
506 data[idx + 1] = color[1];
507 data[idx + 2] = color[2];
508 }
509 };
510
511 while x <= y {
512 plot(data, cx + x, cy + y);
513 plot(data, cx - x, cy + y);
514 plot(data, cx + x, cy - y);
515 plot(data, cx - x, cy - y);
516 plot(data, cx + y, cy + x);
517 plot(data, cx - y, cy + x);
518 plot(data, cx + y, cy - x);
519 plot(data, cx - y, cy - x);
520
521 if d < 0 {
522 d += 2 * x + 3;
523 } else {
524 d += 2 * (x - y) + 5;
525 y -= 1;
526 }
527 x += 1;
528 }
529 }
530
531 Ok(())
532}
533
534pub fn draw_polylines(
539 image: &mut Tensor,
540 points: &[(i32, i32)],
541 closed: bool,
542 color: [f32; 3],
543 thickness: usize,
544) -> Result<(), ImgProcError> {
545 if points.len() < 2 {
546 return Ok(());
547 }
548 for i in 0..points.len() - 1 {
549 draw_line(
550 image,
551 points[i].0,
552 points[i].1,
553 points[i + 1].0,
554 points[i + 1].1,
555 color,
556 thickness,
557 )?;
558 }
559 if closed {
560 let last = points.len() - 1;
561 draw_line(
562 image,
563 points[last].0,
564 points[last].1,
565 points[0].0,
566 points[0].1,
567 color,
568 thickness,
569 )?;
570 }
571 Ok(())
572}
573
574#[allow(unsafe_code)]
576pub fn fill_poly(
577 image: &mut Tensor,
578 points: &[(i32, i32)],
579 color: [f32; 3],
580) -> Result<(), ImgProcError> {
581 let (img_h, img_w, channels) = hwc_shape(image)?;
582 if channels != 3 {
583 return Err(ImgProcError::InvalidChannelCount {
584 expected: 3,
585 got: channels,
586 });
587 }
588 if points.len() < 3 {
589 return Ok(());
590 }
591
592 let min_y = points
593 .iter()
594 .map(|p| p.1)
595 .min()
596 .expect("non-empty points")
597 .max(0) as usize;
598 let max_y = points
599 .iter()
600 .map(|p| p.1)
601 .max()
602 .expect("non-empty points")
603 .min(img_h as i32 - 1) as usize;
604
605 let data = image.data_mut();
606 let n = points.len();
607
608 for y in min_y..=max_y {
609 let mut intersections: Vec<i32> = Vec::new();
610 let yf = y as i32;
611 for i in 0..n {
612 let j = (i + 1) % n;
613 let (y0, y1) = (points[i].1, points[j].1);
614 let (x0, x1) = (points[i].0, points[j].0);
615 if (y0 <= yf && y1 > yf) || (y1 <= yf && y0 > yf) {
616 let x = x0 + (yf - y0) * (x1 - x0) / (y1 - y0);
617 intersections.push(x);
618 }
619 }
620 intersections.sort();
621
622 for pair in intersections.chunks(2) {
623 if pair.len() == 2 {
624 let x_start = pair[0].max(0) as usize;
625 let x_end = (pair[1] as usize).min(img_w - 1);
626 for px in x_start..=x_end {
627 let idx = (y * img_w + px) * 3;
628 data[idx] = color[0];
629 data[idx + 1] = color[1];
630 data[idx + 2] = color[2];
631 }
632 }
633 }
634 }
635
636 Ok(())
637}
638
639#[allow(unsafe_code)]
643pub fn draw_text_scaled(
644 image: &mut Tensor,
645 text: &str,
646 x: usize,
647 y: usize,
648 scale: usize,
649 color: [f32; 3],
650) -> Result<(), ImgProcError> {
651 let (img_h, img_w, channels) = hwc_shape(image)?;
652 if channels != 3 {
653 return Err(ImgProcError::InvalidChannelCount {
654 expected: 3,
655 got: channels,
656 });
657 }
658 let scale = scale.max(1);
659 let data = image.data_mut();
660
661 for (ci, ch) in text.bytes().enumerate() {
662 let glyph_idx = if (32..=126).contains(&ch) {
663 (ch - 32) as usize
664 } else {
665 31 };
667 let glyph = &FONT_8X8[glyph_idx];
668 let cx = x + ci * 8 * scale;
669 for row in 0..8 {
670 let bits = glyph[row];
671 for col in 0..8 {
672 if bits & (0x80 >> col) != 0 {
673 for sy in 0..scale {
675 let py = y + row * scale + sy;
676 if py >= img_h {
677 break;
678 }
679 for sx in 0..scale {
680 let px = cx + col * scale + sx;
681 if px >= img_w {
682 break;
683 }
684 let idx = (py * img_w + px) * 3;
685 data[idx] = color[0];
686 data[idx + 1] = color[1];
687 data[idx + 2] = color[2];
688 }
689 }
690 }
691 }
692 }
693 }
694
695 Ok(())
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 fn blank_image(h: usize, w: usize) -> Tensor {
703 Tensor::from_vec(vec![h, w, 3], vec![0.0f32; h * w * 3]).unwrap()
704 }
705
706 #[test]
707 fn test_draw_rect_fill() {
708 let mut img = blank_image(10, 10);
709 draw_rect(&mut img, 2, 2, 3, 3, [1.0, 0.0, 0.0], 0).unwrap();
710
711 let idx = (3 * 10 + 3) * 3;
713 let data = img.data();
714 assert_eq!(data[idx], 1.0);
715 assert_eq!(data[idx + 1], 0.0);
716
717 assert_eq!(data[0], 0.0);
719 }
720
721 #[test]
722 fn test_draw_rect_outline() {
723 let mut img = blank_image(20, 20);
724 draw_rect(&mut img, 5, 5, 10, 10, [0.0, 1.0, 0.0], 1).unwrap();
725
726 let idx = (5 * 20 + 5) * 3;
728 let data = img.data();
729 assert_eq!(data[idx + 1], 1.0);
730
731 let idx2 = (10 * 20 + 10) * 3;
733 assert_eq!(data[idx2], 0.0);
734 }
735
736 #[test]
737 fn test_draw_text_renders_pixels() {
738 let mut img = blank_image(16, 80);
739 draw_text(&mut img, "Hi", 0, 0, [1.0, 1.0, 1.0]).unwrap();
740
741 let data = img.data();
743 let white_count: usize = (0..8 * 16 * 3)
744 .step_by(3)
745 .filter(|&i| data[i] > 0.5)
746 .count();
747 assert!(
748 white_count > 5,
749 "expected some rendered pixels, got {white_count}"
750 );
751 }
752
753 #[test]
754 fn test_draw_detections_smoke() {
755 let mut img = blank_image(100, 100);
756 let dets = vec![
757 Detection {
758 x: 10,
759 y: 10,
760 width: 30,
761 height: 30,
762 score: 0.95,
763 class_id: 0,
764 },
765 Detection {
766 x: 50,
767 y: 50,
768 width: 20,
769 height: 20,
770 score: 0.80,
771 class_id: 1,
772 },
773 ];
774 draw_detections(&mut img, &dets, &["cat", "dog"]).unwrap();
775
776 let data = img.data();
778 let non_zero = data.iter().filter(|&&v| v > 0.0).count();
779 assert!(non_zero > 0, "expected some drawn pixels");
780 }
781
782 #[test]
783 fn test_draw_line_diagonal() {
784 let mut img = blank_image(20, 20);
785 draw_line(&mut img, 0, 0, 19, 19, [1.0, 0.0, 0.0], 1).unwrap();
786 let idx = (10 * 20 + 10) * 3;
788 assert_eq!(img.data()[idx], 1.0);
789 }
790
791 #[test]
792 fn test_draw_circle_filled() {
793 let mut img = blank_image(50, 50);
794 draw_circle(&mut img, 25, 25, 10, [0.0, 1.0, 0.0], 0).unwrap();
795 let idx = (25 * 50 + 25) * 3;
797 assert_eq!(img.data()[idx + 1], 1.0);
798 assert_eq!(img.data()[0], 0.0);
800 }
801
802 #[test]
803 fn test_draw_circle_outline() {
804 let mut img = blank_image(50, 50);
805 draw_circle(&mut img, 25, 25, 10, [1.0, 0.0, 0.0], 1).unwrap();
806 let idx = (25 * 50 + 25) * 3;
808 assert_eq!(img.data()[idx], 0.0);
809 let on_circle = (25 * 50 + 35) * 3; assert_eq!(img.data()[on_circle], 1.0);
812 }
813
814 #[test]
815 fn test_draw_polylines_closed() {
816 let mut img = blank_image(20, 20);
817 let pts = [(2, 2), (17, 2), (17, 17), (2, 17)];
818 draw_polylines(&mut img, &pts, true, [0.0, 0.0, 1.0], 1).unwrap();
819 let data = img.data();
820 let blue_count = (0..data.len())
821 .step_by(3)
822 .filter(|&i| data[i + 2] > 0.5)
823 .count();
824 assert!(blue_count > 40, "expected outline pixels, got {blue_count}");
825 }
826
827 #[test]
828 fn test_fill_poly_triangle() {
829 let mut img = blank_image(30, 30);
830 let pts = [(15, 2), (28, 27), (2, 27)];
831 fill_poly(&mut img, &pts, [1.0, 0.0, 0.0]).unwrap();
832 let idx = (15 * 30 + 15) * 3;
834 assert_eq!(img.data()[idx], 1.0);
835 }
836
837 #[test]
838 fn test_draw_text_scaled_larger() {
839 let mut img = blank_image(32, 64);
840 draw_text_scaled(&mut img, "A", 0, 0, 2, [1.0, 1.0, 1.0]).unwrap();
841 let data = img.data();
842 let white_count = (0..data.len())
843 .step_by(3)
844 .filter(|&i| data[i] > 0.5)
845 .count();
846 assert!(
848 white_count > 20,
849 "expected scaled text pixels, got {white_count}"
850 );
851 }
852}