Added ability to search for an empty frame (#2221)

* Fixed some eslint issues in cvat-core

* Added ability to search empty frames

* Updated version

* Updated changelog

* Fixed issue with track

* Fixed eslint issues
main
Boris Sekachev 5 years ago committed by GitHub
parent b215f04840
commit a5b2229039
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Datumaro] CLI command for dataset equality comparison (<https://github.com/opencv/cvat/pull/1989>)
- [Datumaro] Merging of datasets with different labels (<https://github.com/opencv/cvat/pull/2098>)
- Add FBRS interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/2094>)
- Ability to change default behaviour of previous/next buttons of a player.
It supports regular navigation, searching a frame according to annotations
filters and searching the nearest frame without any annotations (<https://github.com/openvinotoolkit/cvat/pull/2221>)
- MacOS users notes in CONTRIBUTING.md
### Changed

@ -848,6 +848,43 @@
};
}
searchEmpty(frameFrom, frameTo) {
const sign = Math.sign(frameTo - frameFrom);
const predicate = sign > 0
? (frame) => frame <= frameTo
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
if (frame in this.shapes && this.shapes[frame].some((shape) => !shape.removed)) {
continue;
}
if (frame in this.tags && this.tags[frame].some((tag) => !tag.removed)) {
continue;
}
const filteredTracks = this.tracks.filter((track) => !track.removed);
let found = false;
for (const track of filteredTracks) {
const keyframes = track.boundedKeyframes(frame);
const { prev, first } = keyframes;
const last = prev === null ? first : prev;
const lastShape = track.shapes[last];
const isKeyfame = frame in track.shapes;
if (first <= frame && (!lastShape.outside || isKeyfame)) {
found = true;
break;
}
}
if (found) continue;
return frame;
}
return null;
}
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);

@ -932,7 +932,9 @@
}, [this.clientID], frame);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape, undoSource, redoSource) {
_appendShapeActionToHistory(
actionType, frame, undoShape, redoShape, undoSource, redoSource,
) {
this.history.do(actionType, () => {
if (!undoShape) {
delete this.shapes[frame];

@ -122,6 +122,19 @@
);
}
function searchEmptyFrame(session, frameFrom, frameTo) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.searchEmpty(frameFrom, frameTo);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -373,6 +386,7 @@
hasUnsavedChanges,
mergeAnnotations,
searchAnnotations,
searchEmptyFrame,
splitAnnotations,
groupAnnotations,
clearAnnotations,

@ -95,7 +95,9 @@
await serverProxy.server.logout();
};
cvat.server.changePassword.implementation = async (oldPassword, newPassword1, newPassword2) => {
cvat.server.changePassword.implementation = async (
oldPassword, newPassword1, newPassword2,
) => {
await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2);
};
@ -103,7 +105,9 @@
await serverProxy.server.requestPasswordReset(email);
};
cvat.server.resetPassword.implementation = async(newPassword1, newPassword2, uid, token) => {
cvat.server.resetPassword.implementation = async (
newPassword1, newPassword2, uid, token,
) => {
await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token);
};

@ -206,7 +206,9 @@ function build() {
*/
async changePassword(oldPassword, newPassword1, newPassword2) {
const result = await PluginRegistry
.apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2);
.apiWrapper(
cvat.server.changePassword, oldPassword, newPassword1, newPassword2,
);
return result;
},
/**

@ -46,7 +46,7 @@
* @property {string} UNKNOWN 'unknown'
* @readonly
*/
const RQStatus = Object.freeze({
const RQStatus = Object.freeze({
QUEUED: 'queued',
STARTED: 'started',
FINISHED: 'finished',
@ -134,8 +134,8 @@
* @readonly
*/
const Source = Object.freeze({
MANUAL:'manual',
AUTO:'auto',
MANUAL: 'manual',
AUTO: 'auto',
});
/**

@ -76,6 +76,13 @@
return result;
},
async searchEmpty(frameFrom, frameTo) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.searchEmpty,
frameFrom, frameTo);
return result;
},
async select(objectStates, x, y) {
const result = await PluginRegistry
.apiWrapper.call(this,
@ -347,6 +354,18 @@
* @instance
* @async
*/
/**
* Find the nearest empty frame without any annotations
* @method searchEmpty
* @memberof Session.annotations
* @param {integer} from lower bound of a search
* @param {integer} to upper bound of a search
* @returns {integer|null} a frame that contains objects according to the filter
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Select shape under a cursor by using minimal distance
* between a cursor and a shape edge or a shape point
@ -746,6 +765,7 @@
group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
import: Object.getPrototypeOf(this).annotations.import.bind(this),
@ -1318,6 +1338,7 @@
group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
searchEmpty: Object.getPrototypeOf(this).annotations.searchEmpty.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
import: Object.getPrototypeOf(this).annotations.import.bind(this),
@ -1411,6 +1432,7 @@
saveAnnotations,
hasUnsavedChanges,
searchAnnotations,
searchEmptyFrame,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
@ -1537,6 +1559,29 @@
return result;
};
Job.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) {
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < this.startFrame || frameFrom > this.stopFrame) {
throw new ArgumentError(
'The start frame is out of the job',
);
}
if (frameTo < this.startFrame || frameTo > this.stopFrame) {
throw new ArgumentError(
'The stop frame is out of the job',
);
}
const result = searchEmptyFrame(this, frameFrom, frameTo);
return result;
};
Job.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate);
return result;
@ -1807,6 +1852,29 @@
return result;
};
Task.prototype.annotations.searchEmpty.implementation = function (frameFrom, frameTo) {
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < 0 || frameFrom >= this.size) {
throw new ArgumentError(
'The start frame is out of the task',
);
}
if (frameTo < 0 || frameTo >= this.size) {
throw new ArgumentError(
'The stop frame is out of the task',
);
}
const result = searchEmptyFrame(this, frameFrom, frameTo);
return result;
};
Task.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate);
return result;

@ -152,7 +152,7 @@
* @readonly
* @instance
*/
get: () => !data.email_verification_required,
get: () => !data.email_verification_required,
},
}));
}

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.9.7",
"version": "1.9.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.9.7",
"version": "1.9.8",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {

@ -186,6 +186,7 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
SEARCH_EMPTY_FRAME_FAILED = 'SEARCH_EMPTY_FRAME_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
@ -1331,6 +1332,28 @@ export function searchAnnotationsAsync(
};
}
export function searchEmptyFrameAsync(
sessionInstance: any,
frameFrom: number,
frameTo: number,
): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const frame = await sessionInstance.annotations.searchEmpty(frameFrom, frameTo);
if (frame !== null) {
dispatch(changeFrameAsync(frame));
}
} catch (error) {
dispatch({
type: AnnotationActionTypes.SEARCH_EMPTY_FRAME_FAILED,
payload: {
error,
},
});
}
};
}
export function pasteShapeAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<path d="M769 534.975L359.2 935.5a51.45 51.45 0 01-71.575 0l-47.8-46.7a48.675 48.675 0 01-.075-69.875L564.5 500 239.75 181.075a48.675 48.675 0 01.075-69.875l47.8-46.7a51.45 51.45 0 0171.575 0L769 465c19.75 19.325 19.75 50.625 0 69.95z"></path><path d="M638.573 908.837l-39.65 54.473c-7.837 10.774-5.464 25.855 5.307 33.693a24.054 24.054 0 0014.174 4.62c7.461 0 14.807-3.442 19.526-9.92l39.963-54.905c26.238 13.988 56.163 21.948 87.925 21.948 103.429 0 187.572-84.147 187.572-187.579 0-52.73-21.899-100.409-57.034-134.52l49.744-68.34c7.838-10.775 5.462-25.853-5.303-33.694-10.8-7.844-25.872-5.448-33.69 5.307l-49.374 67.835c-27.195-15.362-58.539-24.166-91.925-24.166-103.432 0-187.578 84.146-187.578 187.575 0 54.374 23.265 103.386 60.343 137.673zm266.578-137.673c0 76.837-62.502 139.346-139.346 139.346-21.143 0-41.122-4.874-59.087-13.329l160.806-220.938c23.268 24.908 37.627 58.238 37.627 94.921zm-139.343-139.34c22.773 0 44.23 5.604 63.225 15.333L667.19 869.514c-25.156-25.222-40.735-59.997-40.735-98.343.01-76.838 62.515-139.346 139.352-139.346z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<path d="M769 534.975L359.2 935.5a51.45 51.45 0 01-71.575 0l-47.8-46.7a48.675 48.675 0 01-.075-69.875L564.5 500 239.75 181.075a48.675 48.675 0 01.075-69.875l47.8-46.7a51.45 51.45 0 0171.575 0L769 465c19.75 19.325 19.75 50.625 0 69.95z"></path><path d="M958.313 590H607.05a16.5 16.5 0 00-14.707 8.96 16.732 16.732 0 001.3 17.415l128.688 181.281c.043.063.09.121.133.184a36.769 36.769 0 017.219 21.816v147.797a16.429 16.429 0 0016.433 16.535c2.227 0 4.426-.445 6.48-1.297l72.313-27.574c6.48-1.976 10.781-8.09 10.781-15.453V819.656a36.774 36.774 0 017.215-21.816c.043-.063.09-.121.133-.184l128.684-181.289a16.717 16.717 0 001.3-17.406 16.502 16.502 0 00-14.71-8.961zM826.78 785.992a56.931 56.931 0 00-11.097 33.664v117.578l-66 25.164V819.656a56.909 56.909 0 00-11.102-33.664L613.648 610h338.07zm0 0" fill="#000"></path>
</svg>

After

Width:  |  Height:  |  Size: 952 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<path d="M239.825 534.975l409.8 400.525c19.75 19.325 51.825 19.325 71.575 0l47.8-46.7c19.75-19.3 19.75-50.55.075-69.875L444.325 500l324.75-318.925A48.675 48.675 0 00769 111.2l-47.8-46.7a51.45 51.45 0 00-71.575 0L239.825 465a48.675 48.675 0 000 69.95z"></path><path d="M118.573 908.837l-39.65 54.473c-7.837 10.774-5.464 25.855 5.307 33.693a24.054 24.054 0 0014.174 4.62c7.461 0 14.807-3.442 19.526-9.92l39.963-54.905c26.238 13.988 56.162 21.948 87.925 21.948 103.429 0 187.572-84.147 187.572-187.579 0-52.73-21.899-100.409-57.034-134.52l49.744-68.34c7.838-10.775 5.462-25.853-5.303-33.694-10.8-7.844-25.872-5.448-33.69 5.307l-49.374 67.835c-27.195-15.362-58.539-24.166-91.925-24.166-103.432 0-187.579 84.146-187.579 187.575 0 54.374 23.265 103.386 60.344 137.673zm266.578-137.673c0 76.837-62.502 139.346-139.346 139.346-21.143 0-41.122-4.874-59.087-13.329l160.806-220.938c23.268 24.908 37.627 58.238 37.627 94.921zm-139.343-139.34c22.773 0 44.23 5.604 63.225 15.333L147.19 869.514c-25.156-25.222-40.735-59.997-40.735-98.343.01-76.838 62.515-139.346 139.352-139.346z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<path d="M239.825 534.975l409.8 400.525c19.75 19.325 51.825 19.325 71.575 0l47.8-46.7c19.75-19.3 19.75-50.55.075-69.875L444.325 500l324.75-318.925A48.675 48.675 0 00769 111.2l-47.8-46.7a51.45 51.45 0 00-71.575 0L239.825 465a48.675 48.675 0 000 69.95z"></path><path d="M408.313 590H57.05a16.5 16.5 0 00-14.707 8.96 16.732 16.732 0 001.3 17.415l128.688 181.281c.043.063.09.121.133.184a36.769 36.769 0 017.219 21.816v147.797a16.429 16.429 0 0016.433 16.535c2.227 0 4.426-.445 6.48-1.297l72.313-27.574c6.48-1.976 10.781-8.09 10.781-15.453V819.656a36.774 36.774 0 017.215-21.816c.043-.063.09-.121.133-.184l128.684-181.289a16.717 16.717 0 001.3-17.406 16.502 16.502 0 00-14.71-8.961zM276.78 785.992a56.931 56.931 0 00-11.097 33.664v117.578l-66 25.164V819.656a56.909 56.909 0 00-11.102-33.664L63.648 610h338.07zm0 0" fill="#000"></path>
</svg>

After

Width:  |  Height:  |  Size: 970 B

@ -300,3 +300,20 @@
}
}
}
.cvat-player-previous-inlined-button,
.cvat-player-next-inlined-button,
.cvat-player-previous-filtered-inlined-button,
.cvat-player-next-filtered-inlined-button,
.cvat-player-previous-empty-inlined-button,
.cvat-player-next-empty-inlined-button {
color: $player-buttons-color;
&:not(:first-child) {
margin-left: 12px;
}
> svg {
transform: scale(1.8);
}
}

@ -7,14 +7,19 @@ import React from 'react';
import { Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import Popover from 'antd/lib/popover';
import {
FirstIcon,
BackJumpIcon,
PreviousIcon,
PreviousFilteredIcon,
PreviousEmptyIcon,
PlayIcon,
PauseIcon,
NextIcon,
NextFilteredIcon,
NextEmptyIcon,
ForwardJumpIcon,
LastIcon,
} from 'icons';
@ -26,6 +31,8 @@ interface Props {
previousFrameShortcut: string;
forwardShortcut: string;
backwardShortcut: string;
prevButtonType: string;
nextButtonType: string;
onSwitchPlay(): void;
onPrevFrame(): void;
onNextFrame(): void;
@ -33,6 +40,8 @@ interface Props {
onBackward(): void;
onFirstFrame(): void;
onLastFrame(): void;
setPrevButton(type: 'regular' | 'filtered' | 'empty'): void;
setNextButton(type: 'regular' | 'filtered' | 'empty'): void;
}
function PlayerButtons(props: Props): JSX.Element {
@ -43,6 +52,8 @@ function PlayerButtons(props: Props): JSX.Element {
previousFrameShortcut,
forwardShortcut,
backwardShortcut,
prevButtonType,
nextButtonType,
onSwitchPlay,
onPrevFrame,
onNextFrame,
@ -50,8 +61,37 @@ function PlayerButtons(props: Props): JSX.Element {
onBackward,
onFirstFrame,
onLastFrame,
setPrevButton,
setNextButton,
} = props;
const prevRegularText = 'Go back';
const prevFilteredText = 'Go back with a filter';
const prevEmptyText = 'Go back to an empty frame';
const nextRegularText = 'Go next';
const nextFilteredText = 'Go next with a filter';
const nextEmptyText = 'Go next to an empty frame';
let prevButton = <Icon className='cvat-player-previous-button' component={PreviousIcon} onClick={onPrevFrame} />;
let prevButtonTooltipMessage = prevRegularText;
if (prevButtonType === 'filtered') {
prevButton = <Icon className='cvat-player-previous-button' component={PreviousFilteredIcon} onClick={onPrevFrame} />;
prevButtonTooltipMessage = prevFilteredText;
} else if (prevButtonType === 'empty') {
prevButton = <Icon className='cvat-player-previous-button' component={PreviousEmptyIcon} onClick={onPrevFrame} />;
prevButtonTooltipMessage = prevEmptyText;
}
let nextButton = <Icon className='cvat-player-next-button' component={NextIcon} onClick={onNextFrame} />;
let nextButtonTooltipMessage = nextRegularText;
if (nextButtonType === 'filtered') {
nextButton = <Icon className='cvat-player-previous-button' component={NextFilteredIcon} onClick={onNextFrame} />;
nextButtonTooltipMessage = nextFilteredText;
} else if (nextButtonType === 'empty') {
nextButton = <Icon className='cvat-player-previous-button' component={NextEmptyIcon} onClick={onNextFrame} />;
nextButtonTooltipMessage = nextEmptyText;
}
return (
<Col className='cvat-player-buttons'>
<Tooltip title='Go to the first frame' mouseLeaveDelay={0}>
@ -60,9 +100,45 @@ function PlayerButtons(props: Props): JSX.Element {
<Tooltip title={`Go back with a step ${backwardShortcut}`} mouseLeaveDelay={0}>
<Icon className='cvat-player-backward-button' component={BackJumpIcon} onClick={onBackward} />
</Tooltip>
<Tooltip title={`Go back ${previousFrameShortcut}`} mouseLeaveDelay={0}>
<Icon className='cvat-player-previous-button' component={PreviousIcon} onClick={onPrevFrame} />
</Tooltip>
<Popover
trigger='contextMenu'
placement='bottom'
content={(
<>
<Tooltip title={`${prevRegularText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-previous-inlined-button'
component={PreviousIcon}
onClick={() => {
setPrevButton('regular');
}}
/>
</Tooltip>
<Tooltip title={`${prevFilteredText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-previous-filtered-inlined-button'
component={PreviousFilteredIcon}
onClick={() => {
setPrevButton('filtered');
}}
/>
</Tooltip>
<Tooltip title={`${prevEmptyText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-previous-empty-inlined-button'
component={PreviousEmptyIcon}
onClick={() => {
setPrevButton('empty');
}}
/>
</Tooltip>
</>
)}
>
<Tooltip placement='top' mouseLeaveDelay={0} title={`${prevButtonTooltipMessage} ${previousFrameShortcut}`}>
{prevButton}
</Tooltip>
</Popover>
{!playing
? (
@ -84,9 +160,45 @@ function PlayerButtons(props: Props): JSX.Element {
</Tooltip>
)}
<Tooltip title={`Go next ${nextFrameShortcut}`} mouseLeaveDelay={0}>
<Icon className='cvat-player-next-button' component={NextIcon} onClick={onNextFrame} />
</Tooltip>
<Popover
trigger='contextMenu'
placement='bottom'
content={(
<>
<Tooltip title={`${nextRegularText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-next-inlined-button'
component={NextIcon}
onClick={() => {
setNextButton('regular');
}}
/>
</Tooltip>
<Tooltip title={`${nextFilteredText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-next-filtered-inlined-button'
component={NextFilteredIcon}
onClick={() => {
setNextButton('filtered');
}}
/>
</Tooltip>
<Tooltip title={`${nextEmptyText}`} mouseLeaveDelay={0}>
<Icon
className='cvat-player-next-empty-inlined-button'
component={NextEmptyIcon}
onClick={() => {
setNextButton('empty');
}}
/>
</Tooltip>
</>
)}
>
<Tooltip placement='top' mouseLeaveDelay={0} title={`${nextButtonTooltipMessage} ${nextFrameShortcut}`}>
{nextButton}
</Tooltip>
</Popover>
<Tooltip title={`Go next with a step ${forwardShortcut}`} mouseLeaveDelay={0}>
<Icon className='cvat-player-forward-button' component={ForwardJumpIcon} onClick={onForward} />
</Tooltip>

@ -34,6 +34,8 @@ interface Props {
previousFrameShortcut: string;
forwardShortcut: string;
backwardShortcut: string;
prevButtonType: string;
nextButtonType: string;
focusFrameInputShortcut: string;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void;
@ -45,6 +47,8 @@ interface Props {
onBackward(): void;
onFirstFrame(): void;
onLastFrame(): void;
setPrevButtonType(type: 'regular' | 'filtered' | 'empty'): void;
setNextButtonType(type: 'regular' | 'filtered' | 'empty'): void;
onSliderChange(value: SliderValue): void;
onInputChange(value: number): void;
onURLIconClick(): void;
@ -73,6 +77,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
previousFrameShortcut,
forwardShortcut,
backwardShortcut,
prevButtonType,
nextButtonType,
focusFrameInputShortcut,
showStatistics,
changeWorkspace,
@ -84,6 +90,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
onBackward,
onFirstFrame,
onLastFrame,
setPrevButtonType,
setNextButtonType,
onSliderChange,
onInputChange,
onURLIconClick,
@ -114,6 +122,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
previousFrameShortcut={previousFrameShortcut}
forwardShortcut={forwardShortcut}
backwardShortcut={backwardShortcut}
prevButtonType={prevButtonType}
nextButtonType={nextButtonType}
onPrevFrame={onPrevFrame}
onNextFrame={onNextFrame}
onForward={onForward}
@ -121,6 +131,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
onFirstFrame={onFirstFrame}
onLastFrame={onLastFrame}
onSwitchPlay={onSwitchPlay}
setPrevButton={setPrevButtonType}
setNextButton={setNextButtonType}
/>
<PlayerNavigation
startFrame={startFrame}

@ -20,6 +20,7 @@ import {
undoActionAsync,
redoActionAsync,
searchAnnotationsAsync,
searchEmptyFrameAsync,
changeWorkspace as changeWorkspaceAction,
activateObject,
} from 'actions/annotation-actions';
@ -56,7 +57,8 @@ interface DispatchToProps {
showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void;
searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void;
changeWorkspace(workspace: Workspace): void;
}
@ -146,9 +148,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void {
dispatch(searchEmptyFrameAsync(sessionInstance, frameFrom, frameTo));
},
changeWorkspace(workspace: Workspace): void {
dispatch(activateObject(null, null));
dispatch(changeWorkspaceAction(workspace));
@ -156,8 +161,13 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
interface State {
prevButtonType: 'regular' | 'filtered' | 'empty';
nextButtonType: 'regular' | 'filtered' | 'empty';
}
type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> {
class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
private inputFrameRef: React.RefObject<InputNumber>;
private autoSaveInterval: number | undefined;
private unblock: any;
@ -165,15 +175,14 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
this.inputFrameRef = React.createRef<InputNumber>();
this.state = {
prevButtonType: 'regular',
nextButtonType: 'regular',
};
}
public componentDidMount(): void {
const {
autoSaveInterval,
history,
jobInstance,
} = this.props;
const { autoSaveInterval, history, jobInstance } = this.props;
this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval);
this.unblock = history.block((location: any) => {
@ -273,10 +282,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
};
private showStatistics = (): void => {
const {
jobInstance,
showStatistics,
} = this.props;
const { jobInstance, showStatistics } = this.props;
showStatistics(jobInstance);
};
@ -333,12 +339,16 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
};
private onPrevFrame = (): void => {
const { prevButtonType } = this.state;
const {
frameNumber,
jobInstance,
playing,
onSwitchPlay,
searchAnnotations,
searchEmptyFrame,
} = this.props;
const { startFrame } = jobInstance;
const newFrame = Math
.max(jobInstance.startFrame, frameNumber - 1);
@ -346,17 +356,27 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
if (prevButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (prevButtonType === 'filtered') {
searchAnnotations(jobInstance, frameNumber - 1, startFrame);
} else {
searchEmptyFrame(jobInstance, frameNumber - 1, startFrame);
}
}
};
private onNextFrame = (): void => {
const { nextButtonType } = this.state;
const {
frameNumber,
jobInstance,
playing,
onSwitchPlay,
searchAnnotations,
searchEmptyFrame,
} = this.props;
const { stopFrame } = jobInstance;
const newFrame = Math
.min(jobInstance.stopFrame, frameNumber + 1);
@ -364,7 +384,13 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
if (nextButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (nextButtonType === 'filtered') {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
} else {
searchEmptyFrame(jobInstance, frameNumber + 1, stopFrame);
}
}
};
@ -404,12 +430,20 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
}
};
private onSaveAnnotation = (): void => {
const {
onSaveAnnotation,
jobInstance,
} = this.props;
private onSetPreviousButtonType = (type: 'regular' | 'filtered' | 'empty'): void => {
this.setState({
prevButtonType: type,
});
};
private onSetNextButtonType = (type: 'regular' | 'filtered' | 'empty'): void => {
this.setState({
nextButtonType: type,
});
};
private onSaveAnnotation = (): void => {
const { onSaveAnnotation, jobInstance } = this.props;
onSaveAnnotation(jobInstance);
};
@ -422,12 +456,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
};
private onChangePlayerInputValue = (value: number): void => {
const {
onSwitchPlay,
playing,
frameNumber,
} = this.props;
const { onSwitchPlay, playing, frameNumber } = this.props;
if (value !== frameNumber) {
if (playing) {
onSwitchPlay(false);
@ -438,10 +467,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onURLIconClick = (): void => {
const { frameNumber } = this.props;
const {
origin,
pathname,
} = window.location;
const { origin, pathname } = window.location;
const url = `${origin}${pathname}?frame=${frameNumber}`;
copy(url);
};
@ -474,6 +500,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
public render(): JSX.Element {
const { nextButtonType, prevButtonType } = this.state;
const {
playing,
saving,
@ -600,6 +627,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
setNextButtonType={this.onSetNextButtonType}
setPrevButtonType={this.onSetPreviousButtonType}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
@ -623,6 +652,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
previousFrameShortcut={normalizedKeyMap.PREV_FRAME}
forwardShortcut={normalizedKeyMap.FORWARD_FRAME}
backwardShortcut={normalizedKeyMap.BACKWARD_FRAME}
nextButtonType={nextButtonType}
prevButtonType={prevButtonType}
focusFrameInputShortcut={normalizedKeyMap.FOCUS_INPUT_FRAME}
onUndoClick={this.undo}
onRedoClick={this.redo}

@ -28,9 +28,13 @@ import SVGRedoIcon from './assets/redo-icon.svg';
import SVGFirstIcon from './assets/first-icon.svg';
import SVGBackJumpIcon from './assets/back-jump-icon.svg';
import SVGPreviousIcon from './assets/previous-icon.svg';
import SVGPreviousFilteredIcon from './assets/previous-filtered-icon.svg';
import SVGPreviousEmptyIcon from './assets/previous-empty-icon.svg';
import SVGPlayIcon from './assets/play-icon.svg';
import SVGPauseIcon from './assets/pause-icon.svg';
import SVGNextIcon from './assets/next-icon.svg';
import SVGNextFilteredIcon from './assets/next-filtered-icon.svg';
import SVGNextEmptyIcon from './assets/next-empty-icon.svg';
import SVGForwardJumpIcon from './assets/forward-jump-icon.svg';
import SVGLastIcon from './assets/last-icon.svg';
import SVGInfoIcon from './assets/info-icon.svg';
@ -117,6 +121,12 @@ export const BackJumpIcon = React.memo(
export const PreviousIcon = React.memo(
(): JSX.Element => <SVGPreviousIcon />,
);
export const PreviousFilteredIcon = React.memo(
(): JSX.Element => <SVGPreviousFilteredIcon />,
);
export const PreviousEmptyIcon = React.memo(
(): JSX.Element => <SVGPreviousEmptyIcon />,
);
export const PauseIcon = React.memo(
(): JSX.Element => <SVGPauseIcon />,
);
@ -126,6 +136,12 @@ export const PlayIcon = React.memo(
export const NextIcon = React.memo(
(): JSX.Element => <SVGNextIcon />,
);
export const NextFilteredIcon = React.memo(
(): JSX.Element => <SVGNextFilteredIcon />,
);
export const NextEmptyIcon = React.memo(
(): JSX.Element => <SVGNextEmptyIcon />,
);
export const ForwardJumpIcon = React.memo(
(): JSX.Element => <SVGForwardJumpIcon />,
);

@ -245,6 +245,7 @@ export interface NotificationsState {
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
searchEmptyFrame: null | ErrorState;
savingLogs: null | ErrorState;
};
boundaries: {

@ -79,6 +79,7 @@ const defaultState: NotificationsState = {
undo: null,
redo: null,
search: null,
searchEmptyFrame: null,
savingLogs: null,
},
boundaries: {
@ -855,6 +856,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.SEARCH_EMPTY_FRAME_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
searchEmptyFrame: {
message: 'Could not search an empty frame',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.SAVE_LOGS_FAILED: {
return {
...state,

Loading…
Cancel
Save