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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
108pub enum InstanceLabelDisplay {
109 #[default]
110 None,
111 IndexLr,
113 IndexTb,
115 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 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 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 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 if poly.points().len() == 4
615 && poly.points_iter().all(|p| {
617 encl_bb.points_iter().any(|p_encl| p == p_encl)})
618 && 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 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 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 fn rot90_with_image_ntimes(self, shape: ShapeI, n: u8) -> RvResult<Self>;
670 fn enclosing_bb(&self) -> BbF;
671 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}