Skip to main content

rvlib/tools_data/
core.rs

1use serde::de::DeserializeOwned;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::{Debug, Display};
5use tracing::info;
6
7use crate::{ShapeI, cfg::ExportPath, util::Visibility};
8use rvimage_domain::{BbF, PtF, TPtF, TPtI};
9use rvimage_domain::{
10    Canvas, GeoFig, Point, Polygon, RvResult, rle_image_to_bb_colmajor, rle_image_to_bb_rowmajor,
11    rle_to_mask_rowmajor, rverr,
12};
13
14use super::annotations::InstanceAnnotations;
15use super::label_map::LabelMap;
16
17pub const OUTLINE_THICKNESS_CONVERSION: TPtF = 10.0;
18
19const DEFAULT_LABEL: &str = "rvimage_fg";
20
21#[allow(clippy::indexing_slicing)]
22fn color_dist(c1: [u8; 3], c2: [u8; 3]) -> f32 {
23    let square_d = |i| (f32::from(c1[i]) - f32::from(c2[i])).powi(2);
24    (square_d(0) + square_d(1) + square_d(2)).sqrt()
25}
26
27#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
28pub enum ImportMode {
29    Merge,
30    #[default]
31    Replace,
32}
33
34#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
35pub struct ImportExportTrigger {
36    export_triggered: bool,
37    import_triggered: bool,
38    import_mode: ImportMode,
39}
40impl ImportExportTrigger {
41    pub fn import_triggered(self) -> bool {
42        self.import_triggered
43    }
44    pub fn import_mode(self) -> ImportMode {
45        self.import_mode
46    }
47    pub fn export_triggered(self) -> bool {
48        self.export_triggered
49    }
50    pub fn untrigger_export(&mut self) {
51        self.export_triggered = false;
52    }
53    pub fn untrigger_import(&mut self) {
54        self.import_triggered = false;
55    }
56    pub fn trigger_export(&mut self) {
57        self.export_triggered = true;
58    }
59    pub fn trigger_import(&mut self) {
60        self.import_triggered = true;
61    }
62    pub fn use_merge_import(&mut self) {
63        self.import_mode = ImportMode::Merge;
64    }
65    pub fn use_replace_import(&mut self) {
66        self.import_mode = ImportMode::Replace;
67    }
68    pub fn merge_mode(self) -> bool {
69        self.import_mode == ImportMode::Merge
70    }
71    pub fn from_export_triggered(export_triggered: bool) -> Self {
72        Self {
73            export_triggered,
74            ..Default::default()
75        }
76    }
77}
78
79pub type AnnotationsMap<T> = LabelMap<InstanceAnnotations<T>>;
80
81fn sort<T>(annos: InstanceAnnotations<T>, access_x_or_y: fn(BbF) -> TPtF) -> InstanceAnnotations<T>
82where
83    T: InstanceAnnotate,
84{
85    let (elts, cat_idxs, selected_mask) = annos.separate_data();
86    let mut tmp_tuples = elts
87        .into_iter()
88        .zip(cat_idxs)
89        .zip(selected_mask)
90        .collect::<Vec<_>>();
91    tmp_tuples.sort_by(|((elt1, _), _), ((elt2, _), _)| {
92        match access_x_or_y(elt1.enclosing_bb()).partial_cmp(&access_x_or_y(elt2.enclosing_bb())) {
93            Some(o) => o,
94            None => {
95                tracing::error!(
96                    "there is a NAN in an annotation box {:?}, {:?}",
97                    elt1.enclosing_bb(),
98                    elt2.enclosing_bb()
99                );
100                std::cmp::Ordering::Equal
101            }
102        }
103    });
104    InstanceAnnotations::from_tuples(tmp_tuples)
105}
106
107/// Small little labels to be displayed in a box below instance annotations
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
109pub enum InstanceLabelDisplay {
110    #[default]
111    None,
112    // count from left to right
113    IndexLr,
114    // count from top to bottom
115    IndexTb,
116    // category label
117    CatLabel,
118}
119
120impl InstanceLabelDisplay {
121    pub fn next(self) -> Self {
122        match self {
123            Self::None => Self::IndexLr,
124            Self::IndexLr => Self::IndexTb,
125            Self::IndexTb => Self::CatLabel,
126            Self::CatLabel => Self::None,
127        }
128    }
129    pub fn sort<T>(self, annos: InstanceAnnotations<T>) -> InstanceAnnotations<T>
130    where
131        T: InstanceAnnotate,
132    {
133        match self {
134            Self::None | Self::CatLabel => annos,
135            Self::IndexLr => sort(annos, |bb| bb.x),
136            Self::IndexTb => sort(annos, |bb| bb.y),
137        }
138    }
139}
140impl Display for InstanceLabelDisplay {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            Self::None => write!(f, "None"),
144            Self::IndexLr => write!(f, "Index-Left-Right"),
145            Self::IndexTb => write!(f, "Index-Top-Bottom"),
146            Self::CatLabel => write!(f, "Category-Label"),
147        }
148    }
149}
150
151#[allow(clippy::struct_excessive_bools)]
152#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct Options {
154    pub visible: bool,
155    pub is_colorchange_triggered: bool,
156    pub is_redraw_annos_triggered: bool,
157    pub is_export_absolute: bool,
158    pub import_export_trigger: ImportExportTrigger,
159    pub is_history_update_triggered: bool,
160    pub track_changes: bool,
161    pub erase: bool,
162    pub label_propagation: Option<usize>,
163    pub label_deletion: Option<usize>,
164    pub auto_paste: bool,
165    pub instance_label_display: InstanceLabelDisplay,
166    pub doublecheck_cocoexport_shape: bool,
167}
168impl Default for Options {
169    fn default() -> Self {
170        Self {
171            visible: true,
172            is_colorchange_triggered: false,
173            is_redraw_annos_triggered: false,
174            is_export_absolute: false,
175            import_export_trigger: ImportExportTrigger::default(),
176            is_history_update_triggered: false,
177            track_changes: false,
178            erase: false,
179            label_propagation: None,
180            label_deletion: None,
181            auto_paste: false,
182            instance_label_display: InstanceLabelDisplay::None,
183            doublecheck_cocoexport_shape: true,
184        }
185    }
186}
187impl Options {
188    pub fn trigger_redraw_and_hist(mut self) -> Self {
189        self.is_history_update_triggered = true;
190        self.is_redraw_annos_triggered = true;
191        self
192    }
193}
194
195const N: usize = 1;
196#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
197pub struct VisibleInactiveToolsState {
198    // should the tool's annotations be shown in the background
199    show_mask: [bool; N],
200}
201impl VisibleInactiveToolsState {
202    pub fn new() -> Self {
203        Self::default()
204    }
205    #[allow(clippy::needless_lifetimes)]
206    pub fn iter<'a>(&'a self) -> impl Iterator<Item = bool> + 'a {
207        self.show_mask.iter().copied()
208    }
209    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut bool> {
210        self.show_mask.iter_mut()
211    }
212    pub fn hide_all(&mut self) {
213        self.show_mask.fill(false);
214    }
215    pub fn set_show(&mut self, idx: usize, is_visible: bool) {
216        if let Some(show_mask) = self.show_mask.get_mut(idx) {
217            *show_mask = is_visible;
218        }
219    }
220}
221
222pub fn random_clr() -> [u8; 3] {
223    let r = rand::random::<u8>();
224    let g = rand::random::<u8>();
225    let b = rand::random::<u8>();
226    [r, g, b]
227}
228
229#[allow(clippy::indexing_slicing)]
230fn argmax_clr_dist(picklist: &[[u8; 3]], legacylist: &[[u8; 3]]) -> [u8; 3] {
231    let (idx, _) = picklist
232        .iter()
233        .enumerate()
234        .map(|(i, pickclr)| {
235            let min_dist = legacylist
236                .iter()
237                .map(|legclr| color_dist(*legclr, *pickclr))
238                .min_by(|a, b| a.partial_cmp(b).unwrap())
239                .unwrap_or(0.0);
240            (i, min_dist)
241        })
242        .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
243        .unwrap();
244    picklist[idx]
245}
246
247pub fn new_color(colors: &[[u8; 3]]) -> [u8; 3] {
248    let mut new_clr_proposals = [[0u8, 0u8, 0u8]; 10];
249    for new_clr in &mut new_clr_proposals {
250        *new_clr = random_clr();
251    }
252    argmax_clr_dist(&new_clr_proposals, colors)
253}
254
255pub fn new_random_colors(n: usize) -> Vec<[u8; 3]> {
256    let mut colors = vec![random_clr()];
257    for _ in 0..(n - 1) {
258        let color = new_color(&colors);
259        colors.push(color);
260    }
261    colors
262}
263
264fn get_visibility(visible: bool, show_only_current: bool, cat_idx_current: usize) -> Visibility {
265    if visible && show_only_current {
266        Visibility::Only(cat_idx_current)
267    } else if visible {
268        Visibility::All
269    } else {
270        Visibility::None
271    }
272}
273
274pub fn vis_from_lfoption(label_info: Option<&LabelInfo>, visible: bool) -> Visibility {
275    if let Some(label_info) = label_info {
276        label_info.visibility(visible)
277    } else if visible {
278        Visibility::All
279    } else {
280        Visibility::None
281    }
282}
283
284pub fn merge<T>(
285    annos1: AnnotationsMap<T>,
286    li1: LabelInfo,
287    annos2: AnnotationsMap<T>,
288    li2: LabelInfo,
289) -> (AnnotationsMap<T>, LabelInfo)
290where
291    T: InstanceAnnotate,
292{
293    let (li, idx_map) = li1.merge(li2);
294    let mut annotations_map = annos1;
295
296    for (k, (v2, s)) in annos2 {
297        if let Some((v1, _)) = annotations_map.get_mut(&k) {
298            let (elts, cat_idxs, _) = v2.separate_data();
299            v1.extend(
300                elts.into_iter(),
301                cat_idxs
302                    .into_iter()
303                    .flat_map(|old_idx| idx_map.get(old_idx).copied()),
304                s,
305                InstanceLabelDisplay::default(),
306            );
307            v1.deselect_all();
308        } else {
309            let (elts, cat_idxs, _) = v2.separate_data();
310            let cat_idxs = cat_idxs
311                .into_iter()
312                .flat_map(|old_idx| idx_map.get(old_idx).copied())
313                .collect::<Vec<_>>();
314            let v2 =
315                InstanceAnnotations::new_relaxed(elts, cat_idxs, InstanceLabelDisplay::default());
316            annotations_map.insert(k, (v2, s));
317        }
318    }
319    (annotations_map, li)
320}
321
322#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
323pub struct LabelInfo {
324    pub new_label: String,
325    labels: Vec<String>,
326    colors: Vec<[u8; 3]>,
327    cat_ids: Vec<u32>,
328    pub cat_idx_current: usize,
329    pub show_only_current: bool,
330}
331impl LabelInfo {
332    /// Merges two `LabelInfo`s. Returns the merged `LabelInfo` and a vector that maps
333    /// the indices of the second `LabelInfo` to the indices of the merged `LabelInfo`.
334    pub fn merge(mut self, other: Self) -> (Self, Vec<usize>) {
335        let mut idx_map = vec![];
336        for other_label in other.labels {
337            let self_cat_idx = self.labels.iter().position(|slab| slab == &other_label);
338            if let Some(scidx) = self_cat_idx {
339                idx_map.push(scidx);
340            } else {
341                self.labels.push(other_label);
342                self.colors.push(new_color(&self.colors));
343                self.cat_ids.push(self.labels.len() as u32);
344                idx_map.push(self.labels.len() - 1);
345            }
346        }
347        (self, idx_map)
348    }
349
350    pub fn visibility(&self, visible: bool) -> Visibility {
351        get_visibility(visible, self.show_only_current, self.cat_idx_current)
352    }
353    pub fn new_random_colors(&mut self) {
354        info!("new random colors for annotations");
355        self.colors = new_random_colors(self.colors.len());
356    }
357    pub fn push(
358        &mut self,
359        label: String,
360        color: Option<[u8; 3]>,
361        cat_id: Option<u32>,
362    ) -> RvResult<()> {
363        if self.labels.contains(&label) {
364            Err(rverr!("label '{}' already exists", label))
365        } else {
366            info!("adding label '{label}'");
367            self.labels.push(label);
368            if let Some(clr) = color {
369                if self.colors.contains(&clr) {
370                    return Err(rverr!("color '{:?}' already exists", clr));
371                }
372                self.colors.push(clr);
373            } else {
374                let new_clr = new_color(&self.colors);
375                self.colors.push(new_clr);
376            }
377            if let Some(cat_id) = cat_id {
378                if self.cat_ids.contains(&cat_id) {
379                    return Err(rverr!("cat id '{:?}' already exists", cat_id));
380                }
381                self.cat_ids.push(cat_id);
382            } else if let Some(max_id) = self.cat_ids.iter().max() {
383                self.cat_ids.push(max_id + 1);
384            } else {
385                self.cat_ids.push(1);
386            }
387            Ok(())
388        }
389    }
390    pub fn rename_label(&mut self, idx: usize, label: String) -> RvResult<()> {
391        if self.labels.contains(&label) {
392            Err(rverr!("label '{label}' already exists"))
393        } else {
394            if let Some(self_label) = self.labels.get_mut(idx) {
395                *self_label = label;
396            }
397            Ok(())
398        }
399    }
400    pub fn from_iter(it: impl Iterator<Item = ((String, [u8; 3]), u32)>) -> RvResult<Self> {
401        let mut info = Self::empty();
402        for ((label, color), cat_id) in it {
403            info.push(label, Some(color), Some(cat_id))?;
404        }
405        Ok(info)
406    }
407    pub fn is_empty(&self) -> bool {
408        self.labels.is_empty()
409    }
410    pub fn len(&self) -> usize {
411        self.labels.len()
412    }
413    pub fn remove(&mut self, idx: usize) -> (String, [u8; 3], u32) {
414        let removed_items = (
415            self.labels.remove(idx),
416            self.colors.remove(idx),
417            self.cat_ids.remove(idx),
418        );
419        info!("label '{}' removed", removed_items.0);
420        removed_items
421    }
422    pub fn find_default(&mut self) -> Option<&mut String> {
423        self.labels.iter_mut().find(|lab| lab == &DEFAULT_LABEL)
424    }
425    pub fn colors(&self) -> &Vec<[u8; 3]> {
426        &self.colors
427    }
428
429    pub fn labels(&self) -> &Vec<String> {
430        &self.labels
431    }
432
433    pub fn cat_ids(&self) -> &Vec<u32> {
434        &self.cat_ids
435    }
436
437    pub fn separate_data(self) -> (Vec<String>, Vec<[u8; 3]>, Vec<u32>) {
438        (self.labels, self.colors, self.cat_ids)
439    }
440
441    pub fn empty() -> Self {
442        Self {
443            new_label: DEFAULT_LABEL.to_string(),
444            labels: vec![],
445            colors: vec![],
446            cat_ids: vec![],
447            cat_idx_current: 0,
448            show_only_current: false,
449        }
450    }
451    pub fn remove_catidx<'a, T>(&mut self, cat_idx: usize, annotaions_map: &mut AnnotationsMap<T>)
452    where
453        T: InstanceAnnotate + PartialEq + Default + 'a,
454    {
455        if self.len() > 1 {
456            self.remove(cat_idx);
457            if self.cat_idx_current >= cat_idx.max(1) {
458                self.cat_idx_current -= 1;
459            }
460            for (anno, _) in annotaions_map.values_mut() {
461                let indices_for_rm = anno
462                    .cat_idxs()
463                    .iter()
464                    .enumerate()
465                    .filter(|(_, geo_cat_idx)| **geo_cat_idx == cat_idx)
466                    .map(|(idx, _)| idx)
467                    .collect::<Vec<_>>();
468                anno.remove_multiple(&indices_for_rm);
469                anno.reduce_cat_idxs(cat_idx);
470            }
471        }
472    }
473}
474
475impl Default for LabelInfo {
476    fn default() -> Self {
477        let new_label = DEFAULT_LABEL.to_string();
478        let new_color = [255, 255, 255];
479        let labels = vec![new_label.clone()];
480        let colors = vec![new_color];
481        let cat_ids = vec![1];
482        Self {
483            new_label,
484            labels,
485            colors,
486            cat_ids,
487            cat_idx_current: 0,
488            show_only_current: false,
489        }
490    }
491}
492
493#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
494pub struct InstanceExportData<A> {
495    pub labels: Vec<String>,
496    pub colors: Vec<[u8; 3]>,
497    pub cat_ids: Vec<u32>,
498    // filename, bounding boxes, classes of the boxes, dimensions of the image
499    pub annotations: HashMap<String, (Vec<A>, Vec<usize>, ShapeI)>,
500    pub coco_file: ExportPath,
501    pub is_export_absolute: bool,
502}
503
504impl<A> InstanceExportData<A>
505where
506    A: InstanceAnnotate,
507{
508    pub fn from_tools_data(
509        options: &Options,
510        label_info: LabelInfo,
511        coco_file: ExportPath,
512        annotations_map: AnnotationsMap<A>,
513    ) -> Self {
514        let is_export_absolute = options.is_export_absolute;
515        let annotations = annotations_map
516            .into_iter()
517            .map(|(filename, (annos, shape))| {
518                let (bbs, labels, _) = annos.separate_data();
519                (filename, (bbs, labels, shape))
520            })
521            .collect::<HashMap<_, _>>();
522        let (labels, colors, cat_ids) = label_info.separate_data();
523        InstanceExportData {
524            labels,
525            colors,
526            cat_ids,
527            annotations,
528            coco_file,
529            is_export_absolute,
530        }
531    }
532    pub fn label_info(&self) -> RvResult<LabelInfo> {
533        LabelInfo::from_iter(
534            self.labels
535                .clone()
536                .into_iter()
537                .zip(self.colors.clone())
538                .zip(self.cat_ids.clone()),
539        )
540    }
541}
542
543#[derive(Serialize, Deserialize, Debug, PartialEq)]
544pub struct CocoRle {
545    pub counts: Vec<TPtI>,
546    pub size: (TPtI, TPtI),
547    pub intensity: Option<TPtF>,
548}
549
550impl CocoRle {
551    /// Convert the Coco RLE to a [`Canvas`].
552    ///
553    /// `legacy_rowmajor` selects the RLE order: Coco/RV Image now uses
554    /// column-major counts with `size = [height, width]`, while Coco files
555    /// exported by older RV Image versions used row-major counts with
556    /// `size = [width, height]`.
557    pub fn to_canvas(&self, bb: BbF, legacy_rowmajor: bool) -> RvResult<Canvas> {
558        let bb = bb.into();
559        let rle_bb = if legacy_rowmajor {
560            // legacy RV Image: size = [width, height], row-major counts
561            let shape_im = ShapeI::new(self.size.0, self.size.1);
562            rle_image_to_bb_rowmajor(&self.counts, bb, shape_im)?
563        } else {
564            // Coco convention: size = [height, width], column-major counts
565            let shape_im = ShapeI::new(self.size.1, self.size.0);
566            rle_image_to_bb_colmajor(&self.counts, bb, shape_im)?
567        };
568        let mask = rle_to_mask_rowmajor(&rle_bb, bb.w, bb.h);
569        let intensity = self.intensity.unwrap_or(1.0);
570        Ok(Canvas {
571            bb,
572            mask,
573            intensity,
574        })
575    }
576}
577
578#[derive(Debug, Serialize, Deserialize, PartialEq)]
579#[serde(untagged)]
580pub enum CocoSegmentation {
581    Polygon(Vec<Vec<TPtF>>),
582    Rle(CocoRle),
583}
584
585#[allow(clippy::indexing_slicing)]
586pub fn polygon_to_geofig(
587    poly: &[Vec<TPtF>],
588    w_factor: f64,
589    h_factor: f64,
590    bb: BbF,
591    mut warn: impl FnMut(&str),
592) -> RvResult<GeoFig> {
593    if poly.len() != 1 {
594        return Err(rverr!(
595            "multiple polygons per box not supported. ignoring all but first."
596        ));
597    }
598    let n_points = poly[0].len();
599    let coco_data = &poly[0];
600
601    let poly_points = (0..n_points)
602        .step_by(2)
603        .filter_map(|idx| {
604            let p = Point {
605                x: (coco_data[idx] * w_factor),
606                y: (coco_data[idx + 1] * h_factor),
607            };
608            if bb.contains(p) { Some(p) } else { None }
609        })
610        .collect();
611    let poly = Polygon::from_vec(poly_points);
612    if let Ok(poly) = poly {
613        let encl_bb = poly.enclosing_bb();
614        if encl_bb.w * encl_bb.h < 1e-6 && bb.w * bb.h > 1e-6 {
615            warn(&format!(
616                "polygon has no area. using bb. bb: {bb:?}, poly: {encl_bb:?}"
617            ));
618            Ok(GeoFig::BB(bb))
619        } else {
620            if !bb.all_corners_close(encl_bb) {
621                let msg = format!(
622                    "bounding box and polygon enclosing box do not match. using bb. bb: {bb:?}, poly: {encl_bb:?}"
623                );
624                warn(&msg);
625            }
626            // check if the poly is just a bounding box
627            if poly.points().len() == 4
628                                // all points are bb corners
629                                && poly.points_iter().all(|p| {
630                                    encl_bb.points_iter().any(|p_encl| p == p_encl)})
631                                // all points are different
632                                && poly
633                                    .points_iter()
634                                    .all(|p| poly.points_iter().filter(|p_| p == *p_).count() == 1)
635            {
636                Ok(GeoFig::BB(bb))
637            } else {
638                Ok(GeoFig::Poly(poly))
639            }
640        }
641    } else if n_points > 0 {
642        Err(rverr!(
643            "Segmentation invalid, could not be created from polygon with {n_points} points"
644        ))
645    } else {
646        // polygon might be empty, we continue with the BB
647        Ok(GeoFig::BB(bb))
648    }
649}
650
651#[macro_export]
652macro_rules! implement_annotate {
653    ($tooldata:ident) => {
654        impl $crate::tools_data::core::Annotate for $tooldata {
655            fn has_annos(&self, relative_path: &str) -> bool {
656                if let Some(v) = self.get_annos(relative_path) {
657                    !v.is_empty()
658                } else {
659                    false
660                }
661            }
662        }
663    };
664}
665
666pub trait Annotate {
667    /// Has the image with the given path annotations of the
668    /// trait-implementing tool?
669    fn has_annos(&self, relative_path: &str) -> bool;
670}
671
672pub trait InstanceAnnotate:
673    Clone + Default + Debug + PartialEq + Serialize + DeserializeOwned
674{
675    fn is_contained_in_image(&self, shape: ShapeI) -> bool;
676    fn contains<P>(&self, point: P) -> bool
677    where
678        P: Into<PtF>;
679    fn dist_to_boundary(&self, p: PtF) -> TPtF;
680    /// # Errors
681    /// Can fail if a bounding box ends up with negative coordinates after rotation
682    fn rot90_with_image_ntimes(self, shape: ShapeI, n: u8) -> RvResult<Self>;
683    fn enclosing_bb(&self) -> BbF;
684    /// # Errors
685    /// Can fail if a bounding box is not on the image.
686    fn to_cocoseg(
687        &self,
688        shape_im: ShapeI,
689        is_export_absolute: bool,
690    ) -> RvResult<Option<CocoSegmentation>>;
691}
692pub trait AccessInstanceData<T: InstanceAnnotate> {
693    fn annotations_map(&self) -> &AnnotationsMap<T>;
694    fn label_info(&self) -> &LabelInfo;
695}
696pub trait ExportAsCoco<A>: AccessInstanceData<A>
697where
698    A: InstanceAnnotate + 'static,
699{
700    fn cocofile_conn(&self) -> ExportPath;
701    fn separate_data(self) -> (Options, LabelInfo, AnnotationsMap<A>, ExportPath);
702    #[cfg(test)]
703    fn anno_iter(&self) -> impl Iterator<Item = (&String, &(InstanceAnnotations<A>, ShapeI))>;
704    fn set_annotations_map(&mut self, map: AnnotationsMap<A>) -> RvResult<()>;
705    fn set_labelinfo(&mut self, info: LabelInfo);
706    fn core_options_mut(&mut self) -> &mut Options;
707    fn new(
708        options: Options,
709        label_info: LabelInfo,
710        anno_map: AnnotationsMap<A>,
711        export_path: ExportPath,
712    ) -> Self;
713}
714
715#[cfg(test)]
716use crate::tools_data::brush_data;
717#[cfg(test)]
718use rvimage_domain::{BrushLine, Line};
719#[test]
720fn test_argmax() {
721    let picklist = [
722        [200, 200, 200u8],
723        [1, 7, 3],
724        [0, 0, 1],
725        [45, 43, 52],
726        [1, 10, 15],
727    ];
728    let legacylist = [
729        [17, 16, 15],
730        [199, 199, 201u8],
731        [50, 50, 50u8],
732        [255, 255, 255u8],
733    ];
734    assert_eq!(argmax_clr_dist(&picklist, &legacylist), [0, 0, 1]);
735}
736
737#[test]
738fn test_labelinfo_merge() {
739    let li1 = LabelInfo::default();
740    let mut li2 = LabelInfo::default();
741    li2.new_random_colors();
742    let (mut li_merged, _) = li1.clone().merge(li2);
743    assert_eq!(li1, li_merged);
744    li_merged
745        .push("new_label".into(), Some([0, 0, 1]), None)
746        .unwrap();
747    let (li_merged, _) = li_merged.merge(li1);
748    let li_reference = LabelInfo {
749        new_label: DEFAULT_LABEL.to_string(),
750        labels: vec![DEFAULT_LABEL.to_string(), "new_label".to_string()],
751        colors: vec![[255, 255, 255], [0, 0, 1]],
752        cat_ids: vec![1, 2],
753        cat_idx_current: 0,
754        show_only_current: false,
755    };
756    assert_eq!(li_merged, li_reference);
757    assert_eq!(li_merged.clone().merge(li_merged.clone()).0, li_reference);
758    let li = LabelInfo {
759        new_label: DEFAULT_LABEL.to_string(),
760        labels: vec!["somelabel".to_string(), "new_label".to_string()],
761        colors: vec![[255, 255, 255], [0, 1, 1]],
762        cat_ids: vec![1, 2],
763        cat_idx_current: 0,
764        show_only_current: false,
765    };
766    let li_merged_ = li_merged.clone().merge(li.clone());
767    let li_reference = (
768        LabelInfo {
769            new_label: DEFAULT_LABEL.to_string(),
770            labels: vec![
771                DEFAULT_LABEL.to_string(),
772                "new_label".to_string(),
773                "somelabel".to_string(),
774            ],
775            colors: vec![[255, 255, 255], [0, 0, 1], li_merged_.0.colors[2]],
776            cat_ids: vec![1, 2, 3],
777            cat_idx_current: 0,
778            show_only_current: false,
779        },
780        vec![2, 1],
781    );
782    assert_ne!([255, 255, 255], li_merged_.0.colors[2]);
783    assert_eq!(li_merged_, li_reference);
784    let li_merged = li.merge(li_merged);
785    let li_reference = LabelInfo {
786        new_label: DEFAULT_LABEL.to_string(),
787        labels: vec![
788            "somelabel".to_string(),
789            "new_label".to_string(),
790            DEFAULT_LABEL.to_string(),
791        ],
792        colors: vec![[255, 255, 255], [0, 1, 1], li_merged.0.colors[2]],
793        cat_ids: vec![1, 2, 3],
794        cat_idx_current: 0,
795        show_only_current: false,
796    };
797    assert_eq!(li_merged.0, li_reference);
798}
799
800#[test]
801fn test_merge_annos() {
802    let orig_shape = ShapeI::new(100, 100);
803    let li1 = LabelInfo {
804        new_label: "x".to_string(),
805        labels: vec!["somelabel".to_string(), "x".to_string()],
806        colors: vec![[255, 255, 255], [0, 1, 1]],
807        cat_ids: vec![1, 2],
808        cat_idx_current: 0,
809        show_only_current: false,
810    };
811    let li2 = LabelInfo {
812        new_label: "x".to_string(),
813        labels: vec![
814            "somelabel".to_string(),
815            "new_label".to_string(),
816            "x".to_string(),
817        ],
818        colors: vec![[255, 255, 255], [0, 1, 2], [1, 1, 1]],
819        cat_ids: vec![1, 2, 3],
820        cat_idx_current: 0,
821        show_only_current: false,
822    };
823    let mut annos_map1: super::brush_data::BrushAnnoMap = AnnotationsMap::new();
824
825    let mut line = Line::new();
826    line.push(PtF { x: 5.0, y: 5.0 });
827    let anno1 = Canvas::new(
828        &BrushLine {
829            line: line.clone(),
830            thickness: 1.0,
831            intensity: 1.0,
832        },
833        orig_shape,
834        None,
835    )
836    .unwrap();
837    annos_map1.insert(
838        "file1".to_string(),
839        (
840            InstanceAnnotations::new(vec![anno1.clone()], vec![1], vec![true]).unwrap(),
841            orig_shape,
842        ),
843    );
844    let mut annos_map2: brush_data::BrushAnnoMap = AnnotationsMap::new();
845    let anno2 = Canvas::new(
846        &BrushLine {
847            line,
848            thickness: 2.0,
849            intensity: 2.0,
850        },
851        orig_shape,
852        None,
853    )
854    .unwrap();
855
856    annos_map2.insert(
857        "file1".to_string(),
858        (
859            InstanceAnnotations::new(vec![anno2.clone()], vec![1], vec![true]).unwrap(),
860            orig_shape,
861        ),
862    );
863    annos_map2.insert(
864        "file2".to_string(),
865        (
866            InstanceAnnotations::new(vec![anno2.clone()], vec![1], vec![true]).unwrap(),
867            orig_shape,
868        ),
869    );
870    let (merged_map, merged_li) = merge(annos_map1, li1, annos_map2, li2.clone());
871    let merged_li_ref = LabelInfo {
872        new_label: "x".to_string(),
873        labels: vec![
874            "somelabel".to_string(),
875            "x".to_string(),
876            "new_label".to_string(),
877        ],
878        colors: vec![[255, 255, 255], [0, 1, 1], merged_li.colors[2]],
879        cat_ids: vec![1, 2, 3],
880        cat_idx_current: 0,
881        show_only_current: false,
882    };
883
884    assert_eq!(merged_li, merged_li_ref);
885    let map_ref = [
886        (
887            "file1".to_string(),
888            (
889                InstanceAnnotations::new(
890                    vec![anno1, anno2.clone()],
891                    vec![1, 2],
892                    vec![false, false],
893                )
894                .unwrap(),
895                orig_shape,
896            ),
897        ),
898        (
899            "file2".to_string(),
900            (
901                InstanceAnnotations::new(vec![anno2], vec![2], vec![false]).unwrap(),
902                orig_shape,
903            ),
904        ),
905    ]
906    .into_iter()
907    .collect::<AnnotationsMap<Canvas>>();
908    for (k, (v, s)) in merged_map.iter() {
909        assert_eq!(map_ref[k].0, *v);
910        assert_eq!(map_ref[k].1, *s);
911    }
912}