From 2f4bea1c11da9c29527b777d6369939914722b12 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 23 Mar 2020 17:23:46 +0300 Subject: [PATCH 1/2] Task creator validators & help message --- .../advanced-configuration-form.tsx | 120 ++++++++++++++++-- .../create-task-page/create-task-content.tsx | 4 +- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx index b8d85eef2105..7ffcbbc08909 100644 --- a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -30,7 +30,7 @@ export interface AdvancedConfiguration { lfs: boolean; repository?: string; useZipChunks: boolean; - dataChunkSize: number; + dataChunkSize?: number; } type Props = FormComponentProps & { @@ -38,6 +38,52 @@ type Props = FormComponentProps & { installedGit: boolean; }; +function isPositiveInteger(_: any, value: any, callback: any): void { + if (!value) { + callback(); + return; + } + + const intValue = +value; + if (Number.isNaN(intValue) + || !Number.isInteger(intValue) || intValue < 1) { + callback('Value must be a positive integer'); + } + + callback(); +} + +function isNonNegativeInteger(_: any, value: any, callback: any): void { + if (!value) { + callback(); + return; + } + + const intValue = +value; + if (Number.isNaN(intValue) || intValue < 0) { + callback('Value must be a non negative integer'); + } + + callback(); +} + +function isIntegerRange(min: number, max: number, _: any, value: any, callback: any): void { + if (!value) { + callback(); + return; + } + + const intValue = +value; + if (Number.isNaN(intValue) + || !Number.isInteger(intValue) + || intValue < min || intValue > max + ) { + callback(`Value must be an integer [${min}, ${max}]`); + } + + callback(); +} + class AdvancedConfigurationForm extends React.PureComponent { public submit(): Promise { return new Promise((resolve, reject) => { @@ -51,6 +97,16 @@ class AdvancedConfigurationForm extends React.PureComponent { const filteredValues = { ...values }; delete filteredValues.frameStep; + if (values.overlapSize && +values.segmentSize <= +values.overlapSize) { + reject(new Error('Overlap size must be more than segment size')); + } + + if (typeof (values.startFrame) !== 'undefined' && typeof (values.stopFrame) !== 'undefined' + && +values.stopFrame < +values.startFrame + ) { + reject(new Error('Stop frame must be more or equal start frame')); + } + onSubmit({ ...values, frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined, @@ -96,14 +152,14 @@ class AdvancedConfigurationForm extends React.PureComponent { initialValue: 70, rules: [{ required: true, - message: 'This field is required', + message: 'The field is required.', + }, { + validator: isIntegerRange.bind(null, 5, 100), }], })( } />, )} @@ -118,7 +174,11 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Overlap size}> - {form.getFieldDecorator('overlapSize')( + {form.getFieldDecorator('overlapSize', { + rules: [{ + validator: isNonNegativeInteger, + }], + })( , )} @@ -132,7 +192,11 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Segment size}> - {form.getFieldDecorator('segmentSize')( + {form.getFieldDecorator('segmentSize', { + rules: [{ + validator: isPositiveInteger, + }], + })( , )} @@ -145,7 +209,11 @@ class AdvancedConfigurationForm extends React.PureComponent { return ( Start frame}> - {form.getFieldDecorator('startFrame')( + {form.getFieldDecorator('startFrame', { + rules: [{ + validator: isNonNegativeInteger, + }], + })( { return ( Stop frame}> - {form.getFieldDecorator('stopFrame')( + {form.getFieldDecorator('stopFrame', { + rules: [{ + validator: isNonNegativeInteger, + }], + })( { return ( Frame step}> - {form.getFieldDecorator('frameStep')( + {form.getFieldDecorator('frameStep', { + rules: [{ + validator: isPositiveInteger, + }], + })( { return ( Chunk size}> - - {form.getFieldDecorator('dataChunkSize')( + + Defines a number of frames to be packed in + a chunk when send from client to server. + Server defines automatically if empty. +
+ Recommended values: +
+ 1080p or less: 36 +
+ 2k or less: 8 - 16 +
+ 4k or less: 4 - 8 +
+ More: 1 - 4 + + )} + > + {form.getFieldDecorator('dataChunkSize', { + rules: [{ + validator: isPositiveInteger, + }], + })( , )}
diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx index 1432dcd87474..ca81c2d72540 100644 --- a/cvat-ui/src/components/create-task-page/create-task-content.tsx +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -142,10 +142,10 @@ export default class CreateTaskContent extends React.PureComponent }).then((): void => { const { onCreate } = this.props; onCreate(this.state); - }).catch((): void => { + }).catch((error: Error): void => { notification.error({ message: 'Could not create a task', - description: 'Please, check configuration you specified', + description: error.toString(), }); }); }; From 8d6e1c6b12774a65cac20913da50a648abc1f0a2 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Tue, 24 Mar 2020 16:08:30 +0300 Subject: [PATCH 2/2] Added filenames in UI --- cvat-core/src/frames.js | 68 +++++++++++++------ cvat-core/src/session.js | 1 + cvat-ui/src/actions/annotation-actions.ts | 11 ++- .../components/annotation-page/styles.scss | 1 + .../top-bar/player-navigation.tsx | 6 +- .../annotation-page/top-bar/top-bar.tsx | 3 + .../annotation-page/top-bar/top-bar.tsx | 10 ++- cvat-ui/src/reducers/annotation-reducer.ts | 8 ++- cvat-ui/src/reducers/interfaces.ts | 1 + 9 files changed, 83 insertions(+), 26 deletions(-) diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index 2e7df5da0f17..4ad3c17124c2 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -13,7 +13,7 @@ const PluginRegistry = require('./plugins'); const serverProxy = require('./server-proxy'); const { isBrowser, isNode } = require('browser-or-node'); - const { Exception, ArgumentError } = require('./exceptions'); + const { Exception, ArgumentError, DataError } = require('./exceptions'); // This is the frames storage const frameDataCache = {}; @@ -24,8 +24,28 @@ * @hideconstructor */ class FrameData { - constructor(width, height, tid, number, startFrame, stopFrame, decodeForward) { + constructor({ + width, + height, + name, + taskID, + frameNumber, + startFrame, + stopFrame, + decodeForward, + }) { Object.defineProperties(this, Object.freeze({ + /** + * @name filename + * @type {string} + * @memberof module:API.cvat.classes.FrameData + * @readonly + * @instance + */ + filename: { + value: name, + writable: false, + }, /** * @name width * @type {integer} @@ -49,7 +69,7 @@ writable: false, }, tid: { - value: tid, + value: taskID, writable: false, }, /** @@ -60,7 +80,7 @@ * @instance */ number: { - value: number, + value: frameNumber, writable: false, }, startFrame: { @@ -297,7 +317,7 @@ }); }; - const getFrameSize = (taskID, frame) => { + function getFrameMeta(taskID, frame) { const { meta, mode } = frameDataCache[taskID]; let size = null; if (mode === 'interpolation') { @@ -311,12 +331,12 @@ size = meta.frames[frame]; } } else { - throw new ArgumentError( + throw new DataError( `Invalid mode is specified ${mode}`, ); } return size; - }; + } class FrameBuffer { constructor(size, chunkSize, stopFrame, taskID) { @@ -347,15 +367,15 @@ }; for (const frame of this._requestedChunks[chunkIdx].requestedFrames.entries()) { const requestedFrame = frame[1]; - const size = getFrameSize(this._taskID, requestedFrame); - const frameData = new FrameData( - size.width, - size.height, - this._taskID, - requestedFrame, - frameDataCache[this._taskID].startFrame, - frameDataCache[this._taskID].stopFrame, - ); + const frameMeta = getFrameMeta(this._taskID, requestedFrame); + const frameData = new FrameData({ + ...frameMeta, + taskID: this._taskID, + frameNumber: requestedFrame, + startFrame: frameDataCache[this._taskID].startFrame, + stopFrame: frameDataCache[this._taskID].stopFrame, + decodeForward: false, + }); frameData.data().then(() => { if (!(chunkIdx in this._requestedChunks) @@ -452,9 +472,15 @@ } this._required = frameNumber; - const size = getFrameSize(taskID, frameNumber); - let frame = new FrameData(size.width, size.height, taskID, frameNumber, - frameDataCache[taskID].startFrame, frameDataCache[taskID].stopFrame, !fillBuffer); + const frameMeta = getFrameMeta(taskID, frameNumber); + let frame = new FrameData({ + ...frameMeta, + taskID, + frameNumber, + startFrame: frameDataCache[taskID].startFrame, + stopFrame: frameDataCache[taskID].stopFrame, + decodeForward: !fillBuffer, + }); if (frameNumber in this._buffer) { frame = this._buffer[frameNumber]; @@ -559,9 +585,9 @@ activeChunkRequest: null, nextChunkRequest: null, }; - const size = getFrameSize(taskID, frame); + const frameMeta = getFrameMeta(taskID, frame); // actual only for video chunks - frameDataCache[taskID].provider.setRenderSize(size.width, size.height); + frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height); } return frameDataCache[taskID].frameBuffer.require(frame, taskID, isPlaying, step); diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 7d66fac9f417..17a712ecaf20 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -421,6 +421,7 @@ * @async * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.DataError} * @throws {module:API.cvat.exceptions.ArgumentError} */ diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 0080d48538fb..d43e9111fd8c 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -744,7 +744,13 @@ ThunkAction, {}, {}, AnyAction> { payload: { number: state.annotation.player.frame.number, data: state.annotation.player.frame.data, + filename: state.annotation.player.frame.filename, + delay: state.annotation.player.frame.delay, + changeTime: state.annotation.player.frame.changeTime, states: state.annotation.annotations.states, + minZ: state.annotation.annotations.zLayer.min, + maxZ: state.annotation.annotations.zLayer.max, + curZ: state.annotation.annotations.zLayer.cur, }, }); @@ -789,9 +795,11 @@ ThunkAction, {}, {}, AnyAction> { payload: { number: toFrame, data, + filename: data.filename, states, minZ, maxZ, + curZ: maxZ, changeTime: currentTime + delay, delay, }, @@ -936,13 +944,13 @@ export function getJobAsync( const colors = [...cvat.enums.colors]; loadJobEvent.close(await jobInfoGenerator(job)); - dispatch({ type: AnnotationActionTypes.GET_JOB_SUCCESS, payload: { job, states, frameNumber, + frameFilename: frameData.filename, frameData, colors, filters, @@ -950,6 +958,7 @@ export function getJobAsync( maxZ, }, }); + dispatch(changeFrameAsync(frameNumber, false)); } catch (error) { dispatch({ type: AnnotationActionTypes.GET_JOB_FAILED, diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index ac89b3032837..8a055bce823c 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -119,6 +119,7 @@ overflow: hidden; text-overflow: ellipsis; user-select: none; + word-break: break-all; } .cvat-player-frame-url-icon { diff --git a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx index 397bf8d6918b..d5f1de2ecdda 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/player-navigation.tsx @@ -20,6 +20,7 @@ interface Props { startFrame: number; stopFrame: number; frameNumber: number; + frameFilename: string; inputFrameRef: React.RefObject; onSliderChange(value: SliderValue): void; onInputChange(value: number | undefined): void; @@ -31,6 +32,7 @@ function PlayerNavigation(props: Props): JSX.Element { startFrame, stopFrame, frameNumber, + frameFilename, inputFrameRef, onSliderChange, onInputChange, @@ -53,8 +55,8 @@ function PlayerNavigation(props: Props): JSX.Element { - - filename.png + + {frameFilename} diff --git a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx index 370702fec026..ede793f90231 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/top-bar.tsx @@ -23,6 +23,7 @@ interface Props { saving: boolean; savingStatuses: string[]; frameNumber: number; + frameFilename: string; inputFrameRef: React.RefObject; startFrame: number; stopFrame: number; @@ -54,6 +55,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { redoAction, playing, frameNumber, + frameFilename, inputFrameRef, startFrame, stopFrame, @@ -102,6 +104,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element { startFrame={startFrame} stopFrame={stopFrame} frameNumber={frameNumber} + frameFilename={frameFilename} inputFrameRef={inputFrameRef} onSliderChange={onSliderChange} onInputChange={onInputChange} diff --git a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx index 83ff8f6f12fc..b715bb792538 100644 --- a/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx +++ b/cvat-ui/src/containers/annotation-page/top-bar/top-bar.tsx @@ -32,6 +32,7 @@ import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces'; interface StateToProps { jobInstance: any; frameNumber: number; + frameFilename: string; frameStep: number; frameSpeed: FrameSpeed; frameDelay: number; @@ -63,6 +64,7 @@ function mapStateToProps(state: CombinedState): StateToProps { player: { playing, frame: { + filename: frameFilename, number: frameNumber, delay: frameDelay, }, @@ -103,6 +105,7 @@ function mapStateToProps(state: CombinedState): StateToProps { saving, savingStatuses, frameNumber, + frameFilename, jobInstance, undoAction: history.undo[history.undo.length - 1], redoAction: history.redo[history.redo.length - 1], @@ -208,7 +211,10 @@ class AnnotationTopBarContainer extends React.PureComponent { setTimeout(() => { const { playing: stillPlaying } = this.props; if (stillPlaying) { - onChangeFrame(frameNumber + 1 + framesSkiped, stillPlaying, framesSkiped + 1); + onChangeFrame( + frameNumber + 1 + framesSkiped, + stillPlaying, framesSkiped + 1, + ); } }, frameDelay); } else { @@ -450,6 +456,7 @@ class AnnotationTopBarContainer extends React.PureComponent { stopFrame, }, frameNumber, + frameFilename, undoAction, redoAction, workspace, @@ -622,6 +629,7 @@ class AnnotationTopBarContainer extends React.PureComponent { startFrame={startFrame} stopFrame={stopFrame} frameNumber={frameNumber} + frameFilename={frameFilename} inputFrameRef={this.inputFrameRef} undoAction={undoAction} redoAction={redoAction} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 3deb1af3959b..9b3cdbca0b5e 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -40,6 +40,7 @@ const defaultState: AnnotationState = { player: { frame: { number: 0, + filename: '', data: null, fetching: false, delay: 0, @@ -111,6 +112,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { job, states, frameNumber: number, + frameFilename: filename, colors, filters, frameData: data, @@ -145,6 +147,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { ...state.player, frame: { ...state.player.frame, + filename, number, data, }, @@ -192,9 +195,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { const { number, data, + filename, states, minZ, maxZ, + curZ, delay, changeTime, } = action.payload; @@ -209,6 +214,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { ...state.player, frame: { data, + filename, number, fetching: false, changeTime, @@ -222,7 +228,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { zLayer: { min: minZ, max: maxZ, - cur: maxZ, + cur: curZ, }, }, }; diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 8dbb2720b64f..1016dd12e1a1 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -320,6 +320,7 @@ export interface AnnotationState { player: { frame: { number: number; + filename: string; data: any | null; fetching: boolean; delay: number;