chalkydri_apriltags/
lib.rs

1#![feature(
2    portable_simd,
3    alloc_layout_extra,
4    slice_as_chunks,
5    sync_unsafe_cell,
6    array_chunks
7)]
8#![warn(clippy::infinite_loop)]
9
10#[cfg(feature = "multi-thread")]
11extern crate rayon;
12
13//mod decode;
14// mod pose_estimation;
15pub mod utils;
16
17use cam_geom::IntrinsicParametersPerspective;
18// use pose_estimation::pose_estimation;
19use ril::{Line, Rgb};
20// TODO: ideally we'd use alloc here and only pull in libstd for sync::atomic when the multi-thread feature is enabled
21use std::{
22    alloc::{alloc_zeroed, dealloc, Layout},
23    sync::atomic::{AtomicUsize, Ordering},
24};
25
26#[cfg(feature = "multi-thread")]
27use rayon::iter::{ParallelBridge, ParallelIterator};
28
29use crate::utils::*;
30
31/// Raw buffers used by a [`detector`](Detector)
32///
33/// We need a separate struct for this so the compiler will treat them as thread-safe.
34/// Interacting with raw buffers is typically lower overhead, but unsafe.
35struct DetectorBufs {
36    /// The thresholded image buffer
37    buf: *mut Color,
38    /// Detected corners
39    points: *mut (usize, usize),
40}
41unsafe impl Send for DetectorBufs {}
42unsafe impl Sync for DetectorBufs {}
43
44/// An AprilTag detector
45///
46/// This is the main entrypoint.
47pub struct Detector {
48    /// Raw buffers used by the detector
49    bufs: DetectorBufs,
50    valid_tags: &'static [usize],
51    points_len: AtomicUsize,
52    /// Checked edges (x1, y1, x2, y2)
53    lines: Vec<(usize, usize, usize, usize)>,
54    /// Width of input frames
55    width: usize,
56    /// Height of input frames
57    height: usize,
58}
59impl Detector {
60    /// Initialize a new detector for the specified dimensions
61    ///
62    /// `valid_tags` is required for optimization and error resistance.
63    pub fn new(
64        width: usize,
65        height: usize,
66        valid_tags: &'static [usize],
67        //intrinsics: IntrinsicParametersPerspective<f32>,
68    ) -> Self {
69        unsafe {
70            // Allocate raw buffers
71            let buf: *mut Color =
72                alloc_zeroed(Layout::array::<Color>(width * height).unwrap()).cast();
73            let points: *mut (usize, usize) =
74                alloc_zeroed(Layout::array::<(usize, usize)>(width * height).unwrap()).cast();
75            let points_len = AtomicUsize::new(0);
76
77            Self {
78                bufs: DetectorBufs { buf, points },
79                valid_tags,
80                points_len,
81                lines: Vec::new(),
82                width,
83                height,
84            }
85        }
86    }
87
88    /// Calculate otsu value
89    ///
90    /// [Otsu's method](https://en.wikipedia.org/wiki/Otsu%27s_method) is an adaptive thresholding
91    /// algorithm. In English: it turns a grayscale image into binary (foreground/background,
92    /// black/white).
93    ///
94    /// We should investigate combining the variations for unbalanced images and triclass
95    /// thresholding.
96    pub fn calc_otsu(&mut self, input: &[u8]) {
97        let mut i = 0usize;
98        // Histogram
99        let mut hist = [0usize; 256];
100
101        // Calculate histogram
102        for i in 0..self.width * self.height {
103            unsafe {
104                // Red, green, and blue are each represent with 1 byte
105                let gray = grayscale(input.get_unchecked((i * 3)..(i * 3) + 3));
106
107                let pix = hist.get_unchecked_mut(*input.get_unchecked(i) as usize);
108                *pix = (*pix).unchecked_add(1);
109            }
110        }
111
112        let mut sum = 0u32;
113        let mut sum_b = 0u32;
114        let mut var_max = 0f64;
115        let mut thresh = 0u8;
116
117        //for t in 0..256 {
118        //    sum += t as u32 * hist[t];
119
120        //    let w_b =
121
122        //println!("{:?} {i}", st.elapsed());
123    }
124
125    /// Process an RGB frame
126    ///
127    /// FAST needs a 3x3 circle around each pixel, so we only process pixels within a 3x3 pixel
128    /// padding.
129    pub fn process_frame(&mut self, input: &[u8]) {
130        // Check that the input is RGB
131        assert_eq!(input.len(), self.width * self.height * 3);
132
133        unsafe {
134            self.thresh(input);
135        }
136        // Reset points_len to 0
137        self.points_len.store(0, Ordering::SeqCst);
138        // Clear the lines Vec
139        self.lines.clear();
140
141        self.detect_corners();
142
143        self.check_edges();
144
145        //pose_estimation(intrinsics);
146    }
147
148    /// Run corner detection
149    #[inline(always)]
150    pub fn detect_corners(&mut self) {
151        #[cfg(not(feature = "multi-thread"))]
152        for x in 3..=self.width - 3 {
153            for y in 3..=self.height - 3 {
154                unsafe {
155                    self.process_pixel(x, y);
156                }
157            }
158        }
159
160        #[cfg(feature = "multi-thread")]
161        (3..=self.width - 3).par_bridge().for_each(|x| {
162            for y in 3..=self.height - 3 {
163                unsafe {
164                    self.process_pixel(x, y);
165                }
166            }
167        });
168    }
169
170    /// Threshold an input RGB buffer
171    ///
172    /// TODO: This needs to use [Self::calc_otsu].
173    ///
174    /// # Safety
175    /// `input` is treated as an RGB buffer, even if it isn't.
176    /// The caller should check that `input` is an RGB buffer.
177    #[inline(always)]
178    pub unsafe fn thresh(&self, input: &[u8]) {
179        // This is mainly memory-bound, so multi-threading probably isn't worth it.
180        for i in 0..self.width * self.height {
181            // Red, green, and blue are each represent with 1 byte
182            let gray = grayscale(input.get_unchecked((i * 3)..(i * 3) + 3));
183
184            // 60 is a "kinda works" value because I haven't implemented the algorithm
185            if gray < 60 {
186                *self.bufs.buf.add(i) = Color::Black;
187            } else if gray > 160 {
188                *self.bufs.buf.add(i) = Color::White;
189            } else {
190                *self.bufs.buf.add(i) = Color::Other;
191            }
192        }
193    }
194
195    /// Process a pixel
196    ///
197    /// This should have as little overhead as possible, as it must be run hundreds of thousands of
198    /// times for each frame.
199    ///
200    /// # Safety
201    /// (`x`, `y`) is assumed to be a valid pixel coord.
202    /// The caller must make sure of this.
203    #[inline(always)]
204    unsafe fn process_pixel(&self, x: usize, y: usize) {
205        // Pull out frame width and frame buffer for cleaner looking code
206        // TODO: is this optimized down into a noop?
207        let width = self.width;
208        let buf = self.bufs.buf;
209
210        // Get binary value of pixel at (x,y)
211        let p = *buf.add(px(x, y, width));
212
213        if p.is_black() {
214            // Get pixels that are diagonal neighbors of p
215            let (up_left, up_right, down_left, down_right) = (
216                *buf.add(px(x - 1, y - 1, width)),
217                *buf.add(px(x + 1, y - 1, width)),
218                *buf.add(px(x - 1, y + 1, width)),
219                *buf.add(px(x + 1, y + 1, width)),
220            );
221
222            // Only one can be black
223            // The carrot is Rust's exclusive or (XOR) operation
224            let clean = up_left.is_black()
225                ^ up_right.is_black()
226                ^ down_left.is_black()
227                ^ down_right.is_black();
228
229            if clean {
230                // Furthest top right
231                let p3 = *buf.add(px(x + 3, y - 3, width));
232                // Furthest bottom right
233                let p7 = *buf.add(px(x + 3, y + 3, width));
234                // Furthest bottom left
235                let p11 = *buf.add(px(x - 3, y + 3, width));
236                // Furthest top left
237                let p15 = *buf.add(px(x - 3, y - 3, width));
238
239                if (p3.is_good() && p7.is_good() && p11.is_good() && p15.is_good())
240                    && (p3.is_black() ^ p7.is_black() ^ p11.is_black() ^ p15.is_black())
241                {
242                    // Furthest top center
243                    let p1 = *buf.add(px(x, y - 3, width));
244                    // Furthest middle right
245                    let p5 = *buf.add(px(x + 3, y, width));
246                    // Furthest bottom center
247                    let p9 = *buf.add(px(x, y + 3, width));
248                    // Furthest middle left
249                    let p13 = *buf.add(px(x - 3, y, width));
250
251                    // Add p to the corner buffer
252                    *self
253                        .bufs
254                        .points
255                        .add(self.points_len.fetch_add(1, Ordering::SeqCst)) = (x, y);
256                }
257            }
258        }
259    }
260
261    /// Check a single edge (imaginary line between two corners)
262    ///
263    /// See [Self::check_edges].
264    ///
265    /// # Safety
266    /// (`x1`, `y1`) and (`x2`, `y2`) are assumed to be a valid pixel coords.
267    /// The caller must make sure of this.
268    unsafe fn check_edge(&mut self, x1: usize, y1: usize, x2: usize, y2: usize) {
269        // idk how to describe this one
270        const CHECK_OFFSET: usize = 5;
271        let width = self.width;
272        let buf = self.bufs.buf;
273
274        // calculate & store midpoint
275        let midpoint_x = (x1 + x2) / 2;
276        let midpoint_y = (y1 + y2) / 2;
277
278        // Figure out if edge is closer to horizontal/vertical
279        let (xdiff, ydiff) = (x1.max(x2) - x1.min(x2), y1.max(y2) - y1.min(y2));
280        let is_vertical_line = x1 == x2 || xdiff < ydiff;
281        let is_horizontal_line = y1 == y2 || ydiff < xdiff;
282
283        // Calculate and store the coords for the midway points
284        let (mw1x, mw1y) = ((midpoint_x + x1) / 2, (midpoint_y + y1) / 2);
285        let (mw2x, mw2y) = ((midpoint_x + x2) / 2, (midpoint_y + y2) / 2);
286
287        if is_vertical_line {
288            // edge is closer to a vertical line instead of a diagonal
289            let mw1right = *buf.add(px(mw1x + CHECK_OFFSET, mw1y, width));
290            let mw2right = *buf.add(px(mw2x + CHECK_OFFSET, mw2y, width));
291
292            let mw1left = *buf.add(px(mw1x - CHECK_OFFSET, mw1y, width));
293            let mw2left = *buf.add(px(mw2x - CHECK_OFFSET, mw2y, width));
294
295            // Check that all of the checking points are valid
296            if mw1left.is_good() && mw2left.is_good() && mw1right.is_good() && mw2right.is_good() {
297                // Check that only one side of the edge is black (the other should be white)
298                if (mw1left.is_black() ^ mw2right.is_black())
299                    && (mw2left.is_black() ^ mw1right.is_black())
300                    && (mw1left == mw2left)
301                {
302                    // midway one has black pixels on both sides
303                    self.lines.push((x1, y1, x2, y2));
304                }
305            }
306        }
307
308        if is_horizontal_line {
309            // edge is closer to a horizontal line instead of a diagonal
310
311            // XXX: Checking midway 1 then midway 2 *might* have marginally better performance,
312            // but likely not worth it for the more complex code.
313
314            // create the point to the right of the two midways
315            let mw1top = *buf.add(px(mw1x, mw1y - CHECK_OFFSET, width));
316            let mw2top = *buf.add(px(mw2x, mw2y - CHECK_OFFSET, width));
317
318            // create the point ot the left of the two midways
319            let mw1bottom = *buf.add(px(mw1x, mw1y + CHECK_OFFSET, width));
320            let mw2bottom = *buf.add(px(mw2x, mw2y + CHECK_OFFSET, width));
321
322            // check if the midways are black pixels,
323            // and if the pixels to the right and left of these midways are black pixels as well.
324
325            if mw1top.is_good() && mw2top.is_good() && mw1bottom.is_good() && mw2bottom.is_good() {
326                if (mw1top.is_black() ^ mw2bottom.is_black())
327                    && (mw2top.is_black() ^ mw1bottom.is_black())
328                    && (mw1top == mw2top)
329                {
330                    // midway one has black pixels on both sides
331                    self.lines.push((x1, y1, x2, y2));
332                }
333            }
334        }
335    }
336
337    /// Perform edge checking on all detected corners
338    #[inline(always)]
339    pub fn check_edges(&mut self) {
340        // Turn the raw buffer into a Rust slice
341        let points = unsafe {
342            core::slice::from_raw_parts(
343                self.bufs.points as *const _,
344                self.points_len.load(Ordering::SeqCst),
345            )
346        };
347
348        // Iterate over every detected corner
349        // TODO: this might benefit from multi-threading
350        for &(x1, y1) in points.iter() {
351            // Iterate over every detected corner in reverse, checking for edges
352            for &(x2, y2) in points.iter().rev() {
353                unsafe {
354                    self.check_edge(x1, y1, x2, y2);
355                }
356            }
357        }
358    }
359
360    pub fn draw(&self) {
361        let mut img = ril::Image::new(self.width as u32, self.height as u32, Rgb::black());
362        for (x1, y1, x2, y2) in self.lines.clone() {
363            img.draw(
364                &ril::draw::Ellipse::circle(x1 as u32, y1 as u32, 2)
365                    .with_fill(Rgb::from_hex("ffa500").unwrap()),
366            );
367            img.draw(
368                &ril::draw::Ellipse::circle(x2 as u32, y2 as u32, 2)
369                    .with_fill(Rgb::from_hex("ff0000").unwrap()),
370            );
371            img.draw(&Line::new(
372                (x1 as u32, y1 as u32),
373                (x2 as u32, y2 as u32),
374                Rgb::white(),
375            ));
376        }
377        img.save(ril::ImageFormat::Png, "lines.png").unwrap();
378    }
379}
380impl Clone for Detector {
381    fn clone(&self) -> Self {
382        Self::new(self.width, self.height, &[])
383    }
384}
385impl Drop for Detector {
386    fn drop(&mut self) {
387        unsafe {
388            dealloc(
389                self.bufs.buf as *mut _,
390                Layout::array::<bool>(self.width * self.height).unwrap(),
391            );
392            dealloc(
393                self.bufs.points as *mut _,
394                Layout::array::<(usize, usize)>(self.width * self.height).unwrap(),
395            );
396        }
397    }
398}