rten_imageproc/
drawing.rs

1use crate::{BoundingRect, Line, Point, Polygon, Rect, RotatedRect, Vec2};
2
3use rten_tensor::{MatrixLayout, NdTensorViewMut};
4
5/// Return a copy of `p` with X and Y coordinates clamped to `[0, width)` and
6/// `[0, height)` respectively.
7fn clamp_to_bounds(p: Point, height: i32, width: i32) -> Point {
8    Point::from_yx(
9        p.y.clamp(0, height.saturating_sub(1).max(0)),
10        p.x.clamp(0, width.saturating_sub(1).max(0)),
11    )
12}
13
14// Draw the outline of a rectangle `rect` with border width `width`.
15//
16// The outline is drawn such that the bounding box of the outermost pixels
17// will be `rect`.
18pub fn stroke_rect<T: Copy>(mut mask: NdTensorViewMut<T, 2>, rect: Rect, value: T, width: u32) {
19    let width = width as i32;
20
21    // Left edge
22    fill_rect(
23        mask.view_mut(),
24        Rect::from_tlbr(rect.top(), rect.left(), rect.bottom(), rect.left() + width),
25        value,
26    );
27
28    // Top edge (minus ends)
29    fill_rect(
30        mask.view_mut(),
31        Rect::from_tlbr(
32            rect.top(),
33            rect.left() + width,
34            rect.top() + width,
35            rect.right() - width,
36        ),
37        value,
38    );
39
40    // Right edge
41    fill_rect(
42        mask.view_mut(),
43        Rect::from_tlbr(
44            rect.top(),
45            rect.right() - width,
46            rect.bottom(),
47            rect.right(),
48        ),
49        value,
50    );
51
52    // Bottom edge (minus ends)
53    fill_rect(
54        mask.view_mut(),
55        Rect::from_tlbr(
56            rect.bottom() - width,
57            rect.left() + width,
58            rect.bottom(),
59            rect.right() - width,
60        ),
61        value,
62    );
63}
64
65/// Fill all points inside `rect` with the value `value`.
66pub fn fill_rect<T: Copy>(mut mask: NdTensorViewMut<T, 2>, rect: Rect, value: T) {
67    for y in rect.top()..rect.bottom() {
68        for x in rect.left()..rect.right() {
69            mask[[y as usize, x as usize]] = value;
70        }
71    }
72}
73
74/// Iterator over points that lie on a line, as determined by the Bresham
75/// algorithm.
76///
77/// The implementation in [Pillow](https://pillow.readthedocs.io/en/stable/) was
78/// used as a reference.
79struct BreshamPoints {
80    /// Next point to return
81    current: Point,
82
83    /// Remaining points to return
84    remaining_steps: u32,
85
86    /// Twice total change in X along line
87    dx: i32,
88
89    /// Twice total change in Y along line
90    dy: i32,
91
92    /// Tracks error between integer points yielded by this iterator and the
93    /// "true" coordinate.
94    error: i32,
95
96    /// Increment to X coordinate of `current`.
97    x_step: i32,
98
99    /// Increment to Y coordinate of `current`.
100    y_step: i32,
101}
102
103impl BreshamPoints {
104    fn new(l: Line) -> BreshamPoints {
105        let dx = (l.end.x - l.start.x).abs();
106        let dy = (l.end.y - l.start.y).abs();
107
108        BreshamPoints {
109            current: l.start,
110            remaining_steps: dx.max(dy) as u32,
111
112            // dx and dy are doubled here as it makes stepping simpler.
113            dx: dx * 2,
114            dy: dy * 2,
115
116            error: if dx >= dy { dy * 2 - dx } else { dx * 2 - dy },
117            x_step: (l.end.x - l.start.x).signum(),
118            y_step: (l.end.y - l.start.y).signum(),
119        }
120    }
121}
122
123impl Iterator for BreshamPoints {
124    type Item = Point;
125
126    fn next(&mut self) -> Option<Point> {
127        if self.remaining_steps == 0 {
128            return None;
129        }
130
131        let current = self.current;
132        self.remaining_steps -= 1;
133
134        if self.x_step == 0 {
135            // Vertical line
136            self.current.y += self.y_step;
137        } else if self.y_step == 0 {
138            // Horizontal line
139            self.current.x += self.x_step;
140        } else if self.dx >= self.dy {
141            // X-major line (width >= height). Advances X on each step and
142            // advances Y on some steps.
143            if self.error >= 0 {
144                self.current.y += self.y_step;
145                self.error -= self.dx;
146            }
147            self.error += self.dy;
148            self.current.x += self.x_step;
149        } else {
150            // Y-major line (height > width). Advances Y on each step and
151            // advances X on some steps.
152            if self.error >= 0 {
153                self.current.x += self.x_step;
154                self.error -= self.dy
155            }
156            self.error += self.dx;
157            self.current.y += self.y_step;
158        }
159
160        Some(current)
161    }
162}
163
164/// Draw a non-antialiased line in an image.
165pub fn draw_line<T: Copy>(mut image: NdTensorViewMut<T, 2>, line: Line, value: T, width: u32) {
166    if width == 0 {
167        return;
168    }
169
170    if width == 1 {
171        // This function uses Bresham's line algorithm, with the implementation
172        // in Pillow (https://pillow.readthedocs.io/en/stable/) used as a reference.
173        let img_height: i32 = image.rows().try_into().unwrap();
174        let img_width: i32 = image.cols().try_into().unwrap();
175
176        let start = clamp_to_bounds(line.start, img_height, img_width);
177        let end = clamp_to_bounds(line.end, img_height, img_width);
178        let clamped = Line::from_endpoints(start, end);
179        for p in BreshamPoints::new(clamped) {
180            image[p.coord()] = value;
181        }
182    } else {
183        // Convert this wide line into a polygon and fill it.
184        let line = line.to_f32();
185        let line_vec = Vec2::from_xy(line.width(), line.height());
186        let rrect = RotatedRect::new(
187            line.center(),
188            line_vec.perpendicular(),
189            line_vec.length(),
190            width as f32,
191        );
192
193        let corners: [Point<i32>; 4] = rrect
194            .corners()
195            .map(|c| Point::from_yx(c.y as i32, c.x as i32));
196
197        for p in Polygon::new(corners).fill_iter() {
198            if let Some(img_val) = image.get_mut(p.coord()) {
199                *img_val = value;
200            }
201        }
202    }
203}
204
205/// Draw the outline of a non anti-aliased polygon in an image.
206pub fn draw_polygon<T: Copy>(
207    mut image: NdTensorViewMut<T, 2>,
208    poly: &[Point],
209    value: T,
210    width: u32,
211) {
212    for edge in Polygon::new(poly).edges() {
213        draw_line(image.view_mut(), edge, value, width);
214    }
215}
216
217/// Tracks data about an edge in a polygon being traversed by [`FillIter`].
218#[derive(Clone, Copy, Debug)]
219struct Edge {
220    /// Y coordinate where this edge starts
221    start_y: i32,
222
223    /// Number of scanlines remaining for this edge
224    y_steps: u32,
225
226    /// X coordinate where this edge intersects the current scanline
227    x: i32,
228
229    /// Error term indicating difference between true X coordinate for current
230    /// scanline and `x`.
231    error: i32,
232
233    /// Amount to increment `error` for every scanline.
234    error_incr: i32,
235
236    /// Amount to decrement `error` when it becomes positive.
237    error_decr: i32,
238
239    /// Amount to increment `x` for every scanline.
240    x_step: i32,
241
242    /// Amount to increment `x` when `error` becomes positive.
243    extra_x_step: i32,
244}
245
246/// Iterator over coordinates of pixels that fill a polygon. See
247/// [`Polygon::fill_iter`] for notes on how this iterator determines which
248/// pixels are inside the polygon.
249///
250/// The implementation follows <https://www.jagregory.com/abrash-black-book/#filling-arbitrary-polygons>.
251pub struct FillIter {
252    /// Edges in the polygon, sorted by Y coordinate.
253    edges: Vec<Edge>,
254
255    /// Edges in the polygon which intersect the horizontal line at `cursor.y`.
256    ///
257    /// Sorted by X coordinate.
258    active_edges: Vec<Edge>,
259
260    /// Bounding rect that contains the polygon.
261    bounds: Rect,
262
263    /// Coordinates of next pixel to return.
264    cursor: Point,
265}
266
267impl FillIter {
268    pub(crate) fn new(poly: Polygon<i32, &[Point]>) -> FillIter {
269        let mut edges: Vec<_> = poly
270            .edges()
271            // Ignore horizontal edges
272            .filter(|e| e.start.y != e.end.y)
273            .map(|e| {
274                // Normalize edge so that `delta_y` is +ve
275                let (start, end) = if e.start.y <= e.end.y {
276                    (e.start, e.end)
277                } else {
278                    (e.end, e.start)
279                };
280
281                let delta_x = end.x - start.x;
282                let delta_y = end.y - start.y;
283
284                Edge {
285                    start_y: start.y,
286                    y_steps: delta_y as u32,
287
288                    x: start.x,
289
290                    // `x_step` is the integer part of `1/slope`.
291                    x_step: delta_x / delta_y,
292
293                    // The error term tracks when `x` needs an adjustment due
294                    // to accumulation of the fractional part of `1/slope`.
295                    error: if delta_x >= 0 {
296                        0
297                    } else {
298                        // TODO - Clarify where this comes from.
299                        -delta_y + 1
300                    },
301                    error_incr: delta_x.abs() % delta_y,
302                    error_decr: delta_y,
303                    extra_x_step: delta_x.signum(),
304                }
305            })
306            .collect();
307        edges.sort_by_key(|e| -e.start_y);
308
309        let active_edges = Vec::with_capacity(edges.len());
310
311        let bounds = poly.bounding_rect();
312        let mut iter = FillIter {
313            edges,
314            active_edges,
315            bounds,
316            cursor: if bounds.is_empty() {
317                // If the polygon is empty, the cursor starts at the bottom right
318                // so that the iterator immediately yields `None`, rather than
319                // having to loop over all the empty rows.
320                bounds.bottom_right()
321            } else {
322                bounds.top_left()
323            },
324        };
325        iter.update_active_edges();
326
327        iter
328    }
329
330    /// Update the `active_edges` list after moving to a new line.
331    fn update_active_edges(&mut self) {
332        // Remove entries that end at this line and update X coord of other entries.
333        self.active_edges.retain_mut(|e| {
334            e.y_steps -= 1;
335            if e.y_steps > 0 {
336                // Advance X coordinate for current line and error term that
337                // tracks difference between `e.x` and true X coord.
338                e.x += e.x_step;
339                e.error += e.error_incr;
340                if e.error > 0 {
341                    e.error -= e.error_decr;
342                    e.x += e.extra_x_step;
343                }
344                true
345            } else {
346                false
347            }
348        });
349
350        // Add edges that begin at this line.
351        while let Some(edge) = self.edges.last().copied() {
352            if edge.start_y > self.cursor.y {
353                // `self.edges` is sorted on Y coordinate, so remaining entries
354                // start on lines with higher Y coordinate than cursor.
355                break;
356            }
357            self.edges.pop();
358            self.active_edges.push(edge);
359        }
360
361        // Sort edges by X coordinate of intersection with scanline. We only
362        // need to sort by `e.x`, but including other elements in the sort key
363        // provides more predictable ordering for debugging.
364        self.active_edges
365            .sort_by_key(|e| (e.x, e.x_step, e.extra_x_step));
366    }
367}
368
369impl Iterator for FillIter {
370    type Item = Point;
371
372    fn next(&mut self) -> Option<Point> {
373        while !self.active_edges.is_empty() {
374            let current = self.cursor;
375            let intersections = self
376                .active_edges
377                .iter()
378                .fold(0, |i, e| if e.x <= current.x { i + 1 } else { i });
379
380            self.cursor.move_by(0, 1);
381            if self.cursor.x == self.bounds.right() {
382                self.cursor.move_to(current.y + 1, self.bounds.left());
383                self.update_active_edges();
384            }
385
386            if intersections % 2 == 1 {
387                return Some(current);
388            }
389        }
390        None
391    }
392}
393
394pub type Rgb<T = u8> = [T; 3];
395
396/// Drawing style properties.
397#[derive(Copy, Clone)]
398struct PainterState<T> {
399    stroke: Rgb<T>,
400    stroke_width: u32,
401}
402
403/// Painter is a context for drawing into an image tensor.
404///
405/// Painter uses a mutable tensor with `[channel, height, width]` dimensions as
406/// the drawing surface. It has a current drawing state with properties such as
407/// stroke color and width and allows states to be saved to and restored from
408/// a stack.
409///
410/// Drawing currently operates in the first 3 channels of the surface, which are
411/// assumed to represent red, green and blue colors.
412pub struct Painter<'a, T> {
413    /// CHW image tensor.
414    surface: NdTensorViewMut<'a, T, 3>,
415    saved_states: Vec<PainterState<T>>,
416    state: PainterState<T>,
417}
418
419impl<'a, T: Copy + Default> Painter<'a, T> {
420    /// Create a Painter which draws into the CHW tensor `surface`.
421    pub fn new(surface: NdTensorViewMut<'a, T, 3>) -> Painter<'a, T> {
422        Painter {
423            surface,
424            state: PainterState {
425                stroke: [T::default(); 3],
426                stroke_width: 1,
427            },
428            saved_states: Vec::new(),
429        }
430    }
431
432    /// Save the current drawing style on a stack.
433    pub fn save(&mut self) {
434        self.saved_states.push(self.state);
435    }
436
437    /// Pop and apply a drawing style from the stack created with [`Painter::save`].
438    pub fn restore(&mut self) {
439        if let Some(state) = self.saved_states.pop() {
440            self.state = state;
441        }
442    }
443
444    /// Save the current drawing style, run `f(self)` and then restore the saved
445    /// style.
446    ///
447    /// This avoids the need to manually save and restore state with
448    /// [`Painter::save`] and [`Painter::restore`].
449    pub fn with_save<F: Fn(&mut Self)>(&mut self, f: F) {
450        self.save();
451        f(self);
452        self.restore();
453    }
454
455    /// Set the RGB color values used by the `draw_*` methods.
456    pub fn set_stroke(&mut self, stroke: Rgb<T>) {
457        self.state.stroke = stroke;
458    }
459
460    pub fn set_stroke_width(&mut self, width: u32) {
461        self.state.stroke_width = width;
462    }
463
464    /// Draw a polygon into the surface.
465    pub fn draw_polygon(&mut self, points: &[Point]) {
466        for i in 0..3 {
467            draw_polygon(
468                self.surface.slice_mut([i]),
469                points,
470                self.state.stroke[i],
471                self.state.stroke_width,
472            );
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use rten_tensor::prelude::*;
480    use rten_tensor::{MatrixLayout, NdTensor, NdTensorView};
481    use rten_testing::TestCases;
482
483    use crate::tests::print_grid;
484    use crate::{BoundingRect, Painter, Point, Polygon, Rect};
485
486    use super::{draw_polygon, stroke_rect};
487
488    /// Return coordinates of all points in `grid` with a non-zero value.
489    fn nonzero_points<T: Default + PartialEq>(grid: NdTensorView<T, 2>) -> Vec<Point> {
490        let mut points = Vec::new();
491        for y in 0..grid.rows() {
492            for x in 0..grid.cols() {
493                if grid[[y, x]] != T::default() {
494                    points.push(Point::from_yx(y as i32, x as i32))
495                }
496            }
497        }
498        points
499    }
500
501    /// Create a 2D NdTensor from an MxN nested array.
502    fn image_from_2d_array<const M: usize, const N: usize>(xs: [[i32; N]; M]) -> NdTensor<i32, 2> {
503        let mut image = NdTensor::zeros([M, N]);
504        for y in 0..M {
505            for x in 0..N {
506                image[[y, x]] = xs[y][x];
507            }
508        }
509        image
510    }
511
512    /// Compare two single-channel images with i32 pixel values.
513    fn compare_images(a: NdTensorView<i32, 2>, b: NdTensorView<i32, 2>) {
514        assert_eq!(a.rows(), b.rows());
515        assert_eq!(a.cols(), b.cols());
516
517        for y in 0..a.rows() {
518            for x in 0..a.cols() {
519                if a[[y, x]] != b[[y, x]] {
520                    print_grid(a);
521                    panic!("mismatch at coord [{}, {}]", y, x);
522                }
523            }
524        }
525    }
526
527    #[test]
528    fn test_draw_polygon() {
529        #[derive(Debug)]
530        struct Case {
531            points: &'static [[i32; 2]],
532            expected: NdTensor<i32, 2>,
533        }
534
535        let cases = [
536            // A simple rect: Straight lines in each direction
537            Case {
538                points: &[[0, 0], [0, 4], [4, 4], [4, 0]],
539                expected: image_from_2d_array([
540                    [1, 1, 1, 1, 1],
541                    [1, 0, 0, 0, 1],
542                    [1, 0, 0, 0, 1],
543                    [1, 0, 0, 0, 1],
544                    [1, 1, 1, 1, 1],
545                ]),
546            },
547            // Slopes in each direction.
548            Case {
549                points: &[[0, 2], [2, 0], [4, 2], [2, 4]],
550                expected: image_from_2d_array([
551                    [0, 0, 1, 0, 0],
552                    [0, 1, 0, 1, 0],
553                    [1, 0, 0, 0, 1],
554                    [0, 1, 0, 1, 0],
555                    [0, 0, 1, 0, 0],
556                ]),
557            },
558            // Steep slopes in each direction.
559            Case {
560                points: &[[0, 2], [2, 1], [4, 2], [2, 3]],
561                expected: image_from_2d_array([
562                    [0, 0, 1, 0, 0],
563                    [0, 1, 1, 0, 0],
564                    [0, 1, 0, 1, 0],
565                    [0, 0, 1, 1, 0],
566                    [0, 0, 1, 0, 0],
567                ]),
568            },
569        ];
570
571        cases.test_each(|case| {
572            let points: Vec<_> = case
573                .points
574                .iter()
575                .map(|[y, x]| Point::from_yx(*y, *x))
576                .collect();
577
578            let mut image = NdTensor::zeros(case.expected.shape());
579            draw_polygon(image.view_mut(), &points, 1, 1 /* width */);
580            compare_images(image.view(), case.expected.view());
581        })
582    }
583
584    #[test]
585    fn test_painter_draw_polygon() {
586        let [width, height] = [6, 6];
587        let mut img = NdTensor::zeros([3, height, width]);
588        let mut painter = Painter::new(img.view_mut());
589        let [r, g, b] = [255, 100, 50];
590        painter.set_stroke([r, g, b]);
591
592        painter.draw_polygon(&Rect::from_tlbr(2, 2, 5, 5).corners());
593
594        let expected_r = image_from_2d_array([
595            [0, 0, 0, 0, 0, 0],
596            [0, 0, 0, 0, 0, 0],
597            [0, 0, r, r, r, r],
598            [0, 0, r, 0, 0, r],
599            [0, 0, r, 0, 0, r],
600            [0, 0, r, r, r, r],
601        ]);
602        let expected_g = expected_r.map(|&x| if x == r { g } else { 0 });
603        let expected_b = expected_r.map(|&x| if x == r { b } else { 0 });
604
605        compare_images(img.slice([0]), expected_r.view());
606        compare_images(img.slice([1]), expected_g.view());
607        compare_images(img.slice([2]), expected_b.view());
608    }
609
610    #[test]
611    fn test_painter_save_restore() {
612        let [width, height] = [6, 6];
613        let mut img = NdTensor::zeros([3, height, width]);
614        let mut painter = Painter::new(img.view_mut());
615
616        let r1 = 255;
617        let r2 = 50;
618
619        // Set custom state to save.
620        painter.set_stroke([r1, 0, 0]);
621
622        painter.with_save(|painter| {
623            painter.set_stroke([r2, 0, 0]);
624            painter.draw_polygon(&Rect::from_tlbr(3, 3, 4, 4).corners());
625        });
626
627        // Draw outer rect with earlier saved state.
628        painter.draw_polygon(&Rect::from_tlbr(2, 2, 5, 5).corners());
629
630        let expected = image_from_2d_array([
631            [0, 0, 0, 0, 0, 0],
632            [0, 0, 0, 0, 0, 0],
633            [0, 0, r1, r1, r1, r1],
634            [0, 0, r1, r2, r2, r1],
635            [0, 0, r1, r2, r2, r1],
636            [0, 0, r1, r1, r1, r1],
637        ]);
638        compare_images(img.slice([0]), expected.view());
639    }
640
641    #[test]
642    fn test_stroke_rect() {
643        let mut mask = NdTensor::zeros([10, 10]);
644        let rect = Rect::from_tlbr(4, 4, 9, 9);
645
646        stroke_rect(mask.view_mut(), rect, 1, 1);
647        let points = nonzero_points(mask.view());
648
649        assert_eq!(
650            Polygon::new(&points).bounding_rect(),
651            rect.adjust_tlbr(0, 0, -1, -1)
652        );
653    }
654}