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