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}