Skip to main content

yscv_imgproc/ops/
draw.rs

1use yscv_tensor::Tensor;
2
3use super::super::ImgProcError;
4use super::super::shape::hwc_shape;
5
6/// Draw a rectangle on a `[H, W, 3]` image tensor (values `[0, 1]`).
7///
8/// `(x, y)` is the top-left corner, `(w, h)` is width and height in pixels.
9/// `thickness` specifies line width; `0` fills the rectangle.
10#[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        // Fill rectangle.
41        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        // Draw outline with given thickness.
48        for t in 0..thickness {
49            // Top and bottom horizontal lines.
50            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            // Left and right vertical lines.
66            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
87/// Embedded 8×8 bitmap font covering ASCII 32–126.
88/// Each character is stored as 8 bytes (one per row, MSB = left pixel).
89const FONT_8X8: [[u8; 8]; 95] = {
90    let mut f = [[0u8; 8]; 95];
91
92    // space
93    f[0] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
94    // !
95    f[1] = [0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x00];
96    // "
97    f[2] = [0x6C, 0x6C, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00];
98    // #
99    f[3] = [0x6C, 0x6C, 0xFE, 0x6C, 0xFE, 0x6C, 0x6C, 0x00];
100    // $
101    f[4] = [0x18, 0x7E, 0xC0, 0x7C, 0x06, 0xFC, 0x18, 0x00];
102    // %
103    f[5] = [0x00, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xC6, 0x00];
104    // &
105    f[6] = [0x38, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0x76, 0x00];
106    // '
107    f[7] = [0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00];
108    // (
109    f[8] = [0x0C, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00];
110    // )
111    f[9] = [0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00];
112    // *
113    f[10] = [0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00];
114    // +
115    f[11] = [0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00];
116    // ,
117    f[12] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30];
118    // -
119    f[13] = [0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00];
120    // .
121    f[14] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00];
122    // /
123    f[15] = [0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00];
124    // 0
125    f[16] = [0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0x7C, 0x00];
126    // 1
127    f[17] = [0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x7E, 0x00];
128    // 2
129    f[18] = [0x7C, 0xC6, 0x06, 0x1C, 0x30, 0x66, 0xFE, 0x00];
130    // 3
131    f[19] = [0x7C, 0xC6, 0x06, 0x3C, 0x06, 0xC6, 0x7C, 0x00];
132    // 4
133    f[20] = [0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x1E, 0x00];
134    // 5
135    f[21] = [0xFE, 0xC0, 0xFC, 0x06, 0x06, 0xC6, 0x7C, 0x00];
136    // 6
137    f[22] = [0x38, 0x60, 0xC0, 0xFC, 0xC6, 0xC6, 0x7C, 0x00];
138    // 7
139    f[23] = [0xFE, 0xC6, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x00];
140    // 8
141    f[24] = [0x7C, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0x7C, 0x00];
142    // 9
143    f[25] = [0x7C, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0x78, 0x00];
144    // :
145    f[26] = [0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00];
146    // ;
147    f[27] = [0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x30];
148    // <
149    f[28] = [0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x00];
150    // =
151    f[29] = [0x00, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x00, 0x00];
152    // >
153    f[30] = [0x60, 0x30, 0x18, 0x0C, 0x18, 0x30, 0x60, 0x00];
154    // ?
155    f[31] = [0x7C, 0xC6, 0x0C, 0x18, 0x18, 0x00, 0x18, 0x00];
156    // @
157    f[32] = [0x7C, 0xC6, 0xDE, 0xDE, 0xDC, 0xC0, 0x7C, 0x00];
158    // A
159    f[33] = [0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x00];
160    // B
161    f[34] = [0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0xFC, 0x00];
162    // C
163    f[35] = [0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0x66, 0x3C, 0x00];
164    // D
165    f[36] = [0xF8, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00];
166    // E
167    f[37] = [0xFE, 0x62, 0x68, 0x78, 0x68, 0x62, 0xFE, 0x00];
168    // F
169    f[38] = [0xFE, 0x62, 0x68, 0x78, 0x68, 0x60, 0xF0, 0x00];
170    // G
171    f[39] = [0x3C, 0x66, 0xC0, 0xC0, 0xCE, 0x66, 0x3E, 0x00];
172    // H
173    f[40] = [0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0x00];
174    // I
175    f[41] = [0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
176    // J
177    f[42] = [0x1E, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00];
178    // K
179    f[43] = [0xE6, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0xE6, 0x00];
180    // L
181    f[44] = [0xF0, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00];
182    // M
183    f[45] = [0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0x00];
184    // N
185    f[46] = [0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0xC6, 0x00];
186    // O
187    f[47] = [0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
188    // P
189    f[48] = [0xFC, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00];
190    // Q
191    f[49] = [0x7C, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x06];
192    // R
193    f[50] = [0xFC, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0xE6, 0x00];
194    // S
195    f[51] = [0x7C, 0xC6, 0xC0, 0x7C, 0x06, 0xC6, 0x7C, 0x00];
196    // T
197    f[52] = [0x7E, 0x5A, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
198    // U
199    f[53] = [0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
200    // V
201    f[54] = [0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00];
202    // W
203    f[55] = [0xC6, 0xC6, 0xC6, 0xD6, 0xFE, 0xEE, 0xC6, 0x00];
204    // X
205    f[56] = [0xC6, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0xC6, 0x00];
206    // Y
207    f[57] = [0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x3C, 0x00];
208    // Z
209    f[58] = [0xFE, 0xC6, 0x8C, 0x18, 0x32, 0x66, 0xFE, 0x00];
210    // [
211    f[59] = [0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00];
212    // backslash
213    f[60] = [0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x02, 0x00];
214    // ]
215    f[61] = [0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00];
216    // ^
217    f[62] = [0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00];
218    // _
219    f[63] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF];
220    // `
221    f[64] = [0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00];
222    // a
223    f[65] = [0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0x76, 0x00];
224    // b
225    f[66] = [0xE0, 0x60, 0x7C, 0x66, 0x66, 0x66, 0xDC, 0x00];
226    // c
227    f[67] = [0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC6, 0x7C, 0x00];
228    // d
229    f[68] = [0x1C, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00];
230    // e
231    f[69] = [0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0x7C, 0x00];
232    // f
233    f[70] = [0x1C, 0x36, 0x30, 0x78, 0x30, 0x30, 0x78, 0x00];
234    // g
235    f[71] = [0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x78];
236    // h
237    f[72] = [0xE0, 0x60, 0x6C, 0x76, 0x66, 0x66, 0xE6, 0x00];
238    // i
239    f[73] = [0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3C, 0x00];
240    // j
241    f[74] = [0x06, 0x00, 0x0E, 0x06, 0x06, 0x66, 0x66, 0x3C];
242    // k
243    f[75] = [0xE0, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0xE6, 0x00];
244    // l
245    f[76] = [0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00];
246    // m
247    f[77] = [0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xC6, 0xC6, 0x00];
248    // n
249    f[78] = [0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x00];
250    // o
251    f[79] = [0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0x00];
252    // p
253    f[80] = [0x00, 0x00, 0xDC, 0x66, 0x66, 0x7C, 0x60, 0xF0];
254    // q
255    f[81] = [0x00, 0x00, 0x76, 0xCC, 0xCC, 0x7C, 0x0C, 0x1E];
256    // r
257    f[82] = [0x00, 0x00, 0xDC, 0x76, 0x60, 0x60, 0xF0, 0x00];
258    // s
259    f[83] = [0x00, 0x00, 0x7C, 0xC0, 0x7C, 0x06, 0xFC, 0x00];
260    // t
261    f[84] = [0x10, 0x30, 0x7C, 0x30, 0x30, 0x34, 0x18, 0x00];
262    // u
263    f[85] = [0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00];
264    // v
265    f[86] = [0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00];
266    // w
267    f[87] = [0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xFE, 0x6C, 0x00];
268    // x
269    f[88] = [0x00, 0x00, 0xC6, 0x6C, 0x38, 0x6C, 0xC6, 0x00];
270    // y
271    f[89] = [0x00, 0x00, 0xC6, 0xC6, 0xCE, 0x76, 0x06, 0xFC];
272    // z
273    f[90] = [0x00, 0x00, 0xFC, 0x98, 0x30, 0x64, 0xFC, 0x00];
274    // {
275    f[91] = [0x0E, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0E, 0x00];
276    // |
277    f[92] = [0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00];
278    // }
279    f[93] = [0x70, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x70, 0x00];
280    // ~
281    f[94] = [0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
282
283    f
284};
285
286/// Draw text on a `[H, W, 3]` image tensor using an embedded 8×8 bitmap font.
287///
288/// `(x, y)` is the top-left pixel of the first character. Characters outside the
289/// image bounds are silently clipped. Only ASCII 32–126 is rendered; other bytes
290/// are replaced with `?`.
291pub 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 // '?'
313        };
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
340/// A detection result for drawing purposes.
341pub 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
350/// Draw detections (bounding boxes + labels) on a `[H, W, 3]` image tensor.
351///
352/// Each detection is drawn with a color picked from a built-in palette based on
353/// `class_id`. If `labels` is provided and `class_id` is in range, the label and
354/// score are drawn above the bounding box.
355pub 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/// Draw a line using Bresenham's algorithm on a `[H, W, 3]` image tensor.
392#[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/// Draw a circle using the midpoint circle algorithm on a `[H, W, 3]` image tensor.
461///
462/// `(cx, cy)` is the center, `radius` is the radius in pixels.
463/// `thickness == 0` fills the circle.
464#[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        // Fill circle
486        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        // Midpoint circle — draw 8 symmetric points
498        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
534/// Draw connected line segments on a `[H, W, 3]` image tensor.
535///
536/// `points` is a list of `(x, y)` coordinates. If `closed` is true,
537/// the last point connects back to the first.
538pub 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/// Fill a polygon using scanline fill on a `[H, W, 3]` image tensor.
575#[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/// Draw text with integer scaling (nearest-neighbor scaled 8x8 bitmap font).
640///
641/// `scale` multiplies each pixel of the 8x8 glyph. scale=2 → 16x16 characters.
642#[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 // '?'
666        };
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                    // Fill scale x scale block
674                    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        // Center pixel (3, 3) should be red.
712        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        // Pixel (0, 0) should still be black.
718        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        // Top-left corner (5, 5) should be green.
727        let idx = (5 * 20 + 5) * 3;
728        let data = img.data();
729        assert_eq!(data[idx + 1], 1.0);
730
731        // Interior pixel (10, 10) should still be black.
732        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        // Some pixels in the first 8x16 block should be set.
742        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        // Just verify it doesn't panic and modifies some pixels.
777        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        // Diagonal pixel (10, 10) should be red
787        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        // Center should be green
796        let idx = (25 * 50 + 25) * 3;
797        assert_eq!(img.data()[idx + 1], 1.0);
798        // Far corner should be black
799        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        // Center should remain black (outline only)
807        let idx = (25 * 50 + 25) * 3;
808        assert_eq!(img.data()[idx], 0.0);
809        // A point on the circle should be drawn
810        let on_circle = (25 * 50 + 35) * 3; // (35, 25) is on the circle
811        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        // Center of triangle should be filled
833        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        // scale=2 should produce ~4x the pixels of scale=1
847        assert!(
848            white_count > 20,
849            "expected scaled text pixels, got {white_count}"
850        );
851    }
852}