From b112f9d362a5d5fb8f021d77a0162dc1295ac0c2 Mon Sep 17 00:00:00 2001 From: Markus Fleischhacker Date: Fri, 27 Dec 2024 15:43:09 +0100 Subject: [PATCH] Switch to tensorflow CSV format. (#142) --- build.gradle | 2 - .../model/io/CSVSaveStrategy.java | 70 +++++++++----- .../model/io/CSVSaveStrategyTest.java | 95 ++++++------------- 3 files changed, 76 insertions(+), 91 deletions(-) diff --git a/build.gradle b/build.gradle index ddd4656..8be99de 100644 --- a/build.gradle +++ b/build.gradle @@ -60,8 +60,6 @@ dependencies { // Mockito-Junit https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter testImplementation 'org.mockito:mockito-junit-jupiter:5.14.2' - testImplementation 'com.google.jimfs:jimfs:1.3.0' - testJavaagent "net.bytebuddy:byte-buddy-agent:1.15.7" // Commons Collections https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java index a76a8a9..dbd5fb5 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java @@ -18,7 +18,9 @@ */ package com.github.mfl28.boundingboxeditor.model.io; -import com.github.mfl28.boundingboxeditor.model.data.*; +import com.github.mfl28.boundingboxeditor.model.data.BoundingBoxData; +import com.github.mfl28.boundingboxeditor.model.data.ImageAnnotation; +import com.github.mfl28.boundingboxeditor.model.data.ImageAnnotationData; import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry; import com.github.mfl28.boundingboxeditor.model.io.results.ImageAnnotationExportResult; import com.opencsv.CSVWriterBuilder; @@ -34,18 +36,18 @@ /** * Saving-strategy to export annotations to a CSV file. - * + *

* The CSVSaveStrategy supports {@link BoundingBoxData} only. */ public class CSVSaveStrategy implements ImageAnnotationSaveStrategy { - private static final String FILE_NAME_SERIALIZED_NAME = "name"; - private static final String ID_SERIALIZED_NAME = "id"; - private static final String LABEL_SERIALIZED_NAME = "label"; - private static final String MIN_X_SERIALIZED_NAME = "xMin"; - private static final String MAX_X_SERIALIZED_NAME = "xMax"; - private static final String MIN_Y_SERIALIZED_NAME = "yMin"; - private static final String MAX_Y_SERIALIZED_NAME = "yMax"; - private static final String UNSUPPORTED_BOUNDING_SHAPE = "CSV can export Rectangles only"; + private static final String FILE_NAME_SERIALIZED_NAME = "filename"; + private static final String WIDTH_SERIALIZED_NAME = "width"; + private static final String HEIGHT_SERIALIZED_NAME = "height"; + private static final String CLASS_SERIALIZED_NAME = "class"; + private static final String MIN_X_SERIALIZED_NAME = "xmin"; + private static final String MAX_X_SERIALIZED_NAME = "xmax"; + private static final String MIN_Y_SERIALIZED_NAME = "ymin"; + private static final String MAX_Y_SERIALIZED_NAME = "ymax"; @Override public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path destination, @@ -55,25 +57,29 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de final List errorEntries = new ArrayList<>(); - try(ICSVWriter writer = new CSVWriterBuilder(Files.newBufferedWriter(destination, StandardCharsets.UTF_8)).build()) { - String[] header = {FILE_NAME_SERIALIZED_NAME, ID_SERIALIZED_NAME, LABEL_SERIALIZED_NAME, MIN_X_SERIALIZED_NAME, MAX_X_SERIALIZED_NAME, MIN_Y_SERIALIZED_NAME, MAX_Y_SERIALIZED_NAME}; + try (ICSVWriter writer = new CSVWriterBuilder(Files.newBufferedWriter(destination, StandardCharsets.UTF_8)).build()) { + String[] header = { + FILE_NAME_SERIALIZED_NAME, + WIDTH_SERIALIZED_NAME, + HEIGHT_SERIALIZED_NAME, + CLASS_SERIALIZED_NAME, + MIN_X_SERIALIZED_NAME, + MIN_Y_SERIALIZED_NAME, + MAX_X_SERIALIZED_NAME, + MAX_Y_SERIALIZED_NAME}; + writer.writeNext(header); - for (ImageAnnotation imageAnnotation : annotations.imageAnnotations()) { - for (BoundingShapeData boundingShapeData : imageAnnotation.getBoundingShapeData()) { + + for (var imageAnnotation : annotations.imageAnnotations()) { + for (var boundingShapeData : imageAnnotation.getBoundingShapeData()) { if (boundingShapeData instanceof BoundingBoxData boundingBoxData) { - double xMin = imageAnnotation.getImageMetaData().getImageWidth() * boundingBoxData.getXMinRelative(); - double xMax = imageAnnotation.getImageMetaData().getImageWidth() * boundingBoxData.getXMaxRelative(); - double yMin = imageAnnotation.getImageMetaData().getImageHeight() * boundingBoxData.getYMinRelative(); - double yMax = imageAnnotation.getImageMetaData().getImageHeight() * boundingBoxData.getYMaxRelative(); - String[] line = { imageAnnotation.getImageFileName(), String.valueOf(nrProcessedAnnotations), boundingShapeData.getCategoryName(), String.valueOf((int) xMin), String.valueOf((int) xMax), String.valueOf((int) yMin), String.valueOf((int) yMax)}; - writer.writeNext(line); - } else { - errorEntries.add(new IOErrorInfoEntry(imageAnnotation.getImageFileName(), UNSUPPORTED_BOUNDING_SHAPE)); + writer.writeNext(buildLine(imageAnnotation, boundingBoxData)); } + progress.set(1.0 * nrProcessedAnnotations++ / totalNrAnnotations); } } - } catch(IOException e) { + } catch (IOException e) { errorEntries.add(new IOErrorInfoEntry(destination.getFileName().toString(), e.getMessage())); } @@ -82,4 +88,22 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de errorEntries ); } + + private static String[] buildLine(ImageAnnotation imageAnnotation, BoundingBoxData boundingBoxData) { + double imageWidth = imageAnnotation.getImageMetaData().getImageWidth(); + double imageHeight = imageAnnotation.getImageMetaData().getImageHeight(); + + var bounds = boundingBoxData.getAbsoluteBoundsInImage(imageWidth, imageHeight); + + return new String[]{ + imageAnnotation.getImageFileName(), + String.valueOf((int) Math.round(imageWidth)), + String.valueOf((int) Math.round(imageHeight)), + boundingBoxData.getCategoryName(), + String.valueOf((int) Math.round(bounds.getMinX())), + String.valueOf((int) Math.round(bounds.getMinY())), + String.valueOf((int) Math.round(bounds.getMaxX())), + String.valueOf((int) Math.round(bounds.getMaxY())) + }; + } } diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java b/src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java index 95ca563..c4d2e53 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java @@ -20,20 +20,20 @@ import com.github.mfl28.boundingboxeditor.model.data.*; import com.github.mfl28.boundingboxeditor.model.io.results.ImageAnnotationExportResult; -import com.google.common.jimfs.Configuration; -import com.google.common.jimfs.Jimfs; import javafx.beans.property.SimpleDoubleProperty; import javafx.scene.paint.Color; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class CSVSaveStrategyTest { @@ -41,81 +41,44 @@ class CSVSaveStrategyTest { * One image with two annotations gets exported to CSV. */ @Test - void multipleRectangles() throws IOException { - ObjectCategory objectCategory = new ObjectCategory("category", Color.YELLOW); + void testCSVSaving(@TempDir Path tempDir) throws IOException { + var objectCategory1 = new ObjectCategory("catA", Color.YELLOW); + var objectCategory2 = new ObjectCategory("catB", Color.BLUE); + var imageMetaData1 = new ImageMetaData("sample1.png", "folderName", "url", 100, 200, 0); + var imageMetaData2 = new ImageMetaData("sample2.png", "folderName", "url", 400, 300, 0); - ImageAnnotation imageAnnotation = new ImageAnnotation(new ImageMetaData("sample.png", "folderName", "url", 100, 200, 0)); - List boundingShapeDatas = new ArrayList<>(); - boundingShapeDatas.add(new BoundingBoxData(objectCategory, 0,0,0.5,0.5, List.of("tag"))); - boundingShapeDatas.add(new BoundingBoxData(objectCategory, 0,0,0.25,0.25, List.of("tag"))); - imageAnnotation.setBoundingShapeData(boundingShapeDatas); + List boundingShapeData1 = List.of( + new BoundingBoxData(objectCategory1, 0, 0, 0.5, 0.5, List.of("tag")), + new BoundingBoxData(objectCategory2, 0, 0, 0.25, 0.25, List.of("tag")), + new BoundingPolygonData(objectCategory1, List.of(0.0, 0.0, 0.5, 0.5), Collections.emptyList()) + ); - ImageAnnotationData annotations = new ImageAnnotationData(List.of(imageAnnotation), - Map.of("object", 1), - Map.of("object", objectCategory)); - Path destination = Jimfs.newFileSystem(Configuration.unix()).getPath("annotations.csv"); - ImageAnnotationExportResult save = new CSVSaveStrategy().save(annotations, destination, new SimpleDoubleProperty(0)); - assertTrue(save.getErrorTableEntries().isEmpty()); + var imageAnnotation1 = new ImageAnnotation(imageMetaData1, boundingShapeData1); - String content = Files.readString(destination); - assertEquals(""" - "name","id","label","xMin","xMax","yMin","yMax" - "sample.png","0","category","0","50","0","100" - "sample.png","1","category","0","25","0","50" - """, content); - } + List boundingShapeData2 = List.of( + new BoundingBoxData(objectCategory2, 0.1, 0, 0.5, 0.2, List.of("tag")) + ); - /** - * Two images with each one annotation gets exported. - */ - @Test - void multipleImages() throws IOException { - ObjectCategory objectCategory = new ObjectCategory("category", Color.YELLOW); + var imageAnnotation2 = new ImageAnnotation(imageMetaData2, boundingShapeData2); - ImageAnnotation imageAnnotation1 = new ImageAnnotation( - new ImageMetaData("sample1.png", "folderName", "url", 100, 200, 0), - List.of(new BoundingBoxData(objectCategory, 0,0,0.5,0.5, List.of("tag")))); + ImageAnnotationData annotations = new ImageAnnotationData( + List.of(imageAnnotation1, imageAnnotation2), + Map.of("catA", 2, "catB", 2), + Map.of("catA", objectCategory1, "catB", objectCategory2)); - ImageAnnotation imageAnnotation2 = new ImageAnnotation( - new ImageMetaData("sample2.png", "folderName", "url", 100, 200, 0), - List.of(new BoundingBoxData(objectCategory, 0,0,0.25,0.25, List.of("tag")))); + Path destination = tempDir.resolve("annotations.csv"); - ImageAnnotationData annotations = new ImageAnnotationData(List.of(imageAnnotation1, imageAnnotation2), - Map.of("object", 1), - Map.of("object", objectCategory)); - Path destination = Jimfs.newFileSystem(Configuration.unix()).getPath("annotations.csv"); ImageAnnotationExportResult save = new CSVSaveStrategy().save(annotations, destination, new SimpleDoubleProperty(0)); + assertTrue(save.getErrorTableEntries().isEmpty()); String content = Files.readString(destination); - assertEquals(""" - "name","id","label","xMin","xMax","yMin","yMax" - "sample1.png","0","category","0","50","0","100" - "sample2.png","1","category","0","25","0","50" - """, content); - } - - /** - * One image with one annotation should be saved. The annotation uses an unsupported Bounding Shape, so a ErrorTableEntry is expected. - */ - @Test - void wrongBoundingShape() throws IOException { - ObjectCategory objectCategory = new ObjectCategory("category", Color.YELLOW); - - ImageAnnotation imageAnnotation1 = new ImageAnnotation( - new ImageMetaData("sample1.png", "folderName", "url", 100, 200, 0), - List.of(new BoundingPolygonData(objectCategory, List.of(0.0, 0.0, 0.5, 0.5), List.of("tag")))); - ImageAnnotationData annotations = new ImageAnnotationData(List.of(imageAnnotation1), - Map.of("object", 1), - Map.of("object", objectCategory)); - Path destination = Jimfs.newFileSystem(Configuration.unix()).getPath("annotations.csv"); - ImageAnnotationExportResult save = new CSVSaveStrategy().save(annotations, destination, new SimpleDoubleProperty(0)); - assertEquals(1, save.getErrorTableEntries().size()); - - String content = Files.readString(destination); assertEquals(""" - "name","id","label","xMin","xMax","yMin","yMax" + "filename","width","height","class","xmin","ymin","xmax","ymax" + "sample1.png","100","200","catA","0","0","50","100" + "sample1.png","100","200","catB","0","0","25","50" + "sample2.png","400","300","catB","40","0","200","60" """, content); } } \ No newline at end of file