Skip to content

Commit

Permalink
Switch to tensorflow CSV format. (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfl28 authored Dec 27, 2024
1 parent a0b3913 commit b112f9d
Showing 3 changed files with 76 additions and 91 deletions.
2 changes: 0 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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<IOErrorInfoEntry> 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()))
};
}
}
Original file line number Diff line number Diff line change
@@ -20,102 +20,65 @@

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 {

/**
* 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<BoundingShapeData> 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<BoundingShapeData> 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<BoundingShapeData> 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);
}
}

0 comments on commit b112f9d

Please sign in to comment.