Skip to content

Commit e73f4c8

Browse files
authored
Merge pull request #25 from mfl28/feature/add-freehand-polygon-drawing
Add freehand polygon-drawing.
2 parents 01e553d + 9d71b64 commit e73f4c8

21 files changed

+1150
-248
lines changed

build.gradle

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

9898
// https://mvnrepository.com/artifact/jakarta.ws.rs/jakarta.ws.rs-api
9999
implementation 'jakarta.ws.rs:jakarta.ws.rs-api:3.1.0'
100+
101+
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
102+
implementation 'org.locationtech.jts:jts-core:1.19.0'
100103
}
101104

102105
javafx {

src/main/java/com/github/mfl28/boundingboxeditor/controller/Controller.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ public void onRegisterSettingsAction() {
216216
.setDisplayedSettingsFromPredictorConfig(model.getBoundingBoxPredictorConfig());
217217
view.getUiSettingsView()
218218
.setDisplayedSettingsFromUISettingsConfig(view.getUiSettingsConfig());
219+
view.getEditorSettingsView()
220+
.setDisplayedSettingsFromEditorSettingsConfig(view.getEditorSettingsConfig());
219221

220222
view.displaySettingsDialog(this, stage);
221223
}
@@ -250,6 +252,8 @@ public void onRegisterSettingsApplyAction(ActionEvent event, ButtonType buttonTy
250252
.applyDisplayedSettingsToPredictorConfig(model.getBoundingBoxPredictorConfig());
251253
view.getUiSettingsView()
252254
.applyDisplayedSettingsToUISettingsConfig(view.getUiSettingsConfig());
255+
view.getEditorSettingsView()
256+
.applyDisplayedSettingsToEditorSettingsConfig(view.getEditorSettingsConfig());
253257

254258
if(!inferenceWasEnabled && model.getBoundingBoxPredictorConfig().isInferenceEnabled()) {
255259
makeClientAvailable();
@@ -479,8 +483,8 @@ public void onRegisterExitAction() {
479483
* @param event the short-cut key-event
480484
*/
481485
public void onRegisterSceneKeyPressed(KeyEvent event) {
482-
// While the user is drawing a bounding box, all key-events will be ignored.
483-
if(view.getEditorImagePane().isBoundingBoxDrawingInProgress()) {
486+
// While the user is drawing a shape, all key-events will be ignored.
487+
if(view.getEditorImagePane().isDrawingInProgress()) {
484488
event.consume();
485489
return;
486490
}
@@ -582,10 +586,16 @@ public void onRegisterImageViewMouseReleasedEvent(MouseEvent event) {
582586
if(imagePane.isImageFullyLoaded() && event.getButton().equals(MouseButton.PRIMARY)) {
583587
if(event.isControlDown()) {
584588
view.getEditorImageView().setCursor(Cursor.OPEN_HAND);
585-
} else if(view.getObjectCategoryTable().isCategorySelected() &&
586-
imagePane.isBoundingBoxDrawingInProgress()) {
587-
imagePane.constructAndAddNewBoundingBox();
588-
imagePane.setBoundingBoxDrawingInProgress(false);
589+
}
590+
591+
if(view.getObjectCategoryTable().isCategorySelected()) {
592+
if(imagePane.getDrawingMode() == EditorImagePaneView.DrawingMode.BOX
593+
&& imagePane.isBoundingBoxDrawingInProgress()) {
594+
imagePane.finalizeBoundingBox();
595+
} else if(imagePane.getDrawingMode() == EditorImagePaneView.DrawingMode.FREEHAND
596+
&& imagePane.isFreehandDrawingInProgress()) {
597+
imagePane.finalizeFreehandShape();
598+
}
589599
}
590600
}
591601
}
@@ -606,6 +616,8 @@ public void onRegisterImageViewMousePressedEvent(MouseEvent event) {
606616
imagePaneView.initializeBoundingRectangle(event);
607617
} else if(imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.POLYGON) {
608618
imagePaneView.initializeBoundingPolygon(event);
619+
} else if(imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.FREEHAND) {
620+
imagePaneView.initializeBoundingFreehandShape(event);
609621
}
610622
} else if(event.getButton().equals(MouseButton.SECONDARY)
611623
&& imagePaneView.getDrawingMode() == EditorImagePaneView.DrawingMode.POLYGON) {
@@ -733,8 +745,12 @@ private List<Pair<KeyCombination, EventHandler<KeyEvent>>> createKeyCombinationH
733745
event -> view.getEditor().getEditorToolBar().getRectangleModeButton().setSelected(true)),
734746
new Pair<>(KeyCombinations.selectPolygonDrawingMode,
735747
event -> view.getEditor().getEditorToolBar().getPolygonModeButton().setSelected(true)),
748+
new Pair<>(KeyCombinations.selectFreehandDrawingMode,
749+
event -> view.getEditor().getEditorToolBar().getFreehandModeButton().setSelected(true)),
736750
new Pair<>(KeyCombinations.changeSelectedBoundingShapeCategory,
737-
event -> view.initiateCurrentSelectedBoundingBoxCategoryChange())
751+
event -> view.initiateCurrentSelectedBoundingBoxCategoryChange()),
752+
new Pair<>(KeyCombinations.simplifyPolygon,
753+
event -> view.simplifyCurrentSelectedBoundingPolygon())
738754
);
739755
}
740756

@@ -1563,6 +1579,8 @@ public static class KeyCombinations {
15631579
new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN);
15641580
public static final KeyCombination selectPolygonDrawingMode =
15651581
new KeyCodeCombination(KeyCode.P, KeyCombination.SHORTCUT_DOWN);
1582+
public static final KeyCombination selectFreehandDrawingMode =
1583+
new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN);
15661584
public static final KeyCombination removeEditingVerticesWhenBoundingPolygonSelected =
15671585
new KeyCodeCombination(KeyCode.DELETE, KeyCombination.SHIFT_DOWN);
15681586
public static final KeyCombination addVerticesToPolygon =
@@ -1572,6 +1590,9 @@ public static class KeyCombinations {
15721590
public static final KeyCombination hideNonSelectedBoundingShapes =
15731591
new KeyCodeCombination(KeyCode.H, KeyCombination.SHIFT_DOWN);
15741592

1593+
public static final KeyCombination simplifyPolygon =
1594+
new KeyCodeCombination(KeyCode.S, KeyCombination.SHIFT_DOWN);
1595+
15751596
private KeyCombinations() {
15761597
throw new IllegalStateException("Key Combination Class");
15771598
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright (C) 2022 Markus Fleischhacker <[email protected]>
3+
*
4+
* This file is part of Bounding Box Editor
5+
*
6+
* Bounding Box Editor is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* Bounding Box Editor is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with Bounding Box Editor. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
package com.github.mfl28.boundingboxeditor.ui;
20+
21+
import com.github.mfl28.boundingboxeditor.model.data.ObjectCategory;
22+
import javafx.beans.binding.Bindings;
23+
import javafx.beans.property.*;
24+
import javafx.geometry.Bounds;
25+
import javafx.geometry.Rectangle2D;
26+
import javafx.scene.control.Toggle;
27+
import javafx.scene.control.ToggleGroup;
28+
import javafx.scene.paint.Color;
29+
import javafx.scene.shape.*;
30+
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import java.util.Objects;
34+
35+
public class BoundingFreehandShapeView extends Path implements View, Toggle,
36+
BoundingShapeViewable {
37+
private static final String BOUNDING_FREEHAND_SHAPE_ID = "bounding-freehand-shape";
38+
private static final double HIGHLIGHTED_FILL_OPACITY = 0.3;
39+
private static final double SELECTED_FILL_OPACITY = 0.5;
40+
private final BoundingShapeViewData boundingShapeViewData;
41+
42+
public BoundingFreehandShapeView(ObjectCategory category) {
43+
this.boundingShapeViewData = new BoundingShapeViewData(this, category);
44+
45+
setManaged(false);
46+
setFill(Color.TRANSPARENT);
47+
setId(BOUNDING_FREEHAND_SHAPE_ID);
48+
49+
boundingShapeViewData.getNodeGroup().setManaged(false);
50+
boundingShapeViewData.getNodeGroup().setViewOrder(0);
51+
52+
setUpInternalListeners();
53+
}
54+
55+
public List<Double> getPointsInImage() {
56+
List<Double> points = new ArrayList<>((getElements().size() - 1) * 2);
57+
58+
for(PathElement pathElement : getElements()) {
59+
if(pathElement instanceof MoveTo moveToElement) {
60+
points.add(moveToElement.getX());
61+
points.add(moveToElement.getY());
62+
} else if(pathElement instanceof LineTo lineToElement) {
63+
points.add(lineToElement.getX());
64+
points.add(lineToElement.getY());
65+
}
66+
}
67+
68+
return points;
69+
}
70+
71+
@Override
72+
public BoundingShapeViewData getViewData() {
73+
return boundingShapeViewData;
74+
}
75+
76+
@Override
77+
public void autoScaleWithBoundsAndInitialize(ReadOnlyObjectProperty<Bounds> autoScaleBounds, double imageWidth,
78+
double imageHeight) {
79+
autoScaleWithBounds(autoScaleBounds);
80+
}
81+
82+
@Override
83+
public Rectangle2D getRelativeOutlineRectangle() {
84+
return null;
85+
}
86+
87+
@Override
88+
public int hashCode() {
89+
return Objects.hash(boundingShapeViewData, getElements());
90+
}
91+
92+
@Override
93+
public boolean equals(Object obj) {
94+
if(this == obj) {
95+
return true;
96+
}
97+
98+
if(!(obj instanceof BoundingFreehandShapeView other)) {
99+
return false;
100+
}
101+
102+
if(!Objects.equals(boundingShapeViewData, other.boundingShapeViewData) ||
103+
getElements().size() != other.getElements().size()) {
104+
return false;
105+
}
106+
107+
return Objects.equals(getElements(), other.getElements());
108+
}
109+
110+
@Override
111+
public BoundingShapeTreeItem toTreeItem() {
112+
return new BoundingPolygonTreeItem(this);
113+
}
114+
115+
@Override
116+
public ToggleGroup getToggleGroup() {
117+
return boundingShapeViewData.getToggleGroup();
118+
}
119+
120+
@Override
121+
public void setToggleGroup(ToggleGroup toggleGroup) {
122+
boundingShapeViewData.setToggleGroup(toggleGroup);
123+
}
124+
125+
@Override
126+
public ObjectProperty<ToggleGroup> toggleGroupProperty() {
127+
return boundingShapeViewData.toggleGroupProperty();
128+
}
129+
130+
@Override
131+
public boolean isSelected() {
132+
return boundingShapeViewData.isSelected();
133+
}
134+
135+
@Override
136+
public void setSelected(boolean selected) {
137+
boundingShapeViewData.setSelected(selected);
138+
}
139+
140+
@Override
141+
public BooleanProperty selectedProperty() {
142+
return boundingShapeViewData.selectedProperty();
143+
}
144+
145+
public void addMoveTo(double x, double y) {
146+
getElements().add(new MoveTo(x, y));
147+
}
148+
149+
public void addLineTo(double x, double y) {
150+
getElements().add(new LineTo(x, y));
151+
}
152+
153+
void autoScaleWithBounds(ReadOnlyObjectProperty<Bounds> autoScaleBounds) {
154+
boundingShapeViewData.autoScaleBounds().bind(autoScaleBounds);
155+
addAutoScaleListener();
156+
}
157+
158+
private void setUpInternalListeners() {
159+
fillProperty().bind(Bindings.when(selectedProperty())
160+
.then(Bindings.createObjectBinding(
161+
() -> Color.web(strokeProperty().get().toString(), SELECTED_FILL_OPACITY),
162+
strokeProperty()))
163+
.otherwise(Bindings.when(boundingShapeViewData.highlightedProperty())
164+
.then(Bindings.createObjectBinding(() -> Color
165+
.web(strokeProperty().get().toString(),
166+
HIGHLIGHTED_FILL_OPACITY), strokeProperty()))
167+
.otherwise(Color.TRANSPARENT)));
168+
169+
boundingShapeViewData.getSelected().addListener((observable, oldValue, newValue) -> {
170+
if(Boolean.TRUE.equals(newValue)) {
171+
boundingShapeViewData.getHighlighted().set(false);
172+
}
173+
});
174+
}
175+
176+
private void addAutoScaleListener() {
177+
boundingShapeViewData.autoScaleBounds().addListener((observable, oldValue, newValue) -> {
178+
double xScaleFactor = newValue.getWidth() / oldValue.getWidth();
179+
double yScaleFactor = newValue.getHeight() / oldValue.getHeight();
180+
181+
for(PathElement pathElement : getElements()) {
182+
if(pathElement instanceof MoveTo moveToElement) {
183+
moveToElement.setX(newValue.getMinX() + (moveToElement.getX() - oldValue.getMinX()) * xScaleFactor);
184+
moveToElement.setY(newValue.getMinY() + (moveToElement.getY() - oldValue.getMinY()) * yScaleFactor);
185+
} else if(pathElement instanceof LineTo lineToElement) {
186+
lineToElement.setX(newValue.getMinX() + (lineToElement.getX() - oldValue.getMinX()) * xScaleFactor);
187+
lineToElement.setY(newValue.getMinY() + (lineToElement.getY() - oldValue.getMinY()) * yScaleFactor);
188+
}
189+
}
190+
});
191+
}
192+
}

src/main/java/com/github/mfl28/boundingboxeditor/ui/BoundingPolygonTreeItem.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public class BoundingPolygonTreeItem extends BoundingShapeTreeItem {
3232
private static final double TOGGLE_ICON_SIDE_LENGTH = 10;
3333

3434
/**
35-
* Creates a new tree-item representing a {@link BoundingPolygonView} in a {@link ObjectTreeElementCell} that is part of
35+
* Creates a new tree-item representing a polygonal shape in a {@link ObjectTreeElementCell} that is part of
3636
* a {@link ObjectTreeView}.
3737
*
38-
* @param boundingPolygon the {@link BoundingPolygonView} that should be associated with the tree-item
38+
* @param boundingShapeViewable the {@link BoundingShapeViewable} that should be associated with the tree-item
3939
*/
40-
BoundingPolygonTreeItem(BoundingPolygonView boundingPolygon) {
41-
super(new TogglePolygon(TOGGLE_ICON_SIDE_LENGTH), boundingPolygon);
40+
BoundingPolygonTreeItem(BoundingShapeViewable boundingShapeViewable) {
41+
super(new TogglePolygon(TOGGLE_ICON_SIDE_LENGTH), boundingShapeViewable);
4242
setGraphic((TogglePolygon) toggleIcon);
4343

4444
setUpInternalListeners();
@@ -63,7 +63,7 @@ public boolean equals(Object obj) {
6363
}
6464

6565
private void setUpInternalListeners() {
66-
((Shape) toggleIcon).fillProperty().bind(((BoundingPolygonView) getValue()).strokeProperty());
66+
((Shape) toggleIcon).fillProperty().bind(((Shape) getValue()).strokeProperty());
6767

6868
((Shape) toggleIcon).setOnMouseClicked(event -> {
6969
setIconToggledOn(!isIconToggledOn());

0 commit comments

Comments
 (0)