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 ExportAsCoco<A>
588where
589    A: InstanceAnnotate + 'static,
590{
591    fn separate_data(self) -> (Options, LabelInfo, AnnotationsMap<A>, ExportPath);
592    fn cocofile_conn(&self) -> ExportPath;
593    fn label_info(&self) -> &LabelInfo;
594    #[cfg(test)]
595    fn anno_iter(&self) -> impl Iterator<Item = (&String, &(InstanceAnnotations<A>, ShapeI))>;
596    fn set_annotations_map(&mut self, map: AnnotationsMap<A>) -> RvResult<()>;
597    fn set_labelinfo(&mut self, info: LabelInfo);
598    fn core_options_mut(&mut self) -> &mut Options;
599    fn new(
600        options: Options,
601        label_info: LabelInfo,
602        anno_map: AnnotationsMap<A>,
603        export_path: ExportPath,
604    ) -> Self;
605}
606
607#[cfg(test)]
608use crate::tools_data::brush_data;
609#[cfg(test)]
610use rvimage_domain::{BrushLine, Canvas, Line};
611#[test]
612fn test_argmax() {
613    let picklist = [
614        [200, 200, 200u8],
615        [1, 7, 3],
616        [0, 0, 1],
617        [45, 43, 52],
618        [1, 10, 15],
619    ];
620    let legacylist = [
621        [17, 16, 15],
622        [199, 199, 201u8],
623        [50, 50, 50u8],
624        [255, 255, 255u8],
625    ];
626    assert_eq!(argmax_clr_dist(&picklist, &legacylist), [0, 0, 1]);
627}
628
629#[test]
630fn test_labelinfo_merge() {
631    let li1 = LabelInfo::default();
632    let mut li2 = LabelInfo::default();
633    li2.new_random_colors();
634    let (mut li_merged, _) = li1.clone().merge(li2);
635    assert_eq!(li1, li_merged);
636    li_merged
637        .push("new_label".into(), Some([0, 0, 1]), None)
638        .unwrap();
639    let (li_merged, _) = li_merged.merge(li1);
640    let li_reference = LabelInfo {
641        new_label: DEFAULT_LABEL.to_string(),
642        labels: vec![DEFAULT_LABEL.to_string(), "new_label".to_string()],
643        colors: vec![[255, 255, 255], [0, 0, 1]],
644        cat_ids: vec![1, 2],
645        cat_idx_current: 0,
646        show_only_current: false,
647    };
648    assert_eq!(li_merged, li_reference);
649    assert_eq!(li_merged.clone().merge(li_merged.clone()).0, li_reference);
650    let li = LabelInfo {
651        new_label: DEFAULT_LABEL.to_string(),
652        labels: vec!["somelabel".to_string(), "new_label".to_string()],
653        colors: vec![[255, 255, 255], [0, 1, 1]],
654        cat_ids: vec![1, 2],
655        cat_idx_current: 0,
656        show_only_current: false,
657    };
658    let li_merged_ = li_merged.clone().merge(li.clone());
659    let li_reference = (
660        LabelInfo {
661            new_label: DEFAULT_LABEL.to_string(),
662            labels: vec![
663                DEFAULT_LABEL.to_string(),
664                "new_label".to_string(),
665                "somelabel".to_string(),
666            ],
667            colors: vec![[255, 255, 255], [0, 0, 1], li_merged_.0.colors[2]],
668            cat_ids: vec![1, 2, 3],
669            cat_idx_current: 0,
670            show_only_current: false,
671        },
672        vec![2, 1],
673    );
674    assert_ne!([255, 255, 255], li_merged_.0.colors[2]);
675    assert_eq!(li_merged_, li_reference);
676    let li_merged = li.merge(li_merged);
677    let li_reference = LabelInfo {
678        new_label: DEFAULT_LABEL.to_string(),
679        labels: vec![
680            "somelabel".to_string(),
681            "new_label".to_string(),
682            DEFAULT_LABEL.to_string(),
683        ],
684        colors: vec![[255, 255, 255], [0, 1, 1], li_merged.0.colors[2]],
685        cat_ids: vec![1, 2, 3],
686        cat_idx_current: 0,
687        show_only_current: false,
688    };
689    assert_eq!(li_merged.0, li_reference);
690}
691
692#[test]
693fn test_merge_annos() {
694    let orig_shape = ShapeI::new(100, 100);
695    let li1 = LabelInfo {
696        new_label: "x".to_string(),
697        labels: vec!["somelabel".to_string(), "x".to_string()],
698        colors: vec![[255, 255, 255], [0, 1, 1]],
699        cat_ids: vec![1, 2],
700        cat_idx_current: 0,
701        show_only_current: false,
702    };
703    let li2 = LabelInfo {
704        new_label: "x".to_string(),
705        labels: vec![
706            "somelabel".to_string(),
707            "new_label".to_string(),
708            "x".to_string(),
709        ],
710        colors: vec![[255, 255, 255], [0, 1, 2], [1, 1, 1]],
711        cat_ids: vec![1, 2, 3],
712        cat_idx_current: 0,
713        show_only_current: false,
714    };
715    let mut annos_map1: super::brush_data::BrushAnnoMap = AnnotationsMap::new();
716
717    let mut line = Line::new();
718    line.push(PtF { x: 5.0, y: 5.0 });
719    let anno1 = Canvas::new(
720        &BrushLine {
721            line: line.clone(),
722            thickness: 1.0,
723            intensity: 1.0,
724        },
725        orig_shape,
726        None,
727    )
728    .unwrap();
729    annos_map1.insert(
730        "file1".to_string(),
731        (
732            InstanceAnnotations::new(vec![anno1.clone()], vec![1], vec![true]).unwrap(),
733            orig_shape,
734        ),
735    );
736    let mut annos_map2: brush_data::BrushAnnoMap = AnnotationsMap::new();
737    let anno2 = Canvas::new(
738        &BrushLine {
739            line,
740            thickness: 2.0,
741            intensity: 2.0,
742        },
743        orig_shape,
744        None,
745    )
746    .unwrap();
747
748    annos_map2.insert(
749        "file1".to_string(),
750        (
751            InstanceAnnotations::new(vec![anno2.clone()], vec![1], vec![true]).unwrap(),
752            orig_shape,
753        ),
754    );
755    annos_map2.insert(
756        "file2".to_string(),
757        (
758            InstanceAnnotations::new(vec![anno2.clone()], vec![1], vec![true]).unwrap(),
759            orig_shape,
760        ),
761    );
762    let (merged_map, merged_li) = merge(annos_map1, li1, annos_map2, li2.clone());
763    let merged_li_ref = LabelInfo {
764        new_label: "x".to_string(),
765        labels: vec![
766            "somelabel".to_string(),
767            "x".to_string(),
768            "new_label".to_string(),
769        ],
770        colors: vec![[255, 255, 255], [0, 1, 1], merged_li.colors[2]],
771        cat_ids: vec![1, 2, 3],
772        cat_idx_current: 0,
773        show_only_current: false,
774    };
775
776    assert_eq!(merged_li, merged_li_ref);
777    let map_ref = [
778        (
779            "file1".to_string(),
780            (
781                InstanceAnnotations::new(
782                    vec![anno1, anno2.clone()],
783                    vec![1, 2],
784                    vec![false, false],
785                )
786                .unwrap(),
787                orig_shape,
788            ),
789        ),
790        (
791            "file2".to_string(),
792            (
793                InstanceAnnotations::new(vec![anno2], vec![2], vec![false]).unwrap(),
794                orig_shape,
795            ),
796        ),
797    ]
798    .into_iter()
799    .collect::<AnnotationsMap<Canvas>>();
800    for (k, (v, s)) in merged_map.iter() {
801        assert_eq!(map_ref[k].0, *v);
802        assert_eq!(map_ref[k].1, *s);
803    }
804}