CVAT-3D: support lidar data on the server side (#2534)

* CVAT-3D Updated the Mime Types with Bin Support, added dependency of open3D
* CVAT-3D Added additional column as Dimension for engine_task table and created a relatedfiles table for PCD to Image mapping.
* Added Support for 3D file Upload in BIN and PCD.
* Added Dimension attribute defaulting to 2D for importer and exporter.
* Added props passing for dimension attribute, filtering of import, Migration Scripts and Dimension attribute for MpegChunk Writers

Co-authored-by: cdp <cdp123>
main
manasars 5 years ago committed by GitHub
parent 3d8f3c6f0b
commit 069fadc053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -73,6 +73,8 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \ DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
apache2 \ apache2 \
libapache2-mod-xsendfile \ libapache2-mod-xsendfile \
libgomp1 \
libgl1 \
supervisor \ supervisor \
libldap-2.4-2 \ libldap-2.4-2 \
libsasl2-2 \ libsasl2-2 \

@ -15,6 +15,7 @@
format: initialData.ext, format: initialData.ext,
version: initialData.version, version: initialData.version,
enabled: initialData.enabled, enabled: initialData.enabled,
dimension: initialData.dimension,
}; };
Object.defineProperties(this, { Object.defineProperties(this, {
@ -58,6 +59,16 @@
*/ */
get: () => data.enabled, get: () => data.enabled,
}, },
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
}); });
} }
} }
@ -74,6 +85,7 @@
format: initialData.ext, format: initialData.ext,
version: initialData.version, version: initialData.version,
enabled: initialData.enabled, enabled: initialData.enabled,
dimension: initialData.dimension,
}; };
Object.defineProperties(this, { Object.defineProperties(this, {
@ -117,6 +129,16 @@
*/ */
get: () => data.enabled, get: () => data.enabled,
}, },
dimension: {
/**
* @name dimension
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
}); });
} }
} }

@ -10,7 +10,7 @@
isBoolean, isInteger, isEnum, isString, checkFilter, isBoolean, isInteger, isEnum, isString, checkFilter,
} = require('./common'); } = require('./common');
const { TaskStatus, TaskMode } = require('./enums'); const { TaskStatus, TaskMode, DimensionType } = require('./enums');
const User = require('./user'); const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats'); const { AnnotationFormats } = require('./annotation-formats');
@ -176,6 +176,7 @@
search: isString, search: isString,
status: isEnum.bind(TaskStatus), status: isEnum.bind(TaskStatus),
mode: isEnum.bind(TaskMode), mode: isEnum.bind(TaskMode),
dimension: isEnum.bind(DimensionType),
}); });
if ('search' in filter && Object.keys(filter).length > 1) { if ('search' in filter && Object.keys(filter).length > 1) {
@ -198,7 +199,7 @@
} }
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) { for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId', 'dimension']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]); searchParams.set(field, filter[field]);
} }

@ -33,6 +33,20 @@
COMPLETED: 'completed', COMPLETED: 'completed',
}); });
/**
* Task dimension
* @enum
* @name DimensionType
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
});
/** /**
* Review statuses * Review statuses
* @enum {string} * @enum {string}
@ -333,5 +347,6 @@
RQStatus, RQStatus,
colors, colors,
Source, Source,
DimensionType,
}; };
})(); })();

@ -974,6 +974,7 @@
use_zip_chunks: undefined, use_zip_chunks: undefined,
use_cache: undefined, use_cache: undefined,
copy_data: undefined, copy_data: undefined,
dimension: undefined,
}; };
let updatedFields = { let updatedFields = {
@ -1452,6 +1453,16 @@
updatedFields = fields; updatedFields = fields;
}, },
}, },
dimension: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
}), }),
); );

@ -11,6 +11,7 @@ import { MenuInfo } from 'rc-menu/lib/interface';
import DumpSubmenu from './dump-submenu'; import DumpSubmenu from './dump-submenu';
import LoadSubmenu from './load-submenu'; import LoadSubmenu from './load-submenu';
import ExportSubmenu from './export-submenu'; import ExportSubmenu from './export-submenu';
import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
taskID: number; taskID: number;
@ -22,7 +23,7 @@ interface Props {
dumpActivities: string[] | null; dumpActivities: string[] | null;
exportActivities: string[] | null; exportActivities: string[] | null;
inferenceIsActive: boolean; inferenceIsActive: boolean;
taskDimension: DimensionType;
onClickMenu: (params: MenuInfo, file?: File) => void; onClickMenu: (params: MenuInfo, file?: File) => void;
} }
@ -47,6 +48,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
dumpActivities, dumpActivities,
exportActivities, exportActivities,
loadActivity, loadActivity,
taskDimension,
} = props; } = props;
let latestParams: MenuInfo | null = null; let latestParams: MenuInfo | null = null;
@ -102,6 +104,7 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
dumpers, dumpers,
dumpActivities, dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO, menuKey: Actions.DUMP_TASK_ANNO,
taskDimension,
})} })}
{LoadSubmenu({ {LoadSubmenu({
loaders, loaders,
@ -110,11 +113,13 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
onClickMenuWrapper(null, file); onClickMenuWrapper(null, file);
}, },
menuKey: Actions.LOAD_TASK_ANNO, menuKey: Actions.LOAD_TASK_ANNO,
taskDimension,
})} })}
{ExportSubmenu({ {ExportSubmenu({
exporters: dumpers, exporters: dumpers,
exportActivities, exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET, menuKey: Actions.EXPORT_TASK_DATASET,
taskDimension,
})} })}
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>} {!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}> <Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>

@ -6,6 +6,7 @@ import React from 'react';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons'; import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { DimensionType } from '../../reducers/interfaces';
function isDefaultFormat(dumperName: string, taskMode: string): boolean { function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return ( return (
@ -19,15 +20,19 @@ interface Props {
menuKey: string; menuKey: string;
dumpers: any[]; dumpers: any[];
dumpActivities: string[] | null; dumpActivities: string[] | null;
taskDimension: DimensionType;
} }
export default function DumpSubmenu(props: Props): JSX.Element { export default function DumpSubmenu(props: Props): JSX.Element {
const { taskMode, menuKey, dumpers, dumpActivities } = props; const {
taskMode, menuKey, dumpers, dumpActivities, taskDimension,
} = props;
return ( return (
<Menu.SubMenu key={menuKey} title='Dump annotations'> <Menu.SubMenu key={menuKey} title='Dump annotations'>
{dumpers {dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name)) .sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((dumper: any): boolean => dumper.dimension === taskDimension)
.map( .map(
(dumper: any): JSX.Element => { (dumper: any): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper.name); const pending = (dumpActivities || []).includes(dumper.name);

@ -6,20 +6,25 @@ import React from 'react';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { ExportOutlined, LoadingOutlined } from '@ant-design/icons'; import { ExportOutlined, LoadingOutlined } from '@ant-design/icons';
import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
menuKey: string; menuKey: string;
exporters: any[]; exporters: any[];
exportActivities: string[] | null; exportActivities: string[] | null;
taskDimension: DimensionType;
} }
export default function ExportSubmenu(props: Props): JSX.Element { export default function ExportSubmenu(props: Props): JSX.Element {
const { menuKey, exporters, exportActivities } = props; const {
menuKey, exporters, exportActivities, taskDimension,
} = props;
return ( return (
<Menu.SubMenu key={menuKey} title='Export as a dataset'> <Menu.SubMenu key={menuKey} title='Export as a dataset'>
{exporters {exporters
.sort((a: any, b: any) => a.name.localeCompare(b.name)) .sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((exporter: any): boolean => exporter.dimension === taskDimension)
.map( .map(
(exporter: any): JSX.Element => { (exporter: any): JSX.Element => {
const pending = (exportActivities || []).includes(exporter.name); const pending = (exportActivities || []).includes(exporter.name);

@ -8,23 +8,26 @@ import Upload from 'antd/lib/upload';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { UploadOutlined, LoadingOutlined } from '@ant-design/icons'; import { UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
menuKey: string; menuKey: string;
loaders: any[]; loaders: any[];
loadActivity: string | null; loadActivity: string | null;
onFileUpload(file: File): void; onFileUpload(file: File): void;
taskDimension: DimensionType;
} }
export default function LoadSubmenu(props: Props): JSX.Element { export default function LoadSubmenu(props: Props): JSX.Element {
const { const {
menuKey, loaders, loadActivity, onFileUpload, menuKey, loaders, loadActivity, onFileUpload, taskDimension,
} = props; } = props;
return ( return (
<Menu.SubMenu key={menuKey} title='Upload annotations'> <Menu.SubMenu key={menuKey} title='Upload annotations'>
{loaders {loaders
.sort((a: any, b: any) => a.name.localeCompare(b.name)) .sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((loader: any): boolean => loader.dimension === taskDimension)
.map( .map(
(loader: any): JSX.Element => { (loader: any): JSX.Element => {
const accept = loader.format const accept = loader.format

@ -164,6 +164,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
dumpers, dumpers,
dumpActivities, dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO, menuKey: Actions.DUMP_TASK_ANNO,
taskDimension: jobInstance.task.dimension,
})} })}
{LoadSubmenu({ {LoadSubmenu({
loaders, loaders,
@ -172,11 +173,13 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
onClickMenuWrapper(null, file); onClickMenuWrapper(null, file);
}, },
menuKey: Actions.LOAD_JOB_ANNO, menuKey: Actions.LOAD_JOB_ANNO,
taskDimension: jobInstance.task.dimension,
})} })}
{ExportSubmenu({ {ExportSubmenu({
exporters: dumpers, exporters: dumpers,
exportActivities, exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET, menuKey: Actions.EXPORT_TASK_DATASET,
taskDimension: jobInstance.task.dimension,
})} })}
<Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item> <Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item>

@ -21,6 +21,7 @@ import { Model, StringObject } from 'reducers/interfaces';
import { clamp } from 'utils/math'; import { clamp } from 'utils/math';
import consts from 'consts'; import consts from 'consts';
import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
withCleanup: boolean; withCleanup: boolean;
@ -127,7 +128,8 @@ function DetectorRunner(props: Props): JSX.Element {
<Col span={4}>Model:</Col> <Col span={4}>Model:</Col>
<Col span={20}> <Col span={20}>
<Select <Select
placeholder='Select a model' placeholder={task.dimension === DimensionType.DIM_2D ? 'Select a model' : 'No models available'}
disabled={task.dimension !== DimensionType.DIM_2D}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={(_modelID: string): void => { onChange={(_modelID: string): void => {
const newmodel = models.filter((_model): boolean => _model.id === _modelID)[0]; const newmodel = models.filter((_model): boolean => _model.id === _modelID)[0];

@ -137,6 +137,7 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
exportActivities={exportActivities} exportActivities={exportActivities}
inferenceIsActive={inferenceIsActive} inferenceIsActive={inferenceIsActive}
onClickMenu={onClickMenu} onClickMenu={onClickMenu}
taskDimension={taskInstance.dimension}
/> />
); );
} }

@ -568,3 +568,8 @@ export interface CombinedState {
shortcuts: ShortcutsState; shortcuts: ShortcutsState;
review: ReviewState; review: ReviewState;
} }
export enum DimensionType {
DIM_3D = '3d',
DIM_2D = '2d',
}

@ -4,6 +4,7 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from datumaro.components.project import Environment from datumaro.components.project import Environment
from cvat.apps.engine.models import DimensionType
dm_env = Environment() dm_env = Environment()
@ -23,7 +24,7 @@ class Importer(_Format):
def __call__(self, src_file, task_data, **options): def __call__(self, src_file, task_data, **options):
raise NotImplementedError() raise NotImplementedError()
def _wrap_format(f_or_cls, klass, name, version, ext, display_name, enabled): def _wrap_format(f_or_cls, klass, name, version, ext, display_name, enabled, dimension=DimensionType.DIM_2D):
import inspect import inspect
assert inspect.isclass(f_or_cls) or inspect.isfunction(f_or_cls) assert inspect.isclass(f_or_cls) or inspect.isfunction(f_or_cls)
if inspect.isclass(f_or_cls): if inspect.isclass(f_or_cls):
@ -45,17 +46,18 @@ def _wrap_format(f_or_cls, klass, name, version, ext, display_name, enabled):
target.DISPLAY_NAME = (display_name or klass.DISPLAY_NAME).format( target.DISPLAY_NAME = (display_name or klass.DISPLAY_NAME).format(
NAME=name, VERSION=version, EXT=ext) NAME=name, VERSION=version, EXT=ext)
assert all([target.NAME, target.VERSION, target.EXT, target.DISPLAY_NAME]) assert all([target.NAME, target.VERSION, target.EXT, target.DISPLAY_NAME])
target.DIMENSION = dimension
target.ENABLED = enabled target.ENABLED = enabled
return target return target
EXPORT_FORMATS = {} EXPORT_FORMATS = {}
def exporter(name, version, ext, display_name=None, enabled=True): def exporter(name, version, ext, display_name=None, enabled=True, dimension=DimensionType.DIM_2D):
assert name not in EXPORT_FORMATS, "Export format '%s' already registered" % name assert name not in EXPORT_FORMATS, "Export format '%s' already registered" % name
def wrap_with_params(f_or_cls): def wrap_with_params(f_or_cls):
t = _wrap_format(f_or_cls, Exporter, t = _wrap_format(f_or_cls, Exporter,
name=name, ext=ext, version=version, display_name=display_name, name=name, ext=ext, version=version, display_name=display_name,
enabled=enabled) enabled=enabled, dimension=dimension)
key = t.DISPLAY_NAME key = t.DISPLAY_NAME
assert key not in EXPORT_FORMATS, "Export format '%s' already registered" % name assert key not in EXPORT_FORMATS, "Export format '%s' already registered" % name
EXPORT_FORMATS[key] = t EXPORT_FORMATS[key] = t
@ -63,11 +65,11 @@ def exporter(name, version, ext, display_name=None, enabled=True):
return wrap_with_params return wrap_with_params
IMPORT_FORMATS = {} IMPORT_FORMATS = {}
def importer(name, version, ext, display_name=None, enabled=True): def importer(name, version, ext, display_name=None, enabled=True, dimension=DimensionType.DIM_2D):
def wrap_with_params(f_or_cls): def wrap_with_params(f_or_cls):
t = _wrap_format(f_or_cls, Importer, t = _wrap_format(f_or_cls, Importer,
name=name, ext=ext, version=version, display_name=display_name, name=name, ext=ext, version=version, display_name=display_name,
enabled=enabled) enabled=enabled, dimension=dimension)
key = t.DISPLAY_NAME key = t.DISPLAY_NAME
assert key not in IMPORT_FORMATS, "Import format '%s' already registered" % name assert key not in IMPORT_FORMATS, "Import format '%s' already registered" % name
IMPORT_FORMATS[key] = t IMPORT_FORMATS[key] = t
@ -92,4 +94,4 @@ import cvat.apps.dataset_manager.formats.pascal_voc
import cvat.apps.dataset_manager.formats.tfrecord import cvat.apps.dataset_manager.formats.tfrecord
import cvat.apps.dataset_manager.formats.yolo import cvat.apps.dataset_manager.formats.yolo
import cvat.apps.dataset_manager.formats.imagenet import cvat.apps.dataset_manager.formats.imagenet
import cvat.apps.dataset_manager.formats.camvid import cvat.apps.dataset_manager.formats.camvid

@ -10,7 +10,8 @@ class DatasetFormatSerializer(serializers.Serializer):
ext = serializers.CharField(max_length=64, source='EXT') ext = serializers.CharField(max_length=64, source='EXT')
version = serializers.CharField(max_length=64, source='VERSION') version = serializers.CharField(max_length=64, source='VERSION')
enabled = serializers.BooleanField(source='ENABLED') enabled = serializers.BooleanField(source='ENABLED')
dimension = serializers.CharField(max_length=2, source='DIMENSION')
class DatasetFormatsSerializer(serializers.Serializer): class DatasetFormatsSerializer(serializers.Serializer):
importers = DatasetFormatSerializer(many=True) importers = DatasetFormatSerializer(many=True)
exporters = DatasetFormatSerializer(many=True) exporters = DatasetFormatSerializer(many=True)

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

@ -12,10 +12,12 @@ from cvat.apps.engine.media_extractors import (Mpeg4ChunkWriter,
Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter) Mpeg4CompressedChunkWriter, ZipChunkWriter, ZipCompressedChunkWriter)
from cvat.apps.engine.models import DataChoice, StorageChoice from cvat.apps.engine.models import DataChoice, StorageChoice
from cvat.apps.engine.prepare import PrepareInfo from cvat.apps.engine.prepare import PrepareInfo
from cvat.apps.engine.models import DimensionType
class CacheInteraction: class CacheInteraction:
def __init__(self): def __init__(self, dimension=DimensionType.DIM_2D):
self._cache = Cache(settings.CACHE_ROOT) self._cache = Cache(settings.CACHE_ROOT)
self._dimension = dimension
def __del__(self): def __del__(self):
self._cache.close() self._cache.close()
@ -38,7 +40,10 @@ class CacheInteraction:
image_quality = 100 if writer_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality image_quality = 100 if writer_classes[quality] in [Mpeg4ChunkWriter, ZipChunkWriter] else db_data.image_quality
mime_type = 'video/mp4' if writer_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip' mime_type = 'video/mp4' if writer_classes[quality] in [Mpeg4ChunkWriter, Mpeg4CompressedChunkWriter] else 'application/zip'
writer = writer_classes[quality](image_quality) kwargs = {}
if self._dimension == DimensionType.DIM_3D:
kwargs["dimension"] = DimensionType.DIM_3D
writer = writer_classes[quality](image_quality, **kwargs)
images = [] images = []
buff = BytesIO() buff = BytesIO()

@ -13,7 +13,7 @@ from PIL import Image
from cvat.apps.engine.cache import CacheInteraction from cvat.apps.engine.cache import CacheInteraction
from cvat.apps.engine.media_extractors import VideoReader, ZipReader from cvat.apps.engine.media_extractors import VideoReader, ZipReader
from cvat.apps.engine.mime_types import mimetypes from cvat.apps.engine.mime_types import mimetypes
from cvat.apps.engine.models import DataChoice, StorageMethodChoice from cvat.apps.engine.models import DataChoice, StorageMethodChoice, DimensionType
class RandomAccessIterator: class RandomAccessIterator:
@ -83,7 +83,7 @@ class FrameProvider:
self.reader_class([self.get_chunk_path(chunk_id, self.quality, self.db_data)[0]])) self.reader_class([self.get_chunk_path(chunk_id, self.quality, self.db_data)[0]]))
return self.chunk_reader return self.chunk_reader
def __init__(self, db_data): def __init__(self, db_data, dimension=DimensionType.DIM_2D):
self._db_data = db_data self._db_data = db_data
self._loaders = {} self._loaders = {}
@ -93,7 +93,7 @@ class FrameProvider:
} }
if db_data.storage_method == StorageMethodChoice.CACHE: if db_data.storage_method == StorageMethodChoice.CACHE:
cache = CacheInteraction() cache = CacheInteraction(dimension=dimension)
self._loaders[self.Quality.COMPRESSED] = self.BuffChunkLoader( self._loaders[self.Quality.COMPRESSED] = self.BuffChunkLoader(
reader_class[db_data.compressed_chunk_type], reader_class[db_data.compressed_chunk_type],

@ -106,7 +106,8 @@ image/x-jng jng
image/jp2 jp2 image/jp2 jp2
image/x-portable-bitmap pbm image/x-portable-bitmap pbm
image/x-eps epsf image/x-eps epsf
image/x-photo-cd pcd image/x.point-cloud-data pcd
image/x.kitti-velodyne bin
image/jpeg jpe image/jpeg jpe
image/jp2 jpf image/jp2 jpf
image/jpeg jpg image/jpeg jpg

@ -8,13 +8,17 @@ import shutil
import zipfile import zipfile
import io import io
import itertools import itertools
import struct
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import av import av
import numpy as np import numpy as np
from pyunpack import Archive from pyunpack import Archive
from PIL import Image, ImageFile from PIL import Image, ImageFile
import open3d as o3d
from cvat.apps.engine.utils import rotate_image from cvat.apps.engine.utils import rotate_image
from cvat.apps.engine.models import DimensionType
# fixes: "OSError:broken data stream" when executing line 72 while loading images downloaded from the web # fixes: "OSError:broken data stream" when executing line 72 while loading images downloaded from the web
# see: https://stackoverflow.com/questions/42462431/oserror-broken-data-stream-when-reading-image-file # see: https://stackoverflow.com/questions/42462431/oserror-broken-data-stream-when-reading-image-file
@ -179,7 +183,8 @@ class PdfReader(ImageListReader):
class ZipReader(ImageListReader): class ZipReader(ImageListReader):
def __init__(self, source_path, step=1, start=0, stop=None): def __init__(self, source_path, step=1, start=0, stop=None):
self._zip_source = zipfile.ZipFile(source_path[0], mode='r') self._dimension = DimensionType.DIM_2D
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 get_mime(f) == 'image'] file_list = [f for f in self._zip_source.namelist() if get_mime(f) == 'image']
super().__init__(file_list, step, start, stop) super().__init__(file_list, step, start, stop)
@ -188,18 +193,42 @@ class ZipReader(ImageListReader):
self._zip_source.close() self._zip_source.close()
def get_preview(self): def get_preview(self):
if self._dimension == DimensionType.DIM_3D:
fp = open(os.path.join(os.path.dirname(__file__), 'assets/3d_preview.jpeg'), "rb")
return self._get_preview(fp)
io_image = io.BytesIO(self._zip_source.read(self._source_path[0])) io_image = io.BytesIO(self._zip_source.read(self._source_path[0]))
return self._get_preview(io_image) return self._get_preview(io_image)
def get_image_size(self, i): def get_image_size(self, i):
if self._dimension == DimensionType.DIM_3D:
with self._zip_source.open(self._source_path[i], "r") as file:
properties = ValidateDimension.get_pcd_properties(file)
return int(properties["WIDTH"]), int(properties["HEIGHT"])
img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i])))
return img.width, img.height return img.width, img.height
def get_image(self, i): def get_image(self, i):
return io.BytesIO(self._zip_source.read(self._source_path[i])) return io.BytesIO(self._zip_source.read(self._source_path[i]))
def add_files(self, source_path):
root_path = os.path.split(self._zip_source.filename)[0]
for path in source_path:
self._zip_source.write(path, path.replace(root_path, ""))
def get_zip_filename(self):
return self._zip_source.filename
def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D):
self._dimension = dimension
super().__init__(
source_path=source_files,
step=step,
start=start,
stop=stop
)
def get_path(self, i): def get_path(self, i):
if self._zip_source.filename: if self._zip_source.filename:
return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) \ return os.path.join(os.path.dirname(self._zip_source.filename), self._source_path[i]) \
if not self.extract_dir else os.path.join(self.extract_dir, self._source_path[i]) if not self.extract_dir else os.path.join(self.extract_dir, self._source_path[i])
else: # necessary for mime_type definition else: # necessary for mime_type definition
@ -283,8 +312,9 @@ class VideoReader(IMediaReader):
return image.width, image.height return image.width, image.height
class IChunkWriter(ABC): class IChunkWriter(ABC):
def __init__(self, quality): def __init__(self, quality, dimension=DimensionType.DIM_2D):
self._image_quality = quality self._image_quality = quality
self._dimension = dimension
@staticmethod @staticmethod
def _compress_image(image_path, quality): def _compress_image(image_path, quality):
@ -326,12 +356,20 @@ class ZipCompressedChunkWriter(IChunkWriter):
def save_as_chunk(self, images, chunk_path): def save_as_chunk(self, images, chunk_path):
image_sizes = [] image_sizes = []
with zipfile.ZipFile(chunk_path, 'x') as zip_chunk: with zipfile.ZipFile(chunk_path, 'x') as zip_chunk:
for idx, (image, _ , _) in enumerate(images): for idx, (image, _, _) in enumerate(images):
w, h, image_buf = self._compress_image(image, self._image_quality) if self._dimension == DimensionType.DIM_2D:
w, h, image_buf = self._compress_image(image, self._image_quality)
extension = "jpeg"
else:
image_buf = open(image, "rb") if isinstance(image, str) else image
properties = ValidateDimension.get_pcd_properties(image_buf)
w, h = int(properties["WIDTH"]), int(properties["HEIGHT"])
extension = "pcd"
image_buf.seek(0, 0)
image_buf = io.BytesIO(image_buf.read())
image_sizes.append((w, h)) image_sizes.append((w, h))
arcname = '{:06d}.jpeg'.format(idx) arcname = '{:06d}.{}'.format(idx, extension)
zip_chunk.writestr(arcname, image_buf.getvalue()) zip_chunk.writestr(arcname, image_buf.getvalue())
return image_sizes return image_sizes
class Mpeg4ChunkWriter(IChunkWriter): class Mpeg4ChunkWriter(IChunkWriter):
@ -511,3 +549,173 @@ MEDIA_TYPES = {
'unique': True, 'unique': True,
} }
} }
class ValidateDimension:
def __init__(self, path=None):
self.dimension = DimensionType.DIM_2D
self.path = path
self.related_files = {}
self.image_files = {}
self.converted_files = []
@staticmethod
def get_pcd_properties(fp, verify_version=False):
kv = {}
pcd_version = ["0.7", "0.6", "0.5", "0.4", "0.3", "0.2", "0.1",
".7", ".6", ".5", ".4", ".3", ".2", ".1"]
try:
for line in fp:
line = line.decode("utf-8")
if line.startswith("#"):
continue
k, v = line.split(" ", maxsplit=1)
kv[k] = v.strip()
if "DATA" in line:
break
if verify_version:
if "VERSION" in kv and kv["VERSION"] in pcd_version:
return True
return None
return kv
except AttributeError:
return None
@staticmethod
def convert_bin_to_pcd(path, delete_source=True):
list_pcd = []
with open(path, "rb") as f:
size_float = 4
byte = f.read(size_float * 4)
while byte:
x, y, z, _ = struct.unpack("ffff", byte)
list_pcd.append([x, y, z])
byte = f.read(size_float * 4)
np_pcd = np.asarray(list_pcd)
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(np_pcd)
pcd_filename = path.replace(".bin", ".pcd")
o3d.io.write_point_cloud(pcd_filename, pcd)
if delete_source:
os.remove(path)
return pcd_filename
def set_path(self, path):
self.path = path
def bin_operation(self, file_path, actual_path):
pcd_path = ValidateDimension.convert_bin_to_pcd(file_path)
self.converted_files.append(pcd_path)
return pcd_path.split(actual_path)[-1][1:]
@staticmethod
def pcd_operation(file_path, actual_path):
with open(file_path, "rb") as file:
is_pcd = ValidateDimension.get_pcd_properties(file, verify_version=True)
return file_path.split(actual_path)[-1][1:] if is_pcd else file_path
def process_files(self, root, actual_path, files):
pcd_files = {}
for file in files:
file_name, file_extension = file.rsplit('.', maxsplit=1)
file_path = os.path.abspath(os.path.join(root, file))
if file_extension == "bin":
path = self.bin_operation(file_path, actual_path)
pcd_files[file_name] = path
self.related_files[path] = []
elif file_extension == "pcd":
path = ValidateDimension.pcd_operation(file_path, actual_path)
if path == file_path:
self.image_files[file_name] = file_path
else:
pcd_files[file_name] = path
self.related_files[path] = []
else:
self.image_files[file_name] = file_path
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.split("pointcloud")[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.split("_")[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].split(".")[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):
"""
Validate the directory structure for kitty and point cloud format.
"""
if not self.path:
return
actual_path = self.path
for root, _, files in os.walk(actual_path):
if root.endswith("data"):
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()):
self.dimension = DimensionType.DIM_3D

@ -0,0 +1,33 @@
# Generated by Django 3.1.1 on 2020-12-16 09:43
import cvat.apps.engine.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('engine', '0035_data_storage'),
]
operations = [
migrations.AddField(
model_name='task',
name='dimension',
field=models.CharField(choices=[('3d', 'DIM_3D'), ('2d', 'DIM_2D')], default=cvat.apps.engine.models.DimensionType['DIM_2D'], max_length=2),
),
migrations.CreateModel(
name='RelatedFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.FileField(max_length=1024, storage=cvat.apps.engine.models.MyFileSystemStorage(), upload_to=cvat.apps.engine.models.upload_path_handler)),
('data', models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_files', to='engine.data')),
('primary_image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='related_files', to='engine.image')),
],
options={
'default_permissions': (),
'unique_together': {('data', 'path')},
},
),
]

@ -19,6 +19,17 @@ class SafeCharField(models.CharField):
return value[:self.max_length] return value[:self.max_length]
return value return value
class DimensionType(str, Enum):
DIM_3D = '3d'
DIM_2D = '2d'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class StatusChoice(str, Enum): class StatusChoice(str, Enum):
ANNOTATION = 'annotation' ANNOTATION = 'annotation'
VALIDATION = 'validation' VALIDATION = 'validation'
@ -202,6 +213,7 @@ class Task(models.Model):
status = models.CharField(max_length=32, choices=StatusChoice.choices(), status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION) default=StatusChoice.ANNOTATION)
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name="tasks") data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name="tasks")
dimension = models.CharField(max_length=2, choices=DimensionType.choices(), default=DimensionType.DIM_2D)
# Extend default permission model # Extend default permission model
class Meta: class Meta:
@ -265,6 +277,17 @@ class RemoteFile(models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
class RelatedFile(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, related_name="related_files", default=1, null=True)
path = models.FileField(upload_to=upload_path_handler,
max_length=1024, storage=MyFileSystemStorage())
primary_image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="related_files", null=True)
class Meta:
default_permissions = ()
unique_together = ("data", "path")
class Segment(models.Model): class Segment(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE)
start_frame = models.IntegerField() start_frame = models.IntegerField()

@ -329,13 +329,15 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
assignee = BasicUserSerializer(allow_null=True, required=False) assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
project_id = serializers.IntegerField(required=False) project_id = serializers.IntegerField(required=False)
dimension = serializers.CharField(allow_blank=True, required=False)
class Meta: class Meta:
model = models.Task model = models.Task
fields = ('url', 'id', 'name', 'project_id', 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id', fields = ('url', 'id', 'name', 'project_id', 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'overlap', 'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'status', 'labels', 'segments', 'segment_size', 'status', 'labels', 'segments',
'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality',
'data', 'dimension')
read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'assignee', read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'assignee',
'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
write_once_fields = ('overlap', 'segment_size', 'project_id') write_once_fields = ('overlap', 'segment_size', 'project_id')

@ -14,10 +14,11 @@ from urllib import error as urlerror
from urllib import parse as urlparse from urllib import parse as urlparse
from urllib import request as urlrequest from urllib import request as urlrequest
from cvat.apps.engine.media_extractors import get_mime, MEDIA_TYPES, Mpeg4ChunkWriter, ZipChunkWriter, Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter from cvat.apps.engine.media_extractors import get_mime, MEDIA_TYPES, Mpeg4ChunkWriter, ZipChunkWriter, Mpeg4CompressedChunkWriter, ZipCompressedChunkWriter, ValidateDimension
from cvat.apps.engine.models import DataChoice, StorageMethodChoice, StorageChoice from cvat.apps.engine.models import DataChoice, StorageMethodChoice, StorageChoice, RelatedFile
from cvat.apps.engine.utils import av_scan_paths from cvat.apps.engine.utils import av_scan_paths
from cvat.apps.engine.prepare import prepare_meta from cvat.apps.engine.prepare import prepare_meta
from cvat.apps.engine.models import DimensionType
import django_rq import django_rq
from django.conf import settings from django.conf import settings
@ -259,8 +260,25 @@ def _create_thread(tid, data):
start=db_data.start_frame, start=db_data.start_frame,
stop=data['stop_frame'], stop=data['stop_frame'],
) )
validate_dimension = ValidateDimension()
if extractor.__class__ == MEDIA_TYPES['zip']['extractor']: if extractor.__class__ == MEDIA_TYPES['zip']['extractor']:
extractor.extract() extractor.extract()
validate_dimension.set_path(os.path.split(extractor.get_zip_filename())[0])
validate_dimension.validate()
if validate_dimension.dimension == DimensionType.DIM_3D:
db_task.dimension = DimensionType.DIM_3D
extractor.reconcile(
source_files=list(validate_dimension.related_files.keys()),
step=db_data.get_frame_step(),
start=db_data.start_frame,
stop=data['stop_frame'],
dimension=DimensionType.DIM_3D,
)
extractor.add_files(validate_dimension.converted_files)
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
@ -282,7 +300,10 @@ def _create_thread(tid, data):
compressed_chunk_writer_class = Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter compressed_chunk_writer_class = Mpeg4CompressedChunkWriter if db_data.compressed_chunk_type == DataChoice.VIDEO else ZipCompressedChunkWriter
original_chunk_writer_class = Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter original_chunk_writer_class = Mpeg4ChunkWriter if db_data.original_chunk_type == DataChoice.VIDEO else ZipChunkWriter
compressed_chunk_writer = compressed_chunk_writer_class(db_data.image_quality) kwargs = {}
if validate_dimension.dimension == DimensionType.DIM_3D:
kwargs["dimension"] = validate_dimension.dimension
compressed_chunk_writer = compressed_chunk_writer_class(db_data.image_quality, **kwargs)
original_chunk_writer = original_chunk_writer_class(100) original_chunk_writer = original_chunk_writer_class(100)
# calculate chunk size if it isn't specified # calculate chunk size if it isn't specified
@ -399,7 +420,27 @@ def _create_thread(tid, data):
update_progress(progress) update_progress(progress)
if db_task.mode == 'annotation': if db_task.mode == 'annotation':
models.Image.objects.bulk_create(db_images) if validate_dimension.dimension == DimensionType.DIM_2D:
models.Image.objects.bulk_create(db_images)
else:
related_file = []
for image_data in db_images:
image_model = models.Image(
data=image_data.data,
path=image_data.path,
frame=image_data.frame,
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(
@ -414,4 +455,4 @@ def _create_thread(tid, data):
preview.save(db_data.get_preview_path()) preview.save(db_data.get_preview_path())
slogger.glob.info("Found frames {} for Data #{}".format(db_data.size, db_data.id)) slogger.glob.info("Found frames {} for Data #{}".format(db_data.size, db_data.id))
_save_task_to_db(db_task) _save_task_to_db(db_task)

@ -16,6 +16,8 @@ from enum import Enum
from glob import glob from glob import glob
from io import BytesIO from io import BytesIO
from unittest import mock from unittest import mock
import open3d as o3d
import struct
import av import av
import numpy as np import numpy as np
@ -31,6 +33,8 @@ from rest_framework.test import APIClient, APITestCase
from cvat.apps.engine.models import (AttributeType, Data, Job, Project, from cvat.apps.engine.models import (AttributeType, Data, Job, Project,
Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice) Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice)
from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload
from cvat.apps.engine.media_extractors import ValidateDimension
from cvat.apps.engine.models import DimensionType
def create_db_users(cls): def create_db_users(cls):
(group_admin, _) = Group.objects.get_or_create(name="admin") (group_admin, _) = Group.objects.get_or_create(name="admin")
@ -1840,6 +1844,47 @@ class TaskDataAPITestCase(APITestCase):
zip_archive.write(data.read()) zip_archive.write(data.read())
cls._image_sizes[filename] = img_sizes cls._image_sizes[filename] = img_sizes
filename = "test_pointcloud_pcd.zip"
path = os.path.join(os.path.dirname(__file__), 'assets', filename)
image_sizes = []
# container = av.open(path, 'r')
zip_file = zipfile.ZipFile(path)
for info in zip_file.namelist():
if info.rsplit(".", maxsplit=1)[-1] == "pcd":
with zip_file.open(info, "r") as file:
data = ValidateDimension.get_pcd_properties(file)
image_sizes.append((int(data["WIDTH"]), int(data["HEIGHT"])))
cls._image_sizes[filename] = image_sizes
filename = "test_velodyne_points.zip"
path = os.path.join(os.path.dirname(__file__), 'assets', filename)
image_sizes = []
# create zip instance
zip_file = zipfile.ZipFile(path, mode='a')
source_path = []
root_path = os.path.abspath(os.path.split(path)[0])
for info in zip_file.namelist():
if os.path.splitext(info)[1][1:] == "bin":
zip_file.extract(info, root_path)
bin_path = os.path.abspath(os.path.join(root_path, info))
source_path.append(ValidateDimension.convert_bin_to_pcd(bin_path))
for path in source_path:
zip_file.write(path, os.path.abspath(path.replace(root_path, "")))
for info in zip_file.namelist():
if os.path.splitext(info)[1][1:] == "pcd":
with zip_file.open(info, "r") as file:
data = ValidateDimension.get_pcd_properties(file)
image_sizes.append((int(data["WIDTH"]), int(data["HEIGHT"])))
root_path = os.path.abspath(os.path.join(root_path, filename.split(".")[0]))
shutil.rmtree(root_path)
cls._image_sizes[filename] = image_sizes
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
super().tearDownClass() super().tearDownClass()
@ -1905,8 +1950,10 @@ class TaskDataAPITestCase(APITestCase):
return self._run_api_v1_task_id_data_get(tid, user, "frame", "original", number) return self._run_api_v1_task_id_data_get(tid, user, "frame", "original", number)
@staticmethod @staticmethod
def _extract_zip_chunk(chunk_buffer): def _extract_zip_chunk(chunk_buffer, dimension=DimensionType.DIM_2D):
chunk = zipfile.ZipFile(chunk_buffer, mode='r') chunk = zipfile.ZipFile(chunk_buffer, mode='r')
if dimension == DimensionType.DIM_3D:
return [BytesIO(chunk.read(f)) for f in sorted(chunk.namelist()) if f.rsplit(".", maxsplit=1)[-1] == "pcd"]
return [Image.open(BytesIO(chunk.read(f))) for f in sorted(chunk.namelist())] return [Image.open(BytesIO(chunk.read(f))) for f in sorted(chunk.namelist())]
@staticmethod @staticmethod
@ -1917,7 +1964,7 @@ class TaskDataAPITestCase(APITestCase):
def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_type, expected_original_type, image_sizes, def _test_api_v1_tasks_id_data_spec(self, user, spec, data, expected_compressed_type, expected_original_type, image_sizes,
expected_storage_method=StorageMethodChoice.FILE_SYSTEM, expected_storage_method=StorageMethodChoice.FILE_SYSTEM,
expected_uploaded_data_location=StorageChoice.LOCAL): expected_uploaded_data_location=StorageChoice.LOCAL, dimension=DimensionType.DIM_2D):
# create task # create task
response = self._create_task(user, spec) response = self._create_task(user, spec)
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -1953,8 +2000,9 @@ class TaskDataAPITestCase(APITestCase):
response = self._get_preview(task_id, user) response = self._get_preview(task_id, user)
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
if expected_status_code == status.HTTP_200_OK: if expected_status_code == status.HTTP_200_OK:
preview = Image.open(io.BytesIO(b"".join(response.streaming_content))) if dimension == DimensionType.DIM_2D:
self.assertLessEqual(preview.size, image_sizes[0]) preview = Image.open(io.BytesIO(b"".join(response.streaming_content)))
self.assertLessEqual(preview.size, image_sizes[0])
# check compressed chunk # check compressed chunk
response = self._get_compressed_chunk(task_id, user, 0) response = self._get_compressed_chunk(task_id, user, 0)
@ -1965,14 +2013,18 @@ class TaskDataAPITestCase(APITestCase):
else: else:
compressed_chunk = io.BytesIO(b"".join(response.streaming_content)) compressed_chunk = io.BytesIO(b"".join(response.streaming_content))
if task["data_compressed_chunk_type"] == self.ChunkType.IMAGESET: if task["data_compressed_chunk_type"] == self.ChunkType.IMAGESET:
images = self._extract_zip_chunk(compressed_chunk) images = self._extract_zip_chunk(compressed_chunk, dimension=dimension)
else: else:
images = self._extract_video_chunk(compressed_chunk) images = self._extract_video_chunk(compressed_chunk)
self.assertEqual(len(images), min(task["data_chunk_size"], len(image_sizes))) self.assertEqual(len(images), min(task["data_chunk_size"], len(image_sizes)))
for image_idx, image in enumerate(images): for image_idx, image in enumerate(images):
self.assertEqual(image.size, image_sizes[image_idx]) if dimension == DimensionType.DIM_3D:
properties = ValidateDimension.get_pcd_properties(image)
self.assertEqual((int(properties["WIDTH"]),int(properties["HEIGHT"])), image_sizes[image_idx])
else:
self.assertEqual(image.size, image_sizes[image_idx])
# check original chunk # check original chunk
response = self._get_original_chunk(task_id, user, 0) response = self._get_original_chunk(task_id, user, 0)
@ -1983,12 +2035,16 @@ class TaskDataAPITestCase(APITestCase):
else: else:
original_chunk = io.BytesIO(b"".join(response.streaming_content)) original_chunk = io.BytesIO(b"".join(response.streaming_content))
if task["data_original_chunk_type"] == self.ChunkType.IMAGESET: if task["data_original_chunk_type"] == self.ChunkType.IMAGESET:
images = self._extract_zip_chunk(original_chunk) images = self._extract_zip_chunk(original_chunk, dimension=dimension)
else: else:
images = self._extract_video_chunk(original_chunk) images = self._extract_video_chunk(original_chunk)
for image_idx, image in enumerate(images): for image_idx, image in enumerate(images):
self.assertEqual(image.size, image_sizes[image_idx]) if dimension == DimensionType.DIM_3D:
properties = ValidateDimension.get_pcd_properties(image)
self.assertEqual((int(properties["WIDTH"]), int(properties["HEIGHT"])), image_sizes[image_idx])
else:
self.assertEqual(image.size, image_sizes[image_idx])
self.assertEqual(len(images), min(task["data_chunk_size"], len(image_sizes))) self.assertEqual(len(images), min(task["data_chunk_size"], len(image_sizes)))
@ -2004,7 +2060,7 @@ class TaskDataAPITestCase(APITestCase):
source_images = [] source_images = []
for f in source_files: for f in source_files:
if zipfile.is_zipfile(f): if zipfile.is_zipfile(f):
source_images.extend(self._extract_zip_chunk(f)) source_images.extend(self._extract_zip_chunk(f, dimension=dimension))
elif isinstance(f, io.BytesIO) and \ elif isinstance(f, io.BytesIO) and \
str(getattr(f, 'name', None)).endswith('.pdf'): str(getattr(f, 'name', None)).endswith('.pdf'):
source_images.extend(convert_from_bytes(f.getvalue(), source_images.extend(convert_from_bytes(f.getvalue(),
@ -2013,9 +2069,14 @@ class TaskDataAPITestCase(APITestCase):
source_images.append(Image.open(f)) source_images.append(Image.open(f))
for img_idx, image in enumerate(images): for img_idx, image in enumerate(images):
server_image = np.array(image) if dimension == DimensionType.DIM_3D:
source_image = np.array(source_images[img_idx]) server_image = np.array(image.getbuffer())
self.assertTrue(np.array_equal(source_image, server_image)) source_image = np.array(source_images[img_idx].getbuffer())
self.assertTrue(np.array_equal(source_image, server_image))
else:
server_image = np.array(image)
source_image = np.array(source_images[img_idx])
self.assertTrue(np.array_equal(source_image, server_image))
def _test_api_v1_tasks_id_data(self, user): def _test_api_v1_tasks_id_data(self, user):
task_spec = { task_spec = {
@ -2415,6 +2476,45 @@ class TaskDataAPITestCase(APITestCase):
self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes) self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.VIDEO, self.ChunkType.VIDEO, image_sizes)
task_spec = {
"name": "my archive task #24",
"overlap": 0,
"segment_size": 0,
"labels": [
{"name": "car"},
{"name": "person"},
]
}
task_data = {
"client_files[0]": open(os.path.join(os.path.dirname(__file__), 'assets', 'test_pointcloud_pcd.zip'), 'rb'),
"image_quality": 100,
}
image_sizes = self._image_sizes["test_pointcloud_pcd.zip"]
self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET,
self.ChunkType.IMAGESET,
image_sizes, dimension=DimensionType.DIM_3D)
task_spec = {
"name": "my archive task #25",
"overlap": 0,
"segment_size": 0,
"labels": [
{"name": "car"},
{"name": "person"},
]
}
task_data = {
"client_files[0]": open(os.path.join(os.path.dirname(__file__), 'assets', 'test_velodyne_points.zip'),
'rb'),
"image_quality": 100,
}
image_sizes = self._image_sizes["test_velodyne_points.zip"]
self._test_api_v1_tasks_id_data_spec(user, task_spec, task_data, self.ChunkType.IMAGESET,
self.ChunkType.IMAGESET,
image_sizes, dimension=DimensionType.DIM_3D)
def test_api_v1_tasks_id_data_admin(self): def test_api_v1_tasks_id_data_admin(self):
self._test_api_v1_tasks_id_data(self.admin) self._test_api_v1_tasks_id_data(self.admin)

@ -447,7 +447,7 @@ class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
if not db_data: if not db_data:
raise NotFound(detail='Cannot find requested data for the task') raise NotFound(detail='Cannot find requested data for the task')
frame_provider = FrameProvider(db_task.data) frame_provider = FrameProvider(db_task.data, db_task.dimension)
if data_type == 'chunk': if data_type == 'chunk':
data_id = int(data_id) data_id = int(data_id)

@ -44,4 +44,5 @@ tensorflow==2.4.0 # Optional requirement of Datumaro
# archives. Don't use as a python module because it has GPL license. # archives. Don't use as a python module because it has GPL license.
patool==1.12 patool==1.12
diskcache==5.0.2 diskcache==5.0.2
open3d==0.11.2
git+https://github.com/openvinotoolkit/datumaro@v0.1.4 git+https://github.com/openvinotoolkit/datumaro@v0.1.4

Loading…
Cancel
Save