diff --git a/.gitattributes b/.gitattributes index bf81fd5..0e6b624 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# Force json test annotation files to always have LF line endings. -/src/test/resources/testannotations/**/*.json text eol=lf \ No newline at end of file +# Force test annotation files to always have LF line endings. +/src/test/resources/testannotations/**/*.json text eol=lf +/src/test/resources/testannotations/**/*.csv text eol=lf \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1c7cb16..f798e53 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ plugins { id 'com.github.hierynomus.license' version '0.16.1' id 'io.miret.etienne.sass' version '1.5.1' id "com.ryandens.javaagent-test" version "0.7.0" + id "io.freefair.lombok" version "8.11" } group 'com.github.mfl28' @@ -112,7 +113,8 @@ dependencies { // https://mvnrepository.com/artifact/com.drewnoakes/metadata-extractor implementation 'com.drewnoakes:metadata-extractor:2.19.0' - implementation 'com.opencsv:opencsv:5.9' + // https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-csv + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.18.3' } javafx { diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java b/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java index e6eb68b..91d49f3 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java @@ -1216,21 +1216,22 @@ private File getAnnotationSavingDestination(ImageAnnotationSaveStrategy.Type sav } private File getAnnotationLoadingSource(ImageAnnotationLoadStrategy.Type loadFormat) { - File source; - - if(loadFormat.equals(ImageAnnotationLoadStrategy.Type.JSON)) { - source = MainView.displayFileChooserAndGetChoice(LOAD_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE, stage, + return switch (loadFormat) { + case JSON -> MainView.displayFileChooserAndGetChoice(LOAD_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE, stage, ioMetaData.getDefaultAnnotationLoadingDirectory(), DEFAULT_JSON_EXPORT_FILENAME, new FileChooser.ExtensionFilter("JSON files", "*.json", "*.JSON"), MainView.FileChooserType.OPEN); - } else { - source = MainView.displayDirectoryChooserAndGetChoice(LOAD_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage, + case CSV -> MainView.displayFileChooserAndGetChoice(LOAD_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE, stage, + ioMetaData.getDefaultAnnotationLoadingDirectory(), + DEFAULT_CSV_EXPORT_FILENAME, + new FileChooser.ExtensionFilter("CSV files", "*.csv", + "*.CSV"), + MainView.FileChooserType.OPEN); + default -> MainView.displayDirectoryChooserAndGetChoice(LOAD_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage, ioMetaData.getDefaultAnnotationLoadingDirectory()); - } - - return source; + }; } private void interruptDirectoryWatcher() { diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVLoadStrategy.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVLoadStrategy.java new file mode 100644 index 0000000..6b972ec --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVLoadStrategy.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2025 Markus Fleischhacker + * + * 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 . + */ +package com.github.mfl28.boundingboxeditor.model.io; + +import com.fasterxml.jackson.databind.MappingIterator; +import com.fasterxml.jackson.databind.RuntimeJsonMappingException; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; +import com.fasterxml.jackson.dataformat.csv.CsvParser; +import com.fasterxml.jackson.dataformat.csv.CsvReadException; +import com.github.mfl28.boundingboxeditor.model.data.*; +import com.github.mfl28.boundingboxeditor.model.io.data.CSVRow; +import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry; +import com.github.mfl28.boundingboxeditor.model.io.results.ImageAnnotationImportResult; +import com.github.mfl28.boundingboxeditor.utils.ColorUtils; +import javafx.beans.property.DoubleProperty; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.*; + +public class CSVLoadStrategy implements ImageAnnotationLoadStrategy { + + private static boolean filterRow(Set filesToLoad, CSVRow csvRow, List errorInfoEntries) { + if (filesToLoad.contains(csvRow.getFilename())) { + return true; + } + + errorInfoEntries.add(new IOErrorInfoEntry(csvRow.getFilename(), + "Image " + csvRow.getFilename() + + " does not belong to currently loaded image files.")); + + return false; + } + + private static void updateAnnotations( + CSVRow csvRow, Map filenameAnnotationMap, + Map categoryNameToCategoryMap, + Map categoryNameToShapeCountMap) { + var filename = csvRow.getFilename(); + + var imageAnnotation = filenameAnnotationMap.computeIfAbsent( + filename, key -> new ImageAnnotation(new ImageMetaData(key))); + + var boundingBoxData = createBoundingBox(csvRow, categoryNameToCategoryMap); + + imageAnnotation.getBoundingShapeData().add(boundingBoxData); + categoryNameToShapeCountMap.merge(boundingBoxData.getCategoryName(), 1, Integer::sum); + } + + private static BoundingBoxData createBoundingBox(CSVRow csvRow, Map existingCategoryNameToCategoryMap) { + var objectCategory = existingCategoryNameToCategoryMap.computeIfAbsent(csvRow.getCategoryName(), + name -> new ObjectCategory(name, ColorUtils.createRandomColor())); + + double xMinRelative = (double) csvRow.getXMin() / csvRow.getWidth(); + double yMinRelative = (double) csvRow.getYMin() / csvRow.getHeight(); + double xMaxRelative = (double) csvRow.getXMax() / csvRow.getWidth(); + double yMaxRelative = (double) csvRow.getYMax() / csvRow.getHeight(); + + return new BoundingBoxData( + objectCategory, xMinRelative, yMinRelative, xMaxRelative, yMaxRelative, + Collections.emptyList()); + } + + @Override + public ImageAnnotationImportResult load(Path path, Set filesToLoad, + Map existingCategoryNameToCategoryMap, + DoubleProperty progress) throws IOException { + final Map categoryNameToBoundingShapesCountMap = new HashMap<>(); + final List errorInfoEntries = new ArrayList<>(); + final Map filenameAnnotationMap = new HashMap<>(); + + progress.set(0); + + final var csvMapper = new CsvMapper(); + final var csvSchema = csvMapper.schemaFor(CSVRow.class) + .withHeader() + .withColumnReordering(true) + .withStrictHeaders(true); + + try (MappingIterator it = csvMapper + .readerFor(CSVRow.class) + .with(csvSchema) + .without(CsvParser.Feature.IGNORE_TRAILING_UNMAPPABLE) + .without(CsvParser.Feature.ALLOW_TRAILING_COMMA) + .with(CsvParser.Feature.FAIL_ON_MISSING_COLUMNS) + .with(CsvParser.Feature.FAIL_ON_MISSING_HEADER_COLUMNS) + .readValues(path.toFile())) { + it.forEachRemaining(csvRow -> { + try { + if (filterRow(filesToLoad, csvRow, errorInfoEntries)) { + updateAnnotations(csvRow, filenameAnnotationMap, + existingCategoryNameToCategoryMap, + categoryNameToBoundingShapesCountMap); + } + + } catch (RuntimeJsonMappingException exception) { + errorInfoEntries.add(new IOErrorInfoEntry(path.getFileName().toString(), + exception.getMessage())); + } + } + ); + } catch (CsvReadException exception) { + errorInfoEntries.add(new IOErrorInfoEntry(path.getFileName().toString(), + exception.getMessage())); + } + + var imageAnnotationData = new ImageAnnotationData( + filenameAnnotationMap.values(), categoryNameToBoundingShapesCountMap, + existingCategoryNameToCategoryMap); + + progress.set(1.0); + + return new ImageAnnotationImportResult( + imageAnnotationData.imageAnnotations().size(), + errorInfoEntries, + imageAnnotationData + ); + } + +} + 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 74f10fe..d3d2b40 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,14 +18,14 @@ */ package com.github.mfl28.boundingboxeditor.model.io; +import com.fasterxml.jackson.dataformat.csv.CsvMapper; 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.data.CSVRow; import com.github.mfl28.boundingboxeditor.model.io.results.IOErrorInfoEntry; import com.github.mfl28.boundingboxeditor.model.io.results.ImageAnnotationExportResult; -import com.opencsv.CSVWriterBuilder; -import com.opencsv.ICSVWriter; import javafx.beans.property.DoubleProperty; +import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -33,6 +33,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; /** * Saving-strategy to export annotations to a CSV file. @@ -40,44 +41,32 @@ * The CSVSaveStrategy supports {@link BoundingBoxData} only. */ public class CSVSaveStrategy implements ImageAnnotationSaveStrategy { - 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, DoubleProperty progress) { final int totalNrAnnotations = annotations.imageAnnotations().size(); - int nrProcessedAnnotations = 0; + final AtomicInteger nrProcessedAnnotations = new AtomicInteger(); final List errorEntries = new ArrayList<>(); - 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); + try (var writer = Files.newBufferedWriter(destination, StandardCharsets.UTF_8)) { + var csvMapper = new CsvMapper(); + var csvSchema = csvMapper.schemaFor(CSVRow.class).withHeader(); - for (var imageAnnotation : annotations.imageAnnotations()) { - for (var boundingShapeData : imageAnnotation.getBoundingShapeData()) { - if (boundingShapeData instanceof BoundingBoxData boundingBoxData) { - writer.writeNext(buildLine(imageAnnotation, boundingBoxData)); - } + try (var valuesWriter = csvMapper.writer(csvSchema).writeValues(writer)) { + valuesWriter.writeAll( + annotations.imageAnnotations().stream() + .flatMap( + imageAnnotation -> { + progress.set(1.0 * nrProcessedAnnotations.getAndIncrement() / totalNrAnnotations); - progress.set(1.0 * nrProcessedAnnotations++ / totalNrAnnotations); - } + return imageAnnotation.getBoundingShapeData().stream() + .filter(BoundingBoxData.class::isInstance) + .map(boundingShapeData -> Pair.of(imageAnnotation, (BoundingBoxData) boundingShapeData)); + }) + .map(pair -> CSVRow.fromData(pair.getLeft(), pair.getRight())) + .toList() + ); } } catch (IOException e) { errorEntries.add(new IOErrorInfoEntry(destination.getFileName().toString(), e.getMessage())); @@ -89,21 +78,4 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de ); } - 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/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationLoadStrategy.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationLoadStrategy.java index 9549a61..505e2fd 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationLoadStrategy.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationLoadStrategy.java @@ -25,7 +25,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.security.InvalidParameterException; import java.util.Map; import java.util.Set; @@ -40,15 +39,12 @@ public interface ImageAnnotationLoadStrategy { * @return the loading-strategy with the provided type */ static ImageAnnotationLoadStrategy createStrategy(Type type) { - if(type.equals(Type.PASCAL_VOC)) { - return new PVOCLoadStrategy(); - } else if(type.equals(Type.YOLO)) { - return new YOLOLoadStrategy(); - } else if(type.equals(Type.JSON)) { - return new JSONLoadStrategy(); - } else { - throw new InvalidParameterException(); - } + return switch (type) { + case PASCAL_VOC -> new PVOCLoadStrategy(); + case YOLO -> new YOLOLoadStrategy(); + case JSON -> new JSONLoadStrategy(); + case CSV -> new CSVLoadStrategy(); + }; } /** @@ -65,7 +61,7 @@ ImageAnnotationImportResult load(Path path, Set filesToLoad, Map existingCategoryNameToCategoryMap, DoubleProperty progress) throws IOException; - enum Type {PASCAL_VOC, YOLO, JSON} + enum Type {PASCAL_VOC, YOLO, JSON, CSV} @SuppressWarnings("serial") class InvalidAnnotationFormatException extends RuntimeException { diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationSaveStrategy.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationSaveStrategy.java index 5a3bdac..c064fbd 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationSaveStrategy.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/ImageAnnotationSaveStrategy.java @@ -24,7 +24,6 @@ import javafx.beans.property.DoubleProperty; import java.nio.file.Path; -import java.security.InvalidParameterException; /** * The interface of an image annotation saving-strategy. @@ -37,17 +36,12 @@ public interface ImageAnnotationSaveStrategy { * @return the saving-strategy with the provided type */ static ImageAnnotationSaveStrategy createStrategy(Type type) { - if(type.equals(Type.PASCAL_VOC)) { - return new PVOCSaveStrategy(); - } else if(type.equals(Type.YOLO)) { - return new YOLOSaveStrategy(); - } else if(type.equals(Type.JSON)) { - return new JSONSaveStrategy(); - } else if(type.equals(Type.CSV)) { - return new CSVSaveStrategy(); - } else { - throw new InvalidParameterException(); - } + return switch (type) { + case PASCAL_VOC -> new PVOCSaveStrategy(); + case YOLO -> new YOLOSaveStrategy(); + case JSON -> new JSONSaveStrategy(); + case CSV -> new CSVSaveStrategy(); + }; } /** diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/data/CSVRow.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/data/CSVRow.java new file mode 100644 index 0000000..20995c7 --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/data/CSVRow.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 Markus Fleischhacker + * + * 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 . + */ +package com.github.mfl28.boundingboxeditor.model.io.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.github.mfl28.boundingboxeditor.model.data.BoundingBoxData; +import com.github.mfl28.boundingboxeditor.model.data.ImageAnnotation; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@JsonPropertyOrder({ "filename", "width", "height", "class" , "xmin", "ymin", "xmax", "ymax" }) +@Jacksonized +@Builder +@Value +public class CSVRow { + @JsonProperty(required = true) + String filename; + + @JsonProperty(required = true) + int width; + + @JsonProperty(required = true) + int height; + + @JsonProperty(value = "class", required = true) + String categoryName; + + @JsonProperty(value = "xmin", required = true) + int xMin; + + @JsonProperty(value = "ymin", required = true) + int yMin; + + @JsonProperty(value = "xmax", required = true) + int xMax; + + @JsonProperty(value = "ymax", required = true) + int yMax; + + public static CSVRow fromData(ImageAnnotation imageAnnotation, BoundingBoxData boundingBoxData) { + double imageWidth = imageAnnotation.getImageMetaData().getImageWidth(); + double imageHeight = imageAnnotation.getImageMetaData().getImageHeight(); + + var bounds = boundingBoxData.getAbsoluteBoundsInImage(imageWidth, imageHeight); + + return new CSVRow( + imageAnnotation.getImageFileName(), + (int) Math.round(imageWidth), + (int) Math.round(imageHeight), + boundingBoxData.getCategoryName(), + (int) Math.round(bounds.getMinX()), + (int) Math.round(bounds.getMinY()), + (int) Math.round(bounds.getMaxX()), + (int) Math.round(bounds.getMaxY())); + } +} diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictionEntry.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictionEntry.java index b11a7f0..36b092e 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictionEntry.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/BoundingBoxPredictionEntry.java @@ -20,43 +20,7 @@ import java.util.List; import java.util.Map; -import java.util.Objects; -public final class BoundingBoxPredictionEntry { - private final Map> categoryToBoundingBoxes; - private final Double score; +public record BoundingBoxPredictionEntry(Map> categoryToBoundingBoxes, Double score) { - public BoundingBoxPredictionEntry(Map> categoryToBoundingBoxes, Double score) { - this.categoryToBoundingBoxes = categoryToBoundingBoxes; - this.score = score; - } - - public Map> categoryToBoundingBoxes() { - return categoryToBoundingBoxes; - } - - public Double score() { - return score; - } - - @Override - public boolean equals(Object obj) { - if(obj == this) { - return true; - } - - if(obj == null || obj.getClass() != this.getClass()) { - return false; - } - - BoundingBoxPredictionEntry that = (BoundingBoxPredictionEntry) obj; - - return Objects.equals(this.categoryToBoundingBoxes, that.categoryToBoundingBoxes) && - Objects.equals(this.score, that.score); - } - - @Override - public int hashCode() { - return Objects.hash(categoryToBoundingBoxes, score); - } } diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/ModelEntry.java b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/ModelEntry.java index 51868fa..2411a38 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/ModelEntry.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/restclients/ModelEntry.java @@ -18,43 +18,5 @@ */ package com.github.mfl28.boundingboxeditor.model.io.restclients; -import java.util.Objects; - -public final class ModelEntry { - private final String modelName; - private final String modelUrl; - - public ModelEntry(String modelName, String modelUrl) { - this.modelName = modelName; - this.modelUrl = modelUrl; - } - - public String modelName() { - return modelName; - } - - public String modelUrl() { - return modelUrl; - } - - @Override - public boolean equals(Object obj) { - if(obj == this) { - return true; - } - - if(obj == null || obj.getClass() != this.getClass()) { - return false; - } - - ModelEntry that = (ModelEntry) obj; - - return Objects.equals(this.modelName, that.modelName) && - Objects.equals(this.modelUrl, that.modelUrl); - } - - @Override - public int hashCode() { - return Objects.hash(modelName, modelUrl); - } +public record ModelEntry(String modelName, String modelUrl) { } diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java b/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java index ba1e65b..148c9b7 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java @@ -51,6 +51,7 @@ class MenuBarView extends MenuBar implements View { private static final String JSON_FORMAT_EXPORT_TEXT = "JSON format..."; private static final String CSV_FORMAT_EXPORT_TEXT = "CSV format..."; private static final String JSON_FORMAT_IMPORT_TEXT = "JSON format..."; + private static final String CSV_FORMAT_IMPORT_TEXT = "CSV format..."; private static final String FILE_MENU_ID = "file-menu"; private static final String FILE_OPEN_FOLDER_MENU_ITEM_ID = "file-open-folder-menu-item"; private static final String FILE_EXPORT_ANNOTATIONS_MENU_ID = "file-export-annotations-menu"; @@ -75,6 +76,7 @@ class MenuBarView extends MenuBar implements View { private static final String ABOUT_TEXT = "_About"; private static final String DOCUMENTATION_MENU_ITEM_ID = "documentation-menu-item"; private static final String ABOUT_MENU_ITEM_ID = "about-menu-item"; + public static final String CSV_IMPORT_MENU_ITEM_ID = "csv-import-menu-item"; private final MenuItem fileOpenFolderItem = new MenuItem(OPEN_FOLDER_TEXT, createIconRegion(OPEN_FOLDER_ICON_ID)); private final Menu fileExportAnnotationsMenu = new Menu(SAVE_TEXT, createIconRegion(SAVE_ICON_ID)); @@ -89,6 +91,7 @@ class MenuBarView extends MenuBar implements View { private final MenuItem pvocImportMenuItem = new MenuItem(PASCAL_VOC_FORMAT_IMPORT_TEXT); private final MenuItem yoloRImportMenuItem = new MenuItem(YOLO_FORMAT_IMPORT_TEXT); private final MenuItem jsonImportMenuItem = new MenuItem(JSON_FORMAT_IMPORT_TEXT); + private final MenuItem csvImportMenuItem = new MenuItem(CSV_FORMAT_IMPORT_TEXT); private final MenuItem fileExitItem = new MenuItem(EXIT_TEXT, createIconRegion(EXIT_ICON_ID)); private final CheckMenuItem viewMaximizeImagesItem = new CheckMenuItem(MAXIMIZE_IMAGES_TEXT); private final CheckMenuItem viewShowImagesPanelItem = new CheckMenuItem(SHOW_IMAGE_FILE_EXPLORER_TEXT); @@ -104,47 +107,56 @@ class MenuBarView extends MenuBar implements View { viewShowImagesPanelItem.setSelected(true); viewMaximizeImagesItem.setSelected(true); - fileExportAnnotationsMenu.getItems().addAll(pvocExportMenuItem, yoloExportMenuItem, jsonExportMenuItem, csvExportMenuItem); + fileExportAnnotationsMenu.getItems().addAll( + pvocExportMenuItem, + yoloExportMenuItem, + jsonExportMenuItem, + csvExportMenuItem); pvocExportMenuItem.setId(PVOC_EXPORT_MENU_ITEM_ID); yoloExportMenuItem.setId(YOLO_EXPORT_MENU_ITEM_ID); jsonExportMenuItem.setId(JSON_EXPORT_MENU_ITEM_ID); csvExportMenuItem.setId(CSV_EXPORT_MENU_ITEM_ID); - fileImportAnnotationsMenu.getItems().addAll(pvocImportMenuItem, - yoloRImportMenuItem, - jsonImportMenuItem); + fileImportAnnotationsMenu.getItems().addAll( + pvocImportMenuItem, + yoloRImportMenuItem, + jsonImportMenuItem, + csvImportMenuItem); pvocImportMenuItem.setId(PVOC_IMPORT_MENU_ITEM_ID); yoloRImportMenuItem.setId(YOLO_IMPORT_MENU_ITEM_ID); jsonImportMenuItem.setId(JSON_IMPORT_MENU_ITEM_ID); + csvImportMenuItem.setId(CSV_IMPORT_MENU_ITEM_ID); } @Override public void connectToController(final Controller controller) { fileOpenFolderItem.setOnAction(action -> - controller.onRegisterOpenImageFolderAction()); + controller.onRegisterOpenImageFolderAction()); pvocExportMenuItem.setOnAction(action -> - controller.onRegisterSaveAnnotationsAction( - ImageAnnotationSaveStrategy.Type.PASCAL_VOC)); + controller.onRegisterSaveAnnotationsAction( + ImageAnnotationSaveStrategy.Type.PASCAL_VOC)); yoloExportMenuItem.setOnAction(action -> - controller.onRegisterSaveAnnotationsAction( - ImageAnnotationSaveStrategy.Type.YOLO)); + controller.onRegisterSaveAnnotationsAction( + ImageAnnotationSaveStrategy.Type.YOLO)); jsonExportMenuItem.setOnAction(action -> - controller.onRegisterSaveAnnotationsAction( - ImageAnnotationSaveStrategy.Type.JSON)); + controller.onRegisterSaveAnnotationsAction( + ImageAnnotationSaveStrategy.Type.JSON)); csvExportMenuItem.setOnAction(action -> - controller.onRegisterSaveAnnotationsAction( - ImageAnnotationSaveStrategy.Type.CSV)); + controller.onRegisterSaveAnnotationsAction( + ImageAnnotationSaveStrategy.Type.CSV)); pvocImportMenuItem.setOnAction(action -> - controller.onRegisterImportAnnotationsAction( - ImageAnnotationLoadStrategy.Type.PASCAL_VOC)); + controller.onRegisterImportAnnotationsAction( + ImageAnnotationLoadStrategy.Type.PASCAL_VOC)); yoloRImportMenuItem.setOnAction(action -> - controller.onRegisterImportAnnotationsAction( - ImageAnnotationLoadStrategy.Type.YOLO)); + controller.onRegisterImportAnnotationsAction( + ImageAnnotationLoadStrategy.Type.YOLO)); jsonImportMenuItem.setOnAction(action -> - controller.onRegisterImportAnnotationsAction( - ImageAnnotationLoadStrategy.Type.JSON)); + controller.onRegisterImportAnnotationsAction( + ImageAnnotationLoadStrategy.Type.JSON)); + csvImportMenuItem.setOnAction(action -> + controller.onRegisterImportAnnotationsAction(ImageAnnotationLoadStrategy.Type.CSV)); fileExitItem.setOnAction(action -> controller.onRegisterExitAction()); settingsMenuItem.setOnAction(action -> controller.onRegisterSettingsAction()); documentationMenuItem.setOnAction(action -> controller.onRegisterDocumentationAction()); diff --git a/src/main/java/com/github/mfl28/boundingboxeditor/utils/MathUtils.java b/src/main/java/com/github/mfl28/boundingboxeditor/utils/MathUtils.java index f2a7759..9bf5772 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/utils/MathUtils.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/utils/MathUtils.java @@ -49,19 +49,8 @@ public static Point2D clampWithinBounds(Point2D point, Bounds bounds) { * @return the clamped point */ public static Point2D clampWithinBounds(double x, double y, Bounds bounds) { - return new Point2D(clamp(x, bounds.getMinX(), bounds.getMaxX()), - clamp(y, bounds.getMinY(), bounds.getMaxY())); - } - - /*** - * Clamps a double value 'val' between the bounds 'min' and 'max'. - * @param val the value to be clamped - * @param min the lower bound - * @param max the upper bound - * @return clamped value - */ - public static double clamp(double val, double min, double max) { - return Math.max(min, Math.min(max, val)); + return new Point2D(Math.clamp(x, bounds.getMinX(), bounds.getMaxX()), + Math.clamp(y, bounds.getMinY(), bounds.getMaxY())); } /** diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index f6f06cf..bed3b1f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -18,15 +18,12 @@ */ module com.github.mfl28.boundingboxeditor { requires javafx.controls; - requires java.desktop; requires org.controlsfx.controls; requires org.apache.commons.collections4; requires org.apache.commons.io; requires com.github.benmanes.caffeine; - requires java.xml; requires org.apache.commons.lang3; requires java.prefs; - requires java.logging; requires com.google.gson; requires jersey.client; requires jersey.common; @@ -39,13 +36,17 @@ requires org.jvnet.mimepull; requires org.locationtech.jts; requires metadata.extractor; - requires com.opencsv; + requires static lombok; + requires com.fasterxml.jackson.dataformat.csv; + requires com.fasterxml.jackson.databind; + requires org.checkerframework.checker.qual; opens com.github.mfl28.boundingboxeditor.model to javafx.base, com.google.gson; opens com.github.mfl28.boundingboxeditor.model.data to javafx.base, com.google.gson; opens com.github.mfl28.boundingboxeditor.model.io to javafx.base, com.google.gson; opens com.github.mfl28.boundingboxeditor.model.io.results to javafx.base, com.google.gson; opens com.github.mfl28.boundingboxeditor.model.io.restclients to javafx.base, com.google.gson; + opens com.github.mfl28.boundingboxeditor.model.io.data to com.fasterxml.jackson.databind; exports com.github.mfl28.boundingboxeditor.model.io.restclients to org.glassfish.hk2.locator; exports com.github.mfl28.boundingboxeditor to javafx.graphics; } \ No newline at end of file diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/BoundingBoxEditorTestBase.java b/src/test/java/com/github/mfl28/boundingboxeditor/BoundingBoxEditorTestBase.java index 021fd44..ce875cb 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/BoundingBoxEditorTestBase.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/BoundingBoxEditorTestBase.java @@ -82,12 +82,12 @@ public class BoundingBoxEditorTestBase { private static final Path SCREENSHOT_PATH = Paths.get("").toAbsolutePath().resolve( "build/test-screenshots/"); private static final String FULL_SCREEN_TESTS_SYSTEM_PROPERTY_NAME = "fullScreenTests"; - protected static int TIMEOUT_DURATION_IN_SEC = 30; - 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 static final int TIMEOUT_DURATION_IN_SEC = 30; + protected static final String TEST_IMAGE_FOLDER_PATH_1 = "/testimages/1"; + protected static final String TEST_IMAGE_FOLDER_PATH_2 = "/testimages/2"; + protected static final String TEST_IMAGE_FOLDER_PATH_3 = "/testimages/3"; + protected static final String TEST_IMAGE_FOLDER_PATH_4 = "/testimages/4"; + protected static final String TEST_EXIF_IMAGE_FOLDER_PATH = "/testimages/ExifJpeg"; protected Controller controller; protected MainView mainView; protected Model model; @@ -240,11 +240,10 @@ protected void onStart(Stage stage) { // Set up image screenshot directory: final File screenShotDirectory = SCREENSHOT_PATH.toFile(); - if(!screenShotDirectory.isDirectory()) { - if(!screenShotDirectory.mkdir()) { + if(!screenShotDirectory.isDirectory() && !screenShotDirectory.mkdir()) { throw new RuntimeException("Could not create test-screenshot directory."); } - } + } @AfterEach @@ -569,25 +568,25 @@ private Scene createSceneFromParent(final Parent parent) { /** * Compares two images pixel by pixel. * - * @param image_l the first image. - * @param image_r the second image. + * @param imageL the first image. + * @param imageR the second image. * @return whether the images are both the same or not. */ - public static boolean compareImages(Image image_l, Image image_r) { + public static boolean compareImages(Image imageL, Image imageR) { // The images must be the same size. - if(image_l.getWidth() != image_r.getWidth() || image_l.getHeight() != image_r.getHeight()) { + if(imageL.getWidth() != imageR.getWidth() || imageL.getHeight() != imageR.getHeight()) { return false; } - final PixelReader image_l_reader = image_l.getPixelReader(); - final PixelReader image_r_reader = image_r.getPixelReader(); + final PixelReader imageLReader = imageL.getPixelReader(); + final PixelReader imageRReader = imageR.getPixelReader(); - int width = (int) image_l.getWidth(); - int height = (int) image_r.getHeight(); + int width = (int) imageL.getWidth(); + int height = (int) imageR.getHeight(); for(int y = 0; y < height; ++y) { for(int x = 0; x < width; ++x) { - if(image_l_reader.getArgb(x, y) != image_r_reader.getArgb(x, y)) { + if(imageLReader.getArgb(x, y) != imageRReader.getArgb(x, y)) { return false; } } diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/controller/ControllerTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/controller/ControllerTests.java index 548e9a4..8254ebd 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/controller/ControllerTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/controller/ControllerTests.java @@ -55,7 +55,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import static org.testfx.api.FxAssert.verifyThat; @@ -956,7 +955,7 @@ void onLoadAnnotation_JSON_WhenFileHasMissingCriticalElements_ShouldNotLoadInval final List objectCategories = model.getObjectCategories(); verifyThat(objectCategories, Matchers.hasSize(4), saveScreenshot(testinfo)); - verifyThat(objectCategories.stream().map(ObjectCategory::getName).collect(Collectors.toList()), + verifyThat(objectCategories.stream().map(ObjectCategory::getName).toList(), Matchers.containsInAnyOrder("Car", "Sail", "Surfboard", "Boat"), saveScreenshot(testinfo)); verifyThat(mainView.getCurrentBoundingShapes(), Matchers.hasSize(4), saveScreenshot(testinfo)); @@ -1171,6 +1170,110 @@ void onLoadAnnotation_YOLO_WhenAnnotationWithinYOLOPrecision_ShouldLoadBoundingB verifyThat(model.getObjectCategories(), Matchers.hasSize(1), saveScreenshot(testinfo)); } + @Test + void onExportAnnotation_CSV_WhenPreviouslyImportedAnnotation_ShouldProduceEquivalentOutput(FxRobot robot, + TestInfo testinfo, + @TempDir Path tempDirectory) + throws IOException { + final String referenceAnnotationFilePath = "/testannotations/csv/reference/annotations.csv"; + + 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 referenceAnnotationFile = + new File(Objects.requireNonNull(getClass().getResource(referenceAnnotationFilePath)).getFile()); + + timeOutClickOn(robot, "#file-menu", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutClickOn(robot, "#file-import-annotations-menu", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutMoveTo(robot, "#pvoc-import-menu-item", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutClickOn(robot, "#csv-import-menu-item", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + robot.push(KeyCode.ESCAPE); + + // Load bounding-boxes defined in the reference annotation-file. + Platform.runLater(() -> controller + .initiateAnnotationImport(referenceAnnotationFile, ImageAnnotationLoadStrategy.Type.CSV)); + WaitForAsyncUtils.waitForFxEvents(); + + timeOutAssertServiceSucceeded(controller.getAnnotationImportService(), testinfo); + + // Create temporary folder to save annotations to. + Path actualFile = tempDirectory.resolve("actual.csv"); + + final Map counts = model.getCategoryToAssignedBoundingShapesCountMap(); + Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS, + () -> Objects.equals(counts.get("Boat"), 2) && + Objects.equals(counts.get("Sail"), 2) && + Objects.equals(counts.get("Surfboard"), 3)), + () -> saveScreenshotAndReturnMessage(testinfo, "Correct bounding shape " + + "per-category-counts were not read within " + + TIMEOUT_DURATION_IN_SEC + " sec.")); + + verifyThat(model.getCategoryToAssignedBoundingShapesCountMap().size(), Matchers.equalTo(3), + saveScreenshot(testinfo)); + verifyThat(model.getObjectCategories(), Matchers.hasSize(3), saveScreenshot(testinfo)); + + Assertions.assertDoesNotThrow(() -> WaitForAsyncUtils.waitFor(TIMEOUT_DURATION_IN_SEC, TimeUnit.SECONDS, + () -> mainView.getImageFileListView() + .getSelectionModel() + .getSelectedItem() + .isHasAssignedBoundingShapes() + && mainView.getCurrentBoundingShapes() + .stream() + .filter(viewable -> viewable instanceof BoundingBoxView) + .count() == 3 + && mainView.getCurrentBoundingShapes() + .stream().noneMatch(viewable -> viewable instanceof BoundingPolygonView)), + () -> saveScreenshotAndReturnMessage(testinfo, + "Bounding shape counts did not match " + + "within " + TIMEOUT_DURATION_IN_SEC + + " sec.")); + + // Zoom a bit to change the image-view size. + robot.moveTo(mainView.getEditorImageView()) + .press(KeyCode.SHORTCUT) + .scroll(-30) + .release(KeyCode.SHORTCUT); + + WaitForAsyncUtils.waitForFxEvents(); + verifyThat(mainView.getStatusBar().getCurrentEventMessage(), + Matchers.startsWith("Successfully imported annotations for 3 images in"), saveScreenshot(testinfo)); + + // 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.")); + } + private void userChoosesNoOnAnnotationImportDialogSubtest(FxRobot robot, File annotationFile, TestInfo testinfo) { userChoosesToSaveExistingAnnotationsOnAnnotationImport(robot, annotationFile, testinfo); userChoosesNotToSaveExistingAnnotationsOnAnnotationImport(robot, annotationFile, testinfo); 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 7ccba23..914a43c 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 @@ -75,10 +75,10 @@ void testCSVSaving(@TempDir Path tempDir) throws IOException { String content = Files.readString(destination); assertEquals(""" - "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" + 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 diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java index 04582cf..965e4c3 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonDrawingTests.java @@ -38,7 +38,6 @@ import java.io.File; import java.util.List; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.testfx.api.FxAssert.verifyThat; @@ -267,7 +266,7 @@ void onOpeningNewImageFolder_WhenBoundingPolygonsExist_ShouldResetCorrectly(FxRo Matchers.equalTo(false), saveScreenshot(testinfo)); verifyThat( reloadedBoundingPolygon.getVertexHandles().stream().map(BoundingPolygonView.VertexHandle::getPointIndex) - .collect(Collectors.toList()), + .toList(), Matchers.containsInRelativeOrder(0, 2, 4, 6, 8, 10), saveScreenshot(testinfo)); verifyThat(reloadedBoundingPolygon.isEditing(), Matchers.equalTo(false), saveScreenshot(testinfo)); verifyThat(reloadedBoundingPolygon.isSelected(), Matchers.equalTo(true), saveScreenshot(testinfo)); @@ -334,7 +333,7 @@ void onOpeningNewImageFolder_WhenBoundingPolygonsExist_ShouldResetCorrectly(FxRo Matchers.equalTo(false), saveScreenshot(testinfo)); verifyThat( newBoundingPolygonView.getVertexHandles().stream().map(BoundingPolygonView.VertexHandle::getPointIndex) - .collect(Collectors.toList()), + .toList(), Matchers.containsInRelativeOrder(0, 2, 4, 6, 8, 10, 12, 14), saveScreenshot(testinfo)); verifyThat(newBoundingPolygonView.isEditing(), Matchers.equalTo(false), saveScreenshot(testinfo)); verifyThat(newBoundingPolygonView.isSelected(), Matchers.equalTo(true), saveScreenshot(testinfo)); @@ -364,7 +363,7 @@ void onOpeningNewImageFolder_WhenBoundingPolygonsExist_ShouldResetCorrectly(FxRo Matchers.not(Matchers.contains(vertexHandle1, vertexHandle2)), saveScreenshot(testinfo)); verifyThat( newBoundingPolygonView.getVertexHandles().stream().map(BoundingPolygonView.VertexHandle::getPointIndex) - .collect(Collectors.toList()), + .toList(), Matchers.containsInRelativeOrder(0, 2, 4, 6, 8, 10), saveScreenshot(testinfo)); verifyThat(newBoundingPolygonView.getVertexHandles().stream() .allMatch(BoundingPolygonView.VertexHandle::isEditing), diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/ui/ImageFolderOpenedBasicTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/ui/ImageFolderOpenedBasicTests.java index 5eedb5f..a0296fa 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/ui/ImageFolderOpenedBasicTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/ui/ImageFolderOpenedBasicTests.java @@ -40,6 +40,7 @@ import java.io.File; import java.nio.file.Files; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -226,53 +227,23 @@ private void verifyMenuBarFunctionality(FxRobot robot, TestInfo testinfo) { () -> saveScreenshotAndReturnMessage(testinfo, "Export annotations item not " + "enabled")); - timeOutClickOn(robot, "#file-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#file-export-annotations-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#pvoc-export-menu-item", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - - Stage errorDialogStage = timeOutGetTopModalStage(robot, "Save Error", testinfo); - verifyThat(errorDialogStage, Matchers.notNullValue(), saveScreenshot(testinfo)); - - timeOutClickOnButtonInDialogStage(robot, errorDialogStage, ButtonType.OK, testinfo); - WaitForAsyncUtils.waitForFxEvents(); - - timeOutAssertTopModalStageClosed(robot, "Save Error", testinfo); - - timeOutClickOn(robot, "#file-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#file-export-annotations-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutMoveTo(robot, "#pvoc-export-menu-item", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#yolo-export-menu-item", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - - Stage errorDialogStage2 = timeOutGetTopModalStage(robot, "Save Error", testinfo); - verifyThat(errorDialogStage2, Matchers.notNullValue(), saveScreenshot(testinfo)); - - timeOutClickOnButtonInDialogStage(robot, errorDialogStage2, ButtonType.OK, testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutAssertTopModalStageClosed(robot, "Save Error", testinfo); - - timeOutClickOn(robot, "#file-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#file-export-annotations-menu", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutMoveTo(robot, "#pvoc-export-menu-item", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - timeOutClickOn(robot, "#json-export-menu-item", testinfo); - WaitForAsyncUtils.waitForFxEvents(); - - Stage errorDialogStage3 = timeOutGetTopModalStage(robot, "Save Error", testinfo); - verifyThat(errorDialogStage3, Matchers.notNullValue(), saveScreenshot(testinfo)); - - timeOutClickOnButtonInDialogStage(robot, errorDialogStage3, ButtonType.OK, testinfo); - WaitForAsyncUtils.waitForFxEvents(); - - timeOutAssertTopModalStageClosed(robot, "Save Error", testinfo); + for(var exportItemName : List.of("pvoc", "yolo", "json", "csv")) { + timeOutClickOn(robot, "#file-menu", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutClickOn(robot, "#file-export-annotations-menu", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutMoveTo(robot, "#pvoc-export-menu-item", testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutClickOn(robot, String.format("#%s-export-menu-item", exportItemName), testinfo); + WaitForAsyncUtils.waitForFxEvents(); + + Stage errorDialogStage = timeOutGetTopModalStage(robot, "Save Error", testinfo); + verifyThat(errorDialogStage, Matchers.notNullValue(), saveScreenshot(testinfo)); + + timeOutClickOnButtonInDialogStage(robot, errorDialogStage, ButtonType.OK, testinfo); + WaitForAsyncUtils.waitForFxEvents(); + timeOutAssertTopModalStageClosed(robot, "Save Error", testinfo); + } MenuItem settingsItem = getSubMenuItem(robot, "File", "Settings"); diff --git a/src/test/java/com/github/mfl28/boundingboxeditor/ui/ObjectTreeTests.java b/src/test/java/com/github/mfl28/boundingboxeditor/ui/ObjectTreeTests.java index e7b6b53..9f3b9dd 100644 --- a/src/test/java/com/github/mfl28/boundingboxeditor/ui/ObjectTreeTests.java +++ b/src/test/java/com/github/mfl28/boundingboxeditor/ui/ObjectTreeTests.java @@ -48,7 +48,6 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import static org.mockito.Mockito.verify; import static org.testfx.api.FxAssert.verifyThat; @@ -373,7 +372,7 @@ void onBoundingBoxesDrawnAndInteractedWith_ShouldCorrectlyDisplayTreeItems(FxRob saveScreenshot(testinfo)); verifyThat(changeCategoryDialog.getContentText(), Matchers.equalTo("New category:"), saveScreenshot(testinfo)); verifyThat(model.getObjectCategories(), Matchers.hasSize(2), saveScreenshot(testinfo)); - verifyThat(model.getObjectCategories().stream().map(ObjectCategory::getName).collect(Collectors.toList()), + verifyThat(model.getObjectCategories().stream().map(ObjectCategory::getName).toList(), Matchers.containsInRelativeOrder("Test", "Dummy")); ObjectCategory testCategory = model.getObjectCategories().get(0); diff --git a/src/test/resources/testannotations/csv/reference/annotations.csv b/src/test/resources/testannotations/csv/reference/annotations.csv new file mode 100644 index 0000000..3483ae1 --- /dev/null +++ b/src/test/resources/testannotations/csv/reference/annotations.csv @@ -0,0 +1,8 @@ +filename,width,height,class,xmin,ymin,xmax,ymax +"nico-bhlr-1067059-unsplash.jpg",6000,4000,Surfboard,2976,1006,3750,3674 +"nico-bhlr-1067059-unsplash.jpg",6000,4000,Surfboard,2098,907,2791,3741 +"nico-bhlr-1067059-unsplash.jpg",6000,4000,Surfboard,2810,816,2810,816 +"austin-neill-685084-unsplash.jpg",3854,2033,Boat,1549,95,2183,1662 +"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1552,448,2229,1043 +"austin-neill-685084-unsplash.jpg",3854,2033,Sail,1759,70,1906,573 +"caleb-george-316073-unsplash.jpg",3968,2976,Boat,1579,1367,2568,2001