Skip to main content

rvlib/tools_data/
coco_io.rs

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/// Order in which the Coco RLE `counts` are laid out. Current RV Image / Coco
34/// files use [`RleOrder::ColumnMajor`]; Coco files exported by older RV Image
35/// versions used [`RleOrder::RowMajor`].
36#[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    /// RLE order marker written by RV Image. Current versions write
47    /// [`RleOrder::ColumnMajor`] (the Coco convention). Older RV Image exports
48    /// do not have this field and used row-major order.
49    #[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    // to store data corresponding to the image on the disk, we need to invert the
154    // applied rotations
155    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        // Determine the RLE order of the file. Current RV Image / Coco files use
382        // column-major counts. Coco files exported by older RV Image versions
383        // used row-major counts and are detected via the RV Image signature in
384        // the description (external Coco files without the signature are assumed
385        // to follow the column-major Coco convention).
386        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                // The annotations in the coco files created by RV Image are stored
418                // ignoring any orientation meta-data. Hence, if the image has been loaded
419                // and rotated with RV Image we correct the rotation.
420                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
612/// Serialize annotations in Coco format. Any orientations changes applied with the rotation tool
613/// are reverted, since the rotation tool does not change the image file. Hence, the Coco file contains the annotation
614/// relative to the image as it is found in memory ignoring any meta-data.
615///
616/// # Errors
617/// - outpath name cannot be created due to weird characters or the like
618/// - serde write-to-json fails
619pub 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
658/// Import annotations in Coco format. Any orientations changes applied with the rotation tool
659/// to images that have annotations in the Coco file are applied to the annotations before importing. We expect, that
660/// the Coco file contains the annotations relative to the image as it is found in memory ignoring any meta-data.
661///
662/// # Errors
663/// - not a coco file
664/// - ssh connection problems
665pub 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            // paths are normalized to forward slashes on import, so compare
870            // separator-agnostically
871            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}