Skip to main content

ads_apriltag/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: BSD-2-Clause
3
4use std::os::raw::c_char;
5
6use machine_vision_formats::pixel_format::Mono8;
7
8/// Associates array pointer destroy function to a Zarray.
9pub trait ArrayDealloc {
10    /// Call `apriltag_x_destroy()` for the correct array type.
11    ///
12    /// # Safety
13    ///
14    /// This is unsafe because it take a pointer and destroys it. The caller
15    /// must ensure it is a valid pointer.
16    unsafe fn array_dealloc(zarray_ptr: *mut apriltag_sys::zarray);
17}
18
19/// An array of a single type.
20#[repr(transparent)]
21pub struct Zarray<T: ArrayDealloc> {
22    inner: *mut apriltag_sys::zarray_t,
23    marker: std::marker::PhantomData<T>,
24}
25
26impl<T: ArrayDealloc> Zarray<T> {
27    unsafe fn from_raw(inner: *mut apriltag_sys::zarray_t) -> Zarray<T> {
28        unsafe {
29            assert!(!inner.is_null());
30            assert!((*inner).el_sz == std::mem::size_of::<T>());
31            // Note that we cannot assume the size is non-zero nor that the data
32            // pointer is not null.
33
34            Self {
35                inner,
36                marker: std::marker::PhantomData,
37            }
38        }
39    }
40
41    /// Return the length of the array.
42    pub fn len(&self) -> usize {
43        unsafe {
44            let ptr = *self.inner;
45            ptr.size.try_into().unwrap()
46        }
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52
53    /// Return a slice viewing the array.
54    pub fn as_slice(&self) -> &[T] {
55        unsafe {
56            let ptr = *self.inner;
57            if self.is_empty() {
58                &[]
59            } else {
60                std::slice::from_raw_parts(ptr.data as *const T, self.len())
61            }
62        }
63    }
64}
65
66impl<T: ArrayDealloc> Drop for Zarray<T> {
67    fn drop(&mut self) {
68        if !self.inner.is_null() {
69            // This calls `apriltag_x_destroy()` for the correct array type.
70            unsafe { T::array_dealloc(self.inner) };
71            self.inner = std::ptr::null::<apriltag_sys::zarray_t>() as *mut _;
72        }
73    }
74}
75
76pub trait ImageU8 {
77    fn inner(&self) -> &apriltag_sys::image_u8;
78
79    fn inner_mut(&mut self) -> &mut apriltag_sys::image_u8;
80
81    fn data(&self) -> &[u8];
82    fn data_mut(&mut self) -> &mut [u8];
83
84    fn width(&self) -> i32 {
85        self.inner().width
86    }
87
88    fn height(&self) -> i32 {
89        self.inner().height
90    }
91
92    fn stride(&self) -> i32 {
93        self.inner().stride
94    }
95}
96
97pub struct ImageU8Owned {
98    inner: apriltag_sys::image_u8,
99    data: Vec<u8>,
100}
101
102impl ImageU8Owned {
103    pub fn new(width: i32, height: i32, stride: i32, mut data: Vec<u8>) -> Option<Self> {
104        let min_size = (height as usize - 1) * stride as usize + width as usize;
105        if data.len() >= min_size {
106            let inner = apriltag_sys::image_u8 {
107                width,
108                height,
109                stride,
110                buf: data.as_mut_ptr(),
111            };
112
113            Some(Self { inner, data })
114        } else {
115            None
116        }
117    }
118}
119
120impl From<ImageU8Owned> for Vec<u8> {
121    fn from(val: ImageU8Owned) -> Self {
122        val.data
123    }
124}
125
126impl ImageU8 for ImageU8Owned {
127    fn inner(&self) -> &apriltag_sys::image_u8 {
128        &self.inner
129    }
130
131    fn inner_mut(&mut self) -> &mut apriltag_sys::image_u8 {
132        &mut self.inner
133    }
134
135    fn data(&self) -> &[u8] {
136        &self.data
137    }
138
139    fn data_mut(&mut self) -> &mut [u8] {
140        &mut self.data
141    }
142}
143
144pub struct ImageU8Borrowed<'a> {
145    inner: apriltag_sys::image_u8,
146    data_lifetime: std::marker::PhantomData<&'a [u8]>,
147}
148
149impl<'a> ImageU8Borrowed<'a> {
150    pub fn new(width: i32, height: i32, stride: i32, data: &'a [u8]) -> Option<Self> {
151        let min_size = (height as usize - 1) * stride as usize + width as usize;
152        if data.len() >= min_size {
153            let inner = apriltag_sys::image_u8 {
154                width,
155                height,
156                stride,
157                buf: data.as_ptr() as *mut u8,
158            };
159
160            Some(Self {
161                inner,
162                data_lifetime: std::marker::PhantomData,
163            })
164        } else {
165            None
166        }
167    }
168    pub fn view(im: &dyn machine_vision_formats::AsImageStride<Mono8>) -> Self {
169        let inner = apriltag_sys::image_u8 {
170            width: im.width().try_into().unwrap(),
171            height: im.height().try_into().unwrap(),
172            stride: im.stride().try_into().unwrap(),
173            buf: im.buffer_ref().data.as_ptr() as *mut u8,
174        };
175
176        Self {
177            inner,
178            data_lifetime: std::marker::PhantomData,
179        }
180    }
181}
182
183impl<'a> ImageU8 for ImageU8Borrowed<'a> {
184    fn inner(&self) -> &apriltag_sys::image_u8 {
185        &self.inner
186    }
187
188    fn inner_mut(&mut self) -> &mut apriltag_sys::image_u8 {
189        &mut self.inner
190    }
191
192    fn data(&self) -> &[u8] {
193        let len = self.inner.height as usize * self.inner.stride as usize;
194        unsafe { std::slice::from_raw_parts(self.inner.buf, len) }
195    }
196
197    fn data_mut(&mut self) -> &mut [u8] {
198        let len = self.inner.height as usize * self.inner.stride as usize;
199        unsafe { std::slice::from_raw_parts_mut(self.inner.buf, len) }
200    }
201}
202
203/// The main type for detecting tags
204#[derive(Debug)]
205pub struct Detector {
206    td: *mut apriltag_sys::apriltag_detector,
207    families: Vec<Family>,
208}
209
210unsafe impl Send for Detector {}
211
212impl Default for Detector {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl Detector {
219    /// Constructor
220    pub fn new() -> Self {
221        let td: *mut apriltag_sys::apriltag_detector =
222            unsafe { apriltag_sys::apriltag_detector_create() };
223        Self {
224            td,
225            families: vec![],
226        }
227    }
228
229    /// Add a tag family.
230    ///
231    /// The maximum number of bit errors to correct is chosen automatically from
232    /// the family (see [`FamilyType::recommended_correction_bits`]). Families
233    /// with a large minimum Hamming distance (e.g. tag36h11) keep the upstream
234    /// default of 2, while families with a small Hamming distance (e.g.
235    /// tag16h5, tag25h9) correct fewer bits to suppress false positives. Use
236    /// [`Detector::add_family_bits`] to choose the correction explicitly.
237    ///
238    /// We take ownership of the family to keep its lifetime.
239    pub fn add_family(&mut self, family: Family) {
240        // In theory, we could add a phantomdata type to detector with the
241        // lifetime of the families, but I'm not sure how we could do that with
242        // multiple families with potentially different lifetimes. Furthermore,
243        // that would make the type signature for Detector more complicated.
244        let bits = family.family_type().recommended_correction_bits();
245        self.add_family_bits(family, bits)
246    }
247
248    /// Add a tag family
249    ///
250    /// We take ownership of the family to keep its lifetime.
251    pub fn add_family_bits(&mut self, family: Family, bits: i32) {
252        unsafe { apriltag_sys::apriltag_detector_add_family_bits(self.td, family.0, bits) };
253        self.families.push(family)
254    }
255
256    /// Remove all tag families
257    pub fn clear_families(&mut self) {
258        unsafe { apriltag_sys::apriltag_detector_clear_families(self.td) };
259        self.families.clear()
260    }
261
262    /// Detect points in an image
263    pub fn detect(&self, im: &apriltag_sys::image_u8) -> Zarray<Detection> {
264        let detections: *mut apriltag_sys::zarray_t = unsafe {
265            let im_orig = im as *const apriltag_sys::image_u8;
266            apriltag_sys::apriltag_detector_detect(self.td, im_orig as *mut _)
267        };
268        let result: Zarray<Detection> = unsafe { Zarray::from_raw(detections) };
269
270        for det in result.as_slice() {
271            debug_assert_eq!(det.center()[0], det.h()[2]);
272            debug_assert_eq!(det.center()[1], det.h()[5]);
273        }
274        result
275    }
276}
277
278impl Drop for Detector {
279    fn drop(&mut self) {
280        if !self.td.is_null() {
281            unsafe { apriltag_sys::apriltag_detector_destroy(self.td) };
282            self.td = std::ptr::null::<apriltag_sys::apriltag_detector>() as *mut _;
283        }
284    }
285}
286
287impl std::convert::AsMut<apriltag_sys::apriltag_detector> for Detector {
288    fn as_mut(&mut self) -> &mut apriltag_sys::apriltag_detector {
289        unsafe { &mut *self.td }
290    }
291}
292
293#[repr(transparent)]
294#[derive(Debug)]
295pub struct Family(*mut apriltag_sys::apriltag_family_t);
296
297unsafe impl Send for Family {}
298
299impl Family {
300    /// Create a new detector family for 16h5 tags.
301    pub fn new_tag_16h5() -> Self {
302        let tf: *mut apriltag_sys::apriltag_family_t = unsafe { apriltag_sys::tag16h5_create() };
303        Self(tf)
304    }
305
306    /// Create a new detector family for 25h9 tags.
307    pub fn new_tag_25h9() -> Self {
308        let tf: *mut apriltag_sys::apriltag_family_t = unsafe { apriltag_sys::tag25h9_create() };
309        Self(tf)
310    }
311
312    /// Create a new detector family for 36h11 tags.
313    pub fn new_tag_36h11() -> Self {
314        let tf: *mut apriltag_sys::apriltag_family_t = unsafe { apriltag_sys::tag36h11_create() };
315        Self(tf)
316    }
317
318    /// Create a new detector family for Circle21h7 tags.
319    pub fn new_tag_circle_21h7() -> Self {
320        let tf: *mut apriltag_sys::apriltag_family_t =
321            unsafe { apriltag_sys::tagCircle21h7_create() };
322        Self(tf)
323    }
324
325    /// Create a new detector family for Circle49h12 tags.
326    pub fn new_tag_circle_49h12() -> Self {
327        let tf: *mut apriltag_sys::apriltag_family_t =
328            unsafe { apriltag_sys::tagCircle49h12_create() };
329        Self(tf)
330    }
331
332    /// Create a new detector family for Custom48h12 tags.
333    pub fn new_tag_custom_48h12() -> Self {
334        let tf: *mut apriltag_sys::apriltag_family_t =
335            unsafe { apriltag_sys::tagCustom48h12_create() };
336        Self(tf)
337    }
338
339    /// Create a new detector family for standard 41h12 tags.
340    pub fn new_tag_standard_41h12() -> Self {
341        let tf: *mut apriltag_sys::apriltag_family_t =
342            unsafe { apriltag_sys::tagStandard41h12_create() };
343        Self(tf)
344    }
345
346    /// Create a new detector family for standard 52h13 tags.
347    pub fn new_tag_standard_52h13() -> Self {
348        let tf: *mut apriltag_sys::apriltag_family_t =
349            unsafe { apriltag_sys::tagStandard52h13_create() };
350        Self(tf)
351    }
352
353    pub fn family_type(&self) -> FamilyType {
354        let name = unsafe { (*self.0).name };
355        FamilyType::from_name(name)
356    }
357}
358
359impl Drop for Family {
360    fn drop(&mut self) {
361        if !self.0.is_null() {
362            use FamilyType::*;
363            match self.family_type() {
364                Family16H5 => unsafe { apriltag_sys::tag16h5_destroy(self.0) },
365                Family25H9 => unsafe { apriltag_sys::tag25h9_destroy(self.0) },
366                Family36H11 => unsafe { apriltag_sys::tag36h11_destroy(self.0) },
367                FamilyCircle21h7 => unsafe { apriltag_sys::tagCircle21h7_destroy(self.0) },
368                FamilyCircle49H12 => unsafe { apriltag_sys::tagCircle49h12_destroy(self.0) },
369                FamilyCustom48h12 => unsafe { apriltag_sys::tagCustom48h12_destroy(self.0) },
370                Family41H12 => unsafe { apriltag_sys::tagStandard41h12_destroy(self.0) },
371                Family52H13 => unsafe { apriltag_sys::tagStandard52h13_destroy(self.0) },
372            }
373            self.0 = std::ptr::null::<apriltag_sys::apriltag_family_t>() as *mut _;
374        }
375    }
376}
377
378#[derive(Debug)]
379pub enum FamilyType {
380    Family16H5,
381    Family25H9,
382    Family36H11,
383    FamilyCircle21h7,
384    FamilyCircle49H12,
385    FamilyCustom48h12,
386    Family41H12,
387    Family52H13,
388}
389
390impl FamilyType {
391    fn from_name(name: *mut c_char) -> Self {
392        let slice = unsafe { std::ffi::CStr::from_ptr(name) };
393
394        use FamilyType::*;
395        match slice.to_bytes() {
396            b"tag16h5" => Family16H5,
397            b"tag25h9" => Family25H9,
398            b"tag36h11" => Family36H11,
399            b"tagCircle21h7" => FamilyCircle21h7,
400            b"tagCircle49h12" => FamilyCircle49H12,
401            b"tagCustom48h12" => FamilyCustom48h12,
402            b"tagStandard41h12" => Family41H12,
403            b"tagStandard52h13" => Family52H13,
404            other => match std::str::from_utf8(other) {
405                Ok(name) => panic!("unknown tag: {}", name),
406                Err(_) => panic!("unknown non-utf8 tag: {:?}", other),
407            },
408        }
409    }
410
411    pub fn to_str(&self) -> &str {
412        use FamilyType::*;
413        match self {
414            Family16H5 => "tag16h5",
415            Family25H9 => "tag25h9",
416            Family36H11 => "tag36h11",
417            FamilyCircle21h7 => "tagCircle21h7",
418            FamilyCircle49H12 => "tagCircle49h12",
419            FamilyCustom48h12 => "tagCustom48h12",
420            Family41H12 => "tagStandard41h12",
421            Family52H13 => "tagStandard52h13",
422        }
423    }
424
425    /// Recommended maximum number of erroneous bits to correct when detecting
426    /// this family.
427    ///
428    /// When matching an observed code against the known tags, the detector may
429    /// "correct" up to this many flipped bits. Allowing corrections extends the
430    /// usable detection range but also raises the false-positive rate, because
431    /// every correctable bit enlarges the set of bit patterns that decode to a
432    /// valid tag. A family tolerates corrections only in proportion to its
433    /// minimum Hamming distance — the trailing `hN` in the family name.
434    ///
435    /// Families with a large Hamming distance (tag36h11 and friends) tolerate
436    /// the upstream default of 2. The low-distance families (tag16h5, tag25h9,
437    /// tagCircle21h7) produce many false positives at that setting, so we
438    /// correct fewer bits — this is also why tag36h11 is the recommended
439    /// default family.
440    pub fn recommended_correction_bits(&self) -> i32 {
441        use FamilyType::*;
442        match self {
443            // Low Hamming distance: correcting bits yields many false
444            // positives, so require (near-)exact code matches.
445            Family16H5 => 0,
446            Family25H9 => 0,
447            FamilyCircle21h7 => 1,
448            // High Hamming distance: the upstream default of 2 is safe.
449            Family36H11 | Family41H12 | FamilyCircle49H12 | FamilyCustom48h12 | Family52H13 => 2,
450        }
451    }
452}
453
454#[repr(transparent)]
455pub struct Detection(*mut apriltag_sys::apriltag_detection_t);
456
457impl Detection {
458    pub fn id(&self) -> i32 {
459        unsafe { (*self.0).id }
460    }
461    pub fn family_type(&self) -> FamilyType {
462        let fam_ptr = unsafe { (*self.0).family };
463        let name = unsafe { (*fam_ptr).name };
464        FamilyType::from_name(name)
465    }
466    pub fn hamming(&self) -> i32 {
467        unsafe { (*self.0).hamming }
468    }
469    pub fn decision_margin(&self) -> f32 {
470        unsafe { (*self.0).decision_margin }
471    }
472    pub fn h(&self) -> &[f64] {
473        unsafe { std::slice::from_raw_parts((*(*self.0).H).data, 9) }
474    }
475    pub fn center(&self) -> &[f64; 2] {
476        unsafe { &(*self.0).c }
477    }
478}
479
480impl std::fmt::Debug for Detection {
481    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
482        f.debug_struct("Detection")
483            .field("id", &self.id())
484            .field("hamming", &self.hamming())
485            .field("decision_margin", &self.decision_margin())
486            .field("center", &self.center())
487            .field("h", &self.h())
488            .finish_non_exhaustive()
489    }
490}
491
492impl ArrayDealloc for Detection {
493    unsafe fn array_dealloc(zarray_ptr: *mut apriltag_sys::zarray) {
494        unsafe {
495            apriltag_sys::apriltag_detections_destroy(zarray_ptr);
496        }
497    }
498}
499
500#[cfg(test)]
501mod test {
502    use crate::*;
503
504    #[test]
505    fn family_types() {
506        let f = Family::new_tag_16h5();
507        let ft = f.family_type();
508        let s = ft.to_str();
509        assert_eq!(s, "tag16h5");
510
511        let f = Family::new_tag_25h9();
512        let ft = f.family_type();
513        let s = ft.to_str();
514        assert_eq!(s, "tag25h9");
515
516        let f = Family::new_tag_36h11();
517        let ft = f.family_type();
518        let s = ft.to_str();
519        assert_eq!(s, "tag36h11");
520
521        let f = Family::new_tag_circle_21h7();
522        let ft = f.family_type();
523        let s = ft.to_str();
524        assert_eq!(s, "tagCircle21h7");
525
526        let f = Family::new_tag_circle_49h12();
527        let ft = f.family_type();
528        let s = ft.to_str();
529        assert_eq!(s, "tagCircle49h12");
530
531        let f = Family::new_tag_custom_48h12();
532        let ft = f.family_type();
533        let s = ft.to_str();
534        assert_eq!(s, "tagCustom48h12");
535
536        let f = Family::new_tag_standard_41h12();
537        let ft = f.family_type();
538        let s = ft.to_str();
539        assert_eq!(s, "tagStandard41h12");
540
541        let f = Family::new_tag_standard_52h13();
542        let ft = f.family_type();
543        let s = ft.to_str();
544        assert_eq!(s, "tagStandard52h13");
545    }
546
547    #[test]
548    fn correction_bits_per_family() {
549        use FamilyType::*;
550        // The high-Hamming-distance families keep the upstream default of 2.
551        assert_eq!(Family36H11.recommended_correction_bits(), 2);
552        assert_eq!(Family41H12.recommended_correction_bits(), 2);
553        assert_eq!(FamilyCircle49H12.recommended_correction_bits(), 2);
554        assert_eq!(FamilyCustom48h12.recommended_correction_bits(), 2);
555        assert_eq!(Family52H13.recommended_correction_bits(), 2);
556        // The low-Hamming-distance families correct fewer bits to suppress
557        // false positives.
558        assert_eq!(Family16H5.recommended_correction_bits(), 0);
559        assert_eq!(Family25H9.recommended_correction_bits(), 0);
560        assert_eq!(FamilyCircle21h7.recommended_correction_bits(), 1);
561    }
562}