Skip to content

Commit ccf13d5

Browse files
committed
Add JSON format annotation export.
1 parent f3054dc commit ccf13d5

15 files changed

+228
-43
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ dependencies {
5353

5454
// Gradle plugin to use the error-prone compiler https://github.com/tbroyer/gradle-errorprone-plugin
5555
errorprone 'com.google.errorprone:error_prone_core:2.4.0'
56+
57+
// Google GSON https://github.com/google/gson
58+
implementation 'com.google.code.gson:gson:2.8.6'
5659
}
5760

5861
javafx {

src/main/java/boundingboxeditor/controller/Controller.java

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javafx.scene.image.Image;
2424
import javafx.scene.input.*;
2525
import javafx.scene.paint.Color;
26+
import javafx.stage.FileChooser;
2627
import javafx.stage.Stage;
2728

2829
import java.io.File;
@@ -51,7 +52,8 @@ public class Controller {
5152
private static final String PROGRAM_NAME_EXTENSION_SEPARATOR = " - ";
5253
private static final String OPEN_FOLDER_ERROR_DIALOG_TITLE = "Error while opening image folder";
5354
private static final String OPEN_FOLDER_ERROR_DIALOG_HEADER = "The selected folder is not a valid image folder.";
54-
private static final String SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE = "Save image annotations";
55+
private static final String SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE = "Save image annotations to a folder";
56+
private static final String SAVE_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE = "Save image annotations to a file";
5557
private static final String LOAD_IMAGE_FOLDER_ERROR_DIALOG_TITLE = "Error loading image folder";
5658
private static final String LOAD_IMAGE_FOLDER_ERROR_DIALOG_CONTENT = "The chosen folder does not contain any valid image files.";
5759
private static final String CATEGORY_INPUT_ERROR_DIALOG_TITLE = "Category Creation Error";
@@ -89,6 +91,7 @@ public class Controller {
8991
private static final String ANNOTATIONS_SAVE_FORMAT_DIALOG_HEADER = "Choose the format for the saved annotations.";
9092
private static final String ANNOTATIONS_SAVE_FORMAT_DIALOG_CONTENT = "Annotation format: ";
9193
private static final String KEEP_EXISTING_CATEGORIES_DIALOG_TEXT = "Keep existing categories?";
94+
private static final String DEFAULT_JSON_EXPORT_FILENAME = "annotations.json";
9295

9396
private final Stage stage;
9497
private final MainView view = new MainView();
@@ -192,12 +195,11 @@ public void onRegisterSaveAnnotationsAction(ImageAnnotationSaveStrategy.Type sav
192195
return;
193196
}
194197

195-
final File saveDirectory = MainView.displayDirectoryChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage,
196-
currentAnnotationSavingDirectory);
198+
File destination = getAnnotationSavingDestination(saveFormat);
197199

198-
if(saveDirectory != null) {
199-
new AnnotationSaverService(saveDirectory, saveFormat).startAndShowProgressDialog();
200-
currentAnnotationSavingDirectory = saveDirectory;
200+
if(destination != null) {
201+
new AnnotationSaverService(destination, saveFormat).startAndShowProgressDialog();
202+
setCurrentAnnotationSavingDirectory(destination);
201203
}
202204
}
203205

@@ -550,21 +552,29 @@ private void initiateAnnotationSavingWithFormatChoiceAndRunOnSaveSuccess(Runnabl
550552

551553
formatChoice.ifPresent(choice -> {
552554
// Ask for annotation save directory.
553-
final File saveDirectory = MainView.displayDirectoryChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage,
554-
currentAnnotationSavingDirectory);
555+
final File destination = getAnnotationSavingDestination(choice);
555556

556-
if(saveDirectory != null) {
557+
if(destination != null) {
557558
// Save annotations.
558-
AnnotationSaverService annotationSaverService = new AnnotationSaverService(saveDirectory, choice);
559+
AnnotationSaverService annotationSaverService = new AnnotationSaverService(destination, choice);
559560
annotationSaverService.runOnSuccess(() -> {
560-
currentAnnotationSavingDirectory = saveDirectory;
561+
setCurrentAnnotationSavingDirectory(destination);
561562
runnable.run();
562563
});
563564
annotationSaverService.startAndShowProgressDialog();
564565
}
565566
});
566567
}
567568

569+
private void setCurrentAnnotationSavingDirectory(File destination) {
570+
if(destination.isDirectory()) {
571+
currentAnnotationSavingDirectory = destination;
572+
} else if(destination.isFile() && destination.getParentFile().isDirectory()
573+
&& destination.getParentFile().exists()) {
574+
currentAnnotationSavingDirectory = destination.getParentFile();
575+
}
576+
}
577+
568578
private void initiateAnnotationSavingWithFormatChoiceAndRunInAnyCase(Runnable runnable) {
569579
// Ask for annotation save format.
570580
Optional<ImageAnnotationSaveStrategy.Type> formatChoice =
@@ -576,14 +586,13 @@ private void initiateAnnotationSavingWithFormatChoiceAndRunInAnyCase(Runnable ru
576586

577587
formatChoice.ifPresentOrElse(choice -> {
578588
// Ask for annotation save directory.
579-
final File saveDirectory = MainView.displayDirectoryChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage,
580-
currentAnnotationSavingDirectory);
589+
final File destination = getAnnotationSavingDestination(choice);
581590

582-
if(saveDirectory != null) {
591+
if(destination != null) {
583592
// Save annotations.
584-
AnnotationSaverService annotationSaverService = new AnnotationSaverService(saveDirectory, choice);
593+
AnnotationSaverService annotationSaverService = new AnnotationSaverService(destination, choice);
585594
annotationSaverService.runOnSuccess(() -> {
586-
currentAnnotationSavingDirectory = saveDirectory;
595+
setCurrentAnnotationSavingDirectory(destination);
587596
runnable.run();
588597
});
589598
annotationSaverService.startAndShowProgressDialog();
@@ -593,6 +602,21 @@ private void initiateAnnotationSavingWithFormatChoiceAndRunInAnyCase(Runnable ru
593602
}, runnable);
594603
}
595604

605+
private File getAnnotationSavingDestination(ImageAnnotationSaveStrategy.Type saveFormat) {
606+
File destination;
607+
608+
if(saveFormat.equals(ImageAnnotationSaveStrategy.Type.JSON)) {
609+
destination = MainView.displayFileChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_FILE_CHOOSER_TITLE, stage,
610+
currentAnnotationSavingDirectory, DEFAULT_JSON_EXPORT_FILENAME,
611+
new FileChooser.ExtensionFilter("JSON files", "*.json", "*.JSON"));
612+
} else {
613+
destination = MainView.displayDirectoryChooserAndGetChoice(SAVE_IMAGE_ANNOTATIONS_DIRECTORY_CHOOSER_TITLE, stage,
614+
currentAnnotationSavingDirectory);
615+
}
616+
617+
return destination;
618+
}
619+
596620
private void interruptDirectoryWatcher() {
597621
if(directoryWatcher != null && directoryWatcher.isAlive()) {
598622
directoryWatcher.interrupt();
@@ -958,11 +982,11 @@ private KeyCombinations() {
958982
}
959983

960984
class AnnotationSaverService extends Service<IOResult> implements OnSuccessRunner<Runnable>, ProgressShower {
961-
private final File saveDirectory;
985+
private final File destination;
962986
private final ImageAnnotationSaveStrategy.Type saveFormat;
963987

964-
AnnotationSaverService(File saveDirectory, ImageAnnotationSaveStrategy.Type saveFormat) {
965-
this.saveDirectory = saveDirectory;
988+
AnnotationSaverService(File destination, ImageAnnotationSaveStrategy.Type saveFormat) {
989+
this.destination = destination;
966990
this.saveFormat = saveFormat;
967991
setOnSucceeded(successEvent -> defaultOnSucceededHandler());
968992
setOnFailed(failedEvent -> defaultOnFailedHandler());
@@ -992,7 +1016,7 @@ protected IOResult call() throws Exception {
9921016

9931017
saver.progressProperty().addListener((observable, oldValue, newValue) -> updateProgress(newValue.doubleValue(), 1.0));
9941018

995-
return saver.save(model.getImageAnnotationData(), Paths.get(saveDirectory.getPath()));
1019+
return saver.save(model.getImageAnnotationData(), Paths.get(destination.getPath()));
9961020
}
9971021
};
9981022
}

src/main/java/boundingboxeditor/model/ImageMetaData.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package boundingboxeditor.model;
22

3+
import com.google.gson.annotations.SerializedName;
4+
35
import javax.imageio.ImageIO;
46
import javax.imageio.ImageReader;
57
import javax.imageio.stream.ImageInputStream;
@@ -151,8 +153,11 @@ private static ImageDimensions readImageDimensionsFromFile(File imageFile) throw
151153

152154
private static class ImageMetaDataDetails {
153155
private final String folderName;
156+
@SerializedName("width")
154157
private final double imageWidth;
158+
@SerializedName("height")
155159
private final double imageHeight;
160+
@SerializedName("depth")
156161
private final int imageDepth;
157162

158163
ImageMetaDataDetails(String folderName, double imageWidth, double imageHeight, int imageDepth) {

src/main/java/boundingboxeditor/model/io/BoundingBoxData.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import boundingboxeditor.model.ObjectCategory;
55
import boundingboxeditor.ui.BoundingBoxView;
66
import boundingboxeditor.ui.BoundingShapeViewable;
7+
import com.google.gson.annotations.SerializedName;
78
import javafx.geometry.BoundingBox;
89
import javafx.geometry.Bounds;
910

@@ -18,6 +19,7 @@
1819
* @see BoundingShapeData#setParts(List)
1920
*/
2021
public class BoundingBoxData extends BoundingShapeData {
22+
@SerializedName("bndbox")
2123
private final Bounds relativeBoundsInImage;
2224

2325
/**

src/main/java/boundingboxeditor/model/io/BoundingPolygonData.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import boundingboxeditor.model.ObjectCategory;
55
import boundingboxeditor.ui.BoundingPolygonView;
66
import boundingboxeditor.ui.BoundingShapeViewable;
7+
import com.google.gson.annotations.SerializedName;
78

89
import java.util.ArrayList;
910
import java.util.List;
@@ -17,6 +18,7 @@
1718
* @see BoundingShapeData#setParts(List)
1819
*/
1920
public class BoundingPolygonData extends BoundingShapeData {
21+
@SerializedName("polygon")
2022
private final List<Double> relativePointsInImage;
2123

2224
public BoundingPolygonData(ObjectCategory category, List<Double> points, List<String> tags) {

src/main/java/boundingboxeditor/model/io/ImageAnnotation.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package boundingboxeditor.model.io;
22

33
import boundingboxeditor.model.ImageMetaData;
4+
import com.google.gson.annotations.SerializedName;
45

56
import java.util.ArrayList;
67
import java.util.List;
@@ -10,7 +11,9 @@
1011
* There will be at most one ImageAnnotation-object for each loaded image.
1112
*/
1213
public class ImageAnnotation {
14+
@SerializedName("image")
1315
private ImageMetaData imageMetaData;
16+
@SerializedName("objects")
1417
private List<BoundingShapeData> boundingShapeData = new ArrayList<>();
1518

1619
/**

src/main/java/boundingboxeditor/model/io/ImageAnnotationSaveStrategy.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ static ImageAnnotationSaveStrategy createStrategy(Type type) {
2020
return new PVOCSaveStrategy();
2121
} else if(type.equals(Type.YOLO)) {
2222
return new YOLOSaveStrategy();
23+
} else if(type.equals(Type.JSON)) {
24+
return new JSONSaveStrategy();
2325
} else {
2426
throw new InvalidParameterException();
2527
}
@@ -28,12 +30,12 @@ static ImageAnnotationSaveStrategy createStrategy(Type type) {
2830
/**
2931
* Saves image-annotations to the provided folder-path.
3032
*
31-
* @param annotations the collection of image-annotations to save
32-
* @param saveFolderPath the path of the directory to which the annotations will be saved
33-
* @param progress the progress-property that will be updated during the saving-operation
33+
* @param annotations the collection of image-annotations to save
34+
* @param destination the path of the directory to which the annotations will be saved
35+
* @param progress the progress-property that will be updated during the saving-operation
3436
* @return an {@link IOResult} containing information about the finished saving
3537
*/
36-
IOResult save(ImageAnnotationData annotations, Path saveFolderPath, DoubleProperty progress);
38+
IOResult save(ImageAnnotationData annotations, Path destination, DoubleProperty progress);
3739

3840
enum Type {
3941
PASCAL_VOC {
@@ -47,6 +49,12 @@ public String toString() {
4749
public String toString() {
4850
return "YOLO";
4951
}
52+
},
53+
JSON {
54+
@Override
55+
public String toString() {
56+
return "JSON";
57+
}
5058
}
5159
}
5260
}

src/main/java/boundingboxeditor/model/io/ImageAnnotationSaver.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ public ImageAnnotationSaver(ImageAnnotationSaveStrategy.Type strategy) {
2727
/**
2828
* Saves the provided annotation as specified by the wrapped {@link ImageAnnotationSaveStrategy}.
2929
*
30-
* @param annotations the annotations to save
31-
* @param saveFolderPath the path of the destination folder
30+
* @param annotations the annotations to save
31+
* @param destination the path of the destination folder
3232
* @return an {@link IOResult} containing information about the finished saving
3333
*/
34-
public IOResult save(final ImageAnnotationData annotations, final Path saveFolderPath) throws Exception {
35-
return IOOperationTimer.time(() -> saveStrategy.save(annotations, saveFolderPath, progress));
34+
public IOResult save(final ImageAnnotationData annotations, final Path destination) throws Exception {
35+
return IOOperationTimer.time(() -> saveStrategy.save(annotations, destination, progress));
3636
}
3737

3838
/**
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package boundingboxeditor.model.io;
2+
3+
import boundingboxeditor.model.ObjectCategory;
4+
import boundingboxeditor.utils.ColorUtils;
5+
import com.google.gson.*;
6+
import javafx.beans.property.DoubleProperty;
7+
import javafx.geometry.Bounds;
8+
9+
import java.io.BufferedWriter;
10+
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.text.DecimalFormat;
14+
import java.text.DecimalFormatSymbols;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Locale;
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
public class JSONSaveStrategy implements ImageAnnotationSaveStrategy {
21+
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
22+
private static final String OBJECT_CATEGORY_SERIALIZED_NAME = "name";
23+
private static final String OBJECT_COLOR_SERIALIZED_NAME = "color";
24+
private static final String BOUNDS_MIN_X_SERIALIZED_NAME = "minX";
25+
private static final String BOUNDS_MIN_Y_SERIALIZED_NAME = "minY";
26+
private static final String BOUNDS_MAX_X_SERIALIZED_NAME = "maxX";
27+
private static final String BOUNDS_MAX_Y_SERIALIZED_NAME = "maxY";
28+
29+
@Override
30+
public IOResult save(ImageAnnotationData annotations, Path destination, DoubleProperty progress) {
31+
final int totalNrAnnotations = annotations.getImageAnnotations().size();
32+
final AtomicInteger nrProcessedAnnotations = new AtomicInteger(0);
33+
34+
final Gson gson = new GsonBuilder()
35+
.setPrettyPrinting()
36+
.registerTypeAdapter(ImageAnnotationData.class, (JsonSerializer<ImageAnnotationData>) (src, typeOfSrc, context) -> {
37+
JsonArray serializedAnnotations = new JsonArray();
38+
39+
for(ImageAnnotation annotation : src.getImageAnnotations()) {
40+
serializedAnnotations.add(context.serialize(annotation));
41+
progress.set(1.0 * nrProcessedAnnotations.incrementAndGet() / totalNrAnnotations);
42+
}
43+
44+
return serializedAnnotations;
45+
})
46+
.registerTypeAdapter(ObjectCategory.class, (JsonSerializer<ObjectCategory>) (src, typeOfSrc, context) -> {
47+
JsonObject categoryObject = new JsonObject();
48+
categoryObject.add(OBJECT_CATEGORY_SERIALIZED_NAME, context.serialize(src.getName()));
49+
categoryObject.add(OBJECT_COLOR_SERIALIZED_NAME,
50+
context.serialize(ColorUtils.colorToHexString(src.getColor())));
51+
52+
return categoryObject;
53+
})
54+
.registerTypeHierarchyAdapter(Bounds.class, (JsonSerializer<Bounds>) (src, typeOfSrc, context) -> {
55+
JsonObject boundsObject = new JsonObject();
56+
boundsObject.add(BOUNDS_MIN_X_SERIALIZED_NAME, context.serialize(src.getMinX()));
57+
boundsObject.add(BOUNDS_MIN_Y_SERIALIZED_NAME, context.serialize(src.getMinY()));
58+
boundsObject.add(BOUNDS_MAX_X_SERIALIZED_NAME, context.serialize(src.getMaxX()));
59+
boundsObject.add(BOUNDS_MAX_Y_SERIALIZED_NAME, context.serialize(src.getMaxY()));
60+
61+
return boundsObject;
62+
})
63+
.registerTypeAdapter(Double.class, (JsonSerializer<Double>) (src, typeOfSrc, context)
64+
-> new JsonPrimitive(Double.parseDouble(DECIMAL_FORMAT.format(src))))
65+
.create();
66+
67+
final List<IOResult.ErrorInfoEntry> errorEntries = new ArrayList<>();
68+
69+
try(BufferedWriter writer = Files.newBufferedWriter(destination)) {
70+
gson.toJson(annotations, writer);
71+
} catch(IOException e) {
72+
errorEntries.add(new IOResult.ErrorInfoEntry(destination.getFileName().toString(), e.getMessage()));
73+
}
74+
75+
return new IOResult(
76+
IOResult.OperationType.ANNOTATION_SAVING,
77+
errorEntries.isEmpty() ? totalNrAnnotations : 0,
78+
errorEntries
79+
);
80+
}
81+
}

src/main/java/boundingboxeditor/model/io/PVOCSaveStrategy.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ public class PVOCSaveStrategy implements ImageAnnotationSaveStrategy {
5959
private Path saveFolderPath;
6060

6161
@Override
62-
public IOResult save(ImageAnnotationData annotations, Path saveFolderPath, DoubleProperty progress) {
63-
this.saveFolderPath = saveFolderPath;
62+
public IOResult save(ImageAnnotationData annotations, Path destination, DoubleProperty progress) {
63+
this.saveFolderPath = destination;
6464

6565
List<IOResult.ErrorInfoEntry> unParsedFileErrorMessages = Collections.synchronizedList(new ArrayList<>());
6666

0 commit comments

Comments
 (0)