未验证 提交 cef42b69 编写于 作者: D Dmitry Kalinin 提交者: GitHub

Project export with 3d tasks (#3502)

Co-authored-by: NMaxim Zhiltsov <maxim.zhiltsov@intel.com>
Co-authored-by: Ndvkruchinin <dvkruchinin@gmail.com>
Co-authored-by: NNikita Manovich <nikita.manovich@intel.com>
上级 68881050
......@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added ability to import data from share with cli without copying the data (<https://github.com/openvinotoolkit/cvat/issues/2862>)
- Notification if the browser does not support nesassary API
- Added ability to export project as a dataset (<https://github.com/openvinotoolkit/cvat/pull/3365>)
and project with 3D tasks (<https://github.com/openvinotoolkit/cvat/pull/3502>)
- Additional inline tips in interactors with demo gifs (<https://github.com/openvinotoolkit/cvat/pull/3473>)
### Changed
......
{
"name": "cvat-core",
"version": "3.14.0",
"version": "3.15.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"name": "cvat-core",
"version": "3.14.0",
"version": "3.15.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
......
......@@ -34,6 +34,7 @@
task_subsets: undefined,
training_project: undefined,
task_ids: undefined,
dimension: undefined,
};
for (const property in data) {
......@@ -153,7 +154,7 @@
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
......@@ -163,13 +164,24 @@
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* Dimesion of the tasks in the project, if no task dimension is null
* @name dimension
* @type {string}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
dimension: {
get: () => data.dimension,
},
/**
* After project has been created value can be appended only.
* @name labels
......
......@@ -34,9 +34,9 @@ function ExportDatasetModal(): JSX.Element {
const instance = useSelector((state: CombinedState) => state.export.instance);
const modalVisible = useSelector((state: CombinedState) => state.export.modalVisible);
const dumpers = useSelector((state: CombinedState) => state.formats.annotationFormats.dumpers);
const {
tasks: taskExportActivities, projects: projectExportActivities,
} = useSelector((state: CombinedState) => state.export);
const { tasks: taskExportActivities, projects: projectExportActivities } = useSelector(
(state: CombinedState) => state.export,
);
const initActivities = (): void => {
if (instance instanceof core.classes.Project) {
......@@ -62,19 +62,28 @@ function ExportDatasetModal(): JSX.Element {
dispatch(exportActions.closeExportModal());
};
const handleExport = useCallback((values: FormValues): void => {
// have to validate format before so it would not be undefined
dispatch(
exportDatasetAsync(instance, values.selectedFormat as string, values.customName ? `${values.customName}.zip` : '', values.saveImages),
);
closeModal();
Notification.info({
message: 'Dataset export started',
description: `Dataset export was started for ${instanceType} #${instance?.id}. ` +
'Download will start automaticly as soon as the dataset is ready.',
className: `cvat-notification-notice-export-${instanceType}-start`,
});
}, [instance?.id, instance instanceof core.classes.Project, instanceType]);
const handleExport = useCallback(
(values: FormValues): void => {
// have to validate format before so it would not be undefined
dispatch(
exportDatasetAsync(
instance,
values.selectedFormat as string,
values.customName ? `${values.customName}.zip` : '',
values.saveImages,
),
);
closeModal();
Notification.info({
message: 'Dataset export started',
description:
`Dataset export was started for ${instanceType} #${instance?.id}. ` +
'Download will start automaticly as soon as the dataset is ready.',
className: `cvat-notification-notice-export-${instanceType}-start`,
});
},
[instance?.id, instance instanceof core.classes.Project, instanceType],
);
return (
<Modal
......@@ -106,11 +115,7 @@ function ExportDatasetModal(): JSX.Element {
<Select placeholder='Select dataset format' className='cvat-modal-export-select'>
{dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter(
(dumper: any): boolean =>
!(instance instanceof core.classes.Task) ||
dumper.dimension === instance?.dimension,
)
.filter((dumper: any): boolean => dumper.dimension === instance?.dimension)
.map(
(dumper: any): JSX.Element => {
const pending = (activities || []).includes(dumper.name);
......
......@@ -37,12 +37,13 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
return (
<Menu className='cvat-project-actions-menu'>
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
<Menu.Item
onClick={() => dispatch(exportActions.openExportModal(projectInstance))}
>
Export project dataset
</Menu.Item>
<hr />
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
</Menu>
);
}
......@@ -513,7 +513,7 @@ class ProjectData(InstanceLabelData):
Track = NamedTuple('Track', [('label', str), ('group', int), ('source', str), ('shapes', List[TrackedShape]), ('task_id', int)])
Tag = NamedTuple('Tag', [('frame', int), ('label', str), ('attributes', List[InstanceLabelData.Attribute]), ('source', str), ('group', int), ('task_id', int)])
Tag.__new__.__defaults__ = (0, )
Frame = NamedTuple('Frame', [('task_id', int), ('subset', str), ('idx', int), ('frame', int), ('name', str), ('width', int), ('height', int), ('labeled_shapes', List[Union[LabeledShape, TrackedShape]]), ('tags', List[Tag])])
Frame = NamedTuple('Frame', [('task_id', int), ('subset', str), ('idx', int), ('id', int), ('frame', int), ('name', str), ('width', int), ('height', int), ('labeled_shapes', List[Union[LabeledShape, TrackedShape]]), ('tags', List[Tag])])
def __init__(self, annotation_irs: Mapping[str, AnnotationIR], db_project: Project, host: str, create_callback: Callable = None):
self._annotation_irs = annotation_irs
......@@ -581,6 +581,7 @@ class ProjectData(InstanceLabelData):
else:
self._frame_info.update({(task.id, self.rel_frame_id(task.id, db_image.frame)): {
"path": mangle_image_name(db_image.path, defaulted_subset, original_names),
"id": db_image.id,
"width": db_image.width,
"height": db_image.height,
"subset": defaulted_subset
......@@ -683,6 +684,7 @@ class ProjectData(InstanceLabelData):
task_id=task_id,
subset=frame_info["subset"],
idx=idx,
id=frame_info.get('id',0),
frame=abs_frame,
name=frame_info["path"],
height=frame_info["height"],
......@@ -807,10 +809,18 @@ class CVATDataExtractorMixin:
for _, attr in label['attributes']:
label_categories.attributes.add(attr['name'])
categories[datumaro.AnnotationType.label] = label_categories
return categories
@staticmethod
def _load_user_info(meta: dict):
return {
"name": meta['owner']['username'],
"createdAt": meta['created'],
"updatedAt": meta['updated']
}
def _read_cvat_anno(self, cvat_frame_anno: Union[ProjectData.Frame, TaskData.Frame], labels: list):
categories = self.categories()
......@@ -827,7 +837,8 @@ class CVATDataExtractorMixin:
class CvatTaskDataExtractor(datumaro.SourceExtractor, CVATDataExtractorMixin):
def __init__(self, task_data, include_images=False, format_type=None, dimension=DimensionType.DIM_2D):
super().__init__()
self._categories, self._user = self._load_categories(task_data, dimension=dimension)
self._categories = self._load_categories(task_data.meta['task']['labels'])
self._user = self._load_user_info(task_data.meta['task']) if dimension == DimensionType.DIM_3D else {}
self._dimension = dimension
self._format_type = format_type
dm_items = []
......@@ -893,11 +904,9 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor, CVATDataExtractorMixin):
attributes["createdAt"] = self._user["createdAt"]
attributes["updatedAt"] = self._user["updatedAt"]
attributes["labels"] = []
index = 0
for _, label in task_data.meta['task']['labels']:
attributes["labels"].append({"label_id": index, "name": label["name"], "color": label["color"]})
for (idx, (_, label)) in enumerate(task_data.meta['task']['labels']):
attributes["labels"].append({"label_id": idx, "name": label["name"], "color": label["color"]})
attributes["track_id"] = -1
index += 1
dm_item = datumaro.DatasetItem(id=osp.split(frame_data.name)[-1].split('.')[0],
annotations=dm_anno, point_cloud=dm_image[0], related_images=dm_image[1],
......@@ -907,27 +916,6 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor, CVATDataExtractorMixin):
self._items = dm_items
@staticmethod
def _load_categories(cvat_anno, dimension): # pylint: disable=arguments-differ
categories = {}
label_categories = datumaro.LabelCategories(attributes=['occluded'])
user_info = {}
if dimension == DimensionType.DIM_3D:
user_info = {"name": cvat_anno.meta['task']['owner']['username'],
"createdAt": cvat_anno.meta['task']['created'],
"updatedAt": cvat_anno.meta['task']['updated']}
for _, label in cvat_anno.meta['task']['labels']:
label_categories.add(label['name'])
for _, attr in label['attributes']:
label_categories.attributes.add(attr['name'])
categories[datumaro.AnnotationType.label] = label_categories
return categories, user_info
def _read_cvat_anno(self, cvat_frame_anno: TaskData.Frame, labels: list):
categories = self.categories()
label_cat = categories[datumaro.AnnotationType.label]
......@@ -940,9 +928,12 @@ class CvatTaskDataExtractor(datumaro.SourceExtractor, CVATDataExtractorMixin):
return convert_cvat_anno_to_dm(cvat_frame_anno, label_attrs, map_label, self._format_type, self._dimension)
class CVATProjectDataExtractor(datumaro.Extractor, CVATDataExtractorMixin):
def __init__(self, project_data: ProjectData, include_images: bool = False):
def __init__(self, project_data: ProjectData, include_images: bool = False, format_type: str = None, dimension: DimensionType = DimensionType.DIM_2D):
super().__init__()
self._categories = self._load_categories(project_data.meta['project']['labels'])
self._user = self._load_user_info(project_data.meta['project']) if dimension == DimensionType.DIM_3D else {}
self._dimension = dimension
self._format_type = format_type
dm_items: List[datumaro.DatasetItem] = []
......@@ -952,12 +943,28 @@ class CVATProjectDataExtractor(datumaro.Extractor, CVATDataExtractorMixin):
for task in project_data.tasks:
is_video = task.mode == 'interpolation'
ext_per_task[task.id] = FrameProvider.VIDEO_FRAME_EXT if is_video else ''
if include_images:
frame_provider = FrameProvider(task.data)
if self._dimension == DimensionType.DIM_3D:
def image_maker_factory(task):
images_query = task.data.images.prefetch_related()
def _make_image(i, **kwargs):
loader = osp.join(
task.data.get_upload_dirname(), kwargs['path'],
)
related_images = []
image = images_query.get(id=i)
for i in image.related_files.all():
path = osp.realpath(str(i.path))
if osp.isfile(path):
related_images.append(path)
return loader, related_images
return _make_image
image_maker_per_task[task.id] = image_maker_factory(task)
elif include_images:
if is_video:
# optimization for videos: use numpy arrays instead of bytes
# some formats or transforms can require image data
def image_maker_factory(frame_provider):
def image_maker_factory(task):
frame_provider = FrameProvider(task.data)
def _make_image(i, **kwargs):
loader = lambda _: frame_provider.get_frame(i,
quality=frame_provider.Quality.ORIGINAL,
......@@ -966,30 +973,48 @@ class CVATProjectDataExtractor(datumaro.Extractor, CVATDataExtractorMixin):
return _make_image
else:
# for images use encoded data to avoid recoding
def image_maker_factory(frame_provider):
def image_maker_factory(task):
frame_provider = FrameProvider(task.data)
def _make_image(i, **kwargs):
loader = lambda _: frame_provider.get_frame(i,
quality=frame_provider.Quality.ORIGINAL,
out_type=frame_provider.Type.BUFFER)[0].getvalue()
return ByteImage(data=loader, **kwargs)
return _make_image
image_maker_per_task[task.id] = image_maker_factory(frame_provider)
image_maker_per_task[task.id] = image_maker_factory(task)
for frame_data in project_data.group_by_frame(include_empty=True):
image_args = {
'path': frame_data.name + ext_per_task[frame_data.task_id],
'size': (frame_data.height, frame_data.width),
}
if include_images:
if self._dimension == DimensionType.DIM_3D:
dm_image = image_maker_per_task[frame_data.task_id](frame_data.id, **image_args)
elif include_images:
dm_image = image_maker_per_task[frame_data.task_id](frame_data.idx, **image_args)
else:
dm_image = Image(**image_args)
dm_anno = self._read_cvat_anno(frame_data, project_data.meta['project']['labels'])
dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0],
annotations=dm_anno, image=dm_image,
subset=frame_data.subset,
attributes={'frame': frame_data.frame}
)
if self._dimension == DimensionType.DIM_2D:
dm_item = datumaro.DatasetItem(id=osp.splitext(frame_data.name)[0],
annotations=dm_anno, image=dm_image,
subset=frame_data.subset,
attributes={'frame': frame_data.frame}
)
else:
attributes = {'frame': frame_data.frame}
if format_type == "sly_pointcloud":
attributes["name"] = self._user["name"]
attributes["createdAt"] = self._user["createdAt"]
attributes["updatedAt"] = self._user["updatedAt"]
attributes["labels"] = []
for (idx, (_, label)) in enumerate(project_data.meta['project']['labels']):
attributes["labels"].append({"label_id": idx, "name": label["name"], "color": label["color"]})
attributes["track_id"] = -1
dm_item = datumaro.DatasetItem(id=osp.splitext(osp.split(frame_data.name)[-1])[0],
annotations=dm_anno, point_cloud=dm_image[0], related_images=dm_image[1],
attributes=attributes, subset=frame_data.subset)
dm_items.append(dm_item)
self._items = dm_items
......@@ -1004,11 +1029,11 @@ class CVATProjectDataExtractor(datumaro.Extractor, CVATDataExtractorMixin):
return len(self._items)
def GetCVATDataExtractor(instance_data: Union[ProjectData, TaskData], include_images: bool=False):
def GetCVATDataExtractor(instance_data: Union[ProjectData, TaskData], include_images: bool = False, format_type: str = None, dimension: DimensionType = DimensionType.DIM_2D):
if isinstance(instance_data, ProjectData):
return CVATProjectDataExtractor(instance_data, include_images)
return CVATProjectDataExtractor(instance_data, include_images, format_type, dimension)
else:
return CvatTaskDataExtractor(instance_data, include_images)
return CvatTaskDataExtractor(instance_data, include_images, format_type, dimension)
class CvatImportError(Exception):
pass
......
......@@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory
from datumaro.components.dataset import Dataset
from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor, TaskData,
from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor,
import_dm_annotations)
from cvat.apps.dataset_manager.util import make_zip_archive
from cvat.apps.engine.models import DimensionType
......@@ -18,10 +18,7 @@ from .registry import dm_env, exporter, importer
@exporter(name='Sly Point Cloud Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _export_images(dst_file, task_data, save_images=False):
if not isinstance(task_data, TaskData):
raise Exception("Export to \"Sly Point Cloud\" format is working only with tasks temporarily")
dataset = Dataset.from_extractors(CvatTaskDataExtractor(
dataset = Dataset.from_extractors(GetCVATDataExtractor(
task_data, include_images=save_images, format_type='sly_pointcloud', dimension=DimensionType.DIM_3D), env=dm_env)
with TemporaryDirectory() as temp_dir:
......
......@@ -7,7 +7,7 @@ from tempfile import TemporaryDirectory
from datumaro.components.dataset import Dataset
from cvat.apps.dataset_manager.bindings import CvatTaskDataExtractor, TaskData, \
from cvat.apps.dataset_manager.bindings import GetCVATDataExtractor, \
import_dm_annotations
from .registry import dm_env
......@@ -20,10 +20,7 @@ from .registry import exporter, importer
@exporter(name='Kitti Raw Format', ext='ZIP', version='1.0', dimension=DimensionType.DIM_3D)
def _export_images(dst_file, task_data, save_images=False):
if not isinstance(task_data, TaskData):
raise Exception("Export to \"Kitti raw\" format is working only with tasks temporarily")
dataset = Dataset.from_extractors(CvatTaskDataExtractor(
dataset = Dataset.from_extractors(GetCVATDataExtractor(
task_data, include_images=save_images, format_type="kitti_raw", dimension=DimensionType.DIM_3D), env=dm_env)
with TemporaryDirectory() as temp_dir:
......
......@@ -504,13 +504,15 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
task_subsets = serializers.ListField(child=serializers.CharField(), required=False)
training_project = TrainingProjectSerializer(required=False, allow_null=True)
dimension = serializers.CharField(max_length=16, required=False)
class Meta:
model = models.Project
fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'status', 'training_project')
read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee')
'bug_tracker', 'task_subsets', 'created_date', 'updated_date', 'status', 'training_project', 'dimension')
read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee', 'task_subsets', 'dimension')
ordering = ['-id']
......@@ -519,6 +521,7 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
task_subsets = set(instance.tasks.values_list('subset', flat=True))
task_subsets.discard('')
response['task_subsets'] = list(task_subsets)
response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None
return response
class ProjectSerializer(ProjectWithoutTaskSerializer):
......@@ -580,7 +583,9 @@ class ProjectSerializer(ProjectWithoutTaskSerializer):
return value
def to_representation(self, instance):
return serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here
response = serializers.ModelSerializer.to_representation(self, instance) # ignoring subsets here
response['dimension'] = instance.tasks.first().dimension if instance.tasks.count() else None
return response
class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255)
......
......@@ -239,6 +239,8 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
http_method_names = ['get', 'post', 'head', 'patch', 'delete']
def get_serializer_class(self):
if self.request.path.endswith('tasks'):
return TaskSerializer
if self.request.query_params and self.request.query_params.get("names_only") == "true":
return ProjectSearchSerializer
if self.request.query_params and self.request.query_params.get("without_tasks") == "true":
......
......@@ -8,10 +8,11 @@ import { taskName, labelName } from '../../support/const_canvas3d';
context('Canvas 3D functionality. Cancel drawing.', () => {
const caseId = '85';
const screenshotsPath = 'cypress/screenshots/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js';
const screenshotsPath =
'cypress/screenshots/canvas3d_functionality/case_85_canvas3d_functionality_cuboid_сancel_drawing.js';
before(() => {
cy.openTask(taskName)
cy.openTask(taskName);
cy.openJob();
cy.wait(1000); // Waiting for the point cloud to display
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册