1use std::{
2 collections::HashMap,
3 fmt::{Debug, Display},
4 mem,
5 path::{Path, PathBuf},
6 thread::{self, JoinHandle},
7 vec,
8};
9
10use serde::{Deserialize, Serialize};
11use tracing::info;
12
13use crate::{
14 GeoFig,
15 cfg::ExportPath,
16 file_util::{self, PathPair, path_to_str, tf_to_annomap_key},
17 image_util,
18 meta_data::MetaData,
19 result::trace_ok_warn,
20 tools_data::core::polygon_to_geofig,
21 util::version_label,
22};
23use rvimage_domain::{BbF, Canvas, ShapeI, TPtF};
24use rvimage_domain::{RvError, RvResult, rverr, to_rv};
25
26use super::{
27 BboxToolData, BrushToolData, InstanceAnnotate, InstanceExportData, Rot90ToolData,
28 annotations::InstanceAnnotations,
29 brush_data::BrushAnnoMap,
30 core::{CocoSegmentation, ExportAsCoco, new_random_colors},
31};
32
33#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
37#[serde(rename_all = "snake_case")]
38enum RleOrder {
39 RowMajor,
40 ColumnMajor,
41}
42
43#[derive(Serialize, Deserialize, Debug, Default)]
44struct CocoInfo {
45 description: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
50 rvimage_rle_order: Option<RleOrder>,
51}
52
53impl Display for CocoInfo {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.write_str(&self.description)
56 }
57}
58
59#[derive(Serialize, Deserialize, Debug)]
60struct CocoImage {
61 id: u32,
62 width: u32,
63 height: u32,
64 file_name: String,
65}
66
67#[derive(Serialize, Deserialize, Debug)]
68struct CocoBboxCategory {
69 id: u32,
70 name: String,
71}
72
73#[derive(Serialize, Deserialize, Debug)]
74struct CocoAnnotation {
75 id: u32,
76 image_id: u32,
77 file_name: String,
78 category_id: u32,
79 bbox: [TPtF; 4],
80 segmentation: Option<CocoSegmentation>,
81 area: Option<TPtF>,
82}
83
84fn colors_to_string(colors: &[[u8; 3]]) -> Option<String> {
85 colors
86 .iter()
87 .map(|[r, g, b]| format!("{r};{g};{b}"))
88 .reduce(|s1, s2| format!("{s1}_{s2}"))
89}
90
91fn string_to_colors(s: &str) -> RvResult<Vec<[u8; 3]>> {
92 let make_err = || rverr!("cannot convert str {} to rgb", s);
93 s.trim()
94 .split('_')
95 .map(|rgb_str| {
96 let mut rgb = [0; 3];
97 let mut it = rgb_str.split(';');
98 for c in &mut rgb {
99 *c = it
100 .next()
101 .and_then(|s| s.parse().ok())
102 .ok_or_else(make_err)?;
103 }
104 Ok(rgb)
105 })
106 .collect::<RvResult<Vec<[u8; 3]>>>()
107}
108
109fn get_n_rotations(rotation_data: Option<&Rot90ToolData>, file_path: &str) -> u8 {
110 rotation_data
111 .and_then(|d| d.get_annos(file_path))
112 .map_or(0, |n_rot| n_rot.to_num())
113}
114
115fn insert_elt<A>(
116 elt: A,
117 annos: &mut HashMap<String, (Vec<A>, Vec<usize>, ShapeI)>,
118 cat_idx: usize,
119 n_rotations: u8,
120 path_as_key: String,
121 shape_coco: ShapeI,
122) where
123 A: InstanceAnnotate,
124{
125 let geo = trace_ok_warn(elt.rot90_with_image_ntimes(shape_coco, n_rotations));
126 if let Some(geo) = geo {
127 if let Some(annos_of_image) = annos.get_mut(&path_as_key) {
128 annos_of_image.0.push(geo);
129 annos_of_image.1.push(cat_idx);
130 } else {
131 annos.insert(
132 path_as_key,
133 (
134 vec![geo],
135 vec![cat_idx],
136 ShapeI::new(shape_coco.w, shape_coco.h),
137 ),
138 );
139 }
140 }
141}
142
143fn instance_to_coco_anno<A>(
144 inst_anno: &A,
145 shape_im_unrotated: ShapeI,
146 n_rotations: u8,
147 is_export_coords_absolute: bool,
148 file_path: &str,
149) -> RvResult<([f64; 4], Option<CocoSegmentation>)>
150where
151 A: InstanceAnnotate,
152{
153 let n_rots_inverted = (4 - n_rotations) % 4;
156 let shape_rotated = shape_im_unrotated.rot90_with_image_ntimes(n_rotations);
157 let inst_anno = inst_anno
158 .clone()
159 .rot90_with_image_ntimes(shape_rotated, n_rots_inverted)?;
160
161 let bb = inst_anno.enclosing_bb();
162
163 let segmentation = inst_anno.to_cocoseg(shape_im_unrotated, is_export_coords_absolute)?;
164 let (imw, imh) = if is_export_coords_absolute {
165 (1.0, 1.0)
166 } else {
167 (
168 TPtF::from(shape_im_unrotated.w),
169 TPtF::from(shape_im_unrotated.h),
170 )
171 };
172
173 let bb_f = [bb.x / imw, bb.y / imh, bb.w / imw, bb.h / imh];
174 if bb_f[1] * bb_f[2] < 1e-6 {
175 tracing::warn!("annotation in {file_path} has no area {bb:?}.");
176 }
177 Ok((bb_f, segmentation))
178}
179
180struct WarnerCounting<'a> {
181 n_warnings: usize,
182 n_max_warnings: usize,
183 suppressing: bool,
184 suppress_str: &'a str,
185}
186impl<'a> WarnerCounting<'a> {
187 fn new(n_max_warnings: usize, suppress_str: &'a str) -> Self {
188 Self {
189 n_warnings: 0,
190 n_max_warnings,
191 suppressing: false,
192 suppress_str,
193 }
194 }
195 fn warn_str<'b>(&mut self, msg: &'b str) -> Option<&'b str>
196 where
197 'a: 'b,
198 {
199 if self.n_warnings < self.n_max_warnings {
200 self.n_warnings += 1;
201 Some(msg)
202 } else if !self.suppressing {
203 self.suppressing = true;
204 Some(self.suppress_str)
205 } else {
206 None
207 }
208 }
209 fn warn(&mut self, msg: &str) {
210 if let Some(msg) = self.warn_str(msg) {
211 tracing::warn!(msg);
212 }
213 }
214}
215
216#[derive(Serialize, Deserialize, Debug, Default)]
217pub struct CocoExportData {
218 info: CocoInfo,
219 images: Vec<CocoImage>,
220 annotations: Vec<CocoAnnotation>,
221 categories: Vec<CocoBboxCategory>,
222}
223impl CocoExportData {
224 pub fn from_tools_data<T, A>(
225 tools_data: T,
226 rotation_data: Option<&Rot90ToolData>,
227 prj_path: Option<&Path>,
228 double_check_shape: bool,
229 ) -> Self
230 where
231 T: ExportAsCoco<A>,
232 A: InstanceAnnotate + 'static,
233 {
234 type AnnoValue<'a, A> = (&'a String, &'a (Vec<A>, Vec<usize>, ShapeI));
235
236 let (options, label_info, anno_map, coco_file) = tools_data.separate_data();
237 let color_str = if let Some(s) = colors_to_string(label_info.colors()) {
238 format!(", {s}")
239 } else {
240 String::new()
241 };
242 let info_str = format!(
243 "created with RV Image {}, https://github.com/bertiqwerty/rvimage{color_str}",
244 version_label()
245 );
246 let info = CocoInfo {
247 description: info_str,
248 rvimage_rle_order: Some(RleOrder::ColumnMajor),
249 };
250 let export_data =
251 InstanceExportData::from_tools_data(&options, label_info, coco_file, anno_map);
252
253 let make_image_map = |(idx, (file_path, (_, _, shape))): (usize, AnnoValue<A>)| CocoImage {
254 id: idx as u32,
255 width: shape.w,
256 height: shape.h,
257 file_name: file_path.clone(),
258 };
259 let images = export_data
260 .annotations
261 .iter()
262 .filter(|(_, (annos, _, _))| !annos.is_empty())
263 .enumerate()
264 .map(make_image_map)
265 .collect::<Vec<_>>();
266
267 let categories = export_data
268 .labels
269 .iter()
270 .zip(export_data.cat_ids.iter())
271 .map(|(label, cat_id)| CocoBboxCategory {
272 id: *cat_id,
273 name: label.clone(),
274 })
275 .collect::<Vec<_>>();
276
277 let mut box_id = 0;
278 let mut imagesum_elapsed = 0;
279 let mut n_images_exported = 1;
280 let make_anno_map =
281 |(image_idx, (file_path, (bbs, cat_idxs, shape))): (usize, AnnoValue<A>)| {
282 let now = std::time::Instant::now();
283 let prj_path = if let Some(prj_path) = prj_path {
284 prj_path
285 } else {
286 Path::new("")
287 };
288 let p = PathPair::new(file_path.clone(), prj_path);
289 let p_abs = p.path_absolute();
290 let shape = if Path::new(p_abs).exists() && double_check_shape {
291 let im = trace_ok_warn(image_util::read_image(file_path));
292 if let Some(im) = im {
293 ShapeI::new(im.width(), im.height())
294 } else {
295 *shape
296 }
297 } else {
298 *shape
299 };
300 let n_rotations = get_n_rotations(rotation_data, file_path);
301 let annos = bbs
302 .iter()
303 .zip(cat_idxs.iter())
304 .filter_map(|(inst_anno, cat_idx): (&A, &usize)| {
305 trace_ok_warn(instance_to_coco_anno(
306 inst_anno,
307 shape,
308 n_rotations,
309 options.is_export_absolute,
310 file_path,
311 ))
312 .and_then(|(bb_f, segmentation)| {
313 box_id += 1;
314 let cat_id = export_data.cat_ids.get(*cat_idx);
315 cat_id.map(|cat_id| CocoAnnotation {
316 id: box_id - 1,
317 image_id: image_idx as u32,
318 file_name: file_path.clone(),
319 category_id: *cat_id,
320 bbox: bb_f,
321 segmentation,
322 area: Some(bb_f[2] * bb_f[3]),
323 })
324 })
325 })
326 .collect::<Vec<_>>();
327 let elapsed = now.elapsed();
328 imagesum_elapsed += elapsed.as_millis();
329 if imagesum_elapsed > 10000 && image_idx % n_images_exported == 0 {
330 let ave = imagesum_elapsed as usize / (image_idx + 1);
331 tracing::info!(
332 "converting with {} ms/image, {}/{}, estimated time left: {} s",
333 ave,
334 image_idx + 1,
335 export_data.annotations.len(),
336 ave * (export_data.annotations.len() - image_idx - 1) / 1000
337 );
338 if n_images_exported == 1 {
339 n_images_exported = image_idx;
340 tracing::info!(
341 "If your export is too slow, consider skipping the shape double check."
342 )
343 }
344 }
345 annos
346 };
347 let annotations = export_data
348 .annotations
349 .iter()
350 .enumerate()
351 .flat_map(make_anno_map)
352 .collect::<Vec<_>>();
353
354 CocoExportData {
355 info,
356 images,
357 annotations,
358 categories,
359 }
360 }
361
362 #[allow(clippy::too_many_lines)]
363 pub fn convert_to_toolsdata(
364 self,
365 coco_file: ExportPath,
366 rotation_data: Option<&Rot90ToolData>,
367 prj_path: Option<&Path>,
368 ) -> RvResult<(BboxToolData, BrushToolData)> {
369 let cat_ids: Vec<u32> = self.categories.iter().map(|coco_cat| coco_cat.id).collect();
370 let labels: Vec<String> = self
371 .categories
372 .into_iter()
373 .map(|coco_cat| coco_cat.name)
374 .collect();
375 let color_str = self.info.description.split(',').next_back();
376 let colors: Vec<[u8; 3]> = if let Some(s) = color_str {
377 string_to_colors(s).unwrap_or_else(|_| new_random_colors(labels.len()))
378 } else {
379 new_random_colors(labels.len())
380 };
381 let is_legacy_rowmajor = match self.info.rvimage_rle_order {
387 Some(RleOrder::RowMajor) => true,
388 Some(RleOrder::ColumnMajor) => false,
389 None => self.info.description.contains("created with RV Image"),
390 };
391 let filename_image_map = self
392 .images
393 .iter()
394 .map(|image| (image.file_name.as_str(), (image.width, image.height)))
395 .collect::<HashMap<_, _>>();
396
397 let mut annotations_bbox: HashMap<String, (Vec<GeoFig>, Vec<usize>, ShapeI)> =
398 HashMap::new();
399 let mut annotations_brush: HashMap<String, (Vec<Canvas>, Vec<usize>, ShapeI)> =
400 HashMap::new();
401
402 let n_annotations = self.annotations.len();
403 let mut warner = WarnerCounting::new(
404 n_annotations / 10,
405 "suppressing further warnings during coco import",
406 );
407 for coco_anno in self.annotations {
408 let file_path = coco_anno.file_name.as_str();
409 let (w_coco, h_coco) = filename_image_map.get(file_path).copied().ok_or_else(|| {
410 rverr!(
411 "could not find image {:?} referenced by annotation {}",
412 file_path,
413 coco_anno.id
414 )
415 })?;
416 {
417 let n_rotations = get_n_rotations(rotation_data, file_path);
421 let shape_coco = ShapeI::new(w_coco, h_coco);
422
423 let path_as_key = if file_path.starts_with("http") {
424 file_util::url_encode(file_path)
425 } else {
426 tf_to_annomap_key(file_path.to_string(), prj_path)
427 };
428
429 let cat_idx = cat_ids
430 .iter()
431 .position(|cat_id| *cat_id == coco_anno.category_id)
432 .ok_or_else(|| {
433 rverr!(
434 "could not find cat id {}, we only have {:?}",
435 coco_anno.category_id,
436 cat_ids
437 )
438 })?;
439 let coords_absolute = coco_anno.bbox.iter().any(|x| *x > 1.0);
440 let (w_factor, h_factor) = if coords_absolute {
441 (1.0, 1.0)
442 } else {
443 (f64::from(w_coco), f64::from(h_coco))
444 };
445 let bbox = [
446 (w_factor * coco_anno.bbox[0]),
447 (h_factor * coco_anno.bbox[1]),
448 (w_factor * coco_anno.bbox[2]),
449 (h_factor * coco_anno.bbox[3]),
450 ];
451
452 let mut insert_geo = |geo| {
453 insert_elt(
454 geo,
455 &mut annotations_bbox,
456 cat_idx,
457 n_rotations,
458 path_as_key.clone(),
459 shape_coco,
460 );
461 };
462
463 let bb = BbF::from(&bbox);
464
465 match coco_anno.segmentation {
466 Some(CocoSegmentation::Polygon(poly)) => {
467 let geofig = if poly.is_empty() {
468 Ok(GeoFig::BB(bb))
469 } else {
470 polygon_to_geofig(&poly, w_factor, h_factor, bb, |s| warner.warn(s))
471 };
472 if let Ok(geofig) = geofig {
473 insert_geo(geofig);
474 } else {
475 warner.warn(&format!("invalid segmentation in coco file {file_path}"));
476 }
477 }
478 Some(CocoSegmentation::Rle(rle)) => {
479 let canvas = rle.to_canvas(bb, is_legacy_rowmajor);
480 if let Ok(canvas) = canvas {
481 insert_elt(
482 canvas,
483 &mut annotations_brush,
484 cat_idx,
485 n_rotations,
486 path_as_key,
487 shape_coco,
488 );
489 } else {
490 warner.warn(&format!("invalid segmentation in coco file {file_path}"));
491 }
492 }
493 _ => {
494 let geo = GeoFig::BB(bb);
495 insert_geo(geo);
496 }
497 }
498 }
499 }
500 let bbox_data = BboxToolData::from_coco_export_data(InstanceExportData {
501 labels: labels.clone(),
502 colors: colors.clone(),
503 cat_ids: cat_ids.clone(),
504 annotations: annotations_bbox,
505 coco_file: coco_file.clone(),
506 is_export_absolute: false,
507 })?;
508 let brush_data = BrushToolData::from_coco_export_data(InstanceExportData {
509 labels,
510 colors,
511 cat_ids,
512 annotations: annotations_brush,
513 coco_file,
514 is_export_absolute: false,
515 })?;
516 Ok((bbox_data, brush_data))
517 }
518}
519
520fn meta_data_to_coco_path(meta_data: &MetaData) -> RvResult<PathBuf> {
521 let export_folder = Path::new(
522 meta_data
523 .export_folder
524 .as_ref()
525 .ok_or_else(|| RvError::new("no export folder given"))?,
526 );
527 let opened_folder = meta_data
528 .opened_folder
529 .as_ref()
530 .map(PathPair::path_absolute)
531 .ok_or_else(|| RvError::new("no folder open"))?;
532 let parent = Path::new(opened_folder)
533 .parent()
534 .and_then(|p| p.file_stem())
535 .and_then(|p| p.to_str());
536
537 let opened_folder_name = Path::new(opened_folder)
538 .file_stem()
539 .and_then(|of| of.to_str())
540 .ok_or_else(|| rverr!("cannot find folder name of {}", opened_folder))?;
541 let file_name = if let Some(p) = parent {
542 format!("{p}_{opened_folder_name}_coco.json")
543 } else {
544 format!("{opened_folder_name}_coco.json")
545 };
546 Ok(export_folder.join(file_name))
547}
548fn get_cocofilepath(meta_data: &MetaData, coco_file: &ExportPath) -> RvResult<PathBuf> {
549 if path_to_str(&coco_file.path)?.is_empty() {
550 meta_data_to_coco_path(meta_data)
551 } else {
552 Ok(coco_file.path.clone())
553 }
554}
555
556pub fn to_per_file_crowd(brush_annotations_map: &mut BrushAnnoMap) {
557 for (i, (filename, (annos, _))) in brush_annotations_map.iter_mut().enumerate() {
558 if i % 10 == 0 {
559 info!("export - image #{i} converting {filename} to per-image-crowd");
560 }
561 if let Some(max_catidx) = annos.cat_idxs().iter().max() {
562 let mut canvas_idxes_of_cats = vec![vec![]; max_catidx + 1];
563 for i in 0..(annos.elts().len()) {
564 if let Some(cat_idxs) = annos
565 .cat_idxs()
566 .get(i)
567 .and_then(|idx| canvas_idxes_of_cats.get_mut(*idx))
568 {
569 cat_idxs.push(i);
570 }
571 }
572 let mut merged_canvases = vec![None; max_catidx + 1];
573 for (cat_idx, canvas_idxes) in canvas_idxes_of_cats.iter().enumerate() {
574 let mut merged_canvas: Option<Canvas> = None;
575 for canvas_idx in canvas_idxes {
576 let elt = &annos.elts().get(*canvas_idx);
577 if let Some(elt) = elt {
578 if let Some(merged_canvas) = &mut merged_canvas {
579 *merged_canvas = mem::take(merged_canvas).merge(elt);
580 } else {
581 merged_canvas = Some((*elt).clone());
582 }
583 }
584 }
585 if let Some(mc) = merged_canvases.get_mut(cat_idx) {
586 *mc = merged_canvas;
587 };
588 }
589 let mut cat_idxes = vec![];
590 let elts = merged_canvases
591 .into_iter()
592 .enumerate()
593 .filter_map(|(i, cvs)| cvs.map(|cvs| (i, cvs)))
594 .map(|(i, cvs)| {
595 cat_idxes.push(i);
596 cvs
597 })
598 .collect();
599 let n_elts = cat_idxes.len();
600 let new_annos = trace_ok_warn(InstanceAnnotations::<Canvas>::new(
601 elts,
602 cat_idxes,
603 vec![false; n_elts],
604 ));
605 if let Some(new_annos) = new_annos {
606 *annos = new_annos;
607 }
608 }
609 }
610}
611
612pub fn write_coco<T, A>(
620 meta_data: &MetaData,
621 tools_data: T,
622 rotation_data: Option<&Rot90ToolData>,
623 coco_file: &ExportPath,
624 double_check_shape: bool,
625) -> RvResult<(PathBuf, JoinHandle<RvResult<()>>)>
626where
627 T: ExportAsCoco<A> + Send + 'static,
628 A: InstanceAnnotate + 'static,
629{
630 let meta_data = meta_data.clone();
631 let coco_out_path = get_cocofilepath(&meta_data, coco_file)?;
632 let coco_out_path_for_thr = coco_out_path.clone();
633 let rotation_data = rotation_data.cloned();
634 let conn = coco_file.conn.clone();
635 let handle = thread::spawn(move || {
636 let coco_data = CocoExportData::from_tools_data(
637 tools_data,
638 rotation_data.as_ref(),
639 meta_data.prj_path(),
640 double_check_shape,
641 );
642 let data_str = serde_json::to_string(&coco_data)
643 .map_err(to_rv)
644 .inspect_err(|e| tracing::error!("export failed due to {e:?}"))?;
645
646 conn.write(
647 &data_str,
648 &coco_out_path_for_thr,
649 meta_data.ssh_cfg.as_ref(),
650 )
651 .inspect_err(|e| tracing::error!("export failed due to {e:?}"))?;
652 tracing::info!("exported coco labels to {coco_out_path_for_thr:?}");
653 Ok(())
654 });
655 Ok((coco_out_path, handle))
656}
657
658pub fn read_coco(
666 meta_data: &MetaData,
667 coco_file: &ExportPath,
668 rotation_data: Option<&Rot90ToolData>,
669) -> RvResult<(BboxToolData, BrushToolData)> {
670 let start = std::time::Instant::now();
671 let coco_inpath = get_cocofilepath(meta_data, coco_file)?;
672 let coco_str = coco_file
673 .conn
674 .read(&coco_inpath, meta_data.ssh_cfg.as_ref())?;
675 let read_data: CocoExportData = serde_json::from_str(coco_str.as_str()).map_err(to_rv)?;
676 let read_data =
677 read_data.convert_to_toolsdata(coco_file.clone(), rotation_data, meta_data.prj_path())?;
678 tracing::info!("done reading coco data");
679 tracing::info!(
680 "reading coco data took {} seconds",
681 start.elapsed().as_secs_f32()
682 );
683 Ok(read_data)
684}
685
686#[cfg(test)]
687use {
688 super::core::CocoRle,
689 crate::{
690 cfg::{ExportPathConnection, SshCfg},
691 defer_file_removal,
692 meta_data::{ConnectionData, MetaDataFlags},
693 tracing_setup::init_tracing_for_tests,
694 },
695 file_util::DEFAULT_TMPDIR,
696 rvimage_domain::{BbI, make_test_bbs},
697 std::{fs, str::FromStr},
698};
699#[cfg(test)]
700fn make_meta_data(opened_folder: Option<&Path>) -> (MetaData, PathBuf) {
701 let opened_folder = if let Some(of) = opened_folder {
702 PathPair::new(of.to_str().unwrap().to_string(), Path::new(""))
703 } else {
704 PathPair::new("xi".to_string(), Path::new(""))
705 };
706 let test_export_folder = DEFAULT_TMPDIR.clone();
707
708 if !test_export_folder.exists() {
709 match fs::create_dir(&test_export_folder) {
710 Ok(_) => (),
711 Err(e) => {
712 println!("{e:?}");
713 }
714 }
715 }
716
717 let test_export_path = DEFAULT_TMPDIR.join(format!("{}.json", opened_folder.path_absolute()));
718 let mut meta = MetaData::from_filepath(
719 test_export_path
720 .with_extension("egal")
721 .to_str()
722 .unwrap()
723 .to_string(),
724 0,
725 Path::new("egal"),
726 );
727 meta.opened_folder = Some(opened_folder);
728 meta.export_folder = Some(test_export_folder.to_str().unwrap().to_string());
729 meta.connection_data = ConnectionData::Ssh(SshCfg::default());
730 (meta, test_export_path)
731}
732#[cfg(test)]
733fn make_data_brush(
734 image_file: &Path,
735 opened_folder: Option<&Path>,
736 export_absolute: bool,
737 n_boxes: Option<usize>,
738) -> (BrushToolData, MetaData, PathBuf, ShapeI) {
739 use super::InstanceLabelDisplay;
740
741 let shape = ShapeI::new(100, 40);
742 let mut bbox_data = BrushToolData::default();
743 bbox_data.options.core.is_export_absolute = export_absolute;
744 bbox_data.coco_file = ExportPath::default();
745 bbox_data
746 .label_info
747 .push("x".to_string(), None, None)
748 .unwrap();
749
750 bbox_data
751 .label_info
752 .remove_catidx(0, &mut bbox_data.annotations_map);
753
754 let mut bbs = make_test_bbs();
755 bbs.extend(bbs.clone());
756 bbs.extend(bbs.clone());
757 bbs.extend(bbs.clone());
758 bbs.extend(bbs.clone());
759 bbs.extend(bbs.clone());
760 bbs.extend(bbs.clone());
761 bbs.extend(bbs.clone());
762 if let Some(n) = n_boxes {
763 bbs = bbs[0..n].to_vec();
764 }
765
766 let annos = bbox_data.get_annos_mut(image_file.as_os_str().to_str().unwrap(), shape);
767 if let Some(a) = annos {
768 for bb in bbs {
769 let mut mask = vec![0; (bb.w * bb.h) as usize];
770 mask[4] = 1;
771 let c = Canvas {
772 bb: bb.into(),
773 mask,
774 intensity: 0.5,
775 };
776 a.add_elt(c, 0, InstanceLabelDisplay::None);
777 }
778 }
779
780 let (meta, test_export_path) = make_meta_data(opened_folder);
781 (bbox_data, meta, test_export_path, shape)
782}
783#[cfg(test)]
784pub fn make_data_bbox(
785 image_file: &Path,
786 opened_folder: Option<&Path>,
787 export_absolute: bool,
788 n_boxes: Option<usize>,
789) -> (BboxToolData, MetaData, PathBuf, ShapeI) {
790 let shape = ShapeI::new(20, 10);
791 let mut bbox_data = BboxToolData::new();
792 bbox_data.options.core.is_export_absolute = export_absolute;
793 bbox_data.coco_file = ExportPath::default();
794 bbox_data
795 .label_info
796 .push("x".to_string(), None, None)
797 .unwrap();
798
799 bbox_data
800 .label_info
801 .remove_catidx(0, &mut bbox_data.annotations_map);
802
803 let mut bbs = make_test_bbs();
804 bbs.extend(bbs.clone());
805 bbs.extend(bbs.clone());
806 bbs.extend(bbs.clone());
807 bbs.extend(bbs.clone());
808 bbs.extend(bbs.clone());
809 bbs.extend(bbs.clone());
810 bbs.extend(bbs.clone());
811 if let Some(n) = n_boxes {
812 bbs = bbs[0..n].to_vec();
813 }
814
815 let annos = bbox_data.get_annos_mut(image_file.as_os_str().to_str().unwrap(), shape);
816 if let Some(a) = annos {
817 for bb in bbs {
818 a.add_bb(bb, 0, super::InstanceLabelDisplay::IndexLr);
819 }
820 }
821 let (meta, test_export_path) = make_meta_data(opened_folder);
822 (bbox_data, meta, test_export_path, shape)
823}
824
825#[cfg(test)]
826fn is_image_duplicate_free(coco_data: &CocoExportData) -> bool {
827 let mut image_ids = coco_data.images.iter().map(|i| i.id).collect::<Vec<_>>();
828 image_ids.sort();
829 let len_prev = image_ids.len();
830 image_ids.dedup();
831 image_ids.len() == len_prev
832}
833
834#[cfg(test)]
835fn no_image_dups<P>(coco_file: P)
836where
837 P: AsRef<Path> + Debug,
838{
839 let s = file_util::read_to_string(&coco_file).unwrap();
840 let read_raw: CocoExportData = serde_json::from_str(s.as_str()).unwrap();
841
842 assert!(is_image_duplicate_free(&read_raw));
843}
844#[test]
845fn test_coco_export() {
846 fn assert_coco_eq<T, A>(data: T, read: T, coco_file: &PathBuf)
847 where
848 T: ExportAsCoco<A> + Send + 'static,
849 A: InstanceAnnotate + 'static + Debug,
850 {
851 assert_eq!(data.label_info().cat_ids(), read.label_info().cat_ids());
852 assert_eq!(data.label_info().labels(), read.label_info().labels());
853 for (brush_anno, read_anno) in data.anno_iter().zip(read.anno_iter()) {
854 let (name, (instance_annos, shape)) = brush_anno;
855 let (read_name, (read_instance_annos, read_shape)) = read_anno;
856 assert_eq!(instance_annos.cat_idxs(), read_instance_annos.cat_idxs());
857 assert_eq!(
858 instance_annos.elts().len(),
859 read_instance_annos.elts().len()
860 );
861 for (i, (a, b)) in instance_annos
862 .elts()
863 .iter()
864 .zip(read_instance_annos.elts().iter())
865 .enumerate()
866 {
867 assert_eq!(a, b, "annos at index {} differ", i);
868 }
869 assert_eq!(name.replace('\\', "/"), read_name.replace('\\', "/"));
872 assert_eq!(shape, read_shape);
873 }
874 no_image_dups(coco_file);
875 }
876 fn write_read<T, A>(meta: &MetaData, tools_data: T) -> ((BboxToolData, BrushToolData), PathBuf)
877 where
878 T: ExportAsCoco<A> + Send + 'static,
879 A: InstanceAnnotate + 'static,
880 {
881 let coco_file = tools_data.cocofile_conn();
882 let (coco_file, handle) = write_coco(meta, tools_data, None, &coco_file, true).unwrap();
883 handle.join().unwrap().unwrap();
884 (
885 read_coco(
886 meta,
887 &ExportPath {
888 path: coco_file.clone(),
889 conn: ExportPathConnection::Local,
890 },
891 None,
892 )
893 .unwrap(),
894 coco_file,
895 )
896 }
897 fn test_br(file_path: &Path, opened_folder: Option<&Path>, export_absolute: bool) {
898 let (brush_data, meta, _, _) =
899 make_data_brush(file_path, opened_folder, export_absolute, None);
900 let ((_, read), coco_file) = write_read(&meta, brush_data.clone());
901 defer_file_removal!(&coco_file);
902 assert_coco_eq(brush_data, read, &coco_file);
903 }
904 fn test_bb(file_path: &Path, opened_folder: Option<&Path>, export_absolute: bool) {
905 let (bbox_data, meta, _, _) =
906 make_data_bbox(file_path, opened_folder, export_absolute, None);
907 let ((read, _), coco_file) = write_read(&meta, bbox_data.clone());
908 defer_file_removal!(&coco_file);
909 assert_coco_eq(bbox_data, read, &coco_file);
910 }
911 let tmpdir = &DEFAULT_TMPDIR;
912 let file_path = tmpdir.join("test_image.png");
913 test_br(&file_path, None, true);
914 test_bb(&file_path, None, true);
915 let folder = Path::new("http://localhost:8000/some_path");
916 let file = Path::new("http://localhost:8000/some_path/xyz.png");
917 test_br(file, Some(folder), false);
918 test_bb(file, Some(folder), false);
919}
920
921#[test]
922fn test_coco_sparse_annotations_roundtrip_by_filename() {
923 let shape = ShapeI::new(128, 16);
924 let mut source = BboxToolData::new();
925
926 for image_idx in 0..64 {
927 let filename = format!("image_{image_idx:02}.png");
928 let annotations = if image_idx % 2 == 0 {
929 InstanceAnnotations::from_elts_cats(
930 vec![GeoFig::BB(BbF::from_arr(&[
931 image_idx as f64,
932 1.0,
933 1.0,
934 1.0,
935 ]))],
936 vec![0],
937 )
938 } else {
939 InstanceAnnotations::default()
940 };
941 source
942 .annotations_map
943 .insert(filename, (annotations, shape));
944 }
945
946 let mut expected = source.annotations_map.clone();
947 expected.retain(|_, (annotations, _)| !annotations.is_empty());
948
949 let coco = CocoExportData::from_tools_data(source, None, None, false);
950 let (imported, _) = coco
951 .convert_to_toolsdata(ExportPath::default(), None, None)
952 .unwrap();
953
954 assert_eq!(imported.annotations_map, expected);
955}
956
957#[test]
958fn test_coco_import_requires_annotation_filename() {
959 let json = r#"{
960 "info": {"description": "test"},
961 "images": [{"id": 0, "width": 1, "height": 1, "file_name": "image.png"}],
962 "annotations": [{
963 "id": 0,
964 "image_id": 0,
965 "category_id": 1,
966 "bbox": [0.0, 0.0, 1.0, 1.0],
967 "segmentation": null,
968 "area": 1.0
969 }],
970 "categories": [{"id": 1, "name": "label"}]
971 }"#;
972
973 let error = serde_json::from_str::<CocoExportData>(json).unwrap_err();
974 assert!(error.to_string().contains("file_name"));
975}
976
977#[cfg(test)]
978const TEST_DATA_FOLDER: &str = "resources/test_data/";
979
980#[test]
981fn test_coco_import_export() {
982 let meta = MetaData::new(
983 None,
984 None,
985 ConnectionData::None,
986 None,
987 Some(PathPair::new("ohm_somefolder".to_string(), Path::new(""))),
988 Some(TEST_DATA_FOLDER.to_string()),
989 MetaDataFlags::default(),
990 None,
991 );
992 let test_file_src = format!("{TEST_DATA_FOLDER}catids_12_coco_imwolab.json");
993 let test_file = "tmp_coco.json";
994 defer_file_removal!(&test_file);
995 fs::copy(test_file_src, test_file).unwrap();
996 let export_path = ExportPath {
997 path: PathBuf::from_str(test_file).unwrap(),
998 conn: ExportPathConnection::Local,
999 };
1000
1001 let (read, _) = read_coco(&meta, &export_path, None).unwrap();
1002 let (_, handle) = write_coco(&meta, read.clone(), None, &export_path.clone(), true).unwrap();
1003 handle.join().unwrap().unwrap();
1004 no_image_dups(&read.coco_file.path);
1005 let (read, _) = read_coco(&meta, &export_path, None).unwrap();
1006 for anno in read.anno_iter() {
1007 let (_, (annos, _)) = anno;
1008 for a in annos.elts() {
1009 println!("{a:?}");
1010 assert!(a.enclosing_bb().w * a.enclosing_bb().h > 1e-3);
1011 }
1012 }
1013}
1014
1015#[test]
1016fn test_coco_import() -> RvResult<()> {
1017 init_tracing_for_tests();
1018 fn test(filename: &str, cat_ids: Vec<u32>, reference_bbs: &[(BbI, &str)]) {
1019 tracing::debug!(filename);
1020 let meta = MetaData::new(
1021 None,
1022 None,
1023 ConnectionData::None,
1024 None,
1025 Some(PathPair::new(filename.to_string(), Path::new(""))),
1026 Some(TEST_DATA_FOLDER.to_string()),
1027 MetaDataFlags::default(),
1028 None,
1029 );
1030 tracing::debug!("{meta:?}");
1031 let (read, _) = read_coco(&meta, &ExportPath::default(), None).unwrap();
1032 assert_eq!(read.label_info.cat_ids(), &cat_ids);
1033 assert_eq!(
1034 read.label_info.labels(),
1035 &vec!["first label", "second label"]
1036 );
1037 for (bb, file_path) in reference_bbs {
1038 let annos = read.get_annos(file_path);
1039 println!();
1040 println!("{file_path:?}");
1041 println!("{annos:?}");
1042 assert!(annos.unwrap().elts().contains(&GeoFig::BB((*bb).into())));
1043 }
1044 }
1045
1046 let bb_im_ref_abs1 = [
1047 (
1048 BbI::from_arr(&[1, 1, 5, 5]),
1049 "http://localhost:5000/%2Bnowhere.png",
1050 ),
1051 (
1052 BbI::from_arr(&[11, 11, 4, 7]),
1053 "http://localhost:5000/%2Bnowhere.png",
1054 ),
1055 (
1056 BbI::from_arr(&[1, 1, 5, 5]),
1057 "http://localhost:5000/%2Bnowhere2.png",
1058 ),
1059 ];
1060 let bb_im_ref_abs2 = [
1061 (BbI::from_arr(&[1, 1, 5, 5]), "nowhere.png"),
1062 (BbI::from_arr(&[11, 11, 4, 7]), "nowhere.png"),
1063 (BbI::from_arr(&[1, 1, 5, 5]), "nowhere2.png"),
1064 ];
1065 let bb_im_ref_relative = [
1066 (BbI::from_arr(&[10, 100, 50, 500]), "nowhere.png"),
1067 (BbI::from_arr(&[91, 870, 15, 150]), "nowhere.png"),
1068 (BbI::from_arr(&[10, 1, 50, 5]), "nowhere2.png"),
1069 ];
1070 test("catids_12", vec![1, 2], &bb_im_ref_abs1);
1071 test("catids_01", vec![0, 1], &bb_im_ref_abs2);
1072 test("catids_12_relative", vec![1, 2], &bb_im_ref_relative);
1073 Ok(())
1074}
1075
1076#[test]
1077fn color_vs_str() {
1078 let colors = vec![[0, 0, 7], [4, 0, 101], [210, 9, 0]];
1079 let s = colors_to_string(&colors);
1080 let colors_back = string_to_colors(&s.unwrap()).unwrap();
1081 assert_eq!(colors, colors_back);
1082}
1083
1084#[test]
1085fn test_rotation_export_import() {
1086 fn test<T, A>(
1087 coco_file: &PathBuf,
1088 bbox_specifics: T,
1089 meta_data: MetaData,
1090 shape: ShapeI,
1091 read_f: impl Fn(&MetaData, &ExportPath, Option<&Rot90ToolData>) -> T,
1092 ) where
1093 T: ExportAsCoco<A> + Send + 'static + Clone,
1094 A: InstanceAnnotate + 'static + Debug,
1095 {
1096 defer_file_removal!(&coco_file);
1097 let mut rotation_data = Rot90ToolData::default();
1098 let annos = rotation_data.get_annos_mut("some_path.png", shape);
1099 if let Some(annos) = annos {
1100 *annos = annos.increase();
1101 }
1102 let coco_file = bbox_specifics.cocofile_conn();
1103 let (out_path, handle) = write_coco(
1104 &meta_data,
1105 bbox_specifics.clone(),
1106 Some(&rotation_data),
1107 &coco_file,
1108 true,
1109 )
1110 .unwrap();
1111 handle.join().unwrap().unwrap();
1112 println!("write to {out_path:?}");
1113 let out_path = ExportPath {
1114 path: out_path,
1115 conn: ExportPathConnection::Local,
1116 };
1117 let read = read_f(&meta_data, &out_path, Some(&rotation_data));
1118
1119 for ((_, (anno_res, _)), (_, (anno_ref, _))) in
1120 bbox_specifics.anno_iter().zip(read.anno_iter())
1121 {
1122 for (read_elt, ref_elt) in anno_res.elts().iter().zip(anno_ref.elts().iter()) {
1123 assert_eq!(read_elt, ref_elt);
1124 }
1125 }
1126 }
1127 let (brush_specifics, meta_data, coco_file, shape) = make_data_brush(
1128 Path::new("some_path.png"),
1129 Some(Path::new("afolder")),
1130 false,
1131 None,
1132 );
1133 test(&coco_file, brush_specifics, meta_data, shape, |m, d, r| {
1134 read_coco(m, d, r).unwrap().1
1135 });
1136 let (bbox_specifics, meta_data, coco_file, shape) = make_data_bbox(
1137 Path::new("some_path.png"),
1138 Some(Path::new("afolder")),
1139 false,
1140 None,
1141 );
1142 test(&coco_file, bbox_specifics, meta_data, shape, |m, d, r| {
1143 read_coco(m, d, r).unwrap().0
1144 });
1145}
1146
1147#[test]
1148fn test_serialize_rle() {
1149 let rle = CocoRle {
1150 counts: vec![1, 2, 3, 4],
1151 size: (5, 6),
1152 intensity: None,
1153 };
1154 let rle = CocoSegmentation::Rle(rle);
1155 let s = serde_json::to_string(&rle).unwrap();
1156 println!("{s}");
1157 let rle2: CocoSegmentation = serde_json::from_str(&s).unwrap();
1158 assert_eq!(format!("{rle:?}"), format!("{rle2:?}"));
1159 let poly = CocoSegmentation::Polygon(vec![vec![1.0, 2.0]]);
1160 let s = serde_json::to_string(&poly).unwrap();
1161 println!("{s}");
1162 let poly2: CocoSegmentation = serde_json::from_str(&s).unwrap();
1163 assert_eq!(format!("{poly:?}"), format!("{poly2:?}"));
1164}
1165
1166#[test]
1167fn test_instance_to_coco() {
1168 let shape = ShapeI::new(2000, 2667);
1169 let bb = BbI::from_arr(&[1342, 1993, 8, 8]);
1170 let n_rot = 1;
1171 let canvas = Canvas {
1172 mask: vec![0; 64],
1173 bb,
1174 intensity: 0.5,
1175 };
1176 let coco_anno = instance_to_coco_anno(&canvas, shape, n_rot, false, "");
1177 assert!(coco_anno.is_err());
1178
1179 let shape_im = ShapeI::new(20, 40);
1180 let mut mask = vec![0; 4];
1181 mask[2] = 1;
1182 let canvas = Canvas {
1183 bb: BbI::from_arr(&[1, 1, 2, 2]),
1184 mask: mask.clone(),
1185 intensity: 0.5,
1186 };
1187 let n_rotations = 1;
1188
1189 let (_, segmentation) =
1190 instance_to_coco_anno(&canvas, shape_im, n_rotations, false, "").unwrap();
1191
1192 let coco_seg = canvas
1193 .rot90_with_image_ntimes(
1194 shape_im.rot90_with_image_ntimes(n_rotations),
1195 4 - n_rotations,
1196 )
1197 .unwrap()
1198 .to_cocoseg(shape_im, false)
1199 .unwrap();
1200 assert_ne!(coco_seg, None);
1201 assert_eq!(segmentation, coco_seg);
1202 let geo = GeoFig::BB(BbF::from_arr(&[1.0, 1.0, 2.0, 8.0]));
1203
1204 let n_rotations = 1;
1205
1206 let (bb_rot, segmentation) =
1207 instance_to_coco_anno(&geo, shape_im, n_rotations, true, "").unwrap();
1208 println!("{bb_rot:?}");
1209 let coco_seg = geo
1210 .rot90_with_image_ntimes(
1211 shape_im.rot90_with_image_ntimes(n_rotations),
1212 4 - n_rotations,
1213 )
1214 .unwrap()
1215 .to_cocoseg(shape_im, true)
1216 .unwrap();
1217 assert_ne!(coco_seg, None);
1218 assert_eq!(segmentation, coco_seg);
1219}
1220
1221#[test]
1222fn test_warner() {
1223 let suppress_msg = "no further warnings";
1224 let mut warner = WarnerCounting::new(3, suppress_msg);
1225 assert_eq!(warner.warn_str("a"), Some("a"));
1226 assert_eq!(warner.warn_str("a"), Some("a"));
1227 assert_eq!(warner.warn_str("b"), Some("b"));
1228 assert_eq!(warner.warn_str("a"), Some(suppress_msg));
1229 assert_eq!(warner.warn_str("a"), None);
1230}
1231
1232#[test]
1233fn test_coco_import_slash_normalization() {
1234 fn test(prj_path: Option<PathBuf>) {
1235 let are_keys_relative = prj_path.is_some();
1236 let meta = MetaData::new(
1237 None,
1238 None,
1239 ConnectionData::None,
1240 None,
1241 None,
1242 Some(TEST_DATA_FOLDER.to_string()),
1243 MetaDataFlags::default(),
1244 prj_path,
1245 );
1246 let coco_file = ExportPath {
1247 path: PathBuf::from(format!("{TEST_DATA_FOLDER}catids_01_coco_win_slash.json")),
1248 conn: ExportPathConnection::Local,
1249 };
1250 let (read_bb_sl, _) = read_coco(&meta, &coco_file, None).unwrap();
1251 let coco_file = ExportPath {
1252 path: PathBuf::from(format!(
1253 "{TEST_DATA_FOLDER}catids_01_coco_win_backslash.json"
1254 )),
1255 conn: ExportPathConnection::Local,
1256 };
1257 let (read_bb_bsl, _) = read_coco(&meta, &coco_file, None).unwrap();
1258
1259 assert_eq!(read_bb_sl.annotations_map, read_bb_bsl.annotations_map);
1260 for k in read_bb_sl.annotations_map.keys() {
1261 assert!(k.starts_with("c:/") != are_keys_relative);
1262 }
1263 }
1264 test(None);
1265 test(Some(PathBuf::from("c:/x/prj.json")));
1266}