Skip to main content

iris/drawing/
mod.rs

1pub mod font;
2
3use crate::core::types::{Point, Rect, Scalar};
4use crate::error::Result;
5use crate::image::Image;
6use burn::tensor::{Tensor, TensorData, backend::Backend};
7use font::FONT_5X7;
8
9impl<B: Backend> Image<B> {
10    /// Draws a line from p1 to p2 on the image.
11    /// This operates on CPU by reading tensor, rasterizing, and uploading.
12    pub fn draw_line(self, p1: Point<usize>, p2: Point<usize>, color: Scalar) -> Result<Self> {
13        let dims = self.tensor.dims();
14        let c = dims[0];
15        let h = dims[1];
16        let w = dims[2];
17
18        let device = self.tensor.device();
19        let tensor_data = self.tensor.into_data();
20        let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
21
22        let mut x0 = p1.x as isize;
23        let mut y0 = p1.y as isize;
24        let x1 = p2.x as isize;
25        let y1 = p2.y as isize;
26
27        let dx = (x1 - x0).abs();
28        let dy = -(y1 - y0).abs();
29        let sx = if x0 < x1 { 1 } else { -1 };
30        let sy = if y0 < y1 { 1 } else { -1 };
31        let mut err = dx + dy;
32
33        loop {
34            if x0 >= 0 && x0 < w as isize && y0 >= 0 && y0 < h as isize {
35                for ch in 0..c {
36                    let val = color.0[ch] as f32;
37                    flat_vals[ch * h * w + (y0 as usize) * w + (x0 as usize)] = val;
38                }
39            }
40            if x0 == x1 && y0 == y1 {
41                break;
42            }
43            let e2 = 2 * err;
44            if e2 >= dy {
45                err += dy;
46                x0 += sx;
47            }
48            if e2 <= dx {
49                err += dx;
50                y0 += sy;
51            }
52        }
53
54        let new_data = TensorData::new(flat_vals, [c, h, w]);
55        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
56        Ok(Image::new(new_tensor))
57    }
58
59    /// Draws a rectangle on the image. If thickness < 0, the rectangle is filled.
60    pub fn draw_rectangle(self, rect: Rect<usize>, color: Scalar, thickness: i32) -> Result<Self> {
61        if thickness >= 0 {
62            // Draw four borders
63            let p1 = Point::new(rect.x, rect.y);
64            let p2 = Point::new(rect.x + rect.width, rect.y);
65            let p3 = Point::new(rect.x + rect.width, rect.y + rect.height);
66            let p4 = Point::new(rect.x, rect.y + rect.height);
67
68            self.draw_line(p1, p2, color)?
69                .draw_line(p2, p3, color)?
70                .draw_line(p3, p4, color)?
71                .draw_line(p4, p1, color)
72        } else {
73            // Filled rectangle
74            let dims = self.tensor.dims();
75            let c = dims[0];
76            let h = dims[1];
77            let w = dims[2];
78
79            let device = self.tensor.device();
80            let tensor_data = self.tensor.into_data();
81            let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
82
83            let x_start = rect.x;
84            let y_start = rect.y;
85            let x_end = (rect.x + rect.width).min(w);
86            let y_end = (rect.y + rect.height).min(h);
87
88            for y in y_start..y_end {
89                for x in x_start..x_end {
90                    for ch in 0..c {
91                        flat_vals[ch * h * w + y * w + x] = color.0[ch] as f32;
92                    }
93                }
94            }
95
96            let new_data = TensorData::new(flat_vals, [c, h, w]);
97            let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
98            Ok(Image::new(new_tensor))
99        }
100    }
101
102    /// Draws a circle on the image. If thickness < 0, the circle is filled.
103    pub fn draw_circle(
104        self,
105        center: Point<usize>,
106        radius: usize,
107        color: Scalar,
108        thickness: i32,
109    ) -> Result<Self> {
110        let dims = self.tensor.dims();
111        let c = dims[0];
112        let h = dims[1];
113        let w = dims[2];
114
115        let device = self.tensor.device();
116        let tensor_data = self.tensor.into_data();
117        let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
118
119        let xc = center.x as isize;
120        let yc = center.y as isize;
121        let r = radius as isize;
122
123        let draw_pixel = |px: isize, py: isize, vals: &mut [f32]| {
124            if px >= 0 && px < w as isize && py >= 0 && py < h as isize {
125                for ch in 0..c {
126                    vals[ch * h * w + (py as usize) * w + (px as usize)] = color.0[ch] as f32;
127                }
128            }
129        };
130
131        if thickness >= 0 {
132            // Midpoint Circle Algorithm
133            let mut x = 0isize;
134            let mut y = r;
135            let mut d = 3 - 2 * r;
136
137            let draw_sym = |x_s: isize, y_s: isize, vals: &mut [f32]| {
138                draw_pixel(xc + x_s, yc + y_s, vals);
139                draw_pixel(xc - x_s, yc + y_s, vals);
140                draw_pixel(xc + x_s, yc - y_s, vals);
141                draw_pixel(xc - x_s, yc - y_s, vals);
142                draw_pixel(xc + y_s, yc + x_s, vals);
143                draw_pixel(xc - y_s, yc + x_s, vals);
144                draw_pixel(xc + y_s, yc - x_s, vals);
145                draw_pixel(xc - y_s, yc - x_s, vals);
146            };
147
148            draw_sym(x, y, &mut flat_vals);
149            while y >= x {
150                x += 1;
151                if d > 0 {
152                    y -= 1;
153                    d = d + 4 * (x - y) + 10;
154                } else {
155                    d = d + 4 * x + 6;
156                }
157                draw_sym(x, y, &mut flat_vals);
158            }
159        } else {
160            // Filled Circle
161            for y in 0..h {
162                for x in 0..w {
163                    let dx = x as isize - xc;
164                    let dy = y as isize - yc;
165                    if dx * dx + dy * dy <= r * r {
166                        for ch in 0..c {
167                            flat_vals[ch * h * w + y * w + x] = color.0[ch] as f32;
168                        }
169                    }
170                }
171            }
172        }
173
174        let new_data = TensorData::new(flat_vals, [c, h, w]);
175        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
176        Ok(Image::new(new_tensor))
177    }
178
179    /// Draws a text label on the image using the built-in 5x7 font.
180    pub fn draw_text(
181        self,
182        text: &str,
183        org: Point<usize>,
184        scale: usize,
185        color: Scalar,
186    ) -> Result<Self> {
187        let dims = self.tensor.dims();
188        let c = dims[0];
189        let h = dims[1];
190        let w = dims[2];
191
192        let device = self.tensor.device();
193        let tensor_data = self.tensor.into_data();
194        let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
195
196        let scale = scale.max(1);
197        let mut cursor_x = org.x;
198
199        for byte in text.bytes() {
200            let char_idx = (byte as usize).min(127);
201            let bitmap = FONT_5X7[char_idx];
202
203            for (col, &col_data) in bitmap.iter().enumerate() {
204                for row in 0..7 {
205                    if (col_data & (1 << row)) != 0 {
206                        // Draw a pixel block based on scale
207                        let px_start = cursor_x + col * scale;
208                        let py_start = org.y + row * scale;
209
210                        for sy in 0..scale {
211                            for sx in 0..scale {
212                                let x = px_start + sx;
213                                let y = py_start + sy;
214                                if x < w && y < h {
215                                    for ch in 0..c {
216                                        flat_vals[ch * h * w + y * w + x] = color.0[ch] as f32;
217                                    }
218                                }
219                            }
220                        }
221                    }
222                }
223            }
224            cursor_x += 6 * scale; // 5 columns + 1 space column
225        }
226
227        let new_data = TensorData::new(flat_vals, [c, h, w]);
228        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
229        Ok(Image::new(new_tensor))
230    }
231
232    /// Draws an ellipse on the image.
233    /// `center` is the center of the ellipse, `axes` is (semi_major, semi_minor).
234    /// `angle` is the rotation angle in degrees. `start_angle` and `end_angle` in degrees.
235    /// If `thickness < 0`, the ellipse is filled.
236    #[allow(clippy::too_many_arguments)]
237    pub fn draw_ellipse(
238        self,
239        center: Point<usize>,
240        axes: (usize, usize),
241        angle: f32,
242        start_angle: f32,
243        end_angle: f32,
244        color: Scalar,
245        thickness: i32,
246    ) -> Result<Self> {
247        let dims = self.tensor.dims();
248        let c = dims[0];
249        let h = dims[1];
250        let w = dims[2];
251
252        let device = self.tensor.device();
253        let tensor_data = self.tensor.into_data();
254        let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
255
256        let cx = center.x as f32;
257        let cy = center.y as f32;
258        let (a, b) = (axes.0 as f32, axes.1 as f32);
259        let angle_rad = angle * std::f32::consts::PI / 180.0;
260        let start_rad = start_angle * std::f32::consts::PI / 180.0;
261        let end_rad = end_angle * std::f32::consts::PI / 180.0;
262
263        let cos_a = angle_rad.cos();
264        let sin_a = angle_rad.sin();
265
266        let draw_px = |px: isize, py: isize, vals: &mut [f32]| {
267            if px >= 0 && px < w as isize && py >= 0 && py < h as isize {
268                for ch in 0..c {
269                    vals[ch * h * w + py as usize * w + px as usize] = color.0[ch] as f32;
270                }
271            }
272        };
273
274        if thickness >= 0 {
275            // Draw ellipse outline
276            for t_idx in 0..720 {
277                let t = start_rad + (end_rad - start_rad) * t_idx as f32 / 720.0;
278                let ex = a * t.cos();
279                let ey = b * t.sin();
280                let rx = ex * cos_a - ey * sin_a;
281                let ry = ex * sin_a + ey * cos_a;
282                draw_px((cx + rx) as isize, (cy + ry) as isize, &mut flat_vals);
283            }
284        } else {
285            // Filled ellipse using scanline
286            let max_r = a.max(b) as usize;
287            let x0 = (cx as isize - max_r as isize).max(0) as usize;
288            let x1 = (cx as usize + max_r).min(w);
289            let y0 = (cy as isize - max_r as isize).max(0) as usize;
290            let y1 = (cy as usize + max_r).min(h);
291
292            for py in y0..y1 {
293                for px in x0..x1 {
294                    let dx = px as f32 - cx;
295                    let dy = py as f32 - cy;
296                    // Transform point into ellipse coordinate system
297                    let tx = dx * cos_a + dy * sin_a;
298                    let ty = -dx * sin_a + dy * cos_a;
299                    if a > 0.0 && b > 0.0 && (tx / a).powi(2) + (ty / b).powi(2) <= 1.0 {
300                        for ch in 0..c {
301                            flat_vals[ch * h * w + py * w + px] = color.0[ch] as f32;
302                        }
303                    }
304                }
305            }
306        }
307
308        let new_data = TensorData::new(flat_vals, [c, h, w]);
309        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
310        Ok(Image::new(new_tensor))
311    }
312
313    /// Draws a polyline (connected line segments) on the image.
314    pub fn draw_polyline(
315        self,
316        points: &[Point<usize>],
317        color: Scalar,
318        _thickness: i32,
319    ) -> Result<Self> {
320        if points.len() < 2 {
321            return Ok(self);
322        }
323        let mut current = self;
324        for i in 0..points.len() - 1 {
325            current = current.draw_line(points[i], points[i + 1], color)?;
326        }
327        Ok(current)
328    }
329
330    /// Draws a filled polygon on the image using scanline fill.
331    pub fn fill_poly(self, points: &[Point<usize>], color: Scalar) -> Result<Self> {
332        let dims = self.tensor.dims();
333        let c = dims[0];
334        let h = dims[1];
335        let w = dims[2];
336
337        let device = self.tensor.device();
338        let tensor_data = self.tensor.into_data();
339        let mut flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
340
341        if points.len() < 3 {
342            return Ok(Image::new(Tensor::<B, 3>::from_data(
343                TensorData::new(flat_vals, [c, h, w]),
344                &device,
345            )));
346        }
347
348        // Find bounding box
349        let min_y = points.iter().map(|p| p.y).min().unwrap_or(0);
350        let max_y = points.iter().map(|p| p.y).max().unwrap_or(h - 1);
351        let max_y = max_y.min(h - 1);
352
353        // Scanline fill
354        for y in min_y..=max_y {
355            let mut intersections = Vec::new();
356            let n = points.len();
357            for i in 0..n {
358                let j = (i + 1) % n;
359                let (p1, p2) = (points[i], points[j]);
360                if (p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y) {
361                    let x_intersect = p1.x as f64
362                        + (y as f64 - p1.y as f64) * (p2.x as f64 - p1.x as f64)
363                            / (p2.y as f64 - p1.y as f64);
364                    intersections.push(x_intersect as usize);
365                }
366            }
367            intersections.sort_unstable();
368
369            for pair in intersections.chunks(2) {
370                if pair.len() == 2 {
371                    let x_start = pair[0];
372                    let x_end = pair[1].min(w - 1);
373                    for x in x_start..=x_end {
374                        for ch in 0..c {
375                            flat_vals[ch * h * w + y * w + x] = color.0[ch] as f32;
376                        }
377                    }
378                }
379            }
380        }
381
382        let new_data = TensorData::new(flat_vals, [c, h, w]);
383        let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
384        Ok(Image::new(new_tensor))
385    }
386
387    /// Draws an arrowed line from p1 to p2 with an arrowhead.
388    pub fn draw_arrowed_line(
389        self,
390        p1: Point<usize>,
391        p2: Point<usize>,
392        color: Scalar,
393        _thickness: i32,
394        tip_length: f32,
395    ) -> Result<Self> {
396        let img = self.draw_line(p1, p2, color)?;
397
398        // Compute arrowhead
399        let dx = p2.x as f64 - p1.x as f64;
400        let dy = p2.y as f64 - p1.y as f64;
401        let len = (dx * dx + dy * dy).sqrt();
402        if len < 1.0 {
403            return Ok(img);
404        }
405
406        let ux = dx / len;
407        let uy = dy / len;
408        let tip_size = len * tip_length as f64;
409        let angle = std::f64::consts::FRAC_PI_6; // 30 degrees
410
411        let left = (
412            (p2.x as f64 - tip_size * (ux * angle.cos() - uy * angle.sin())) as usize,
413            (p2.y as f64 - tip_size * (uy * angle.cos() + ux * angle.sin())) as usize,
414        );
415        let right = (
416            (p2.x as f64 - tip_size * (ux * angle.cos() + uy * angle.sin())) as usize,
417            (p2.y as f64 - tip_size * (uy * angle.cos() - ux * angle.sin())) as usize,
418        );
419
420        img.draw_line(p2, Point::new(left.0, left.1), color)?
421            .draw_line(p2, Point::new(right.0, right.1), color)
422    }
423
424    /// Draws a marker symbol at a point.
425    pub fn draw_marker(
426        self,
427        center: Point<usize>,
428        color: Scalar,
429        marker_type: MarkerType,
430        marker_size: usize,
431    ) -> Result<Self> {
432        match marker_type {
433            MarkerType::Cross => {
434                let half = marker_size / 2;
435                self.draw_line(
436                    Point::new(center.x.saturating_sub(half), center.y),
437                    Point::new(center.x + half, center.y),
438                    color,
439                )?
440                .draw_line(
441                    Point::new(center.x, center.y.saturating_sub(half)),
442                    Point::new(center.x, center.y + half),
443                    color,
444                )
445            }
446            MarkerType::TiltedCross => {
447                let half = marker_size / 2;
448                self.draw_line(
449                    Point::new(center.x.saturating_sub(half), center.y.saturating_sub(half)),
450                    Point::new(center.x + half, center.y + half),
451                    color,
452                )?
453                .draw_line(
454                    Point::new(center.x + half, center.y.saturating_sub(half)),
455                    Point::new(center.x.saturating_sub(half), center.y + half),
456                    color,
457                )
458            }
459            MarkerType::Diamond => {
460                let half = marker_size / 2;
461                self.draw_polyline(
462                    &[
463                        Point::new(center.x, center.y.saturating_sub(half)),
464                        Point::new(center.x + half, center.y),
465                        Point::new(center.x, center.y + half),
466                        Point::new(center.x.saturating_sub(half), center.y),
467                        Point::new(center.x, center.y.saturating_sub(half)),
468                    ],
469                    color,
470                    1,
471                )
472            }
473            MarkerType::Square => self.draw_rectangle(
474                Rect::new(
475                    center.x.saturating_sub(marker_size / 2),
476                    center.y.saturating_sub(marker_size / 2),
477                    marker_size,
478                    marker_size,
479                ),
480                color,
481                1,
482            ),
483            MarkerType::Circle => self.draw_circle(center, marker_size / 2, color, 1),
484            MarkerType::Filled => self.draw_circle(center, marker_size / 2, color, -1),
485        }
486    }
487}
488
489/// Types of drawing markers.
490#[derive(Clone, Copy, Debug, PartialEq)]
491pub enum MarkerType {
492    Cross,
493    TiltedCross,
494    Diamond,
495    Square,
496    Circle,
497    Filled,
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use crate::test_helpers::{TestBackend, test_device};
504
505    #[test]
506    fn test_drawing_operations() {
507        let device = test_device();
508        let flat_data = vec![0.0f32; 3 * 100 * 100];
509        let tensor =
510            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 100, 100]), &device);
511        let img = Image::new(tensor);
512
513        let img = img
514            .draw_line(Point::new(10, 10), Point::new(90, 90), Scalar::all(1.0))
515            .unwrap();
516        let img = img
517            .draw_rectangle(Rect::new(20, 20, 30, 40), Scalar::all(0.5), 1)
518            .unwrap();
519        let img = img
520            .draw_circle(Point::new(50, 50), 20, Scalar::all(0.8), -1)
521            .unwrap();
522        let img = img
523            .draw_text("Hello", Point::new(10, 80), 2, Scalar::all(0.9))
524            .unwrap();
525
526        assert_eq!(img.shape(), [3, 100, 100]);
527    }
528
529    #[test]
530    fn test_draw_ellipse() {
531        let device = test_device();
532        let data = vec![0.0f32; 3 * 60 * 60];
533        let tensor =
534            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [3, 60, 60]), &device);
535        let img = Image::new(tensor);
536
537        // Outline
538        let img = img
539            .draw_ellipse(
540                Point::new(30, 30),
541                (15, 10),
542                30.0,
543                0.0,
544                360.0,
545                Scalar::all(1.0),
546                1,
547            )
548            .unwrap();
549        assert_eq!(img.shape(), [3, 60, 60]);
550
551        // Filled
552        let img = img
553            .draw_ellipse(
554                Point::new(30, 30),
555                (15, 10),
556                30.0,
557                0.0,
558                360.0,
559                Scalar::all(0.5),
560                -1,
561            )
562            .unwrap();
563        assert_eq!(img.shape(), [3, 60, 60]);
564    }
565
566    #[test]
567    fn test_draw_polyline() {
568        let device = test_device();
569        let data = vec![0.0f32; 3 * 50 * 50];
570        let tensor =
571            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [3, 50, 50]), &device);
572        let img = Image::new(tensor);
573
574        let points = vec![
575            Point::new(10, 10),
576            Point::new(40, 10),
577            Point::new(40, 40),
578            Point::new(10, 40),
579            Point::new(10, 10),
580        ];
581        let img = img.draw_polyline(&points, Scalar::all(1.0), 1).unwrap();
582        assert_eq!(img.shape(), [3, 50, 50]);
583    }
584
585    #[test]
586    fn test_fill_poly() {
587        let device = test_device();
588        let data = vec![0.0f32; 3 * 50 * 50];
589        let tensor =
590            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [3, 50, 50]), &device);
591        let img = Image::new(tensor);
592
593        let points = vec![
594            Point::new(10, 10),
595            Point::new(40, 10),
596            Point::new(40, 40),
597            Point::new(10, 40),
598        ];
599        let img = img.fill_poly(&points, Scalar::all(0.8)).unwrap();
600        assert_eq!(img.shape(), [3, 50, 50]);
601    }
602
603    #[test]
604    fn test_draw_arrowed_line() {
605        let device = test_device();
606        let data = vec![0.0f32; 3 * 50 * 50];
607        let tensor =
608            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [3, 50, 50]), &device);
609        let img = Image::new(tensor);
610
611        let img = img
612            .draw_arrowed_line(
613                Point::new(10, 10),
614                Point::new(40, 40),
615                Scalar::all(1.0),
616                1,
617                0.3,
618            )
619            .unwrap();
620        assert_eq!(img.shape(), [3, 50, 50]);
621    }
622
623    #[test]
624    fn test_draw_marker() {
625        let device = test_device();
626        let data = vec![0.0f32; 3 * 50 * 50];
627        let tensor =
628            Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [3, 50, 50]), &device);
629        let img = Image::new(tensor);
630
631        let img = img
632            .draw_marker(Point::new(25, 25), Scalar::all(1.0), MarkerType::Cross, 10)
633            .unwrap();
634        let img = img
635            .draw_marker(Point::new(25, 25), Scalar::all(0.5), MarkerType::Circle, 10)
636            .unwrap();
637        let img = img
638            .draw_marker(Point::new(25, 25), Scalar::all(0.8), MarkerType::Filled, 10)
639            .unwrap();
640        assert_eq!(img.shape(), [3, 50, 50]);
641    }
642}