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