From 183d9e3b1f91ce65ed1476dd9ce4af6eea3d8a88 Mon Sep 17 00:00:00 2001 From: Kai Winter Date: Wed, 28 Feb 2024 21:05:51 +0100 Subject: [PATCH 1/5] CSV export for specific format The CSV file can be used with the tooling mentioned here: https://apple.github.io/turicreate/docs/userguide/object_detection/data-preparation.html --- build.gradle | 2 + .../controller/Controller.java | 9 +++ .../model/io/CSVSaveStrategy.java | 81 +++++++++++++++++++ .../model/io/ImageAnnotationSaveStrategy.java | 8 ++ .../boundingboxeditor/ui/MenuBarView.java | 9 ++- 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java diff --git a/build.gradle b/build.gradle index 4085421..2e2e29b 100644 --- a/build.gradle +++ b/build.gradle @@ -107,6 +107,8 @@ dependencies { // https://mvnrepository.com/artifact/com.drewnoakes/metadata-extractor implementation 'com.drewnoakes:metadata-extractor:2.18.0' + + implementation 'com.opencsv:opencsv:5.9' } 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 6c4d2ba..1016460 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java @@ -136,6 +136,7 @@ public class Controller { private static final String ANNOTATIONS_SAVE_FORMAT_DIALOG_CONTENT = "Annotation format:"; private static final String KEEP_EXISTING_CATEGORIES_DIALOG_TEXT = "Keep existing categories?"; private static final String DEFAULT_JSON_EXPORT_FILENAME = "annotations.json"; + private static final String DEFAULT_CSV_EXPORT_FILENAME = "annotations.csv"; private static final String ANNOTATION_IMPORT_SAVE_EXISTING_DIALOG_CONTENT = "All current annotations are about " + "to be removed. Do you want to save them first?"; private static final String IMAGE_IMPORT_ERROR_ALERT_TITLE = "Image Import Error"; @@ -1197,6 +1198,14 @@ private File getAnnotationSavingDestination(ImageAnnotationSaveStrategy.Type sav "*.json", "*.JSON"), MainView.FileChooserType.SAVE); + } else if(saveFormat.equals(ImageAnnotationSaveStrategy.Type.CSV)) { + destination = MainView.displayFileChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE, stage, + ioMetaData.getDefaultAnnotationSavingDirectory(), + DEFAULT_CSV_EXPORT_FILENAME, + new FileChooser.ExtensionFilter("CSV files", + "*.csv", + "*.CSV"), + MainView.FileChooserType.SAVE); } else { destination = MainView.displayDirectoryChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage, 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 new file mode 100644 index 0000000..ac0152f --- /dev/null +++ b/src/main/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 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.github.mfl28.boundingboxeditor.model.data.*; +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 java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +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"; + + @Override + public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path destination, + DoubleProperty progress) { + final int totalNrAnnotations = annotations.imageAnnotations().size(); + int nrProcessedAnnotations = 0; + + final List errorEntries = new ArrayList<>(); + + try(ICSVWriter writer = new CSVWriterBuilder(Files.newBufferedWriter(destination, StandardCharsets.UTF_8)) + .withSeparator(',') + .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}; + writer.writeNext(header); + for (ImageAnnotation imageAnnotation : annotations.imageAnnotations()) { + for (BoundingShapeData 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 { + throw new ImageAnnotationLoadStrategy.InvalidAnnotationFormatException("CSV can export Rectangles only"); + } + progress.set(1.0 * nrProcessedAnnotations++ / totalNrAnnotations); + } + } + } catch(IOException e) { + errorEntries.add(new IOErrorInfoEntry(destination.getFileName().toString(), e.getMessage())); + } + + return new ImageAnnotationExportResult( + errorEntries.isEmpty() ? totalNrAnnotations : 0, + errorEntries + ); + } +} 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 0b2db7a..5ba6ea1 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 @@ -43,6 +43,8 @@ static ImageAnnotationSaveStrategy createStrategy(Type type) { 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(); } @@ -76,6 +78,12 @@ public String toString() { public String toString() { return "JSON"; } + }, + CSV { + @Override + public String toString() { + return "CSV"; + } } } } 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 86d898e..e53e444 100644 --- a/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java +++ b/src/main/java/com/github/mfl28/boundingboxeditor/ui/MenuBarView.java @@ -49,6 +49,7 @@ class MenuBarView extends MenuBar implements View { private static final String PASCAL_VOC_FORMAT_EXPORT_TEXT = "Pascal-VOC format..."; private static final String YOLO_FORMAT_EXPORT_TEXT = "YOLO format..."; 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 FILE_MENU_ID = "file-menu"; private static final String FILE_OPEN_FOLDER_MENU_ITEM_ID = "file-open-folder-menu-item"; @@ -61,6 +62,7 @@ class MenuBarView extends MenuBar implements View { private static final String PVOC_EXPORT_MENU_ITEM_ID = "pvoc-export-menu-item"; private static final String YOLO_EXPORT_MENU_ITEM_ID = "yolo-export-menu-item"; private static final String JSON_EXPORT_MENU_ITEM_ID = "json-export-menu-item"; + private static final String CSV_EXPORT_MENU_ITEM_ID = "csv-export-menu-item"; private static final String PVOC_IMPORT_MENU_ITEM_ID = "pvoc-import-menu-item"; private static final String YOLO_IMPORT_MENU_ITEM_ID = "yolo-import-menu-item"; private static final String JSON_IMPORT_MENU_ITEM_ID = "json-import-menu-item"; @@ -79,6 +81,7 @@ class MenuBarView extends MenuBar implements View { private final MenuItem pvocExportMenuItem = new MenuItem(PASCAL_VOC_FORMAT_EXPORT_TEXT); private final MenuItem yoloExportMenuItem = new MenuItem(YOLO_FORMAT_EXPORT_TEXT); private final MenuItem jsonExportMenuItem = new MenuItem(JSON_FORMAT_EXPORT_TEXT); + private final MenuItem csvExportMenuItem = new MenuItem(CSV_FORMAT_EXPORT_TEXT); private final MenuItem settingsMenuItem = new MenuItem(SETTINGS_TEXT, createIconRegion(SETTINGS_ICON_ID)); private final Menu fileImportAnnotationsMenu = @@ -101,11 +104,12 @@ class MenuBarView extends MenuBar implements View { viewShowImagesPanelItem.setSelected(true); viewMaximizeImagesItem.setSelected(true); - fileExportAnnotationsMenu.getItems().addAll(pvocExportMenuItem, yoloExportMenuItem, jsonExportMenuItem); + 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, @@ -129,6 +133,9 @@ public void connectToController(final Controller controller) { jsonExportMenuItem.setOnAction(action -> controller.onRegisterSaveAnnotationsAction( ImageAnnotationSaveStrategy.Type.JSON)); + csvExportMenuItem.setOnAction(action -> + controller.onRegisterSaveAnnotationsAction( + ImageAnnotationSaveStrategy.Type.CSV)); pvocImportMenuItem.setOnAction(action -> controller.onRegisterImportAnnotationsAction( ImageAnnotationLoadStrategy.Type.PASCAL_VOC)); From bf11ad2d4c5408b2f49cbd15f22f62daa1b413a6 Mon Sep 17 00:00:00 2001 From: Kai Winter Date: Wed, 28 Feb 2024 21:10:13 +0100 Subject: [PATCH 2/5] Added com.opencsv to module-info --- src/main/java/module-info.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e76970b..748a123 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -39,6 +39,7 @@ requires org.jvnet.mimepull; requires org.locationtech.jts; requires metadata.extractor; + requires com.opencsv; 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; From 1efe76a7f70a480f1291abca2782eedd7f264c38 Mon Sep 17 00:00:00 2001 From: Kai Winter Date: Wed, 13 Mar 2024 20:30:04 +0100 Subject: [PATCH 3/5] CSVSaveStrategy tests --- build.gradle | 2 + .../model/io/CSVSaveStrategy.java | 12 +- .../model/io/CSVSaveStrategyTest.java | 103 ++++++++++++++++++ 3 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java diff --git a/build.gradle b/build.gradle index 2e2e29b..45511ec 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,8 @@ dependencies { // Mockito-Junit https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter testImplementation 'org.mockito:mockito-junit-jupiter:5.6.0' + testImplementation 'com.google.jimfs:jimfs:1.3.0' + // Commons Collections https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 implementation 'org.apache.commons:commons-collections4:4.4' 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 ac0152f..5b0fd83 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 @@ -32,6 +32,11 @@ import java.util.ArrayList; import java.util.List; +/** + * 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"; @@ -40,6 +45,7 @@ public class CSVSaveStrategy implements ImageAnnotationSaveStrategy { 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"; + public static final String UNSUPPORTED_BOUNDING_SHAPE = "CSV can export Rectangles only"; @Override public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path destination, @@ -49,9 +55,7 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de final List errorEntries = new ArrayList<>(); - try(ICSVWriter writer = new CSVWriterBuilder(Files.newBufferedWriter(destination, StandardCharsets.UTF_8)) - .withSeparator(',') - .build()) { + 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}; writer.writeNext(header); for (ImageAnnotation imageAnnotation : annotations.imageAnnotations()) { @@ -64,7 +68,7 @@ public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path de 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 { - throw new ImageAnnotationLoadStrategy.InvalidAnnotationFormatException("CSV can export Rectangles only"); + errorEntries.add(new IOErrorInfoEntry(imageAnnotation.getImageFileName(), UNSUPPORTED_BOUNDING_SHAPE)); } progress.set(1.0 * nrProcessedAnnotations++ / totalNrAnnotations); } 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 new file mode 100644 index 0000000..c8605bc --- /dev/null +++ b/src/test/java/com/github/mfl28/boundingboxeditor/model/io/CSVSaveStrategyTest.java @@ -0,0 +1,103 @@ +package com.github.mfl28.boundingboxeditor.model.io; + +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class CSVSaveStrategyTest { + + /** + * One image with two annotations gets exported to CSV. + */ + @Test + void multipleRectangles() throws IOException { + ObjectCategory objectCategory = new ObjectCategory("category", Color.YELLOW); + + 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); + + 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()); + + 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); + } + + /** + * Two images with each one annotation gets exported. + */ + @Test + void multipleImages() 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 BoundingBoxData(objectCategory, 0,0,0.5,0.5, List.of("tag")))); + + 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")))); + + 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" + """, content); + } +} \ No newline at end of file From 3f45a542171e473cb367dbe5731847653a7e2d12 Mon Sep 17 00:00:00 2001 From: Kai Winter Date: Thu, 14 Mar 2024 22:24:23 +0100 Subject: [PATCH 4/5] Made constant private --- .../mfl28/boundingboxeditor/model/io/CSVSaveStrategy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5b0fd83..b301dbe 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 @@ -45,7 +45,7 @@ public class CSVSaveStrategy implements ImageAnnotationSaveStrategy { 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"; - public static final String UNSUPPORTED_BOUNDING_SHAPE = "CSV can export Rectangles only"; + private static final String UNSUPPORTED_BOUNDING_SHAPE = "CSV can export Rectangles only"; @Override public ImageAnnotationExportResult save(ImageAnnotationData annotations, Path destination, From 917c6f1b0b19f8888bc9d426194926b230a47be6 Mon Sep 17 00:00:00 2001 From: Kai Winter Date: Fri, 15 Mar 2024 20:45:24 +0100 Subject: [PATCH 5/5] Updated license headers --- .../model/io/CSVSaveStrategy.java | 2 +- .../model/io/CSVSaveStrategyTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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 b301dbe..a76a8a9 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Markus Fleischhacker + * Copyright (C) 2024 Markus Fleischhacker * * This file is part of Bounding Box Editor * 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 c8605bc..95ca563 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 @@ -1,3 +1,21 @@ +/* + * Copyright (C) 2024 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.github.mfl28.boundingboxeditor.model.data.*;