Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.github.mfl28.boundingboxeditor.model.data.BoundingBoxData;
import com.github.mfl28.boundingboxeditor.model.data.BoundingShapeData;
import com.github.mfl28.boundingboxeditor.model.data.ImageAnnotationData;
import com.github.mfl28.boundingboxeditor.model.io.data.CSVRow;
import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry;
Expand All @@ -31,9 +32,12 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

/**
* Saving-strategy to export annotations to a CSV file.
Expand Down Expand Up @@ -61,8 +65,8 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de
progress.set(1.0 * nrProcessedAnnotations.getAndIncrement() / totalNrAnnotations);

return imageAnnotation.getBoundingShapeData().stream()
.filter(BoundingBoxData.class::isInstance)
.map(boundingShapeData -> Pair.of(imageAnnotation, (BoundingBoxData) boundingShapeData));
.flatMap(this::extractBoundingBoxDataElements)
.map(boundingBoxData -> Pair.of(imageAnnotation, boundingBoxData));
})
.map(pair -> CSVRow.fromData(pair.getLeft(), pair.getRight()))
.toList()
Expand All @@ -78,4 +82,31 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de
);
}

private Stream<BoundingBoxData> extractBoundingBoxDataElements(BoundingShapeData boundingShapeData) {
if(boundingShapeData.getParts().isEmpty()) {
if(boundingShapeData instanceof BoundingBoxData boundingBoxData) {
return Stream.of(boundingBoxData);
}

return Stream.empty();
}

final Deque<BoundingShapeData> stack = new ArrayDeque<>();
final List<BoundingBoxData> result = new ArrayList<>();

stack.push(boundingShapeData);

while(!stack.isEmpty()) {
var currentBoundingShape = stack.pop();

if(currentBoundingShape instanceof BoundingBoxData boundingBoxData) {
result.add(boundingBoxData);
}

stack.addAll(currentBoundingShape.getParts());
}

return result.stream();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Saves bounding-box and bounding-polygon annotations (with at least 3 nodes)
Expand Down Expand Up @@ -106,7 +107,9 @@ private void createAnnotationFile(ImageAnnotation annotation) throws IOException
try (BufferedWriter fileWriter = Files.newBufferedWriter(
saveFolderPath.resolve(imageFileNameWithoutExtension +
YOLO_ANNOTATION_FILE_EXTENSION))) {
List<BoundingShapeData> boundingShapeDataList = annotation.getBoundingShapeData();
List<BoundingShapeData> boundingShapeDataList = annotation.getBoundingShapeData().stream()
.flatMap(this::extractBoundingShapeDataElements)
.toList();

for (int i = 0; i < boundingShapeDataList.size(); ++i) {
BoundingShapeData boundingShapeData = boundingShapeDataList.get(i);
Expand Down Expand Up @@ -153,4 +156,24 @@ private String createBoundingPolygonDataEntry(BoundingPolygonData boundingPolygo

return StringUtils.join(List.of(categoryIndex, relativePointsEntry), " ");
}

private Stream<BoundingShapeData> extractBoundingShapeDataElements(BoundingShapeData boundingShapeData) {
if(boundingShapeData.getParts().isEmpty()) {
return Stream.of(boundingShapeData);
}

final Deque<BoundingShapeData> stack = new ArrayDeque<>();
final List<BoundingShapeData> result = new ArrayList<>();

stack.push(boundingShapeData);

while(!stack.isEmpty()) {
var currentBoundingBox = stack.pop();

result.add(currentBoundingBox);
stack.addAll(currentBoundingBox.getParts());
}

return result.stream();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,118 @@ void onExportAnnotation_CSV_WhenPreviouslyImportedAnnotation_ShouldProduceEquiva
TIMEOUT_DURATION_IN_SEC + " sec."));
}

@Test
void onExportAnnotation_CSV_WhenPreviouslyImportedAnnotationWithParts_OutputShouldIncludeNestedBoxes(FxRobot robot,
TestInfo testinfo,
@TempDir Path tempDirectory)
throws IOException {
loadJson("/testannotations/json/reference/annotations.json", testinfo);

final String referenceAnnotationFilePath = "/testannotations/csv/reference/annotations_with_parts.csv";
final File referenceAnnotationFile =
new File(Objects.requireNonNull(getClass().getResource(referenceAnnotationFilePath)).getFile());

// Create temporary folder to save annotations to.
Path actualFile = tempDirectory.resolve("actual.csv");

// Save the annotations to the temporary folder.
Platform.runLater(
() -> controller.initiateAnnotationExport(actualFile.toFile(), ImageAnnotationSaveStrategy.Type.CSV));
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getAnnotationExportService(), testinfo);

// Wait until the output-file actually exists. If the file was not created in
// the specified time-frame, a TimeoutException is thrown and the test fails.
Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS,
() -> Files.exists(actualFile)),
() -> saveScreenshotAndReturnMessage(testinfo,
"Output-file was not created within " +
TIMEOUT_DURATION_IN_SEC + " sec."));

final byte[] referenceFileBytes = Files.readAllBytes(referenceAnnotationFile.toPath());

// Wait until the annotations were written to the output file and the file is equivalent to the reference file
// or throw a TimeoutException if this did not happen within the specified time-frame.
Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS,
() -> Arrays.equals(referenceFileBytes,
Files.readAllBytes(actualFile))),
() -> saveScreenshotAndReturnMessage(testinfo,
"Expected annotation output-file " +
"content was not created within " +
TIMEOUT_DURATION_IN_SEC + " sec."));
}
@Test
void onExportAnnotation_YOLO_WhenPreviouslyImportedAnnotationWithParts_OutputShouldIncludeNestedBoxes(FxRobot robot,
TestInfo testinfo,
@TempDir Path tempDirectory)
throws IOException {
loadPvoc("/testannotations/pvoc/reference/austin-neill-685084-unsplash_jpg_A.xml", testinfo);

final String referenceAnnotationDirectoryPath = "/testannotations/yolo/reference_with_parts";
final String expectedAnnotationFileName = "austin-neill-685084-unsplash.txt";

final File referenceAnnotationFolder =
new File(Objects.requireNonNull(getClass().getResource(referenceAnnotationDirectoryPath)).getFile());

// Create temporary folder to save annotations to.
Path actualDir = Files.createDirectory(tempDirectory.resolve("actual"));

Assertions.assertTrue(Files.isDirectory(actualDir),
() -> saveScreenshotAndReturnMessage(testinfo, "Actual files " +
"directory does not exist."));


// Save the annotations to the temporary folder.
Platform.runLater(
() -> controller.initiateAnnotationExport(actualDir.toFile(), ImageAnnotationSaveStrategy.Type.YOLO));
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getAnnotationExportService(), testinfo);

Path actualFilePath = actualDir.resolve(expectedAnnotationFileName);
Path actualObjectDataFilePath = actualDir.resolve("object.data");

// Wait until the output-file actually exists. If the file was not created in
// the specified time-frame, a TimeoutException is thrown and the test fails.
Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS,
() -> Files.exists(actualFilePath) &&
Files.exists(actualObjectDataFilePath)),
() -> saveScreenshotAndReturnMessage(testinfo,
"Output-files were not created within " +
TIMEOUT_DURATION_IN_SEC + " sec."));

final File objectDataFile = referenceAnnotationFolder.toPath().resolve("object.data").toFile();
final byte[] objectDataFileArray = Files.readAllBytes(objectDataFile.toPath());

// Wait until the annotations were written to the output file and the file is equivalent to the reference file
// or throw a TimeoutException if this did not happen within the specified time-frame.
Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS,
() -> Arrays.equals(objectDataFileArray,
Files.readAllBytes(
actualObjectDataFilePath))),
() -> saveScreenshotAndReturnMessage(testinfo,
"Expected object-data output-file " +
"content was not created within " +
TIMEOUT_DURATION_IN_SEC + " sec."));


// The output file should be exactly the same as the reference file.
final File referenceFile = referenceAnnotationFolder.toPath().resolve(expectedAnnotationFileName).toFile();
final byte[] referenceArray = Files.readAllBytes(referenceFile.toPath());

// Wait until the annotations were written to the output file and the file is equivalent to the reference file
// or throw a TimeoutException if this did not happen within the specified time-frame.
Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS,
() -> Arrays.equals(referenceArray,
Files.readAllBytes(
actualFilePath))),
() -> saveScreenshotAndReturnMessage(testinfo,
"Expected annotation output-file " +
"content was not created within " +
TIMEOUT_DURATION_IN_SEC + " sec."));
}

private void userChoosesNoOnAnnotationImportDialogSubtest(FxRobot robot, File annotationFile, TestInfo testinfo) {
userChoosesToSaveExistingAnnotationsOnAnnotationImport(robot, annotationFile, testinfo);
userChoosesNotToSaveExistingAnnotationsOnAnnotationImport(robot, annotationFile, testinfo);
Expand Down Expand Up @@ -1443,4 +1555,47 @@ private void importAnnotationAndClickDialogOption(FxRobot robot, File annotation

WaitForAsyncUtils.waitForFxEvents();
}

private void loadJson(String resourceName, TestInfo testinfo) {
waitUntilCurrentImageIsLoaded(testinfo);
WaitForAsyncUtils.waitForFxEvents();
timeOutAssertServiceSucceeded(controller.getImageMetaDataLoadingService(), testinfo);

verifyThat(mainView.getStatusBar().getCurrentEventMessage(),
Matchers.startsWith("Successfully loaded 4 image-files from folder "), saveScreenshot(testinfo));
final File inputAnnotationFile =
new File(Objects.requireNonNull(getClass().getResource(resourceName)).getFile());

// Load bounding-boxes defined in the reference annotation-file.
Platform.runLater(() -> controller
.initiateAnnotationImport(inputAnnotationFile, ImageAnnotationLoadStrategy.Type.JSON));
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getAnnotationImportService(), testinfo);
WaitForAsyncUtils.waitForFxEvents();

verifyThat(mainView.getStatusBar().getCurrentEventMessage(),
Matchers.startsWith("Successfully imported annotations for 1 image in"), saveScreenshot(testinfo));
}

private void loadPvoc(String resourceName, TestInfo testinfo) {
waitUntilCurrentImageIsLoaded(testinfo);
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getImageMetaDataLoadingService(), testinfo);
verifyThat(mainView.getStatusBar().getCurrentEventMessage(),
Matchers.startsWith("Successfully loaded 4 image-files from folder "), saveScreenshot(testinfo));
verifyThat(model.isSaved(), Matchers.is(true), saveScreenshot(testinfo));

final File referenceAnnotationFile = new File(Objects.requireNonNull(getClass().getResource(resourceName)).getFile());

// Load bounding-boxes defined in the reference annotation-file.
Platform.runLater(() -> controller
.initiateAnnotationImport(referenceAnnotationFile, ImageAnnotationLoadStrategy.Type.PASCAL_VOC));
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getAnnotationImportService(), testinfo);

verifyThat(model.isSaved(), Matchers.is(true), saveScreenshot(testinfo));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
filename,width,height,class,xmin,ymin,xmax,ymax
"austin-neill-685084-unsplash.jpg",3854,2033,Boat,1815,1572,2008,1660
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1761,84,1904,583
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1798,236,1901,553
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1562,453,2113,1046
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1632,442,2169,993
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1769,811,1915,1337
"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1719,1470,1870,1710
"austin-neill-685084-unsplash.jpg",3854,2033,Flag,1565,1748,1690,1786
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
0 0.45055 0.402755 0.449634 0.46699 0.45055 0.598928 0.460623 0.699616 0.4739 0.76819 0.494964 0.782946 0.512364 0.776872 0.524268 0.756906 0.537545 0.637118 0.541209 0.485219 0.54075 0.374983 0.5261 0.28818 0.496796 0.253458 0.474359 0.269085 0.45467 0.328977
0 0.496083 0.794786 0.050039 0.04301
2 0.475549 0.164092 0.037211 0.245519
2 0.479923 0.194161 0.026894 0.156173
2 0.476855 0.36865 0.142958 0.2918
2 0.493108 0.352932 0.139434 0.270762
2 0.477964 0.528286 0.037859 0.258637
2 0.465627 0.782098 0.039037 0.118303
1 0.422241 0.869144 0.03241 0.018741
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Boat
Flag
Sail