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