Support of context images for 2D tasks (#3122)

main
Boris Sekachev 5 years ago committed by GitHub
parent 975996ef62
commit 6f52ef30e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,11 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- - Support of context images for 2D image tasks (<https://github.com/openvinotoolkit/cvat/pull/3122>)
### Changed ### Changed
- - Updated manifest format, added meta with related images (<https://github.com/openvinotoolkit/cvat/pull/3122>)
### Deprecated ### Deprecated
@ -111,7 +111,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed filters select overflow (<https://github.com/openvinotoolkit/cvat/pull/2614>) - Fixed filters select overflow (<https://github.com/openvinotoolkit/cvat/pull/2614>)
- Fixed tasks in project auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2725>) - Fixed tasks in project auto annotation (<https://github.com/openvinotoolkit/cvat/pull/2725>)
- Cuboids are missed in annotations statistics (<https://github.com/openvinotoolkit/cvat/pull/2704>) - Cuboids are missed in annotations statistics (<https://github.com/openvinotoolkit/cvat/pull/2704>)
- The list of files attached to the task is not displayed (<https://github.com/openvinotoolkit/cvat/pul - The list of files attached to the task is not displayed (<https://github.com/openvinotoolkit/cvat/pull/2706>)
- A couple of css-related issues (top bar disappear, wrong arrow position on collapse elements) (<https://github.com/openvinotoolkit/cvat/pull/2736>) - A couple of css-related issues (top bar disappear, wrong arrow position on collapse elements) (<https://github.com/openvinotoolkit/cvat/pull/2736>)
- Issue with point region doesn't work in Firefox (<https://github.com/openvinotoolkit/cvat/pull/2727>) - Issue with point region doesn't work in Firefox (<https://github.com/openvinotoolkit/cvat/pull/2727>)
- Fixed cuboid perspective change (<https://github.com/openvinotoolkit/cvat/pull/2733>) - Fixed cuboid perspective change (<https://github.com/openvinotoolkit/cvat/pull/2733>)

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.12.1", "version": "3.13.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.12.1", "version": "3.13.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {

@ -19,7 +19,15 @@
*/ */
class FrameData { class FrameData {
constructor({ constructor({
width, height, name, taskID, frameNumber, startFrame, stopFrame, decodeForward, width,
height,
name,
taskID,
frameNumber,
startFrame,
stopFrame,
decodeForward,
has_related_context: hasRelatedContext,
}) { }) {
Object.defineProperties( Object.defineProperties(
this, this,
@ -72,6 +80,18 @@
value: frameNumber, value: frameNumber,
writable: false, writable: false,
}, },
/**
* True if some context images are associated with this frame
* @name hasRelatedContext
* @type {boolean}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
hasRelatedContext: {
value: hasRelatedContext,
writable: false,
},
startFrame: { startFrame: {
value: startFrame, value: startFrame,
writable: false, writable: false,

@ -756,11 +756,7 @@
}, },
); );
} catch (errorData) { } catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code; throw generateError(errorData);
throw new ServerError(
`Could not get Image Context of the frame for the task ${tid} from the server`,
code,
);
} }
return response.data; return response.data;

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

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

@ -196,6 +196,8 @@ export enum AnnotationActionTypes {
GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS', GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS',
HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE', HIDE_SHOW_CONTEXT_IMAGE = 'HIDE_SHOW_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE', GET_CONTEXT_IMAGE = 'GET_CONTEXT_IMAGE',
GET_CONTEXT_IMAGE_SUCCESS = 'GET_CONTEXT_IMAGE_SUCCESS',
GET_CONTEXT_IMAGE_FAILED = 'GET_CONTEXT_IMAGE_FAILED',
} }
export function saveLogsAsync(): ThunkAction { export function saveLogsAsync(): ThunkAction {
@ -715,6 +717,7 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
number: state.annotation.player.frame.number, number: state.annotation.player.frame.number,
data: state.annotation.player.frame.data, data: state.annotation.player.frame.data,
filename: state.annotation.player.frame.filename, filename: state.annotation.player.frame.filename,
hasRelatedContext: state.annotation.player.frame.hasRelatedContext,
delay: state.annotation.player.frame.delay, delay: state.annotation.player.frame.delay,
changeTime: state.annotation.player.frame.changeTime, changeTime: state.annotation.player.frame.changeTime,
states: state.annotation.annotations.states, states: state.annotation.annotations.states,
@ -766,6 +769,7 @@ export function changeFrameAsync(toFrame: number, fillBuffer?: boolean, frameSte
number: toFrame, number: toFrame,
data, data,
filename: data.filename, filename: data.filename,
hasRelatedContext: data.hasRelatedContext,
states, states,
minZ, minZ,
maxZ, maxZ,
@ -1031,6 +1035,7 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
states, states,
frameNumber, frameNumber,
frameFilename: frameData.filename, frameFilename: frameData.filename,
frameHasRelatedContext: frameData.hasRelatedContext,
frameData, frameData,
colors, colors,
filters, filters,
@ -1636,31 +1641,23 @@ export function getContextImage(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job; const { instance: job } = state.annotation.job;
const { frame, contextImage } = state.annotation.player; const { number: frameNumber } = state.annotation.player.frame;
try { try {
const context = await job.frames.contextImage(job.task.id, frame.number);
const loaded = true;
const contextImageHide = contextImage.hidden;
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE, type: AnnotationActionTypes.GET_CONTEXT_IMAGE,
payload: { payload: {},
context, });
loaded,
contextImageHide, const contextImageData = await job.frames.contextImage(job.task.id, frameNumber);
}, dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData },
}); });
} catch (error) { } catch (error) {
const context = '';
const loaded = true;
const contextImageHide = contextImage.hidden;
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE, type: AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED,
payload: { payload: { error },
context,
loaded,
contextImageHide,
},
}); });
} }
}; };

@ -18,6 +18,7 @@ import getCore from 'cvat-core-wrapper';
import consts from 'consts'; import consts from 'consts';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import ImageSetupsContent from './image-setups-content'; import ImageSetupsContent from './image-setups-content';
import ContextImage from '../standard-workspace/context-image/context-image';
const cvat = getCore(); const cvat = getCore();
@ -773,12 +774,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
maxZLayer, maxZLayer,
curZLayer, curZLayer,
minZLayer, minZLayer,
onSwitchZLayer,
onAddZLayer,
keyMap, keyMap,
switchableAutomaticBordering, switchableAutomaticBordering,
automaticBordering, automaticBordering,
onSwitchAutomaticBordering, onSwitchAutomaticBordering,
onSwitchZLayer,
onAddZLayer,
} = this.props; } = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => { const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -817,6 +818,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}} }}
/> />
<ContextImage />
<Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}> <Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' /> <UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown> </Dropdown>

@ -14,20 +14,16 @@ import { Workspace } from 'reducers/interfaces';
import { import {
CAMERA_ACTION, Canvas3d, MouseInteraction, ViewType, CAMERA_ACTION, Canvas3d, MouseInteraction, ViewType,
} from 'cvat-canvas3d-wrapper'; } from 'cvat-canvas3d-wrapper';
import ContextImage from '../standard3D-workspace/context-image/context-image'; import ContextImage from 'components/annotation-page/standard-workspace/context-image/context-image';
import CVATTooltip from '../../common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
interface Props { interface Props {
canvasInstance: Canvas3d; canvasInstance: Canvas3d;
jobInstance: any; jobInstance: any;
frameData: any; frameData: any;
curZLayer: number; curZLayer: number;
contextImageHide: boolean;
loaded: boolean;
data: string;
annotations: any[]; annotations: any[];
onSetupCanvas: () => void; onSetupCanvas: () => void;
getContextImage(): void;
onResetCanvas(): void; onResetCanvas(): void;
workspace: Workspace; workspace: Workspace;
animateID: any; animateID: any;
@ -119,9 +115,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
const sideView = useRef<HTMLDivElement | null>(null); const sideView = useRef<HTMLDivElement | null>(null);
const frontView = useRef<HTMLDivElement | null>(null); const frontView = useRef<HTMLDivElement | null>(null);
const { const { frameData, annotations, curZLayer } = props;
frameData, contextImageHide, getContextImage, loaded, data, annotations, curZLayer,
} = props;
const onCanvasSetup = (): void => { const onCanvasSetup = (): void => {
const { onSetupCanvas } = props; const { onSetupCanvas } = props;
@ -345,13 +339,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
return ( return (
<Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'> <Layout.Content className='cvat-canvas3d-fullsize' id='canvas3d-container'>
<ContextImage <ContextImage />
frame={frameData}
contextImageHide={contextImageHide}
getContextImage={getContextImage}
loaded={loaded}
data={data}
/>
<ResizableBox <ResizableBox
className='cvat-resizable' className='cvat-resizable'
width={Infinity} width={Infinity}

@ -0,0 +1,74 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import { notification } from 'antd';
import { useDispatch, useSelector } from 'react-redux';
import { QuestionCircleOutlined, ShrinkOutlined } from '@ant-design/icons';
import Spin from 'antd/lib/spin';
import Image from 'antd/lib/image';
import { CombinedState } from 'reducers/interfaces';
import { hideShowContextImage, getContextImage } from 'actions/annotation-actions';
import CVATTooltip from 'components/common/cvat-tooltip';
export default function ContextImage(): JSX.Element | null {
const dispatch = useDispatch();
const { number: frame, hasRelatedContext } = useSelector((state: CombinedState) => state.annotation.player.frame);
const { data: contextImageData, hidden: contextImageHidden, fetching: contextImageFetching } = useSelector(
(state: CombinedState) => state.annotation.player.contextImage,
);
const [requested, setRequested] = useState(false);
useEffect(() => {
if (requested) {
setRequested(false);
}
}, [frame]);
useEffect(() => {
if (hasRelatedContext && !contextImageHidden && !requested) {
dispatch(getContextImage());
setRequested(true);
}
}, [contextImageHidden, requested, hasRelatedContext]);
if (!hasRelatedContext) {
return null;
}
return (
<div className='cvat-context-image-wrapper' {...(contextImageHidden ? { style: { width: '32px' } } : {})}>
<div className='cvat-context-image-wrapper-header' />
{contextImageFetching ? <Spin size='small' /> : null}
{contextImageHidden ? (
<CVATTooltip title='A context image is available'>
<QuestionCircleOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(false))}
/>
</CVATTooltip>
) : (
<>
<ShrinkOutlined
className='cvat-context-image-switcher'
onClick={() => dispatch(hideShowContextImage(true))}
/>
<Image
{...(contextImageData ? { src: contextImageData } : {})}
onError={() => {
notification.error({
message: 'Could not display context image',
description: `Source is ${
contextImageData === null ? 'empty' : contextImageData.slice(0, 100)
}`,
});
}}
className='cvat-context-image'
/>
</>
)}
</div>
);
}

@ -41,7 +41,7 @@ export function ExtraControlsControl(): JSX.Element {
> >
<SmallDashOutlined <SmallDashOutlined
style={{ visibility: hasChildren ? 'visible' : 'hidden' }} style={{ visibility: hasChildren ? 'visible' : 'hidden' }}
className='cvat-extra-controls-control' className='cvat-extra-controls-control cvat-antd-icon-control'
/> />
</Popover> </Popover>
); );

@ -8,6 +8,55 @@
height: 100%; height: 100%;
} }
.cvat-context-image-wrapper {
height: auto;
width: $grid-unit-size * 32;
position: absolute;
top: $grid-unit-size;
right: $grid-unit-size;
z-index: 100;
background: black;
display: flex;
flex-direction: column;
justify-content: space-between;
user-select: none;
> .cvat-context-image-wrapper-header {
height: $grid-unit-size * 4;
width: 100%;
z-index: 101;
background: rgba(0, 0, 0, 0.2);
position: absolute;
top: 0;
left: 0;
}
> .ant-image {
margin: $grid-unit-size / 2;
}
> span {
position: absolute;
font-size: 18px;
top: 7px;
right: 7px;
z-index: 102;
color: white;
&:hover {
> svg {
transform: scale(1.2);
}
}
}
}
.cvat-context-image {
width: 100%;
height: auto;
display: block;
}
.cvat-objects-sidebar-sider { .cvat-objects-sidebar-sider {
top: 0; top: 0;
right: 0; right: 0;
@ -56,8 +105,7 @@
.cvat-issue-control, .cvat-issue-control,
.cvat-tools-control, .cvat-tools-control,
.cvat-extra-controls-control, .cvat-extra-controls-control,
.cvat-opencv-control, .cvat-opencv-control {
.cvat-context-image-control {
border-radius: 3.3px; border-radius: 3.3px;
transform: scale(0.65); transform: scale(0.65);
padding: 2px; padding: 2px;
@ -76,7 +124,7 @@
} }
} }
.cvat-extra-controls-control { .cvat-antd-icon-control {
> svg { > svg {
width: 40px; width: 40px;
height: 40px; height: 40px;

@ -1,34 +0,0 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect } from 'react';
interface Props {
frame: number;
contextImageHide: boolean;
loaded: boolean;
data: string;
getContextImage(): void;
}
export default function ContextImage(props: Props): JSX.Element {
const {
contextImageHide, loaded, data, getContextImage,
} = props;
useEffect(() => {
if (!contextImageHide && !loaded) {
getContextImage();
}
}, [contextImageHide, loaded]);
if (!contextImageHide && data !== '') {
return (
<div className='cvat-contextImage'>
<img src={data} alt='Context not available' className='cvat-contextImage-show' />
</div>
);
}
return null;
}

@ -9,20 +9,15 @@ import { Canvas3d as Canvas } from 'cvat-canvas3d-wrapper';
import CursorControl from './cursor-control'; import CursorControl from './cursor-control';
import MoveControl from './move-control'; import MoveControl from './move-control';
import DrawCuboidControl from './draw-cuboid-control'; import DrawCuboidControl from './draw-cuboid-control';
import PhotoContextControl from './photo-context';
interface Props { interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
activeControl: ActiveControl; activeControl: ActiveControl;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
contextImageHide: boolean;
hideShowContextImage: (hidden: boolean) => void;
} }
export default function ControlsSideBarComponent(props: Props): JSX.Element { export default function ControlsSideBarComponent(props: Props): JSX.Element {
const { const { canvasInstance, activeControl, normalizedKeyMap } = props;
canvasInstance, activeControl, normalizedKeyMap, contextImageHide, hideShowContextImage,
} = props;
return ( return (
<Layout.Sider className='cvat-canvas-controls-sidebar' theme='light' width={44}> <Layout.Sider className='cvat-canvas-controls-sidebar' theme='light' width={44}>
@ -37,12 +32,6 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
canvasInstance={canvasInstance} canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_CUBOID} isDrawing={activeControl === ActiveControl.DRAW_CUBOID}
/> />
<PhotoContextControl
canvasInstance={canvasInstance}
activeControl={activeControl}
contextImageHide={contextImageHide}
hideShowContextImage={hideShowContextImage}
/>
</Layout.Sider> </Layout.Sider>
); );
} }

@ -1,37 +0,0 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import CameraIcon from '@ant-design/icons/CameraOutlined';
import CVATTooltip from 'components/common/cvat-tooltip';
import { Canvas3d as Canvas } from 'cvat-canvas3d-wrapper';
import { ActiveControl } from 'reducers/interfaces';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
hideShowContextImage: (hidden: boolean) => void;
contextImageHide: boolean;
}
function PhotoContextControl(props: Props): JSX.Element {
const { activeControl, contextImageHide, hideShowContextImage } = props;
return (
<CVATTooltip title='Photo context show/hide' placement='right'>
<CameraIcon
className={`cvat-context-image-control
cvat-control-side-bar-icon-size ${
activeControl === ActiveControl.PHOTO_CONTEXT ? 'cvat-active-canvas-control' : ''
}`}
onClick={(): void => {
hideShowContextImage(!contextImageHide);
}}
/>
</CVATTooltip>
);
}
export default React.memo(PhotoContextControl);

@ -4,173 +4,12 @@
@import 'base.scss'; @import 'base.scss';
.cvat-standard-workspace.ant-layout {
height: 100%;
}
.cvat-contextImage {
width: $grid-unit-size * 32;
position: absolute;
background: $border-color-3;
top: $grid-unit-size;
right: $grid-unit-size;
z-index: 100;
border-radius: $grid-unit-size;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: $grid-unit-size/2;
}
.cvat-contextImage-show {
max-width: 100%;
max-height: 100%;
}
.cvat-contextImage-loading {
text-align: center;
}
.cvat-objects-sidebar-filter-input {
width: calc(100% - 35px);
}
.cvat-objects-sidebar-sider {
top: 0;
right: 0;
left: auto;
background-color: $background-color-2;
border-left: 1px solid $border-color-1;
border-bottom: 1px solid $border-color-1;
border-radius: $grid-unit-size/2 0 0 $grid-unit-size/2;
z-index: 2;
}
.cvat-objects-sidebar {
height: 100%;
}
.cvat-rotate-canvas-controls-right > svg {
transform: scaleX(-1);
}
.cvat-canvas-controls-sidebar {
background-color: $background-color-2;
border-right: 1px solid $border-color-1;
> div {
> i {
border-radius: 3.3px;
transform: scale(0.65);
padding: $grid-unit-size/4;
&:hover {
background: $header-color;
transform: scale(0.75);
}
&:active {
transform: scale(0.65);
}
> svg {
transform: scale(0.8);
}
}
}
}
.cvat-active-canvas-control {
background: $header-color;
transform: scale(0.75);
}
.cvat-rotate-canvas-controls-left,
.cvat-rotate-canvas-controls-right {
transform: scale(0.65);
border-radius: $grid-unit-size/2;
&:hover {
transform: scale(0.75);
}
&:active {
transform: scale(0.65);
}
}
.cvat-rotate-canvas-controls > .ant-popover-content > .ant-popover-inner > div > .ant-popover-inner-content {
padding: 0;
}
.cvat-draw-shape-popover,
.cvat-tools-control-popover {
> .ant-popover-content > .ant-popover-inner > div > .ant-popover-inner-content {
padding: 0;
}
}
.cvat-tools-track-button,
.cvat-tools-interact-button {
width: 100%;
margin-top: $grid-unit-size;
}
.cvat-draw-shape-popover-points-selector {
width: 100%;
}
.cvat-tools-control-popover-content {
width: fit-content;
padding: $grid-unit-size;
border-radius: $grid-unit-size/2;
background: $background-color-2;
}
.cvat-draw-shape-popover-content {
padding: $grid-unit-size;
border-radius: $grid-unit-size/2;
background: $background-color-2;
width: 270px;
> div {
margin-top: $grid-unit-size/2;
}
> div:nth-child(3) > div > div {
width: 100%;
}
> div:last-child {
span {
width: 100%;
}
button {
width: 100%;
&:nth-child(1) {
border-radius: $grid-unit-size/2 0 0 $grid-unit-size/2;
}
&:nth-child(2) {
border-radius: 0 $grid-unit-size/2 $grid-unit-size/2 0;
}
}
}
}
.cvat-canvas-container-overflow { .cvat-canvas-container-overflow {
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.cvat-control-side-bar-icon-size {
font-size: $grid-unit-size * 5;
}
.cvat-canvas3d-perspective { .cvat-canvas3d-perspective {
height: 100%; height: 100%;
width: 100%; width: 100%;

@ -57,7 +57,6 @@ interface Props {
onUndoClick(): void; onUndoClick(): void;
onRedoClick(): void; onRedoClick(): void;
jobInstance: any; jobInstance: any;
hideShowContextImage(): any;
} }
export default function AnnotationTopBarComponent(props: Props): JSX.Element { export default function AnnotationTopBarComponent(props: Props): JSX.Element {

@ -5,7 +5,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D'; import CanvasWrapperComponent from 'components/annotation-page/canvas/canvas-wrapper3D';
import { confirmCanvasReady, getContextImage, resetCanvas } from 'actions/annotation-actions'; import { confirmCanvasReady, resetCanvas } from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
@ -16,15 +16,11 @@ interface StateToProps {
jobInstance: any; jobInstance: any;
frameData: any; frameData: any;
curZLayer: number; curZLayer: number;
contextImageHide: boolean;
loaded: boolean;
data: string;
annotations: any[]; annotations: any[];
} }
interface DispatchToProps { interface DispatchToProps {
onSetupCanvas(): void; onSetupCanvas(): void;
getContextImage(): void;
onResetCanvas(): void; onResetCanvas(): void;
} }
@ -35,7 +31,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
job: { instance: jobInstance }, job: { instance: jobInstance },
player: { player: {
frame: { data: frameData }, frame: { data: frameData },
contextImage: { hidden: contextImageHide, data, loaded },
}, },
annotations: { annotations: {
states: annotations, states: annotations,
@ -49,9 +44,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance, jobInstance,
frameData, frameData,
curZLayer, curZLayer,
contextImageHide,
loaded,
data,
annotations, annotations,
}; };
} }
@ -61,9 +53,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSetupCanvas(): void { onSetupCanvas(): void {
dispatch(confirmCanvasReady()); dispatch(confirmCanvasReady());
}, },
getContextImage(): void {
dispatch(getContextImage());
},
onResetCanvas(): void { onResetCanvas(): void {
dispatch(resetCanvas()); dispatch(resetCanvas());
}, },

@ -2,11 +2,10 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { KeyMap } from 'utils/mousetrap-react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { KeyMap } from 'utils/mousetrap-react';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import { hideShowContextImage } from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarComponent from 'components/annotation-page/standard3D-workspace/controls-side-bar/controls-side-bar';
import { ActiveControl, CombinedState } from 'reducers/interfaces'; import { ActiveControl, CombinedState } from 'reducers/interfaces';
@ -15,21 +14,13 @@ interface StateToProps {
activeControl: ActiveControl; activeControl: ActiveControl;
keyMap: KeyMap; keyMap: KeyMap;
normalizedKeyMap: Record<string, string>; normalizedKeyMap: Record<string, string>;
contextImageHide: boolean;
loaded: boolean; loaded: boolean;
} }
interface DispatchToProps {
hideShowContextImage(hidden: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { annotation: {
canvas: { instance: canvasInstance, activeControl }, canvas: { instance: canvasInstance, activeControl },
player: {
contextImage: { hidden: contextImageHide, loaded },
},
}, },
shortcuts: { keyMap, normalizedKeyMap }, shortcuts: { keyMap, normalizedKeyMap },
} = state; } = state;
@ -39,17 +30,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
activeControl, activeControl,
normalizedKeyMap, normalizedKeyMap,
keyMap, keyMap,
contextImageHide,
loaded,
};
}
function dispatchToProps(dispatch: any): DispatchToProps {
return {
hideShowContextImage(hidden: boolean): void {
dispatch(hideShowContextImage(hidden));
},
}; };
} }
export default connect(mapStateToProps, dispatchToProps)(ControlsSideBarComponent); export default connect(mapStateToProps)(ControlsSideBarComponent);

@ -51,6 +51,7 @@ const defaultState: AnnotationState = {
number: 0, number: 0,
filename: '', filename: '',
data: null, data: null,
hasRelatedContext: false,
fetching: false, fetching: false,
delay: 0, delay: 0,
changeTime: null, changeTime: null,
@ -58,8 +59,8 @@ const defaultState: AnnotationState = {
playing: false, playing: false,
frameAngles: [], frameAngles: [],
contextImage: { contextImage: {
loaded: false, fetching: false,
data: '', data: null,
hidden: false, hidden: false,
}, },
}, },
@ -145,6 +146,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
openTime, openTime,
frameNumber: number, frameNumber: number,
frameFilename: filename, frameFilename: filename,
frameHasRelatedContext,
colors, colors,
filters, filters,
frameData: data, frameData: data,
@ -189,6 +191,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
frame: { frame: {
...state.player.frame, ...state.player.frame,
filename, filename,
hasRelatedContext: frameHasRelatedContext,
number, number,
data, data,
}, },
@ -226,11 +229,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.player.frame, ...state.player.frame,
fetching: false, fetching: false,
}, },
contextImage: {
loaded: false,
data: '',
hidden: state.player.contextImage.hidden,
},
}, },
}; };
} }
@ -252,7 +250,16 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
} }
case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: { case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: {
const { const {
number, data, filename, states, minZ, maxZ, curZ, delay, changeTime, number,
data,
filename,
hasRelatedContext,
states,
minZ,
maxZ,
curZ,
delay,
changeTime,
} = action.payload; } = action.payload;
const activatedStateID = states const activatedStateID = states
@ -268,6 +275,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
frame: { frame: {
data, data,
filename, filename,
hasRelatedContext,
number, number,
fetching: false, fetching: false,
changeTime, changeTime,
@ -275,7 +283,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
contextImage: { contextImage: {
...state.player.contextImage, ...state.player.contextImage,
loaded: false, data: null,
}, },
}, },
annotations: { annotations: {
@ -1170,30 +1178,52 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
} }
case AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE: { case AnnotationActionTypes.HIDE_SHOW_CONTEXT_IMAGE: {
const { hidden } = action.payload; const { hidden } = action.payload;
const { loaded, data } = state.player.contextImage;
return { return {
...state, ...state,
player: { player: {
...state.player, ...state.player,
contextImage: { contextImage: {
loaded, ...state.player.contextImage,
data,
hidden, hidden,
}, },
}, },
}; };
} }
case AnnotationActionTypes.GET_CONTEXT_IMAGE: { case AnnotationActionTypes.GET_CONTEXT_IMAGE: {
const { context, loaded } = action.payload; return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
fetching: true,
},
},
};
}
case AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS: {
const { contextImageData } = action.payload;
return { return {
...state, ...state,
player: { player: {
...state.player, ...state.player,
contextImage: { contextImage: {
loaded, ...state.player.contextImage,
data: context, fetching: false,
hidden: state.player.contextImage.hidden, data: contextImageData,
},
},
};
}
case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: {
return {
...state,
player: {
...state.player,
contextImage: {
...state.player.contextImage,
fetching: false,
}, },
}, },
}; };

@ -269,6 +269,7 @@ export interface NotificationsState {
saving: null | ErrorState; saving: null | ErrorState;
jobFetching: null | ErrorState; jobFetching: null | ErrorState;
frameFetching: null | ErrorState; frameFetching: null | ErrorState;
contextImageFetching: null | ErrorState;
changingLabelColor: null | ErrorState; changingLabelColor: null | ErrorState;
updating: null | ErrorState; updating: null | ErrorState;
creating: null | ErrorState; creating: null | ErrorState;
@ -417,6 +418,7 @@ export interface AnnotationState {
frame: { frame: {
number: number; number: number;
filename: string; filename: string;
hasRelatedContext: boolean;
data: any | null; data: any | null;
fetching: boolean; fetching: boolean;
delay: number; delay: number;
@ -425,8 +427,8 @@ export interface AnnotationState {
playing: boolean; playing: boolean;
frameAngles: number[]; frameAngles: number[];
contextImage: { contextImage: {
loaded: boolean; fetching: boolean;
data: string; data: string | null;
hidden: boolean; hidden: boolean;
}; };
}; };

@ -69,6 +69,7 @@ const defaultState: NotificationsState = {
saving: null, saving: null,
jobFetching: null, jobFetching: null,
frameFetching: null, frameFetching: null,
contextImageFetching: null,
changingLabelColor: null, changingLabelColor: null,
updating: null, updating: null,
creating: null, creating: null,
@ -689,6 +690,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case AnnotationActionTypes.GET_CONTEXT_IMAGE_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
contextImageFetching: {
message: 'Could not fetch context image from the server',
reason: action.payload.error,
},
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: { case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: {
return { return {
...state, ...state,

@ -9,7 +9,6 @@ import zipfile
import io import io
import itertools import itertools
import struct import struct
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import closing from contextlib import closing
@ -112,6 +111,10 @@ class ImageListReader(IMediaReader):
for i in range(self._start, self._stop, self._step): for i in range(self._start, self._stop, self._step):
yield (self.get_image(i), self.get_path(i), i) yield (self.get_image(i), self.get_path(i), i)
def filter(self, callback):
source_path = list(filter(callback, self._source_path))
ImageListReader.__init__(self, source_path, step=self._step, start=self._start, stop=self._stop)
def get_path(self, i): def get_path(self, i):
return self._source_path[i] return self._source_path[i]
@ -199,7 +202,7 @@ class ZipReader(ImageListReader):
self._zip_source = zipfile.ZipFile(source_path[0], mode='a') self._zip_source = zipfile.ZipFile(source_path[0], mode='a')
self.extract_dir = source_path[1] if len(source_path) > 1 else None self.extract_dir = source_path[1] if len(source_path) > 1 else None
file_list = [f for f in self._zip_source.namelist() if files_to_ignore(f) and get_mime(f) == 'image'] file_list = [f for f in self._zip_source.namelist() if files_to_ignore(f) and get_mime(f) == 'image']
super().__init__(file_list, step, start, stop) super().__init__(file_list, step=step, start=start, stop=stop)
def __del__(self): def __del__(self):
self._zip_source.close() self._zip_source.close()
@ -759,66 +762,6 @@ class ValidateDimension:
self.image_files[file_name] = file_path self.image_files[file_name] = file_path
return pcd_files return pcd_files
def validate_velodyne_points(self, *args):
root, actual_path, files = args
velodyne_files = self.process_files(root, actual_path, files)
related_path = os.path.split(os.path.split(root)[0])[0]
path_list = [re.search(r'image_\d.*', path, re.IGNORECASE) for path in os.listdir(related_path) if
os.path.isdir(os.path.join(related_path, path))]
for path_ in path_list:
if path_:
path = os.path.join(path_.group(), "data")
path = os.path.abspath(os.path.join(related_path, path))
files = [file for file in os.listdir(path) if
os.path.isfile(os.path.abspath(os.path.join(path, file)))]
for file in files:
f_name = file.split(".")[0]
if velodyne_files.get(f_name, None):
self.related_files[velodyne_files[f_name]].append(
os.path.abspath(os.path.join(path, file)))
def validate_pointcloud(self, *args):
root, actual_path, files = args
pointcloud_files = self.process_files(root, actual_path, files)
related_path = root.rsplit("/pointcloud", 1)[0]
related_images_path = os.path.join(related_path, "related_images")
if os.path.isdir(related_images_path):
paths = [path for path in os.listdir(related_images_path) if
os.path.isdir(os.path.abspath(os.path.join(related_images_path, path)))]
for k in pointcloud_files:
for path in paths:
if k == path.rsplit("_", 1)[0]:
file_path = os.path.abspath(os.path.join(related_images_path, path))
files = [file for file in os.listdir(file_path) if
os.path.isfile(os.path.join(file_path, file))]
for related_image in files:
self.related_files[pointcloud_files[k]].append(os.path.join(file_path, related_image))
def validate_default(self, *args):
root, actual_path, files = args
pcd_files = self.process_files(root, actual_path, files)
if len(list(pcd_files.keys())):
for image in self.image_files.keys():
if pcd_files.get(image, None):
self.related_files[pcd_files[image]].append(self.image_files[image])
current_directory_name = os.path.split(root)
if len(pcd_files.keys()) == 1:
pcd_name = list(pcd_files.keys())[0].rsplit(".", 1)[0]
if current_directory_name[1] == pcd_name:
for related_image in self.image_files.values():
if root == os.path.split(related_image)[0]:
self.related_files[pcd_files[pcd_name]].append(related_image)
def validate(self): def validate(self):
""" """
Validate the directory structure for kitty and point cloud format. Validate the directory structure for kitty and point cloud format.
@ -830,15 +773,7 @@ class ValidateDimension:
if not files_to_ignore(root): if not files_to_ignore(root):
continue continue
if root.endswith("data"): self.process_files(root, actual_path, files)
if os.path.split(os.path.split(root)[0])[1] == "velodyne_points":
self.validate_velodyne_points(root, actual_path, files)
elif os.path.split(root)[-1] == "pointcloud":
self.validate_pointcloud(root, actual_path, files)
else:
self.validate_default(root, actual_path, files)
if len(self.related_files.keys()): if len(self.related_files.keys()):
self.dimension = DimensionType.DIM_3D self.dimension = DimensionType.DIM_3D

@ -545,6 +545,7 @@ class FrameMetaSerializer(serializers.Serializer):
width = serializers.IntegerField() width = serializers.IntegerField()
height = serializers.IntegerField() height = serializers.IntegerField()
name = serializers.CharField(max_length=1024) name = serializers.CharField(max_length=1024)
has_related_context = serializers.BooleanField()
class PluginsSerializer(serializers.Serializer): class PluginsSerializer(serializers.Serializer):
GIT_INTEGRATION = serializers.BooleanField() GIT_INTEGRATION = serializers.BooleanField()

@ -7,6 +7,7 @@ import itertools
import os import os
import sys import sys
import rq import rq
import re
import shutil import shutil
from traceback import print_exception from traceback import print_exception
from urllib import parse as urlparse from urllib import parse as urlparse
@ -19,6 +20,7 @@ from cvat.apps.engine.utils import av_scan_paths
from cvat.apps.engine.models import DimensionType from cvat.apps.engine.models import DimensionType
from utils.dataset_manifest import ImageManifestManager, VideoManifestManager from utils.dataset_manifest import ImageManifestManager, VideoManifestManager
from utils.dataset_manifest.core import VideoManifestValidator from utils.dataset_manifest.core import VideoManifestValidator
from utils.dataset_manifest.utils import detect_related_images
import django_rq import django_rq
from django.conf import settings from django.conf import settings
@ -273,10 +275,14 @@ def _create_thread(tid, data):
start=db_data.start_frame, start=db_data.start_frame,
stop=data['stop_frame'], stop=data['stop_frame'],
dimension=DimensionType.DIM_3D, dimension=DimensionType.DIM_3D,
) )
extractor.add_files(validate_dimension.converted_files) extractor.add_files(validate_dimension.converted_files)
related_images = {}
if isinstance(extractor, MEDIA_TYPES['image']['extractor']):
extractor.filter(lambda x: not re.search(r'(^|{0})related_images{0}'.format(os.sep), x))
related_images = detect_related_images(extractor.absolute_source_paths, upload_dir)
db_task.mode = task_mode db_task.mode = task_mode
db_data.compressed_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' and not data['use_zip_chunks'] else models.DataChoice.IMAGESET db_data.compressed_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' and not data['use_zip_chunks'] else models.DataChoice.IMAGESET
db_data.original_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' else models.DataChoice.IMAGESET db_data.original_chunk_type = models.DataChoice.VIDEO if task_mode == 'interpolation' else models.DataChoice.IMAGESET
@ -394,13 +400,14 @@ def _create_thread(tid, data):
base_msg = str(ex) if isinstance(ex, AssertionError) \ base_msg = str(ex) if isinstance(ex, AssertionError) \
else "Uploaded video does not support a quick way of task creating." else "Uploaded video does not support a quick way of task creating."
_update_status("{} The task will be created using the old method".format(base_msg)) _update_status("{} The task will be created using the old method".format(base_msg))
else:# images, archive, pdf else: # images, archive, pdf
db_data.size = len(extractor) db_data.size = len(extractor)
manifest = ImageManifestManager(db_data.get_manifest_path()) manifest = ImageManifestManager(db_data.get_manifest_path())
if not manifest_file: if not manifest_file:
if db_task.dimension == DimensionType.DIM_2D: if db_task.dimension == DimensionType.DIM_2D:
meta_info = manifest.prepare_meta( meta_info = manifest.prepare_meta(
sources=extractor.absolute_source_paths, sources=extractor.absolute_source_paths,
meta={ k: {'related_images': related_images[k] } for k in related_images },
data_dir=upload_dir data_dir=upload_dir
) )
content = meta_info.content content = meta_info.content
@ -410,6 +417,7 @@ def _create_thread(tid, data):
name, ext = os.path.splitext(os.path.relpath(source, upload_dir)) name, ext = os.path.splitext(os.path.relpath(source, upload_dir))
content.append({ content.append({
'name': name, 'name': name,
'meta': { 'related_images': related_images[''.join((name, ext))] },
'extension': ext 'extension': ext
}) })
manifest.create(content) manifest.create(content)
@ -465,27 +473,15 @@ def _create_thread(tid, data):
update_progress(progress) update_progress(progress)
if db_task.mode == 'annotation': if db_task.mode == 'annotation':
if validate_dimension.dimension == DimensionType.DIM_2D: models.Image.objects.bulk_create(db_images)
models.Image.objects.bulk_create(db_images) created_images = models.Image.objects.filter(data_id=db_data.id)
else:
related_file = [] db_related_files = [
for image_data in db_images: RelatedFile(data=image.data, primary_image=image, path=os.path.join(upload_dir, related_file_path))
image_model = models.Image( for image in created_images
data=image_data.data, for related_file_path in related_images.get(image.path, [])
path=image_data.path, ]
frame=image_data.frame, RelatedFile.objects.bulk_create(db_related_files)
width=image_data.width,
height=image_data.height
)
image_model.save()
image_data = models.Image.objects.get(id=image_model.id)
if validate_dimension.related_files.get(image_data.path, None):
for related_image_file in validate_dimension.related_files[image_data.path]:
related_file.append(
RelatedFile(data=db_data, primary_image_id=image_data.id, path=related_image_file))
RelatedFile.objects.bulk_create(related_file)
db_images = [] db_images = []
else: else:
models.Video.objects.create( models.Video.objects.create(

@ -42,7 +42,7 @@ from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.models import ( from cvat.apps.engine.models import (
Job, StatusChoice, Task, Project, Review, Issue, Job, StatusChoice, Task, Project, Review, Issue,
Comment, StorageMethodChoice, ReviewStatus, StorageChoice, DimensionType, Image Comment, StorageMethodChoice, ReviewStatus, StorageChoice, Image
) )
from cvat.apps.engine.serializers import ( from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
@ -487,21 +487,17 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
return sendfile(request, frame_provider.get_preview()) return sendfile(request, frame_provider.get_preview())
elif data_type == 'context_image': elif data_type == 'context_image':
if db_task.dimension == DimensionType.DIM_3D: data_id = int(data_id)
data_id = int(data_id) image = Image.objects.get(data_id=db_data.id, frame=data_id)
image = Image.objects.get(data_id=db_task.data_id, frame=data_id) for i in image.related_files.all():
for i in image.related_files.all(): path = os.path.realpath(str(i.path))
path = os.path.realpath(str(i.path)) image = cv2.imread(path)
image = cv2.imread(path) success, result = cv2.imencode('.JPEG', image)
success, result = cv2.imencode('.JPEG', image) if not success:
if not success: raise Exception('Failed to encode image to ".jpeg" format')
raise Exception("Failed to encode image to '%s' format" % (".jpeg")) return HttpResponse(io.BytesIO(result.tobytes()), content_type='image/jpeg')
return HttpResponse(io.BytesIO(result.tobytes()), content_type="image/jpeg") return Response(data='No context image related to the frame',
return Response(data='No context image related to the frame', status=status.HTTP_404_NOT_FOUND)
status=status.HTTP_404_NOT_FOUND)
else:
return Response(data='Only 3D tasks support context images',
status=status.HTTP_400_BAD_REQUEST)
else: else:
return Response(data='unknown data type {}.'.format(data_type), status=status.HTTP_400_BAD_REQUEST) return Response(data='unknown data type {}.'.format(data_type), status=status.HTTP_400_BAD_REQUEST)
except APIException as e: except APIException as e:
@ -636,7 +632,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
@action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer, @action(detail=True, methods=['GET'], serializer_class=DataMetaSerializer,
url_path='data/meta') url_path='data/meta')
def data_info(request, pk): def data_info(request, pk):
db_task = models.Task.objects.prefetch_related('data__images').select_related('data__video').get(pk=pk) db_task = models.Task.objects.prefetch_related('data__images__related_files').select_related('data__video').get(pk=pk)
if hasattr(db_task.data, 'video'): if hasattr(db_task.data, 'video'):
media = [db_task.data.video] media = [db_task.data.video]
@ -647,6 +643,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
'width': item.width, 'width': item.width,
'height': item.height, 'height': item.height,
'name': item.path, 'name': item.path,
'has_related_context': hasattr(item, 'related_files') and bool(len(item.related_files.all()))
} for item in media] } for item in media]
db_data = db_task.data db_data = db_task.data

@ -57,10 +57,10 @@ context('Canvas 3D functionality. Basic actions.', () => {
} }
function testContextImage() { function testContextImage() {
cy.get('.cvat-contextImage-show').should('exist').and('be.visible'); cy.get('.cvat-context-image-wrapper img').should('exist').and('be.visible');
cy.get('[data-icon="camera"]').click(); // Context image hide cy.get('.cvat-context-image-switcher').click(); // Context image hide
cy.get('.cvat-contextImage-show').should('not.exist'); cy.get('.cvat-context-image-wrapper img').should('not.exist');
cy.get('[data-icon="camera"]').click(); // Context image show cy.get('.cvat-context-image-switcher').click(); // Context image show
} }
function testControlButtonTooltip(button, expectedTooltipText) { function testControlButtonTooltip(button, expectedTooltipText) {
@ -104,9 +104,11 @@ context('Canvas 3D functionality. Basic actions.', () => {
cy.get('.cvat-canvas3d-topview').should('exist').and('be.visible'); cy.get('.cvat-canvas3d-topview').should('exist').and('be.visible');
cy.get('.cvat-canvas3d-sideview').should('exist').and('be.visible'); cy.get('.cvat-canvas3d-sideview').should('exist').and('be.visible');
cy.get('.cvat-canvas3d-frontview').should('exist').and('be.visible'); cy.get('.cvat-canvas3d-frontview').should('exist').and('be.visible');
cy.get('.cvat-canvas-controls-sidebar').find('[role="img"]').then(($controlButtons) => { cy.get('.cvat-canvas-controls-sidebar')
expect($controlButtons.length).to.be.equal(4); .find('[role="img"]')
}); .then(($controlButtons) => {
expect($controlButtons.length).to.be.equal(3);
});
cy.get('.cvat-canvas-controls-sidebar') cy.get('.cvat-canvas-controls-sidebar')
.should('exist') .should('exist')
.and('be.visible') .and('be.visible')
@ -114,12 +116,10 @@ context('Canvas 3D functionality. Basic actions.', () => {
cy.get('.cvat-move-control').should('exist').and('be.visible'); cy.get('.cvat-move-control').should('exist').and('be.visible');
cy.get('.cvat-cursor-control').should('exist').and('be.visible'); cy.get('.cvat-cursor-control').should('exist').and('be.visible');
cy.get('.cvat-draw-cuboid-control').should('exist').and('be.visible'); cy.get('.cvat-draw-cuboid-control').should('exist').and('be.visible');
cy.get('.cvat-context-image-control').should('exist').and('be.visible');
}); });
[ [
['.cvat-move-control', 'Move the image'], ['.cvat-move-control', 'Move the image'],
['.cvat-cursor-control', 'Cursor [Esc]'], ['.cvat-cursor-control', 'Cursor [Esc]'],
['.cvat-context-image-control', 'Photo context show/hide']
].forEach(([button, tooltip]) => { ].forEach(([button, tooltip]) => {
testControlButtonTooltip(button, tooltip); testControlButtonTooltip(button, tooltip);
}); });

@ -34,7 +34,6 @@ context('Canvas 3D functionality. Control button. Mouse interaction.', () => {
before(() => { before(() => {
cy.openTaskJob(taskName); cy.openTaskJob(taskName);
cy.get('.cvat-contextImage-show').should('be.visible');
}); });
describe(`Testing case "${caseId}"`, () => { describe(`Testing case "${caseId}"`, () => {

@ -145,8 +145,9 @@ class VideoStreamReader:
class DatasetImagesReader: class DatasetImagesReader:
def __init__(self, sources, is_sorted=True, use_image_hash=False, *args, **kwargs): def __init__(self, sources, meta=None, is_sorted=True, use_image_hash=False, *args, **kwargs):
self._sources = sources if is_sorted else sorted(sources) self._sources = sources if is_sorted else sorted(sources)
self._meta = meta
self._content = [] self._content = []
self._data_dir = kwargs.get('data_dir', None) self._data_dir = kwargs.get('data_dir', None)
self._use_image_hash = use_image_hash self._use_image_hash = use_image_hash
@ -163,6 +164,8 @@ class DatasetImagesReader:
'width': img.width, 'width': img.width,
'height': img.height, 'height': img.height,
} }
if self._meta and img_name in self._meta:
image_properties['meta'] = self._meta[img_name]
if self._use_image_hash: if self._use_image_hash:
image_properties['checksum'] = md5_hash(img) image_properties['checksum'] = md5_hash(img)
yield image_properties yield image_properties
@ -177,7 +180,7 @@ class DatasetImagesReader:
class _Manifest: class _Manifest:
FILE_NAME = 'manifest.jsonl' FILE_NAME = 'manifest.jsonl'
VERSION = '1.0' VERSION = '1.1'
def __init__(self, path, is_created=False): def __init__(self, path, is_created=False):
assert path, 'A path to manifest file not found' assert path, 'A path to manifest file not found'

@ -2,21 +2,12 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import argparse import argparse
import mimetypes
import os import os
import sys import sys
import re
from glob import glob from glob import glob
def _define_data_type(media): from utils import detect_related_images, is_image, is_video
media_type, _ = mimetypes.guess_type(media)
if media_type:
return media_type.split('/')[0]
def _is_video(media_file):
return _define_data_type(media_file) == 'video'
def _is_image(media_file):
return _define_data_type(media_file) == 'image'
def get_args(): def get_args():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
@ -33,7 +24,7 @@ def main():
manifest_directory = os.path.abspath(args.output_dir) manifest_directory = os.path.abspath(args.output_dir)
os.makedirs(manifest_directory, exist_ok=True) os.makedirs(manifest_directory, exist_ok=True)
source = os.path.abspath(args.source) source = os.path.abspath(os.path.expanduser(args.source))
sources = [] sources = []
if not os.path.isfile(source): # directory/pattern with images if not os.path.isfile(source): # directory/pattern with images
@ -41,7 +32,7 @@ def main():
if os.path.isdir(source): if os.path.isdir(source):
data_dir = source data_dir = source
for root, _, files in os.walk(source): for root, _, files in os.walk(source):
sources.extend([os.path.join(root, f) for f in files if _is_image(f)]) sources.extend([os.path.join(root, f) for f in files if is_image(f)])
else: else:
items = source.lstrip('/').split('/') items = source.lstrip('/').split('/')
position = 0 position = 0
@ -56,18 +47,28 @@ def main():
data_dir = source.split(items[position])[0] data_dir = source.split(items[position])[0]
except Exception as ex: except Exception as ex:
sys.exit(str(ex)) sys.exit(str(ex))
sources = list(filter(_is_image, glob(source, recursive=True))) sources = list(filter(is_image, glob(source, recursive=True)))
sources = list(filter(lambda x: 'related_images{}'.format(os.sep) not in x, sources))
# If the source is a glob expression, we need additional processing
abs_root = source
while abs_root and re.search('[*?\[\]]', abs_root):
abs_root = os.path.split(abs_root)[0]
related_images = detect_related_images(sources, abs_root)
meta = { k: {'related_images': related_images[k] } for k in related_images }
try: try:
assert len(sources), 'A images was not found' assert len(sources), 'A images was not found'
manifest = ImageManifestManager(manifest_path=manifest_directory) manifest = ImageManifestManager(manifest_path=manifest_directory)
meta_info = manifest.prepare_meta(sources=sources, is_sorted=False, meta_info = manifest.prepare_meta(sources=sources, meta=meta, is_sorted=False,
use_image_hash=True, data_dir=data_dir) use_image_hash=True, data_dir=data_dir)
manifest.create(meta_info) manifest.create(meta_info)
except Exception as ex: except Exception as ex:
sys.exit(str(ex)) sys.exit(str(ex))
else: # video else: # video
try: try:
assert _is_video(source), 'You can specify a video path or a directory/pattern with images' assert is_video(source), 'You can specify a video path or a directory/pattern with images'
manifest = VideoManifestManager(manifest_path=manifest_directory) manifest = VideoManifestManager(manifest_path=manifest_directory)
try: try:
meta_info = manifest.prepare_meta(media_file=source, force=args.force) meta_info = manifest.prepare_meta(media_file=source, force=args.force)

@ -1,7 +1,10 @@
# Copyright (C) 2021 Intel Corporation # Copyright (C) 2021 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os
import re
import hashlib import hashlib
import mimetypes
import cv2 as cv import cv2 as cv
from av import VideoFrame from av import VideoFrame
@ -21,4 +24,163 @@ def rotate_image(image, angle):
def md5_hash(frame): def md5_hash(frame):
if isinstance(frame, VideoFrame): if isinstance(frame, VideoFrame):
frame = frame.to_image() frame = frame.to_image()
return hashlib.md5(frame.tobytes()).hexdigest() # nosec return hashlib.md5(frame.tobytes()).hexdigest() # nosec
def _define_data_type(media):
return mimetypes.guess_type(media)[0]
def is_video(media_file):
data_type = _define_data_type(media_file)
return data_type is not None and data_type.startswith('video')
def is_image(media_file):
data_type = _define_data_type(media_file)
return data_type is not None and data_type.startswith('image') and \
not data_type.startswith('image/svg')
def _list_and_join(root):
files = os.listdir(root)
for f in files:
yield os.path.join(root, f)
def _prepare_context_list(files, base_dir):
return sorted(map(lambda x: os.path.relpath(x, base_dir), filter(is_image, files)))
# Expected 2D format is:
# data/
# 00001.png
# related_images/
# 00001_png/
# context_image_1.jpeg
# context_image_2.png
def _detect_related_images_2D(image_paths, root_path):
related_images = {}
latest_dirname = ''
related_images_exist = False
for image_path in sorted(image_paths):
rel_image_path = os.path.relpath(image_path, root_path)
dirname = os.path.dirname(image_path)
related_images_dirname = os.path.join(dirname, 'related_images')
related_images[rel_image_path] = []
if latest_dirname == dirname and not related_images_exist:
continue
elif latest_dirname != dirname:
# Update some data applicable for a subset of paths (within the current dirname)
latest_dirname = dirname
related_images_exist = os.path.isdir(related_images_dirname)
if related_images_exist:
related_images_dirname = os.path.join(
related_images_dirname, '_'.join(os.path.basename(image_path).rsplit('.', 1))
)
if os.path.isdir(related_images_dirname):
related_images[rel_image_path] = _prepare_context_list(_list_and_join(related_images_dirname), root_path)
return related_images
# Possible 3D formats are:
# velodyne_points/
# data/
# image_01.bin
# IMAGE_00 # any number?
# data/
# image_01.png
# pointcloud/
# 00001.pcd
# related_images/
# 00001_pcd/
# image_01.png # or other image
# Default formats
# Option 1
# data/
# image.pcd
# image.png
# Option 2
# data/
# image_1/
# image_1.pcd
# context_1.png
# context_2.jpg
def _detect_related_images_3D(image_paths, root_path):
related_images = {}
latest_dirname = ''
dirname_files = []
related_images_exist = False
velodyne_context_images_dirs = []
for image_path in sorted(image_paths):
rel_image_path = os.path.relpath(image_path, root_path)
name = os.path.splitext(os.path.basename(image_path))[0]
dirname = os.path.dirname(image_path)
related_images_dirname = os.path.normpath(os.path.join(dirname, '..', 'related_images'))
related_images[rel_image_path] = []
if latest_dirname != dirname:
# Update some data applicable for a subset of paths (within the current dirname)
latest_dirname = dirname
related_images_exist = os.path.isdir(related_images_dirname)
dirname_files = list(filter(lambda x: x != image_path, _list_and_join(dirname)))
velodyne_context_images_dirs = [directory for directory
in _list_and_join(os.path.normpath(os.path.join(dirname, '..', '..')))
if os.path.isdir(os.path.join(directory, 'data')) and re.search(r'image_\d.*', directory, re.IGNORECASE)
]
if os.path.basename(dirname) == name:
# default format (option 2)
related_images[rel_image_path].extend(_prepare_context_list(dirname_files, root_path))
filtered_dirname_files = list(filter(lambda x: os.path.splitext(os.path.basename(x))[0] == name, dirname_files))
if len(filtered_dirname_files):
# default format (option 1)
related_images[rel_image_path].extend(_prepare_context_list(filtered_dirname_files, root_path))
if related_images_exist:
related_images_dirname = os.path.join(
related_images_dirname, '_'.join(os.path.basename(image_path).rsplit('.', 1))
)
if os.path.isdir(related_images_dirname):
related_images[rel_image_path].extend(
_prepare_context_list(_list_and_join(related_images_dirname), root_path)
)
if dirname.endswith(os.path.join('velodyne_points', 'data')):
# velodynepoints format
for context_images_dir in velodyne_context_images_dirs:
context_files = _list_and_join(os.path.join(context_images_dir, 'data'))
context_files = list(
filter(lambda x: os.path.splitext(os.path.basename(x))[0] == name, context_files)
)
related_images[rel_image_path].extend(
_prepare_context_list(context_files, root_path)
)
related_images[rel_image_path].sort()
return related_images
# This function is expected to be called only for images tasks
# image_path is expected to be a list of absolute path to images
# root_path is expected to be a string (dataset root)
def detect_related_images(image_paths, root_path):
data_are_2d = False
data_are_3d = False
# First of all need to define data type we are working with
for image_path in image_paths:
# .bin files are expected to be converted to .pcd before this code
if os.path.splitext(image_path)[1].lower() == '.pcd':
data_are_3d = True
else:
data_are_2d = True
assert not (data_are_3d and data_are_2d), 'Combined data types 2D and 3D are not supported'
if data_are_2d:
return _detect_related_images_2D(image_paths, root_path)
elif data_are_3d:
return _detect_related_images_3D(image_paths, root_path)
return {}

Loading…
Cancel
Save