Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Video single frame annotation #1019

Merged
merged 17 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ import Styles from '../../stores/styles';
import { shiftKeyOnly } from '@biigle/ol/events/condition';
import snapInteraction from '../../snapInteraction.vue';
import { Point } from '@biigle/ol/geom';
import * as preventDoubleclick from '../../../prevent-doubleclick';


function computeDistance(point1, point2) {
let p1=point1.getCoordinates();
let p2=point2.getCoordinates();
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
}

/**
* Mixin for the annotationCanvas component that contains logic for the draw interactions.
*
Expand All @@ -21,9 +16,6 @@ function computeDistance(point1, point2) {

let drawInteraction;

const POINT_CLICK_COOLDOWN = 400;
const POINT_CLICK_DISTANCE = 5;

// Custom OpenLayers freehandCondition that is true if a pen is used for input or
// if Shift is pressed otherwise.
let penOrShift = function (mapBrowserEvent) {
Expand Down Expand Up @@ -142,8 +134,8 @@ export default {
}
},
isPointDoubleClick(e) {
return new Date().getTime() - this.lastDrawnPointTime < POINT_CLICK_COOLDOWN
&& computeDistance(this.lastDrawnPoint,e.feature.getGeometry()) < POINT_CLICK_DISTANCE;
return new Date().getTime() - this.lastDrawnPointTime < preventDoubleclick.POINT_CLICK_COOLDOWN
&& preventDoubleclick.computeDistance(this.lastDrawnPoint,e.feature.getGeometry()) < preventDoubleclick.POINT_CLICK_DISTANCE;
},
},
watch: {
Expand Down
18 changes: 18 additions & 0 deletions resources/assets/js/prevent-doubleclick.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Compute the Euclidean distance between two points.
*
* @param Object point1 - The first point with getCoordinates() method.
* @param Object point2 - The second point with getCoordinates() method.
* @returns number - The computed distance between the two points.
*/

const POINT_CLICK_COOLDOWN = 400;
const POINT_CLICK_DISTANCE = 5;

let computeDistance = function (point1, point2) {
let p1 = point1.getCoordinates();
let p2 = point2.getCoordinates();
return Math.sqrt(Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2));
};

export { computeDistance, POINT_CLICK_COOLDOWN, POINT_CLICK_DISTANCE };
12 changes: 12 additions & 0 deletions resources/assets/js/videos/components/settingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default {
'enableJumpByFrame',
'jumpStep',
'muteVideo',
'singleAnnotation',
],
annotationOpacity: 1,
showMinimap: true,
Expand All @@ -38,6 +39,7 @@ export default {
showThumbnailPreview: true,
enableJumpByFrame: false,
muteVideo: true,
singleAnnotation: false,
};
},
computed: {
Expand Down Expand Up @@ -88,6 +90,12 @@ export default {
handleUnmuteVideo() {
this.muteVideo = false;
},
handleSingleAnnotation() {
this.singleAnnotation = true;
},
handleDisableSingleAnnotation() {
this.singleAnnotation = false;
},
toggleAnnotationOpacity() {
if (this.annotationOpacity > 0) {
this.annotationOpacity = 0;
Expand Down Expand Up @@ -148,6 +156,10 @@ export default {
this.$emit('update', 'muteVideo', show);
Settings.set('muteVideo', show);
},
singleAnnotation(show) {
this.$emit('update', 'singleAnnotation', show);
Settings.set('singleAnnotation', show);
},
},
created() {
this.restoreKeys.forEach((key) => {
Expand Down
73 changes: 66 additions & 7 deletions resources/assets/js/videos/components/videoScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,34 @@
<div v-if="canAdd" class="btn-group">
<control-button
icon="icon-point"
title="Start a point annotation 𝗔"
:title="(singleAnnotation ? 'Set a point' : 'Start a point annotation') + ' 𝗔'"
:hover="false"
:open="isDrawingPoint"
:active="isDrawingPoint"
:disabled="hasError"
@click="drawPoint"
>
<control-button
v-if="singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-check"
title="Finish the point annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
@click="finishDrawAnnotation"
></control-button>
<control-button
v-if="singleAnnotation"
icon="fa-project-diagram"
title="Disable the single-frame annotation option to track annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-project-diagram"
title="Finish and track the point annotation"
v-on:click="finishTrackAnnotation"
Expand All @@ -92,14 +106,21 @@
</control-button>
<control-button
icon="icon-rectangle"
title="Start a rectangle annotation 𝗦"
:title="(singleAnnotation ? 'Draw a rectangle' : 'Start a rectangle annotation') + ' 𝗦'"
:hover="false"
:open="isDrawingRectangle"
:active="isDrawingRectangle"
:disabled="hasError"
@click="drawRectangle"
>
<control-button
v-if="singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-check"
title="Finish the rectangle annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
Expand All @@ -108,20 +129,34 @@
</control-button>
<control-button
icon="icon-circle"
title="Start a circle annotation 𝗗"
:title="(singleAnnotation ? 'Draw a circle' : 'Start a circle annotation') + ' 𝗗'"
:hover="false"
:open="isDrawingCircle"
:active="isDrawingCircle"
:disabled="hasError"
@click="drawCircle"
>
<control-button
v-if="singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-check"
title="Finish the circle annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
@click="finishDrawAnnotation"
></control-button>
<control-button
v-if="singleAnnotation"
icon="fa-project-diagram"
title="Disable the single-frame annotation option to track annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-project-diagram"
title="Finish and track the circle annotation"
v-on:click="finishTrackAnnotation"
Expand All @@ -131,14 +166,21 @@
</control-button>
<control-button
icon="icon-linestring"
title="Start a line annotation 𝗙"
:title="(singleAnnotation ? 'Draw a line string' : 'Start a line annotation') + ' 𝗙'"
:hover="false"
:open="isDrawingLineString"
:active="isDrawingLineString"
:disabled="hasError"
@click="drawLineString"
>
<control-button
v-if="singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-check"
title="Finish the line annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
Expand All @@ -147,14 +189,20 @@
</control-button>
<control-button
icon="icon-polygon"
title="Start a polygon annotation 𝗚"
:title="(singleAnnotation ? 'Draw a polygon' : 'Start a polygon annotation') + ' 𝗚'"
:open="isDrawingPolygon"
:active="isDrawingPolygon"
:disabled="hasError"
@click="drawPolygon"
>
<control-button
v-if="isDrawingPolygon || isUsingPolygonBrush"
v-if="(isDrawingPolygon || isUsingPolygonBrush) && singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else-if="isDrawingPolygon || isUsingPolygonBrush"
icon="fa-check"
title="Finish the polygon annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
Expand All @@ -181,14 +229,21 @@
</control-button>
<control-button
icon="icon-wholeframe"
title="Start a whole frame annotation 𝗛"
:title="(singleAnnotation ? 'Create a whole frame annotation' : 'Start a whole frame annotation') + ' 𝗛'"
:hover="false"
:open="isDrawingWholeFrame"
:active="isDrawingWholeFrame"
:disabled="hasError"
@click="drawWholeFrame"
>
<control-button
v-if="singleAnnotation"
icon="fa-check"
title="Disable the single-frame annotation option to create multi-frame annotations"
:disabled="true"
></control-button>
<control-button
v-else
icon="fa-check"
title="Finish the whole frame annotation 𝗘𝗻𝘁𝗲𝗿"
:disabled="cantFinishDrawAnnotation"
Expand Down Expand Up @@ -360,6 +415,10 @@ export default {
type: Boolean,
default: true,
},
singleAnnotation: {
type: Boolean,
default: false,
},
showMousePosition: {
type: Boolean,
default: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@ import VectorLayer from '@biigle/ol/layer/Vector';
import VectorSource from '@biigle/ol/source/Vector';
import snapInteraction from "./snapInteraction.vue";
import { isInvalidShape } from '../../../annotations/utils';
import { Point } from '@biigle/ol/geom';
import * as preventDoubleclick from '../../../prevent-doubleclick';

/**
* Mixin for the videoScreen component that contains logic for the draw interactions.
*
* @type {Object}
*/

export default {
mixins: [snapInteraction],
data() {
return {
pendingAnnotation: {},
autoplayDrawTimeout: null,
drawEnded: true,
lastDrawnPoint: new Point(0, 0),
lastDrawnPointTime: 0,
};
},
props: {
singleAnnotation: {
type: Boolean,
default: false
}
},
computed: {
hasSelectedLabel() {
return !!this.selectedLabel;
Expand Down Expand Up @@ -123,6 +134,9 @@ export default {
if (this.isDrawingWholeFrame) {
this.pendingAnnotation.frames.push(this.video.currentTime);
this.$emit('pending-annotation', this.pendingAnnotation);
if (this.singleAnnotation) {
this.finishDrawAnnotation();
}
} else {
this.drawInteraction = new DrawInteraction({
source: this.pendingAnnotationSource,
Expand Down Expand Up @@ -207,6 +221,24 @@ export default {
window.clearTimeout(this.autoplayDrawTimeout);
this.autoplayDrawTimeout = window.setTimeout(this.pause, this.autoplayDraw * 1000);
}

if (this.singleAnnotation) {
if (this.isDrawingPoint) {
if (this.isPointDoubleClick(e)) {
// The feature is added to the source only after this event
// is handled, so remove has to happen after the addfeature
// event.
this.pendingAnnotationSource.once('addfeature', function (e) {
this.removeFeature(e.feature);
});
this.resetPendingAnnotation(this.pendingAnnotation.shape);
return;
}
this.lastDrawnPointTime = new Date().getTime();
this.lastDrawnPoint = e.feature.getGeometry();
}
this.pendingAnnotationSource.once('addfeature', this.finishDrawAnnotation);
}
} 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
Expand All @@ -217,6 +249,11 @@ export default {
}

this.$emit('pending-annotation', this.pendingAnnotation);

},
isPointDoubleClick(e) {
return new Date().getTime() - this.lastDrawnPointTime < preventDoubleclick.POINT_CLICK_COOLDOWN
&& preventDoubleclick.computeDistance(this.lastDrawnPoint, e.feature.getGeometry()) < preventDoubleclick.POINT_CLICK_DISTANCE;
},
},
created() {
Expand Down
1 change: 1 addition & 0 deletions resources/assets/js/videos/stores/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let defaults = {
enableJumpByFrame: false,
jumpStep: 5.0,
muteVideo: true,
singleAnnotation: false,
};

export default new Settings({
Expand Down
1 change: 1 addition & 0 deletions resources/assets/js/videos/videoContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export default {
showThumbnailPreview: true,
enableJumpByFrame: false,
muteVideo: true,
singleAnnotation: false,
},
openTab: '',
urlParams: {
Expand Down
4 changes: 4 additions & 0 deletions resources/views/manual/tutorials/videos/sidebar.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,9 @@
<p>
The mute video switch enables or disables the audio track of the video.
</p>

<p>
The Single Frame Annotation switch allows you to add annotations with a single click by automatically completing them after the first frame. When enabled, additional controls for finishing and tracking are disabled.
</p>
</div>
@endsection
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 @@ -38,6 +38,7 @@
:selected-label="selectedLabel"
:show-label-tooltip="settings.showLabelTooltip"
:show-minimap="settings.showMinimap"
:single-annotation="settings.singleAnnotation"
:show-mouse-position="settings.showMousePosition"
:enable-jump-by-frame="settings.enableJumpByFrame"
:video="video"
Expand Down
4 changes: 4 additions & 0 deletions resources/views/videos/show/sidebar-settings.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
<div class="sidebar-tab__section">
<power-toggle :active="muteVideo" title-off="Mute video" title-on="Unmute video" v-on:on="handleMuteVideo" v-on:off="handleUnmuteVideo">Mute Video</power-toggle>
</div>

<div class="sidebar-tab__section">
<power-toggle :active="singleAnnotation" title-off="Enable always creating single-frame annotations" title-on="Disable always creating single-frame annotations" v-on:on="handleSingleAnnotation" v-on:off="handleDisableSingleAnnotation">Single-Frame Annotation</power-toggle>
</div>
</div>
</settings-tab>
</sidebar-tab>