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

Add propagate shapes action #8044

Merged
merged 17 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -0,0 +1,4 @@
### Added

- `Propagate shapes` action that allows to propagate all filtered shapes simultaneously
klakhov marked this conversation as resolved.
Show resolved Hide resolved
(<https://github.com/cvat-ai/cvat/pull/8044>)
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "15.0.6",
"version": "15.0.7",
"type": "module",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
Expand Down
55 changes: 54 additions & 1 deletion cvat-core/src/annotations-actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -9,6 +9,7 @@ import { Job, Task } from './session';
import { EventScope, ObjectType } from './enums';
import ObjectState from './object-state';
import { getAnnotations, getCollection } from './annotations';
import { propagateShapes } from './object-utils';

export interface SingleFrameActionInput {
collection: Omit<SerializedCollection, 'tracks' | 'tags' | 'version'>;
Expand All @@ -28,12 +29,20 @@ export enum ActionParameterType {
NUMBER = 'number',
}

// For SELECT values should be a list of possible options
// For NUMBER values should be a list with [min, max, step],
// supported special values: 'frameCount'
type ActionParameters = Record<string, {
type: ActionParameterType;
values: string[];
defaultValue: string;
}>;

export enum FrameSelectionType {
SEGMENT = 'segment',
CURRENT_FRAME = 'current_frame',
}

export default class BaseSingleFrameAction {
/* eslint-disable @typescript-eslint/no-unused-vars */
public async init(
Expand All @@ -58,6 +67,10 @@ export default class BaseSingleFrameAction {
public get parameters(): ActionParameters | null {
throw new Error('Method not implemented');
}

public get frameSelection(): FrameSelectionType {
return FrameSelectionType.SEGMENT;
}
}

class RemoveFilteredShapes extends BaseSingleFrameAction {
Expand All @@ -82,6 +95,45 @@ class RemoveFilteredShapes extends BaseSingleFrameAction {
}
}

class PropagateShapes extends BaseSingleFrameAction {
#targetFrame: number;

public async init(instance, parameters): Promise<void> {
this.#targetFrame = parameters['Target frame'];
}

public async destroy(): Promise<void> {
// nothing to destroy
}

public async run(
instance,
{ collection: frameCollection, frameData: { number } },
): Promise<SingleFrameActionOutput> {
const { shapes } = frameCollection;
const propagatedShapes = propagateShapes(shapes, number, this.#targetFrame);
return { collection: { shapes: [...shapes, ...propagatedShapes] } };
klakhov marked this conversation as resolved.
Show resolved Hide resolved
}

public get name(): string {
return 'Propagate shapes';
}

public get parameters(): ActionParameters | null {
return {
'Target frame': {
type: ActionParameterType.NUMBER,
values: ['0', 'frameCount', '1'],
defaultValue: '0',
bsekachev marked this conversation as resolved.
Show resolved Hide resolved
},
};
}

public get frameSelection(): FrameSelectionType {
return FrameSelectionType.CURRENT_FRAME;
}
}

const registeredActions: BaseSingleFrameAction[] = [];

export async function listActions(): Promise<BaseSingleFrameAction[]> {
Expand All @@ -102,6 +154,7 @@ export async function registerAction(action: BaseSingleFrameAction): Promise<voi
}

registerAction(new RemoveFilteredShapes());
registerAction(new PropagateShapes());

async function runSingleFrameChain(
instance: Job | Task,
Expand Down
33 changes: 33 additions & 0 deletions cvat-core/src/object-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { DataError, ArgumentError } from './exceptions';
import { Attribute } from './labels';
import { ShapeType, AttributeType } from './enums';
import { SerializedShape } from './server-response-types';

export function checkNumberOfPoints(shapeType: ShapeType, points: number[]): void {
if (shapeType === ShapeType.RECTANGLE) {
Expand Down Expand Up @@ -356,3 +357,35 @@ export function rle2Mask(rle: number[], width: number, height: number): number[]

return decoded;
}

export function propagateShapes(shapes: SerializedShape[], from: number, to: number): SerializedShape[] {
const getCopyFromShape = (shape: SerializedShape): SerializedShape => ({
klakhov marked this conversation as resolved.
Show resolved Hide resolved
attributes: shape.attributes,
klakhov marked this conversation as resolved.
Show resolved Hide resolved
points: shape.type === 'skeleton' ? null : [...shape.points],
occluded: shape.occluded,
type: shape.type,
label_id: shape.label_id,
z_order: shape.z_order,
rotation: shape.rotation,
frame: from,
elements: shape.type === 'skeleton' ? shape.elements
.map((element: SerializedShape): SerializedShape => getCopyFromShape(element)) : [],
source: shape.source,
group: 0,
outside: false,
});

const states = [];
const sign = Math.sign(to - from);
for (let frame = from + sign; sign > 0 ? frame <= to : frame >= to; frame += sign) {
for (let idx = 0; idx < shapes.length; idx++) {
const shape = shapes[idx];
const copy = getCopyFromShape(shape);

copy.frame = frame;
copy.elements.forEach((element: SerializedShape) => { element.frame = frame; });
states.push(copy);
}
}
return states;
}
2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.63.11",
"version": "1.63.12",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand All @@ -19,7 +19,9 @@ import config from 'config';
import { useIsMounted } from 'utils/hooks';
import { createAction, ActionUnion } from 'utils/redux';
import { getCVATStore } from 'cvat-store';
import { BaseSingleFrameAction, Job, getCore } from 'cvat-core-wrapper';
import {
BaseSingleFrameAction, FrameSelectionType, Job, getCore,
} from 'cvat-core-wrapper';
import { Canvas } from 'cvat-canvas-wrapper';
import { fetchAnnotationsAsync, saveAnnotationsAsync } from 'actions/annotation-actions';
import { switchAutoSave } from 'actions/settings-actions';
Expand Down Expand Up @@ -108,6 +110,18 @@ const reducer = (state: State, action: ActionUnion<typeof reducerActions>): Stat
}

if (action.type === ReducerActionType.SET_ACTIVE_ANNOTATIONS_ACTION) {
const { frameSelection } = action.payload.activeAction;
if (frameSelection === FrameSelectionType.CURRENT_FRAME) {
const storage = getCVATStore();
const currentFrame = storage.getState().annotation.player.frame.number;
return {
...state,
frameFrom: currentFrame,
frameTo: currentFrame,
activeAction: action.payload.activeAction,
actionParameters: {},
};
}
return {
...state,
activeAction: action.payload.activeAction,
Expand Down Expand Up @@ -204,6 +218,7 @@ function ActionParameterComponent(props: Props & { onChange: (value: string) =>
const {
defaultValue, type, values, onChange,
} = props;
const store = getCVATStore();
const [value, setValue] = useState(defaultValue);

useEffect(() => {
Expand All @@ -220,7 +235,13 @@ function ActionParameterComponent(props: Props & { onChange: (value: string) =>
);
}

const [min, max, step] = values.map((val) => +val);
const [min, max, step] = values.map((val) => {
if (val === 'frameCount') {
return store.getState().annotation.job.instance?.frameCount || 0;
}
return +val;
});

return (
<InputNumber
value={+value}
Expand Down Expand Up @@ -277,6 +298,8 @@ function AnnotationsActionsModalContent(props: { onClose: () => void; }): JSX.El
progress, progressMessage, frameFrom, frameTo, actionParameters, modalVisible,
} = state;

const currentFrameAction = activeAction?.frameSelection === FrameSelectionType.CURRENT_FRAME;

return (
<Modal
closable={false}
Expand Down Expand Up @@ -414,6 +437,7 @@ function AnnotationsActionsModalContent(props: { onClose: () => void; }): JSX.El
value={frameFrom}
min={(jobInstance as Job).startFrame}
max={frameTo}
disabled={currentFrameAction}
step={1}
bsekachev marked this conversation as resolved.
Show resolved Hide resolved
onChange={(value) => {
if (typeof value === 'number') {
Expand All @@ -428,6 +452,7 @@ function AnnotationsActionsModalContent(props: { onClose: () => void; }): JSX.El
value={frameTo}
min={frameFrom}
max={(jobInstance as Job).stopFrame}
disabled={currentFrameAction}
step={1}
onChange={(value) => {
if (typeof value === 'number') {
Expand All @@ -440,51 +465,55 @@ function AnnotationsActionsModalContent(props: { onClose: () => void; }): JSX.El
</Col>
</Row>
</Col>
<Col span={24} className='cvat-action-runner-frames-predefined'>
<Row>
<Col span={24}>
<Text strong>Or choose one of predefined options </Text>
<hr />
</Col>
<Col span={24}>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(current));
dispatch(reducerActions.updateFrameTo(current));
}}
>
Current frame
</Button>
<Button
onClick={() => {
dispatch(reducerActions.updateFrameFrom(jobInstance.startFrame));
dispatch(reducerActions.updateFrameTo(jobInstance.stopFrame));
}}
>
All frames
</Button>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(current));
dispatch(reducerActions.updateFrameTo(jobInstance.stopFrame));
}}
>
From current
</Button>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(jobInstance.startFrame));
dispatch(reducerActions.updateFrameTo(current));
}}
>
Up to current
</Button>
{
!currentFrameAction ? (
<Col span={24} className='cvat-action-runner-frames-predefined'>
<Row>
<Col span={24}>
<Text strong>Or choose one of predefined options </Text>
<hr />
</Col>
<Col span={24}>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(current));
dispatch(reducerActions.updateFrameTo(current));
}}
>
Current frame
</Button>
<Button
onClick={() => {
dispatch(reducerActions.updateFrameFrom(jobInstance.startFrame));
dispatch(reducerActions.updateFrameTo(jobInstance.stopFrame));
}}
>
All frames
</Button>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(current));
dispatch(reducerActions.updateFrameTo(jobInstance.stopFrame));
}}
>
From current
</Button>
<Button
onClick={() => {
const current = storage.getState().annotation.player.frame.number;
dispatch(reducerActions.updateFrameFrom(jobInstance.startFrame));
dispatch(reducerActions.updateFrameTo(current));
}}
>
Up to current
</Button>
</Col>
</Row>
</Col>
</Row>
</Col>
) : null
}
</>
) : null}

Expand Down
5 changes: 3 additions & 2 deletions cvat-ui/src/cvat-core-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
// Copyright (C) 2023-2024 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -37,7 +37,7 @@ import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-co
import { Dumper } from 'cvat-core/src/annotation-formats';
import { Event } from 'cvat-core/src/event';
import { APIWrapperEnterOptions } from 'cvat-core/src/plugins';
import BaseSingleFrameAction, { ActionParameterType } from 'cvat-core/src/annotations-actions';
import BaseSingleFrameAction, { ActionParameterType, FrameSelectionType } from 'cvat-core/src/annotations-actions';

const cvat: CVATCore = _cvat;

Expand Down Expand Up @@ -96,6 +96,7 @@ export {
Event,
FrameData,
ActionParameterType,
FrameSelectionType,
};

export type {
Expand Down
Loading