Skip to content

Commit

Permalink
Lift YOLO annotation image file association restrictions. (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfl28 committed Nov 14, 2023
1 parent 71410cf commit 086724c
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 10 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@
/artifacts

# Chocolatey
/chocolatey/*.nupkg
/chocolatey/*.nupkg

# Mac
**/.DS_STORE
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,9 @@ class AnnotationToNonExistentImageException extends RuntimeException {
super(message);
}
}

@SuppressWarnings("serial")
class AnnotationAssociationException extends RuntimeException {
AnnotationAssociationException(String message) { super(message); }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.github.mfl28.boundingboxeditor.model.io.results.ImageAnnotationImportResult;
import com.github.mfl28.boundingboxeditor.utils.ColorUtils;
import javafx.beans.property.DoubleProperty;
import org.apache.commons.io.FilenameUtils;

import java.io.BufferedReader;
import java.io.File;
Expand All @@ -32,6 +33,8 @@
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -42,11 +45,10 @@ public class YOLOLoadStrategy implements ImageAnnotationLoadStrategy {
public static final String INVALID_BOUNDING_BOX_COORDINATES_MESSAGE = "Invalid bounding-box coordinates on line ";
private static final boolean INCLUDE_SUBDIRECTORIES = false;
private static final String OBJECT_DATA_FILE_NAME = "object.data";
private static final String YOLO_IMAGE_EXTENSION = ".jpg";
private final List<String> categories = new ArrayList<>();
private final List<IOErrorInfoEntry> unParsedFileErrorMessages =
Collections.synchronizedList(new ArrayList<>());
private Set<String> fileNamesToLoad;
private Map<String, List<String>> baseFileNameToImageFileMap;
private Map<String, ObjectCategory> categoryNameToCategoryMap;
private Map<String, Integer> boundingShapeCountPerCategory;

Expand All @@ -55,7 +57,10 @@ public ImageAnnotationImportResult load(Path path, Set<String> filesToLoad,
Map<String, ObjectCategory> existingCategoryNameToCategoryMap,
DoubleProperty progress)
throws IOException {
this.fileNamesToLoad = filesToLoad;
this.baseFileNameToImageFileMap = filesToLoad.stream().collect(
Collectors.groupingBy(FilenameUtils::getBaseName, HashMap::new,
Collectors.mapping(Function.identity(), Collectors.toList()))
);
this.boundingShapeCountPerCategory = new ConcurrentHashMap<>();
this.categoryNameToCategoryMap = new ConcurrentHashMap<>(existingCategoryNameToCategoryMap);

Expand Down Expand Up @@ -89,6 +94,7 @@ public ImageAnnotationImportResult load(Path path, Set<String> filesToLoad,
return loadAnnotationFromFile(file);
} catch(InvalidAnnotationFormatException |
AnnotationToNonExistentImageException |
AnnotationAssociationException |
IOException e) {
unParsedFileErrorMessages
.add(new IOErrorInfoEntry(
Expand Down Expand Up @@ -128,14 +134,19 @@ private void loadObjectCategories(Path root) throws IOException {
}

private ImageAnnotation loadAnnotationFromFile(File file) throws IOException {
String annotatedImageFileName =
file.getName().substring(0, file.getName().lastIndexOf('.')) + YOLO_IMAGE_EXTENSION;

if(!fileNamesToLoad.contains(annotatedImageFileName)) {
throw new AnnotationToNonExistentImageException("The image file \"" + annotatedImageFileName +
"\" does not belong to the currently loaded images.");
final List<String> annotatedImageFiles = baseFileNameToImageFileMap.get(
FilenameUtils.getBaseName(file.getName()));

if(annotatedImageFiles == null) {
throw new AnnotationToNonExistentImageException(
"No associated image file.");
} else if(annotatedImageFiles.size() > 1) {
throw new AnnotationAssociationException(
"More than one associated image file.");
}

final String annotatedImageFileName = annotatedImageFiles.get(0);

try(BufferedReader fileReader = Files.newBufferedReader(file.toPath())) {
String line;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public class BoundingBoxEditorTestBase {
protected static String TEST_IMAGE_FOLDER_PATH_1 = "/testimages/1";
protected static String TEST_IMAGE_FOLDER_PATH_2 = "/testimages/2";
protected static String TEST_IMAGE_FOLDER_PATH_3 = "/testimages/3";
protected static String TEST_IMAGE_FOLDER_PATH_4 = "/testimages/4";
protected static String TEST_EXIF_IMAGE_FOLDER_PATH = "/testimages/ExifJpeg";
protected Controller controller;
protected MainView mainView;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2023 Markus Fleischhacker <markus.fleischhacker28@gmail.com>
*
* This file is part of Bounding Box Editor
*
* Bounding Box Editor is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Bounding Box Editor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Bounding Box Editor. If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.mfl28.boundingboxeditor.controller;

import com.github.mfl28.boundingboxeditor.BoundingBoxEditorTestBase;
import com.github.mfl28.boundingboxeditor.model.io.ImageAnnotationLoadStrategy;
import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry;
import javafx.application.Platform;
import javafx.scene.control.DialogPane;
import javafx.scene.control.TableView;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.testfx.api.FxRobot;
import org.testfx.framework.junit5.Start;
import org.testfx.util.WaitForAsyncUtils;

import java.io.File;
import java.util.List;
import java.util.Map;

import static org.testfx.api.FxAssert.verifyThat;

@Tag("ui")
public class AnnotationTests extends BoundingBoxEditorTestBase {

@Start
void start(Stage stage) {
super.onStart(stage);
controller.loadImageFiles(new File(getClass().getResource(TEST_IMAGE_FOLDER_PATH_4).getFile()));
}
@Test
void onLoadAnnotation_YOLO_WhenAnnotationAssociationsProblemsPresent_ShouldNotLoadBoundingBoxes(FxRobot robot,
TestInfo testinfo) {
final String inputPath = "/testannotations/yolo/association-annotations";

waitUntilCurrentImageIsLoaded(testinfo);
WaitForAsyncUtils.waitForFxEvents();
timeOutAssertServiceSucceeded(controller.getImageMetaDataLoadingService(), testinfo);

verifyThat(mainView.getStatusBar().getCurrentEventMessage(),
Matchers.startsWith("Successfully loaded 2 image-files from folder "), saveScreenshot(testinfo));

final File inputFile = new File(getClass().getResource(inputPath).getFile());

// Load bounding-boxes defined in annotation-file.
Platform.runLater(() -> controller.initiateAnnotationImport(inputFile, ImageAnnotationLoadStrategy.Type.YOLO));
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertServiceSucceeded(controller.getAnnotationImportService(), testinfo);

final Stage errorReportStage = timeOutGetTopModalStage(robot, "Annotation Import Error Report", testinfo);
verifyThat(errorReportStage, Matchers.notNullValue(), saveScreenshot(testinfo));

final String errorReportDialogContentReferenceText =
"The source does not contain any valid annotations.";
final DialogPane errorReportDialog = (DialogPane) errorReportStage.getScene().getRoot();
verifyThat(errorReportDialog.getContentText(), Matchers.equalTo(errorReportDialogContentReferenceText),
saveScreenshot(testinfo));

verifyThat(errorReportDialog.getExpandableContent(), Matchers.instanceOf(GridPane.class),
saveScreenshot(testinfo));
verifyThat(((GridPane) errorReportDialog.getExpandableContent()).getChildren().get(0),
Matchers.instanceOf(TableView.class), saveScreenshot(testinfo));
final GridPane errorReportDialogContentPane = (GridPane) errorReportDialog.getExpandableContent();

verifyThat(errorReportDialogContentPane.getChildren().get(0), Matchers.instanceOf(TableView.class),
saveScreenshot(testinfo));

@SuppressWarnings("unchecked") final TableView<IOErrorInfoEntry> errorInfoTable =
(TableView<IOErrorInfoEntry>) errorReportDialogContentPane.getChildren().get(0);

final List<IOErrorInfoEntry> errorInfoEntries = errorInfoTable.getItems();

verifyThat(errorInfoEntries, Matchers.hasSize(2), saveScreenshot(testinfo));

final IOErrorInfoEntry referenceErrorInfoEntry1 =
new IOErrorInfoEntry("no_associated_file.txt",
"No associated image file.");

final IOErrorInfoEntry referenceErrorInfoEntry2 =
new IOErrorInfoEntry("test.txt",
"More than one associated image file.");

verifyThat(errorInfoEntries, Matchers.containsInAnyOrder(
referenceErrorInfoEntry1, referenceErrorInfoEntry2
), saveScreenshot(testinfo));

WaitForAsyncUtils.waitForFxEvents();

// Close error report dialog.
timeOutLookUpInStageAndClickOn(robot, errorReportStage, "OK", testinfo);
WaitForAsyncUtils.waitForFxEvents();

timeOutAssertTopModalStageClosed(robot, "Annotation Import Error Report", testinfo);

verifyThat(mainView.getCurrentBoundingShapes(), Matchers.hasSize(0), saveScreenshot(testinfo));
verifyThat(model.createImageAnnotationData().imageAnnotations(), Matchers.hasSize(0),
saveScreenshot(testinfo));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0 0.497002 0.519272 0.084716 0.513919
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dummy
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0 0.497002 0.519272 0.084716 0.513919
Binary file added src/test/resources/testimages/4/test.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/test/resources/testimages/4/test.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 086724c

Please sign in to comment.