Skip to content

Commit

Permalink
Merge pull request #634 from biigle/non-simple-polygon-bug
Browse files Browse the repository at this point in the history
Add checks for (non-)simple polygons
  • Loading branch information
mzur authored Nov 21, 2023
2 parents 32a80ca + fabfa21 commit cd0a8c5
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 1 deletion.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@fortawesome/fontawesome-free": "^5.2.0",
"bootstrap-sass": "^3.3.7",
"echarts": "^5.3.2",
"jsts": "^2.11.0",
"magic-wand-tool": "^1.1.4",
"polymorph-js": "^0.2.4",
"uiv": "^1.2.4",
Expand Down
3 changes: 3 additions & 0 deletions resources/assets/js/annotations/annotatorContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ export default {
dismissCrossOriginError() {
this.crossOriginError = false;
},
handleInvalidPolygon() {
Messages.danger(`Invalid shape. Polygon needs at least 3 non-overlapping vertices.`);
},
},
watch: {
async imageId(id) {
Expand Down
18 changes: 18 additions & 0 deletions resources/assets/js/annotations/components/annotationCanvas.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script>
import * as PolygonValidator from '../ol/PolygonValidator';
import AnnotationTooltip from './annotationCanvas/annotationTooltip';
import AttachLabelInteraction from './annotationCanvas/attachLabelInteraction';
import CanvasSource from '@biigle/ol/source/Canvas';
Expand Down Expand Up @@ -411,6 +412,7 @@ export default {
return this.featureRevisionMap[feature.getId()] !== feature.getRevision();
})
.map((feature) => {
PolygonValidator.simplifyPolygon(feature);
return {
id: feature.getId(),
image_id: feature.get('annotation').image_id,
Expand Down Expand Up @@ -512,6 +514,22 @@ export default {
handleNewFeature(e) {
if (this.hasSelectedLabel) {
let geometry = e.feature.getGeometry();
if (geometry.getType() === 'Polygon') {
if (PolygonValidator.isInvalidPolygon(e.feature)) {
this.$emit('is-invalid-polygon');
// This must be done in the change event handler.
// Not exactly sure why.
this.annotationSource.once('change', () => {
if (this.annotationSource.hasFeature(e.feature)) {
this.annotationSource.removeFeature(e.feature);
}
});
return;
}
PolygonValidator.simplifyPolygon(e.feature);
}
e.feature.set('color', this.selectedLabel.color);
// This callback is called when saving the annotation succeeded or
Expand Down
141 changes: 141 additions & 0 deletions resources/assets/js/annotations/ol/PolygonValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import 'jsts/org/locationtech/jts/monkey'; // This monkey patches jsts prototypes.
import JstsLinearRing from 'jsts/org/locationtech/jts/geom/LinearRing';
import LinearRing from '@biigle/ol/geom/LinearRing';
import LineString from '@biigle/ol/geom/LineString';
import MultiLineString from '@biigle/ol/geom/MultiLineString';
import MultiPoint from '@biigle/ol/geom/MultiPoint';
import MultiPolygon from '@biigle/ol/geom/MultiPolygon';
import OL3Parser from 'jsts/org/locationtech/jts/io/OL3Parser';
import Point from '@biigle/ol/geom/Point';
import Polygon from '@biigle/ol/geom/Polygon';
import Polygonizer from 'jsts/org/locationtech/jts/operation/polygonize/Polygonizer';

/**
* Checks if polygon consists of at least 3 unique points
*
* @param feature containing the polygon
* @returns True if coordinates contains at least 3 unique points, otherwise false
*/
export function isInvalidPolygon(feature) {
let polygon = feature.getGeometry();
let points = polygon.getCoordinates()[0];

return (new Set(points.map(xy => String([xy])))).size < 3;
}

/**
* Makes non-simple polygon simple
*
* @param feature feature containing the (non-simple) polygon
*/
export function simplifyPolygon(feature) {
if (feature.getGeometry().getType() !== 'Polygon') {
throw new Error("Only polygon geometries are supported.");
}

// Check if polygon is self-intersecting
const parser = new OL3Parser();
parser.inject(
Point,
LineString,
LinearRing,
Polygon,
MultiPoint,
MultiLineString,
MultiPolygon
);

// Translate ol geometry into jsts geometry
let jstsPolygon = parser.read(feature.getGeometry());

if (jstsPolygon.isSimple()) {
return feature;
}

let simplePolygons = jstsSimplify(jstsPolygon);
let greatestPolygon = getGreatestPolygon(simplePolygons);
// Convert back to OL geometry.
greatestPolygon = parser.write(greatestPolygon);
feature.getGeometry().setCoordinates(greatestPolygon.getCoordinates());
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
*
*
* Get / create a valid version of the geometry given. If the geometry is a polygon or multi polygon, self intersections /
* inconsistencies are fixed. Otherwise the geometry is returned.
*
* @param geom
* @return a geometry
*/
function jstsSimplify(geom) {
if (geom.isValid()) {
geom.normalize(); // validate does not pick up rings in the wrong order - this will fix that
return geom; // If the polygon is valid just return it
}

let polygonizer = new Polygonizer();
jstsAddPolygon(geom, polygonizer);

let polygons = polygonizer.getPolygons().array
// Remove holes by using the exterior ring.
.map(p => p.getExteriorRing())
// Convert (exterior) LinearRing to Polygon.
.map(r => geom.getFactory().createPolygon(r));

return polygons;
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
*
* Add all line strings from the polygon given to the polygonizer given
*
* @param polygon polygon from which to extract line strings
* @param polygonizer polygonizer
*/
function jstsAddPolygon(polygon, polygonizer) {
jstsAddLineString(polygon.getExteriorRing(), polygonizer);
}

/**
* @author Martin Kirk
*
* @link https://stackoverflow.com/questions/36118883/using-jsts-buffer-to-identify-a-self-intersecting-polygon
* Add the linestring given to the polygonizer
*
* @param linestring line string
* @param polygonizer polygonizer
*/
function jstsAddLineString(lineString, polygonizer) {

if (lineString instanceof JstsLinearRing) {
// LinearRings are treated differently to line strings : we need a LineString NOT a LinearRing
lineString = lineString.getFactory().createLineString(lineString.getCoordinateSequence());
}

// unioning the linestring with the point makes any self intersections explicit.
var point = lineString.getFactory().createPoint(lineString.getCoordinateN(0));
var toAdd = lineString.union(point); //geometry

//Add result to polygonizer
polygonizer.add(toAdd);
}

/**
* Determine polygon with largest area
*
* @param polygonList List of polygons
* @returns Polygon
* **/
function getGreatestPolygon(polygonList) {
let areas = polygonList.map(p => p.getArea());
let idx = areas.indexOf(Math.max(...areas));

return polygonList[idx];
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script>
import * as PolygonValidator from "../../../annotations/ol/PolygonValidator";
import DrawInteraction from '@biigle/ol/interaction/Draw';
import Keyboard from '../../../core/keyboard';
import Styles from '../../../annotations/stores/styles';
import VectorLayer from '@biigle/ol/layer/Vector';
import VectorSource from '@biigle/ol/source/Vector';
/**
* Mixin for the videoScreen component that contains logic for the draw interactions.
*
Expand Down Expand Up @@ -163,6 +163,22 @@ export default {
this.$emit('pending-annotation', null);
},
extendPendingAnnotation(e) {
// Check polygons
if (e.feature.getGeometry().getType() === 'Polygon') {
if (PolygonValidator.isInvalidPolygon(e.feature)) {
// Disallow polygons with less than three non-overlapping points
this.$emit('is-invalid-polygon')
// Wait for this feature to be added to the source, then clear.
this.pendingAnnotationSource.once('addfeature', () => {
this.resetPendingAnnotation();
});
return;
}
// If polygon is self-intersecting, create simple polygon
PolygonValidator.simplifyPolygon(e.feature);
}
let lastFrame = this.pendingAnnotation.frames[this.pendingAnnotation.frames.length - 1];
if (lastFrame === undefined || lastFrame < this.video.currentTime) {
Expand All @@ -175,6 +191,9 @@ export default {
this.autoplayDrawTimeout = window.setTimeout(this.pause, this.autoplayDraw * 1000);
}
} else {
// If the pending annotation (time) is invalid, remove it again.
// We have to wait for this feature to be added to the source to be able
// to remove it.
this.pendingAnnotationSource.once('addfeature', function (e) {
this.removeFeature(e.feature);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ModifyInteraction from '@biigle/ol/interaction/Modify';
import TranslateInteraction from '../../../annotations/ol/TranslateInteraction';
import {shiftKeyOnly as shiftKeyOnlyCondition} from '@biigle/ol/events/condition';
import {singleClick as singleClickCondition} from '@biigle/ol/events/condition';
import * as PolygonValidator from "../../../annotations/ol/PolygonValidator";
const allowedSplitShapes = ['Point', 'Circle', 'Rectangle', 'WholeFrame'];
Expand Down Expand Up @@ -79,6 +80,17 @@ export default {
return this.featureRevisionMap[feature.getId()] !== feature.getRevision();
})
.map((feature) => {
// Check polygons
if (feature.getGeometry().getType() === 'Polygon') {
if (PolygonValidator.isInvalidPolygon(feature)) {
// Disallow polygons with less than three non-overlapping points
this.$emit('is-invalid-polygon')
return;
}
// If polygon is self-intersecting, create simple polygon
PolygonValidator.simplifyPolygon(feature);
}
return {
annotation: feature.get('annotation'),
points: this.getPointsFromGeometry(feature.getGeometry()),
Expand Down
4 changes: 4 additions & 0 deletions resources/assets/js/videos/videoContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export default {
shape_id: this.shapes[pendingAnnotation.shape],
label_id: this.selectedLabel ? this.selectedLabel.id : 0,
});
delete annotation.shape;
return VideoAnnotationApi.save({id: this.videoId}, annotation)
Expand Down Expand Up @@ -624,6 +625,9 @@ export default {
annotation.failTracking();
}
},
handleInvalidPolygon() {
Messages.danger(`Invalid shape. Polygon needs at least 3 non-overlapping vertices.`);
},
},
watch: {
'settings.playbackRate'(rate) {
Expand Down
1 change: 1 addition & 0 deletions resources/views/annotations/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
v-on:delete="handleDeleteAnnotations"
v-on:measuring="fetchImagesArea"
v-on:requires-selected-label="handleRequiresSelectedLabel"
v-on:is-invalid-polygon="handleInvalidPolygon"
ref="canvas"
inline-template>
@include('annotations.show.annotationCanvas')
Expand Down
1 change: 1 addition & 0 deletions resources/views/videos/show/content.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
v-on:attaching-active="handleAttachingLabelActive"
v-on:swapping-active="handleSwappingLabelActive"
v-on:seek="seek"
v-on:is-invalid-polygon="handleInvalidPolygon"
></video-screen>
<video-timeline
ref="videoTimeline"
Expand Down

0 comments on commit cd0a8c5

Please sign in to comment.