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