RESTful API (#389)

main
Nikita Manovich 7 years ago committed by GitHub
parent 7ed31cac0e
commit ae6a4891e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,11 +27,16 @@
"airbnb",
],
"rules": {
"no-new": [0],
"class-methods-use-this": [0],
"no-restricted-properties": [0, {
"object": "Math",
"property": "pow",
}],
"no-param-reassign": [0],
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
"no-continue": [0],
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
// This rule actual for user input data on the node.js environment mainly.
"security/detect-object-injection": 0,

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "CVAT Server",
"name": "server",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -15,19 +15,14 @@
"args": [
"runserver",
"--noreload",
"--nothreading",
"--insecure",
"127.0.0.1:7000"
],
"debugOptions": [
"RedirectOutput",
"DjangoDebugging"
],
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"django": true,
"cwd": "${workspaceFolder}"
},
{
"name": "CVAT Client",
"name": "client",
"type": "chrome",
"request": "launch",
"url": "http://localhost:7000/",
@ -40,7 +35,7 @@
}
},
{
"name": "CVAT RQ - default",
"name": "RQ - default",
"type": "python",
"request": "launch",
"stopOnEntry": false,
@ -53,17 +48,12 @@
"--worker-class",
"cvat.simpleworker.SimpleWorker",
],
"debugOptions": [
"RedirectOutput",
"DjangoDebugging"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"envFile": "${workspaceFolder}/.env",
"env": {}
},
{
"name": "CVAT RQ - low",
"name": "RQ - low",
"type": "python",
"request": "launch",
"debugStdLib": true,
@ -76,16 +66,12 @@
"--worker-class",
"cvat.simpleworker.SimpleWorker",
],
"debugOptions": [
"RedirectOutput",
"DjangoDebugging"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"envFile": "${workspaceFolder}/.env",
"env": {}
},
{
"name": "CVAT git",
"name": "git",
"type": "python",
"request": "launch",
"debugStdLib": true,
@ -95,24 +81,53 @@
"args": [
"update_git_states"
],
"debugOptions": [
"RedirectOutput",
"DjangoDebugging"
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
},
{
"name": "migrate",
"type": "python",
"request": "launch",
"debugStdLib": true,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"migrate"
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {}
},
{
"name": "tests",
"type": "python",
"request": "launch",
"debugStdLib": true,
"stopOnEntry": false,
"pythonPath": "${config:python.pythonPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"test",
"--settings",
"cvat.settings.testing",
"cvat/apps/engine",
],
"django": true,
"cwd": "${workspaceFolder}",
"env": {},
"envFile": "${workspaceFolder}/.env",
"env": {}
},
],
"compounds": [
{
"name": "CVAT Debugging",
"name": "debugging",
"configurations": [
"CVAT Client",
"CVAT Server",
"CVAT RQ - default",
"CVAT RQ - low",
"CVAT git",
"client",
"server",
"RQ - default",
"RQ - low",
"git",
]
}
]

@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ability to rotate images/video in the client part (Ctrl+R, Shift+Ctrl+R shortcuts) (#305)
- The ReID application for automatic bounding box merging has been added (#299)
- Keyboard shortcuts to switch next/previous default shape type (box, polygon etc) [Alt + <, Alt + >] (#316)
- Converter for VOC now supports interpolation tracks
- Converter for VOC now supports interpolation tracks
- REST API (/api/v1/*, /api/docs)
- Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work
### Changed

@ -10,34 +10,48 @@ filter {
# 1. Decode the event from json in 'message' field
# 2. Remove unnecessary field from it
# 3. Type it as client
mutate {
rename => { "message" => "source_message" }
}
json {
source => "message"
source => "source_message"
}
date {
match => ["timestamp", "UNIX", "UNIX_MS"]
remove_field => "timestamp"
match => ["time", "ISO8601"]
remove_field => "time"
}
if [event] == "Send exception" {
if [payload] {
ruby {
code => "
event.get('payload').each { |key, value|
event.set(key, value)
}
"
}
}
if [name] == "Send exception" {
aggregate {
task_id => "%{userid}_%{application}_%{message}_%{filename}_%{line}"
task_id => "%{username}_%{message}_%{filename}_%{line}"
code => "
require 'time'
map['userid'] ||= event.get('userid');
map['application'] ||= event.get('application');
map['username'] ||= event.get('username');
map['error'] ||= event.get('message');
map['filename'] ||= event.get('filename');
map['line'] ||= event.get('line');
map['task'] ||= event.get('task');
map['task_id'] ||= event.get('task_id');
map['job_id'] ||= event.get('job_id');
map['error_count'] ||= 0;
map['error_count'] += 1;
map['aggregated_stack'] ||= '';
map['aggregated_stack'] += event.get('stack') + '\n\n\n';"
map['aggregated_stack'] += event.get('stack') + '\n\n\n';
"
timeout => 3600
timeout_tags => ['aggregated_exception']
push_map_as_event_on_timeout => true
@ -45,12 +59,17 @@ filter {
}
prune {
blacklist_names => ["level", "host", "logger_name", "message", "path",
"port", "stack_info"]
blacklist_names => ["level", "host", "logger_name", "path",
"port", "stack_info", "payload", "source_message"]
}
mutate {
replace => { "type" => "client" }
copy => {
"job_id" => "task"
"username" => "userid"
"name" => "event"
}
}
} else if [logger_name] =~ /cvat.server/ {
# 1. Remove 'logger_name' field and create 'task' field
@ -58,14 +77,14 @@ filter {
# 3. Type it as server
if [logger_name] =~ /cvat\.server\.task_[0-9]+/ {
mutate {
rename => { "logger_name" => "task" }
gsub => [ "task", "cvat.server.task_", "" ]
rename => { "logger_name" => "task_id" }
gsub => [ "task_id", "cvat.server.task_", "" ]
}
# Need to split the mutate because otherwise the conversion
# doesn't work.
mutate {
convert => { "task" => "integer" }
convert => { "task_id" => "integer" }
}
}

@ -3,3 +3,8 @@
#
# SPDX-License-Identifier: MIT
from cvat.utils.version import get_version
VERSION = (0, 4, 0, 'alpha', 0)
__version__ = get_version(VERSION)

@ -4,8 +4,10 @@
import os
from django.conf import settings
from django.db.models import Q
import rules
from . import AUTH_ROLE
from rest_framework.permissions import BasePermission
def register_signals():
from django.db.models.signals import post_migrate, post_save
@ -67,6 +69,11 @@ def is_job_annotator(db_user, db_job):
return has_rights
# AUTH PERMISSIONS RULES
rules.add_perm('engine.role.user', has_user_role)
rules.add_perm('engine.role.admin', has_admin_role)
rules.add_perm('engine.role.annotator', has_annotator_role)
rules.add_perm('engine.role.observer', has_observer_role)
rules.add_perm('engine.task.create', has_admin_role | has_user_role)
rules.add_perm('engine.task.access', has_admin_role | has_observer_role |
is_task_owner | is_task_annotator)
@ -78,3 +85,64 @@ rules.add_perm('engine.job.access', has_admin_role | has_observer_role |
is_job_owner | is_job_annotator)
rules.add_perm('engine.job.change', has_admin_role | is_job_owner |
is_job_annotator)
class AdminRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.admin")
class UserRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.user")
class AnnotatorRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.annotator")
class ObserverRolePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.role.observer")
class TaskCreatePermission(BasePermission):
# pylint: disable=no-self-use
def has_permission(self, request, view):
return request.user.has_perm("engine.task.create")
class TaskAccessPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.task.access", obj)
class TaskGetQuerySetMixin(object):
def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
# Don't filter queryset for admin, observer and detail methods
if has_admin_role(user) or has_observer_role(user) or self.detail:
return queryset
else:
return queryset.filter(Q(owner=user) | Q(assignee=user) |
Q(segment__job__assignee=user) | Q(assignee=None)).distinct()
class TaskChangePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.task.change", obj)
class TaskDeletePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.task.delete", obj)
class JobAccessPermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.job.access", obj)
class JobChangePermission(BasePermission):
# pylint: disable=no-self-use
def has_object_permission(self, request, view, obj):
return request.user.has_perm("engine.job.change", obj)

@ -16,7 +16,8 @@ from django.conf import settings
from cvat.apps.engine.log import slogger
from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine import annotation
from cvat.apps.engine.serializers import LabeledDataSerializer
from cvat.apps.engine.annotation import put_task_data, patch_task_data
from .models import AnnotationModel, FrameworkChoice
from .model_loader import ModelLoader
@ -208,71 +209,42 @@ def get_image_data(path_to_data):
image_list.sort(key=get_image_key)
return ImageLoader(image_list)
def create_anno_container():
return {
"boxes": [],
"polygons": [],
"polylines": [],
"points": [],
"box_paths": [],
"polygon_paths": [],
"polyline_paths": [],
"points_paths": [],
}
class Results():
def __init__(self):
self._results = create_anno_container()
self._results = {
"shapes": [],
"tracks": []
}
def add_box(self, xtl, ytl, xbr, ybr, label, frame_number, attributes=None):
self.get_boxes().append({
self.get_shapes().append({
"label": label,
"frame": frame_number,
"xtl": xtl,
"ytl": ytl,
"xbr": xbr,
"ybr": ybr,
"points": [xtl, ytl, xbr, ybr],
"type": "rectangle",
"attributes": attributes or {},
})
def add_points(self, points, label, frame_number, attributes=None):
self.get_points().append(
self._create_polyshape(points, label, frame_number, attributes)
)
points = self._create_polyshape(points, label, frame_number, attributes)
points["type"] = "points"
self.get_shapes().append(points)
def add_polygon(self, points, label, frame_number, attributes=None):
self.get_polygons().append(
self._create_polyshape(points, label, frame_number, attributes)
)
polygon = self._create_polyshape(points, label, frame_number, attributes)
polygon["type"] = "polygon"
self.get_shapes().append(polygon)
def add_polyline(self, points, label, frame_number, attributes=None):
self.get_polylines().append(
self._create_polyshape(points, label, frame_number, attributes)
)
def get_boxes(self):
return self._results["boxes"]
def get_polygons(self):
return self._results["polygons"]
def get_polylines(self):
return self._results["polylines"]
def get_points(self):
return self._results["points"]
def get_box_paths(self):
return self._results["box_paths"]
def get_polygon_paths(self):
return self._results["polygon_paths"]
polyline = self._create_polyshape(points, label, frame_number, attributes)
polyline["type"] = "polyline"
self.get_shapes().append(polyline)
def get_polyline_paths(self):
return self._results["polyline_paths"]
def get_shapes(self):
return self._results["shapes"]
def get_points_paths(self):
return self._results["points_paths"]
def get_tracks(self):
return self._results["tracks"]
@staticmethod
def _create_polyshape(self, points, label, frame_number, attributes=None):
@ -315,7 +287,7 @@ def _run_inference_engine_annotation(data, model_file, weights_file,
return attributes
def add_polyshapes(shapes, target_container):
def add_shapes(shapes, target_container):
for shape in shapes:
if shape["label"] not in labels_mapping:
continue
@ -325,35 +297,18 @@ def _run_inference_engine_annotation(data, model_file, weights_file,
"label_id": db_label,
"frame": shape["frame"],
"points": shape["points"],
"type": shape["type"],
"z_order": 0,
"group_id": 0,
"group": None,
"occluded": False,
"attributes": process_attributes(shape["attributes"], attribute_spec[db_label]),
})
def add_boxes(boxes, target_container):
for box in boxes:
if box["label"] not in labels_mapping:
continue
db_label = labels_mapping[box["label"]]
target_container.append({
"label_id": db_label,
"frame": box["frame"],
"xtl": box["xtl"],
"ytl": box["ytl"],
"xbr": box["xbr"],
"ybr": box["ybr"],
"z_order": 0,
"group_id": 0,
"occluded": False,
"attributes": process_attributes(box["attributes"], attribute_spec[db_label]),
})
result = {
"create": create_anno_container(),
"update": create_anno_container(),
"delete": create_anno_container(),
"shapes": [],
"tracks": [],
"tags": [],
"version": 0
}
data_len = len(data)
@ -375,16 +330,15 @@ def _run_inference_engine_annotation(data, model_file, weights_file,
frame_counter += 1
if job and update_progress and not update_progress(job, frame_counter * 100 / data_len):
return None
processed_detections = _process_detections(detections, convertation_file)
add_boxes(processed_detections.get_boxes(), result["create"]["boxes"])
add_polyshapes(processed_detections.get_points(), result["create"]["points"])
add_polyshapes(processed_detections.get_polygons(), result["create"]["polygons"])
add_polyshapes(processed_detections.get_polylines(), result["create"]["polylines"])
add_shapes(processed_detections.get_shapes(), result["shapes"])
return result
def run_inference_thread(tid, model_file, weights_file, labels_mapping, attributes, convertation_file, reset):
def run_inference_thread(tid, model_file, weights_file, labels_mapping, attributes, convertation_file, reset, user):
def update_progress(job, progress):
job.refresh()
if "cancel" in job.meta:
@ -418,9 +372,13 @@ def run_inference_thread(tid, model_file, weights_file, labels_mapping, attribut
slogger.glob.info("auto annotation for task {} canceled by user".format(tid))
return
if reset:
annotation.clear_task(tid)
annotation.save_task(tid, result)
serializer = LabeledDataSerializer(data = result)
if serializer.is_valid(raise_exception=True):
if reset:
put_task_data(tid, user, result)
else:
patch_task_data(tid, user, result, "create")
slogger.glob.info("auto annotation for task {} done".format(tid))
except Exception as e:
try:

@ -11,8 +11,6 @@
*/
window.cvat = window.cvat || {};
window.cvat.dashboard = window.cvat.dashboard || {};
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
const AutoAnnotationServer = {
start(modelId, taskId, data, success, error, progress, check) {
@ -602,7 +600,7 @@ class AutoAnnotationModelRunnerView {
this.id = null;
this.initButton = initButton;
this.tid = data.taskid;
this.tid = data.id;
this.modelsTable.empty();
this.labelsTable.empty();
this.active = null;
@ -617,7 +615,7 @@ class AutoAnnotationModelRunnerView {
this.active.style.color = 'darkblue';
this.labelsTable.empty();
const labels = Object.values(event.data.data.spec.labels);
const labels = event.data.data.labels.map(x => x.name);
const intersection = labels.filter(el => event.data.model.labels.indexOf(el) !== -1);
intersection.forEach((label) => {
const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
@ -705,47 +703,37 @@ window.cvat.autoAnnotation = {
managerButtonId: 'annotatorManagerButton',
};
window.cvat.dashboard.uiCallbacks.push((newElements) => {
window.addEventListener('DOMContentLoaded', () => {
window.cvat.autoAnnotation.server = AutoAnnotationServer;
window.cvat.autoAnnotation.manager = new AutoAnnotationModelManagerView();
window.cvat.autoAnnotation.runner = new AutoAnnotationModelRunnerView();
const tids = Array.from(newElements, el => el.id.split('_')[1]);
$('body').append(window.cvat.autoAnnotation.manager.element, window.cvat.autoAnnotation.runner.element);
$(`<button id="${window.cvat.autoAnnotation.managerButtonId}" class="regular h1" style=""> Model Manager</button>`)
.on('click', () => {
const overlay = showOverlay('The manager are being setup..');
window.cvat.autoAnnotation.manager.reset().show();
overlay.remove();
}).appendTo('#dashboardManageButtons');
});
window.addEventListener('dashboardReady', (event) => {
const elements = $('.dashboardItem');
const tids = Array.from(elements, el => +el.getAttribute('tid'));
window.cvat.autoAnnotation.server.meta(tids, (data) => {
window.cvat.autoAnnotation.data = data;
$('body').append(window.cvat.autoAnnotation.manager.element, window.cvat.autoAnnotation.runner.element);
$(`<button id="${window.cvat.autoAnnotation.managerButtonId}" class="regular h1" style=""> Model Manager</button>`)
.on('click', () => {
const overlay = showOverlay('The manager are being setup..');
window.cvat.autoAnnotation.manager.reset().show();
overlay.remove();
}).appendTo('#dashboardManageButtons');
newElements.each((_, element) => {
const elem = $(element);
const tid = +elem.attr('id').split('_')[1];
elements.each(function setupDashboardItem() {
const elem = $(this);
const tid = +elem.attr('tid');
const button = $('<button> Run Auto Annotation </button>').addClass('regular dashboardButtonUI');
button[0].setupRun = function setupRun() {
const self = $(this);
const taskInfo = event.detail.filter(task => task.id === tid)[0];
self.text('Run Auto Annotation').off('click').on('click', () => {
const overlay = showOverlay('Task date are being recieved from the server..');
$.ajax({
url: `/get/task/${tid}`,
dataType: 'json',
success: (responseData) => {
overlay.setMessage('The model runner are being setup..');
window.cvat.autoAnnotation.runner.reset(responseData, self).show();
overlay.remove();
},
error: (responseData) => {
showMessage(`Can't get task data. Code: ${responseData.status}. Message: ${responseData.responseText || responseData.statusText}`);
},
complete: () => {
overlay.remove();
},
});
window.cvat.autoAnnotation.runner.reset(taskInfo, self).show();
});
};

@ -155,7 +155,7 @@ def get_meta_info(request):
job = queue.fetch_job(rq_id)
if job is not None:
response["run"][tid] = {
"status": job.status,
"status": job.get_status(),
"rq_id": rq_id,
}
@ -191,7 +191,7 @@ def start_annotation(request, mid, tid):
db_labels = db_task.label_set.prefetch_related("attributespec_set").all()
db_attributes = {db_label.id:
{db_attr.get_name(): db_attr.id for db_attr in db_label.attributespec_set.all()} for db_label in db_labels}
{db_attr.name: db_attr.id for db_attr in db_label.attributespec_set.all()} for db_label in db_labels}
db_labels = {db_label.name:db_label.id for db_label in db_labels}
model_labels = {value: key for key, value in load_label_map(labelmap_file).items()}
@ -214,6 +214,7 @@ def start_annotation(request, mid, tid):
db_attributes,
convertation_file_path,
should_reset,
request.user,
),
job_id = rq_id,
timeout=604800) # 7 days

@ -1,9 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.contrib import admin
# Register your models here.

@ -5,7 +5,9 @@
from django.apps import AppConfig
class DashboardConfig(AppConfig):
name = 'dashboard'
name = 'cvat.apps.dashboard'
def ready(self):
# plugin registration
pass

@ -1,9 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.db import models
# Create your models here.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -4,12 +4,9 @@
* SPDX-License-Identifier: MIT
*/
"use strict";
window.addEventListener('DOMContentLoaded', () => {
$(`<button class="menuButton semiBold h2"> Open Task </button>`).on('click', () => {
let win = window.open(`${window.location.origin }/dashboard/?jid=${window.cvat.job.id}`, '_blank');
$('<button class="menuButton semiBold h2"> Open Task </button>').on('click', () => {
const win = window.open(`${window.location.origin}/dashboard/?id=${window.cvat.job.task_id}`, '_blank');
win.focus();
}).prependTo('#engineMenuButtons');
});

@ -4,7 +4,7 @@
* SPDX-License-Identifier: MIT
*/
.dashboardTaskUI {
.dashboardItem {
margin: 5px auto;
width: 1200px;
height: 335px;

@ -3,6 +3,7 @@
SPDX-License-Identifier: MIT
-->
{% extends 'engine/base.html' %}
{% load static %}
{% load pagination_tags %}
@ -14,7 +15,8 @@
{% block head_css %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/stylesheet.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/jstree/themes/default/style.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/jstree/themes/default/style.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/js/3rdparty/pagination/pagination.css' %}">
{% for css_file in css_3rdparty %}
<link rel="stylesheet" type="text/css" href="{% static css_file %}">
{% endfor %}
@ -22,7 +24,8 @@
{% block head_js_3rdparty %}
{{ block.super }}
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/jstree/jstree.js' %}"></script>
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/jstree/jstree.min.js' %}"></script>
<script type="text/javascript" src="{% static 'dashboard/js/3rdparty/pagination/pagination.min.js' %}"></script>
{% for js_file in js_3rdparty %}
<script type="text/javascript" src="{% static js_file %}" defer></script>
{% endfor %}
@ -36,17 +39,13 @@
<script type="text/javascript" src="{% static 'engine/js/shapes.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationParser.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/server.js' %}"></script>
<script>
window.maxUploadSize = {{ max_upload_size }};
window.maxUploadCount = {{ max_upload_count }};
</script>
{% endblock %}
{% block content %}
<div id="content">
<div style="width: 100%; display: flex;">
<div style="width: 50%; display: flex;"> </div>
<div style="width: 50%; display: flex;">
<div style="width: 100%; display: flex;">
<div id="dashboardManageButtons" style="display: flex;">
<button id="dashboardCreateTaskButton" class="regular h1" style="padding: 7px;"> Create New Task </button>
</div>
@ -58,13 +57,9 @@
<div style="width: 50%; display: flex;"> </div>
</div>
{% autopaginate data %}
<div style="float: left; width: 100%">
{% for item in data %}
{% include "dashboard/task.html" %}
{% endfor %}
<div id="dashboardPagination">
<div id="dashboardList" style="float: left; width: 100%"> </div>
</div>
<center>{% paginate %}</center>
</div>
<div id="dashboardCreateModal" class="modal hidden">
@ -113,14 +108,6 @@ Example: @select=race:__undefined__,skip,asian,black,caucasian,other'/>
<br> <input id="dashboardShareSource" type="radio" name="sourceType" value="share"/> <label for="dashboardShareSource" class="regular h2" for="shareSource"> Share </label>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Flip images </label>
</td>
<td>
<input type="checkbox" id="dashboardFlipImages"/>
</td>
</tr>
<tr>
<td>
<label class="regular h2"> Z-Order </label>
@ -180,7 +167,7 @@ Example: @select=race:__undefined__,skip,asian,black,caucasian,other'/>
<div id="dashboardShareBrowseModal" class="modal hidden">
<div style="width: 600px; height: 400px;" class="modal-content noSelect">
<center> <label class="regular h1"> {{ share_path }} </label> </center>
<center> <label id="dashboardShareBasePath" class="regular h1"> </label> </center>
<div id="dashboardShareBrowser"> </div>
<center>
<button id="dashboardCancelBrowseServer" class="regular h2" style="margin: 0px 10px"> Cancel </button>
@ -189,14 +176,16 @@ Example: @select=race:__undefined__,skip,asian,black,caucasian,other'/>
</div>
</div>
<div id="dashboardUpdateModal" class="modal hidden">
<div id="dashboardUpdateContent" class="modal-content">
<input id="dashboardOldLabels" type="text" readonly=true placeholder="Please Wait.." class="regular h2">
<input id="dashboardNewLabels" type="text" placeholder="New Labels" class="regular h2">
<center>
<button id="dashboardCancelUpdate" class="regular h2"> Cancel </button>
<button id="dashboardSubmitUpdate" class="regular h2"> Update </button>
</center>
<template id="dashboardUpdateTemplate">
<div id="dashboardUpdateModal" class="modal">
<div id="dashboardUpdateContent" class="modal-content">
<input id="dashboardOldLabels" type="text" readonly=true class="regular h2">
<input id="dashboardNewLabels" type="text" placeholder="expand the specification here" class="regular h2">
<center>
<button id="dashboardCancelUpdate" class="regular h2"> Cancel </button>
<button id="dashboardSubmitUpdate" class="regular h2"> Update </button>
</center>
</div>
</div>
</div>
</template>
{% endblock %}

@ -1,38 +0,0 @@
<!--
Copyright (C) 2018 Intel Corporation
SPDX-License-Identifier: MIT
-->
<div class="dashboardTaskUI" id="dashboardTask_{{item.id}}">
<center class="dashboardTitleWrapper">
<label class="semiBold h1 dashboardTaskNameLabel selectable"> {{ item.name }} </label>
</center>
<center class="dashboardTitleWrapper">
<label class="regular dashboardStatusLabel"> {{ item.status }} </label>
</center>
<div class="dashboardTaskIntro" style='background-image: url("/get/task/{{item.id}}/frame/0")'> </div>
<div class="dashboardButtonsUI">
<button class="dashboardDumpAnnotation regular dashboardButtonUI"> Dump Annotation </button>
<button class="dashboardUploadAnnotation regular dashboardButtonUI"> Upload Annotation </button>
<button class="dashboardUpdateTask regular dashboardButtonUI"> Update Task </button>
<button class="dashboardDeleteTask regular dashboardButtonUI"> Delete Task </button>
{%if item.bug_tracker %}
<button class="dashboardOpenTrackerButton regular dashboardButtonUI"> Open Bug Tracker </button>
<a class="dashboardBugTrackerLink" href='{{item.bug_tracker}}' style="display: none;"> </a>
{% endif %}
</div>
<div class="dashboardJobsUI">
<center class="dashboardTitleWrapper">
<label class="regular h1"> Jobs </label>
</center>
<table class="dashboardJobList regular">
{% for segm in item.segment_set.all %}
{% for job in segm.job_set.all %}
<tr>
<td> <a href="{{base_url}}?id={{job.id}}"> {{base_url}}?id={{job.id}} </a> </td>
</tr>
{% endfor %}
{% endfor %}
</table>
</div>
</div>

@ -1,7 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
# Create your tests here.

@ -7,7 +7,7 @@ from django.urls import path
from . import views
urlpatterns = [
path('get_share_nodes', views.JsTreeView),
path('', views.DashboardView),
path('meta', views.DashboardMeta),
]

@ -9,73 +9,22 @@ from django.shortcuts import render
from django.conf import settings
from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel, Job as JobModel
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
import os
def ScanNode(directory):
if '..' in directory.split(os.path.sep):
return HttpResponseBadRequest('Permission Denied')
act_dir = os.path.normpath(settings.SHARE_ROOT + directory)
result = []
nodes = os.listdir(act_dir)
files = filter(os.path.isfile, map(lambda f: os.path.join(act_dir, f), nodes))
dirs = filter(os.path.isdir, map(lambda d: os.path.join(act_dir, d), nodes))
for d in dirs:
name = os.path.basename(d)
children = len(os.listdir(d)) > 0
node = {'id': directory + name + '/', 'text': name, 'children': children}
result.append(node)
for f in files:
name = os.path.basename(f)
node = {'id': directory + name, 'text': name, "icon" : "jstree-file"}
result.append(node)
return result
@login_required
def JsTreeView(request):
node_id = None
if 'id' in request.GET:
node_id = request.GET['id']
if node_id is None or node_id == '#':
node_id = '/'
response = [{"id": node_id, "text": node_id, "children": ScanNode(node_id)}]
else:
response = ScanNode(node_id)
return JsonResponse(response, safe=False,
json_dumps_params=dict(ensure_ascii=False))
@login_required
def DashboardView(request):
query_name = request.GET['search'] if 'search' in request.GET else None
query_job = int(request.GET['jid']) if 'jid' in request.GET and request.GET['jid'].isdigit() else None
task_list = None
if query_job is not None and JobModel.objects.filter(pk = query_job).exists():
task_list = [JobModel.objects.select_related('segment__task').get(pk = query_job).segment.task]
else:
task_list = list(TaskModel.objects.prefetch_related('segment_set__job_set').order_by('-created_date').all())
if query_name is not None:
task_list = list(filter(lambda x: query_name.lower() in x.name.lower(), task_list))
task_list = list(filter(lambda task: request.user.has_perm(
'engine.task.access', task), task_list))
return render(request, 'dashboard/dashboard.html', {
'data': task_list,
'js_3rdparty': JS_3RDPARTY.get('dashboard', []),
'css_3rdparty': CSS_3RDPARTY.get('dashboard', []),
})
@login_required
def DashboardMeta(request):
return JsonResponse({
'max_upload_size': settings.LOCAL_LOAD_MAX_FILES_SIZE,
'max_upload_count': settings.LOCAL_LOAD_MAX_FILES_COUNT,
'base_url': "{0}://{1}/".format(request.scheme, request.get_host()),
'share_path': os.getenv('CVAT_SHARE_URL', default=r'${cvat_root}/share'),
'js_3rdparty': JS_3RDPARTY.get('dashboard', []),
'css_3rdparty': CSS_3RDPARTY.get('dashboard', []),
})
})

@ -69,7 +69,6 @@ class DEXTR_HANDLER:
input_dextr = np.concatenate((resized, heatmap[:, :, np.newaxis].astype(resized.dtype)), axis=2)
input_dextr = input_dextr.transpose((2,0,1))
np.set_printoptions(threshold=np.nan)
pred = self._exec_network.infer(inputs={self._input_blob: input_dextr[np.newaxis, ...]})[self._output_blob][0, 0, :, :]
pred = cv2.resize(pred, tuple(reversed(numpy_cropped.shape[:2])), interpolation = cv2.INTER_CUBIC)
result = np.zeros(numpy_image.shape[:2])

@ -8,7 +8,6 @@ from rules.contrib.views import permission_required, objectgetter
from cvat.apps.engine.models import Job
from cvat.apps.engine.log import slogger
from cvat.apps.engine.task import get_frame_path
from cvat.apps.dextr_segmentation.dextr import DEXTR_HANDLER
import django_rq
@ -39,8 +38,8 @@ def create(request, jid):
slogger.job[jid].info("create dextr request for the JOB: {} ".format(jid)
+ "by the USER: {} on the FRAME: {}".format(username, frame))
tid = Job.objects.select_related("segment__task").get(id=jid).segment.task.id
im_path = os.path.realpath(get_frame_path(tid, frame))
db_task = Job.objects.select_related("segment__task").get(id=jid).segment.task
im_path = os.path.realpath(db_task.get_frame_path(frame))
queue = django_rq.get_queue(__RQ_QUEUE_NAME)
rq_id = "dextr.create/{}/{}".format(jid, username)

@ -5,5 +5,4 @@
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['documentation/js/shortcuts.js']
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['documentation/js/dashboardPlugin.js']

@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
window.addEventListener('DOMContentLoaded', () => {
$('<button class="regular h1" style="margin-left: 5px;"> User Guide </button>').on('click', () => {
window.open('/documentation/user_guide.html');
}).appendTo('#dashboardManageButtons');
});

@ -1,15 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
Mousetrap:false
*/
Mousetrap.bind(window.cvat.config.shortkeys["open_help"].value, function() {
window.open("/documentation/user_guide.html");
return false;
});

@ -61,8 +61,6 @@ There you can:
__Source__. To create huge tasks please use ``shared`` server directory (choose ``Share`` option in the dialog).
__Flip images__. All selected files will be turned around 180.
__Z-Order__. Defines the order on drawn polygons. Check the box for enable layered displaying.
__Overlap Size__. Use this option to make overlapped segments. The option makes tracks continuous from one segment into another. Use it for interpolation mode. There are several use cases for the parameter:
@ -116,7 +114,7 @@ Usage examples:
![](static/documentation/images/image082.jpg) ![](static/documentation/images/image081.jpg)
2. Create a new annotation:
- Choose right ``Shape`` (box etc.) and ``Label`` (was specified by you while creating the task) beforehand:
![](static/documentation/images/image080.jpg) ![](static/documentation/images/image083.jpg)
@ -294,7 +292,7 @@ By clicking on the points of poly-shapes ``Remove`` option is available.
By clicking outside any shapes you can either copy ``Frame URL`` (link to present frame) or ``Job URL`` (link from address bar)
![](static/documentation/images/image091.jpg)
![](static/documentation/images/image091.jpg)
---
### Settings
@ -574,7 +572,7 @@ When ``Shift`` isn't pressed, you can zoom in/out (on mouse wheel scroll) and mo
![](static/documentation/images/gif007.gif)
Also you can set fixed number of points in the field "poly shape size", then drawing will be stopped automatically.
Also you can set fixed number of points in the field "poly shape size", then drawing will be stopped automatically.
To enable dragging, right-click inside polygon and choose ``Enable Dragging``.

@ -1,3 +1,5 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
default_app_config = 'cvat.apps.engine.apps.EngineConfig'

@ -56,8 +56,7 @@ class SegmentAdmin(admin.ModelAdmin):
class TaskAdmin(admin.ModelAdmin):
date_hierarchy = 'updated_date'
readonly_fields = ('size', 'path', 'created_date', 'updated_date',
'overlap', 'flipped')
readonly_fields = ('size', 'created_date', 'updated_date', 'overlap', 'flipped')
list_display = ('name', 'mode', 'owner', 'assignee', 'created_date', 'updated_date')
search_fields = ('name', 'mode', 'owner__username', 'owner__first_name',
'owner__last_name', 'owner__email', 'assignee__username', 'assignee__first_name',

File diff suppressed because it is too large Load Diff

@ -5,7 +5,12 @@
from django.apps import AppConfig
class EngineConfig(AppConfig):
name = 'engine'
name = 'cvat.apps.engine'
def ready(self):
from django.db.models.signals import post_save
from .signals import update_task_status
post_save.connect(update_task_status, sender='engine.Job',
dispatch_uid="update_task_status")

@ -90,7 +90,8 @@ class dotdict(dict):
clogger = dotdict({
'task': TaskClientLoggerStorage(),
'job': JobClientLoggerStorage()
'job': JobClientLoggerStorage(),
'glob': logging.getLogger('cvat.client'),
})
slogger = dotdict({

@ -0,0 +1,212 @@
# Generated by Django 2.1.5 on 2019-02-17 19:32
from django.conf import settings
from django.db import migrations, models
import django.db.migrations.operations.special
import django.db.models.deletion
import cvat.apps.engine.models
def set_segment_size(apps, schema_editor):
Task = apps.get_model('engine', 'Task')
for task in Task.objects.all():
segment = task.segment_set.first()
if segment:
task.segment_size = segment.stop_frame - segment.start_frame + 1
task.save()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('engine', '0014_job_max_shape_id'),
]
operations = [
migrations.AddField(
model_name='task',
name='segment_size',
field=models.PositiveIntegerField(null=True),
),
migrations.RunPython(
code=set_segment_size,
reverse_code=django.db.migrations.operations.special.RunPython.noop,
),
migrations.AlterField(
model_name='task',
name='segment_size',
field=models.PositiveIntegerField(),
),
migrations.CreateModel(
name='ClientFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.FileField(max_length=1024, storage=cvat.apps.engine.models.MyFileSystemStorage(),
upload_to=cvat.apps.engine.models.upload_path_handler)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Task')),
],
options={
'default_permissions': (),
},
),
migrations.CreateModel(
name='RemoteFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(max_length=1024)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Task')),
],
options={
'default_permissions': (),
},
),
migrations.CreateModel(
name='ServerFile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('file', models.CharField(max_length=1024)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Task')),
],
options={
'default_permissions': (),
},
),
migrations.AlterField(
model_name='task',
name='status',
field=models.CharField(choices=[('ANNOTATION', 'annotation'), ('VALIDATION', 'validation'), ('COMPLETED', 'completed')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32),
),
migrations.AlterField(
model_name='task',
name='overlap',
field=models.PositiveIntegerField(null=True),
),
migrations.RemoveField(
model_name='task',
name='path',
),
migrations.AddField(
model_name='task',
name='image_quality',
field=models.PositiveSmallIntegerField(default=50),
),
migrations.CreateModel(
name='Plugin',
fields=[
('name', models.SlugField(max_length=32, primary_key=True, serialize=False)),
('description', cvat.apps.engine.models.SafeCharField(max_length=8192)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now_add=True)),
('maintainer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='maintainers', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': (),
},
),
migrations.CreateModel(
name='PluginOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', cvat.apps.engine.models.SafeCharField(max_length=32)),
('value', cvat.apps.engine.models.SafeCharField(max_length=1024)),
('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Plugin')),
],
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('task', 'name')},
),
migrations.AlterUniqueTogether(
name='clientfile',
unique_together={('task', 'file')},
),
migrations.AddField(
model_name='attributespec',
name='default_value',
field=models.CharField(default='', max_length=128),
preserve_default=False,
),
migrations.AddField(
model_name='attributespec',
name='input_type',
field=models.CharField(choices=[('CHECKBOX', 'checkbox'), ('RADIO', 'radio'), ('NUMBER', 'number'), ('TEXT', 'text'), ('SELECT', 'select')], default='select', max_length=16),
preserve_default=False,
),
migrations.AddField(
model_name='attributespec',
name='mutable',
field=models.BooleanField(default=True),
preserve_default=False,
),
migrations.AddField(
model_name='attributespec',
name='name',
field=models.CharField(default='test', max_length=64),
preserve_default=False,
),
migrations.AddField(
model_name='attributespec',
name='values',
field=models.CharField(default='', max_length=4096),
preserve_default=False,
),
migrations.AlterField(
model_name='job',
name='status',
field=models.CharField(choices=[('ANNOTATION', 'annotation'), ('VALIDATION', 'validation'), ('COMPLETED', 'completed')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32),
),
migrations.AlterField(
model_name='attributespec',
name='text',
field=models.CharField(default='', max_length=1024),
),
migrations.AlterField(
model_name='attributespec',
name='input_type',
field=models.CharField(choices=[('checkbox', 'CHECKBOX'), ('radio', 'RADIO'), ('number', 'NUMBER'), ('text', 'TEXT'), ('select', 'SELECT')], max_length=16),
),
migrations.AlterField(
model_name='task',
name='segment_size',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='job',
name='status',
field=models.CharField(choices=[('annotation', 'ANNOTATION'), ('validation', 'VALIDATION'), ('completed', 'COMPLETED')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32),
),
migrations.AlterField(
model_name='task',
name='status',
field=models.CharField(choices=[('annotation', 'ANNOTATION'), ('validation', 'VALIDATION'), ('completed', 'COMPLETED')], default=cvat.apps.engine.models.StatusChoice('annotation'), max_length=32),
),
migrations.CreateModel(
name='Image',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(max_length=1024)),
('frame', models.PositiveIntegerField()),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Task')),
('height', models.PositiveIntegerField()),
('width', models.PositiveIntegerField()),
],
options={
'default_permissions': (),
},
),
migrations.CreateModel(
name='Video',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('path', models.CharField(max_length=1024)),
('start_frame', models.PositiveIntegerField()),
('stop_frame', models.PositiveIntegerField()),
('step', models.PositiveIntegerField(default=1)),
('task', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='engine.Task')),
('height', models.PositiveIntegerField()),
('width', models.PositiveIntegerField()),
],
options={
'default_permissions': (),
},
),
]

@ -0,0 +1,172 @@
import os
import re
import csv
from io import StringIO
from PIL import Image
from django.db import migrations
from django.conf import settings
from cvat.apps.engine.task import _get_mime
def parse_attribute(value):
match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', value)
if match:
prefix = match.group(1)
input_type = match.group(2)
name = match.group(3)
if match.group(4):
values = list(csv.reader(StringIO(match.group(4)),
quotechar="'"))[0]
else:
values = []
return {'prefix':prefix, 'type':input_type, 'name':name, 'values':values}
else:
return None
def split_text_attribute(apps, schema_editor):
AttributeSpec = apps.get_model('engine', 'AttributeSpec')
for attribute in AttributeSpec.objects.all():
spec = parse_attribute(attribute.text)
if spec:
attribute.mutable = (spec['prefix'] == '~')
attribute.input_type = spec['type']
attribute.name = spec['name']
attribute.default_value = spec['values'][0] if spec['values'] else ''
attribute.values = '\n'.join(spec['values'])
attribute.save()
def join_text_attribute(apps, schema_editor):
AttributeSpec = apps.get_model('engine', 'AttributeSpec')
for attribute in AttributeSpec.objects.all():
attribute.text = ""
if attribute.mutable:
attribute.text += "~"
else:
attribute.text += "@"
attribute.text += attribute.input_type
attribute.text += "=" + attribute.name + ":"
attribute.text += ",".join(attribute.values.split('\n'))
attribute.save()
def _get_task_dirname(task_obj):
return os.path.join(settings.DATA_ROOT, str(task_obj.id))
def _get_upload_dirname(task_obj):
return os.path.join(_get_task_dirname(task_obj), ".upload")
def _get_frame_path(task_obj, frame):
return os.path.join(
_get_task_dirname(task_obj),
"data",
str(int(frame) // 10000),
str(int(frame) // 100),
str(frame) + '.jpg',
)
def fill_task_meta_data_forward(apps, schema_editor):
db_alias = schema_editor.connection.alias
task_model = apps.get_model('engine', 'Task')
video_model = apps.get_model('engine', "Video")
image_model = apps.get_model('engine', 'Image')
for db_task in task_model.objects.all():
if db_task.mode == 'interpolation':
db_video = video_model()
db_video.task_id = db_task.id
db_video.start_frame = 0
db_video.stop_frame = db_task.size
db_video.step = 1
video = ""
for root, _, files in os.walk(_get_upload_dirname(db_task)):
fullnames = map(lambda f: os.path.join(root, f), files)
videos = list(filter(lambda x: _get_mime(x) == 'video', fullnames))
if len(videos):
video = videos[0]
break
db_video.path = video
try:
image = Image.open(_get_frame_path(db_task, 0))
db_video.width = image.width
db_video.height = image.height
image.close()
except FileNotFoundError:
db_video.width = 0
db_video.height = 0
db_video.save()
else:
filenames = []
for root, _, files in os.walk(_get_upload_dirname(db_task)):
fullnames = map(lambda f: os.path.join(root, f), files)
images = filter(lambda x: _get_mime(x) == 'image', fullnames)
filenames.extend(images)
filenames.sort()
db_images = []
for i, image_path in enumerate(filenames):
db_image = image_model()
db_image.task_id = db_task.id
db_image.path = image_path
db_image.frame = i
try:
image = Image.open(image_path)
db_image.width = image.width
db_image.height = image.height
image.close()
except FileNotFoundError:
db_image.width = 0
db_image.height = 0
db_images.append(db_image)
image_model.objects.using(db_alias).bulk_create(db_images)
def fill_task_meta_data_backward(apps, schema_editor):
task_model = apps.get_model('engine', 'Task')
video_model = apps.get_model('engine', "Video")
image_model = apps.get_model('engine', 'Image')
for db_task in task_model.objects.all():
upload_dir = _get_upload_dirname(db_task)
if db_task.mode == 'interpolation':
video = video_model.objects.get(task__id=db_task.id)
db_task.source = os.path.relpath(video.path, upload_dir)
video.delete()
else:
images = image_model.objects.filter(task__id=db_task.id)
db_task.source = '{} images: {}, ...'.format(
len(images),
", ".join([os.path.relpath(x.path, upload_dir) for x in images[0:2]])
)
images.delete()
db_task.save()
class Migration(migrations.Migration):
dependencies = [
('engine', '0015_db_redesign_20190217'),
]
operations = [
migrations.RunPython(
code=split_text_attribute,
reverse_code=join_text_attribute,
),
migrations.RemoveField(
model_name='attributespec',
name='text',
),
migrations.AlterUniqueTogether(
name='attributespec',
unique_together={('label', 'name')},
),
migrations.RunPython(
code=fill_task_meta_data_forward,
reverse_code=fill_task_meta_data_backward,
),
migrations.RemoveField(
model_name='task',
name='source',
),
]

@ -0,0 +1,915 @@
# Generated by Django 2.1.5 on 2019-02-21 12:25
import cvat.apps.engine.models
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
from cvat.apps.engine.annotation import _merge_table_rows
# some modified functions to transer annotation
def _bulk_create(db_model, db_alias, objects, flt_param):
if objects:
if flt_param:
if 'postgresql' in settings.DATABASES["default"]["ENGINE"]:
return db_model.objects.using(db_alias).bulk_create(objects)
else:
ids = list(db_model.objects.using(db_alias).filter(**flt_param).values_list('id', flat=True))
db_model.objects.using(db_alias).bulk_create(objects)
return list(db_model.objects.using(db_alias).exclude(id__in=ids).filter(**flt_param))
else:
return db_model.objects.using(db_alias).bulk_create(objects)
def get_old_db_shapes(shape_type, db_job):
def _get_shape_set(db_job, shape_type):
if shape_type == 'polygons':
return db_job.labeledpolygon_set
elif shape_type == 'polylines':
return db_job.labeledpolyline_set
elif shape_type == 'boxes':
return db_job.labeledbox_set
elif shape_type == 'points':
return db_job.labeledpoints_set
def get_values(shape_type):
if shape_type == 'polygons':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpolygonattributeval__value', 'labeledpolygonattributeval__spec_id',
'labeledpolygonattributeval__id'), {
'attributes': [
'labeledpolygonattributeval__value',
'labeledpolygonattributeval__spec_id',
'labeledpolygonattributeval__id'
]
}, 'labeledpolygonattributeval_set'
]
elif shape_type == 'polylines':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpolylineattributeval__value', 'labeledpolylineattributeval__spec_id',
'labeledpolylineattributeval__id'), {
'attributes': [
'labeledpolylineattributeval__value',
'labeledpolylineattributeval__spec_id',
'labeledpolylineattributeval__id'
]
}, 'labeledpolylineattributeval_set'
]
elif shape_type == 'boxes':
return [
('id', 'frame', 'xtl', 'ytl', 'xbr', 'ybr', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledboxattributeval__value', 'labeledboxattributeval__spec_id',
'labeledboxattributeval__id'), {
'attributes': [
'labeledboxattributeval__value',
'labeledboxattributeval__spec_id',
'labeledboxattributeval__id'
]
}, 'labeledboxattributeval_set'
]
elif shape_type == 'points':
return [
('id', 'frame', 'points', 'label_id', 'group_id', 'occluded', 'z_order', 'client_id',
'labeledpointsattributeval__value', 'labeledpointsattributeval__spec_id',
'labeledpointsattributeval__id'), {
'attributes': [
'labeledpointsattributeval__value',
'labeledpointsattributeval__spec_id',
'labeledpointsattributeval__id'
]
}, 'labeledpointsattributeval_set'
]
(values, merge_keys, prefetch) = get_values(shape_type)
db_shapes = list(_get_shape_set(db_job, shape_type).prefetch_related(prefetch).values(*values).order_by('frame'))
return _merge_table_rows(db_shapes, merge_keys, 'id')
def get_old_db_paths(db_job):
db_paths = db_job.objectpath_set
for shape in ['trackedpoints_set', 'trackedbox_set', 'trackedpolyline_set', 'trackedpolygon_set']:
db_paths.prefetch_related(shape)
for shape_attr in ['trackedpoints_set__trackedpointsattributeval_set', 'trackedbox_set__trackedboxattributeval_set',
'trackedpolygon_set__trackedpolygonattributeval_set', 'trackedpolyline_set__trackedpolylineattributeval_set']:
db_paths.prefetch_related(shape_attr)
db_paths.prefetch_related('objectpathattributeval_set')
db_paths = list (db_paths.values('id', 'frame', 'group_id', 'shapes', 'client_id', 'objectpathattributeval__spec_id',
'objectpathattributeval__id', 'objectpathattributeval__value',
'trackedbox', 'trackedpolygon', 'trackedpolyline', 'trackedpoints',
'trackedbox__id', 'label_id', 'trackedbox__xtl', 'trackedbox__ytl',
'trackedbox__xbr', 'trackedbox__ybr', 'trackedbox__frame', 'trackedbox__occluded',
'trackedbox__z_order','trackedbox__outside', 'trackedbox__trackedboxattributeval__spec_id',
'trackedbox__trackedboxattributeval__value', 'trackedbox__trackedboxattributeval__id',
'trackedpolygon__id' ,'trackedpolygon__points', 'trackedpolygon__frame', 'trackedpolygon__occluded',
'trackedpolygon__z_order', 'trackedpolygon__outside', 'trackedpolygon__trackedpolygonattributeval__spec_id',
'trackedpolygon__trackedpolygonattributeval__value', 'trackedpolygon__trackedpolygonattributeval__id',
'trackedpolyline__id', 'trackedpolyline__points', 'trackedpolyline__frame', 'trackedpolyline__occluded',
'trackedpolyline__z_order', 'trackedpolyline__outside', 'trackedpolyline__trackedpolylineattributeval__spec_id',
'trackedpolyline__trackedpolylineattributeval__value', 'trackedpolyline__trackedpolylineattributeval__id',
'trackedpoints__id', 'trackedpoints__points', 'trackedpoints__frame', 'trackedpoints__occluded',
'trackedpoints__z_order', 'trackedpoints__outside', 'trackedpoints__trackedpointsattributeval__spec_id',
'trackedpoints__trackedpointsattributeval__value', 'trackedpoints__trackedpointsattributeval__id')
.order_by('id', 'trackedbox__frame', 'trackedpolygon__frame', 'trackedpolyline__frame', 'trackedpoints__frame'))
db_box_paths = list(filter(lambda path: path['shapes'] == 'boxes', db_paths ))
db_polygon_paths = list(filter(lambda path: path['shapes'] == 'polygons', db_paths ))
db_polyline_paths = list(filter(lambda path: path['shapes'] == 'polylines', db_paths ))
db_points_paths = list(filter(lambda path: path['shapes'] == 'points', db_paths ))
object_path_attr_merge_key = [
'objectpathattributeval__value',
'objectpathattributeval__spec_id',
'objectpathattributeval__id'
]
db_box_paths = _merge_table_rows(db_box_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedbox__id', 'trackedbox__xtl', 'trackedbox__ytl',
'trackedbox__xbr', 'trackedbox__ybr', 'trackedbox__frame',
'trackedbox__occluded', 'trackedbox__z_order', 'trackedbox__outside',
'trackedbox__trackedboxattributeval__value',
'trackedbox__trackedboxattributeval__spec_id',
'trackedbox__trackedboxattributeval__id'
],
}, 'id')
db_polygon_paths = _merge_table_rows(db_polygon_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpolygon__id', 'trackedpolygon__points', 'trackedpolygon__frame',
'trackedpolygon__occluded', 'trackedpolygon__z_order', 'trackedpolygon__outside',
'trackedpolygon__trackedpolygonattributeval__value',
'trackedpolygon__trackedpolygonattributeval__spec_id',
'trackedpolygon__trackedpolygonattributeval__id'
]
}, 'id')
db_polyline_paths = _merge_table_rows(db_polyline_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpolyline__id', 'trackedpolyline__points', 'trackedpolyline__frame',
'trackedpolyline__occluded', 'trackedpolyline__z_order', 'trackedpolyline__outside',
'trackedpolyline__trackedpolylineattributeval__value',
'trackedpolyline__trackedpolylineattributeval__spec_id',
'trackedpolyline__trackedpolylineattributeval__id'
],
}, 'id')
db_points_paths = _merge_table_rows(db_points_paths, {
'attributes': object_path_attr_merge_key,
'shapes': [
'trackedpoints__id', 'trackedpoints__points', 'trackedpoints__frame',
'trackedpoints__occluded', 'trackedpoints__z_order', 'trackedpoints__outside',
'trackedpoints__trackedpointsattributeval__value',
'trackedpoints__trackedpointsattributeval__spec_id',
'trackedpoints__trackedpointsattributeval__id'
]
}, 'id')
for db_box_path in db_box_paths:
db_box_path.attributes = list(set(db_box_path.attributes))
db_box_path.type = 'box_path'
db_box_path.shapes = _merge_table_rows(db_box_path.shapes, {
'attributes': [
'trackedboxattributeval__value',
'trackedboxattributeval__spec_id',
'trackedboxattributeval__id'
]
}, 'id')
for db_polygon_path in db_polygon_paths:
db_polygon_path.attributes = list(set(db_polygon_path.attributes))
db_polygon_path.type = 'poligon_path'
db_polygon_path.shapes = _merge_table_rows(db_polygon_path.shapes, {
'attributes': [
'trackedpolygonattributeval__value',
'trackedpolygonattributeval__spec_id',
'trackedpolygonattributeval__id'
]
}, 'id')
for db_polyline_path in db_polyline_paths:
db_polyline_path.attributes = list(set(db_polyline_path.attributes))
db_polyline_path.type = 'polyline_path'
db_polyline_path.shapes = _merge_table_rows(db_polyline_path.shapes, {
'attributes': [
'trackedpolylineattributeval__value',
'trackedpolylineattributeval__spec_id',
'trackedpolylineattributeval__id'
]
}, 'id')
for db_points_path in db_points_paths:
db_points_path.attributes = list(set(db_points_path.attributes))
db_points_path.type = 'points_path'
db_points_path.shapes = _merge_table_rows(db_points_path.shapes, {
'attributes': [
'trackedpointsattributeval__value',
'trackedpointsattributeval__spec_id',
'trackedpointsattributeval__id'
]
}, 'id')
return db_box_paths + db_polygon_paths + db_polyline_paths + db_points_paths
def process_shapes(db_job, apps, db_labels, db_attributes, db_alias):
LabeledShape = apps.get_model('engine', 'LabeledShape')
LabeledShapeAttributeVal = apps.get_model('engine', 'LabeledShapeAttributeVal')
new_db_shapes = []
new_db_attrvals = []
for shape_type in ['boxes', 'points', 'polygons', 'polylines']:
for shape in get_old_db_shapes(shape_type, db_job):
new_db_shape = LabeledShape()
new_db_shape.job = db_job
new_db_shape.label = db_labels[shape.label_id]
new_db_shape.group = shape.group_id
if shape_type == 'boxes':
new_db_shape.type = cvat.apps.engine.models.ShapeType.RECTANGLE
new_db_shape.points = [shape.xtl, shape.ytl, shape.xbr, shape.ybr]
else:
new_db_shape.points = shape.points.replace(',', ' ').split()
if shape_type == 'points':
new_db_shape.type = cvat.apps.engine.models.ShapeType.POINTS
elif shape_type == 'polygons':
new_db_shape.type = cvat.apps.engine.models.ShapeType.POLYGON
elif shape_type == 'polylines':
new_db_shape.type = cvat.apps.engine.models.ShapeType.POLYLINE
new_db_shape.frame = shape.frame
new_db_shape.occluded = shape.occluded
new_db_shape.z_order = shape.z_order
for attr in shape.attributes:
db_attrval = LabeledShapeAttributeVal()
db_attrval.shape_id = len(new_db_shapes)
db_attrval.spec = db_attributes[attr.spec_id]
db_attrval.value = attr.value
new_db_attrvals.append(db_attrval)
new_db_shapes.append(new_db_shape)
new_db_shapes = _bulk_create(LabeledShape, db_alias, new_db_shapes, {"job_id": db_job.id})
for db_attrval in new_db_attrvals:
db_attrval.shape_id = new_db_shapes[db_attrval.shape_id].id
_bulk_create(LabeledShapeAttributeVal, db_alias, new_db_attrvals, {})
def process_paths(db_job, apps, db_labels, db_attributes, db_alias):
TrackedShape = apps.get_model('engine', 'TrackedShape')
LabeledTrack = apps.get_model('engine', 'LabeledTrack')
LabeledTrackAttributeVal = apps.get_model('engine', 'LabeledTrackAttributeVal')
TrackedShapeAttributeVal = apps.get_model('engine', 'TrackedShapeAttributeVal')
tracks = get_old_db_paths(db_job)
new_db_tracks = []
new_db_track_attrvals = []
new_db_shapes = []
new_db_shape_attrvals = []
for track in tracks:
db_track = LabeledTrack()
db_track.job = db_job
db_track.label = db_labels[track.label_id]
db_track.frame = track.frame
db_track.group = track.group_id
for attr in track.attributes:
db_attrspec = db_attributes[attr.spec_id]
db_attrval = LabeledTrackAttributeVal()
db_attrval.track_id = len(new_db_tracks)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
new_db_track_attrvals.append(db_attrval)
for shape in track.shapes:
db_shape = TrackedShape()
db_shape.track_id = len(new_db_tracks)
db_shape.frame = shape.frame
db_shape.occluded = shape.occluded
db_shape.z_order = shape.z_order
db_shape.outside = shape.outside
if track.type == 'box_path':
db_shape.type = cvat.apps.engine.models.ShapeType.RECTANGLE
db_shape.points = [shape.xtl, shape.ytl, shape.xbr, shape.ybr]
else:
db_shape.points = shape.points.replace(',', ' ').split()
if track.type == 'points_path':
db_shape.type = cvat.apps.engine.models.ShapeType.POINTS
elif track.type == 'polygon_path':
db_shape.type = cvat.apps.engine.models.ShapeType.POLYGON
elif track.type == 'polyline_path':
db_shape.type = cvat.apps.engine.models.ShapeType.POLYLINE
for attr in shape.attributes:
db_attrspec = db_attributes[attr.spec_id]
db_attrval = TrackedShapeAttributeVal()
db_attrval.shape_id = len(new_db_shapes)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
new_db_shape_attrvals.append(db_attrval)
new_db_shapes.append(db_shape)
new_db_tracks.append(db_track)
new_db_tracks = _bulk_create(LabeledTrack, db_alias, new_db_tracks, {"job_id": db_job.id})
for db_attrval in new_db_track_attrvals:
db_attrval.track_id = new_db_tracks[db_attrval.track_id].id
_bulk_create(LabeledTrackAttributeVal, db_alias, new_db_track_attrvals, {})
for db_shape in new_db_shapes:
db_shape.track_id = new_db_tracks[db_shape.track_id].id
new_db_shapes = _bulk_create(TrackedShape, db_alias, new_db_shapes, {"track__job_id": db_job.id})
for db_attrval in new_db_shape_attrvals:
db_attrval.shape_id = new_db_shapes[db_attrval.shape_id].id
_bulk_create(TrackedShapeAttributeVal, db_alias, new_db_shape_attrvals, {})
def copy_annotations_forward(apps, schema_editor):
db_alias = schema_editor.connection.alias
Task = apps.get_model('engine', 'Task')
AttributeSpec = apps.get_model('engine', 'AttributeSpec')
for task in Task.objects.all():
print("run anno migration for the task {}".format(task.id))
db_labels = {db_label.id:db_label for db_label in task.label_set.all()}
db_attributes = {db_attr.id:db_attr for db_attr in AttributeSpec.objects.filter(label__task__id=task.id)}
for segment in task.segment_set.prefetch_related('job_set').all():
db_job = segment.job_set.first()
print("run anno migration for the job {}".format(db_job.id))
process_shapes(db_job, apps, db_labels, db_attributes, db_alias)
process_paths(db_job, apps, db_labels, db_attributes, db_alias)
def _save_old_shapes_to_db(apps, db_shapes, db_attributes, db_alias, db_job):
def _get_shape_class(shape_type):
if shape_type == 'polygons':
return apps.get_model('engine', 'LabeledPolygon')
elif shape_type == 'polylines':
return apps.get_model('engine', 'LabeledPolyline')
elif shape_type == 'boxes':
return apps.get_model('engine', 'LabeledBox')
elif shape_type == 'points':
return apps.get_model('engine', 'LabeledPoints')
def _get_shape_attr_class(shape_type):
if shape_type == 'polygons':
return apps.get_model('engine', 'LabeledPolygonAttributeVal')
elif shape_type == 'polylines':
return apps.get_model('engine', 'LabeledPolylineAttributeVal')
elif shape_type == 'boxes':
return apps.get_model('engine', 'LabeledBoxAttributeVal')
elif shape_type == 'points':
return apps.get_model('engine', 'LabeledPointsAttributeVal')
shapes = [
list(filter(lambda s: s.type == cvat.apps.engine.models.ShapeType.RECTANGLE, db_shapes)),
list(filter(lambda s: s.type == cvat.apps.engine.models.ShapeType.POLYLINE, db_shapes)),
list(filter(lambda s: s.type == cvat.apps.engine.models.ShapeType.POLYGON, db_shapes)),
list(filter(lambda s: s.type == cvat.apps.engine.models.ShapeType.POINTS, db_shapes)),
]
for i, shape_type in enumerate(['boxes', 'polylines', 'polygons', 'points']):
new_db_shapes = []
new_db_attrvals = []
for shape in shapes[i]:
db_shape = _get_shape_class(shape_type)()
db_shape.job = shape.job
db_shape.label = shape.label
db_shape.group_id = shape.group
if shape.type == cvat.apps.engine.models.ShapeType.RECTANGLE:
db_shape.xtl = shape.points[0]
db_shape.ytl = shape.points[1]
db_shape.xbr = shape.points[2]
db_shape.ybr = shape.points[3]
else:
point_iterator = iter(shape.points)
db_shape.points = ' '.join(['{},{}'.format(point, next(point_iterator)) for point in point_iterator])
db_shape.frame = shape.frame
db_shape.occluded = shape.occluded
db_shape.z_order = shape.z_order
for attr in list(shape.labeledshapeattributeval_set.all()):
db_attrval = _get_shape_attr_class(shape_type)()
if shape.type == cvat.apps.engine.models.ShapeType.POLYGON:
db_attrval.polygon_id = len(new_db_shapes)
elif shape.type == cvat.apps.engine.models.ShapeType.POLYLINE:
db_attrval.polyline_id = len(new_db_shapes)
elif shape.type == cvat.apps.engine.models.ShapeType.RECTANGLE:
db_attrval.box_id = len(new_db_shapes)
else:
db_attrval.points_id = len(new_db_shapes)
db_attrval.spec = db_attributes[attr.spec_id]
db_attrval.value = attr.value
new_db_attrvals.append(db_attrval)
new_db_shapes.append(db_shape)
new_db_shapes = _bulk_create(_get_shape_class(shape_type), db_alias, new_db_shapes, {"job_id": db_job.id})
for db_attrval in new_db_attrvals:
if shape_type == 'polygons':
db_attrval.polygon_id = new_db_shapes[db_attrval.polygon_id].id
elif shape_type == 'polylines':
db_attrval.polyline_id = new_db_shapes[db_attrval.polyline_id].id
elif shape_type == 'boxes':
db_attrval.box_id = new_db_shapes[db_attrval.box_id].id
else:
db_attrval.points_id = new_db_shapes[db_attrval.points_id].id
_bulk_create(_get_shape_attr_class(shape_type), db_alias, new_db_attrvals, {})
def _save_old_tracks_to_db(apps, db_shapes, db_attributes, db_alias, db_job):
def _get_shape_class(shape_type):
if shape_type == 'polygon_paths':
return apps.get_model('engine', 'TrackedPolygon')
elif shape_type == 'polyline_paths':
return apps.get_model('engine', 'TrackedPolyline')
elif shape_type == 'box_paths':
return apps.get_model('engine', 'TrackedBox')
elif shape_type == 'points_paths':
return apps.get_model('engine', 'TrackedPoints')
def _get_shape_attr_class(shape_type):
if shape_type == 'polygon_paths':
return apps.get_model('engine', 'TrackedPolygonAttributeVal')
elif shape_type == 'polyline_paths':
return apps.get_model('engine', 'TrackedPolylineAttributeVal')
elif shape_type == 'box_paths':
return apps.get_model('engine', 'TrackedBoxAttributeVal')
elif shape_type == 'points_paths':
return apps.get_model('engine', 'TrackedPointsAttributeVal')
tracks = [
list(filter(lambda t: t.trackedshape_set.first().type == cvat.apps.engine.models.ShapeType.RECTANGLE, db_shapes)),
list(filter(lambda t: t.trackedshape_set.first().type == cvat.apps.engine.models.ShapeType.POLYLINE, db_shapes)),
list(filter(lambda t: t.trackedshape_set.first().type == cvat.apps.engine.models.ShapeType.POLYGON, db_shapes)),
list(filter(lambda t: t.trackedshape_set.first().type == cvat.apps.engine.models.ShapeType.POINTS, db_shapes)),
]
ObjectPath = apps.get_model('engine', 'ObjectPath')
ObjectPathAttributeVal = apps.get_model('engine', 'ObjectPathAttributeVal')
for i, shape_type in enumerate(['box_paths', 'polyline_paths', 'polygon_paths', 'points_paths', ]):
new_db_paths = []
new_db_path_attrvals = []
new_db_shapes = []
new_db_shape_attrvals = []
for path in tracks[i]:
db_path = ObjectPath()
db_path.job = db_job
db_path.label = path.label
db_path.frame = path.frame
db_path.group_id = path.group
# db_path.client_id = path.client_id
if shape_type == 'polygon_paths':
db_path.shapes = 'polygons'
elif shape_type == 'polyline_paths':
db_path.shapes = 'polylines'
elif shape_type == 'box_paths':
db_path.shapes = 'boxes'
elif shape_type == 'points_paths':
db_path.shapes = 'points'
for attr in list(path.labeledtrackattributeval_set.all()):
db_attrspec = db_attributes[attr.spec_id]
db_attrval = ObjectPathAttributeVal()
db_attrval.track_id = len(new_db_paths)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
new_db_path_attrvals.append(db_attrval)
for shape in list(path.trackedshape_set.all()):
db_shape = _get_shape_class(shape_type)()
db_shape.track_id = len(new_db_paths)
if shape_type == 'box_paths':
db_shape.xtl = shape.points[0]
db_shape.ytl = shape.points[1]
db_shape.xbr = shape.points[2]
db_shape.ybr = shape.points[3]
else:
point_iterator = iter(shape.points)
db_shape.points = ' '.join(['{},{}'.format(point, next(point_iterator)) for point in point_iterator])
db_shape.frame = shape.frame
db_shape.occluded = shape.occluded
db_shape.z_order = shape.z_order
db_shape.outside = shape.outside
for attr in list(shape.trackedshapeattributeval_set.all()):
db_attrspec = db_attributes[attr.spec_id]
db_attrval = _get_shape_attr_class(shape_type)()
if shape_type == 'polygon_paths':
db_attrval.polygon_id = len(new_db_shapes)
elif shape_type == 'polyline_paths':
db_attrval.polyline_id = len(new_db_shapes)
elif shape_type == 'box_paths':
db_attrval.box_id = len(new_db_shapes)
elif shape_type == 'points_paths':
db_attrval.points_id = len(new_db_shapes)
db_attrval.spec = db_attrspec
db_attrval.value = attr.value
new_db_shape_attrvals.append(db_attrval)
new_db_shapes.append(db_shape)
new_db_paths.append(db_path)
new_db_paths = _bulk_create(ObjectPath, db_alias, new_db_paths, {"job_id": db_job.id})
for db_attrval in new_db_path_attrvals:
db_attrval.track_id = new_db_paths[db_attrval.track_id].id
_bulk_create(ObjectPathAttributeVal, db_alias, new_db_path_attrvals, {})
for db_shape in new_db_shapes:
db_shape.track_id = new_db_paths[db_shape.track_id].id
db_shapes = _bulk_create(_get_shape_class(shape_type), db_alias, new_db_shapes, {"track__job_id": db_job.id})
for db_attrval in new_db_shape_attrvals:
if shape_type == 'polygon_paths':
db_attrval.polygon_id = db_shapes[db_attrval.polygon_id].id
elif shape_type == 'polyline_paths':
db_attrval.polyline_id = db_shapes[db_attrval.polyline_id].id
elif shape_type == 'box_paths':
db_attrval.box_id = db_shapes[db_attrval.box_id].id
elif shape_type == 'points_paths':
db_attrval.points_id = db_shapes[db_attrval.points_id].id
_bulk_create(_get_shape_attr_class(shape_type), db_alias, new_db_shape_attrvals, {})
def copy_annotations_backward(apps, schema_editor):
Task = apps.get_model('engine', 'Task')
AttributeSpec = apps.get_model('engine', 'AttributeSpec')
db_alias = schema_editor.connection.alias
for task in Task.objects.all():
db_attributes = {db_attr.id:db_attr for db_attr in AttributeSpec.objects.filter(label__task__id=task.id)}
for segment in task.segment_set.prefetch_related('job_set').all():
db_job = segment.job_set.first()
db_shapes = list(db_job.labeledshape_set
.prefetch_related("label")
.prefetch_related("labeledshapeattributeval_set"))
_save_old_shapes_to_db(apps, db_shapes, db_attributes, db_alias, db_job)
db_tracks = list(db_job.labeledtrack_set
.select_related("label")
.prefetch_related("labeledtrackattributeval_set")
.prefetch_related("trackedshape_set__trackedshapeattributeval_set"))
_save_old_tracks_to_db(apps, db_tracks, db_attributes, db_alias, db_job)
class Migration(migrations.Migration):
dependencies = [
('engine', '0016_attribute_spec_20190217'),
]
operations = [
migrations.CreateModel(
name='LabeledImageAttributeVal',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('value', cvat.apps.engine.models.SafeCharField(max_length=64)),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.AttributeSpec')),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='LabeledShapeAttributeVal',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('value', cvat.apps.engine.models.SafeCharField(max_length=64)),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.AttributeSpec')),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='LabeledTrackAttributeVal',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('value', cvat.apps.engine.models.SafeCharField(max_length=64)),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.AttributeSpec')),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='TrackedShape',
fields=[
('type', models.CharField(choices=[('rectangle', 'RECTANGLE'), ('polygon', 'POLYGON'), ('polyline', 'POLYLINE'), ('points', 'POINTS')], max_length=16)),
('occluded', models.BooleanField(default=False)),
('z_order', models.IntegerField(default=0)),
('points', cvat.apps.engine.models.FloatArrayField()),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('frame', models.PositiveIntegerField()),
('outside', models.BooleanField(default=False)),
],
options={
'default_permissions': (),
},
),
migrations.CreateModel(
name='TrackedShapeAttributeVal',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('value', cvat.apps.engine.models.SafeCharField(max_length=64)),
('shape', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.TrackedShape')),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.AttributeSpec')),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='LabeledImage',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('frame', models.PositiveIntegerField()),
('group', models.PositiveIntegerField(null=True)),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='LabeledShape',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('frame', models.PositiveIntegerField()),
('group', models.PositiveIntegerField(null=True)),
('type', models.CharField(choices=[('rectangle', 'RECTANGLE'), ('polygon', 'POLYGON'), ('polyline', 'POLYLINE'), ('points', 'POINTS')], max_length=16)),
('occluded', models.BooleanField(default=False)),
('z_order', models.IntegerField(default=0)),
('points', cvat.apps.engine.models.FloatArrayField()),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.CreateModel(
name='LabeledTrack',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('frame', models.PositiveIntegerField()),
('group', models.PositiveIntegerField(null=True)),
],
options={
'abstract': False,
'default_permissions': (),
},
),
migrations.AddField(
model_name='labeledimage',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Job'),
),
migrations.AddField(
model_name='labeledtrack',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Job'),
),
migrations.AddField(
model_name='labeledshape',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Job'),
),
migrations.AddField(
model_name='labeledimage',
name='label',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Label'),
),
migrations.AddField(
model_name='labeledshape',
name='label',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Label'),
),
migrations.AddField(
model_name='labeledtrack',
name='label',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.Label'),
),
migrations.AddField(
model_name='trackedshape',
name='track',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.LabeledTrack'),
),
migrations.AddField(
model_name='labeledtrackattributeval',
name='track',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.LabeledTrack'),
),
migrations.AddField(
model_name='labeledshapeattributeval',
name='shape',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.LabeledShape'),
),
migrations.AddField(
model_name='labeledimageattributeval',
name='image',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='engine.LabeledImage'),
),
migrations.RunPython(
code=copy_annotations_forward,
reverse_code=copy_annotations_backward,
),
migrations.RemoveField(
model_name='labeledbox',
name='job',
),
migrations.RemoveField(
model_name='labeledbox',
name='label',
),
migrations.RemoveField(
model_name='labeledboxattributeval',
name='box',
),
migrations.RemoveField(
model_name='labeledboxattributeval',
name='spec',
),
migrations.RemoveField(
model_name='labeledpoints',
name='job',
),
migrations.RemoveField(
model_name='labeledpoints',
name='label',
),
migrations.RemoveField(
model_name='labeledpointsattributeval',
name='points',
),
migrations.RemoveField(
model_name='labeledpointsattributeval',
name='spec',
),
migrations.RemoveField(
model_name='labeledpolygon',
name='job',
),
migrations.RemoveField(
model_name='job',
name='max_shape_id',
),
migrations.RemoveField(
model_name='labeledpolygon',
name='label',
),
migrations.RemoveField(
model_name='labeledpolygonattributeval',
name='polygon',
),
migrations.RemoveField(
model_name='labeledpolygonattributeval',
name='spec',
),
migrations.RemoveField(
model_name='labeledpolyline',
name='job',
),
migrations.RemoveField(
model_name='labeledpolyline',
name='label',
),
migrations.RemoveField(
model_name='labeledpolylineattributeval',
name='polyline',
),
migrations.RemoveField(
model_name='labeledpolylineattributeval',
name='spec',
),
migrations.RemoveField(
model_name='objectpath',
name='job',
),
migrations.RemoveField(
model_name='objectpath',
name='label',
),
migrations.RemoveField(
model_name='objectpathattributeval',
name='spec',
),
migrations.RemoveField(
model_name='objectpathattributeval',
name='track',
),
migrations.RemoveField(
model_name='trackedbox',
name='track',
),
migrations.RemoveField(
model_name='trackedboxattributeval',
name='box',
),
migrations.RemoveField(
model_name='trackedboxattributeval',
name='spec',
),
migrations.RemoveField(
model_name='trackedpoints',
name='track',
),
migrations.RemoveField(
model_name='trackedpointsattributeval',
name='points',
),
migrations.RemoveField(
model_name='trackedpointsattributeval',
name='spec',
),
migrations.RemoveField(
model_name='trackedpolygon',
name='track',
),
migrations.RemoveField(
model_name='trackedpolygonattributeval',
name='polygon',
),
migrations.RemoveField(
model_name='trackedpolygonattributeval',
name='spec',
),
migrations.RemoveField(
model_name='trackedpolyline',
name='track',
),
migrations.RemoveField(
model_name='trackedpolylineattributeval',
name='polyline',
),
migrations.RemoveField(
model_name='trackedpolylineattributeval',
name='spec',
),
migrations.DeleteModel(
name='LabeledBox',
),
migrations.DeleteModel(
name='LabeledBoxAttributeVal',
),
migrations.DeleteModel(
name='LabeledPoints',
),
migrations.DeleteModel(
name='LabeledPointsAttributeVal',
),
migrations.DeleteModel(
name='LabeledPolygon',
),
migrations.DeleteModel(
name='LabeledPolygonAttributeVal',
),
migrations.DeleteModel(
name='LabeledPolyline',
),
migrations.DeleteModel(
name='LabeledPolylineAttributeVal',
),
migrations.DeleteModel(
name='ObjectPath',
),
migrations.DeleteModel(
name='ObjectPathAttributeVal',
),
migrations.DeleteModel(
name='TrackedBox',
),
migrations.DeleteModel(
name='TrackedBoxAttributeVal',
),
migrations.DeleteModel(
name='TrackedPoints',
),
migrations.DeleteModel(
name='TrackedPointsAttributeVal',
),
migrations.DeleteModel(
name='TrackedPolygon',
),
migrations.DeleteModel(
name='TrackedPolygonAttributeVal',
),
migrations.DeleteModel(
name='TrackedPolyline',
),
migrations.DeleteModel(
name='TrackedPolylineAttributeVal',
),
]

@ -0,0 +1,31 @@
# Generated by Django 2.1.7 on 2019-04-17 09:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('engine', '0017_db_redesign_20190221'),
]
operations = [
migrations.CreateModel(
name='JobCommit',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('version', models.PositiveIntegerField(default=0)),
('timestamp', models.DateTimeField(auto_now=True)),
('message', models.CharField(default='', max_length=4096)),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='commits', to='engine.Job')),
],
options={
'abstract': False,
'default_permissions': (),
},
),
]

@ -2,42 +2,39 @@
#
# SPDX-License-Identifier: MIT
from enum import Enum
import shlex
import os
from django.db import models
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
from io import StringIO
from enum import Enum
import shlex
import csv
import re
import os
class SafeCharField(models.CharField):
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value:
return value[:self.max_length]
return value
class StatusChoice(Enum):
class StatusChoice(str, Enum):
ANNOTATION = 'annotation'
VALIDATION = 'validation'
COMPLETED = 'completed'
@classmethod
def choices(self):
return tuple((x.name, x.value) for x in self)
return tuple((x.value, x.name) for x in self)
def __str__(self):
return self.value
class SafeCharField(models.CharField):
def get_prep_value(self, value):
value = super().get_prep_value(value)
if value:
return value[:self.max_length]
return value
class Task(models.Model):
name = SafeCharField(max_length=256)
size = models.PositiveIntegerField()
path = models.CharField(max_length=256)
mode = models.CharField(max_length=32)
owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="owners")
@ -46,45 +43,110 @@ class Task(models.Model):
bug_tracker = models.CharField(max_length=2000, blank=True, default="")
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now_add=True)
overlap = models.PositiveIntegerField(default=0)
overlap = models.PositiveIntegerField(null=True)
# Zero means that there are no limits (default)
segment_size = models.PositiveIntegerField(default=0)
z_order = models.BooleanField(default=False)
flipped = models.BooleanField(default=False)
source = SafeCharField(max_length=256, default="unknown")
status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION)
image_quality = models.PositiveSmallIntegerField(default=50)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
# Extend default permission model
class Meta:
default_permissions = ()
def get_frame_path(self, frame):
d1 = str(int(frame) // 10000)
d2 = str(int(frame) // 100)
path = os.path.join(self.get_data_dirname(), d1, d2,
str(frame) + '.jpg')
return path
def get_upload_dirname(self):
return os.path.join(self.path, ".upload")
return os.path.join(self.get_task_dirname(), ".upload")
def get_data_dirname(self):
return os.path.join(self.path, "data")
def get_dump_path(self):
name = re.sub(r'[\\/*?:"<>|]', '_', self.name)
return os.path.join(self.path, "{}.xml".format(name))
return os.path.join(self.get_task_dirname(), "data")
def get_log_path(self):
return os.path.join(self.path, "task.log")
return os.path.join(self.get_task_dirname(), "task.log")
def get_client_log_path(self):
return os.path.join(self.path, "client.log")
return os.path.join(self.get_task_dirname(), "client.log")
def get_image_meta_cache_path(self):
return os.path.join(self.path, "image_meta.cache")
def set_task_dirname(self, path):
self.path = path
self.save(update_fields=['path'])
return os.path.join(self.get_task_dirname(), "image_meta.cache")
def get_task_dirname(self):
return self.path
return os.path.join(settings.DATA_ROOT, str(self.id))
def __str__(self):
return self.name
# Redefined a couple of operation for FileSystemStorage to avoid renaming
# or other side effects.
class MyFileSystemStorage(FileSystemStorage):
def get_valid_name(self, name):
return name
def get_available_name(self, name, max_length=None):
if self.exists(name) or (max_length and len(name) > max_length):
raise IOError('`{}` file already exists or its name is too long'.format(name))
return name
def upload_path_handler(instance, filename):
return os.path.join(instance.task.get_upload_dirname(), filename)
# For client files which the user is uploaded
class ClientFile(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
file = models.FileField(upload_to=upload_path_handler,
max_length=1024, storage=MyFileSystemStorage())
class Meta:
default_permissions = ()
unique_together = ("task", "file")
# For server files on the mounted share
class ServerFile(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
file = models.CharField(max_length=1024)
class Meta:
default_permissions = ()
# For URLs
class RemoteFile(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
file = models.CharField(max_length=1024)
class Meta:
default_permissions = ()
class Video(models.Model):
task = models.OneToOneField(Task, on_delete=models.CASCADE)
path = models.CharField(max_length=1024)
start_frame = models.PositiveIntegerField()
stop_frame = models.PositiveIntegerField()
step = models.PositiveIntegerField(default=1)
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
class Meta:
default_permissions = ()
class Image(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
path = models.CharField(max_length=1024)
frame = models.PositiveIntegerField()
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
class Meta:
default_permissions = ()
class Segment(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
start_frame = models.IntegerField()
@ -96,8 +158,8 @@ class Segment(models.Model):
class Job(models.Model):
segment = models.ForeignKey(Segment, on_delete=models.CASCADE)
assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION)
max_shape_id = models.BigIntegerField(default=-1)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
class Meta:
default_permissions = ()
@ -111,53 +173,37 @@ class Label(models.Model):
class Meta:
default_permissions = ()
unique_together = ('task', 'name')
class AttributeType(str, Enum):
CHECKBOX = 'checkbox'
RADIO = 'radio'
NUMBER = 'number'
TEXT = 'text'
SELECT = 'select'
def parse_attribute(text):
match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', text)
prefix = match.group(1)
type = match.group(2)
name = match.group(3)
if match.group(4):
values = list(csv.reader(StringIO(match.group(4)), quotechar="'"))[0]
else:
values = []
@classmethod
def choices(self):
return tuple((x.value, x.name) for x in self)
return {'prefix':prefix, 'type':type, 'name':name, 'values':values}
def __str__(self):
return self.value
class AttributeSpec(models.Model):
label = models.ForeignKey(Label, on_delete=models.CASCADE)
text = models.CharField(max_length=1024)
name = models.CharField(max_length=64)
mutable = models.BooleanField()
input_type = models.CharField(max_length=16,
choices=AttributeType.choices())
default_value = models.CharField(max_length=128)
values = models.CharField(max_length=4096)
class Meta:
default_permissions = ()
def get_attribute(self):
return parse_attribute(self.text)
def is_mutable(self):
attr = self.get_attribute()
return attr['prefix'] == '~'
def get_type(self):
attr = self.get_attribute()
return attr['type']
def get_name(self):
attr = self.get_attribute()
return attr['name']
def get_default_value(self):
attr = self.get_attribute()
return attr['values'][0]
def get_values(self):
attr = self.get_attribute()
return attr['values']
unique_together = ('label', 'name')
def __str__(self):
return self.get_attribute()['name']
return self.name
class AttributeVal(models.Model):
# TODO: add a validator here to be sure that it corresponds to self.label
@ -169,103 +215,112 @@ class AttributeVal(models.Model):
abstract = True
default_permissions = ()
class ShapeType(str, Enum):
RECTANGLE = 'rectangle' # (x0, y0, x1, y1)
POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn)
@classmethod
def choices(self):
return tuple((x.value, x.name) for x in self)
def __str__(self):
return self.value
class Annotation(models.Model):
job = models.ForeignKey(Job, on_delete=models.CASCADE)
id = models.BigAutoField(primary_key=True)
job = models.ForeignKey(Job, on_delete=models.CASCADE)
label = models.ForeignKey(Label, on_delete=models.CASCADE)
frame = models.PositiveIntegerField()
group_id = models.PositiveIntegerField(default=0)
client_id = models.BigIntegerField(default=-1)
class Meta:
abstract = True
class Shape(models.Model):
occluded = models.BooleanField(default=False)
z_order = models.IntegerField(default=0)
group = models.PositiveIntegerField(null=True)
class Meta:
abstract = True
default_permissions = ()
class BoundingBox(Shape):
class Commit(models.Model):
id = models.BigAutoField(primary_key=True)
xtl = models.FloatField()
ytl = models.FloatField()
xbr = models.FloatField()
ybr = models.FloatField()
author = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
version = models.PositiveIntegerField(default=0)
timestamp = models.DateTimeField(auto_now=True)
message = models.CharField(max_length=4096, default="")
class Meta:
abstract = True
default_permissions = ()
class PolyShape(Shape):
id = models.BigAutoField(primary_key=True)
points = models.TextField()
class JobCommit(Commit):
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="commits")
class FloatArrayField(models.TextField):
separator = ","
def from_db_value(self, value, expression, connection):
if value is None:
return value
return [float(v) for v in value.split(self.separator)]
def to_python(self, value):
if isinstance(value, list):
return value
return self.from_db_value(value, None, None)
def get_prep_value(self, value):
return self.separator.join(map(str, value))
class Shape(models.Model):
type = models.CharField(max_length=16, choices=ShapeType.choices())
occluded = models.BooleanField(default=False)
z_order = models.IntegerField(default=0)
points = FloatArrayField()
class Meta:
abstract = True
default_permissions = ()
class LabeledBox(Annotation, BoundingBox):
class LabeledImage(Annotation):
pass
class LabeledBoxAttributeVal(AttributeVal):
box = models.ForeignKey(LabeledBox, on_delete=models.CASCADE)
class LabeledImageAttributeVal(AttributeVal):
image = models.ForeignKey(LabeledImage, on_delete=models.CASCADE)
class LabeledPolygon(Annotation, PolyShape):
class LabeledShape(Annotation, Shape):
pass
class LabeledPolygonAttributeVal(AttributeVal):
polygon = models.ForeignKey(LabeledPolygon, on_delete=models.CASCADE)
class LabeledShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(LabeledShape, on_delete=models.CASCADE)
class LabeledPolyline(Annotation, PolyShape):
class LabeledTrack(Annotation):
pass
class LabeledPolylineAttributeVal(AttributeVal):
polyline = models.ForeignKey(LabeledPolyline, on_delete=models.CASCADE)
class LabeledPoints(Annotation, PolyShape):
pass
class LabeledTrackAttributeVal(AttributeVal):
track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
class LabeledPointsAttributeVal(AttributeVal):
points = models.ForeignKey(LabeledPoints, on_delete=models.CASCADE)
class ObjectPath(Annotation):
class TrackedShape(Shape):
id = models.BigAutoField(primary_key=True)
shapes = models.CharField(max_length=10, default='boxes')
class ObjectPathAttributeVal(AttributeVal):
track = models.ForeignKey(ObjectPath, on_delete=models.CASCADE)
class TrackedObject(models.Model):
track = models.ForeignKey(ObjectPath, on_delete=models.CASCADE)
track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
frame = models.PositiveIntegerField()
outside = models.BooleanField(default=False)
class Meta:
abstract = True
default_permissions = ()
class TrackedBox(TrackedObject, BoundingBox):
pass
class TrackedShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(TrackedShape, on_delete=models.CASCADE)
class TrackedBoxAttributeVal(AttributeVal):
box = models.ForeignKey(TrackedBox, on_delete=models.CASCADE)
class TrackedPolygon(TrackedObject, PolyShape):
pass
class TrackedPolygonAttributeVal(AttributeVal):
polygon = models.ForeignKey(TrackedPolygon, on_delete=models.CASCADE)
class Plugin(models.Model):
name = models.SlugField(max_length=32, primary_key=True)
description = SafeCharField(max_length=8192)
maintainer = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="maintainers")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now_add=True)
class TrackedPolyline(TrackedObject, PolyShape):
pass
class TrackedPolylineAttributeVal(AttributeVal):
polyline = models.ForeignKey(TrackedPolyline, on_delete=models.CASCADE)
class TrackedPoints(TrackedObject, PolyShape):
pass
# Extend default permission model
class Meta:
default_permissions = ()
class TrackedPointsAttributeVal(AttributeVal):
points = models.ForeignKey(TrackedPoints, on_delete=models.CASCADE)
class PluginOption(models.Model):
plugin = models.ForeignKey(Plugin, on_delete=models.CASCADE)
name = SafeCharField(max_length=32)
value = SafeCharField(max_length=1024)

@ -0,0 +1,366 @@
# Copyright (C) 2019 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os
import shutil
from rest_framework import serializers
from django.contrib.auth.models import User, Group
from cvat.apps.engine import models
from cvat.apps.engine.log import slogger
class AttributeSerializer(serializers.ModelSerializer):
class Meta:
model = models.AttributeSpec
fields = ('id', 'name', 'mutable', 'input_type', 'default_value',
'values')
# pylint: disable=no-self-use
def to_internal_value(self, data):
attribute = data.copy()
attribute['values'] = '\n'.join(data.get('values', []))
return attribute
def to_representation(self, instance):
attribute = super().to_representation(instance)
attribute['values'] = attribute['values'].split('\n')
return attribute
class LabelSerializer(serializers.ModelSerializer):
attributes = AttributeSerializer(many=True, source='attributespec_set',
default=[])
class Meta:
model = models.Label
fields = ('id', 'name', 'attributes')
class JobCommitSerializer(serializers.ModelSerializer):
class Meta:
model = models.JobCommit
fields = ('id', 'version', 'author', 'message', 'timestamp')
class JobSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id")
start_frame = serializers.ReadOnlyField(source="segment.start_frame")
stop_frame = serializers.ReadOnlyField(source="segment.stop_frame")
class Meta:
model = models.Job
fields = ('url', 'id', 'assignee', 'status', 'start_frame',
'stop_frame', 'task_id')
class SimpleJobSerializer(serializers.ModelSerializer):
class Meta:
model = models.Job
fields = ('url', 'id', 'assignee', 'status')
class SegmentSerializer(serializers.ModelSerializer):
jobs = SimpleJobSerializer(many=True, source='job_set')
class Meta:
model = models.Segment
fields = ('start_frame', 'stop_frame', 'jobs')
class ClientFileSerializer(serializers.ModelSerializer):
class Meta:
model = models.ClientFile
fields = ('file', )
# pylint: disable=no-self-use
def to_internal_value(self, data):
return {'file': data}
# pylint: disable=no-self-use
def to_representation(self, instance):
upload_dir = instance.task.get_upload_dirname()
return instance.file.path[len(upload_dir) + 1:]
class ServerFileSerializer(serializers.ModelSerializer):
class Meta:
model = models.ServerFile
fields = ('file', )
# pylint: disable=no-self-use
def to_internal_value(self, data):
return {'file': data}
# pylint: disable=no-self-use
def to_representation(self, instance):
return instance.file
class RemoteFileSerializer(serializers.ModelSerializer):
class Meta:
model = models.RemoteFile
fields = ('file', )
class RqStatusSerializer(serializers.Serializer):
state = serializers.ChoiceField(choices=[
"Queued", "Started", "Finished", "Failed"])
message = serializers.CharField(allow_blank=True, default="")
class TaskDataSerializer(serializers.ModelSerializer):
client_files = ClientFileSerializer(many=True, source='clientfile_set',
default=[])
server_files = ServerFileSerializer(many=True, source='serverfile_set',
default=[])
remote_files = RemoteFileSerializer(many=True, source='remotefile_set',
default=[])
class Meta:
model = models.Task
fields = ('client_files', 'server_files', 'remote_files')
# pylint: disable=no-self-use
def update(self, instance, validated_data):
client_files = validated_data.pop('clientfile_set')
server_files = validated_data.pop('serverfile_set')
remote_files = validated_data.pop('remotefile_set')
for file in client_files:
client_file = models.ClientFile(task=instance, **file)
client_file.save()
for file in server_files:
server_file = models.ServerFile(task=instance, **file)
server_file.save()
for file in remote_files:
remote_file = models.RemoteFile(task=instance, **file)
remote_file.save()
return instance
class WriteOnceMixin:
"""Adds support for write once fields to serializers.
To use it, specify a list of fields as `write_once_fields` on the
serializer's Meta:
```
class Meta:
model = SomeModel
fields = '__all__'
write_once_fields = ('collection', )
```
Now the fields in `write_once_fields` can be set during POST (create),
but cannot be changed afterwards via PUT or PATCH (update).
Inspired by http://stackoverflow.com/a/37487134/627411.
"""
def get_extra_kwargs(self):
extra_kwargs = super().get_extra_kwargs()
# We're only interested in PATCH/PUT.
if 'update' in getattr(self.context.get('view'), 'action', ''):
return self._set_write_once_fields(extra_kwargs)
return extra_kwargs
def _set_write_once_fields(self, extra_kwargs):
"""Set all fields in `Meta.write_once_fields` to read_only."""
write_once_fields = getattr(self.Meta, 'write_once_fields', None)
if not write_once_fields:
return extra_kwargs
if not isinstance(write_once_fields, (list, tuple)):
raise TypeError(
'The `write_once_fields` option must be a list or tuple. '
'Got {}.'.format(type(write_once_fields).__name__)
)
for field_name in write_once_fields:
kwargs = extra_kwargs.get(field_name, {})
kwargs['read_only'] = True
extra_kwargs[field_name] = kwargs
return extra_kwargs
class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True)
segments = SegmentSerializer(many=True, source='segment_set', read_only=True)
image_quality = serializers.IntegerField(min_value=0, max_value=100)
class Meta:
model = models.Task
fields = ('url', 'id', 'name', 'size', 'mode', 'owner', 'assignee',
'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'z_order', 'flipped', 'status', 'labels', 'segments',
'image_quality')
read_only_fields = ('size', 'mode', 'created_date', 'updated_date',
'status')
write_once_fields = ('overlap', 'segment_size', 'image_quality')
ordering = ['-id']
# pylint: disable=no-self-use
def create(self, validated_data):
labels = validated_data.pop('label_set')
db_task = models.Task.objects.create(size=0, **validated_data)
for label in labels:
attributes = label.pop('attributespec_set')
db_label = models.Label.objects.create(task=db_task, **label)
for attr in attributes:
models.AttributeSpec.objects.create(label=db_label, **attr)
task_path = db_task.get_task_dirname()
if os.path.isdir(task_path):
shutil.rmtree(task_path)
upload_dir = db_task.get_upload_dirname()
os.makedirs(upload_dir)
output_dir = db_task.get_data_dirname()
os.makedirs(output_dir)
return db_task
# pylint: disable=no-self-use
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.owner = validated_data.get('owner', instance.owner)
instance.assignee = validated_data.get('assignee', instance.assignee)
instance.bug_tracker = validated_data.get('bug_tracker',
instance.bug_tracker)
instance.z_order = validated_data.get('z_order', instance.z_order)
instance.flipped = validated_data.get('flipped', instance.flipped)
instance.image_quality = validated_data.get('image_quality',
instance.image_quality)
labels = validated_data.get('label_set', [])
for label in labels:
attributes = label.pop('attributespec_set', [])
(db_label, created) = models.Label.objects.get_or_create(task=instance,
name=label['name'])
if created:
slogger.task[instance.id].info("New {} label was created"
.format(db_label.name))
else:
slogger.task[instance.id].info("{} label was updated"
.format(db_label.name))
for attr in attributes:
(db_attr, created) = models.AttributeSpec.objects.get_or_create(
label=db_label, name=attr['name'], defaults=attr)
if created:
slogger.task[instance.id].info("New {} attribute for {} label was created"
.format(db_attr.name, db_label.name))
else:
slogger.task[instance.id].info("{} attribute for {} label was updated"
.format(db_attr.name, db_label.name))
# FIXME: need to update only "safe" fields
db_attr.default_value = attr.get('default_value', db_attr.default_value)
db_attr.mutable = attr.get('mutable', db_attr.mutable)
db_attr.input_type = attr.get('input_type', db_attr.input_type)
db_attr.values = attr.get('values', db_attr.values)
db_attr.save()
return instance
class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email',
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login',
'date_joined', 'groups')
read_only_fields = ('last_login', 'date_joined')
write_only_fields = ('password', )
ordering = ['-id']
class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255)
client = serializers.CharField(max_length=255)
time = serializers.DateTimeField()
job_id = serializers.IntegerField(required=False)
task_id = serializers.IntegerField(required=False)
proj_id = serializers.IntegerField(required=False)
client_id = serializers.IntegerField()
message = serializers.CharField(max_length=4096)
filename = serializers.URLField()
line = serializers.IntegerField()
column = serializers.IntegerField()
stack = serializers.CharField(max_length=8192,
style={'base_template': 'textarea.html'}, allow_blank=True)
class AboutSerializer(serializers.Serializer):
name = serializers.CharField(max_length=128)
description = serializers.CharField(max_length=2048)
version = serializers.CharField(max_length=64)
class ImageMetaSerializer(serializers.Serializer):
width = serializers.IntegerField()
height = serializers.IntegerField()
class AttributeValSerializer(serializers.Serializer):
spec_id = serializers.IntegerField()
value = serializers.CharField(max_length=64, allow_blank=True)
def to_internal_value(self, data):
data['value'] = str(data['value'])
return super().to_internal_value(data)
class AnnotationSerializer(serializers.Serializer):
id = serializers.IntegerField(default=None, allow_null=True)
frame = serializers.IntegerField(min_value=0)
label_id = serializers.IntegerField(min_value=0)
group = serializers.IntegerField(min_value=0, allow_null=True)
class LabeledImageSerializer(AnnotationSerializer):
attributes = AttributeValSerializer(many=True,
source="labeledimageattributeval_set")
class ShapeSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=models.ShapeType.choices())
occluded = serializers.BooleanField()
z_order = serializers.IntegerField(default=0)
points = serializers.ListField(
child=serializers.FloatField(min_value=0)
)
class LabeledShapeSerializer(ShapeSerializer, AnnotationSerializer):
attributes = AttributeValSerializer(many=True,
source="labeledshapeattributeval_set")
class TrackedShapeSerializer(ShapeSerializer):
id = serializers.IntegerField(default=None, allow_null=True)
frame = serializers.IntegerField(min_value=0)
outside = serializers.BooleanField()
attributes = AttributeValSerializer(many=True,
source="trackedshapeattributeval_set")
class LabeledTrackSerializer(AnnotationSerializer):
shapes = TrackedShapeSerializer(many=True, allow_empty=False,
source="trackedshape_set")
attributes = AttributeValSerializer(many=True,
source="labeledtrackattributeval_set")
class LabeledDataSerializer(serializers.Serializer):
version = serializers.IntegerField()
tags = LabeledImageSerializer(many=True)
shapes = LabeledShapeSerializer(many=True)
tracks = LabeledTrackSerializer(many=True)
class FileInfoSerializer(serializers.Serializer):
name = serializers.CharField(max_length=1024)
type = serializers.ChoiceField(choices=["REG", "DIR"])
class PluginSerializer(serializers.ModelSerializer):
class Meta:
model = models.Plugin
fields = ('name', 'description', 'maintainer', 'created_at',
'updated_at')
class LogEventSerializer(serializers.Serializer):
job_id = serializers.IntegerField(required=False)
task_id = serializers.IntegerField(required=False)
proj_id = serializers.IntegerField(required=False)
client_id = serializers.IntegerField()
name = serializers.CharField(max_length=64)
time = serializers.DateTimeField()
message = serializers.CharField(max_length=4096, required=False)
payload = serializers.DictField(required=False)
is_active = serializers.BooleanField()

@ -0,0 +1,19 @@
# Copyright (C) 2019 Intel Corporation
#
# SPDX-License-Identifier: MIT
from .models import Job, StatusChoice
def update_task_status(instance, **kwargs):
db_task = instance.segment.task
db_jobs = list(Job.objects.filter(segment__task_id=db_task.id))
status = StatusChoice.COMPLETED
if list(filter(lambda x: x.status == StatusChoice.ANNOTATION, db_jobs)):
status = StatusChoice.ANNOTATION
elif list(filter(lambda x: x.status == StatusChoice.VALIDATION, db_jobs)):
status = StatusChoice.VALIDATION
if status != db_task.status:
db_task.status = status
db_task.save()

@ -1,882 +0,0 @@
/*
* defiant.js.js [v1.4.5]
* http://www.defiantjs.com
* Copyright (c) 2013-2017, Hakan Bilgin <hbi@longscript.com>
* Licensed under the MIT License
*/
/*
* x10.js v0.1.3
* Web worker wrapper with simple interface
*
* Copyright (c) 2013-2015, Hakan Bilgin <hbi@longscript.com>
* Licensed under the MIT License
*/
(function(window, undefined) {
//'use strict';
var x10 = {
init: function() {
return this;
},
work_handler: function(event) {
var args = Array.prototype.slice.call(event.data, 1),
func = event.data[0],
ret = tree[func].apply(tree, args);
// return process finish
postMessage([func, ret]);
},
setup: function(tree) {
var url = window.URL || window.webkitURL,
script = 'var tree = {'+ this.parse(tree).join(',') +'};',
blob = new Blob([script + 'self.addEventListener("message", '+ this.work_handler.toString() +', false);'],
{type: 'text/javascript'}),
worker = new Worker(url.createObjectURL(blob));
// thread pipe
worker.onmessage = function(event) {
var args = Array.prototype.slice.call(event.data, 1),
func = event.data[0];
x10.observer.emit('x10:'+ func, args);
};
return worker;
},
call_handler: function(func, worker) {
return function() {
var args = Array.prototype.slice.call(arguments, 0, -1),
callback = arguments[arguments.length-1];
// add method name
args.unshift(func);
// listen for 'done'
x10.observer.on('x10:'+ func, function(event) {
callback(event.detail[0]);
});
// start worker
worker.postMessage(args);
};
},
compile: function(hash) {
var worker = this.setup(typeof(hash) === 'function' ? {func: hash} : hash),
obj = {},
fn;
// create return object
if (typeof(hash) === 'function') {
obj.func = this.call_handler('func', worker);
return obj.func;
} else {
for (fn in hash) {
obj[fn] = this.call_handler(fn, worker);
}
return obj;
}
},
parse: function(tree, isArray) {
var hash = [],
key,
val,
v;
for (key in tree) {
v = tree[key];
// handle null
if (v === null) {
hash.push(key +':null');
continue;
}
// handle undefined
if (v === undefined) {
hash.push(key +':undefined');
continue;
}
switch (v.constructor) {
case Date: val = 'new Date('+ v.valueOf() +')'; break;
case Object: val = '{'+ this.parse(v).join(',') +'}'; break;
case Array: val = '['+ this.parse(v, true).join(',') +']'; break;
case String: val = '"'+ v.replace(/"/g, '\\"') +'"'; break;
case RegExp:
case Function: val = v.toString(); break;
default: val = v;
}
if (isArray) hash.push(val);
else hash.push(key +':'+ val);
}
return hash;
},
// simple event emitter
observer: (function() {
var stack = {};
return {
on: function(type, fn) {
if (!stack[type]) {
stack[type] = [];
}
stack[type].unshift(fn);
},
off: function(type, fn) {
if (!stack[type]) return;
var i = stack[type].indexOf(fn);
stack[type].splice(i,1);
},
emit: function(type, detail) {
if (!stack[type]) return;
var event = {
type : type,
detail : detail,
isCanceled : false,
cancelBubble : function() {
this.isCanceled = true;
}
},
len = stack[type].length;
while(len--) {
if (event.isCanceled) return;
stack[type][len](event);
}
}
};
})()
};
if (typeof module === "undefined") {
// publish x10
window.x10 = x10.init();
} else {
module.exports = x10.init();
}
})(this);
(function(window, module, undefined) {
'use strict';
var Defiant = {
is_ie : /(msie|trident)/i.test(navigator.userAgent),
is_safari : /safari/i.test(navigator.userAgent),
env : 'production',
xml_decl : '<?xml version="1.0" encoding="utf-8"?>',
namespace : 'xmlns:d="defiant-namespace"',
tabsize : 4,
render: function(template, data) {
var processor = new XSLTProcessor(),
span = document.createElement('span'),
opt = {match: '/'},
tmpltXpath,
scripts,
temp,
sorter;
// handle arguments
switch (typeof(template)) {
case 'object':
this.extend(opt, template);
if (!opt.data) opt.data = data;
break;
case 'string':
opt.template = template;
opt.data = data;
break;
default:
throw 'error';
}
opt.data = JSON.toXML(opt.data);
tmpltXpath = '//xsl:template[@name="'+ opt.template +'"]';
if (!this.xsl_template) this.gatherTemplates();
if (opt.sorter) {
sorter = this.node.selectSingleNode(this.xsl_template, tmpltXpath +'//xsl:for-each//xsl:sort');
if (sorter) {
if (opt.sorter.order) sorter.setAttribute('order', opt.sorter.order);
if (opt.sorter.select) sorter.setAttribute('select', opt.sorter.select);
sorter.setAttribute('data-type', opt.sorter.type || 'text');
}
}
temp = this.node.selectSingleNode(this.xsl_template, tmpltXpath);
temp.setAttribute('match', opt.match);
processor.importStylesheet(this.xsl_template);
span.appendChild(processor.transformToFragment(opt.data, document));
temp.removeAttribute('match');
if (this.is_safari) {
scripts = span.getElementsByTagName('script');
for (var i=0, il=scripts.length; i<il; i++) scripts[i].defer = true;
}
return span.innerHTML;
},
gatherTemplates: function() {
var scripts = document.getElementsByTagName('script'),
str = '',
i = 0,
il = scripts.length;
for (; i<il; i++) {
if (scripts[i].type === 'defiant/xsl-template') str += scripts[i].innerHTML;
}
this.xsl_template = this.xmlFromString('<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xlink="http://www.w3.org/1999/xlink" '+ this.namespace +'>'+ str.replace(/defiant:(\w+)/g, '$1') +'</xsl:stylesheet>');
},
getSnapshot: function(data, callback) {
return JSON.toXML(data, callback || true);
},
xmlFromString: function(str) {
var parser,
doc;
str = str.replace(/>\s{1,}</g, '><');
if (str.trim().match(/<\?xml/) === null) {
str = this.xml_decl + str;
}
if ( 'ActiveXObject' in window ) {
doc = new ActiveXObject('Msxml2.DOMDocument');
doc.loadXML(str);
doc.setProperty('SelectionNamespaces', this.namespace);
if (str.indexOf('xsl:stylesheet') === -1) {
doc.setProperty('SelectionLanguage', 'XPath');
}
} else {
parser = new DOMParser();
doc = parser.parseFromString(str, 'text/xml');
}
return doc;
},
extend: function(src, dest) {
for (var content in dest) {
if (!src[content] || typeof(dest[content]) !== 'object') {
src[content] = dest[content];
} else {
this.extend(src[content], dest[content]);
}
}
return src;
},
node: {}
};
// Export
window.Defiant = module.exports = Defiant;
})(
typeof window !== 'undefined' ? window : {},
typeof module !== 'undefined' ? module : {}
);
if (typeof(XSLTProcessor) === 'undefined') {
// emulating XSLT Processor (enough to be used in defiant)
var XSLTProcessor = function() {};
XSLTProcessor.prototype = {
importStylesheet: function(xsldoc) {
this.xsldoc = xsldoc;
},
transformToFragment: function(data, doc) {
var str = data.transformNode(this.xsldoc),
span = document.createElement('span');
span.innerHTML = str;
return span;
}
};
} else if (typeof(XSLTProcessor) !== 'function' && !XSLTProcessor) {
// throw error
throw 'XSLTProcessor transformNode not implemented';
}
// extending STRING
if (!String.prototype.fill) {
String.prototype.fill = function(i,c) {
var str = this;
c = c || ' ';
for (; str.length<i; str+=c){}
return str;
};
}
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/gm, '');
};
}
if (!String.prototype.xTransform) {
String.prototype.xTransform = function () {
var str = this;
if (this.indexOf('translate(') === -1) {
str = this.replace(/contains\(([^,]+),([^\\)]+)\)/g, function(c,h,n) {
var a = 'abcdefghijklmnopqrstuvwxyz';
return "contains(translate("+ h +", \""+ a.toUpperCase() +"\", \""+ a +"\"),"+ n.toLowerCase() +")";
});
}
return str.toString();
};
}
/* jshint ignore:start */
if (typeof(JSON) === 'undefined') {
window.JSON = {
parse: function (sJSON) { return eval("(" + sJSON + ")"); },
stringify: function (vContent) {
if (vContent instanceof Object) {
var sOutput = "";
if (vContent.constructor === Array) {
for (var nId = 0; nId < vContent.length; sOutput += this.stringify(vContent[nId]) + ",", nId++);
return "[" + sOutput.substr(0, sOutput.length - 1) + "]";
}
if (vContent.toString !== Object.prototype.toString) {
return "\"" + vContent.toString().replace(/"/g, "\\$&") + "\"";
}
for (var sProp in vContent) {
sOutput += "\"" + sProp.replace(/"/g, "\\$&") + "\":" + this.stringify(vContent[sProp]) + ",";
}
return "{" + sOutput.substr(0, sOutput.length - 1) + "}";
}
return typeof vContent === "string" ? "\"" + vContent.replace(/"/g, "\\$&") + "\"" : String(vContent);
}
};
}
/* jshint ignore:end */
if (!JSON.toXML) {
JSON.toXML = function(tree, callback) {
'use strict';
var interpreter = {
map : [],
rx_validate_name : /^(?!xml)[a-z_][\w\d.:]*$/i,
rx_node : /<(.+?)( .*?)>/,
rx_constructor : /<(.+?)( d:contr=".*?")>/,
rx_namespace : / xmlns\:d="defiant\-namespace"/,
rx_data : /(<.+?>)(.*?)(<\/d:data>)/i,
rx_function : /function (\w+)/i,
namespace : 'xmlns:d="defiant-namespace"',
to_xml_str: function(tree) {
return {
str: this.hash_to_xml(null, tree),
map: this.map
};
},
hash_to_xml: function(name, tree, array_child) {
var is_array = tree.constructor === Array,
self = this,
elem = [],
attr = [],
key,
val,
val_is_array,
type,
is_attr,
cname,
constr,
cnName,
i,
il,
fn = function(key, tree) {
val = tree[key];
if (val === null || val === undefined || val.toString() === 'NaN') val = null;
is_attr = key.slice(0,1) === '@';
cname = array_child ? name : key;
if (cname == +cname && tree.constructor !== Object) cname = 'd:item';
if (val === null) {
constr = null;
cnName = false;
} else {
constr = val.constructor;
cnName = constr.toString().match(self.rx_function)[1];
}
if (is_attr) {
attr.push( cname.slice(1) +'="'+ self.escape_xml(val) +'"' );
if (cnName !== 'String') attr.push( 'd:'+ cname.slice(1) +'="'+ cnName +'"' );
} else if (val === null) {
elem.push( self.scalar_to_xml( cname, val ) );
} else {
switch (constr) {
case Function:
// if constructor is function, then it's not a JSON structure
throw 'JSON data should not contain functions. Please check your structure.';
/* falls through */
case Object:
elem.push( self.hash_to_xml( cname, val ) );
break;
case Array:
if (key === cname) {
val_is_array = val.constructor === Array;
if (val_is_array) {
i = val.length;
while (i--) {
if (val[i] === null || !val[i] || val[i].constructor === Array) val_is_array = true;
if (!val_is_array && val[i].constructor === Object) val_is_array = true;
}
}
elem.push( self.scalar_to_xml( cname, val, val_is_array ) );
break;
}
/* falls through */
case String:
if (typeof(val) === 'string') {
val = val.toString().replace(/\&/g, '&amp;')
.replace(/\r|\n/g, '&#13;');
}
if (cname === '#text') {
// prepare map
self.map.push(tree);
attr.push('d:mi="'+ self.map.length +'"');
attr.push('d:constr="'+ cnName +'"');
elem.push( self.escape_xml(val) );
break;
}
/* falls through */
case Number:
case Boolean:
if (cname === '#text' && cnName !== 'String') {
// prepare map
self.map.push(tree);
attr.push('d:mi="'+ self.map.length +'"');
attr.push('d:constr="'+ cnName +'"');
elem.push( self.escape_xml(val) );
break;
}
elem.push( self.scalar_to_xml( cname, val ) );
break;
}
}
};
if (tree.constructor === Array) {
i = 0;
il = tree.length;
for (; i<il; i++) {
fn(i.toString(), tree);
}
} else {
for (key in tree) {
fn(key, tree);
}
}
if (!name) {
name = 'd:data';
attr.push(this.namespace);
if (is_array) attr.push('d:constr="Array"');
}
if (name.match(this.rx_validate_name) === null) {
attr.push( 'd:name="'+ name +'"' );
name = 'd:name';
}
if (array_child) return elem.join('');
// prepare map
this.map.push(tree);
attr.push('d:mi="'+ this.map.length +'"');
return '<'+ name + (attr.length ? ' '+ attr.join(' ') : '') + (elem.length ? '>'+ elem.join('') +'</'+ name +'>' : '/>' );
},
scalar_to_xml: function(name, val, override) {
var attr = '',
text,
constr,
cnName;
// check whether the nodename is valid
if (name.match(this.rx_validate_name) === null) {
attr += ' d:name="'+ name +'"';
name = 'd:name';
override = false;
}
if (val === null || val.toString() === 'NaN') val = null;
if (val === null) return '<'+ name +' d:constr="null"/>';
if (val.length === 1 && val.constructor === Array && !val[0]) {
return '<'+ name +' d:constr="null" d:type="ArrayItem"/>';
}
if (val.length === 1 && val[0].constructor === Object) {
text = this.hash_to_xml(false, val[0]);
var a1 = text.match(this.rx_node),
a2 = text.match(this.rx_constructor);
a1 = (a1 !== null)? a1[2]
.replace(this.rx_namespace, '')
.replace(/>/, '')
.replace(/"\/$/, '"') : '';
a2 = (a2 !== null)? a2[2] : '';
text = text.match(this.rx_data);
text = (text !== null)? text[2] : '';
return '<'+ name + a1 +' '+ a2 +' d:type="ArrayItem">'+ text +'</'+ name +'>';
} else if (val.length === 0 && val.constructor === Array) {
return '<'+ name +' d:constr="Array"/>';
}
// else
if (override) {
return this.hash_to_xml( name, val, true );
}
constr = val.constructor;
cnName = constr.toString().match(this.rx_function)[1];
text = (constr === Array) ? this.hash_to_xml( 'd:item', val, true )
: this.escape_xml(val);
attr += ' d:constr="'+ cnName +'"';
// prepare map
this.map.push(val);
attr += ' d:mi="'+ this.map.length +'"';
return (name === '#text') ? this.escape_xml(val) : '<'+ name + attr +'>'+ text +'</'+ name +'>';
},
escape_xml: function(text) {
return String(text) .replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/&nbsp;/g, '&#160;');
}
},
processed,
doc,
task;
// depending on request
switch (typeof callback) {
case 'function':
// compile interpreter with 'x10.js'
task = x10.compile(interpreter);
// parse in a dedicated thread
task.to_xml_str(tree, function(processed) {
// snapshot distinctly improves performance
callback({
doc: Defiant.xmlFromString(processed.str),
src: tree,
map: processed.map
});
});
return;
case 'boolean':
processed = interpreter.to_xml_str.call(interpreter, tree);
// return snapshot
return {
doc: Defiant.xmlFromString(processed.str),
src: tree,
map: processed.map
};
default:
processed = interpreter.to_xml_str.call(interpreter, tree);
doc = Defiant.xmlFromString(processed.str);
this.search.map = processed.map;
return doc;
}
};
}
if (!JSON.search) {
JSON.search = function(tree, xpath, single) {
'use strict';
var isSnapshot = tree.doc && tree.doc.nodeType,
doc = isSnapshot ? tree.doc : JSON.toXML(tree),
map = isSnapshot ? tree.map : this.search.map,
src = isSnapshot ? tree.src : tree,
xres = Defiant.node[ single ? 'selectSingleNode' : 'selectNodes' ](doc, xpath.xTransform()),
ret = [],
mapIndex,
i;
if (single) xres = [xres];
i = xres.length;
while (i--) {
switch(xres[i].nodeType) {
case 2:
case 3:
ret.unshift( xres[i].nodeValue );
break;
default:
mapIndex = +xres[i].getAttribute('d:mi');
//if (map[mapIndex-1] !== false) {
ret.unshift( map[mapIndex-1] );
//}
}
}
// if environment = development, add search tracing
if (Defiant.env === 'development') {
this.trace = JSON.mtrace(src, ret, xres);
}
return ret;
};
}
if (!JSON.mtrace) {
JSON.mtrace = function(root, hits, xres) {
'use strict';
var win = window,
stringify = JSON.stringify,
sroot = stringify( root, null, '\t' ).replace(/\t/g, ''),
trace = [],
i = 0,
il = xres.length,
od = il ? xres[i].ownerDocument.documentElement : false,
map = this.search.map,
hstr,
cConstr,
fIndex = 0,
mIndex,
lStart,
lEnd;
for (; i<il; i++) {
switch (xres[i].nodeType) {
case 2:
cConstr = xres[i].ownerElement ? xres[i].ownerElement.getAttribute('d:'+ xres[i].nodeName) : 'String';
hstr = '"@'+ xres[i].nodeName +'": '+ win[ cConstr ]( hits[i] );
mIndex = sroot.indexOf(hstr);
lEnd = 0;
break;
case 3:
cConstr = xres[i].parentNode.getAttribute('d:constr');
hstr = win[ cConstr ]( hits[i] );
hstr = '"'+ xres[i].parentNode.nodeName +'": '+ (hstr === 'Number' ? hstr : '"'+ hstr +'"');
mIndex = sroot.indexOf(hstr);
lEnd = 0;
break;
default:
if (xres[i] === od) continue;
if (xres[i].getAttribute('d:constr') === 'String' || xres[i].getAttribute('d:constr') === 'Number') {
cConstr = xres[i].getAttribute('d:constr');
hstr = win[ cConstr ]( hits[i] );
mIndex = sroot.indexOf(hstr, fIndex);
hstr = '"'+ xres[i].nodeName +'": '+ (cConstr === 'Number' ? hstr : '"'+ hstr +'"');
lEnd = 0;
fIndex = mIndex + 1;
} else {
hstr = stringify( hits[i], null, '\t' ).replace(/\t/g, '');
mIndex = sroot.indexOf(hstr);
lEnd = hstr.match(/\n/g).length;
}
}
lStart = sroot.substring(0,mIndex).match(/\n/g).length+1;
trace.push([lStart, lEnd]);
}
return trace;
};
}
Defiant.node.selectNodes = function(XNode, XPath) {
if (XNode.evaluate) {
var ns = XNode.createNSResolver(XNode.documentElement),
qI = XNode.evaluate(XPath, XNode, ns, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null),
res = [],
i = 0,
il = qI.snapshotLength;
for (; i<il; i++) {
res.push( qI.snapshotItem(i) );
}
return res;
} else {
return XNode.selectNodes(XPath);
}
};
Defiant.node.selectSingleNode = function(XNode, XPath) {
if (XNode.evaluate) {
var xI = this.selectNodes(XNode, XPath);
return (xI.length > 0)? xI[0] : null;
} else {
return XNode.selectSingleNode(XPath);
}
};
Defiant.node.prettyPrint = function(node) {
var root = Defiant,
tabs = root.tabsize,
decl = root.xml_decl.toLowerCase(),
ser,
xstr;
if (root.is_ie) {
xstr = node.xml;
} else {
ser = new XMLSerializer();
xstr = ser.serializeToString(node);
}
if (root.env !== 'development') {
// if environment is not development, remove defiant related info
xstr = xstr.replace(/ \w+\:d=".*?"| d\:\w+=".*?"/g, '');
}
var str = xstr.trim().replace(/(>)\s*(<)(\/*)/g, '$1\n$2$3'),
lines = str.split('\n'),
indent = -1,
i = 0,
il = lines.length,
start,
end;
for (; i<il; i++) {
if (i === 0 && lines[i].toLowerCase() === decl) continue;
start = lines[i].match(/<[A-Za-z_\:]+.*?>/g) !== null;
//start = lines[i].match(/<[^\/]+>/g) !== null;
end = lines[i].match(/<\/[\w\:]+>/g) !== null;
if (lines[i].match(/<.*?\/>/g) !== null) start = end = true;
if (start) indent++;
lines[i] = String().fill(indent, '\t') + lines[i];
if (start && end) indent--;
if (!start && end) indent--;
}
return lines.join('\n').replace(/\t/g, String().fill(tabs, ' '));
};
Defiant.node.toJSON = function(xnode, stringify) {
'use strict';
var interpret = function(leaf) {
var obj = {},
win = window,
attr,
type,
item,
cname,
cConstr,
cval,
text,
i, il, a;
switch (leaf.nodeType) {
case 1:
cConstr = leaf.getAttribute('d:constr');
if (cConstr === 'Array') obj = [];
else if (cConstr === 'String' && leaf.textContent === '') obj = '';
attr = leaf.attributes;
i = 0;
il = attr.length;
for (; i<il; i++) {
a = attr.item(i);
if (a.nodeName.match(/\:d|d\:/g) !== null) continue;
cConstr = leaf.getAttribute('d:'+ a.nodeName);
if (cConstr && cConstr !== 'undefined') {
if (a.nodeValue === 'null') cval = null;
else cval = win[ cConstr ]( (a.nodeValue === 'false') ? '' : a.nodeValue );
} else {
cval = a.nodeValue;
}
obj['@'+ a.nodeName] = cval;
}
break;
case 3:
type = leaf.parentNode.getAttribute('d:type');
cval = (type) ? win[ type ]( leaf.nodeValue === 'false' ? '' : leaf.nodeValue ) : leaf.nodeValue;
obj = cval;
break;
}
if (leaf.hasChildNodes()) {
i = 0;
il = leaf.childNodes.length;
for(; i<il; i++) {
item = leaf.childNodes.item(i);
cname = item.nodeName;
attr = leaf.attributes;
if (cname === 'd:name') {
cname = item.getAttribute('d:name');
}
if (cname === '#text') {
cConstr = leaf.getAttribute('d:constr');
if (cConstr === 'undefined') cConstr = undefined;
text = item.textContent || item.text;
cval = cConstr === 'Boolean' && text === 'false' ? '' : text;
if (!cConstr && !attr.length) obj = cval;
else if (cConstr && il === 1) {
obj = win[cConstr](cval);
} else if (!leaf.hasChildNodes()) {
obj[cname] = (cConstr)? win[cConstr](cval) : cval;
} else {
if (attr.length < 3) obj = (cConstr)? win[cConstr](cval) : cval;
else obj[cname] = (cConstr)? win[cConstr](cval) : cval;
}
} else {
if (item.getAttribute('d:constr') === 'null') {
if (obj[cname] && obj[cname].push) obj[cname].push(null);
else if (item.getAttribute('d:type') === 'ArrayItem') obj[cname] = [obj[cname]];
else obj[cname] = null;
continue;
}
if (obj[cname]) {
if (obj[cname].push) obj[cname].push(interpret(item));
else obj[cname] = [obj[cname], interpret(item)];
continue;
}
cConstr = item.getAttribute('d:constr');
switch (cConstr) {
case 'null':
if (obj.push) obj.push(null);
else obj[cname] = null;
break;
case 'Array':
//console.log( Defiant.node.prettyPrint(item) );
if (item.parentNode.firstChild === item && cConstr === 'Array' && cname !== 'd:item') {
if (cname === 'd:item' || cConstr === 'Array') {
cval = interpret(item);
obj[cname] = cval.length ? [cval] : cval;
} else {
obj[cname] = interpret(item);
}
}
else if (obj.push) obj.push( interpret(item) );
else obj[cname] = interpret(item);
break;
case 'String':
case 'Number':
case 'Boolean':
text = item.textContent || item.text;
cval = cConstr === 'Boolean' && text === 'false' ? '' : text;
if (obj.push) obj.push( win[cConstr](cval) );
else obj[cname] = interpret(item);
break;
default:
if (obj.push) obj.push( interpret( item ) );
else obj[cname] = interpret( item );
}
}
}
}
if (leaf.nodeType === 1 && leaf.getAttribute('d:type') === 'ArrayItem') {
obj = [obj];
}
return obj;
},
node = (xnode.nodeType === 9) ? xnode.documentElement : xnode,
ret = interpret(node),
rn = ret[node.nodeName];
// exclude root, if "this" is root node
if (node === node.ownerDocument.documentElement && rn && rn.constructor === Array) {
ret = rn;
}
if (stringify && stringify.toString() === 'true') stringify = '\t';
return stringify ? JSON.stringify(ret, null, stringify) : ret;
};
// check if jQuery is present
if (typeof(jQuery) !== 'undefined') {
(function ( $ ) {
'use strict';
$.fn.defiant = function(template, xpath) {
this.html( Defiant.render(template, xpath) );
return this;
};
}(jQuery));
}

File diff suppressed because one or more lines are too long

@ -8,105 +8,104 @@
/* global
PolyShapeModel:false
LabelsInfo:false
*/
"use strict";
class AnnotationParser {
constructor(job, labelsInfo, idGenerator) {
constructor(job, labelsInfo) {
this._parser = new DOMParser();
this._startFrame = job.start;
this._stopFrame = job.stop;
this._flipped = job.flipped;
this._im_meta = job.image_meta_data;
this._labelsInfo = labelsInfo;
this._idGen = idGenerator;
}
_xmlParseError(parsedXML) {
return parsedXML.getElementsByTagName("parsererror");
return parsedXML.getElementsByTagName('parsererror');
}
_getBoxPosition(box, frame) {
frame = Math.min(frame - this._startFrame, this._im_meta['original_size'].length - 1);
let im_w = this._im_meta['original_size'][frame].width;
let im_h = this._im_meta['original_size'][frame].height;
frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
const imWidth = this._im_meta[frame].width;
const imHeight = this._im_meta[frame].height;
let xtl = +box.getAttribute('xtl');
let ytl = +box.getAttribute('ytl');
let xbr = +box.getAttribute('xbr');
let ybr = +box.getAttribute('ybr');
if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0 ||
xtl > im_w || ytl > im_h || xbr > im_w || ybr > im_h) {
let message = `Incorrect bb found in annotation file: xtl=${xtl} ytl=${ytl} xbr=${xbr} ybr=${ybr}. `;
message += `Box out of range: ${im_w}x${im_h}`;
if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0
|| xtl > imWidth || ytl > imHeight || xbr > imWidth || ybr > imHeight) {
const message = `Incorrect bb found in annotation file: xtl=${xtl} `
+ `ytl=${ytl} xbr=${xbr} ybr=${ybr}. `
+ `Box out of range: ${imWidth}x${imHeight}`;
throw Error(message);
}
if (this._flipped) {
let _xtl = im_w - xbr;
let _xbr = im_w - xtl;
let _ytl = im_h - ybr;
let _ybr = im_h - ytl;
xtl = _xtl;
ytl = _ytl;
xbr = _xbr;
ybr = _ybr;
[xtl, ytl, xbr, ybr] = [
imWidth - xbr,
imWidth - xtl,
imHeight - ybr,
imHeight - ytl,
];
}
let occluded = +box.getAttribute('occluded');
let z_order = box.getAttribute('z_order') || '0';
return [xtl, ytl, xbr, ybr, occluded, +z_order];
const occluded = box.getAttribute('occluded');
const zOrder = box.getAttribute('z_order') || '0';
return [[xtl, ytl, xbr, ybr], +occluded, +zOrder];
}
_getPolyPosition(shape, frame) {
frame = Math.min(frame - this._startFrame, this._im_meta['original_size'].length - 1);
let im_w = this._im_meta['original_size'][frame].width;
let im_h = this._im_meta['original_size'][frame].height;
frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
const imWidth = this._im_meta[frame].width;
const imHeight = this._im_meta[frame].height;
let points = shape.getAttribute('points').split(';').join(' ');
points = PolyShapeModel.convertStringToNumberArray(points);
for (let point of points) {
if (point.x < 0 || point.y < 0 || point.x > im_w || point.y > im_h) {
let message = `Incorrect point found in annotation file x=${point.x} y=${point.y}. `;
message += `Point out of range ${im_w}x${im_h}`;
for (const point of points) {
if (point.x < 0 || point.y < 0 || point.x > imWidth || point.y > imHeight) {
const message = `Incorrect point found in annotation file x=${point.x} `
+ `y=${point.y}. Point out of range ${imWidth}x${imHeight}`;
throw Error(message);
}
if (this._flipped) {
point.x = im_w - point.x;
point.y = im_h - point.y;
point.x = imWidth - point.x;
point.y = imHeight - point.y;
}
}
points = PolyShapeModel.convertNumberArrayToString(points);
let occluded = +shape.getAttribute('occluded');
let z_order = shape.getAttribute('z_order') || '0';
return [points, occluded, +z_order];
points = points.reduce((acc, el) => {
acc.push(el.x, el.y);
return acc;
}, []);
const occluded = shape.getAttribute('occluded');
const zOrder = shape.getAttribute('z_order') || '0';
return [points, +occluded, +zOrder];
}
_getAttribute(labelId, attrTag) {
let name = attrTag.getAttribute('name');
let attrId = this._labelsInfo.attrIdOf(labelId, name);
const name = attrTag.getAttribute('name');
const attrId = this._labelsInfo.attrIdOf(labelId, name);
if (attrId === null) {
throw Error('An unknown attribute found in the annotation file: ' + name);
throw Error(`An unknown attribute found in the annotation file: ${name}`);
}
let attrInfo = this._labelsInfo.attrInfo(attrId);
let value = this._labelsInfo.strToValues(attrInfo.type, attrTag.textContent)[0];
const attrInfo = this._labelsInfo.attrInfo(attrId);
const value = LabelsInfo.normalize(attrInfo.type, attrTag.textContent);
if (['select', 'radio'].includes(attrInfo.type) && !attrInfo.values.includes(value)) {
throw Error('Incorrect attribute value found for "' + name + '" attribute: ' + value);
}
else if (attrInfo.type === 'number') {
if (isNaN(+value)) {
throw Error('Incorrect attribute value found for "' + name + '" attribute: ' + value + '. Value must be a number.');
}
else {
let min = +attrInfo.values[0];
let max = +attrInfo.values[1];
throw Error(`Incorrect attribute value found for "${name}" + attribute: "${value}"`);
} else if (attrInfo.type === 'number') {
if (Number.isNaN(+value)) {
throw Error(`Incorrect attribute value found for "${name}" attribute: "${value}". Value must be a number.`);
} else {
const min = +attrInfo.values[0];
const max = +attrInfo.values[1];
if (+value < min || +value > max) {
throw Error('Number attribute value out of range for "' + name +'" attribute: ' + value);
throw Error(`Number attribute value out of range for "${name}" attribute: "${value}"`);
}
}
}
@ -115,46 +114,48 @@ class AnnotationParser {
}
_getAttributeList(shape, labelId) {
let attributeDict = {};
let attributes = shape.getElementsByTagName('attribute');
for (let attribute of attributes ) {
let [id, value] = this._getAttribute(labelId, attribute);
const attributeDict = {};
const attributes = shape.getElementsByTagName('attribute');
for (const attribute of attributes) {
const [id, value] = this._getAttribute(labelId, attribute);
attributeDict[id] = value;
}
let attributeList = [];
for (let attrId in attributeDict) {
attributeList.push({
id: attrId,
value: attributeDict[attrId],
});
const attributeList = [];
for (const attrId in attributeDict) {
if (Object.prototype.hasOwnProperty.call(attributeDict, attrId)) {
attributeList.push({
spec_id: attrId,
value: attributeDict[attrId],
});
}
}
return attributeList;
}
_getShapeFromPath(shape_type, tracks) {
let result = [];
for (let track of tracks) {
let label = track.getAttribute('label');
let group_id = track.getAttribute('group_id') || '0';
let labelId = this._labelsInfo.labelIdOf(label);
_getShapeFromPath(shapeType, tracks) {
const result = [];
for (const track of tracks) {
const label = track.getAttribute('label');
const group = track.getAttribute('group_id') || '0';
const labelId = this._labelsInfo.labelIdOf(label);
if (labelId === null) {
throw Error(`An unknown label found in the annotation file: ${label}`);
}
let shapes = Array.from(track.getElementsByTagName(shape_type));
shapes.sort((a,b) => +a.getAttribute('frame') - + b.getAttribute('frame'));
const shapes = Array.from(track.getElementsByTagName(shapeType));
shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
while (shapes.length && +shapes[0].getAttribute('outside')) {
shapes.shift();
}
if (shapes.length === 2) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1 &&
!+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
&& !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
shapes[0].setAttribute('label', label);
shapes[0].setAttribute('group_id', group_id);
shapes[0].setAttribute('group_id', group);
result.push(shapes[0]);
}
}
@ -164,87 +165,93 @@ class AnnotationParser {
}
_parseAnnotationData(xml) {
let data = {
const data = {
boxes: [],
polygons: [],
polylines: [],
points: []
points: [],
};
let tracks = xml.getElementsByTagName('track');
let parsed = {
boxes: this._getShapeFromPath('box', tracks),
polygons: this._getShapeFromPath('polygon', tracks),
polylines: this._getShapeFromPath('polyline', tracks),
const tracks = xml.getElementsByTagName('track');
const parsed = {
box: this._getShapeFromPath('box', tracks),
polygon: this._getShapeFromPath('polygon', tracks),
polyline: this._getShapeFromPath('polyline', tracks),
points: this._getShapeFromPath('points', tracks),
};
const shapeTarget = {
box: 'boxes',
polygon: 'polygons',
polyline: 'polylines',
points: 'points',
};
let images = xml.getElementsByTagName('image');
for (let image of images) {
let frame = image.getAttribute('id');
const images = xml.getElementsByTagName('image');
for (const image of images) {
const frame = image.getAttribute('id');
for (let box of image.getElementsByTagName('box')) {
for (const box of image.getElementsByTagName('box')) {
box.setAttribute('frame', frame);
parsed.boxes.push(box);
parsed.box.push(box);
}
for (let polygon of image.getElementsByTagName('polygon')) {
for (const polygon of image.getElementsByTagName('polygon')) {
polygon.setAttribute('frame', frame);
parsed.polygons.push(polygon);
parsed.polygon.push(polygon);
}
for (let polyline of image.getElementsByTagName('polyline')) {
for (const polyline of image.getElementsByTagName('polyline')) {
polyline.setAttribute('frame', frame);
parsed.polylines.push(polyline);
parsed.polyline.push(polyline);
}
for (let points of image.getElementsByTagName('points')) {
for (const points of image.getElementsByTagName('points')) {
points.setAttribute('frame', frame);
parsed.points.push(points);
}
}
for (let shape_type in parsed) {
for (let shape of parsed[shape_type]) {
let frame = +shape.getAttribute('frame');
if (frame < this._startFrame || frame > this._stopFrame) continue;
for (const shapeType in parsed) {
if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
for (const shape of parsed[shapeType]) {
const frame = +shape.getAttribute('frame');
if (frame < this._startFrame || frame > this._stopFrame) {
continue;
}
let labelId = this._labelsInfo.labelIdOf(shape.getAttribute('label'));
let groupId = shape.getAttribute('group_id') || "0";
if (labelId === null) {
throw Error('An unknown label found in the annotation file: ' + shape.getAttribute('label'));
}
const labelId = this._labelsInfo.labelIdOf(shape.getAttribute('label'));
const group = shape.getAttribute('group_id') || '0';
if (labelId === null) {
throw Error(`An unknown label found in the annotation file: "${shape.getAttribute('label')}"`);
}
let attributeList = this._getAttributeList(shape, labelId);
if (shape_type === 'boxes') {
let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, frame);
data.boxes.push({
label_id: labelId,
group_id: +groupId,
frame: frame,
occluded: occluded,
xtl: xtl,
ytl: ytl,
xbr: xbr,
ybr: ybr,
z_order: z_order,
attributes: attributeList,
id: this._idGen.next(),
});
}
else {
let [points, occluded, z_order] = this._getPolyPosition(shape, frame);
data[shape_type].push({
label_id: labelId,
group_id: +groupId,
frame: frame,
points: points,
occluded: occluded,
z_order: z_order,
attributes: attributeList,
id: this._idGen.next(),
});
const attributeList = this._getAttributeList(shape, labelId);
if (shapeType === 'box') {
const [points, occluded, zOrder] = this._getBoxPosition(shape, frame);
data[shapeTarget[shapeType]].push({
label_id: labelId,
group: +group,
attributes: attributeList,
type: 'rectangle',
z_order: zOrder,
frame,
occluded,
points,
});
} else {
const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
data[shapeTarget[shapeType]].push({
label_id: labelId,
group: +group,
attributes: attributeList,
type: shapeType,
z_order: zOrder,
frame,
points,
occluded,
});
}
}
}
}
@ -253,76 +260,81 @@ class AnnotationParser {
}
_parseInterpolationData(xml) {
let data = {
const data = {
box_paths: [],
polygon_paths: [],
polyline_paths: [],
points_paths: []
points_paths: [],
};
let tracks = xml.getElementsByTagName('track');
for (let track of tracks) {
let labelId = this._labelsInfo.labelIdOf(track.getAttribute('label'));
let groupId = track.getAttribute('group_id') || '0';
const tracks = xml.getElementsByTagName('track');
for (const track of tracks) {
const labelId = this._labelsInfo.labelIdOf(track.getAttribute('label'));
const group = track.getAttribute('group_id') || '0';
if (labelId === null) {
throw Error('An unknown label found in the annotation file: ' + name);
throw Error(`An unknown label found in the annotation file: "${track.getAttribute('label')}"`);
}
let parsed = {
boxes: Array.from(track.getElementsByTagName('box')),
polygons: Array.from(track.getElementsByTagName('polygon')),
polylines: Array.from(track.getElementsByTagName('polyline')),
const parsed = {
box: Array.from(track.getElementsByTagName('box')),
polygon: Array.from(track.getElementsByTagName('polygon')),
polyline: Array.from(track.getElementsByTagName('polyline')),
points: Array.from(track.getElementsByTagName('points')),
};
for (let shape_type in parsed) {
let shapes = parsed[shape_type];
shapes.sort((a,b) => +a.getAttribute('frame') - + b.getAttribute('frame'));
for (const shapeType in parsed) {
if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
const shapes = parsed[shapeType];
shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
while (shapes.length && +shapes[0].getAttribute('outside')) {
shapes.shift();
}
while (shapes.length && +shapes[0].getAttribute('outside')) {
shapes.shift();
}
if (shapes.length === 2) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1 &&
!+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
parsed[shape_type] = []; // pseudo interpolation track (actually is annotation)
if (shapes.length === 2) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
&& !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
// pseudo interpolation track (actually is annotation)
parsed[shapeType] = [];
}
}
}
}
let type = null, target = null;
if (parsed.boxes.length) {
type = 'boxes';
let type = null;
let target = null;
if (parsed.box.length) {
type = 'box';
target = 'box_paths';
}
else if (parsed.polygons.length) {
type = 'polygons';
} else if (parsed.polygon.length) {
type = 'polygon';
target = 'polygon_paths';
}
else if (parsed.polylines.length) {
type = 'polylines';
} else if (parsed.polyline.length) {
type = 'polyline';
target = 'polyline_paths';
}
else if (parsed.points.length) {
} else if (parsed.points.length) {
type = 'points';
target = 'points_paths';
} else {
continue;
}
else continue;
let path = {
const path = {
label_id: labelId,
group_id: +groupId,
group: +group,
frame: +parsed[type][0].getAttribute('frame'),
attributes: [],
shapes: [],
id: this._idGen.next(),
};
for (let shape of parsed[type]) {
let keyFrame = +shape.getAttribute('keyframe');
let outside = +shape.getAttribute('outside');
let frame = +shape.getAttribute('frame');
if (path.frame < this._startFrame || path.frame > this._stopFrame) {
continue;
}
for (const shape of parsed[type]) {
const keyFrame = +shape.getAttribute('keyframe');
const outside = +shape.getAttribute('outside');
const frame = +shape.getAttribute('frame');
/*
All keyframes are significant.
@ -330,53 +342,53 @@ class AnnotationParser {
Ignore all frames less then start.
Ignore all frames more then stop.
*/
let significant = keyFrame || frame === this._startFrame;
const significant = (keyFrame || frame === this._startFrame)
&& frame >= this._startFrame && frame <= this._stopFrame;
if (significant) {
let attributeList = this._getAttributeList(shape, labelId);
let shapeAttributes = [];
let pathAttributes = [];
const attributeList = this._getAttributeList(shape, labelId);
const shapeAttributes = [];
const pathAttributes = [];
for (let attr of attributeList) {
let attrInfo = this._labelsInfo.attrInfo(attr.id);
for (const attr of attributeList) {
const attrInfo = this._labelsInfo.attrInfo(attr.spec_id);
if (attrInfo.mutable) {
shapeAttributes.push({
id: attr.id,
spec_id: attr.spec_id,
value: attr.value,
});
}
else {
} else {
pathAttributes.push({
id: attr.id,
spec_id: attr.spec_id,
value: attr.value,
});
}
}
path.attributes = pathAttributes;
if (type === 'boxes') {
let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame));
if (type === 'box') {
const [points, occluded, zOrder] = this._getBoxPosition(shape,
Math.clamp(frame, this._startFrame, this._stopFrame));
path.shapes.push({
frame: frame,
occluded: occluded,
outside: outside,
xtl: xtl,
ytl: ytl,
xbr: xbr,
ybr: ybr,
z_order: z_order,
attributes: shapeAttributes,
type: 'rectangle',
frame,
occluded,
outside,
points,
zOrder,
});
}
else {
let [points, occluded, z_order] = this._getPolyPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame));
} else {
const [points, occluded, zOrder] = this._getPolyPosition(shape,
Math.clamp(frame, this._startFrame, this._stopFrame));
path.shapes.push({
frame: frame,
occluded: occluded,
outside: outside,
points: points,
z_order: z_order,
attributes: shapeAttributes,
type,
frame,
occluded,
outside,
points,
zOrder,
});
}
}
@ -391,14 +403,33 @@ class AnnotationParser {
}
parse(text) {
let xml = this._parser.parseFromString(text, 'text/xml');
let parseerror = this._xmlParseError(xml);
const xml = this._parser.parseFromString(text, 'text/xml');
const parseerror = this._xmlParseError(xml);
if (parseerror.length) {
throw Error('Annotation page parsing error. ' + parseerror[0].innerText);
throw Error(`Annotation page parsing error. ${parseerror[0].innerText}`);
}
let interpolationData = this._parseInterpolationData(xml);
let annotationData = this._parseAnnotationData(xml);
return Object.assign({}, annotationData, interpolationData);
const interpolationData = this._parseInterpolationData(xml);
const annotationData = this._parseAnnotationData(xml);
const data = {
shapes: [],
tracks: [],
};
for (const type in interpolationData) {
if (Object.prototype.hasOwnProperty.call(interpolationData, type)) {
Array.prototype.push.apply(data.tracks, interpolationData[type]);
}
}
for (const type in annotationData) {
if (Object.prototype.hasOwnProperty.call(annotationData, type)) {
Array.prototype.push.apply(data.shapes, annotationData[type]);
}
}
return data;
}
}

@ -0,0 +1,402 @@
/* exported buildAnnotationSaver */
/* global
showOverlay:false
showMessage:false
Listener:false
Logger:false
Mousetrap:false
*/
class AnnotationSaverModel extends Listener {
constructor(initialData, shapeCollection) {
super('onAnnotationSaverUpdate', () => this._state);
this._state = {
status: null,
message: null,
};
this._version = initialData.version;
this._shapeCollection = shapeCollection;
this._initialObjects = [];
this._hash = this._getHash();
// We need use data from export instead of initialData
// Otherwise we have differ keys order and JSON comparison code incorrect
const data = this._shapeCollection.export()[0];
for (const shape of data.shapes) {
this._initialObjects[shape.id] = shape;
}
for (const track of data.tracks) {
this._initialObjects[track.id] = track;
}
}
async _request(data, action) {
return new Promise((resolve, reject) => {
$.ajax({
url: `/api/v1/jobs/${window.cvat.job.id}/annotations?action=${action}`,
type: 'PATCH',
data: JSON.stringify(data),
contentType: 'application/json',
}).done((savedData) => {
resolve(savedData);
}).fail((errorData) => {
const message = `Could not make ${action} annotations. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
reject(new Error(message));
});
});
}
async _put(data) {
return new Promise((resolve, reject) => {
$.ajax({
url: `/api/v1/jobs/${window.cvat.job.id}/annotations`,
type: 'PUT',
data: JSON.stringify(data),
contentType: 'application/json',
}).done((savedData) => {
resolve(savedData);
}).fail((errorData) => {
const message = `Could not put annotations. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
reject(new Error(message));
});
});
}
async _create(created) {
return this._request(created, 'create');
}
async _update(updated) {
return this._request(updated, 'update');
}
async _delete(deleted) {
return this._request(deleted, 'delete');
}
async _logs() {
Logger.addEvent(Logger.EventType.saveJob);
const totalStat = this._shapeCollection.collectStatistic()[1];
Logger.addEvent(Logger.EventType.sendTaskInfo, {
'track count': totalStat.boxes.annotation + totalStat.boxes.interpolation
+ totalStat.polygons.annotation + totalStat.polygons.interpolation
+ totalStat.polylines.annotation + totalStat.polylines.interpolation
+ totalStat.points.annotation + totalStat.points.interpolation,
'frame count': window.cvat.player.frames.stop - window.cvat.player.frames.start + 1,
'object count': totalStat.total,
'box count': totalStat.boxes.annotation + totalStat.boxes.interpolation,
'polygon count': totalStat.polygons.annotation + totalStat.polygons.interpolation,
'polyline count': totalStat.polylines.annotation + totalStat.polylines.interpolation,
'points count': totalStat.points.annotation + totalStat.points.interpolation,
});
const annotationLogs = Logger.getLogs();
return new Promise((resolve, reject) => {
$.ajax({
url: '/api/v1/server/logs',
type: 'POST',
data: JSON.stringify(annotationLogs.export()),
contentType: 'application/json',
}).done(() => {
resolve();
}).fail((errorData) => {
annotationLogs.save();
const message = `Could not send logs. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
reject(new Error(message));
});
});
}
_split(exported) {
const exportedIDs = Array.from(exported.shapes, shape => +shape.id)
.concat(Array.from(exported.tracks, track => +track.id));
const created = {
version: this._version,
shapes: [],
tracks: [],
tags: [],
};
const updated = {
version: this._version + 1,
shapes: [],
tracks: [],
tags: [],
};
const deleted = {
version: this._version + 2,
shapes: [],
tracks: [],
tags: [],
};
// Compare initial state objects and export state objects
// in order to get updated and created objects
for (const obj of exported.shapes.concat(exported.tracks)) {
if (obj.id in this._initialObjects) {
const exportedHash = JSON.stringify(obj);
const initialSash = JSON.stringify(this._initialObjects[obj.id]);
if (exportedHash !== initialSash) {
const target = 'shapes' in obj ? updated.tracks : updated.shapes;
target.push(obj);
}
} else if (typeof obj.id === 'undefined') {
const target = 'shapes' in obj ? created.tracks : created.shapes;
target.push(obj);
} else {
throw Error(`Bad object ID found: ${obj.id}. `
+ 'It is not contained in initial state and have server ID');
}
}
// Compare initial state indexes and export state indexes
// in order to get removed objects
for (const shapeID in this._initialObjects) {
if (!exportedIDs.includes(+shapeID)) {
const initialShape = this._initialObjects[shapeID];
const target = 'shapes' in initialShape ? deleted.tracks : deleted.shapes;
target.push(initialShape);
}
}
return [created, updated, deleted];
}
_getHash() {
const exported = this._shapeCollection.export()[0];
return JSON.stringify(exported);
}
_updateCreatedObjects(objectsToSave, savedObjects, mapping) {
// Method setups IDs of created objects after saving on a server
const allSavedObjects = savedObjects.shapes.concat(savedObjects.tracks);
const allObjectsToSave = objectsToSave.shapes.concat(objectsToSave.tracks);
if (allSavedObjects.length !== allObjectsToSave.length) {
throw Error('Number of saved objects and objects to save is not match');
}
for (let idx = 0; idx < allSavedObjects.length; idx += 1) {
const objectModel = mapping.filter(el => el[0] === allObjectsToSave[idx])[0][1];
const { id } = allSavedObjects[idx];
objectModel.serverID = id;
allObjectsToSave[idx].id = id;
}
this._shapeCollection.update();
}
notify(status, message = null) {
this._state.status = status;
this._state.message = message;
Listener.prototype.notify.call(this);
}
hasUnsavedChanges() {
return this._getHash() !== this._hash;
}
async save() {
this.notify('saveStart');
try {
const [exported, mapping] = this._shapeCollection.export();
const { flush } = this._shapeCollection;
if (flush) {
const data = Object.assign({}, exported, {
version: this._version,
tags: [],
});
this._version += 1;
this.notify('saveCreated');
const savedObjects = await this._put(data);
this._updateCreatedObjects(exported, savedObjects, mapping);
this._shapeCollection.flush = false;
this._version = savedObjects.version;
for (const object of savedObjects.shapes.concat(savedObjects.tracks)) {
this._initialObjects[object.id] = object;
}
this._version = savedObjects.version;
} else {
const [created, updated, deleted] = this._split(exported);
this.notify('saveCreated');
const savedCreated = await this._create(created);
this._updateCreatedObjects(created, savedCreated, mapping);
this._version = savedCreated.version;
for (const object of created.shapes.concat(created.tracks)) {
this._initialObjects[object.id] = object;
}
this.notify('saveUpdated');
const savedUpdated = await this._update(updated);
this._version = savedUpdated.version;
for (const object of updated.shapes.concat(updated.tracks)) {
if (object.id in this._initialObjects) {
this._initialObjects[object.id] = object;
}
}
this.notify('saveDeleted');
const savedDeleted = await this._delete(deleted);
this._version = savedDeleted.version;
for (const object of savedDeleted.shapes.concat(savedDeleted.tracks)) {
if (object.id in this._initialObjects) {
delete this._initialObjects[object.id];
}
}
this._version = savedDeleted.version;
}
await this._logs();
} catch (error) {
this.notify('saveUnlocked');
this.notify('saveError', error.message);
this._state = {
status: null,
message: null,
};
throw Error(error);
}
this._hash = this._getHash();
this.notify('saveDone');
setTimeout(() => {
this.notify('saveUnlocked');
this._state = {
status: null,
message: null,
};
}, 1000);
}
get state() {
return JSON.parse(JSON.stringify(this._state));
}
}
class AnnotationSaverController {
constructor(model) {
this._model = model;
this._autoSaveInterval = null;
const { shortkeys } = window.cvat.config;
Mousetrap.bind(shortkeys.save_work.value, () => {
this.save();
return false;
}, 'keydown');
}
autoSave(enabled, time) {
if (this._autoSaveInterval) {
clearInterval(this._autoSaveInterval);
this._autoSaveInterval = null;
}
if (enabled) {
this._autoSaveInterval = setInterval(() => {
this.save();
}, time * 1000 * 60);
}
}
hasUnsavedChanges() {
return this._model.hasUnsavedChanges();
}
save() {
if (this._model.state.status === null) {
this._model.save().catch((error) => {
setTimeout(() => {
throw error;
});
});
}
}
}
class AnnotationSaverView {
constructor(model, controller) {
model.subscribe(this);
this._controller = controller;
this._overlay = null;
const { shortkeys } = window.cvat.config;
const saveHelp = `${shortkeys.save_work.view_value} - ${shortkeys.save_work.description}`;
this._saveButton = $('#saveButton').on('click', () => {
this._controller.save();
}).attr('title', saveHelp);
this._autoSaveBox = $('#autoSaveBox').on('change', (e) => {
const enabled = e.target.checked;
const time = +this._autoSaveTime.prop('value');
this._controller.autoSave(enabled, time);
});
this._autoSaveTime = $('#autoSaveTime').on('change', (e) => {
e.target.value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
this._autoSaveBox.trigger('change');
});
window.onbeforeunload = (e) => {
if (this._controller.hasUnsavedChanges()) { // eslint-disable-line react/no-this-in-sfc
const message = 'You have unsaved changes. Leave this page?';
e.returnValue = message;
return message;
}
return null;
};
}
onAnnotationSaverUpdate(state) {
if (state.status === 'saveStart') {
this._overlay = showOverlay('Annotations are being saved..');
this._saveButton.prop('disabled', true).text('Saving..');
} else if (state.status === 'saveDone') {
this._saveButton.text('Successful save');
this._overlay.remove();
} else if (state.status === 'saveError') {
this._saveButton.prop('disabled', false).text('Save Work');
const message = `Couldn't to save the job. Errors occured: ${state.message}. `
+ 'Please report the problem to support team immediately.';
showMessage(message);
this._overlay.remove();
} else if (state.status === 'saveCreated') {
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Created objects are being saved..`);
} else if (state.status === 'saveUpdated') {
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Updated objects are being saved..`);
} else if (state.status === 'saveDeleted') {
this._overlay.setMessage(`${this._overlay.getMessage()} <br /> - Deleted objects are being saved..`);
} else if (state.status === 'saveUnlocked') {
this._saveButton.prop('disabled', false).text('Save Work');
} else {
const message = `Unknown state has been reached during annotation saving: ${state.status} `
+ 'Please report the problem to support team immediately.';
showMessage(message);
}
}
}
function buildAnnotationSaver(initialData, shapeCollection) {
const model = new AnnotationSaverModel(initialData, shapeCollection);
const controller = new AnnotationSaverController(model);
new AnnotationSaverView(model, controller);
}

File diff suppressed because it is too large Load Diff

@ -14,8 +14,6 @@
SVG:false
*/
"use strict";
const AAMUndefinedKeyword = '__undefined__';
class AAMModel extends Listener {
@ -31,40 +29,27 @@ class AAMModel extends Listener {
this._currentShapes = [];
this._attrNumberByLabel = {};
this._helps = {};
for (let labelId in window.cvat.labelsInfo.labels()) {
let labelAttributes = window.cvat.labelsInfo.labelAttributes(labelId);
if (Object.keys(labelAttributes).length) {
this._attrNumberByLabel[labelId] = {
current: 0,
end: Object.keys(labelAttributes).length
};
for (let attrId in labelAttributes) {
this._helps[attrId] = {
title: `${window.cvat.labelsInfo.labels()[labelId]}, ${window.cvat.labelsInfo.attributes()[attrId]}`,
help: getHelp(attrId),
};
}
}
}
function getHelp(attrId) {
let attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
let help = [];
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
const help = [];
switch (attrInfo.type) {
case 'checkbox':
help.push('0 - ' + attrInfo.values[0]);
help.push('1 - ' + !attrInfo.values[0]);
help.push(`0 - ${attrInfo.values[0]}`);
help.push(`1 - ${!attrInfo.values[0]}`);
break;
default:
for (let idx = 0; idx < attrInfo.values.length; idx ++) {
if (idx > 9) break;
if (attrInfo.values[0] === AAMUndefinedKeyword) {
if (!idx) continue;
help.push(idx - 1 + ' - ' + attrInfo.values[idx]);
for (let idx = 0; idx < attrInfo.values.length; idx += 1) {
if (idx > 9) {
break;
}
else {
help.push(idx + ' - ' + attrInfo.values[idx]);
if (attrInfo.values[0] === AAMUndefinedKeyword) {
if (!idx) {
continue;
}
help.push(`${idx - 1} - ${attrInfo.values[idx]}`);
} else {
help.push(`${idx} - ${attrInfo.values[idx]}`);
}
}
}
@ -72,35 +57,56 @@ class AAMModel extends Listener {
return help;
}
const labels = window.cvat.labelsInfo.labels();
for (const labelId in labels) {
if (Object.prototype.hasOwnProperty.call(labels, labelId)) {
const labelAttributes = window.cvat.labelsInfo.labelAttributes(labelId);
if (Object.keys(labelAttributes).length) {
this._attrNumberByLabel[labelId] = {
current: 0,
end: Object.keys(labelAttributes).length,
};
for (const attrId in labelAttributes) {
if (Object.prototype.hasOwnProperty.call(labelAttributes, attrId)) {
this._helps[attrId] = {
title: `${window.cvat.labelsInfo.labels()[labelId]}, ${window.cvat.labelsInfo.attributes()[attrId]}`,
help: getHelp(attrId),
};
}
}
}
}
}
shapeCollection.subscribe(this);
}
_bbRect(pos) {
if ('points' in pos) {
let points = PolyShapeModel.convertStringToNumberArray(pos.points);
const points = PolyShapeModel.convertStringToNumberArray(pos.points);
let xtl = Number.MAX_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
for (let point of points) {
for (const point of points) {
xtl = Math.min(xtl, point.x);
ytl = Math.min(ytl, point.y);
xbr = Math.max(xbr, point.x);
ybr = Math.max(ybr, point.y);
}
return [xtl, ytl, xbr, ybr];
}
else {
return [pos.xtl, pos.ytl, pos.xbr, pos.ybr];
return [xtl, ytl, xbr, ybr];
}
return [pos.xtl, pos.ytl, pos.xbr, pos.ybr];
}
_updateCollection() {
this._currentShapes = [];
for (let shape of this._shapeCollection.currentShapes) {
let labelAttributes = window.cvat.labelsInfo.labelAttributes(shape.model.label);
if (Object.keys(labelAttributes).length && !shape.model.removed && !shape.interpolation.position.outside) {
for (const shape of this._shapeCollection.currentShapes) {
const labelAttributes = window.cvat.labelsInfo.labelAttributes(shape.model.label);
if (Object.keys(labelAttributes).length
&& !shape.model.removed && !shape.interpolation.position.outside) {
this._currentShapes.push({
model: shape.model,
interpolation: shape.model.interpolate(window.cvat.player.frames.current),
@ -111,8 +117,7 @@ class AAMModel extends Listener {
if (this._currentShapes.length) {
this._activeIdx = 0;
this._active = this._currentShapes[0].model;
}
else {
} else {
this._activeIdx = null;
this._active = null;
}
@ -124,15 +129,16 @@ class AAMModel extends Listener {
_activate() {
if (this._activeAAM && this._active) {
let label = this._active.label;
let attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
const { label } = this._active;
const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
let [xtl, ytl, xbr, ybr] = this._bbRect(this._currentShapes[this._activeIdx].interpolation.position);
this._focus(xtl - this._margin, xbr + this._margin, ytl - this._margin, ybr + this._margin);
const [xtl, ytl, xbr, ybr] = this._bbRect(this._currentShapes[this._activeIdx]
.interpolation.position);
this._focus(xtl - this._margin, xbr + this._margin,
ytl - this._margin, ybr + this._margin);
this.notify();
this._active.activeAttribute = attrId;
}
else {
} else {
this.notify();
}
}
@ -170,8 +176,7 @@ class AAMModel extends Listener {
switchAAMMode() {
if (this._activeAAM) {
this._disable();
}
else {
} else {
this._enable();
}
}
@ -184,14 +189,13 @@ class AAMModel extends Listener {
this._deactivate();
if (Math.sign(direction) < 0) {
// next
this._activeIdx ++;
this._activeIdx += 1;
if (this._activeIdx >= this._currentShapes.length) {
this._activeIdx = 0;
}
}
else {
} else {
// prev
this._activeIdx --;
this._activeIdx -= 1;
if (this._activeIdx < 0) {
this._activeIdx = this._currentShapes.length - 1;
}
@ -206,7 +210,7 @@ class AAMModel extends Listener {
return;
}
let curAttr = this._attrNumberByLabel[this._active.label];
const curAttr = this._attrNumberByLabel[this._active.label];
if (curAttr.end < 2) {
return;
@ -214,14 +218,13 @@ class AAMModel extends Listener {
if (Math.sign(direction) > 0) {
// next
curAttr.current ++;
curAttr.current += 1;
if (curAttr.current >= curAttr.end) {
curAttr.current = 0;
}
}
else {
} else {
// prev
curAttr.current --;
curAttr.current -= 1;
if (curAttr.current < 0) {
curAttr.current = curAttr.end - 1;
}
@ -233,10 +236,10 @@ class AAMModel extends Listener {
if (!this._activeAAM || !this._active) {
return;
}
let label = this._active.label;
let frame = window.cvat.player.frames.current;
let attrId = this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
let attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
const { label } = this._active;
const frame = window.cvat.player.frames.current;
const attrId = this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
if (key >= attrInfo.values.length) {
if (attrInfo.type === 'checkbox' && key < 2) {
this._active.updateAttribute(frame, attrId, !attrInfo.values[0]);
@ -247,7 +250,7 @@ class AAMModel extends Listener {
if (key >= attrInfo.values.length - 1) {
return;
}
key ++;
key += 1;
}
this._active.updateAttribute(frame, attrId, attrInfo.values[key]);
}
@ -262,13 +265,11 @@ class AAMModel extends Listener {
generateHelps() {
if (this._active) {
let label = this._active.label;
let attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
const { label } = this._active;
const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
return [this._helps[attrId].title, this._helps[attrId].help, `${this._activeIdx + 1}/${this._currentShapes.length}`];
}
else {
return ['No Shapes Found', '', '0/0'];
}
return ['No Shapes Found', '', '0/0'];
}
get activeAAM() {
@ -285,60 +286,57 @@ class AAMModel extends Listener {
}
class AAMController {
constructor(aamModel) {
this._model = aamModel;
setupAAMShortkeys.call(this);
function setupAAMShortkeys() {
let switchAAMHandler = Logger.shortkeyLogDecorator(function() {
const switchAAMHandler = Logger.shortkeyLogDecorator(() => {
this._model.switchAAMMode();
}.bind(this));
});
let nextAttributeHandler = Logger.shortkeyLogDecorator(function(e) {
const nextAttributeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveAttr(1);
e.preventDefault();
}.bind(this));
});
let prevAttributeHandler = Logger.shortkeyLogDecorator(function(e) {
const prevAttributeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveAttr(-1);
e.preventDefault();
}.bind(this));
});
let nextShapeHandler = Logger.shortkeyLogDecorator(function(e) {
const nextShapeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveShape(1);
e.preventDefault();
}.bind(this));
});
let prevShapeHandler = Logger.shortkeyLogDecorator(function(e) {
const prevShapeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveShape(-1);
e.preventDefault();
}.bind(this));
});
let selectAttributeHandler = Logger.shortkeyLogDecorator(function(e) {
const selectAttributeHandler = Logger.shortkeyLogDecorator((e) => {
let key = e.keyCode;
if (key >= 48 && key <= 57) {
key -= 48; // 0 and 9
}
else if (key >= 96 && key <= 105) {
key -= 48; // 0 and 9
} else if (key >= 96 && key <= 105) {
key -= 96; // num 0 and 9
}
else {
} else {
return;
}
this._model.setupAttributeValue(key);
}.bind(this));
let shortkeys = window.cvat.config.shortkeys;
Mousetrap.bind(shortkeys["switch_aam_mode"].value, switchAAMHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_next_attribute"].value, nextAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_prev_attribute"].value, prevAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_next_shape"].value, nextShapeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_prev_shape"].value, prevShapeHandler, 'keydown');
Mousetrap.bind(shortkeys["select_i_attribute"].value, selectAttributeHandler, 'keydown');
});
const { shortkeys } = window.cvat.config;
Mousetrap.bind(shortkeys.switch_aam_mode.value, switchAAMHandler, 'keydown');
Mousetrap.bind(shortkeys.aam_next_attribute.value, nextAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys.aam_prev_attribute.value, prevAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys.aam_next_shape.value, nextShapeHandler, 'keydown');
Mousetrap.bind(shortkeys.aam_prev_shape.value, prevShapeHandler, 'keydown');
Mousetrap.bind(shortkeys.select_i_attribute.value, selectAttributeHandler, 'keydown');
}
setupAAMShortkeys.call(this);
}
setMargin(value) {
@ -359,15 +357,15 @@ class AAMView {
this._controller = aamController;
this._zoomMargin.on('change', (e) => {
let value = +e.target.value;
const value = +e.target.value;
this._controller.setMargin(value);
}).trigger('change');
aamModel.subscribe(this);
}
_setupAAMView(active, type, pos) {
let oldRect = $('#outsideRect');
let oldMask = $('#outsideMask');
const oldRect = $('#outsideRect');
const oldMask = $('#outsideMask');
if (active) {
if (oldRect.length) {
@ -375,45 +373,47 @@ class AAMView {
oldMask.remove();
}
let size = window.cvat.translate.box.actualToCanvas({
const size = window.cvat.translate.box.actualToCanvas({
x: 0,
y: 0,
width: window.cvat.player.geometry.frameWidth,
height: window.cvat.player.geometry.frameHeight
height: window.cvat.player.geometry.frameHeight,
});
let excludeField = this._frameContent.rect(size.width, size.height).move(size.x, size.y).fill('#666');
const excludeField = this._frameContent.rect(size.width, size.height).move(size.x, size.y).fill('#666');
let includeField = null;
if (type === 'box') {
pos = window.cvat.translate.box.actualToCanvas(pos);
includeField = this._frameContent.rect(pos.xbr - pos.xtl, pos.ybr - pos.ytl).move(pos.xtl, pos.ytl);
}
else {
includeField = this._frameContent.rect(pos.xbr - pos.xtl,
pos.ybr - pos.ytl).move(pos.xtl, pos.ytl);
} else {
pos.points = window.cvat.translate.points.actualToCanvas(pos.points);
includeField = this._frameContent.polygon(pos.points);
}
this._frameContent.mask().add(excludeField).add(includeField).fill('black').attr('id', 'outsideMask');
this._frameContent.rect(size.width, size.height).move(size.x, size.y).attr({
mask: 'url(#outsideMask)',
id: 'outsideRect'
});
this._frameContent.mask().add(excludeField)
.add(includeField).fill('black')
.attr('id', 'outsideMask');
this._frameContent.rect(size.width, size.height)
.move(size.x, size.y).attr({
mask: 'url(#outsideMask)',
id: 'outsideRect',
});
let content = $(this._frameContent.node);
let texts = content.find('.shapeText');
for (let text of texts) {
const content = $(this._frameContent.node);
const texts = content.find('.shapeText');
for (const text of texts) {
content.append(text);
}
}
else {
} else {
oldRect.remove();
oldMask.remove();
}
}
onAAMUpdate(aam) {
this._setupAAMView(aam.active ? true : false,
this._setupAAMView(Boolean(aam.active),
aam.active ? aam.active.type.split('_')[1] : '',
aam.active ? aam.active.interpolate(window.cvat.player.frames.current).position : 0);
@ -423,20 +423,17 @@ class AAMView {
this._aamMenu.removeClass('hidden');
}
let [title, help, counter] = aam.generateHelps();
const [title, help, counter] = aam.generateHelps();
this._aamHelpContainer.empty();
this._aamCounter.text(counter);
this._aamTitle.text(title);
for (let helpRow of help) {
for (const helpRow of help) {
$(`<label> ${helpRow} <label> <br>`).appendTo(this._aamHelpContainer);
}
}
else {
if (this._trackManagement.hasClass('hidden')) {
this._aamMenu.addClass('hidden');
this._trackManagement.removeClass('hidden');
}
} else if (this._trackManagement.hasClass('hidden')) {
this._aamMenu.addClass('hidden');
this._trackManagement.removeClass('hidden');
}
}
}

@ -6,10 +6,7 @@
/* exported
userConfirm
createExportContainer
dumpAnnotationRequest
ExportType
getExportTargetContainer
showMessage
showOverlay
*/
@ -18,54 +15,84 @@
Cookies:false
*/
"use strict";
Math.clamp = function(x, min, max) {
return Math.min(Math.max(x, min), max);
Math.clamp = (x, min, max) => Math.min(Math.max(x, min), max);
String.customSplit = (string, separator) => {
const regex = /"/gi;
const occurences = [];
let occurence = regex.exec(string);
while (occurence) {
occurences.push(occurence.index);
occurence = regex.exec(string);
}
if (occurences.length % 2) {
occurences.pop();
}
let copy = '';
if (occurences.length) {
let start = 0;
for (let idx = 0; idx < occurences.length; idx += 2) {
copy += string.substr(start, occurences[idx] - start);
copy += string.substr(occurences[idx], occurences[idx + 1] - occurences[idx] + 1)
.replace(new RegExp(separator, 'g'), '\0');
start = occurences[idx + 1] + 1;
}
copy += string.substr(occurences[occurences.length - 1] + 1);
} else {
copy = string;
}
return copy.split(new RegExp(separator, 'g')).map(x => x.replace(/\0/g, separator));
};
function userConfirm(message, onagree, ondisagree) {
let template = $('#confirmTemplate');
let confirmWindow = $(template.html()).css('display', 'block');
const template = $('#confirmTemplate');
const confirmWindow = $(template.html()).css('display', 'block');
let annotationConfirmMessage = confirmWindow.find('.templateMessage');
let agreeConfirm = confirmWindow.find('.templateAgreeButton');
let disagreeConfirm = confirmWindow.find('.templateDisagreeButton');
const annotationConfirmMessage = confirmWindow.find('.templateMessage');
const agreeConfirm = confirmWindow.find('.templateAgreeButton');
const disagreeConfirm = confirmWindow.find('.templateDisagreeButton');
function hideConfirm() {
agreeConfirm.off('click');
disagreeConfirm.off('click');
confirmWindow.remove();
}
annotationConfirmMessage.text(message);
$('body').append(confirmWindow);
agreeConfirm.on('click', function() {
agreeConfirm.on('click', () => {
hideConfirm();
if (onagree) onagree();
if (onagree) {
onagree();
}
});
disagreeConfirm.on('click', function() {
disagreeConfirm.on('click', () => {
hideConfirm();
if (ondisagree) ondisagree();
if (ondisagree) {
ondisagree();
}
});
disagreeConfirm.focus();
confirmWindow.on('keydown', (e) => {
e.stopPropagation();
});
function hideConfirm() {
agreeConfirm.off('click');
disagreeConfirm.off('click');
confirmWindow.remove();
}
}
function showMessage(message) {
let template = $('#messageTemplate');
let messageWindow = $(template.html()).css('display', 'block');
const template = $('#messageTemplate');
const messageWindow = $(template.html()).css('display', 'block');
let messageText = messageWindow.find('.templateMessage');
let okButton = messageWindow.find('.templateOKButton');
const messageText = messageWindow.find('.templateMessage');
const okButton = messageWindow.find('.templateOKButton');
messageText.text(message);
$('body').append(messageWindow);
@ -74,7 +101,7 @@ function showMessage(message) {
e.stopPropagation();
});
okButton.on('click', function() {
okButton.on('click', () => {
okButton.off('click');
messageWindow.remove();
});
@ -85,15 +112,14 @@ function showMessage(message) {
function showOverlay(message) {
let template = $('#overlayTemplate');
let overlayWindow = $(template.html()).css('display', 'block');
let overlayText = overlayWindow.find('.templateMessage');
overlayWindow[0].setMessage = function(message) {
overlayText.text(message);
};
overlayWindow[0].remove = function() {
overlayWindow.remove();
const template = $('#overlayTemplate');
const overlayWindow = $(template.html()).css('display', 'block');
const overlayText = overlayWindow.find('.templateMessage');
overlayWindow[0].getMessage = () => overlayText.html();
overlayWindow[0].remove = () => overlayWindow.remove();
overlayWindow[0].setMessage = (msg) => {
overlayText.html(msg);
};
$('body').append(overlayWindow);
@ -101,151 +127,34 @@ function showOverlay(message) {
return overlayWindow[0];
}
function dumpAnnotationRequest(dumpButton, taskID) {
dumpButton = $(dumpButton);
dumpButton.attr('disabled', true);
$.ajax({
url: '/dump/annotation/task/' + taskID,
success: onDumpRequestSuccess,
error: onDumpRequestError,
});
function onDumpRequestSuccess() {
let requestInterval = 3000;
let requestSended = false;
let checkInterval = setInterval(function() {
if (requestSended) return;
requestSended = true;
$.ajax({
url: '/check/annotation/task/' + taskID,
success: onDumpCheckSuccess,
error: onDumpCheckError,
complete: () => requestSended = false,
});
}, requestInterval);
function onDumpCheckSuccess(data) {
if (data.state === 'created') {
clearInterval(checkInterval);
getDumpedFile();
}
else if (data.state != 'started' ) {
clearInterval(checkInterval);
let message = 'Dump process completed with an error. ' + data.stderr;
dumpButton.attr('disabled', false);
showMessage(message);
throw Error(message);
}
function getDumpedFile() {
$.ajax({
url: '/download/annotation/task/' + taskID,
error: onGetDumpError,
success: () => window.location = '/download/annotation/task/' + taskID,
complete: () => dumpButton.attr('disabled', false)
async function dumpAnnotationRequest(tid, taskName) {
const name = encodeURIComponent(`${tid}_${taskName}`);
return new Promise((resolve, reject) => {
const url = `/api/v1/tasks/${tid}/annotations/${name}`;
async function request() {
$.get(url)
.done((...args) => {
if (args[2].status === 202) {
setTimeout(request, 3000);
} else {
const a = document.createElement('a');
a.href = `${url}?action=download`;
document.body.appendChild(a);
a.click();
a.remove();
resolve();
}
}).fail((errorData) => {
const message = `Can not dump annotations for the task. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
reject(new Error(message));
});
function onGetDumpError(response) {
let message = 'Get the dump request error: ' + response.responseText;
showMessage(message);
throw Error(message);
}
}
}
function onDumpCheckError(response) {
clearInterval(checkInterval);
let message = 'Check the dump request error: ' + response.responseText;
dumpButton.attr('disabled', false);
showMessage(message);
throw Error(message);
}
}
function onDumpRequestError(response) {
let message = "Dump request error: " + response.responseText;
dumpButton.attr('disabled', false);
showMessage(message);
throw Error(message);
}
}
const ExportType = Object.freeze({
'create': 0,
'update': 1,
'delete': 2,
});
function createExportContainer() {
const container = {};
Object.keys(ExportType).forEach( action => {
container[action] = {
"boxes": [],
"box_paths": [],
"points": [],
"points_paths": [],
"polygons": [],
"polygon_paths": [],
"polylines": [],
"polyline_paths": [],
};
setTimeout(request);
});
return container;
}
function getExportTargetContainer(export_type, shape_type, container) {
let shape_container_target = undefined;
let export_action_container = undefined;
switch (export_type) {
case ExportType.create:
export_action_container = container.create;
break;
case ExportType.update:
export_action_container = container.update;
break;
case ExportType.delete:
export_action_container = container.delete;
break;
default:
throw Error('Unexpected export type');
}
switch (shape_type) {
case 'annotation_box':
shape_container_target = export_action_container.boxes;
break;
case 'interpolation_box':
shape_container_target = export_action_container.box_paths;
break;
case 'annotation_points':
shape_container_target = export_action_container.points;
break;
case 'interpolation_points':
shape_container_target = export_action_container.points_paths;
break;
case 'annotation_polygon':
shape_container_target = export_action_container.polygons;
break;
case 'interpolation_polygon':
shape_container_target = export_action_container.polygon_paths;
break;
case 'annotation_polyline':
shape_container_target = export_action_container.polylines;
break;
case 'interpolation_polyline':
shape_container_target = export_action_container.polyline_paths;
break;
default:
throw Error('Undefined shape type');
}
return shape_container_target;
}
/* These HTTP methods do not require CSRF protection */
function csrfSafeMethod(method) {
@ -254,17 +163,17 @@ function csrfSafeMethod(method) {
$.ajaxSetup({
beforeSend: function(xhr, settings) {
beforeSend(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
xhr.setRequestHeader('X-CSRFToken', Cookies.get('csrftoken'));
}
}
},
});
$(document).ready(function(){
$(document).ready(() => {
$('body').css({
width: window.screen.width + 'px',
height: window.screen.height * 0.95 + 'px'
width: `${window.screen.width}px`,
height: `${window.screen.height * 0.95}px`,
});
});

@ -10,27 +10,25 @@
platform:false
*/
"use strict";
String.prototype.normalize = function() {
String.normalize = () => {
let target = this;
target = target.charAt(0).toUpperCase() + target.substr(1);
return target;
};
window.onload = function() {
window.onerror = function(errorMsg, url, lineNumber, colNumber, error) {
Logger.sendException({
message: errorMsg,
filename: url,
line: lineNumber,
column: colNumber ? colNumber : '',
stack: error && error.stack ? error.stack : '',
browser: platform.name + ' ' + platform.version,
os: platform.os.toString(),
}).catch(() => { return; });
window.onload = function boot() {
window.onerror = function exception(errorMsg, url, lineNumber, colNumber, error) {
Logger.sendException(
errorMsg,
url,
lineNumber,
colNumber ? String(colNumber) : '',
error && error.stack ? error.stack : '',
`${platform.name} ${platform.version}`,
platform.os.toString(),
).catch(() => {});
};
let id = window.location.href.match('id=[0-9]+')[0].slice(3);
const id = window.location.href.match('id=[0-9]+')[0].slice(3);
callAnnotationUI(id);
};

@ -11,13 +11,12 @@
// legacy syntax for IE support
var supportedPlatforms = ['Chrome'];
if (supportedPlatforms.indexOf(platform.name) == -1) {
if (supportedPlatforms.indexOf(platform.name) === -1) {
try {
document.documentElement.innerHTML = "<center><h1> Your browser is detected as " + platform.name +
". This tool does not support it. Please use the latest version of Google Chrome.</h1></center>";
window.stop();
}
catch (err) {
} catch (err) {
document.execCommand('Stop');
}
}

@ -11,8 +11,8 @@ class CoordinateTranslator {
constructor() {
this._boxTranslator = {
_playerOffset: 0,
_convert: function(box, sign) {
for (let prop of ["xtl", "ytl", "xbr", "ybr", "x", "y"]) {
_convert(box, sign) {
for (const prop of ['xtl', 'ytl', 'xbr', 'ybr', 'x', 'y']) {
if (prop in box) {
box[prop] += this._playerOffset * sign;
}
@ -20,89 +20,118 @@ class CoordinateTranslator {
return box;
},
actualToCanvas: function(actualBox) {
let canvasBox = {};
for (let key in actualBox) {
actualToCanvas(actualBox) {
const canvasBox = {};
for (const key in actualBox) {
canvasBox[key] = actualBox[key];
}
return this._convert(canvasBox, 1);
},
canvasToActual: function(canvasBox) {
let actualBox = {};
for (let key in canvasBox) {
canvasToActual(canvasBox) {
const actualBox = {};
for (const key in canvasBox) {
actualBox[key] = canvasBox[key];
}
return this._convert(actualBox, -1);
},
canvasToClient: function(sourceCanvas, canvasBox) {
let points = [
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y),
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, canvasBox.y),
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y + canvasBox.height),
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, canvasBox.y + canvasBox.height),
];
canvasToClient(sourceCanvas, canvasBox) {
const points = [
[canvasBox.x, canvasBox.y],
[canvasBox.x + canvasBox.width, canvasBox.y],
[canvasBox.x, canvasBox.y + canvasBox.height],
[canvasBox.x + canvasBox.width, canvasBox.y + canvasBox.height],
].map(el => window.cvat.translate.point.canvasToClient(sourceCanvas, ...el));
let xes = points.map((el) => el.x);
let yes = points.map((el) => el.y);
const xes = points.map(el => el.x);
const yes = points.map(el => el.y);
let xmin = Math.min(...xes);
let xmax = Math.max(...xes);
let ymin = Math.min(...yes);
let ymax = Math.max(...yes);
const xmin = Math.min(...xes);
const xmax = Math.max(...xes);
const ymin = Math.min(...yes);
const ymax = Math.max(...yes);
return {
x: xmin,
y: ymin,
width: xmax - xmin,
height: ymax - ymin
height: ymax - ymin,
};
},
serverToClient(shape) {
return {
xtl: shape.points[0],
ytl: shape.points[1],
xbr: shape.points[2],
ybr: shape.points[3],
};
},
clientToServer(clientObject) {
return {
points: [clientObject.xtl, clientObject.ytl,
clientObject.xbr, clientObject.ybr],
};
},
};
this._pointsTranslator = {
_playerOffset: 0,
_convert: function(points, sign) {
if (typeof(points) === 'string') {
return points.split(' ').map((coord) => coord.split(',')
.map((x) => +x + this._playerOffset * sign).join(',')).join(' ');
}
else if (typeof(points) === 'object') {
let result = [];
for (let point of points) {
result.push({
x: point.x + this._playerOffset * sign,
y: point.y + this._playerOffset * sign,
});
}
return result;
_convert(points, sign) {
if (typeof (points) === 'string') {
return points.split(' ').map(coord => coord.split(',')
.map(x => +x + this._playerOffset * sign).join(',')).join(' ');
}
else {
throw Error('Unknown points type was found');
if (typeof (points) === 'object') {
return points.map(point => ({
x: point.x + this._playerOffset * sign,
y: point.y + this._playerOffset * sign,
}));
}
throw Error('Unknown points type was found');
},
actualToCanvas: function(actualPoints) {
actualToCanvas(actualPoints) {
return this._convert(actualPoints, 1);
},
canvasToActual: function(canvasPoints) {
canvasToActual(canvasPoints) {
return this._convert(canvasPoints, -1);
}
},
},
serverToClient(shape) {
return {
points: shape.points.reduce((acc, el, idx) => {
if (idx % 2) {
acc.slice(-1)[0].push(el);
} else {
acc.push([el]);
}
return acc;
}, []).map(point => point.join(',')).join(' '),
};
},
clientToServer(clientPoints) {
return {
points: clientPoints.points.split(' ').join(',').split(',').map(x => +x),
};
},
};
this._pointTranslator = {
_rotation: 0,
clientToCanvas: function(targetCanvas, clientX, clientY) {
clientToCanvas(targetCanvas, clientX, clientY) {
let pt = targetCanvas.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
pt = pt.matrixTransform(targetCanvas.getScreenCTM().inverse());
return pt;
},
canvasToClient: function(sourceCanvas, canvasX, canvasY) {
canvasToClient(sourceCanvas, canvasX, canvasY) {
let pt = sourceCanvas.createSVGPoint();
pt.x = canvasX;
pt.y = canvasY;
@ -110,18 +139,18 @@ class CoordinateTranslator {
return pt;
},
rotate(x, y, cx, cy) {
cx = (typeof cx === "undefined" ? 0 : cx);
cy = (typeof cy === "undefined" ? 0 : cy);
cx = (typeof cx === 'undefined' ? 0 : cx);
cy = (typeof cy === 'undefined' ? 0 : cy);
let radians = (Math.PI / 180) * window.cvat.player.rotation;
let cos = Math.cos(radians);
let sin = Math.sin(radians);
const radians = (Math.PI / 180) * window.cvat.player.rotation;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
return {
x: (cos * (x - cx)) + (sin * (y - cy)) + cx,
y: (cos * (y - cy)) - (sin * (x - cx)) + cy
}
}
y: (cos * (y - cy)) - (sin * (x - cx)) + cy,
};
},
};
}

@ -14,7 +14,7 @@
"use strict";
class HistoryModel extends Listener {
constructor(playerModel, idGenerator) {
constructor(playerModel) {
super('onHistoryUpdate', () => this );
this._deep = 128;
@ -23,15 +23,10 @@ class HistoryModel extends Listener {
this._redo_stack = [];
this._locked = false;
this._player = playerModel;
this._idGenerator = idGenerator;
window.cvat.addAction = (name, undo, redo, frame) => this.addAction(name, undo, redo, frame);
}
generateId() {
return this._idGenerator.next();
}
undo() {
let frame = window.cvat.player.frames.current;
let undo = this._undo_stack.pop();
@ -47,7 +42,7 @@ class HistoryModel extends Listener {
this._player.shift(undo.frame, true);
}
this._locked = true;
undo.undo(this);
undo.undo();
}
catch(err) {
this.notify();
@ -78,7 +73,7 @@ class HistoryModel extends Listener {
this._player.shift(redo.frame, true);
}
this._locked = true;
redo.redo(this);
redo.redo();
}
catch(err) {
this.notify();

@ -1,36 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* exported
IncrementIdGenerator
ConstIdGenerator
*/
"use strict";
class IncrementIdGenerator {
constructor(startId=0) {
this._startId = startId;
}
next() {
return this._startId++;
}
reset(startId=0) {
this._startId = startId;
}
}
class ConstIdGenerator {
constructor(startId=-1) {
this._startId = startId;
}
next() {
return this._startId;
}
}

@ -6,148 +6,187 @@
/* exported LabelsInfo */
/* global
showMessage:false
*/
class LabelsInfo {
constructor(labels) {
function convertAttribute(attribute) {
return {
mutable: attribute.mutable,
type: attribute.input_type,
name: attribute.name,
values: attribute.input_type === 'checkbox'
? [attribute.values[0].toLowerCase() !== 'false'] : attribute.values,
};
}
"use strict";
this._labels = {};
this._attributes = {};
this._colorIdxs = {};
class LabelsInfo {
constructor(job) {
this._labels = new Object;
this._attributes = new Object;
this._colorIdxs = new Object;
for (let labelKey in job.labels) {
let label = {
name: job.labels[labelKey],
for (const label of labels) {
this._labels[label.id] = {
name: label.name,
attributes: {},
};
for (let attrKey in job.attributes[labelKey]) {
label.attributes[attrKey] = parseAttributeRow.call(this, job.attributes[labelKey][attrKey]);
this._attributes[attrKey] = label.attributes[attrKey];
for (const attr of label.attributes) {
this._attributes[attr.id] = convertAttribute(attr);
this._labels[label.id].attributes[attr.id] = this._attributes[attr.id];
}
this._labels[labelKey] = label;
this._colorIdxs[labelKey] = +labelKey;
this._colorIdxs[label.id] = +label.id;
}
function parseAttributeRow(attrRow) {
let match = attrRow.match(/([~@]{1})(.+)=(.+):(.*)/);
if (match == null) {
let message = 'Can not parse attribute string: ' + attrRow;
showMessage(message);
throw new Error(message);
}
return {
mutable: match[1] === "~",
type: match[2],
name: match[3],
values: this.strToValues(match[2], match[4]),
};
}
return this;
}
labelColorIdx(labelId) {
return this._colorIdxs[labelId];
}
updateLabelColorIdx(labelId) {
if (labelId in this._colorIdxs) {
this._colorIdxs[labelId] += 1;
}
}
normalize() {
let labels = "";
for (let labelId in this._labels) {
labels += " " + this._labels[labelId].name;
for (let attrId in this._labels[labelId].attributes) {
let attr = this._labels[labelId].attributes[attrId];
labels += ' ' + (attr.mutable? "~":"@");
labels += attr.type + '=' + attr.name + ':';
labels += attr.values.map(function(val) {
val = String(val);
return val.search(' ') != -1? "'" + val + "'": val;
}).join(',');
}
}
return labels.trim();
}
labels() {
let tempLabels = new Object();
for (let labelId in this._labels) {
tempLabels[labelId] = this._labels[labelId].name;
const labels = {};
for (const labelId in this._labels) {
if (Object.prototype.hasOwnProperty.call(this._labels, labelId)) {
labels[labelId] = this._labels[labelId].name;
}
}
return tempLabels;
return labels;
}
labelAttributes(labelId) {
let attributes = new Object();
if (labelId in this._labels) {
for (let attrId in this._labels[labelId].attributes) {
attributes[attrId] = this._labels[labelId].attributes[attrId].name;
const attributes = {};
const labelAttributes = this._labels[labelId].attributes;
for (const attrId in labelAttributes) {
if (Object.prototype.hasOwnProperty.call(labelAttributes, attrId)) {
attributes[attrId] = labelAttributes[attrId].name;
}
}
return attributes;
}
return attributes;
throw Error('Unknown label ID');
}
attributes() {
let attributes = new Object();
for (let attrId in this._attributes) {
attributes[attrId] = this._attributes[attrId].name;
const attributes = {};
for (const attrId in this._attributes) {
if (Object.prototype.hasOwnProperty.call(this._attributes, attrId)) {
attributes[attrId] = this._attributes[attrId].name;
}
}
return attributes;
}
attrInfo(attrId) {
let info = new Object();
if (attrId in this._attributes) {
let object = this._attributes[attrId];
info.name = object.name;
info.type = object.type;
info.mutable = object.mutable;
info.values = object.values.slice();
return JSON.parse(JSON.stringify(this._attributes[attrId]));
}
return info;
throw Error('Unknown attribute ID');
}
labelIdOf(name) {
for (let labelId in this._labels) {
for (const labelId in this._labels) {
if (this._labels[labelId].name === name) {
return +labelId;
}
}
return null;
throw Error('Unknown label name');
}
attrIdOf(labelId, name) {
let attributes = this.labelAttributes(labelId);
for (let attrId in attributes) {
const attributes = this.labelAttributes(labelId);
for (const attrId in attributes) {
if (this._attributes[attrId].name === name) {
return +attrId;
}
}
return null;
throw Error('Unknown attribute name');
}
strToValues(type, string) {
switch (type) {
case 'checkbox':
return [string !== '0' && string !== 'false' && string !== false];
case 'text':
return [string];
default:
return string.toString().split(',');
static normalize(type, attrValue) {
const value = String(attrValue);
if (type === 'checkbox') {
return value !== '0' && value.toLowerCase() !== 'false';
}
if (type === 'text') {
return value;
}
if (type === 'number') {
if (Number.isNaN(+value)) {
throw Error(`Can not convert ${value} to number.`);
} else {
return +value;
}
}
return value;
}
static serialize(deserialized) {
let serialized = '';
for (const label of deserialized) {
serialized += ` ${label.name}`;
for (const attr of label.attributes) {
serialized += ` ${attr.mutable ? '~' : '@'}`;
serialized += `${attr.input_type}=${attr.name}:`;
serialized += attr.values.join(',');
}
}
return serialized.trim();
}
static deserialize(serialized) {
const normalized = serialized.replace(/'+/g, '\'').replace(/"+/g, '"').replace(/\s+/g, ' ').trim();
const fragments = String.customSplit(normalized, ' ');
const deserialized = [];
let latest = null;
for (let fragment of fragments) {
fragment = fragment.trim();
if ((fragment.startsWith('~')) || (fragment.startsWith('@'))) {
const regex = /(@|~)(checkbox|select|number|text|radio)=([,?!-_0-9a-zA-Z()\s"]+):([,?!-_0-9a-zA-Z()"\s]+)/g;
const result = regex.exec(fragment);
if (result === null || latest === null) {
throw Error('Bad labels format');
}
const values = String.customSplit(result[4], ',');
latest.attributes.push({
name: result[3].replace(/^"/, '').replace(/"$/, ''),
mutable: result[1] === '~',
input_type: result[2],
default_value: values[0].replace(/^"/, '').replace(/"$/, ''),
values: values.map(val => val.replace(/^"/, '').replace(/"$/, '')),
});
} else {
latest = {
name: fragment.replace(/^"/, '').replace(/"$/, ''),
attributes: [],
};
deserialized.push(latest);
}
}
return deserialized;
}
}

@ -12,34 +12,31 @@
"use strict";
var UserActivityHandler = function()
{
this._TIME_TRESHHOLD = 100000; //ms
this._prevEventTime = Date.now();
this._workingTime = 0;
class UserActivityHandler {
constructor() {
this._TIME_TRESHHOLD = 100000; //ms
this._prevEventTime = Date.now();
this._workingTime = 0;
}
this.updateTimer = function()
{
updateTimer() {
if (document.hasFocus()) {
let now = Date.now();
let diff = now - this._prevEventTime;
this._prevEventTime = now;
this._workingTime += diff < this._TIME_TRESHHOLD ? diff : 0;
}
};
}
this.resetTimer = function()
{
resetTimer() {
this._prevEventTime = Date.now();
this._workingTime = 0;
};
}
this.getWorkingTime = function()
{
getWorkingTime() {
return this._workingTime;
};
};
}
}
class LogCollection extends Array {
constructor(logger, items) {
@ -55,134 +52,116 @@ class LogCollection extends Array {
}
export() {
return Array.from(this, log => log.toString());
return Array.from(this, log => log.serialize());
}
}
var LoggerHandler = function(applicationName, jobId)
{
this._application = applicationName;
this._tabId = Date.now().toString().substr(-6);
this._jobId = jobId;
this._username = null;
this._userActivityHandler = null;
this._logEvents = [];
this._userActivityHandler = new UserActivityHandler();
this._timeThresholds = {};
this.isInitialized = Boolean(this._userActivityHandler);
this.addEvent = function(event)
{
class LoggerHandler {
constructor(jobId) {
this._clientID = Date.now().toString().substr(-6);
this._jobId = jobId;
this._logEvents = [];
this._userActivityHandler = new UserActivityHandler();
this._timeThresholds = {};
}
addEvent(event) {
this._pushEvent(event);
};
}
this.addContinuedEvent = function(event)
{
addContinuedEvent(event) {
this._userActivityHandler.updateTimer();
event.onCloseCallback = this._closeCallback;
return event;
};
this.sendExceptions = function(exceptions)
{
for (let e of exceptions) {
this._extendEvent(e);
}
return new Promise( (resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('POST', '/save/exception/' + this._jobId);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
let onreject = () => {
Array.prototype.push.apply(this._logEvents, exceptions);
reject({
status: xhr.status,
statusText: xhr.statusText,
});
};
}
xhr.onload = () => {
if (xhr.status == 200)
{
resolve(xhr.response);
}
else {
sendExceptions(exception) {
this._extendEvent(exception);
return new Promise((resolve, reject) => {
let retries = 3;
let makeRequest = () => {
let xhr = new XMLHttpRequest();
xhr.open('POST', '/api/v1/server/exception');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader("X-CSRFToken", Cookies.get('csrftoken'));
let onreject = () => {
if (retries--) {
setTimeout(() => makeRequest(), 30000); //30 sec delay
} else {
let payload = exception.serialize();
delete Object.assign(payload, {origin_message: payload.message }).message;
this.addEvent(new Logger.LogEvent(
Logger.EventType.sendException,
payload,
"Can't send exception",
));
reject({
status: xhr.status,
statusText: xhr.statusText,
});
}
};
xhr.onload = () => {
switch (xhr.status) {
case 200:
case 403: // ignore forbidden response
resolve(xhr.response);
break;
default:
onreject();
}
};
xhr.onerror = () => {
onreject();
}
};
xhr.onerror = () => {
onreject();
};
xhr.send(JSON.stringify(exception.serialize()));
};
const data = {'exceptions': Array.from(exceptions, log => log.toString())};
xhr.send(JSON.stringify(data));
makeRequest();
});
};
}
this.getLogs = function()
{
getLogs() {
let logs = new LogCollection(this, this._logEvents);
this._logEvents.length = 0;
return logs;
};
}
this.pushLogs = function(logEvents)
{
pushLogs(logEvents) {
Array.prototype.push.apply(this._logEvents, logEvents);
};
}
this._extendEvent = function(event)
{
event.addValues({
application: this._application,
task: this._jobId,
userid: this._username,
tabid: this._tabId,
focus: document.hasFocus()
});
};
_extendEvent(event) {
event._jobId = this._jobId;
event._clientId = this._clientID;
}
this._pushEvent = function(event)
{
_pushEvent(event) {
this._extendEvent(event);
if (event._type in this._timeThresholds) {
this._timeThresholds[event._type].wait(event);
}
else {
this._logEvents.push(event);
}
this._userActivityHandler.updateTimer();
};
}
this._closeCallback = event => { this._pushEvent(event); };
_closeCallback = event => { this._pushEvent(event); };
this.setUsername = function(username)
{
this._username = username;
};
this.updateTimer = function()
{
updateTimer() {
this._userActivityHandler.updateTimer();
};
}
this.resetTimer = function()
{
resetTimer() {
this._userActivityHandler.resetTimer();
};
}
this.getWorkingTime = function()
{
getWorkingTime() {
return this._userActivityHandler.getWorkingTime();
};
}
this.setTimeThreshold = function(eventType, threshold)
{
setTimeThreshold(eventType, threshold) {
this._timeThresholds[eventType] = {
_threshold: threshold,
_timeoutHandler: null,
@ -191,27 +170,26 @@ var LoggerHandler = function(applicationName, jobId)
_logEvents: this._logEvents,
wait: function(event) {
if (this._event) {
if (this._timeoutHandler) clearTimeout(this._timeoutHandler);
if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
}
}
else {
this._timestamp = event._timestamp;
}
this._event = event;
this._timeoutHandler = setTimeout( () => {
this._timeoutHandler = setTimeout(() => {
if ('duration' in this._event._values) {
this._event._values.duration += this._event._timestamp - this._timestamp;
}
this._event._timestamp = this._timestamp;
this._logEvents.push(this._event);
this._event = null;
}, threshold);
},
};
};
};
}
}
/*
@ -242,8 +220,30 @@ are Logger.EventType.addObject, Logger.EventType.deleteObject and
Logger.EventType.sendTaskInfo. Value of "count" property should be a number.
*/
var Logger = {
class LoggerEvent {
constructor(type, message) {
this._time = new Date().toISOString();
this._clientId = null;
this._jobId = null;
this._type = type;
this._message = message;
}
serialize() {
let serializedObj = {
job_id: this._jobId,
client_id: this._clientId,
name: Logger.eventTypeToString(this._type),
time: this._time,
};
if (this._message) {
Object.assign(serializedObj, { message: this._message,});
}
return serializedObj;
}
}
var Logger = {
/**
* @private
*/
@ -256,26 +256,25 @@ var Logger = {
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'}
* @param {Function} closeCallback callback function which will be called by close method. Setted by
*/
LogEvent: function(type, values, closeCallback=null)
{
this._type = type;
this._timestamp = Date.now();
this.onCloseCallback = closeCallback;
this._values = values || {};
LogEvent: class extends LoggerEvent {
constructor(type, values, message) {
super(type, message);
this._timestamp = Date.now();
this.onCloseCallback = null;
this._is_active = document.hasFocus();
this._values = values || {};
}
this.toString = function()
{
return Object.assign({
event: Logger.eventTypeToString(this._type),
timestamp: this._timestamp,
}, this._values);
serialize() {
return Object.assign(super.serialize(), {
payload: this._values,
is_active: this._is_active,
});
};
this.close = function(endTimestamp)
{
if (this.onCloseCallback)
{
close(endTimestamp) {
if (this.onCloseCallback) {
this.addValues({
duration: endTimestamp ? endTimestamp - this._timestamp : Date.now() - this._timestamp,
});
@ -283,12 +282,35 @@ var Logger = {
}
};
this.addValues = function(values)
{
addValues(values) {
Object.assign(this._values, values);
};
},
ExceptionEvent: class extends LoggerEvent {
constructor(message, filename, line, column, stack, client, system) {
super(Logger.EventType.sendException, message);
this._client = client;
this._column = column;
this._filename = filename;
this._line = line;
this._stack = stack;
this._system = system;
}
serialize() {
return Object.assign(super.serialize(), {
client: this._client,
column: this._column,
filename: this._filename,
line: this._line,
stack: this._stack,
system: this._system,
});
};
},
/**
* Logger.EventType Enumeration.
*/
@ -375,12 +397,11 @@ var Logger = {
* @return {Bool} true if initialization was succeed
* @static
*/
initializeLogger: function(applicationName, taskId)
{
initializeLogger: function(jobId) {
if (!this._logger)
{
this._logger = new LoggerHandler(applicationName, taskId);
return this._logger.isInitialized;
this._logger = new LoggerHandler(jobId);
return true;
}
return false;
},
@ -389,11 +410,11 @@ var Logger = {
* Logger.addEvent Use this method to add a log event without duration field.
* @param {Logger.EventType} type Event Type
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'}
* @param {String} message Any string message. Empty by default.
* @static
*/
addEvent: function(type, values)
{
this._logger.addEvent(new Logger.LogEvent(type, values));
addEvent: function(type, values, message='') {
this._logger.addEvent(new Logger.LogEvent(type, values, message));
},
/**
@ -404,12 +425,12 @@ var Logger = {
* @param {Logger.EventType} type Event Type
* @param {Object} values Any event values, for example {count: 1, label:
* 'vehicle'}
* @param {String} message Any string message. Empty by default.
* @return {LogEvent} instance of LogEvent
* @static
*/
addContinuedEvent: function(type, values)
{
return this._logger.addContinuedEvent(new Logger.LogEvent(type, values));
addContinuedEvent: function(type, values, message='') {
return this._logger.addContinuedEvent(new Logger.LogEvent(type, values, message));
},
/**
@ -420,8 +441,7 @@ var Logger = {
* @return {Function} is decorated decoredFunc
* @static
*/
shortkeyLogDecorator: function(decoredFunc)
{
shortkeyLogDecorator: function(decoredFunc) {
let self = this;
return function(e, combo) {
let pressKeyEvent = self.addContinuedEvent(self.EventType.pressShortcut, {key: combo});
@ -437,9 +457,19 @@ var Logger = {
* @param {LogEvent} exceptionEvent
* @static
*/
sendException: function(exceptionData)
{
return this._logger.sendExceptions([new Logger.LogEvent(Logger.EventType.sendException, exceptionData)]);
sendException: function(message, filename, line, column, stack, client, system) {
return this._logger.sendExceptions(
new Logger.ExceptionEvent(
message,
filename,
line,
column,
stack,
client,
system
)
);
},
/**
@ -458,17 +488,6 @@ var Logger = {
return this._logger.getLogs();
},
/**
* Logger.setUsername just set username property which will be added to all
* log messages
* @param {String} username
* @static
*/
setUsername: function(username)
{
this._logger.setUsername(username);
},
/** Logger.updateUserActivityTimer method updates internal timer for working
* time calculation logic
* @static
@ -514,7 +533,7 @@ var Logger = {
case this.EventType.drawObject: return 'Draw object';
case this.EventType.changeLabel: return 'Change label';
case this.EventType.sendTaskInfo: return 'Send task info';
case this.EventType.loadJob: return 'Load job'; // FIXME add track count, object count, fields
case this.EventType.loadJob: return 'Load job';
case this.EventType.moveImage: return 'Move image';
case this.EventType.zoomImage: return 'Zoom image';
case this.EventType.lockObject: return 'Lock object';

@ -14,7 +14,7 @@
Mousetrap:false
*/
"use strict";
'use strict';
class FrameProvider extends Listener {
constructor(stop, tid) {
@ -45,7 +45,7 @@ class FrameProvider extends Listener {
}
_onImageLoad(image, frame) {
let next = frame + 1;
const next = frame + 1;
if (next <= this._stop && this._loadCounter > 0) {
this._stack.push(next);
}
@ -63,9 +63,9 @@ class FrameProvider extends Listener {
return;
}
let last = Math.min(this._stop, frame + Math.ceil(this._MAX_LOAD / 2));
const last = Math.min(this._stop, frame + Math.ceil(this._MAX_LOAD / 2));
if (!(last in this._frameCollection)) {
for (let idx = frame + 1; idx <= last; idx ++) {
for (let idx = frame + 1; idx <= last; idx++) {
if (!(idx in this._frameCollection)) {
this._loadCounter = this._MAX_LOAD - (idx - frame);
this._stack.push(idx);
@ -79,7 +79,7 @@ class FrameProvider extends Listener {
_load() {
if (!this._loadInterval) {
this._loadInterval = setInterval(function() {
this._loadInterval = setInterval(() => {
if (!this._loadAllowed) {
return;
}
@ -100,10 +100,10 @@ class FrameProvider extends Listener {
this._required = null;
}
let frame = this._stack.pop();
const frame = this._stack.pop();
if (frame in this._frameCollection) {
this._loadCounter--;
let next = frame + 1;
const next = frame + 1;
if (next <= this._stop && this._loadCounter > 0) {
this._stack.push(frame + 1);
}
@ -116,15 +116,15 @@ class FrameProvider extends Listener {
}
this._loadAllowed = false;
let image = new Image();
const image = new Image();
image.onload = this._onImageLoad.bind(this, image, frame);
image.onerror = () => {
this._loadAllowed = true;
image.onload = null;
image.onerror = null;
};
image.src = `get/task/${this._tid}/frame/${frame}`;
}.bind(this), 25);
image.src = `/api/v1/tasks/${this._tid}/frames/${frame}`;
}, 25);
}
}
}
@ -134,25 +134,25 @@ const MAX_PLAYER_SCALE = 10;
const MIN_PLAYER_SCALE = 0.1;
class PlayerModel extends Listener {
constructor(job, playerSize) {
constructor(task, playerSize) {
super('onPlayerUpdate', () => this);
this._frame = {
start: job.start,
stop: job.stop,
current: job.start,
previous: null
start: window.cvat.player.frames.start,
stop: window.cvat.player.frames.stop,
current: window.cvat.player.frames.current,
previous: null,
};
this._settings = {
multipleStep: 10,
fps: 25,
rotateAll: job.mode === 'interpolation',
resetZoom: job.mode === 'annotation'
rotateAll: task.mode === 'interpolation',
resetZoom: task.mode === 'annotation',
};
this._playInterval = null;
this._pauseFlag = null;
this._frameProvider = new FrameProvider(this._frame.stop, job.taskid);
this._frameProvider = new FrameProvider(this._frame.stop, task.id);
this._continueAfterLoad = false;
this._continueTimeout = null;
@ -166,11 +166,9 @@ class PlayerModel extends Listener {
rotation: 0,
};
this._framewiseRotation = {};
this._geometry.frameOffset = Math.floor(Math.max(
(playerSize.height - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE,
(playerSize.width - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE
));
const frameOffset = Math.max((playerSize.height - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE,
(playerSize.width - MIN_PLAYER_SCALE) / MIN_PLAYER_SCALE);
this._geometry.frameOffset = Math.floor(frameOffset);
window.cvat.translate.playerOffset = this._geometry.frameOffset;
window.cvat.player.rotation = this._geometry.rotation;
@ -182,14 +180,14 @@ class PlayerModel extends Listener {
start: this._frame.start,
stop: this._frame.stop,
current: this._frame.current,
previous: this._frame.previous
previous: this._frame.previous,
};
}
get geometry() {
let copy = Object.assign({}, this._geometry);
copy.rotation = this._settings.rotateAll ? this._geometry.rotation :
this._framewiseRotation[this._frame.current] || 0;
const copy = Object.assign({}, this._geometry);
copy.rotation = this._settings.rotateAll ? this._geometry.rotation
: this._framewiseRotation[this._frame.current] || 0;
return copy;
}
@ -241,7 +239,7 @@ class PlayerModel extends Listener {
return this._frame.previous === this._frame.current;
}
onFrameLoad(last) { // callback for FrameProvider instance
onFrameLoad(last) { // callback for FrameProvider instance
if (last === this._frame.current) {
if (this._continueTimeout) {
clearTimeout(this._continueTimeout);
@ -250,19 +248,17 @@ class PlayerModel extends Listener {
// If need continue playing after load, set timeout for additional frame download
if (this._continueAfterLoad) {
this._continueTimeout = setTimeout(function() {
this._continueTimeout = setTimeout(() => {
// If you still need to play, start it
this._continueTimeout = null;
if (this._continueAfterLoad) {
this._continueAfterLoad = false;
this.play();
} // Else update the frame
else {
} else { // Else update the frame
this.shift(0);
}
}.bind(this), 5000);
}
else { // Just update frame if no need to play
}, 5000);
} else { // Just update frame if no need to play
this.shift(0);
}
}
@ -270,8 +266,8 @@ class PlayerModel extends Listener {
play() {
this._pauseFlag = false;
this._playInterval = setInterval(function() {
if (this._pauseFlag) { // pause method without notify (for frame downloading)
this._playInterval = setInterval(() => {
if (this._pauseFlag) { // pause method without notify (for frame downloading)
if (this._playInterval) {
clearInterval(this._playInterval);
this._playInterval = null;
@ -279,9 +275,9 @@ class PlayerModel extends Listener {
return;
}
let skip = Math.max( Math.floor(this._settings.fps / 25), 1 );
if (!this.shift(skip)) this.pause(); // if not changed, pause
}.bind(this), 1000 / this._settings.fps);
const skip = Math.max(Math.floor(this._settings.fps / 25), 1);
if (!this.shift(skip)) this.pause(); // if not changed, pause
}, 1000 / this._settings.fps);
}
pause() {
@ -299,17 +295,15 @@ class PlayerModel extends Listener {
}
shift(delta, absolute) {
if (['resize', 'drag'].indexOf(window.cvat.mode) != -1) {
if (['resize', 'drag'].indexOf(window.cvat.mode) !== -1) {
return false;
}
this._continueAfterLoad = false; // default reset continue
this._frame.current = Math.clamp(
absolute ? delta : this._frame.current + delta,
this._continueAfterLoad = false; // default reset continue
this._frame.current = Math.clamp(absolute ? delta : this._frame.current + delta,
this._frame.start,
this._frame.stop
);
let frame = this._frameProvider.require(this._frame.current);
this._frame.stop);
const frame = this._frameProvider.require(this._frame.current);
if (!frame) {
this._continueAfterLoad = this.playing;
this._pauseFlag = true;
@ -326,14 +320,15 @@ class PlayerModel extends Listener {
to: this._frame.current,
});
let changed = this._frame.previous != this._frame.current;
let differentRotation = this._framewiseRotation[this._frame.previous] != this._framewiseRotation[this._frame.current];
const changed = this._frame.previous !== this._frame.current;
const curFrameRotation = this._framewiseRotation[this._frame.current];
const prevFrameRotation = this._framewiseRotation[this._frame.previous];
const differentRotation = curFrameRotation !== prevFrameRotation;
// fit if tool is in the annotation mode or frame loading is first in the interpolation mode
if (this._settings.resetZoom || this._frame.previous === null || differentRotation) {
this._frame.previous = this._frame.current;
this.fit(); // notify() inside the fit()
}
else {
this.fit(); // notify() inside the fit()
} else {
this._frame.previous = this._frame.current;
this.notify();
}
@ -342,22 +337,23 @@ class PlayerModel extends Listener {
}
fit() {
let img = this._frameProvider.require(this._frame.current);
const img = this._frameProvider.require(this._frame.current);
if (!img) return;
let rotation = this.geometry.rotation;
const { rotation } = this.geometry;
if ((rotation / 90) % 2) {
// 90, 270, ..
this._geometry.scale = Math.min(this._geometry.width / img.height, this._geometry.height / img.width);
}
else {
this._geometry.scale = Math.min(this._geometry.width / img.height,
this._geometry.height / img.width);
} else {
// 0, 180, ..
this._geometry.scale = Math.min(this._geometry.width / img.width, this._geometry.height / img.height);
this._geometry.scale = Math.min(this._geometry.width / img.width,
this._geometry.height / img.height);
}
this._geometry.top = (this._geometry.height - img.height * this._geometry.scale) / 2;
this._geometry.left = (this._geometry.width - img.width * this._geometry.scale ) / 2;
this._geometry.left = (this._geometry.width - img.width * this._geometry.scale) / 2;
window.cvat.player.rotation = rotation;
window.cvat.player.geometry.scale = this._geometry.scale;
@ -365,14 +361,15 @@ class PlayerModel extends Listener {
}
focus(xtl, xbr, ytl, ybr) {
let img = this._frameProvider.require(this._frame.current);
const img = this._frameProvider.require(this._frame.current);
if (!img) return;
let fittedScale = Math.min(this._geometry.width / img.width, this._geometry.height / img.height);
const fittedScale = Math.min(this._geometry.width / img.width,
this._geometry.height / img.height);
let boxWidth = xbr - xtl;
let boxHeight = ybr - ytl;
let wScale = this._geometry.width / boxWidth;
let hScale = this._geometry.height / boxHeight;
const boxWidth = xbr - xtl;
const boxHeight = ybr - ytl;
const wScale = this._geometry.width / boxWidth;
const hScale = this._geometry.height / boxHeight;
this._geometry.scale = Math.min(wScale, hScale);
this._geometry.scale = Math.min(this._geometry.scale, MAX_PLAYER_SCALE);
this._geometry.scale = Math.max(this._geometry.scale, MIN_PLAYER_SCALE);
@ -380,25 +377,22 @@ class PlayerModel extends Listener {
if (this._geometry.scale < fittedScale) {
this._geometry.scale = fittedScale;
this._geometry.top = (this._geometry.height - img.height * this._geometry.scale) / 2;
this._geometry.left = (this._geometry.width - img.width * this._geometry.scale ) / 2;
}
else {
this._geometry.left = (this._geometry.width - img.width * this._geometry.scale) / 2;
} else {
this._geometry.left = (this._geometry.width / this._geometry.scale - xtl * 2 - boxWidth) * this._geometry.scale / 2;
this._geometry.top = (this._geometry.height / this._geometry.scale - ytl * 2 - boxHeight) * this._geometry.scale / 2;
}
window.cvat.player.geometry.scale = this._geometry.scale;
this._frame.previous = this._frame.current; // fix infinite loop via playerUpdate->collectionUpdate*->AAMUpdate->playerUpdate->...
this._frame.previous = this._frame.current; // fix infinite loop via playerUpdate->collectionUpdate*->AAMUpdate->playerUpdate->...
this.notify();
}
scale(point, value) {
if (!this._frameProvider.require(this._frame.current)) return;
let oldScale = this._geometry.scale;
this._geometry.scale = Math.clamp(
value > 0 ? this._geometry.scale * 6/5 : this._geometry.scale * 5/6,
MIN_PLAYER_SCALE, MAX_PLAYER_SCALE
);
const oldScale = this._geometry.scale;
const newScale = value > 0 ? this._geometry.scale * 6 / 5 : this._geometry.scale * 5 / 6;
this._geometry.scale = Math.clamp(newScale, MIN_PLAYER_SCALE, MAX_PLAYER_SCALE);
this._geometry.left += (point.x * (oldScale / this._geometry.scale - 1)) * this._geometry.scale;
this._geometry.top += (point.y * (oldScale / this._geometry.scale - 1)) * this._geometry.scale;
@ -414,20 +408,18 @@ class PlayerModel extends Listener {
}
rotate(angle) {
if (['resize', 'drag'].indexOf(window.cvat.mode) != -1) {
if (['resize', 'drag'].indexOf(window.cvat.mode) !== -1) {
return false;
}
if (this._settings.rotateAll) {
this._geometry.rotation += angle;
this._geometry.rotation %= 360;
} else if (typeof (this._framewiseRotation[this._frame.current]) === 'undefined') {
this._framewiseRotation[this._frame.current] = angle;
} else {
if (typeof(this._framewiseRotation[this._frame.current]) === 'undefined') {
this._framewiseRotation[this._frame.current] = angle;
} else {
this._framewiseRotation[this._frame.current] += angle;
this._framewiseRotation[this._frame.current] %= 360;
}
this._framewiseRotation[this._frame.current] += angle;
this._framewiseRotation[this._frame.current] %= 360;
}
this.fit();
@ -451,106 +443,104 @@ class PlayerController {
move: null,
};
setupPlayerShortcuts.call(this, playerModel);
function setupPlayerShortcuts(playerModel) {
let nextHandler = Logger.shortkeyLogDecorator(function(e) {
const nextHandler = Logger.shortkeyLogDecorator((e) => {
this.next();
e.preventDefault();
}.bind(this));
});
let prevHandler = Logger.shortkeyLogDecorator(function(e) {
const prevHandler = Logger.shortkeyLogDecorator((e) => {
this.previous();
e.preventDefault();
}.bind(this));
});
let nextKeyFrameHandler = Logger.shortkeyLogDecorator(function() {
let active = activeTrack();
const nextKeyFrameHandler = Logger.shortkeyLogDecorator(() => {
const active = activeTrack();
if (active && active.type.split('_')[0] === 'interpolation') {
let nextKeyFrame = active.nextKeyFrame();
const nextKeyFrame = active.nextKeyFrame();
if (nextKeyFrame != null) {
this._model.shift(nextKeyFrame, true);
}
}
}.bind(this));
});
let prevKeyFrameHandler = Logger.shortkeyLogDecorator(function() {
let active = activeTrack();
const prevKeyFrameHandler = Logger.shortkeyLogDecorator(() => {
const active = activeTrack();
if (active && active.type.split('_')[0] === 'interpolation') {
let prevKeyFrame = active.prevKeyFrame();
const prevKeyFrame = active.prevKeyFrame();
if (prevKeyFrame != null) {
this._model.shift(prevKeyFrame, true);
}
}
}.bind(this));
});
let nextFilterFrameHandler = Logger.shortkeyLogDecorator(function(e) {
let frame = this._find(1);
const nextFilterFrameHandler = Logger.shortkeyLogDecorator((e) => {
const frame = this._find(1);
if (frame != null) {
this._model.shift(frame, true);
}
e.preventDefault();
}.bind(this));
});
let prevFilterFrameHandler = Logger.shortkeyLogDecorator(function(e) {
let frame = this._find(-1);
const prevFilterFrameHandler = Logger.shortkeyLogDecorator((e) => {
const frame = this._find(-1);
if (frame != null) {
this._model.shift(frame, true);
}
e.preventDefault();
}.bind(this));
});
let forwardHandler = Logger.shortkeyLogDecorator(function() {
const forwardHandler = Logger.shortkeyLogDecorator(() => {
this.forward();
}.bind(this));
});
let backwardHandler = Logger.shortkeyLogDecorator(function() {
const backwardHandler = Logger.shortkeyLogDecorator(() => {
this.backward();
}.bind(this));
});
let playPauseHandler = Logger.shortkeyLogDecorator(function() {
const playPauseHandler = Logger.shortkeyLogDecorator(() => {
if (playerModel.playing) {
this.pause();
}
else {
} else {
this.play();
}
return false;
}.bind(this));
let shortkeys = window.cvat.config.shortkeys;
Mousetrap.bind(shortkeys["next_frame"].value, nextHandler, 'keydown');
Mousetrap.bind(shortkeys["prev_frame"].value, prevHandler, 'keydown');
Mousetrap.bind(shortkeys["next_filter_frame"].value, nextFilterFrameHandler, 'keydown');
Mousetrap.bind(shortkeys["prev_filter_frame"].value, prevFilterFrameHandler, 'keydown');
Mousetrap.bind(shortkeys["next_key_frame"].value, nextKeyFrameHandler, 'keydown');
Mousetrap.bind(shortkeys["prev_key_frame"].value, prevKeyFrameHandler, 'keydown');
Mousetrap.bind(shortkeys["forward_frame"].value, forwardHandler, 'keydown');
Mousetrap.bind(shortkeys["backward_frame"].value, backwardHandler, 'keydown');
Mousetrap.bind(shortkeys["play_pause"].value, playPauseHandler, 'keydown');
Mousetrap.bind(shortkeys['clockwise_rotation'].value, (e) => {
});
const { shortkeys } = window.cvat.config;
Mousetrap.bind(shortkeys.next_frame.value, nextHandler, 'keydown');
Mousetrap.bind(shortkeys.prev_frame.value, prevHandler, 'keydown');
Mousetrap.bind(shortkeys.next_filter_frame.value, nextFilterFrameHandler, 'keydown');
Mousetrap.bind(shortkeys.prev_filter_frame.value, prevFilterFrameHandler, 'keydown');
Mousetrap.bind(shortkeys.next_key_frame.value, nextKeyFrameHandler, 'keydown');
Mousetrap.bind(shortkeys.prev_key_frame.value, prevKeyFrameHandler, 'keydown');
Mousetrap.bind(shortkeys.forward_frame.value, forwardHandler, 'keydown');
Mousetrap.bind(shortkeys.backward_frame.value, backwardHandler, 'keydown');
Mousetrap.bind(shortkeys.play_pause.value, playPauseHandler, 'keydown');
Mousetrap.bind(shortkeys.clockwise_rotation.value, (e) => {
e.preventDefault();
this.rotate(90);
}, 'keydown');
Mousetrap.bind(shortkeys['counter_clockwise_rotation'].value, (e) => {
Mousetrap.bind(shortkeys.counter_clockwise_rotation.value, (e) => {
e.preventDefault();
this.rotate(-90);
}, 'keydown');
}
setupPlayerShortcuts.call(this, playerModel);
}
zoom(e, canvas) {
let point = window.cvat.translate.point.clientToCanvas(canvas, e.clientX, e.clientY);
const point = window.cvat.translate.point.clientToCanvas(canvas, e.clientX, e.clientY);
let zoomImageEvent = Logger.addContinuedEvent(Logger.EventType.zoomImage);
const zoomImageEvent = Logger.addContinuedEvent(Logger.EventType.zoomImage);
if (e.originalEvent.deltaY < 0) {
this._model.scale(point, 1);
}
else {
} else {
this._model.scale(point, -1);
}
zoomImageEvent.close();
@ -558,7 +548,7 @@ class PlayerController {
}
fit() {
Logger.addEvent(Logger.EventType.fitImage)
Logger.addEvent(Logger.EventType.fitImage);
this._model.fit();
}
@ -566,7 +556,7 @@ class PlayerController {
if ((e.which === 1 && !window.cvat.mode) || (e.which === 2)) {
this._moving = true;
let p = window.cvat.translate.point.rotate(e.clientX, e.clientY);
const p = window.cvat.translate.point.rotate(e.clientX, e.clientY);
this._lastClickX = p.x;
this._lastClickY = p.y;
@ -587,9 +577,9 @@ class PlayerController {
this._events.move = Logger.addContinuedEvent(Logger.EventType.moveImage);
}
let p = window.cvat.translate.point.rotate(e.clientX, e.clientY);
let topOffset = p.y - this._lastClickY;
let leftOffset = p.x - this._lastClickX;
const p = window.cvat.translate.point.rotate(e.clientX, e.clientY);
const topOffset = p.y - this._lastClickY;
const leftOffset = p.x - this._lastClickX;
this._lastClickX = p.x;
this._lastClickY = p.y;
this._model.move(topOffset, leftOffset);
@ -619,24 +609,24 @@ class PlayerController {
this._events.jump = Logger.addContinuedEvent(Logger.EventType.jumpFrame);
}
let frames = this._model.frames;
let progressWidth = e.target.clientWidth;
let x = e.clientX + window.pageXOffset - e.target.offsetLeft;
let percent = x / progressWidth;
let targetFrame = Math.round((frames.stop - frames.start) * percent);
const { frames } = this._model;
const progressWidth = e.target.clientWidth;
const x = e.clientX + window.pageXOffset - e.target.offsetLeft;
const percent = x / progressWidth;
const targetFrame = Math.round((frames.stop - frames.start) * percent);
this._model.pause();
this._model.shift(targetFrame + frames.start, true);
}
}
changeStep(e) {
let value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
e.target.value = value;
this._model.multipleStep = value;
}
changeFPS(e) {
let fpsMap = {
const fpsMap = {
1: 1,
2: 5,
3: 12,
@ -644,7 +634,7 @@ class PlayerController {
5: 50,
6: 100,
};
let value = Math.clamp(+e.target.value, 1, 6);
const value = Math.clamp(+e.target.value, 1, 6);
this._model.fps = fpsMap[value];
}
@ -747,28 +737,29 @@ class PlayerView {
this._controller.rotate(-90);
});
this._rotatateAllImagesUI.prop("checked", this._controller.rotateAll);
this._rotatateAllImagesUI.on("change", (e) => {
this._rotatateAllImagesUI.prop('checked', this._controller.rotateAll);
this._rotatateAllImagesUI.on('change', (e) => {
this._controller.rotateAll = e.target.checked;
});
$('*').on('mouseup.player', () => this._controller.frameMouseUp());
this._playerContentUI.on('mousedown', (e) => {
let pos = window.cvat.translate.point.clientToCanvas(this._playerBackgroundUI[0], e.clientX, e.clientY);
let frameWidth = window.cvat.player.geometry.frameWidth;
let frameHeight = window.cvat.player.geometry.frameHeight;
const pos = window.cvat.translate.point.clientToCanvas(this._playerBackgroundUI[0],
e.clientX, e.clientY);
const { frameWidth } = window.cvat.player.geometry;
const { frameHeight } = window.cvat.player.geometry;
if (pos.x >= 0 && pos.y >= 0 && pos.x <= frameWidth && pos.y <= frameHeight) {
this._controller.frameMouseDown(e);
}
e.preventDefault();
});
this._playerContentUI.on('wheel', (e) => this._controller.zoom(e, this._playerBackgroundUI[0]));
this._playerContentUI.on('wheel', e => this._controller.zoom(e, this._playerBackgroundUI[0]));
this._playerContentUI.on('dblclick', () => this._controller.fit());
this._playerContentUI.on('mousemove', (e) => this._controller.frameMouseMove(e));
this._progressUI.on('mousedown', (e) => this._controller.progressMouseDown(e));
this._playerContentUI.on('mousemove', e => this._controller.frameMouseMove(e));
this._progressUI.on('mousedown', e => this._controller.progressMouseDown(e));
this._progressUI.on('mouseup', () => this._controller.progressMouseUp());
this._progressUI.on('mousemove', (e) => this._controller.progressMouseMove(e));
this._progressUI.on('mousemove', e => this._controller.progressMouseMove(e));
this._playButtonUI.on('click', () => this._controller.play());
this._pauseButtonUI.on('click', () => this._controller.pause());
this._nextButtonUI.on('click', () => this._controller.next());
@ -777,48 +768,47 @@ class PlayerView {
this._multiplePrevButtonUI.on('click', () => this._controller.backward());
this._firstButtonUI.on('click', () => this._controller.first());
this._lastButtonUI.on('click', () => this._controller.last());
this._playerSpeedUI.on('change', (e) => this._controller.changeFPS(e));
this._resetZoomUI.on('change', (e) => this._controller.changeResetZoom(e));
this._playerStepUI.on('change', (e) => this._controller.changeStep(e));
this._frameNumber.on('change', (e) =>
{
this._playerSpeedUI.on('change', e => this._controller.changeFPS(e));
this._resetZoomUI.on('change', e => this._controller.changeResetZoom(e));
this._playerStepUI.on('change', e => this._controller.changeStep(e));
this._frameNumber.on('change', (e) => {
if (Number.isInteger(+e.target.value)) {
this._controller.seek(+e.target.value);
blurAllElements();
}
});
let shortkeys = window.cvat.config.shortkeys;
const { shortkeys } = window.cvat.config;
this._clockwiseRotationButtonUI.attr('title', `
${shortkeys['clockwise_rotation'].view_value} - ${shortkeys['clockwise_rotation'].description}`);
${shortkeys.clockwise_rotation.view_value} - ${shortkeys.clockwise_rotation.description}`);
this._counterClockwiseRotationButtonUI.attr('title', `
${shortkeys['counter_clockwise_rotation'].view_value} - ${shortkeys['counter_clockwise_rotation'].description}`);
${shortkeys.counter_clockwise_rotation.view_value} - ${shortkeys.counter_clockwise_rotation.description}`);
let playerGridOpacityInput = $('#playerGridOpacityInput');
const playerGridOpacityInput = $('#playerGridOpacityInput');
playerGridOpacityInput.on('input', (e) => {
let value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
e.target.value = value;
this._playerGridPath.attr({
'opacity': value / +e.target.max,
opacity: value / +e.target.max,
});
});
playerGridOpacityInput.attr('title', `
${shortkeys['change_grid_opacity'].view_value} - ${shortkeys['change_grid_opacity'].description}`);
${shortkeys.change_grid_opacity.view_value} - ${shortkeys.change_grid_opacity.description}`);
let playerGridStrokeInput = $('#playerGridStrokeInput');
const playerGridStrokeInput = $('#playerGridStrokeInput');
playerGridStrokeInput.on('change', (e) => {
this._playerGridPath.attr({
'stroke': e.target.value,
stroke: e.target.value,
});
});
playerGridStrokeInput.attr('title', `
${shortkeys['change_grid_color'].view_value} - ${shortkeys['change_grid_color'].description}`);
${shortkeys.change_grid_color.view_value} - ${shortkeys.change_grid_color.description}`);
$('#playerGridSizeInput').on('change', (e) => {
let value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
const value = Math.clamp(+e.target.value, +e.target.min, +e.target.max);
e.target.value = value;
this._playerGridPattern.attr({
width: value,
@ -826,32 +816,32 @@ class PlayerView {
});
});
Mousetrap.bind(shortkeys['focus_to_frame'].value, () => this._frameNumber.focus(), 'keydown');
Mousetrap.bind(shortkeys["change_grid_opacity"].value,
Logger.shortkeyLogDecorator(function(e) {
let ui = playerGridOpacityInput;
Mousetrap.bind(shortkeys.focus_to_frame.value, () => this._frameNumber.focus(), 'keydown');
Mousetrap.bind(shortkeys.change_grid_opacity.value,
Logger.shortkeyLogDecorator((e) => {
const ui = playerGridOpacityInput;
let value = +ui.prop('value');
value += e.key === '=' ? 1 : -1;
value = Math.clamp(value, 0, 5);
ui.prop('value', value);
this._playerGridPath.attr({
'opacity': value / +ui.prop('max'),
opacity: value / +ui.prop('max'),
});
}.bind(this)),
}),
'keydown');
Mousetrap.bind(shortkeys["change_grid_color"].value,
Logger.shortkeyLogDecorator(function() {
let ui = playerGridStrokeInput;
let colors = [];
for (let opt of ui.find('option')) {
Mousetrap.bind(shortkeys.change_grid_color.value,
Logger.shortkeyLogDecorator(() => {
const ui = playerGridStrokeInput;
const colors = [];
for (const opt of ui.find('option')) {
colors.push(opt.value);
}
let idx = colors.indexOf(this._playerGridPath.attr('stroke')) + 1;
let value = colors[idx] || colors[0];
const idx = colors.indexOf(this._playerGridPath.attr('stroke')) + 1;
const value = colors[idx] || colors[0];
this._playerGridPath.attr('stroke', value);
ui.prop('value', value);
}.bind(this)),
}),
'keydown');
this._progressUI['0'].max = playerModel.frames.stop - playerModel.frames.start;
@ -862,53 +852,55 @@ class PlayerView {
this._playerSpeedUI.prop('value', '4');
this._frameNumber.attr('title', `
${shortkeys['focus_to_frame'].view_value} - ${shortkeys['focus_to_frame'].description}`);
${shortkeys.focus_to_frame.view_value} - ${shortkeys.focus_to_frame.description}`);
this._nextButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['next_frame'].view_value} - ${shortkeys['next_frame'].description}`));
.html(`${shortkeys.next_frame.view_value} - ${shortkeys.next_frame.description}`));
this._prevButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['prev_frame'].view_value} - ${shortkeys['prev_frame'].description}`));
.html(`${shortkeys.prev_frame.view_value} - ${shortkeys.prev_frame.description}`));
this._playButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['play_pause'].view_value} - ${shortkeys['play_pause'].description}`));
.html(`${shortkeys.play_pause.view_value} - ${shortkeys.play_pause.description}`));
this._pauseButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['play_pause'].view_value} - ${shortkeys['play_pause'].description}`));
.html(`${shortkeys.play_pause.view_value} - ${shortkeys.play_pause.description}`));
this._multipleNextButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['forward_frame'].view_value} - ${shortkeys['forward_frame'].description}`));
.html(`${shortkeys.forward_frame.view_value} - ${shortkeys.forward_frame.description}`));
this._multiplePrevButtonUI.find('polygon').append($(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
.html(`${shortkeys['backward_frame'].view_value} - ${shortkeys['backward_frame'].description}`));
.html(`${shortkeys.backward_frame.view_value} - ${shortkeys.backward_frame.description}`));
this._contextMenuUI.click((e) => {
$('.custom-menu').hide(100);
switch($(e.target).attr("action")) {
case "job_url": {
switch ($(e.target).attr('action')) {
case 'job_url': {
window.cvat.search.set('frame', null);
window.cvat.search.set('filter', null);
copyToClipboard(window.cvat.search.toString());
break;
}
case "frame_url":
case 'frame_url': {
window.cvat.search.set('frame', window.cvat.player.frames.current);
window.cvat.search.set('filter', null);
copyToClipboard(window.cvat.search.toString());
window.cvat.search.set('frame', null);
break;
}
default:
}
});
this._playerUI.on('contextmenu.playerContextMenu', (e) => {
if (!window.cvat.mode) {
$('.custom-menu').hide(100);
this._contextMenuUI.finish().show(100);
let x = Math.min(e.pageX, this._playerUI[0].offsetWidth -
this._contextMenuUI[0].scrollWidth);
let y = Math.min(e.pageY, this._playerUI[0].offsetHeight -
this._contextMenuUI[0].scrollHeight);
const x = Math.min(e.pageX, this._playerUI[0].offsetWidth
- this._contextMenuUI[0].scrollWidth);
const y = Math.min(e.pageY, this._playerUI[0].offsetHeight
- this._contextMenuUI[0].scrollHeight);
this._contextMenuUI.offset({
left: x,
top: y,
@ -925,9 +917,9 @@ class PlayerView {
}
onPlayerUpdate(model) {
let image = model.image;
let frames = model.frames;
let geometry = model.geometry;
const { image } = model;
const { frames } = model;
const { geometry } = model;
if (!image) {
this._loadingUI.removeClass('hidden');
@ -936,15 +928,14 @@ class PlayerView {
}
this._loadingUI.addClass('hidden');
if (this._playerBackgroundUI.css('background-image').slice(5,-2) != image.src) {
this._playerBackgroundUI.css('background-image', 'url(' + '"' + image.src + '"' + ')');
if (this._playerBackgroundUI.css('background-image').slice(5, -2) !== image.src) {
this._playerBackgroundUI.css('background-image', `url("${image.src}")`);
}
if (model.playing) {
this._playButtonUI.addClass('hidden');
this._pauseButtonUI.removeClass('hidden');
}
else {
} else {
this._pauseButtonUI.addClass('hidden');
this._playButtonUI.removeClass('hidden');
}
@ -953,8 +944,7 @@ class PlayerView {
this._firstButtonUI.addClass('disabledPlayerButton');
this._prevButtonUI.addClass('disabledPlayerButton');
this._multiplePrevButtonUI.addClass('disabledPlayerButton');
}
else {
} else {
this._firstButtonUI.removeClass('disabledPlayerButton');
this._prevButtonUI.removeClass('disabledPlayerButton');
this._multiplePrevButtonUI.removeClass('disabledPlayerButton');
@ -965,8 +955,7 @@ class PlayerView {
this._nextButtonUI.addClass('disabledPlayerButton');
this._playButtonUI.addClass('disabledPlayerButton');
this._multipleNextButtonUI.addClass('disabledPlayerButton');
}
else {
} else {
this._lastButtonUI.removeClass('disabledPlayerButton');
this._nextButtonUI.removeClass('disabledPlayerButton');
this._playButtonUI.removeClass('disabledPlayerButton');
@ -975,24 +964,24 @@ class PlayerView {
this._progressUI['0'].value = frames.current - frames.start;
this._rotationWrapperUI.css("transform", `rotate(${geometry.rotation}deg)`);
this._rotationWrapperUI.css('transform', `rotate(${geometry.rotation}deg)`);
for (let obj of [this._playerBackgroundUI, this._playerGridUI]) {
for (const obj of [this._playerBackgroundUI, this._playerGridUI]) {
obj.css('width', image.width);
obj.css('height', image.height);
obj.css('top', geometry.top);
obj.css('left', geometry.left);
obj.css('transform', 'scale(' + geometry.scale + ')');
obj.css('transform', `scale(${geometry.scale})`);
}
for (let obj of [this._playerContentUI, this._playerTextUI]) {
for (const obj of [this._playerContentUI, this._playerTextUI]) {
obj.css('width', image.width + geometry.frameOffset * 2);
obj.css('height', image.height + geometry.frameOffset * 2);
obj.css('top', geometry.top - geometry.frameOffset * geometry.scale);
obj.css('left', geometry.left - geometry.frameOffset * geometry.scale);
}
this._playerContentUI.css('transform', 'scale(' + geometry.scale + ')');
this._playerContentUI.css('transform', `scale(${geometry.scale})`);
this._playerTextUI.css('transform', `scale(10) rotate(${-geometry.rotation}deg)`);
this._playerGridPath.attr('stroke-width', 2 / geometry.scale);
this._frameNumber.prop('value', frames.current);

File diff suppressed because it is too large Load Diff

@ -4,42 +4,8 @@
* SPDX-License-Identifier: MIT
*/
/* exported serverRequest saveJobRequest encodeFilePathToURI */
/* global
showOverlay:false
*/
"use strict";
function serverRequest(url, successCallback)
{
$.ajax({
url: url,
dataType: "json",
success: successCallback,
error: serverError
});
}
function saveJobRequest(jid, data, success, error) {
$.ajax({
url: "save/annotation/job/" + jid,
type: "POST",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
success: success,
error: error,
processData: false,
});
}
/* exported encodeFilePathToURI */
function encodeFilePathToURI(path) {
return path.split('/').map(x => encodeURIComponent(x)).join('/');
}
function serverError() {
let message = 'Server errors was occured. Please contact with research automation team.';
showOverlay(message);
throw Error(message);
}

@ -79,28 +79,32 @@ class ShapeBufferModel extends Listener {
}
object.label_id = this._shape.label;
object.group_id = 0;
object.group = 0;
object.frame = window.cvat.player.frames.current;
object.attributes = attributes;
if (this._shape.type === 'box') {
box.occluded = this._shape.position.occluded;
box.frame = window.cvat.player.frames.current;
box.z_order = this._collection.zOrder(box.frame).max;
const position = {
xtl: box.xtl,
ytl: box.ytl,
xbr: box.xbr,
ybr: box.ybr,
occluded: this._shape.position.occluded,
frame: window.cvat.player.frames.current,
z_order: this._collection.zOrder(window.cvat.player.frames.current).max,
};
if (isTracked) {
object.shapes = [];
object.shapes.push(Object.assign(box, {
object.shapes.push(Object.assign(position, {
outside: false,
attributes: []
attributes: [],
}));
} else {
Object.assign(object, position);
}
else {
Object.assign(object, box);
}
}
else {
let position = {};
} else {
const position = {};
position.points = points;
position.occluded = this._shape.position.occluded;
position.frame = window.cvat.player.frames.current;
@ -157,9 +161,8 @@ class ShapeBufferModel extends Listener {
window.cvat.addAction('Paste Object', () => {
model.removed = true;
model.unsubscribe(this._collection);
}, (self) => {
}, () => {
model.subscribe(this._collection);
model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
@ -180,8 +183,7 @@ class ShapeBufferModel extends Listener {
ybr: this._shape.position.ybr,
};
object = this._makeObject(box, null, false);
}
else {
} else {
object = this._makeObject(null, this._shape.position.points, false);
}
@ -190,7 +192,7 @@ class ShapeBufferModel extends Listener {
count: numOfFrames,
});
let imageSizes = window.cvat.job.images.original_size;
let imageSizes = window.cvat.job.images;
let startFrame = window.cvat.player.frames.start;
let originalImageSize = imageSizes[object.frame - startFrame] || imageSizes[0];
@ -248,9 +250,8 @@ class ShapeBufferModel extends Listener {
object.removed = true;
object.unsubscribe(this._collection);
}
}, (self) => {
}, () => {
for (let object of addedObjects) {
object.id = self.generateId();
object.removed = false;
object.subscribe(this._collection);
}
@ -301,7 +302,7 @@ class ShapeBufferController {
let curFrame = window.cvat.player.frames.current;
let startFrame = window.cvat.player.frames.start;
let endFrame = Math.min(window.cvat.player.frames.stop, curFrame + this._model.propagateFrames);
let imageSizes = window.cvat.job.images.original_size;
let imageSizes = window.cvat.job.images;
let message = `Propagate up to ${endFrame} frame. `;
let refSize = imageSizes[curFrame - startFrame] || imageSizes[0];

@ -11,12 +11,9 @@
buildShapeModel:false
buildShapeView:false
copyToClipboard:false
createExportContainer:false
ExportType:false
FilterController:false
FilterModel:false
FilterView:false
getExportTargetContainer:false
Listener:false
Logger:false
Mousetrap:false
@ -30,19 +27,19 @@
"use strict";
class ShapeCollectionModel extends Listener {
constructor(idGenereator) {
constructor() {
super('onCollectionUpdate', () => this);
this._annotationShapes = {};
this._groups = {};
this._interpolationShapes = [];
this._shapes = [];
this._showAllInterpolation = false;
this._hash = null;
this._currentShapes = [];
this._idx = 0;
this._groupIdx = 0;
this._frame = null;
this._activeShape = null;
this._flush = false;
this._lastPos = {
x: 0,
y: 0,
@ -72,10 +69,6 @@ class ShapeCollectionModel extends Listener {
this._colorIdx = 0;
this._filter = new FilterModel(() => this.update());
this._splitter = new ShapeSplitter();
this._initialShapes = {};
this._exportedShapes = {};
this._shapesToDelete = createExportContainer();
this._idGen = idGenereator;
}
_nextGroupIdx() {
@ -179,20 +172,14 @@ class ShapeCollectionModel extends Listener {
return shape;
}
_importShape(shape, shapeType, udpateInitialState) {
let importedShape = this.add(shape, shapeType);
if (udpateInitialState) {
if (shape.id === -1) {
const toDelete = getExportTargetContainer(ExportType.delete, importedShape.type, this._shapesToDelete);
toDelete.push(shape.id);
}
else {
this._initialShapes[shape.id] = {
type: importedShape.type,
exportedString: importedShape.export(),
};
cleanupClientObjects() {
for (const shape of this._shapes) {
if (typeof (shape.serverID) === 'undefined') {
shape.removed = true;
}
}
this.notify();
}
colorsByGroup(groupId) {
@ -224,96 +211,110 @@ class ShapeCollectionModel extends Listener {
updateGroupIdx(groupId) {
if (groupId in this._groups) {
let newGroupId = this._nextGroupIdx();
const newGroupId = this._nextGroupIdx();
this._groups[newGroupId] = this._groups[groupId];
delete this._groups[groupId];
for (let elem of this._groups[newGroupId]) {
for (const elem of this._groups[newGroupId]) {
elem.groupId = newGroupId;
}
}
}
import(data, udpateInitialState=false) {
for (let box of data.boxes) {
this._importShape(box, 'annotation_box', udpateInitialState);
}
import(data) {
function _convertShape(shape) {
if (shape.type === 'rectangle') {
Object.assign(shape, window.cvat.translate.box.serverToClient(shape));
delete shape.points;
shape.type = 'box';
} else {
Object.assign(shape, window.cvat.translate.points.serverToClient(shape));
}
for (let boxPath of data.box_paths) {
this._importShape(boxPath, 'interpolation_box', udpateInitialState);
for (const attr of shape.attributes) {
attr.id = attr.spec_id;
delete attr.spec_id;
}
}
for (let points of data.points) {
this._importShape(points, 'annotation_points', udpateInitialState);
}
// Make copy of data in order to don't affect original data
data = JSON.parse(JSON.stringify(data));
for (let pointsPath of data.points_paths) {
this._importShape(pointsPath, 'interpolation_points', udpateInitialState);
}
for (const imported of data.shapes.concat(data.tracks)) {
// Conversion from client object format to server object format
if (imported.shapes) {
for (const attr of imported.attributes) {
attr.id = attr.spec_id;
delete attr.spec_id;
}
for (let polygon of data.polygons) {
this._importShape(polygon, 'annotation_polygon', udpateInitialState);
for (const shape of imported.shapes) {
_convertShape(shape);
}
this.add(imported, `interpolation_${imported.shapes[0].type}`);
} else {
_convertShape(imported);
this.add(imported, `annotation_${imported.type}`);
}
}
for (let polygonPath of data.polygon_paths) {
this._importShape(polygonPath, 'interpolation_polygon', udpateInitialState);
}
this.notify();
return this;
}
for (let polyline of data.polylines) {
this._importShape(polyline, 'annotation_polyline', udpateInitialState);
}
export() {
function _convertShape(shape) {
if (shape.type === 'box') {
Object.assign(shape, window.cvat.translate.box.clientToServer(shape));
shape.type = 'rectangle';
delete shape.xtl;
delete shape.ytl;
delete shape.xbr;
delete shape.ybr;
} else {
Object.assign(shape, window.cvat.translate.points.clientToServer(shape));
}
for (let polylinePath of data.polyline_paths) {
this._importShape(polylinePath, 'interpolation_polyline', udpateInitialState);
for (const attr of shape.attributes) {
attr.spec_id = attr.id;
delete attr.id;
}
}
this.notify();
return this;
}
const data = {
shapes: [],
tracks: [],
};
confirmExportedState() {
this._initialShapes = this._exportedShapes;
this._shapesToDelete = createExportContainer();
}
const mapping = [];
export() {
const response = createExportContainer();
for (let shape of this._shapes) {
if (!shape.removed) {
const exported = shape.export();
// Conversion from client object format to server object format
if (exported.shapes) {
for (let attr of exported.attributes) {
attr.spec_id = attr.id;
delete attr.id;
}
for (const shape of this._shapes) {
let targetExportContainer = undefined;
if (!shape._removed) {
if (!(shape.id in this._initialShapes)) {
targetExportContainer = getExportTargetContainer(ExportType.create, shape.type, response);
} else if (JSON.stringify(this._initialShapes[shape.id].exportedString) !== JSON.stringify(shape.export())) {
targetExportContainer = getExportTargetContainer(ExportType.update, shape.type, response);
for (let shape of exported.shapes) {
_convertShape(shape);
}
} else {
continue;
_convertShape(exported);
}
targetExportContainer.push(shape.export());
}
else if (shape.id in this._initialShapes) {
targetExportContainer = getExportTargetContainer(ExportType.delete, shape.type, response);
targetExportContainer.push(shape.id);
}
else {
continue;
}
}
for (const shapeType in this._shapesToDelete.delete) {
const shapes = this._shapesToDelete.delete[shapeType];
response.delete[shapeType].push.apply(response.delete[shapeType], shapes);
}
return response;
}
if (shape.type.split('_')[0] === 'annotation') {
data.shapes.push(exported);
} else {
data.tracks.push(exported);
}
exportAll() {
const response = createExportContainer();
for (const shape of this._shapes) {
if (!shape._removed) {
getExportTargetContainer(ExportType.create, shape.type, response).push(shape.export());
mapping.push([exported, shape]);
}
}
return response.create;
return [data, mapping];
}
find(direction) {
@ -371,40 +372,8 @@ class ShapeCollectionModel extends Listener {
}
}
hasUnsavedChanges() {
const exportData = this.export();
for (const actionType in ExportType) {
for (const shapes of Object.values(exportData[actionType])) {
if (shapes.length) {
return true;
}
}
}
return false;
}
updateExportedState() {
this._exportedShapes = {};
for (const shape of this._shapes) {
if (!shape.removed) {
this._exportedShapes[shape.id] = {
type: shape.type,
exportedString: shape.export(),
};
}
}
return this;
}
empty() {
for (const shapeId in this._initialShapes) {
const exportTarget = getExportTargetContainer(ExportType.delete, this._initialShapes[shapeId].type, this._shapesToDelete);
exportTarget.push(+shapeId);
}
this._initialShapes = {};
this._flush = true;
this._annotationShapes = {};
this._interpolationShapes = [];
this._shapes = [];
@ -414,13 +383,12 @@ class ShapeCollectionModel extends Listener {
}
add(data, type) {
let id = 'id' in data && data.id !== -1 ? data.id : this._idGen.next();
let model = buildShapeModel(data, type, id, this.nextColor());
this._idx += 1;
const id = this._idx;
const model = buildShapeModel(data, type, id, this.nextColor());
if (type.startsWith('interpolation')) {
this._interpolationShapes.push(model);
}
else {
} else {
this._annotationShapes[model.frame] = this._annotationShapes[model.frame] || [];
this._annotationShapes[model.frame].push(model);
}
@ -428,7 +396,7 @@ class ShapeCollectionModel extends Listener {
model.subscribe(this);
// Update collection groups & group index
let groupIdx = model.groupId;
const groupIdx = model.groupId;
this._groupIdx = Math.max(this._groupIdx, groupIdx);
if (groupIdx) {
this._groups[groupIdx] = this._groups[groupIdx] || [];
@ -794,8 +762,6 @@ class ShapeCollectionModel extends Listener {
}
}
removePointFromActiveShape(idx) {
if (this._activeShape && !this._activeShape.lock) {
this._activeShape.removePoint(idx);
@ -814,16 +780,14 @@ class ShapeCollectionModel extends Listener {
// Undo/redo code
let newShapes = this._shapes.slice(-list.length);
let originalShape = this._activeShape;
window.cvat.addAction('Split Object', (self) => {
window.cvat.addAction('Split Object', () => {
for (let shape of newShapes) {
shape.removed = true;
shape.unsubscribe(this);
}
originalShape.id = self.generateId();
originalShape.removed = false;
}, (self) => {
}, () => {
for (let shape of newShapes) {
shape.id = self.generateId();
shape.removed = false;
shape.subscribe(this);
}
@ -852,6 +816,14 @@ class ShapeCollectionModel extends Listener {
}
}
get flush() {
return this._flush;
}
set flush(value) {
this._flush = value;
}
get activeShape() {
return this._activeShape;
}
@ -1285,11 +1257,15 @@ class ShapeCollectionView {
case "object_url": {
let active = this._controller.activeShape;
if (active) {
window.cvat.search.set('frame', window.cvat.player.frames.current);
window.cvat.search.set('filter', `*[id="${active.id}"]`);
copyToClipboard(window.cvat.search.toString());
window.cvat.search.set('frame', null);
window.cvat.search.set('filter', null);
if (typeof active.serverID !== 'undefined') {
window.cvat.search.set('frame', window.cvat.player.frames.current);
window.cvat.search.set('filter', `*[serverID="${active.serverID}"]`);
copyToClipboard(window.cvat.search.toString());
window.cvat.search.set('frame', null);
window.cvat.search.set('filter', null);
} else {
showMessage('First save job in order to get static object URL');
}
}
break;
}
@ -1476,7 +1452,7 @@ class ShapeCollectionView {
let newShapes = collection.currentShapes;
let newModels = newShapes.map((el) => el.model);
let frameChanged = this._frameMarker != window.cvat.player.frames.current;
const frameChanged = this._frameMarker !== window.cvat.player.frames.current;
if (frameChanged) {
this._frameContent.node.parent = null;

@ -38,7 +38,7 @@ class ShapeCreatorModel extends Listener {
let frame = window.cvat.player.frames.current;
data.label_id = this._defaultLabel;
data.group_id = 0;
data.group = 0;
data.frame = frame;
data.occluded = false;
data.outside = false;
@ -70,9 +70,8 @@ class ShapeCreatorModel extends Listener {
window.cvat.addAction('Draw Object', () => {
model.removed = true;
model.unsubscribe(this._shapeCollection);
}, (self) => {
}, () => {
model.subscribe(this._shapeCollection);
model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
@ -389,20 +388,26 @@ class ShapeCreatorView {
sizeUI = null;
}
let frameWidth = window.cvat.player.geometry.frameWidth;
let frameHeight = window.cvat.player.geometry.frameHeight;
let rect = window.cvat.translate.box.canvasToActual(e.target.getBBox());
let box = {};
box.xtl = Math.clamp(rect.x, 0, frameWidth);
box.ytl = Math.clamp(rect.y, 0, frameHeight);
box.xbr = Math.clamp(rect.x + rect.width, 0, frameWidth);
box.ybr = Math.clamp(rect.y + rect.height, 0, frameHeight);
if (this._mode === 'interpolation') {
box.outside = false;
}
const frameWidth = window.cvat.player.geometry.frameWidth;
const frameHeight = window.cvat.player.geometry.frameHeight;
const rect = window.cvat.translate.box.canvasToActual(e.target.getBBox());
const xtl = Math.clamp(rect.x, 0, frameWidth);
const ytl = Math.clamp(rect.y, 0, frameHeight);
const xbr = Math.clamp(rect.x + rect.width, 0, frameWidth);
const ybr = Math.clamp(rect.y + rect.height, 0, frameHeight);
if ((ybr - ytl) * (xbr - xtl) >= AREA_TRESHOLD) {
const box = {
xtl,
ytl,
xbr,
ybr,
}
if (this._mode === 'interpolation') {
box.outside = false;
}
if ((box.ybr - box.ytl) * (box.xbr - box.xtl) >= AREA_TRESHOLD) {
this._controller.finish(box, this._type);
}

@ -7,25 +7,28 @@
/* exported FilterModel FilterController FilterView */
/* eslint no-unused-vars: ["error", { "caughtErrors": "none" }] */
"use strict";
/* global
defiant:false
*/
class FilterModel {
constructor(update) {
this._filter = "";
this._filter = '';
this._update = update;
this._labels = window.cvat.labelsInfo.labels();
this._attributes = window.cvat.labelsInfo.attributes();
}
_convertShape(shape) {
let converted = {
const converted = {
id: shape.model.id,
serverid: shape.model.serverID,
label: shape.model.label,
type: shape.model.type.split("_")[1],
mode: shape.model.type.split("_")[0],
occluded: shape.interpolation.position.occluded ? true : false,
type: shape.model.type.split('_')[1],
mode: shape.model.type.split('_')[0],
occluded: Boolean(shape.interpolation.position.occluded),
attr: convertAttributes(shape.interpolation.attributes),
lock: shape.model.lock
lock: shape.model.lock,
};
if (shape.model.type.split('_')[1] === 'box') {
@ -40,22 +43,25 @@ class FilterModel {
// We replace all dashes due to defiant.js can't work with it
function convertAttributes(attributes) {
let converted = {};
for (let attrId in attributes) {
converted[attributes[attrId].name.toLowerCase().replace(/-/g, "_")] = ("" + attributes[attrId].value).toLowerCase();
const convertedAttributes = {};
for (const attrId in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attrId)) {
const key = attributes[attrId].name.toLowerCase().replace(/[-,\s]+/g, '_');
convertedAttributes[key] = String(attributes[attrId].value).toLowerCase();
}
}
return converted;
return convertedAttributes;
}
}
_convertCollection(collection) {
let converted = {};
for (let labelId in this._labels) {
converted[this._labels[labelId].replace(/-/g, "_")] = [];
converted[this._labels[labelId].replace(/[-,\s]+/g, '_')] = [];
}
for (let shape of collection) {
converted[this._labels[shape.model.label].toLowerCase().replace(/-/g, "_")].push(this._convertShape(shape));
for (const shape of collection) {
converted[this._labels[shape.model.label].toLowerCase().replace(/[-,\s]+/g, '_')].push(this._convertShape(shape));
}
return converted;
}
@ -64,14 +70,12 @@ class FilterModel {
if (this._filter.length) {
// Get shape indexes
try {
let idxs = JSON.search(this._convertCollection(interpolation), `(${this._filter})/id`);
return interpolation.filter(x => idxs.indexOf(x.model.id) != -1);
}
catch(ignore) {
const idxs = defiant.search(this._convertCollection(interpolation), `(${this._filter})/id`);
return interpolation.filter(x => idxs.indexOf(x.model.id) !== -1);
} catch (ignore) {
return [];
}
}
else {
} else {
return interpolation;
}
}
@ -91,20 +95,19 @@ class FilterController {
updateFilter(value, silent) {
if (value.length) {
value = value.split("|").map(x => "/d:data/" + x).join("|").toLowerCase().replace(/-/g, "_");
value = value.split('|').map(x => `/d:data/${x}`).join('|').toLowerCase()
.replace(/[-,\s]+/g, '_');
try {
document.evaluate(value, document, () => "ns");
}
catch (ignore) {
document.evaluate(value, document, () => 'ns');
} catch (ignore) {
return false;
}
this._model.updateFilter(value, silent);
return true;
}
else {
this._model.updateFilter("", silent);
return true;
}
this._model.updateFilter('', silent);
return true;
}
deactivate() {

@ -93,7 +93,7 @@ class ShapeMergerModel extends Listener {
let object = {
label_id: label,
group_id: 0,
group: 0,
frame: sortedFrames[0],
attributes: [],
shapes: [],
@ -167,22 +167,20 @@ class ShapeMergerModel extends Listener {
let shapes = this._shapesForMerge;
// Undo/redo code
window.cvat.addAction('Merge Objects', (self) => {
window.cvat.addAction('Merge Objects', () => {
model.unsubscribe(this._collectionModel);
model.removed = true;
for (let shape of shapes) {
shape.id = self.generateId();
shape.removed = false;
shape.subscribe(this._collectionModel);
}
this._collectionModel.update();
}, (self) => {
}, () => {
for (let shape of shapes) {
shape.removed = true;
shape.unsubscribe(this._collectionModel);
}
model.subscribe(this._collectionModel);
model.id = self.generateId();
model.removed = false;
}, window.cvat.player.frames.current);
// End of undo/redo code
@ -214,7 +212,10 @@ class ShapeMergerModel extends Listener {
click() {
if (this._mergeMode) {
let active = this._collectionModel.selectShape(this._collectionModel.lastPosition, true);
const active = this._collectionModel.selectShape(
this._collectionModel.lastPosition,
true,
);
if (active) {
this._pushForMerge(active);
}

@ -8,17 +8,17 @@
"use strict";
class ShapeSplitter {
constructor() {}
_convertMutableAttributes(attributes) {
let result = [];
for (let attrId in attributes) {
let attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
if (attrInfo.mutable) {
result.push({
id: +attrId,
value: attributes[attrId].value
});
const result = [];
for (const attrId in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attrId)) {
const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
if (attrInfo.mutable) {
result.push({
id: +attrId,
value: attributes[attrId].value,
});
}
}
}
@ -26,58 +26,66 @@ class ShapeSplitter {
}
split(track, frame) {
let keyFrames = track.keyframes.sort((a,b) => a - b);
let exported = track.export();
const keyFrames = track.keyframes.map(keyframe => +keyframe).sort((a, b) => a - b);
const exported = track.export();
if (frame > +keyFrames[0]) {
let curInterpolation = track.interpolate(frame);
let prevInterpolation = track.interpolate(frame - 1);
let curAttributes = this._convertMutableAttributes(curInterpolation.attributes);
let prevAttrributes = this._convertMutableAttributes(prevInterpolation.attributes);
let curPositionList = [];
let prevPositionList = [];
const curInterpolation = track.interpolate(frame);
const prevInterpolation = track.interpolate(frame - 1);
const curAttributes = this._convertMutableAttributes(curInterpolation.attributes);
const prevAttrributes = this._convertMutableAttributes(prevInterpolation.attributes);
const curPositionList = [];
const prevPositionList = [];
for (let shape of exported.shapes) {
if (shape.frame < frame) {
for (const shape of exported.shapes) {
if (shape.frame < frame - 1) {
prevPositionList.push(shape);
}
else if (shape.frame > frame) {
} else if (shape.frame > frame) {
curPositionList.push(shape);
}
}
if (track.type.split('_')[1] === 'box') {
prevPositionList.push(Object.assign({}, prevInterpolation.position, {
const prevPos = prevInterpolation.position;
prevPositionList.push(Object.assign({}, {
frame: frame - 1,
attributes: prevAttrributes,
}));
type: 'box',
}, prevPos));
if (!prevInterpolation.position.outside) {
prevPositionList.push(Object.assign({}, prevInterpolation.position, {
outside: true,
frame: frame,
attributes: [],
}));
}
}
const curPos = curInterpolation.position;
prevPositionList.push(Object.assign({}, {
frame,
attributes: curAttributes,
type: 'box',
}, curPos, { outside: true }));
curPositionList.push(Object.assign(curInterpolation.position, {
frame: frame,
attributes: curAttributes,
}));
curPositionList.push(Object.assign({}, {
frame,
attributes: curAttributes,
type: 'box',
}, curPos));
} else {
const curPos = curInterpolation.position;
curPositionList.push(Object.assign({
frame,
attributes: curAttributes,
type: track.type.split('_')[1],
}, curPos));
}
// don't clone id of splitted object
delete exported.id;
let prevExported = Object.assign({}, exported);
let curExported = Object.assign({}, exported);
// don't clone group of splitted object
delete exported.group;
const prevExported = JSON.parse(JSON.stringify(exported));
const curExported = JSON.parse(JSON.stringify(exported));
prevExported.shapes = prevPositionList;
prevExported.group_id = 0;
curExported.shapes = curPositionList;
curExported.group_id = 0;
curExported.frame = frame;
return [prevExported, curExported];
}
else {
return [exported];
}
return [exported];
}
}

@ -15,6 +15,7 @@
Mousetrap:false
ShapeCollectionView:false
SVG:false
LabelsInfo:false
*/
"use strict";
@ -28,10 +29,11 @@ const TEXT_MARGIN = 10;
/******************************** SHAPE MODELS ********************************/
class ShapeModel extends Listener {
constructor(data, positions, type, id, color) {
constructor(data, positions, type, clientID, color) {
super('onShapeUpdate', () => this );
this._id = id;
this._groupId = data.group_id;
this._serverID = data.id;
this._id = clientID;
this._groupId = data.group || 0;
this._type = type;
this._color = color;
this._label = data.label_id;
@ -70,30 +72,28 @@ class ShapeModel extends Listener {
if (attrInfo.mutable) {
this._attributes.mutable[this._frame] = this._attributes.mutable[this._frame] || {};
this._attributes.mutable[this._frame][attrId] = attrInfo.values[0];
}
else {
} else {
this._attributes.immutable[attrId] = attrInfo.values[0];
}
}
for (let attrId in attributes) {
let attrInfo = labelsInfo.attrInfo(attrId);
const labelValue = LabelsInfo.normalize(attrInfo.type, attributes[attrId]);
if (attrInfo.mutable) {
this._attributes.mutable[this._frame][attrId] = labelsInfo.strToValues(attrInfo.type, attributes[attrId])[0];
}
else {
this._attributes.immutable[attrId] = labelsInfo.strToValues(attrInfo.type, attributes[attrId])[0];
this._attributes.mutable[this._frame][attrId] = labelValue;
} else {
this._attributes.immutable[attrId] = labelValue;
}
}
for (let pos of positions) {
let frame = pos.frame;
let attributes = pos.attributes;
for (let attr of attributes) {
let attrInfo = labelsInfo.attrInfo(attr.id);
for (const pos of positions) {
for (const attr of pos.attributes) {
const attrInfo = labelsInfo.attrInfo(attr.id);
if (attrInfo.mutable) {
this._attributes.mutable[frame] = this._attributes.mutable[frame] || {};
this._attributes.mutable[frame][attr.id] = labelsInfo.strToValues(attrInfo.type, attr.value)[0];
this._attributes.mutable[pos.frame] = this._attributes.mutable[pos.frame] || {};
const labelValue = LabelsInfo.normalize(attrInfo.type, attr.value);
this._attributes.mutable[pos.frame][attr.id] = labelValue;
}
}
}
@ -264,11 +264,10 @@ class ShapeModel extends Listener {
if (attrInfo.mutable) {
this._attributes.mutable[frame] = this._attributes.mutable[frame]|| {};
this._attributes.mutable[frame][attrId] = labelsInfo.strToValues(attrInfo.type, value)[0];
this._attributes.mutable[frame][attrId] = LabelsInfo.normalize(attrInfo.type, value);
this._setupKeyFrames();
}
else {
this._attributes.immutable[attrId] = labelsInfo.strToValues(attrInfo.type, value)[0];
} else {
this._attributes.immutable[attrId] = LabelsInfo.normalize(attrInfo.type, value);
}
this.notify('attributes');
@ -476,8 +475,7 @@ class ShapeModel extends Listener {
this.removed = true;
// Undo/redo code
window.cvat.addAction('Remove Object', (self) => {
this.id = self.generateId();
window.cvat.addAction('Remove Object', () => {
this.removed = false;
}, () => {
this.removed = true;
@ -498,6 +496,7 @@ class ShapeModel extends Listener {
set removed(value) {
if (value) {
this._active = false;
this._serverID = undefined;
}
this._removed = value;
@ -578,6 +577,14 @@ class ShapeModel extends Listener {
this._id = value;
}
get serverID() {
return this._serverID;
}
set serverID(value) {
this._serverID = value;
}
get frame() {
return this._frame;
}
@ -605,8 +612,8 @@ class ShapeModel extends Listener {
class BoxModel extends ShapeModel {
constructor(data, type, id, color) {
super(data, data.shapes || [], type, id, color);
constructor(data, type, clientID, color) {
super(data, data.shapes || [], type, clientID, color);
this._positions = BoxModel.importPositions.call(this, data.shapes || data);
this._setupKeyFrames();
}
@ -748,61 +755,62 @@ class BoxModel extends ShapeModel {
}
export() {
let immutableAttributes = [];
for (let attrId in this._attributes.immutable) {
immutableAttributes.push({
id: +attrId,
value: this._attributes.immutable[attrId],
const objectAttributes = [];
for (let attributeId in this._attributes.immutable) {
objectAttributes.push({
id: +attributeId,
value: String(this._attributes.immutable[attributeId]),
});
}
if (this._type === 'annotation_box') {
if (this._frame in this._attributes.mutable) {
for (let attrId in this._attributes.mutable[this._frame]) {
immutableAttributes.push({
objectAttributes.push({
id: +attrId,
value: this._attributes.mutable[this._frame][attrId],
value: String(this._attributes.mutable[this._frame][attrId]),
});
}
}
return Object.assign({}, this._positions[this._frame], {
id: this._id,
attributes: immutableAttributes,
return Object.assign({}, {
id: this._serverID,
attributes: objectAttributes,
label_id: this._label,
group_id: this._groupId,
group: this._groupId,
frame: this._frame,
});
type: 'box',
}, this._positions[this._frame]);
}
else {
let boxPath = {
id: this._id,
const track = {
id: this._serverID,
label_id: this._label,
group_id: this._groupId,
group: this._groupId,
frame: this._frame,
attributes: immutableAttributes,
attributes: objectAttributes,
shapes: [],
};
for (let frame in this._positions) {
let mutableAttributes = [];
const shapeAttributes = [];
if (frame in this._attributes.mutable) {
for (let attrId in this._attributes.mutable[frame]) {
mutableAttributes.push({
shapeAttributes.push({
id: +attrId,
value: this._attributes.mutable[frame][attrId],
value: String(this._attributes.mutable[frame][attrId]),
});
}
}
let position = Object.assign({}, this._positions[frame], {
attributes: mutableAttributes,
track.shapes.push(Object.assign({}, {
frame: +frame,
});
boxPath.shapes.push(position);
type: 'box',
attributes: shapeAttributes,
}, this._positions[frame]));
}
return boxPath;
return track;
}
}
@ -868,8 +876,8 @@ class BoxModel extends ShapeModel {
}
class PolyShapeModel extends ShapeModel {
constructor(data, type, id, color) {
super(data, data.shapes || [], type, id, color);
constructor(data, type, clientID, color) {
super(data, data.shapes || [], type, clientID, color);
this._positions = PolyShapeModel.importPositions.call(this, data.shapes || data);
this._setupKeyFrames();
}
@ -972,62 +980,62 @@ class PolyShapeModel extends ShapeModel {
}
export() {
let immutableAttributes = [];
const objectAttributes = [];
for (let attrId in this._attributes.immutable) {
immutableAttributes.push({
objectAttributes.push({
id: +attrId,
value: this._attributes.immutable[attrId],
value: String(this._attributes.immutable[attrId]),
});
}
if (this._type.startsWith('annotation')) {
if (this._frame in this._attributes.mutable) {
for (let attrId in this._attributes.mutable[this._frame]) {
immutableAttributes.push({
objectAttributes.push({
id: +attrId,
value: this._attributes.mutable[this._frame][attrId],
value: String(this._attributes.mutable[this._frame][attrId]),
});
}
}
return Object.assign({}, this._positions[this._frame], {
id: this._id,
attributes: immutableAttributes,
return Object.assign({}, {
id: this._serverID,
attributes: objectAttributes,
label_id: this._label,
group_id: this._groupId,
group: this._groupId,
frame: this._frame,
});
type: this._type.split('_')[1],
}, this._positions[this._frame]);
}
else {
let polyPath = {
id: this._id,
const track = {
id: this._serverID,
attributes: objectAttributes,
label_id: this._label,
group_id: this._groupId,
group: this._groupId,
frame: this._frame,
attributes: immutableAttributes,
shapes: [],
};
for (let frame in this._positions) {
let mutableAttributes = [];
let shapeAttributes = [];
if (frame in this._attributes.mutable) {
for (let attrId in this._attributes.mutable[frame]) {
mutableAttributes.push({
shapeAttributes.push({
id: +attrId,
value: this._attributes.mutable[frame][attrId],
value: String(this._attributes.mutable[frame][attrId]),
});
}
}
let position = Object.assign({}, this._positions[frame], {
attributes: mutableAttributes,
track.shapes.push(Object.assign({
frame: +frame,
});
polyPath.shapes.push(position);
attributes: shapeAttributes,
type: this._type.split('_')[1],
}, this._positions[frame]));
}
return polyPath;
return track;
}
}
@ -1132,8 +1140,8 @@ class PolyShapeModel extends ShapeModel {
}
class PointsModel extends PolyShapeModel {
constructor(data, type, id, color) {
super(data, type, id, color);
constructor(data, type, clientID, color) {
super(data, type, clientID, color);
this._minPoints = 1;
}
@ -1158,8 +1166,8 @@ class PointsModel extends PolyShapeModel {
class PolylineModel extends PolyShapeModel {
constructor(data, type, id, color) {
super(data, type, id, color);
constructor(data, type, clientID, color) {
super(data, type, clientID, color);
this._minPoints = 2;
}
@ -3237,20 +3245,20 @@ class PointsView extends PolyShapeView {
}
}
function buildShapeModel(data, type, idx, color) {
function buildShapeModel(data, type, clientID, color) {
switch (type) {
case 'interpolation_box':
case 'annotation_box':
return new BoxModel(data, type, idx, color);
return new BoxModel(data, type, clientID, color);
case 'interpolation_points':
case 'annotation_points':
return new PointsModel(data, type, idx, color);
return new PointsModel(data, type, clientID, color);
case 'interpolation_polyline':
case 'annotation_polyline':
return new PolylineModel(data, type, idx, color);
return new PolylineModel(data, type, clientID, color);
case 'interpolation_polygon':
case 'annotation_polygon':
return new PolygonModel(data, type, idx, color);
return new PolygonModel(data, type, clientID, color);
}
throw Error('Unreacheable code was reached.');
}

@ -5,322 +5,314 @@
*/
/* exported Config */
"use strict";
class Config {
constructor() {
this._username = "_default_";
this._username = '_default_';
this._shortkeys = {
switch_lock_property: {
value: "l",
view_value: "L",
description: "switch lock property for active shape"
value: 'l',
view_value: 'L',
description: 'switch lock property for active shape',
},
switch_all_lock_property: {
value: "t l",
view_value: "T + L",
description: "switch lock property for all shapes on current frame"
value: 't l',
view_value: 'T + L',
description: 'switch lock property for all shapes on current frame',
},
switch_occluded_property: {
value: "q,/".split(','),
view_value: "Q or Num Devision",
description: "switch occluded property for active shape"
value: 'q,/'.split(','),
view_value: 'Q or Num Devision',
description: 'switch occluded property for active shape',
},
switch_draw_mode: {
value: "n",
view_value: "N",
description: "start draw / stop draw"
value: 'n',
view_value: 'N',
description: 'start draw / stop draw',
},
switch_merge_mode: {
value: "m",
view_value: "M",
description: "start merge / apply changes"
value: 'm',
view_value: 'M',
description: 'start merge / apply changes',
},
switch_group_mode: {
value: "g",
view_value: "G",
description: "start group / apply changes"
value: 'g',
view_value: 'G',
description: 'start group / apply changes',
},
reset_group: {
value: "shift+g",
view_value: "Shift + G",
description: "reset group for selected shapes"
value: 'shift+g',
view_value: 'Shift + G',
description: 'reset group for selected shapes',
},
change_shape_label: {
value: "ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9".split(','),
view_value: "Ctrl + (1,2,3,4,5,6,7,8,9)",
description: "change shape label for existing object"
value: 'ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9'.split(','),
view_value: 'Ctrl + (1,2,3,4,5,6,7,8,9)',
description: 'change shape label for existing object',
},
change_default_label: {
value: "shift+1,shift+2,shift+3,shift+4,shift+5,shift+6,shift+7,shift+8,shift+9".split(','),
view_value: "Shift + (1,2,3,4,5,6,7,8,9)",
description: "change label default label"
value: 'shift+1,shift+2,shift+3,shift+4,shift+5,shift+6,shift+7,shift+8,shift+9'.split(','),
view_value: 'Shift + (1,2,3,4,5,6,7,8,9)',
description: 'change label default label',
},
change_shape_color: {
value: "enter",
view_value: "Enter",
description: "change color for highligted shape"
value: 'enter',
view_value: 'Enter',
description: 'change color for highligted shape',
},
change_player_brightness: {
value: "shift+b,alt+b".split(','),
view_value: "Shift+B / Alt+B",
description: "increase/decrease brightness of an image"
value: 'shift+b,alt+b'.split(','),
view_value: 'Shift+B / Alt+B',
description: 'increase/decrease brightness of an image',
},
change_player_contrast: {
value: "shift+c,alt+c".split(','),
view_value: "Shift+C / Alt+C",
description: "increase/decrease contrast of an image"
value: 'shift+c,alt+c'.split(','),
view_value: 'Shift+C / Alt+C',
description: 'increase/decrease contrast of an image',
},
change_player_saturation: {
value: "shift+s,alt+s".split(','),
view_value: "Shift+S / Alt+S",
description: "increase/decrease saturation of an image"
value: 'shift+s,alt+s'.split(','),
view_value: 'Shift+S / Alt+S',
description: 'increase/decrease saturation of an image',
},
switch_hide_mode: {
value: "h",
view_value: "H",
description: "switch hide mode for active shape"
value: 'h',
view_value: 'H',
description: 'switch hide mode for active shape',
},
switch_active_keyframe: {
value: "k",
view_value: "K",
description: "switch keyframe property for active shape"
value: 'k',
view_value: 'K',
description: 'switch keyframe property for active shape',
},
switch_active_outside: {
value: "o",
view_value: "O",
description: "switch outside property for active shape"
value: 'o',
view_value: 'O',
description: 'switch outside property for active shape',
},
switch_all_hide_mode: {
value: "t h",
view_value: "T + H",
description: "switch hide mode for all shapes"
value: 't h',
view_value: 'T + H',
description: 'switch hide mode for all shapes',
},
delete_shape: {
value: "del,shift+del".split(','),
view_value: "Del, Shift + Del",
description: "delete active shape (use shift for force deleting)"
value: 'del,shift+del'.split(','),
view_value: 'Del, Shift + Del',
description: 'delete active shape (use shift for force deleting)',
},
focus_to_frame: {
value: '`,~'.split(','),
view_value: '~ / `',
description: "focus to 'go to frame' element"
description: 'focus to "go to frame" element',
},
next_frame: {
value: "f",
view_value: "F",
description: "move to next player frame"
value: 'f',
view_value: 'F',
description: 'move to next player frame',
},
prev_frame: {
value: "d",
view_value: "D",
description: "move to previous player frame"
value: 'd',
view_value: 'D',
description: 'move to previous player frame',
},
forward_frame: {
value: "v",
view_value: "V",
description: "move forward several frames"
value: 'v',
view_value: 'V',
description: 'move forward several frames',
},
backward_frame: {
value: "c",
view_value: "C",
description: "move backward several frames"
value: 'c',
view_value: 'C',
description: 'move backward several frames',
},
next_key_frame: {
value: "r",
view_value: "R",
description: "move to next key frame of highlighted track"
value: 'r',
view_value: 'R',
description: 'move to next key frame of highlighted track',
},
prev_key_frame: {
value: "e",
view_value: "E",
description: "move to previous key frame of highlighted track"
value: 'e',
view_value: 'E',
description: 'move to previous key frame of highlighted track',
},
prev_filter_frame: {
value: 'left',
view_value: 'Left Arrow',
description: 'move to prev frame which satisfies the filter'
description: 'move to prev frame which satisfies the filter',
},
next_filter_frame: {
value: 'right',
view_value: 'Right Arrow',
description: 'move to next frame which satisfies the filter'
description: 'move to next frame which satisfies the filter',
},
play_pause: {
value: "space",
view_value: "Space",
description: "switch play / pause of player"
value: 'space',
view_value: 'Space',
description: 'switch play / pause of player',
},
open_help: {
value: "f1",
view_value: "F1",
description: "open help window"
value: 'f1',
view_value: 'F1',
description: 'open help window',
},
open_settings: {
value: "f2",
view_value: "F2",
description: "open settings window "
},
open_analytics: {
value: "f3",
view_value: "F3",
description: "open analytics window"
value: 'f2',
view_value: 'F2',
description: 'open settings window ',
},
save_work: {
value: "ctrl+s",
view_value: "Ctrl + S",
description: "save work on the server"
value: 'ctrl+s',
view_value: 'Ctrl + S',
description: 'save work on the server',
},
copy_shape: {
value: "ctrl+c",
view_value: "Ctrl + C",
description: "copy active shape to buffer"
value: 'ctrl+c',
view_value: 'Ctrl + C',
description: 'copy active shape to buffer',
},
propagate_shape: {
value: "ctrl+b",
view_value: "Ctrl + B",
description: "propagate active shape"
value: 'ctrl+b',
view_value: 'Ctrl + B',
description: 'propagate active shape',
},
switch_paste: {
value: "ctrl+v",
view_value: "Ctrl + V",
description: "swich paste mode"
value: 'ctrl+v',
view_value: 'Ctrl + V',
description: 'swich paste mode',
},
switch_aam_mode: {
value: "shift+enter",
view_value: "Shift + Enter",
description: "switch attribute annotation mode"
value: 'shift+enter',
view_value: 'Shift + Enter',
description: 'switch attribute annotation mode',
},
aam_next_attribute: {
value: "down",
view_value: "Down Arrow",
description: "move to next attribute in attribute annotation mode"
value: 'down',
view_value: 'Down Arrow',
description: 'move to next attribute in attribute annotation mode',
},
aam_prev_attribute: {
value: "up",
view_value: "Up Arrow",
description: "move to previous attribute in attribute annotation mode"
value: 'up',
view_value: 'Up Arrow',
description: 'move to previous attribute in attribute annotation mode',
},
aam_next_shape: {
value: "tab",
view_value: "Tab",
description: "move to next shape in attribute annotation mode"
value: 'tab',
view_value: 'Tab',
description: 'move to next shape in attribute annotation mode',
},
aam_prev_shape: {
value: "shift+tab",
view_value: "Shift + Tab",
description: "move to previous shape in attribute annotation mode"
value: 'shift+tab',
view_value: 'Shift + Tab',
description: 'move to previous shape in attribute annotation mode',
},
select_i_attribute: {
value: "1,2,3,4,5,6,7,8,9,0".split(','),
view_value: "1,2,3,4,5,6,7,8,9,0",
description: "setup corresponding attribute value in attribute annotation mode"
value: '1,2,3,4,5,6,7,8,9,0'.split(','),
view_value: '1,2,3,4,5,6,7,8,9,0',
description: 'setup corresponding attribute value in attribute annotation mode',
},
change_grid_opacity: {
value: ['alt+g+=', 'alt+g+-'],
view_value: "Alt + G + '+', Alt + G + '-'",
description: "increase/decrease grid opacity"
view_value: 'Alt + G + "+", Alt + G + "-"',
description: 'increase/decrease grid opacity',
},
change_grid_color: {
value: "alt+g+enter",
view_value: "Alt + G + Enter",
description: "change grid color"
value: 'alt+g+enter',
view_value: 'Alt + G + Enter',
description: 'change grid color',
},
undo: {
value: "ctrl+z",
view_value: "Ctrl + Z",
description: "undo"
value: 'ctrl+z',
view_value: 'Ctrl + Z',
description: 'undo',
},
redo: {
value: ['ctrl+shift+z', 'ctrl+y'],
view_value: "Ctrl + Shift + Z / Ctrl + Y",
description: "redo"
view_value: 'Ctrl + Shift + Z / Ctrl + Y',
description: 'redo',
},
cancel_mode: {
value: 'esc',
view_value: "Esc",
description: "cancel active mode"
view_value: 'Esc',
description: 'cancel active mode',
},
clockwise_rotation: {
value: 'ctrl+r',
view_value: 'Ctrl + R',
description: 'clockwise image rotation'
description: 'clockwise image rotation',
},
counter_clockwise_rotation: {
value: 'ctrl+shift+r',
view_value: 'Ctrl + Shift + R',
description: 'counter clockwise image rotation'
description: 'counter clockwise image rotation',
},
next_shape_type: {
value: ['alt+.'],
view_value: 'Alt + >',
description: 'switch next default shape type'
description: 'switch next default shape type',
},
prev_shape_type: {
value: ['alt+,'],
view_value: 'Alt + <',
description: 'switch previous default shape type'
description: 'switch previous default shape type',
},
};
if (window.cvat && window.cvat.job && window.cvat.job.z_order) {
this._shortkeys['inc_z'] = {
this._shortkeys.inc_z = {
value: '+,='.split(','),
view_value: '+',
description: 'increase z order for active shape',
};
this._shortkeys['dec_z'] = {
this._shortkeys.dec_z = {
value: '-,_'.split(','),
view_value: '-',
description: 'decrease z order for active shape',
@ -329,28 +321,28 @@ class Config {
this._settings = {
player_step: {
value: "10",
description: "step size for player when move on several frames forward/backward"
value: '10',
description: 'step size for player when move on several frames forward/backward',
},
player_speed: {
value: "25 FPS",
description: "playback speed of the player"
value: '25 FPS',
description: 'playback speed of the player',
},
reset_zoom: {
value: "false",
description: "reset frame zoom when move beetween the frames"
value: 'false',
description: 'reset frame zoom when move beetween the frames',
},
enable_auto_save: {
value: "false",
description: "enable auto save ability"
value: 'false',
description: 'enable auto save ability',
},
auto_save_interval: {
value: "15",
description: "auto save interval (min)"
value: '15',
description: 'auto save interval (min)',
},
};

@ -6,7 +6,6 @@
import os
import sys
import rq
import shlex
import shutil
import tempfile
import numpy as np
@ -19,254 +18,31 @@ _SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
_MEDIA_MIMETYPES_FILE = os.path.join(_SCRIPT_DIR, "media.mimetypes")
mimetypes.init(files=[_MEDIA_MIMETYPES_FILE])
from cvat.apps.engine.models import StatusChoice
from cvat.apps.engine.plugins import plugin_decorator
import django_rq
from django.conf import settings
from django.db import transaction
from ffmpy import FFmpeg
from pyunpack import Archive
from distutils.dir_util import copy_tree
from collections import OrderedDict
from . import models
from .log import slogger
############################# Low Level server API
@transaction.atomic
def create_empty(params):
"""Create empty directory structure for a new task, add it to DB."""
db_task = models.Task()
db_task.name = params['task_name']
db_task.bug_tracker = params['bug_tracker_link']
db_task.path = ""
db_task.size = 0
db_task.owner = params['owner']
db_task.save()
task_path = os.path.join(settings.DATA_ROOT, str(db_task.id))
db_task.set_task_dirname(task_path)
task_path = db_task.get_task_dirname()
if os.path.isdir(task_path):
shutil.rmtree(task_path)
os.mkdir(task_path)
upload_dir = db_task.get_upload_dirname()
os.makedirs(upload_dir)
output_dir = db_task.get_data_dirname()
os.makedirs(output_dir)
return db_task
def create(tid, params):
def create(tid, data):
"""Schedule the task"""
q = django_rq.get_queue('default')
q.enqueue_call(func=_create_thread, args=(tid, params),
job_id="task.create/{}".format(tid))
def check(tid):
"""Check status of the scheduled task"""
response = {}
queue = django_rq.get_queue('default')
job = queue.fetch_job("task.create/{}".format(tid))
if job is None:
response = {"state": "unknown"}
elif job.is_failed:
response = {"state": "error", "stderr": "Could not create the task. " + job.exc_info }
elif job.is_finished:
response = {"state": "created"}
else:
response = {"state": "started"}
if 'status' in job.meta:
response['status'] = job.meta['status']
return response
@transaction.atomic
def delete(tid):
"""Delete the task"""
db_task = models.Task.objects.select_for_update().get(pk=tid)
if db_task:
db_task.delete()
shutil.rmtree(db_task.get_task_dirname(), ignore_errors=True)
else:
raise Exception("The task doesn't exist")
@transaction.atomic
def update(tid, labels):
"""Update labels for the task"""
db_task = models.Task.objects.select_for_update().get(pk=tid)
db_labels = list(db_task.label_set.prefetch_related('attributespec_set').all())
new_labels = _parse_labels(labels)
old_labels = _parse_db_labels(db_labels)
for label_name in new_labels:
if label_name in old_labels:
db_label = [l for l in db_labels if l.name == label_name][0]
for attr_name in new_labels[label_name]:
if attr_name in old_labels[label_name]:
db_attr = [attr for attr in db_label.attributespec_set.all()
if attr.get_name() == attr_name][0]
new_attr = new_labels[label_name][attr_name]
old_attr = old_labels[label_name][attr_name]
if new_attr['prefix'] != old_attr['prefix']:
raise Exception("new_attr['prefix'] != old_attr['prefix']")
if new_attr['type'] != old_attr['type']:
raise Exception("new_attr['type'] != old_attr['type']")
if set(old_attr['values']) - set(new_attr['values']):
raise Exception("set(old_attr['values']) - set(new_attr['values'])")
db_attr.text = "{}{}={}:{}".format(new_attr['prefix'],
new_attr['type'], attr_name, ",".join(new_attr['values']))
db_attr.save()
else:
db_attr = models.AttributeSpec()
attr = new_labels[label_name][attr_name]
db_attr.text = "{}{}={}:{}".format(attr['prefix'],
attr['type'], attr_name, ",".join(attr['values']))
db_attr.label = db_label
db_attr.save()
else:
db_label = models.Label()
db_label.name = label_name
db_label.task = db_task
db_label.save()
for attr_name in new_labels[label_name]:
db_attr = models.AttributeSpec()
attr = new_labels[label_name][attr_name]
db_attr.text = "{}{}={}:{}".format(attr['prefix'],
attr['type'], attr_name, ",".join(attr['values']))
db_attr.label = db_label
db_attr.save()
def get_frame_path(tid, frame):
"""Read corresponding frame for the task"""
db_task = models.Task.objects.get(pk=tid)
path = _get_frame_path(frame, db_task.get_data_dirname())
return path
def get(tid):
"""Get the task as dictionary of attributes"""
db_task = models.Task.objects.get(pk=tid)
if db_task:
db_labels = db_task.label_set.prefetch_related('attributespec_set').order_by('-pk').all()
im_meta_data = get_image_meta_cache(db_task)
attributes = {}
for db_label in db_labels:
attributes[db_label.id] = {}
for db_attrspec in db_label.attributespec_set.all():
attributes[db_label.id][db_attrspec.id] = db_attrspec.text
db_segments = list(db_task.segment_set.prefetch_related('job_set').all())
segment_length = max(db_segments[0].stop_frame - db_segments[0].start_frame + 1, 1)
job_indexes = []
for segment in db_segments:
db_job = segment.job_set.first()
job_indexes.append({
"job_id": db_job.id,
"max_shape_id": db_job.max_shape_id,
})
response = {
"status": db_task.status,
"spec": {
"labels": OrderedDict((db_label.id, db_label.name) for db_label in db_labels),
"attributes": attributes
},
"size": db_task.size,
"taskid": db_task.id,
"name": db_task.name,
"mode": db_task.mode,
"segment_length": segment_length,
"jobs": job_indexes,
"overlap": db_task.overlap,
"z_orded": db_task.z_order,
"flipped": db_task.flipped,
"image_meta_data": im_meta_data
}
else:
raise Exception("Cannot find the task: {}".format(tid))
return response
@transaction.atomic
def save_job_status(jid, status, user):
db_job = models.Job.objects.select_related("segment__task").select_for_update().get(pk = jid)
db_task = db_job.segment.task
status = StatusChoice(status)
slogger.job[jid].info('changing job status from {} to {} by an user {}'.format(db_job.status, str(status), user))
db_job.status = status.value
db_job.save()
db_segments = list(db_task.segment_set.prefetch_related('job_set').all())
db_jobs = [db_segment.job_set.first() for db_segment in db_segments]
if len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.ANNOTATION, db_jobs))) > 0:
db_task.status = StatusChoice.ANNOTATION
elif len(list(filter(lambda x: StatusChoice(x.status) == StatusChoice.VALIDATION, db_jobs))) > 0:
db_task.status = StatusChoice.VALIDATION
else:
db_task.status = StatusChoice.COMPLETED
db_task.save()
def get_job(jid):
"""Get the job as dictionary of attributes"""
db_job = models.Job.objects.select_related("segment__task").get(id=jid)
if db_job:
db_segment = db_job.segment
db_task = db_segment.task
im_meta_data = get_image_meta_cache(db_task)
# Truncate extra image sizes
if db_task.mode == 'annotation':
im_meta_data['original_size'] = im_meta_data['original_size'][db_segment.start_frame:db_segment.stop_frame + 1]
db_labels = db_task.label_set.prefetch_related('attributespec_set').order_by('-pk').all()
attributes = {}
for db_label in db_labels:
attributes[db_label.id] = {}
for db_attrspec in db_label.attributespec_set.all():
attributes[db_label.id][db_attrspec.id] = db_attrspec.text
response = {
"status": db_job.status,
"labels": OrderedDict((db_label.id, db_label.name) for db_label in db_labels),
"stop": db_segment.stop_frame,
"taskid": db_task.id,
"slug": db_task.name,
"jobid": jid,
"start": db_segment.start_frame,
"mode": db_task.mode,
"overlap": db_task.overlap,
"attributes": attributes,
"z_order": db_task.z_order,
"flipped": db_task.flipped,
"image_meta_data": im_meta_data,
"max_shape_id": db_job.max_shape_id,
}
else:
raise Exception("Cannot find the job: {}".format(jid))
return response
q.enqueue_call(func=_create_thread, args=(tid, data),
job_id="/api/v1/tasks/{}".format(tid))
@transaction.atomic
def rq_handler(job, exc_type, exc_value, traceback):
tid = job.id.split('/')[1]
splitted = job.id.split('/')
tid = int(splitted[splitted.index('tasks') + 1])
db_task = models.Task.objects.select_for_update().get(pk=tid)
with open(db_task.get_log_path(), "wt") as log_file:
print_exception(exc_type, exc_value, traceback, file=log_file)
db_task.delete()
return False
############################# Internal implementation for server API
@ -304,14 +80,14 @@ class _FrameExtractor:
yield self[i]
i += 1
def _make_image_meta_cache(db_task):
def make_image_meta_cache(db_task):
with open(db_task.get_image_meta_cache_path(), 'w') as meta_file:
cache = {
'original_size': []
}
if db_task.mode == 'interpolation':
image = Image.open(get_frame_path(db_task.id, 0))
image = Image.open(db_task.get_frame_path(0))
cache['original_size'].append({
'width': image.size[0],
'height': image.size[1]
@ -341,7 +117,7 @@ def get_image_meta_cache(db_task):
with open(db_task.get_image_meta_cache_path()) as meta_cache_file:
return literal_eval(meta_cache_file.read())
except Exception:
_make_image_meta_cache(db_task)
make_image_meta_cache(db_task)
with open(db_task.get_image_meta_cache_path()) as meta_cache_file:
return literal_eval(meta_cache_file.read())
@ -362,223 +138,79 @@ def _get_mime(name):
elif mime_type.startswith('image'):
return 'image'
else:
return 'empty'
return 'unknown'
else:
if os.path.isdir(name):
return 'directory'
else:
return 'empty'
def _get_frame_path(frame, base_dir):
d1 = str(frame // 10000)
d2 = str(frame // 100)
path = os.path.join(d1, d2, str(frame) + '.jpg')
if base_dir:
path = os.path.join(base_dir, path)
return path
return 'unknown'
def _parse_labels(labels):
parsed_labels = OrderedDict()
last_label = ""
for token in shlex.split(labels):
if token[0] != "~" and token[0] != "@":
if token in parsed_labels:
raise ValueError("labels string is not corect. " +
"`{}` label is specified at least twice.".format(token))
def _copy_data_from_share(server_files, upload_dir):
job = rq.get_current_job()
job.meta['status'] = 'Data are being copied from share..'
job.save_meta()
parsed_labels[token] = {}
last_label = token
for path in server_files:
source_path = os.path.join(settings.SHARE_ROOT, os.path.normpath(path))
target_path = os.path.join(upload_dir, path)
if os.path.isdir(source_path):
copy_tree(source_path, target_path)
else:
attr = models.parse_attribute(token)
attr['text'] = token
if not attr['type'] in ['checkbox', 'radio', 'number', 'text', 'select']:
raise ValueError("labels string is not corect. " +
"`{}` attribute has incorrect type {}.".format(
attr['name'], attr['type']))
values = attr['values']
if attr['type'] == 'checkbox': # <prefix>checkbox=name:true/false
if not (len(values) == 1 and values[0] in ['true', 'false']):
raise ValueError("labels string is not corect. " +
"`{}` attribute has incorrect value.".format(attr['name']))
elif attr['type'] == 'number': # <prefix>number=name:min,max,step
try:
if len(values) != 3 or float(values[2]) <= 0 or \
float(values[0]) >= float(values[1]):
raise ValueError
except ValueError:
raise ValueError("labels string is not correct. " +
"`{}` attribute has incorrect format.".format(attr['name']))
if attr['name'] in parsed_labels[last_label]:
raise ValueError("labels string is not corect. " +
"`{}` attribute is specified at least twice.".format(attr['name']))
parsed_labels[last_label][attr['name']] = attr
return parsed_labels
def _parse_db_labels(db_labels):
result = []
for db_label in db_labels:
result += [db_label.name]
result += [attr.text for attr in db_label.attributespec_set.all()]
return _parse_labels(" ".join(result))
'''
Count all files, remove garbage (unknown mime types or extra dirs)
'''
def _prepare_paths(source_paths, target_paths, storage):
counters = {
"image": 0,
"directory": 0,
"video": 0,
"archive": 0
}
target_dir = os.path.dirname(target_path)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
shutil.copyfile(source_path, target_path)
share_dirs_mapping = {}
share_files_mapping = {}
if storage == 'local':
# Files were uploaded early. Remove trash if it exists. Count them.
for path in target_paths:
mime = _get_mime(path)
if mime in ['video', 'archive', 'image']:
counters[mime] += 1
else:
try:
os.remove(path)
except:
os.rmdir(path)
else:
# Files are available via mount share. Count them and separate dirs.
for source_path, target_path in zip(source_paths, target_paths):
mime = _get_mime(source_path)
if mime in ['directory', 'image', 'video', 'archive']:
counters[mime] += 1
if mime == 'directory':
share_dirs_mapping[source_path] = target_path
else:
share_files_mapping[source_path] = target_path
# Remove directories if other files from them exists in input paths
exclude = []
for dir_name in share_dirs_mapping.keys():
for patch in share_files_mapping.keys():
if dir_name in patch:
exclude.append(dir_name)
break
for excluded_dir in exclude:
del share_dirs_mapping[excluded_dir]
counters['directory'] = len(share_dirs_mapping.keys())
return (counters, share_dirs_mapping, share_files_mapping)
'''
Check file set on valid
Valid if:
1 video, 0 images and 0 dirs (interpolation mode)
1 archive, 0 images and 0 dirs (annotation mode)
Many images or many dirs with images (annotation mode), 0 archives and 0 videos
'''
def _valid_file_set(counters):
if (counters['image'] or counters['directory']) and (counters['video'] or counters['archive']):
return False
elif counters['video'] > 1 or (counters['video'] and (counters['archive'] or counters['image'] or counters['directory'])):
return False
elif counters['archive'] > 1 or (counters['archive'] and (counters['video'] or counters['image'] or counters['directory'])):
return False
return True
'''
Copy data from share to local
'''
def _copy_data_from_share(share_files_mapping, share_dirs_mapping):
for source_path in share_dirs_mapping:
copy_tree(source_path, share_dirs_mapping[source_path])
for source_path in share_files_mapping:
target_path = share_files_mapping[source_path]
target_dir = os.path.dirname(target_path)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
shutil.copyfile(source_path, target_path)
'''
Find and unpack archive in upload dir
'''
def _find_and_unpack_archive(upload_dir):
archive = None
for root, _, files in os.walk(upload_dir):
fullnames = map(lambda f: os.path.join(root, f), files)
archives = list(filter(lambda x: _get_mime(x) == 'archive', fullnames))
if len(archives):
archive = archives[0]
break
if archive:
Archive(archive).extractall(upload_dir)
os.remove(archive)
else:
raise Exception('Type defined as archive, but archives were not found.')
def _unpack_archive(archive, upload_dir):
job = rq.get_current_job()
job.meta['status'] = 'Archive is being unpacked..'
job.save_meta()
return archive
Archive(archive).extractall(upload_dir)
os.remove(archive)
def _copy_video_to_task(video, db_task):
job = rq.get_current_job()
job.meta['status'] = 'Video is being extracted..'
job.save_meta()
'''
Search a video in upload dir and split it by frames. Copy frames to target dirs
'''
def _find_and_extract_video(upload_dir, output_dir, db_task, compress_quality, flip_flag, job):
video = None
extractor = _FrameExtractor(video, db_task.image_quality)
for frame, image_orig_path in enumerate(extractor):
image_dest_path = db_task.get_frame_path(frame)
db_task.size += 1
dirname = os.path.dirname(image_dest_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
shutil.copyfile(image_orig_path, image_dest_path)
image = Image.open(db_task.get_frame_path(0))
models.Video.objects.create(task=db_task, path=video,
start_frame=0, stop_frame=db_task.size, step=1,
width=image.width, height=image.height)
image.close()
def _copy_images_to_task(upload_dir, db_task):
image_paths = []
for root, _, files in os.walk(upload_dir):
fullnames = map(lambda f: os.path.join(root, f), files)
videos = list(filter(lambda x: _get_mime(x) == 'video', fullnames))
if len(videos):
video = videos[0]
break
if video:
job.meta['status'] = 'Video is being extracted..'
job.save_meta()
extractor = _FrameExtractor(video, compress_quality, flip_flag)
for frame, image_orig_path in enumerate(extractor):
image_dest_path = _get_frame_path(frame, output_dir)
paths = map(lambda f: os.path.join(root, f), files)
paths = filter(lambda x: _get_mime(x) == 'image', paths)
image_paths.extend(paths)
image_paths.sort()
db_images = []
if len(image_paths):
job = rq.get_current_job()
for frame, image_orig_path in enumerate(image_paths):
progress = frame * 100 // len(image_paths)
job.meta['status'] = 'Images are being compressed.. {}%'.format(progress)
job.save_meta()
image_dest_path = db_task.get_frame_path(frame)
db_task.size += 1
dirname = os.path.dirname(image_dest_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
shutil.copyfile(image_orig_path, image_dest_path)
else:
raise Exception("Video files were not found")
return video
'''
Recursive search for all images in upload dir and compress it to RGB jpg with specified quality. Create symlinks for them.
'''
def _find_and_compress_images(upload_dir, output_dir, db_task, compress_quality, flip_flag, job):
filenames = []
for root, _, files in os.walk(upload_dir):
fullnames = map(lambda f: os.path.join(root, f), files)
images = filter(lambda x: _get_mime(x) == 'image', fullnames)
filenames.extend(images)
filenames.sort()
if len(filenames):
for idx, name in enumerate(filenames):
job.meta['status'] = 'Images are being compressed.. {}%'.format(idx * 100 // len(filenames))
job.save_meta()
compressed_name = os.path.splitext(name)[0] + '.jpg'
image = Image.open(name)
image = Image.open(image_orig_path)
# Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion
if image.mode == "I":
# Image mode is 32bit integer pixels.
@ -587,41 +219,40 @@ def _find_and_compress_images(upload_dir, output_dir, db_task, compress_quality,
im_data = im_data * (2**8 / im_data.max())
image = Image.fromarray(im_data.astype(np.int32))
image = image.convert('RGB')
if flip_flag:
image = image.transpose(Image.ROTATE_180)
image.save(compressed_name, quality=compress_quality, optimize=True)
image.save(image_dest_path, quality=db_task.image_quality, optimize=True)
db_images.append(models.Image(task=db_task, path=image_orig_path,
frame=frame, width=image.width, height=image.height))
image.close()
if compressed_name != name:
os.remove(name)
# PIL::save uses filename in order to define image extension.
# We need save it as jpeg for compression and after rename the file
# Else annotation file will contain invalid file names (with other extensions)
os.rename(compressed_name, name)
for frame, image_orig_path in enumerate(filenames):
image_dest_path = _get_frame_path(frame, output_dir)
image_orig_path = os.path.abspath(image_orig_path)
db_task.size += 1
dirname = os.path.dirname(image_dest_path)
if not os.path.exists(dirname):
os.makedirs(dirname)
os.symlink(image_orig_path, image_dest_path)
models.Image.objects.bulk_create(db_images)
else:
raise Exception("Image files were not found")
raise ValueError("Image files were not found")
def _save_task_to_db(db_task):
job = rq.get_current_job()
job.meta['status'] = 'Task is being saved in database'
job.save_meta()
segment_size = db_task.segment_size
segment_step = segment_size
if segment_size == 0:
segment_size = db_task.size
# Segment step must be more than segment_size + overlap in single-segment tasks
# Otherwise a task contains an extra segment
segment_step = sys.maxsize
return filenames
default_overlap = 5 if db_task.mode == 'interpolation' else 0
if db_task.overlap is None:
db_task.overlap = default_overlap
db_task.overlap = min(db_task.overlap, segment_size // 2)
def _save_task_to_db(db_task, task_params):
db_task.overlap = min(db_task.size, task_params['overlap'])
db_task.mode = task_params['mode']
db_task.z_order = task_params['z_order']
db_task.flipped = task_params['flip']
db_task.source = task_params['data']
segment_step -= db_task.overlap
segment_step = task_params['segment'] - db_task.overlap
for x in range(0, db_task.size, segment_step):
start_frame = x
stop_frame = min(x + task_params['segment'] - 1, db_task.size - 1)
stop_frame = min(x + segment_size - 1, db_task.size - 1)
slogger.glob.info("New segment for task #{}: start_frame = {}, \
stop_frame = {}".format(db_task.id, start_frame, stop_frame))
@ -635,87 +266,96 @@ def _save_task_to_db(db_task, task_params):
db_job.segment = db_segment
db_job.save()
parsed_labels = _parse_labels(task_params['labels'])
for label in parsed_labels:
db_label = models.Label()
db_label.task = db_task
db_label.name = label
db_label.save()
db_task.save()
def _validate_data(data):
share_root = settings.SHARE_ROOT
server_files = {
'dirs': [],
'files': [],
}
for path in data["server_files"]:
path = os.path.normpath(path).lstrip('/')
if '..' in path.split(os.path.sep):
raise ValueError("Don't use '..' inside file paths")
full_path = os.path.abspath(os.path.join(share_root, path))
if 'directory' == _get_mime(full_path):
server_files['dirs'].append(path)
else:
server_files['files'].append(path)
if os.path.commonprefix([share_root, full_path]) != share_root:
raise ValueError("Bad file path: " + path)
# Remove directories if other files from them exists in server files
data['server_files'] = server_files['files'] + [ dir_name for dir_name in server_files['dirs']
if not [ f_name for f_name in server_files['files'] if f_name.startswith(dir_name)]]
def count_files(file_mapping, counter):
archive = None
video = None
for rel_path, full_path in file_mapping.items():
mime = _get_mime(full_path)
counter[mime] += 1
if mime == "archive":
archive = rel_path
elif mime == "video":
video = rel_path
return video, archive
counter = {"image": 0, "video": 0, "archive": 0, "directory": 0}
client_video, client_archive = count_files(
file_mapping={ f:f for f in data['client_files']},
counter=counter,
)
for attr in parsed_labels[label]:
db_attrspec = models.AttributeSpec()
db_attrspec.label = db_label
db_attrspec.text = parsed_labels[label][attr]['text']
db_attrspec.save()
server_video, server_archive = count_files(
file_mapping={ f:os.path.abspath(os.path.join(share_root, f)) for f in data['server_files']},
counter=counter,
)
db_task.save()
num_videos = counter["video"]
num_archives = counter["archive"]
num_images = counter["image"] + counter["directory"]
if (num_videos > 1 or num_archives > 1 or
(num_videos == 1 and num_archives + num_images > 0) or
(num_archives == 1 and num_videos + num_images > 0) or
(num_images > 0 and num_archives + num_videos > 0)):
raise ValueError("Only one archive, one video or many images can be \
dowloaded simultaneously. {} image(s), {} dir(s), {} video(s), {} \
archive(s) found".format(counter['image'], counter['directory'],
counter['video'], counter['archive']))
return client_video or server_video, client_archive or server_archive
@plugin_decorator
@transaction.atomic
def _create_thread(tid, params):
def _create_thread(tid, data):
slogger.glob.info("create task #{}".format(tid))
job = rq.get_current_job()
db_task = models.Task.objects.select_for_update().get(pk=tid)
if db_task.size != 0:
raise NotImplementedError("Adding more data is not implemented")
upload_dir = db_task.get_upload_dirname()
output_dir = db_task.get_data_dirname()
video, archive = _validate_data(data)
counters, share_dirs_mapping, share_files_mapping = _prepare_paths(
params['SOURCE_PATHS'],
params['TARGET_PATHS'],
params['storage']
)
if data['server_files']:
_copy_data_from_share(data['server_files'], upload_dir)
if (not _valid_file_set(counters)):
raise Exception('Only one archive, one video or many images can be dowloaded simultaneously. \
{} image(s), {} dir(s), {} video(s), {} archive(s) found'.format(
counters['image'],
counters['directory'],
counters['video'],
counters['archive']
)
)
if params['storage'] == 'share':
job.meta['status'] = 'Data are being copied from share..'
job.save_meta()
_copy_data_from_share(share_files_mapping, share_dirs_mapping)
archive = None
if counters['archive']:
job.meta['status'] = 'Archive is being unpacked..'
job.save_meta()
archive = _find_and_unpack_archive(upload_dir)
# Define task mode and other parameters
task_params = {
'mode': 'annotation' if counters['image'] or counters['directory'] or counters['archive'] else 'interpolation',
'flip': params['flip_flag'].lower() == 'true',
'z_order': params['z_order'].lower() == 'true',
'compress': int(params.get('compress_quality', 50)),
'segment': int(params.get('segment_size', sys.maxsize)),
'labels': params['labels'],
}
task_params['overlap'] = int(params.get('overlap_size', 5 if task_params['mode'] == 'interpolation' else 0))
task_params['overlap'] = min(task_params['overlap'], task_params['segment'] - 1)
slogger.glob.info("Task #{} parameters: {}".format(tid, task_params))
if task_params['mode'] == 'interpolation':
video = _find_and_extract_video(upload_dir, output_dir, db_task,
task_params['compress'], task_params['flip'], job)
task_params['data'] = os.path.relpath(video, upload_dir)
if archive:
archive = os.path.join(upload_dir, archive)
_unpack_archive(archive, upload_dir)
if video:
db_task.mode = "interpolation"
video = os.path.join(upload_dir, video)
_copy_video_to_task(video, db_task)
else:
files =_find_and_compress_images(upload_dir, output_dir, db_task,
task_params['compress'], task_params['flip'], job)
if archive:
task_params['data'] = os.path.relpath(archive, upload_dir)
else:
task_params['data'] = '{} images: {}, ...'.format(len(files),
", ".join([os.path.relpath(x, upload_dir) for x in files[0:2]]))
db_task.mode = "annotation"
_copy_images_to_task(upload_dir, db_task)
slogger.glob.info("Founded frames {} for task #{}".format(db_task.size, tid))
_save_task_to_db(db_task)
job.meta['status'] = 'Task is being saved in database'
job.save_meta()
_save_task_to_db(db_task, task_params)

@ -22,7 +22,7 @@
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.resize.min.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.draggable.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/svg.select.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/defiant.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/defiant.min.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery-3.3.1.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery.fullscreen.js' %}"></script>
@ -58,6 +58,7 @@
<script type="text/javascript" src="{% static 'engine/js/shapeBuffer.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/shapeGrouper.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationSaver.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/annotationUI.js' %}"></script>
{% endblock %}

@ -37,7 +37,6 @@
{% compress js file cvat %}
{% block head_js_cvat %}
<script type="text/javascript" src="{% static 'engine/js/base.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/idGenerator.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/userConfig.js' %}"></script>
{% endblock %}
{% endcompress %}

@ -1,9 +0,0 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
from django.test import TestCase
# Create your tests here.

File diff suppressed because it is too large Load Diff

@ -3,26 +3,42 @@
#
# SPDX-License-Identifier: MIT
from django.urls import path
from django.urls import path, include
from . import views
from rest_framework import routers
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="CVAT REST API",
default_version='v1',
description="REST API for Computer Vision Annotation Tool (CVAT)",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="nikita.manovich@intel.com"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(permissions.IsAuthenticated,),
)
router = routers.DefaultRouter(trailing_slash=False)
router.register('tasks', views.TaskViewSet)
router.register('jobs', views.JobViewSet)
router.register('users', views.UserViewSet)
router.register('server', views.ServerViewSet, basename='server')
router.register('plugins', views.PluginViewSet)
urlpatterns = [
# Entry point for a client
path('', views.dispatch_request),
path('create/task', views.create_task),
path('get/task/<int:tid>/frame/<int:frame>', views.get_frame),
path('check/task/<int:tid>', views.check_task),
path('delete/task/<int:tid>', views.delete_task),
path('update/task/<int:tid>', views.update_task),
path('get/job/<int:jid>', views.get_job),
path('get/task/<int:tid>', views.get_task),
path('dump/annotation/task/<int:tid>', views.dump_annotation),
path('check/annotation/task/<int:tid>', views.check_annotation),
path('download/annotation/task/<int:tid>', views.download_annotation),
path('save/annotation/job/<int:jid>', views.save_annotation_for_job),
path('save/annotation/task/<int:tid>', views.save_annotation_for_task),
path('delete/annotation/task/<int:tid>', views.delete_annotation_for_task),
path('get/annotation/job/<int:jid>', views.get_annotation),
path('get/username', views.get_username),
path('save/exception/<int:jid>', views.catch_client_exception),
path('save/status/job/<int:jid>', views.save_job_status),
# documentation for API
path('api/swagger.<slug:format>$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('api/docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
# entry point for API
path('api/v1/', include((router.urls, 'cvat'), namespace='v1'))
]

@ -1,38 +1,46 @@
# Copyright (C) 2018 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os
import json
import re
import traceback
from ast import literal_eval
import shutil
from datetime import datetime
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect, render
from django.conf import settings
from rules.contrib.views import permission_required, objectgetter
from django.views.decorators.gzip import gzip_page
from sendfile import sendfile
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.renderers import JSONRenderer
from rest_framework import status
from rest_framework import viewsets
from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework import mixins
from django_filters import rest_framework as filters
import django_rq
from django.db import IntegrityError
from . import annotation, task, models
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
from cvat.apps.authentication.decorators import login_required
from requests.exceptions import RequestException
import logging
from .log import slogger, clogger
from cvat.apps.engine.models import StatusChoice
############################# High Level server API
@login_required
@permission_required(perm=['engine.job.access'],
fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def catch_client_exception(request, jid):
data = json.loads(request.body.decode('utf-8'))
for event in data['exceptions']:
clogger.job[jid].error(json.dumps(event))
return HttpResponse()
from cvat.apps.engine.models import StatusChoice, Task, Job, Plugin
from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
ExceptionSerializer, AboutSerializer, JobSerializer, ImageMetaSerializer,
RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer,
PluginSerializer, FileInfoSerializer, LogEventSerializer)
from django.contrib.auth.models import User
from cvat.apps.authentication import auth
from rest_framework.permissions import SAFE_METHODS
# Server REST API
@login_required
def dispatch_request(request):
"""An entry point to dispatch legacy requests"""
@ -45,289 +53,413 @@ def dispatch_request(request):
else:
return redirect('/dashboard/')
@login_required
@permission_required(perm=['engine.task.create'], raise_exception=True)
def create_task(request):
"""Create a new annotation task"""
db_task = None
params = request.POST.dict()
params['owner'] = request.user
slogger.glob.info("create task with params = {}".format(params))
try:
db_task = task.create_empty(params)
target_paths = []
source_paths = []
upload_dir = db_task.get_upload_dirname()
share_root = settings.SHARE_ROOT
if params['storage'] == 'share':
data_list = request.POST.getlist('data')
data_list.sort(key=len)
for share_path in data_list:
relpath = os.path.normpath(share_path).lstrip('/')
if '..' in relpath.split(os.path.sep):
raise Exception('Permission denied')
abspath = os.path.abspath(os.path.join(share_root, relpath))
if os.path.commonprefix([share_root, abspath]) != share_root:
raise Exception('Bad file path on share: ' + abspath)
source_paths.append(abspath)
target_paths.append(os.path.join(upload_dir, relpath))
class ServerViewSet(viewsets.ViewSet):
serializer_class = None
# To get nice documentation about ServerViewSet actions it is necessary
# to implement the method. By default, ViewSet doesn't provide it.
def get_serializer(self, *args, **kwargs):
pass
@staticmethod
@action(detail=False, methods=['GET'], serializer_class=AboutSerializer)
def about(request):
from cvat import __version__ as cvat_version
about = {
"name": "Computer Vision Annotation Tool",
"version": cvat_version,
"description": "CVAT is completely re-designed and re-implemented " +
"version of Video Annotation Tool from Irvine, California " +
"tool. It is free, online, interactive video and image annotation " +
"tool for computer vision. It is being used by our team to " +
"annotate million of objects with different properties. Many UI " +
"and UX decisions are based on feedbacks from professional data " +
"annotation team."
}
serializer = AboutSerializer(data=about)
if serializer.is_valid(raise_exception=True):
return Response(data=serializer.data)
@staticmethod
@action(detail=False, methods=['POST'], serializer_class=ExceptionSerializer)
def exception(request):
serializer = ExceptionSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
additional_info = {
"username": request.user.username,
"name": "Send exception",
}
message = JSONRenderer().render({**serializer.data, **additional_info}).decode('UTF-8')
jid = serializer.data.get("job_id")
tid = serializer.data.get("task_id")
if jid:
clogger.job[jid].error(message)
elif tid:
clogger.task[tid].error(message)
else:
clogger.glob.error(message)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@staticmethod
@action(detail=False, methods=['POST'], serializer_class=LogEventSerializer)
def logs(request):
serializer = LogEventSerializer(many=True, data=request.data)
if serializer.is_valid(raise_exception=True):
user = { "username": request.user.username }
for event in serializer.data:
message = JSONRenderer().render({**event, **user}).decode('UTF-8')
jid = event.get("job_id")
tid = event.get("task_id")
if jid:
clogger.job[jid].info(message)
elif tid:
clogger.task[tid].info(message)
else:
clogger.glob.info(message)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@staticmethod
@action(detail=False, methods=['GET'], serializer_class=FileInfoSerializer)
def share(request):
param = request.query_params.get('directory', '/')
if param.startswith("/"):
param = param[1:]
directory = os.path.abspath(os.path.join(settings.SHARE_ROOT, param))
if directory.startswith(settings.SHARE_ROOT) and os.path.isdir(directory):
data = []
content = os.scandir(directory)
for entry in content:
entry_type = None
if entry.is_file():
entry_type = "REG"
elif entry.is_dir():
entry_type = "DIR"
if entry_type:
data.append({"name": entry.name, "type": entry_type})
serializer = FileInfoSerializer(many=True, data=data)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
else:
data_list = request.FILES.getlist('data')
if len(data_list) > settings.LOCAL_LOAD_MAX_FILES_COUNT:
raise Exception('Too many files. Please use download via share')
common_size = 0
for f in data_list:
common_size += f.size
if common_size > settings.LOCAL_LOAD_MAX_FILES_SIZE:
raise Exception('Too many size. Please use download via share')
for data_file in data_list:
source_paths.append(data_file.name)
path = os.path.join(upload_dir, data_file.name)
target_paths.append(path)
with open(path, 'wb') as upload_file:
for chunk in data_file.chunks():
upload_file.write(chunk)
params['SOURCE_PATHS'] = source_paths
params['TARGET_PATHS'] = target_paths
task.create(db_task.id, params)
return JsonResponse({'tid': db_task.id})
except Exception as exc:
slogger.glob.error("cannot create task {}".format(params['task_name']), exc_info=True)
db_task.delete()
return HttpResponseBadRequest(str(exc))
return JsonResponse({'tid': db_task.id})
@login_required
#@permission_required(perm=['engine.task.access'],
# fn=objectgetter(models.Task, 'tid'), raise_exception=True)
# We have commented lines above because the objectgetter() will raise 404 error in
# cases when a task creating ends with an error. So an user don't get an actual reason of an error.
def check_task(request, tid):
"""Check the status of a task"""
try:
slogger.glob.info("check task #{}".format(tid))
response = task.check(tid)
except Exception as e:
slogger.glob.error("cannot check task #{}".format(tid), exc_info=True)
return HttpResponseBadRequest(str(e))
return JsonResponse(response)
@login_required
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def get_frame(request, tid, frame):
"""Stream corresponding from for the task"""
try:
# Follow symbol links if the frame is a link on a real image otherwise
# mimetype detection inside sendfile will work incorrectly.
path = os.path.realpath(task.get_frame_path(tid, frame))
return sendfile(request, path)
except Exception as e:
slogger.task[tid].error("cannot get frame #{}".format(frame), exc_info=True)
return HttpResponseBadRequest(str(e))
@login_required
@permission_required(perm=['engine.task.delete'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def delete_task(request, tid):
"""Delete the task"""
try:
slogger.glob.info("delete task #{}".format(tid))
task.delete(tid)
except Exception as e:
slogger.glob.error("cannot delete task #{}".format(tid), exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
@login_required
@permission_required(perm=['engine.task.change'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def update_task(request, tid):
"""Update labels for the task"""
try:
slogger.task[tid].info("update task request")
labels = request.POST['labels']
task.update(tid, labels)
except Exception as e:
slogger.task[tid].error("cannot update task", exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
@login_required
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def get_task(request, tid):
try:
slogger.task[tid].info("get task request")
response = task.get(tid)
except Exception as e:
slogger.task[tid].error("cannot get task", exc_info=True)
return HttpResponseBadRequest(str(e))
return JsonResponse(response, safe=False)
@login_required
@permission_required(perm=['engine.job.access'],
fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def get_job(request, jid):
try:
slogger.job[jid].info("get job #{} request".format(jid))
response = task.get_job(jid)
except Exception as e:
slogger.job[jid].error("cannot get job #{}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(e))
return JsonResponse(response, safe=False)
@login_required
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def dump_annotation(request, tid):
try:
slogger.task[tid].info("dump annotation request")
annotation.dump(tid, annotation.FORMAT_XML, request.scheme, request.get_host())
except Exception as e:
slogger.task[tid].error("cannot dump annotation", exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
@login_required
@gzip_page
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def check_annotation(request, tid):
try:
slogger.task[tid].info("check annotation")
response = annotation.check(tid)
except Exception as e:
slogger.task[tid].error("cannot check annotation", exc_info=True)
return HttpResponseBadRequest(str(e))
return JsonResponse(response)
return Response("{} is an invalid directory".format(param),
status=status.HTTP_400_BAD_REQUEST)
class TaskFilter(filters.FilterSet):
name = filters.CharFilter(field_name="name", lookup_expr="icontains")
owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
mode = filters.CharFilter(field_name="mode", lookup_expr="icontains")
status = filters.CharFilter(field_name="status", lookup_expr="icontains")
assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
class Meta:
model = Task
fields = ("id", "name", "owner", "mode", "status", "assignee")
class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
queryset = Task.objects.all().prefetch_related(
"label_set__attributespec_set",
"segment_set__job_set",
).order_by('-id')
serializer_class = TaskSerializer
search_fields = ("name", "owner__username", "mode", "status")
filterset_class = TaskFilter
ordering_fields = ("id", "name", "owner", "status", "assignee")
def get_permissions(self):
http_method = self.request.method
permissions = [IsAuthenticated]
if http_method in SAFE_METHODS:
permissions.append(auth.TaskAccessPermission)
elif http_method in ["POST"]:
permissions.append(auth.TaskCreatePermission)
elif http_method in ["PATCH", "PUT"]:
permissions.append(auth.TaskChangePermission)
elif http_method in ["DELETE"]:
permissions.append(auth.TaskDeletePermission)
else:
permissions.append(auth.AdminRolePermission)
return [perm() for perm in permissions]
@login_required
@gzip_page
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def download_annotation(request, tid):
try:
slogger.task[tid].info("get dumped annotation")
db_task = models.Task.objects.get(pk=tid)
response = sendfile(request, db_task.get_dump_path(), attachment=True,
attachment_filename='{}_{}.xml'.format(db_task.id, db_task.name))
except Exception as e:
slogger.task[tid].error("cannot get dumped annotation", exc_info=True)
return HttpResponseBadRequest(str(e))
return response
def perform_create(self, serializer):
if self.request.data.get('owner', None):
serializer.save()
else:
serializer.save(owner=self.request.user)
def perform_destroy(self, instance):
task_dirname = instance.get_task_dirname()
super().perform_destroy(instance)
shutil.rmtree(task_dirname, ignore_errors=True)
@staticmethod
@action(detail=True, methods=['GET'], serializer_class=JobSerializer)
def jobs(request, pk):
queryset = Job.objects.filter(segment__task_id=pk)
serializer = JobSerializer(queryset, many=True,
context={"request": request})
return Response(serializer.data)
@action(detail=True, methods=['POST'], serializer_class=TaskDataSerializer)
def data(self, request, pk):
db_task = self.get_object()
serializer = TaskDataSerializer(db_task, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
task.create(db_task.id, serializer.data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
@action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
serializer_class=LabeledDataSerializer)
def annotations(self, request, pk):
if request.method == 'GET':
data = annotation.get_task_data(pk, request.user)
serializer = LabeledDataSerializer(data=data)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
elif request.method == 'PUT':
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
data = annotation.put_task_data(pk, request.user, serializer.data)
return Response(data)
elif request.method == 'DELETE':
annotation.delete_task_data(pk, request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
elif request.method == 'PATCH':
action = self.request.query_params.get("action", None)
if action not in annotation.PatchAction.values():
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.patch_task_data(pk, request.user, serializer.data, action)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
@action(detail=True, methods=['GET'], serializer_class=None,
url_path='annotations/(?P<filename>[^/]+)')
def dump(self, request, pk, filename):
filename = re.sub(r'[\\/*?:"<>|]', '_', filename)
queue = django_rq.get_queue("default")
username = request.user.username
db_task = self.get_object()
timestamp = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
file_ext = request.query_params.get("format", "xml")
action = request.query_params.get("action")
if action not in [None, "download"]:
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
file_path = os.path.join(db_task.get_task_dirname(),
filename + ".{}.{}.".format(username, timestamp) + "xml")
rq_id = "{}@/api/v1/tasks/{}/annotations/{}".format(username, pk, filename)
rq_job = queue.fetch_job(rq_id)
if rq_job:
if rq_job.is_finished:
if not rq_job.meta.get("download"):
if action == "download":
rq_job.meta[action] = True
rq_job.save_meta()
return sendfile(request, rq_job.meta["file_path"], attachment=True,
attachment_filename=filename + "." + file_ext)
else:
return Response(status=status.HTTP_201_CREATED)
else: # Remove the old dump file
try:
os.remove(rq_job.meta["file_path"])
except OSError:
pass
finally:
rq_job.delete()
elif rq_job.is_failed:
rq_job.delete()
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
return Response(status=status.HTTP_202_ACCEPTED)
rq_job = queue.enqueue_call(func=annotation.dump_task_data,
args=(pk, request.user, file_path, request.scheme,
request.get_host(), request.query_params),
job_id=rq_id)
rq_job.meta["file_path"] = file_path
rq_job.save_meta()
return Response(status=status.HTTP_202_ACCEPTED)
@action(detail=True, methods=['GET'], serializer_class=RqStatusSerializer)
def status(self, request, pk):
response = self._get_rq_response(queue="default",
job_id="/api/{}/tasks/{}".format(request.version, pk))
serializer = RqStatusSerializer(data=response)
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
@staticmethod
def _get_rq_response(queue, job_id):
queue = django_rq.get_queue(queue)
job = queue.fetch_job(job_id)
response = {}
if job is None or job.is_finished:
response = { "state": "Finished" }
elif job.is_queued:
response = { "state": "Queued" }
elif job.is_failed:
response = { "state": "Failed", "message": job.exc_info }
else:
response = { "state": "Started" }
if 'status' in job.meta:
response['message'] = job.meta['status']
return response
@staticmethod
@action(detail=True, methods=['GET'], serializer_class=ImageMetaSerializer,
url_path='frames/meta')
def data_info(request, pk):
try:
db_task = models.Task.objects.get(pk=pk)
meta_cache_file = open(db_task.get_image_meta_cache_path())
except OSError:
task.make_image_meta_cache(db_task)
meta_cache_file = open(db_task.get_image_meta_cache_path())
data = literal_eval(meta_cache_file.read())
serializer = ImageMetaSerializer(many=True, data=data['original_size'])
if serializer.is_valid(raise_exception=True):
return Response(serializer.data)
@action(detail=True, methods=['GET'], serializer_class=None,
url_path='frames/(?P<frame>\d+)')
def frame(self, request, pk, frame):
"""Get a frame for the task"""
try:
# Follow symbol links if the frame is a link on a real image otherwise
# mimetype detection inside sendfile will work incorrectly.
db_task = self.get_object()
path = os.path.realpath(db_task.get_frame_path(frame))
return sendfile(request, path)
except Exception as e:
slogger.task[pk].error(
"cannot get frame #{}".format(frame), exc_info=True)
return HttpResponseBadRequest(str(e))
class JobViewSet(viewsets.GenericViewSet,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
queryset = Job.objects.all().order_by('id')
serializer_class = JobSerializer
def get_permissions(self):
http_method = self.request.method
permissions = [IsAuthenticated]
if http_method in SAFE_METHODS:
permissions.append(auth.JobAccessPermission)
elif http_method in ["PATCH", "PUT", "DELETE"]:
permissions.append(auth.JobChangePermission)
else:
permissions.append(auth.AdminRolePermission)
return [perm() for perm in permissions]
@action(detail=True, methods=['GET', 'DELETE', 'PUT', 'PATCH'],
serializer_class=LabeledDataSerializer)
def annotations(self, request, pk):
if request.method == 'GET':
data = annotation.get_job_data(pk, request.user)
return Response(data)
elif request.method == 'PUT':
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.put_job_data(pk, request.user, serializer.data)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
elif request.method == 'DELETE':
annotation.delete_job_data(pk, request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
elif request.method == 'PATCH':
action = self.request.query_params.get("action", None)
if action not in annotation.PatchAction.values():
raise serializers.ValidationError(
"Please specify a correct 'action' for the request")
serializer = LabeledDataSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
try:
data = annotation.patch_job_data(pk, request.user,
serializer.data, action)
except (AttributeError, IntegrityError) as e:
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
queryset = User.objects.all().order_by('id')
serializer_class = UserSerializer
def get_permissions(self):
permissions = [IsAuthenticated]
if self.action in ["self"]:
pass
else:
user = self.request.user
if self.action != "retrieve" or int(self.kwargs.get("pk", 0)) != user.id:
permissions.append(auth.AdminRolePermission)
return [perm() for perm in permissions]
@login_required
@gzip_page
@permission_required(perm=['engine.job.access'],
fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def get_annotation(request, jid):
try:
slogger.job[jid].info("get annotation for {} job".format(jid))
response = annotation.get(jid)
except Exception as e:
slogger.job[jid].error("cannot get annotation for job {}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(e))
return JsonResponse(response, safe=False)
@staticmethod
@action(detail=False, methods=['GET'], serializer_class=UserSerializer)
def self(request):
serializer = UserSerializer(request.user, context={ "request": request })
return Response(serializer.data)
@login_required
@permission_required(perm=['engine.job.change'],
fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def save_annotation_for_job(request, jid):
try:
slogger.job[jid].info("save annotation for {} job".format(jid))
data = json.loads(request.body.decode('utf-8'))
if 'annotation' in data:
annotation.save_job(jid, json.loads(data['annotation']))
if 'logs' in data:
for event in json.loads(data['logs']):
clogger.job[jid].info(json.dumps(event))
slogger.job[jid].info("annotation have been saved for the {} job".format(jid))
except RequestException as e:
slogger.job[jid].error("cannot send annotation logs for job {}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(e))
except Exception as e:
slogger.job[jid].error("cannot save annotation for job {}".format(jid), exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
class PluginViewSet(viewsets.ModelViewSet):
queryset = Plugin.objects.all()
serializer_class = PluginSerializer
@login_required
@permission_required(perm=['engine.task.change'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def save_annotation_for_task(request, tid):
try:
slogger.task[tid].info("save annotation request")
data = json.loads(request.body.decode('utf-8'))
annotation.save_task(tid, data)
except Exception as e:
slogger.task[tid].error("cannot save annotation", exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
# @action(detail=True, methods=['GET', 'PATCH', 'PUT'], serializer_class=None)
# def config(self, request, name):
# pass
@login_required
@permission_required(perm=['engine.task.change'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def delete_annotation_for_task(request, tid):
try:
slogger.task[tid].info("delete annotation request")
annotation.clear_task(tid)
except Exception as e:
slogger.task[tid].error("cannot delete annotation", exc_info=True)
return HttpResponseBadRequest(str(e))
# @action(detail=True, methods=['GET', 'POST'], serializer_class=None)
# def data(self, request, name):
# pass
return HttpResponse()
# @action(detail=True, methods=['GET', 'DELETE', 'PATCH', 'PUT'],
# serializer_class=None, url_path='data/(?P<id>\d+)')
# def data_detail(self, request, name, id):
# pass
@login_required
@permission_required(perm=['engine.job.change'],
fn=objectgetter(models.Job, 'jid'), raise_exception=True)
def save_job_status(request, jid):
try:
data = json.loads(request.body.decode('utf-8'))
status = data['status']
slogger.job[jid].info("changing job status request")
task.save_job_status(jid, status, request.user.username)
except Exception as e:
if jid:
slogger.job[jid].error("cannot change status", exc_info=True)
else:
slogger.glob.error("cannot change status", exc_info=True)
return HttpResponseBadRequest(str(e))
return HttpResponse()
@action(detail=True, methods=['GET', 'POST'], serializer_class=RqStatusSerializer)
def requests(self, request, name):
pass
@login_required
def get_username(request):
response = {'username': request.user.username}
return JsonResponse(response, safe=False)
@action(detail=True, methods=['GET', 'DELETE'],
serializer_class=RqStatusSerializer, url_path='requests/(?P<id>\d+)')
def request_detail(self, request, name, rq_id):
pass
def rq_handler(job, exc_type, exc_value, tb):
job.exc_info = "".join(traceback.format_exception_only(exc_type, exc_value))
job.exc_info = "".join(
traceback.format_exception_only(exc_type, exc_value))
job.save()
module = job.id.split('.')[0]
if module == 'task':
if "tasks" in job.id.split("/"):
return task.rq_handler(job, exc_type, exc_value, tb)
elif module == 'annotation':
return annotation.rq_handler(job, exc_type, exc_value, tb)
return True

@ -7,22 +7,23 @@ from django.utils import timezone
from cvat.apps.engine.log import slogger
from cvat.apps.engine.models import Task, Job, User
from cvat.apps.engine.annotation import _dump as dump, FORMAT_XML
from cvat.apps.engine.annotation import dump_task_data
from cvat.apps.engine.plugins import add_plugin
from cvat.apps.git.models import GitStatusChoice
from cvat.apps.git.models import GitData
from collections import OrderedDict
import subprocess
import django_rq
import datetime
import shutil
import json
import math
import git
import os
import re
import rq
def _have_no_access_exception(ex):
if 'Permission denied' in ex.stderr or 'Could not read from remote repository' in ex.stderr:
@ -39,35 +40,22 @@ def _have_no_access_exception(ex):
class Git:
__url = None
__path = None
__tid = None
__task_name = None
__branch_name = None
__user = None
__cwd = None
__rep = None
__diffs_dir = None
__annotation_file = None
__sync_date = None
__lfs = None
def __init__(self, db_git, tid, user):
self.__db_git = db_git
self.__url = db_git.url
self.__path = db_git.path
self.__tid = tid
self.__user = {
self._db_git = db_git
self._url = db_git.url
self._path = db_git.path
self._tid = tid
self._user = {
"name": user.username,
"email": user.email or "dummy@cvat.com"
}
self.__cwd = os.path.join(os.getcwd(), "data", str(tid), "repos")
self.__diffs_dir = os.path.join(os.getcwd(), "data", str(tid), "repos_diffs")
self.__task_name = re.sub(r'[\\/*?:"<>|\s]', '_', Task.objects.get(pk = tid).name)[:100]
self.__branch_name = 'cvat_{}_{}'.format(tid, self.__task_name)
self.__annotation_file = os.path.join(self.__cwd, self.__path)
self.__sync_date = db_git.sync_date
self.__lfs = db_git.lfs
self._cwd = os.path.join(os.getcwd(), "data", str(tid), "repos")
self._diffs_dir = os.path.join(os.getcwd(), "data", str(tid), "repos_diffs_v2")
self._task_name = re.sub(r'[\\/*?:"<>|\s]', '_', Task.objects.get(pk = tid).name)[:100]
self._branch_name = 'cvat_{}_{}'.format(tid, self._task_name)
self._annotation_file = os.path.join(self._cwd, self._path)
self._sync_date = db_git.sync_date
self._lfs = db_git.lfs
# Method parses an got URL.
@ -78,8 +66,8 @@ class Git:
http_pattern = "([https|http]+)*[://]*([a-zA-Z0-9._-]+.[a-zA-Z]+)/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)"
ssh_pattern = "([a-zA-Z0-9._-]+)@([a-zA-Z0-9._-]+):([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)"
http_match = re.match(http_pattern, self.__url)
ssh_match = re.match(ssh_pattern, self.__url)
http_match = re.match(http_pattern, self._url)
ssh_match = re.match(ssh_pattern, self._url)
user = "git"
host = None
@ -106,44 +94,43 @@ class Git:
# Method creates the main branch if repostory doesn't have any branches
def _create_master_branch(self):
if len(self.__rep.heads):
if len(self._rep.heads):
raise Exception("Some heads already exists")
readme_md_name = os.path.join(self.__cwd, "README.md")
readme_md_name = os.path.join(self._cwd, "README.md")
with open(readme_md_name, "w"):
pass
self.__rep.index.add([readme_md_name])
self.__rep.index.commit("CVAT Annotation. Initial commit by {} at {}".format(self.__user["name"], timezone.now()))
self._rep.index.add([readme_md_name])
self._rep.index.commit("CVAT Annotation. Initial commit by {} at {}".format(self._user["name"], timezone.now()))
self.__rep.git.push("origin", "master")
self._rep.git.push("origin", "master")
# Method creates task branch for repository from current master
def _to_task_branch(self):
# Remove user branch from local repository if it exists
if self.__branch_name not in list(map(lambda x: x.name, self.__rep.heads)):
self.__rep.create_head(self.__branch_name)
if self._branch_name not in list(map(lambda x: x.name, self._rep.heads)):
self._rep.create_head(self._branch_name)
self.__rep.head.reference = self.__rep.heads[self.__branch_name]
self._rep.head.reference = self._rep.heads[self._branch_name]
# Method setups a config file for current user
def _update_config(self):
slogger.task[self.__tid].info("User config initialization..")
with self.__rep.config_writer() as cw:
slogger.task[self._tid].info("User config initialization..")
with self._rep.config_writer() as cw:
if not cw.has_section("user"):
cw.add_section("user")
cw.set("user", "name", self.__user["name"])
cw.set("user", "email", self.__user["email"])
cw.set("user", "name", self._user["name"])
cw.set("user", "email", self._user["email"])
cw.release()
# Method initializes repos. It setup configuration, creates master branch if need and checkouts to task branch
def _configurate(self):
self._update_config()
if not len(self.__rep.heads):
if not len(self._rep.heads):
self._create_master_branch()
self._to_task_branch()
os.makedirs(self.__diffs_dir, exist_ok = True)
os.makedirs(self._diffs_dir, exist_ok = True)
def _ssh_url(self):
@ -153,12 +140,12 @@ class Git:
# Method clones a remote repos to the local storage using SSH and initializes it
def _clone(self):
os.makedirs(self.__cwd)
os.makedirs(self._cwd)
ssh_url = self._ssh_url()
# Cloning
slogger.task[self.__tid].info("Cloning remote repository from {}..".format(ssh_url))
self.__rep = git.Repo.clone_from(ssh_url, self.__cwd)
slogger.task[self._tid].info("Cloning remote repository from {}..".format(ssh_url))
self._rep = git.Repo.clone_from(ssh_url, self._cwd)
# Intitialization
self._configurate()
@ -168,13 +155,13 @@ class Git:
# It restores state if any errors have occured
# It useful if merge conflicts have occured during pull
def _reclone(self):
if os.path.exists(self.__cwd):
if not os.path.isdir(self.__cwd):
os.remove(self.__cwd)
if os.path.exists(self._cwd):
if not os.path.isdir(self._cwd):
os.remove(self._cwd)
else:
# Rename current repository dir
tmp_repo = os.path.abspath(os.path.join(self.__cwd, "..", "tmp_repo"))
os.rename(self.__cwd, tmp_repo)
tmp_repo = os.path.abspath(os.path.join(self._cwd, "..", "tmp_repo"))
os.rename(self._cwd, tmp_repo)
# Try clone repository
try:
@ -182,9 +169,9 @@ class Git:
shutil.rmtree(tmp_repo, True)
except Exception as ex:
# Restore state if any errors have occured
if os.path.isdir(self.__cwd):
shutil.rmtree(self.__cwd, True)
os.rename(tmp_repo, self.__cwd)
if os.path.isdir(self._cwd):
shutil.rmtree(self._cwd, True)
os.rename(tmp_repo, self._cwd)
raise ex
else:
self._clone()
@ -192,14 +179,14 @@ class Git:
# Method checkouts to master branch and pulls it from remote repos
def _pull(self):
self.__rep.head.reference = self.__rep.heads["master"]
self._rep.head.reference = self._rep.heads["master"]
try:
self.__rep.git.pull("origin", "master")
self._rep.git.pull("origin", "master")
if self.__branch_name in list(map(lambda x: x.name, self.__rep.heads)):
self.__rep.head.reference = self.__rep.heads["master"]
self.__rep.delete_head(self.__branch_name, force=True)
self.__rep.head.reset("HEAD", index=True, working_tree=True)
if self._branch_name in list(map(lambda x: x.name, self._rep.heads)):
self._rep.head.reference = self._rep.heads["master"]
self._rep.delete_head(self._branch_name, force=True)
self._rep.head.reset("HEAD", index=True, working_tree=True)
self._to_task_branch()
except git.exc.GitError:
@ -212,62 +199,42 @@ class Git:
def init_repos(self, wo_remote = False):
try:
# Try to use a local repos. It can throw GitError exception
self.__rep = git.Repo(self.__cwd)
self._rep = git.Repo(self._cwd)
self._configurate()
# Check if remote URL is actual
if self._ssh_url() != self.__rep.git.remote('get-url', '--all', 'origin'):
slogger.task[self.__tid].info("Local repository URL is obsolete.")
if self._ssh_url() != self._rep.git.remote('get-url', '--all', 'origin'):
slogger.task[self._tid].info("Local repository URL is obsolete.")
# We need reinitialize repository if it's false
raise git.exc.GitError("Actual and saved repository URLs aren't match")
except git.exc.GitError:
if wo_remote:
raise Exception('Local repository is failed')
slogger.task[self.__tid].info("Local repository initialization..")
shutil.rmtree(self.__cwd, True)
slogger.task[self._tid].info("Local repository initialization..")
shutil.rmtree(self._cwd, True)
self._clone()
# Method prepares an annotation, merges diffs and pushes it to remote repository to user branch
def push(self, scheme, host, format, last_save):
# Helpful function which merges diffs
def _accumulate(source, target, target_key):
if isinstance(source, dict):
if target_key is not None and target_key not in target:
target[target_key] = {}
for key in source:
if target_key is not None:
_accumulate(source[key], target[target_key], key)
else:
_accumulate(source[key], target, key)
elif isinstance(source, int):
if source:
if target_key is not None and target_key not in target:
target[target_key] = 0
target[target_key] += source
else:
raise Exception("Unhandled accumulate type: {}".format(type(source)))
def push(self, user, scheme, host, db_task, last_save):
# Update local repository
self._pull()
os.makedirs(os.path.join(self.__cwd, os.path.dirname(self.__annotation_file)), exist_ok = True)
os.makedirs(os.path.join(self._cwd, os.path.dirname(self._annotation_file)), exist_ok = True)
# Remove old annotation file if it exists
if os.path.exists(self.__annotation_file):
os.remove(self.__annotation_file)
if os.path.exists(self._annotation_file):
os.remove(self._annotation_file)
# Initialize LFS if need
if self.__lfs:
if self._lfs:
updated = False
lfs_settings = ["*.xml\tfilter=lfs diff=lfs merge=lfs -text\n", "*.zip\tfilter=lfs diff=lfs merge=lfs -text\n"]
if not os.path.isfile(os.path.join(self.__cwd, ".gitattributes")):
with open(os.path.join(self.__cwd, ".gitattributes"), "w") as gitattributes:
if not os.path.isfile(os.path.join(self._cwd, ".gitattributes")):
with open(os.path.join(self._cwd, ".gitattributes"), "w") as gitattributes:
gitattributes.writelines(lfs_settings)
updated = True
else:
with open(os.path.join(self.__cwd, ".gitattributes"), "r+") as gitattributes:
with open(os.path.join(self._cwd, ".gitattributes"), "r+") as gitattributes:
lines = gitattributes.readlines()
for setting in lfs_settings:
if setting not in lines:
@ -278,100 +245,118 @@ class Git:
gitattributes.truncate()
if updated:
self.__rep.git.add(['.gitattributes'])
self._rep.git.add(['.gitattributes'])
# Dump an annotation
dump(self.__tid, format, scheme, host, OrderedDict())
dump_name = Task.objects.get(pk = self.__tid).get_dump_path()
# TODO: Fix dump, query params
timestamp = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
dump_name = os.path.join(db_task.get_task_dirname(),
"git_annotation_{}.".format(timestamp) + "dump")
dump_task_data(
pk=self._tid,
user=user,
file_path=dump_name,
scheme=scheme,
host=host,
query_params={},
)
ext = os.path.splitext(self.__path)[1]
ext = os.path.splitext(self._path)[1]
if ext == '.zip':
subprocess.call('zip -j -r "{}" "{}"'.format(self.__annotation_file, dump_name), shell=True)
subprocess.call('zip -j -r "{}" "{}"'.format(self._annotation_file, dump_name), shell=True)
elif ext == '.xml':
shutil.copyfile(dump_name, self.__annotation_file)
shutil.copyfile(dump_name, self._annotation_file)
else:
raise Exception("Got unknown annotation file type")
self.__rep.git.add(self.__annotation_file)
os.remove(dump_name)
self._rep.git.add(self._annotation_file)
# Merge diffs
summary_diff = {}
for diff_name in list(map(lambda x: os.path.join(self.__diffs_dir, x), os.listdir(self.__diffs_dir))):
for diff_name in list(map(lambda x: os.path.join(self._diffs_dir, x), os.listdir(self._diffs_dir))):
with open(diff_name, 'r') as f:
diff = json.loads(f.read())
_accumulate(diff, summary_diff, None)
for key in diff:
if key not in summary_diff:
summary_diff[key] = 0
summary_diff[key] += diff[key]
message = "CVAT Annotation updated by {}. \n".format(self._user["name"])
message += 'Task URL: {}://{}/dashboard?id={}\n'.format(scheme, host, db_task.id)
if db_task.bug_tracker:
message += 'Bug Tracker URL: {}\n'.format(db_task.bug_tracker)
message += "Created: {}, updated: {}, deleted: {}\n".format(
summary_diff["create"],
summary_diff["update"],
summary_diff["delete"]
)
message += "Annotation time: {} hours\n".format(math.ceil((last_save - self._sync_date).total_seconds() / 3600))
message += "Total annotation time: {} hours".format(math.ceil((last_save - db_task.created_date).total_seconds() / 3600))
self.__rep.index.commit("CVAT Annotation updated by {}. Summary: {}".format(self.__user["name"], str(summary_diff)))
self.__rep.git.push("origin", self.__branch_name, "--force")
self._rep.index.commit(message)
self._rep.git.push("origin", self._branch_name, "--force")
shutil.rmtree(self.__diffs_dir, True)
shutil.rmtree(self._diffs_dir, True)
# Method checks status of repository annotation
def remote_status(self, last_save):
# Check repository exists and archive exists
if not os.path.isfile(self.__annotation_file) or last_save != self.__sync_date:
if not os.path.isfile(self._annotation_file) or last_save != self._sync_date:
return GitStatusChoice.NON_SYNCED
else:
self.__rep.git.update_ref('-d', 'refs/remotes/origin/{}'.format(self.__branch_name))
self.__rep.git.remote('-v', 'update')
self._rep.git.update_ref('-d', 'refs/remotes/origin/{}'.format(self._branch_name))
self._rep.git.remote('-v', 'update')
last_hash = self.__rep.git.show_ref('refs/heads/{}'.format(self.__branch_name), '--hash')
merge_base_hash = self.__rep.merge_base('refs/remotes/origin/master', self.__branch_name)[0].hexsha
last_hash = self._rep.git.show_ref('refs/heads/{}'.format(self._branch_name), '--hash')
merge_base_hash = self._rep.merge_base('refs/remotes/origin/master', self._branch_name)[0].hexsha
if last_hash == merge_base_hash:
return GitStatusChoice.MERGED
else:
try:
self.__rep.git.show_ref('refs/remotes/origin/{}'.format(self.__branch_name), '--hash')
self._rep.git.show_ref('refs/remotes/origin/{}'.format(self._branch_name), '--hash')
return GitStatusChoice.SYNCED
except git.exc.GitCommandError:
# Remote branch has been deleted w/o merge
return GitStatusChoice.NON_SYNCED
def _initial_create(tid, params):
if 'git_path' in params:
def initial_create(tid, git_path, lfs, user):
try:
db_task = Task.objects.get(pk = tid)
path_pattern = r"\[(.+)\]"
path_search = re.search(path_pattern, git_path)
path = None
if path_search is not None:
path = path_search.group(1)
git_path = git_path[0:git_path.find(path) - 1].strip()
path = os.path.join('/', path.strip())
else:
anno_file = re.sub(r'[\\/*?:"<>|\s]', '_', db_task.name)[:100]
path = '/annotation/{}.zip'.format(anno_file)
path = path[1:]
_split = os.path.splitext(path)
if len(_split) < 2 or _split[1] not in [".xml", ".zip"]:
raise Exception("Only .xml and .zip formats are supported")
db_git = GitData()
db_git.url = git_path
db_git.path = path
db_git.task = db_task
db_git.lfs = lfs
try:
job = rq.get_current_job()
job.meta['status'] = 'Cloning a repository..'
job.save_meta()
user = params['owner']
git_path = params['git_path']
db_task = Task.objects.get(pk = tid)
path_pattern = r"\[(.+)\]"
path_search = re.search(path_pattern, git_path)
path = None
if path_search is not None:
path = path_search.group(1)
git_path = git_path[0:git_path.find(path) - 1].strip()
path = os.path.join('/', path.strip())
else:
anno_file = re.sub(r'[\\/*?:"<>|\s]', '_', db_task.name)[:100]
path = '/annotation/{}.zip'.format(anno_file)
path = path[1:]
_split = os.path.splitext(path)
if len(_split) < 2 or _split[1] not in [".xml", ".zip"]:
raise Exception("Only .xml and .zip formats are supported")
db_git = GitData()
db_git.url = git_path
db_git.path = path
db_git.task = db_task
db_git.lfs = params["use_lfs"].lower() == "true"
try:
_git = Git(db_git, tid, user)
_git.init_repos()
db_git.save()
except git.exc.GitCommandError as ex:
_have_no_access_exception(ex)
except Exception as ex:
slogger.task[tid].exception('exception occured during git _initial_create', exc_info = True)
raise ex
_git = Git(db_git, tid, db_task.owner)
_git.init_repos()
db_git.save()
except git.exc.GitCommandError as ex:
_have_no_access_exception(ex)
except Exception as ex:
slogger.task[tid].exception('exception occured during git initial_create', exc_info = True)
raise ex
@transaction.atomic
@ -382,7 +367,7 @@ def push(tid, user, scheme, host):
try:
_git = Git(db_git, tid, user)
_git.init_repos()
_git.push(scheme, host, FORMAT_XML, db_task.updated_date)
_git.push(user, scheme, host, db_task, db_task.updated_date)
# Update timestamp
db_git.sync_date = db_task.updated_date
@ -441,33 +426,41 @@ def update_states():
slogger.glob("Exception occured during a status updating for db_git with tid: {}".format(db_git.task_id))
@transaction.atomic
def _onsave(jid, data):
def _onsave(jid, user, data, action):
db_task = Job.objects.select_related('segment__task').get(pk = jid).segment.task
try:
db_git = GitData.objects.select_for_update().get(pk = db_task.id)
diff_dir = os.path.join(os.getcwd(), "data", str(db_task.id), "repos_diffs")
os.makedirs(diff_dir, exist_ok = True)
diff_dir_v2 = os.path.join(os.getcwd(), "data", str(db_task.id), "repos_diffs_v2")
updated = sum([ len(data["update"][key]) for key in data["update"] ])
deleted = sum([ len(data["delete"][key]) for key in data["delete"] ])
created = sum([ len(data["create"][key]) for key in data["create"] ])
summary = {
"update": 0,
"create": 0,
"delete": 0
}
if os.path.isdir(diff_dir) and not os.path.isdir(diff_dir_v2):
diff_files = list(map(lambda x: os.path.join(diff_dir, x), os.listdir(diff_dir)))
for diff_file in diff_files:
diff_file = open(diff_file, 'r')
diff = json.loads(diff_file.read())
if updated or deleted or created:
diff = {
"update": {key: len(data["update"][key]) for key in data["update"].keys()},
"delete": {key: len(data["delete"][key]) for key in data["delete"].keys()},
"create": {key: len(data["create"][key]) for key in data["create"].keys()}
}
for action_key in diff:
summary[action_key] += sum([diff[action_key][key] for key in diff[action_key]])
diff_files = list(map(lambda x: os.path.join(diff_dir, x), os.listdir(diff_dir)))
os.makedirs(diff_dir_v2, exist_ok = True)
summary[action] += sum([len(data[key]) for key in ['shapes', 'tracks', 'tags']])
if summary["update"] or summary["create"] or summary["delete"]:
diff_files = list(map(lambda x: os.path.join(diff_dir_v2, x), os.listdir(diff_dir_v2)))
last_num = 0
for f in diff_files:
number = os.path.splitext(os.path.basename(f))[0]
number = int(number) if number.isdigit() else last_num
last_num = max(last_num, number)
with open(os.path.join(diff_dir, "{}.diff".format(last_num + 1)), 'w') as f:
f.write(json.dumps(diff))
with open(os.path.join(diff_dir_v2, "{}.diff".format(last_num + 1)), 'w') as f:
f.write(json.dumps(summary))
db_git.status = GitStatusChoice.NON_SYNCED
db_git.save()
@ -475,7 +468,7 @@ def _onsave(jid, data):
except GitData.DoesNotExist:
pass
def _ondump(tid, data_format, scheme, host, plugin_meta_data):
def _ondump(tid, user, data_format, scheme, host, plugin_meta_data):
db_task = Task.objects.get(pk = tid)
try:
db_git = GitData.objects.get(pk = db_task)
@ -486,6 +479,7 @@ def _ondump(tid, data_format, scheme, host, plugin_meta_data):
except GitData.DoesNotExist:
pass
add_plugin("save_job", _onsave, "after", exc_ok = False)
add_plugin("_create_thread", _initial_create, "before", exc_ok = False)
add_plugin("_dump", _ondump, "before", exc_ok = False)
add_plugin("patch_job_data", _onsave, "after", exc_ok = False)
# TODO: Append git repository into dump file
# add_plugin("_dump", _ondump, "before", exc_ok = False)

@ -6,247 +6,262 @@
/* global
showMessage:false
DashboardView:false
*/
"use strict";
window.cvat = window.cvat || {};
window.cvat.dashboard = window.cvat.dashboard || {};
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
window.cvat.dashboard.uiCallbacks.push(function(newElements) {
$.ajax({
type: "GET",
url: "/git/repository/meta/get",
success: (data) => {
newElements.each(function(idx) {
let elem = $(newElements[idx]);
let tid = +elem.attr("id").split("_")[1];
if (tid in data) {
if (["sync", "syncing"].includes(data[tid])) {
elem.css("background", "floralwhite");
}
else if (data[tid] === "merged") {
elem.css("background", "azure");
}
else {
elem.css("background", "mistyrose");
}
// GIT ENTRYPOINT
window.addEventListener('dashboardReady', () => {
const reposWindowId = 'gitReposWindow';
const closeReposWindowButtonId = 'closeGitReposButton';
const reposURLTextId = 'gitReposURLText';
const reposSyncButtonId = 'gitReposSyncButton';
const labelStatusId = 'gitReposLabelStatus';
const labelMessageId = 'gitReposLabelMessage';
const createURLInputTextId = 'gitCreateURLInputText';
const lfsCheckboxId = 'gitLFSCheckbox';
$("<button> Git Repository Sync </button>").addClass("regular dashboardButtonUI").on("click", () => {
let gitDialogWindow = $(`#${window.cvat.git.reposWindowId}`);
gitDialogWindow.attr("current_tid", tid);
gitDialogWindow.removeClass("hidden");
window.cvat.git.updateState();
}).appendTo(elem.find("div.dashboardButtonsUI")[0]);
const reposWindowTemplate = `
<div id="${reposWindowId}" class="modal">
<div style="width: 700px; height: auto;" class="modal-content">
<div style="width: 100%; height: 60%; overflow-y: auto;">
<table style="width: 100%;">
<tr>
<td style="width: 20%;">
<label class="regular h2"> Repository URL: </label>
</td>
<td style="width: 80%;" colspan="2">
<input class="regular h2" type="text" style="width: 92%;" id="${reposURLTextId}" readonly/>
</td>
</td>
<tr>
<td style="width: 20%;">
<label class="regular h2"> Status: </label>
</td>
<td style="width: 60%;">
<div>
<label class="regular h2" id="${labelStatusId}"> </label>
<label class="regular h2" id="${labelMessageId}" style="word-break: break-word; user-select: text;"> </label>
</div>
</td>
<td style="width: 20%;">
<button style="width: 70%;" id="${reposSyncButtonId}" class="regular h2"> Sync </button>
</td>
</tr>
</table>
</div>
<center>
<button id="${closeReposWindowButtonId}" class="regular h1" style="margin-top: 15px;"> Close </button>
</center>
</div>
</div>`;
$.get('/git/repository/meta/get').done((gitData) => {
const dashboardItems = $('.dashboardItem');
dashboardItems.each(function setupDashboardItem() {
const tid = +this.getAttribute('tid');
if (tid in gitData) {
if (['sync', 'syncing'].includes(gitData[tid])) {
this.style.background = 'floralwhite';
} else if (gitData[tid] === 'merged') {
this.style.background = 'azure';
} else {
this.style.background = 'mistyrose';
}
});
},
error: (data) => {
let message = `Can not get git repositories meta info. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
showMessage(message);
throw Error(message);
}
});
$('<button> Git Repository Sync </button>').addClass('regular dashboardButtonUI').on('click', () => {
$(`#${reposWindowId}`).remove();
const gitWindow = $(reposWindowTemplate).appendTo('body');
const closeReposWindowButton = $(`#${closeReposWindowButtonId}`);
const reposSyncButton = $(`#${reposSyncButtonId}`);
const gitLabelMessage = $(`#${labelMessageId}`);
const gitLabelStatus = $(`#${labelStatusId}`);
const reposURLText = $(`#${reposURLTextId}`);
});
function updateState() {
reposURLText.attr('placeholder', 'Waiting for server response..');
reposURLText.prop('value', '');
gitLabelMessage.css('color', '#cccc00').text('Waiting for server response..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
reposSyncButton.attr('disabled', true);
window.cvat.git = {
reposWindowId: "gitReposWindow",
closeReposWindowButtonId: "closeGitReposButton",
reposURLTextId: "gitReposURLText",
reposSyncButtonId: "gitReposSyncButton",
labelStatusId: "gitReposLabelStatus",
labelMessageId: "gitReposLabelMessage",
createURLInputTextId: "gitCreateURLInputText",
lfsCheckboxId: "gitLFSCheckbox",
updateState: () => {
let gitWindow = $(`#${window.cvat.git.reposWindowId}`);
let gitLabelMessage = $(`#${window.cvat.git.labelMessageId}`);
let gitLabelStatus = $(`#${window.cvat.git.labelStatusId}`);
let reposURLText = $(`#${window.cvat.git.reposURLTextId}`);
let syncButton = $(`#${window.cvat.git.reposSyncButtonId}`);
reposURLText.attr("placeholder", "Waiting for server response..");
reposURLText.prop("value", "");
gitLabelMessage.css("color", "#cccc00").text("Waiting for server response..");
gitLabelStatus.css("color", "#cccc00").text("\u25cc");
syncButton.attr("disabled", true);
let tid = gitWindow.attr("current_tid");
$.get(`/git/repository/get/${tid}`).done(function(data) {
if (!data.url.value) {
gitLabelMessage.css("color", "black").text("Repository is not attached");
reposURLText.prop("value", "");
reposURLText.attr("placeholder", "Repository is not attached");
return;
}
$.get(`/git/repository/get/${tid}`).done((data) => {
reposURLText.attr('placeholder', '');
reposURLText.prop('value', data.url.value);
reposURLText.attr("placeholder", "");
reposURLText.prop("value", data.url.value);
if (!data.status.value) {
gitLabelStatus.css('color', 'red').text('\u26a0');
gitLabelMessage.css('color', 'red').text(data.status.error);
reposSyncButton.attr('disabled', false);
return;
}
if (!data.status.value) {
gitLabelStatus.css("color", "red").text("\u26a0");
gitLabelMessage.css("color", "red").text(data.status.error);
syncButton.attr("disabled", false);
return;
}
if (data.status.value === '!sync') {
gitLabelStatus.css('color', 'red').text('\u2606');
gitLabelMessage.css('color', 'red').text('Repository is not synchronized');
reposSyncButton.attr('disabled', false);
} else if (data.status.value === 'sync') {
gitLabelStatus.css('color', '#cccc00').text('\u2605');
gitLabelMessage.css('color', 'black').text('Synchronized (merge required)');
} else if (data.status.value === 'merged') {
gitLabelStatus.css('color', 'darkgreen').text('\u2605');
gitLabelMessage.css('color', 'darkgreen').text('Synchronized');
} else if (data.status.value === 'syncing') {
gitLabelMessage.css('color', '#cccc00').text('Synchronization..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
} else {
const message = `Got unknown repository status: ${data.status.value}`;
gitLabelStatus.css('color', 'red').text('\u26a0');
gitLabelMessage.css('color', 'red').text(message);
}
}).fail((data) => {
gitWindow.remove();
const message = 'Error occured during get an repos status. '
+ `Code: ${data.status}, text: ${data.responseText || data.statusText}`;
showMessage(message);
});
}
if (data.status.value === "!sync") {
gitLabelStatus.css("color", "red").text("\u2606");
gitLabelMessage.css("color", "red").text("Repository is not synchronized");
syncButton.attr("disabled", false);
}
else if (data.status.value === "sync") {
gitLabelStatus.css("color", "#cccc00").text("\u2605");
gitLabelMessage.css("color", "black").text("Synchronized (merge required)");
}
else if (data.status.value === "merged") {
gitLabelStatus.css("color", "darkgreen").text("\u2605");
gitLabelMessage.css("color", "darkgreen").text("Synchronized");
}
else if (data.status.value === "syncing") {
gitLabelMessage.css("color", "#cccc00").text("Synchronization..");
gitLabelStatus.css("color", "#cccc00").text("\u25cc");
}
else {
let message = `Got unknown repository status: ${data.status.value}`;
gitLabelStatus.css("color", "red").text("\u26a0");
gitLabelMessage.css("color", "red").text(message);
throw Error(message);
closeReposWindowButton.on('click', () => {
gitWindow.remove();
});
reposSyncButton.on('click', () => {
function badResponse(message) {
try {
showMessage(message);
throw Error(message);
} finally {
gitWindow.remove();
}
}
gitLabelMessage.css('color', '#cccc00').text('Synchronization..');
gitLabelStatus.css('color', '#cccc00').text('\u25cc');
reposSyncButton.attr('disabled', true);
$.get(`/git/repository/push/${tid}`).done((rqData) => {
function checkCallback() {
$.get(`/git/repository/check/${rqData.rq_id}`).done((statusData) => {
if (['queued', 'started'].includes(statusData.status)) {
setTimeout(checkCallback, 1000);
} else if (statusData.status === 'finished') {
updateState();
} else if (statusData.status === 'failed') {
const message = `Can not push to remote repository. Message: ${statusData.stderr}`;
badResponse(message);
} else {
const message = `Check returned status "${statusData.status}".`;
badResponse(message);
}
}).fail((errorData) => {
const message = 'Errors occured during pushing an repos entry. '
+ `Code: ${errorData.status}, text: ${errorData.responseText || errorData.statusText}`;
badResponse(message);
});
}
setTimeout(checkCallback, 1000);
}).fail((errorData) => {
const message = 'Errors occured during pushing an repos entry. '
+ `Code: ${errorData.status}, text: ${errorData.responseText || errorData.statusText}`;
badResponse(message);
});
});
updateState();
}).appendTo($(this).find('div.dashboardButtonsUI')[0]);
}
}).fail(function(data) {
gitWindow.addClass("hidden");
let message = `Error occured during get an repos status. ` +
`Code: ${data.status}, text: ${data.responseText || data.statusText}`;
showMessage(message);
throw Error(message);
});
},
};
}).fail((errorData) => {
const message = `Can not get repository meta information. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
// Setup the "Create task" dialog
const title = 'Field for a repository URL and a relative path inside the repository. \n'
+ 'Default repository path is `annotation/<dump_file_name>.zip`. \n'
+ 'There are .zip or .xml extenstions are supported.';
const placeh = 'github.com/user/repos [annotation/<dump_file_name>.zip]';
document.addEventListener("DOMContentLoaded", () => {
$(`
<tr>
<td> <label class="regular h2"> Dataset Repository: </label> </td>
<td> <input type="text" id="${window.cvat.git.createURLInputTextId}" class="regular"` +
`style="width: 90%", placeholder="github.com/user/repos [annotation/<dump_file_name>.zip]" ` +
`title = "Field for a repository URL and a relative path inside the repository. Default repository path is 'annotation/<dump_file_name>.zip'. There are .zip or .xml extenstions are supported."/>` +
`</td>
<td>
<input type="text" id="${createURLInputTextId}" class="regular" style="width: 90%", placeholder="${placeh}" title="${title}"/>
</td>
</tr>
<tr>
<td> <label class="regular h2" checked> Use LFS: </label> </td>
<td> <input type="checkbox" checked id="${window.cvat.git.lfsCheckboxId}" </td>
</tr>`
).insertAfter($("#dashboardBugTrackerInput").parent().parent());
// Wrap create task request function
let originalCreateTaskRequest = window.createTaskRequest;
window.createTaskRequest = function(oData, onSuccessRequest, onSuccessCreate, onError, onComplete, onUpdateStatus) {
let gitPath = $(`#${window.cvat.git.createURLInputTextId}`).prop("value").replace(/\s/g, "");
if (gitPath.length) {
oData.append("git_path", gitPath);
oData.append("use_lfs", $(`#${window.cvat.git.lfsCheckboxId}`).prop("checked"));
}
originalCreateTaskRequest(oData, onSuccessRequest, onSuccessCreate, onError, onComplete, onUpdateStatus);
};
/* GIT MODAL WINDOW PLUGIN PART */
$(`<div id="${window.cvat.git.reposWindowId}" class="modal hidden">
<div style="width: 700px; height: auto;" class="modal-content">
<div style="width: 100%; height: 60%; overflow-y: auto;">
<table style="width: 100%;">
<tr>
<td style="width: 20%;">
<label class="regular h2"> Repository URL: </label>
</td>
<td style="width: 80%;" colspan="2">
<input class="regular h2" type="text" style="width: 92%;" id="${window.cvat.git.reposURLTextId}" readonly/>
</td>
</td>
<tr>
<td style="width: 20%;">
<label class="regular h2"> Status: </label>
</td>
<td style="width: 60%;">
<div>
<label class="regular h2" id="${window.cvat.git.labelStatusId}"> </label>
<label class="regular h2" id="${window.cvat.git.labelMessageId}" style="word-break: break-word; user-select: text;"> </label>
</div>
</td>
<td style="width: 20%;">
<button style="width: 70%;" id="${window.cvat.git.reposSyncButtonId}" class="regular h2"> Sync </button>
</td>
</tr>
</table>
</div>
<center>
<button id="${window.cvat.git.closeReposWindowButtonId}" class="regular h1" style="margin-top: 15px;"> Close </button>
</center>
</div>
</div>`).appendTo("body");
let gitWindow = $(`#${window.cvat.git.reposWindowId}`);
let closeRepositoryWindowButton = $(`#${window.cvat.git.closeReposWindowButtonId}`);
let repositorySyncButton = $(`#${window.cvat.git.reposSyncButtonId}`);
let gitLabelMessage = $(`#${window.cvat.git.labelMessageId}`);
let gitLabelStatus = $(`#${window.cvat.git.labelStatusId}`);
closeRepositoryWindowButton.on("click", () => {
gitWindow.addClass("hidden");
});
<td> <input type="checkbox" checked id="${lfsCheckboxId}" </td>
</tr>`).insertAfter($('#dashboardBugTrackerInput').parent().parent());
repositorySyncButton.on("click", () => {
function badResponse(message) {
try {
showMessage(message);
throw Error(message);
}
finally {
window.cvat.git.updateState();
}
}
gitLabelMessage.css("color", "#cccc00").text("Synchronization..");
gitLabelStatus.css("color", "#cccc00").text("\u25cc");
repositorySyncButton.attr("disabled", true);
DashboardView.registerDecorator('createTask', (taskData, next, onFault) => {
const taskMessage = $('#dashboardCreateTaskMessage');
let tid = gitWindow.attr("current_tid");
$.get(`/git/repository/push/${tid}`).done((data) => {
setTimeout(timeoutCallback, 1000);
const path = $(`#${createURLInputTextId}`).prop('value').replace(/\s/g, '');
const lfs = $(`#${lfsCheckboxId}`).prop('checked');
function timeoutCallback() {
$.get(`/git/repository/check/${data.rq_id}`).done((data) => {
if (["finished", "failed", "unknown"].indexOf(data.status) != -1) {
if (data.status === "failed") {
let message = data.error;
badResponse(message);
}
else if (data.status === "unknown") {
let message = `Request for pushing returned status "${data.status}".`;
badResponse(message);
}
else {
window.cvat.git.updateState();
if (path.length) {
taskMessage.css('color', 'blue');
taskMessage.text('Git repository is being cloned..');
$.ajax({
url: `/git/repository/create/${taskData.id}`,
type: 'POST',
data: JSON.stringify({
path,
lfs,
tid: taskData.id,
}),
contentType: 'application/json',
}).done((rqData) => {
function checkCallback() {
$.ajax({
url: `/git/repository/check/${rqData.rq_id}`,
type: 'GET',
}).done((statusData) => {
if (['queued', 'started'].includes(statusData.status)) {
setTimeout(checkCallback, 1000);
} else if (statusData.status === 'finished') {
taskMessage.css('color', 'blue');
taskMessage.text('Git repository has been cloned');
next();
} else if (statusData.status === 'failed') {
let message = 'Repository status check failed. ';
if (statusData.stderr) {
message += statusData.stderr;
}
taskMessage.css('color', 'red');
taskMessage.text(message);
onFault();
} else {
const message = `Repository status check returned the status "${statusData.status}"`;
taskMessage.css('color', 'red');
taskMessage.text(message);
onFault();
}
}
else {
setTimeout(timeoutCallback, 1000);
}
}).fail((data) => {
let message = `Error was occured during pushing an repos entry. ` +
`Code: ${data.status}, text: ${data.responseText || data.statusText}`;
badResponse(message);
});
}
}).fail((data) => {
let message = `Error was occured during pushing an repos entry. ` +
`Code: ${data.status}, text: ${data.responseText || data.statusText}`;
badResponse(message);
});
}).fail((errorData) => {
const message = `Can not sent a request to clone the repository. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
taskMessage.css('color', 'red');
taskMessage.text(message);
onFault();
});
}
setTimeout(checkCallback, 1000);
}).fail((errorData) => {
const message = `Can not sent a request to clone the repository. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
taskMessage.css('color', 'red');
taskMessage.text(message);
onFault();
});
} else {
next();
}
});
});

@ -8,6 +8,7 @@ from . import views
urlpatterns = [
path('create/<int:tid>', views.create),
path('get/<int:tid>', views.get_repository),
path('push/<int:tid>', views.push_repository),
path('check/<str:rq_id>', views.check_process),

@ -12,6 +12,7 @@ from cvat.apps.git.models import GitData
import cvat.apps.git.git as CVATGit
import django_rq
import json
@login_required
def check_process(request, rq_id):
@ -21,11 +22,11 @@ def check_process(request, rq_id):
if rq_job is not None:
if rq_job.is_queued or rq_job.is_started:
return JsonResponse({"status": "processing"})
return JsonResponse({"status": rq_job.get_status()})
elif rq_job.is_finished:
return JsonResponse({"status": "finished"})
return JsonResponse({"status": rq_job.get_status()})
else:
return JsonResponse({"status": "failed", "error": rq_job.exc_info})
return JsonResponse({"status": rq_job.get_status(), "stderr": rq_job.exc_info})
else:
return JsonResponse({"status": "unknown"})
except Exception as ex:
@ -33,6 +34,26 @@ def check_process(request, rq_id):
return HttpResponseBadRequest(str(ex))
@login_required
@permission_required(perm=['engine.task.create'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)
def create(request, tid):
try:
slogger.task[tid].info("create repository request")
body = json.loads(request.body.decode('utf-8'))
path = body["path"]
lfs = body["lfs"]
rq_id = "git.create.{}".format(tid)
queue = django_rq.get_queue("default")
queue.enqueue_call(func = CVATGit.initial_create, args = (tid, path, lfs, request.user), job_id = rq_id)
return JsonResponse({ "rq_id": rq_id })
except Exception as ex:
slogger.glob.error("error occured during initial cloning repository request with rq id {}".format(rq_id), exc_info=True)
return HttpResponseBadRequest(str(ex))
@login_required
@permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True)

@ -4,4 +4,4 @@
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['log_viewer/js/shortcuts.js']
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['log_viewer/js/dashboardPlugin.js']

@ -0,0 +1,11 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
window.addEventListener('DOMContentLoaded', () => {
$('<button class="regular h1" style="margin-left: 5px;"> Analytics </button>').on('click', () => {
window.open('/analytics/app/kibana');
}).appendTo('#dashboardManageButtons');
});

@ -1,15 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
Mousetrap:false
*/
Mousetrap.bind(window.cvat.config.shortkeys["open_analytics"].value, function() {
window.open("/analytics/app/kibana");
return false;
});

@ -82,10 +82,10 @@ class ReID:
def __boxes_are_compatible(self, cur_box, next_box):
cur_c_x = (cur_box["xtl"] + cur_box["xbr"]) / 2
cur_c_y = (cur_box["ytl"] + cur_box["ybr"]) / 2
next_c_x = (next_box["xtl"] + next_box["xbr"]) / 2
next_c_y = (next_box["ytl"] + next_box["ybr"]) / 2
cur_c_x = (cur_box["points"][0] + cur_box["points"][2]) / 2
cur_c_y = (cur_box["points"][1] + cur_box["points"][3]) / 2
next_c_x = (next_box["points"][0] + next_box["points"][2]) / 2
next_c_y = (next_box["points"][1] + next_box["points"][3]) / 2
compatible_distance = euclidean([cur_c_x, cur_c_y], [next_c_x, next_c_y]) <= self.__max_distance
compatible_label = cur_box["label_id"] == next_box["label_id"]
return compatible_distance and compatible_label and "path_id" not in next_box
@ -123,8 +123,8 @@ class ReID:
cur_width = cur_image.shape[1]
cur_height = cur_image.shape[0]
cur_xtl, cur_xbr, cur_ytl, cur_ybr = (
_int(cur_box["xtl"], cur_width), _int(cur_box["xbr"], cur_width),
_int(cur_box["ytl"], cur_height), _int(cur_box["ybr"], cur_height)
_int(cur_box["points"][0], cur_width), _int(cur_box["points"][2], cur_width),
_int(cur_box["points"][1], cur_height), _int(cur_box["points"][3], cur_height)
)
for col, next_box in enumerate(next_boxes):
@ -132,8 +132,8 @@ class ReID:
next_width = next_image.shape[1]
next_height = next_image.shape[0]
next_xtl, next_xbr, next_ytl, next_ybr = (
_int(next_box["xtl"], next_width), _int(next_box["xbr"], next_width),
_int(next_box["ytl"], next_height), _int(next_box["ybr"], next_height)
_int(next_box["points"][0], next_width), _int(next_box["points"][2], next_width),
_int(next_box["points"][1], next_height), _int(next_box["points"][3], next_height)
)
if not self.__boxes_are_compatible(cur_box, next_box):
@ -149,7 +149,7 @@ class ReID:
def __apply_matching(self):
frames = sorted(list(self.__frame_boxes.keys()))
job = rq.get_current_job()
box_paths = {}
box_tracks = {}
for idx, (cur_frame, next_frame) in enumerate(list(zip(frames[:-1], frames[1:]))):
job.refresh()
@ -164,8 +164,8 @@ class ReID:
for box in cur_boxes:
if "path_id" not in box:
path_id = len(box_paths)
box_paths[path_id] = [box]
path_id = len(box_tracks)
box_tracks[path_id] = [box]
box["path_id"] = path_id
if not (len(cur_boxes) and len(next_boxes)):
@ -180,38 +180,39 @@ class ReID:
cur_box = cur_boxes[cur_idx]
next_box = next_boxes[next_idxs[idx]]
next_box["path_id"] = cur_box["path_id"]
box_paths[cur_box["path_id"]].append(next_box)
box_tracks[cur_box["path_id"]].append(next_box)
for box in self.__frame_boxes[frames[-1]]:
if "path_id" not in box:
path_id = len(box_paths)
path_id = len(box_tracks)
box["path_id"] = path_id
box_paths[path_id] = [box]
box_tracks[path_id] = [box]
return box_paths
return box_tracks
def run(self):
box_paths = self.__apply_matching()
box_tracks = self.__apply_matching()
output = []
# ReID process has been canceled
if box_paths is None:
if box_tracks is None:
return
for path_id in box_paths:
for path_id in box_tracks:
output.append({
"label_id": box_paths[path_id][0]["label_id"],
"group_id": 0,
"label_id": box_tracks[path_id][0]["label_id"],
"group": None,
"attributes": [],
"frame": box_paths[path_id][0]["frame"],
"shapes": box_paths[path_id]
"frame": box_tracks[path_id][0]["frame"],
"shapes": box_tracks[path_id]
})
for box in output[-1]["shapes"]:
del box["id"]
if "id" in box:
del box["id"]
del box["path_id"]
del box["group_id"]
del box["group"]
del box["label_id"]
box["outside"] = False
box["attributes"] = []

@ -8,97 +8,96 @@
document.addEventListener('DOMContentLoaded', () => {
function run(overlay, cancelButton, thresholdInput, distanceInput) {
async function run(overlay, cancelButton, thresholdInput, distanceInput) {
const collection = window.cvat.data.get();
const data = {
threshold: +thresholdInput.prop('value'),
maxDistance: +distanceInput.prop('value'),
boxes: collection.boxes,
boxes: collection.shapes.filter(el => el.type === 'rectangle'),
};
overlay.removeClass('hidden');
cancelButton.prop('disabled', true);
$.ajax({
url: `reid/start/job/${window.cvat.job.id}`,
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
success: () => {
function checkCallback() {
$.ajax({
url: `/reid/check/${window.cvat.job.id}`,
type: 'GET',
success: (jobData) => {
if (jobData.progress) {
cancelButton.text(`Cancel ReID Merge (${jobData.progress.toString().slice(0, 4)}%)`);
}
if (['queued', 'started'].includes(jobData.status)) {
setTimeout(checkCallback, 1000);
} else {
overlay.addClass('hidden');
if (jobData.status === 'finished') {
if (jobData.result) {
collection.boxes = [];
collection.box_paths = collection.box_paths
.concat(JSON.parse(jobData.result));
window.cvat.data.clear();
window.cvat.data.set(collection);
showMessage('ReID merge has done.');
} else {
showMessage('ReID merge been canceled.');
}
} else if (jobData.status === 'failed') {
const message = `ReID merge has fallen. Error: '${jobData.stderr}'`;
showMessage(message);
} else {
let message = `Check request returned "${jobData.status}" status.`;
if (jobData.stderr) {
message += ` Error: ${jobData.stderr}`;
}
showMessage(message);
}
}
},
error: (errorData) => {
overlay.addClass('hidden');
const message = `Can not check ReID merge. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
},
});
}
setTimeout(checkCallback, 1000);
},
error: (errorData) => {
async function checkCallback() {
let jobData = null;
try {
jobData = await $.get(`/reid/check/${window.cvat.job.id}`);
} catch (errorData) {
overlay.addClass('hidden');
const message = `Can not start ReID merge. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
const message = `Can not check ReID merge. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
},
complete: () => {
cancelButton.prop('disabled', false);
},
});
}
}
function cancel(overlay, cancelButton) {
cancelButton.prop('disabled', true);
$.ajax({
url: `/reid/cancel/${window.cvat.job.id}`,
type: 'GET',
success: () => {
if (jobData.progress) {
cancelButton.text(`Cancel ReID Merge (${jobData.progress.toString().slice(0, 4)}%)`);
}
if (['queued', 'started'].includes(jobData.status)) {
setTimeout(checkCallback, 1000);
} else {
overlay.addClass('hidden');
cancelButton.text('Cancel ReID Merge (0%)');
},
error: (errorData) => {
const message = `Can not cancel ReID process. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
},
complete: () => {
cancelButton.prop('disabled', false);
if (jobData.status === 'finished') {
if (jobData.result) {
const result = JSON.parse(jobData.result);
collection.shapes = collection.shapes
.filter(el => el.type !== 'rectangle');
collection.tracks = collection.tracks
.concat(result);
window.cvat.data.clear();
window.cvat.data.set(collection);
showMessage('ReID merge has done.');
} else {
showMessage('ReID merge been canceled.');
}
} else if (jobData.status === 'failed') {
const message = `ReID merge has fallen. Error: '${jobData.stderr}'`;
showMessage(message);
} else {
let message = `Check request returned "${jobData.status}" status.`;
if (jobData.stderr) {
message += ` Error: ${jobData.stderr}`;
}
showMessage(message);
}
}
});
}
try {
await $.ajax({
url: `/reid/start/job/${window.cvat.job.id}`,
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
});
setTimeout(checkCallback, 1000);
} catch (errorData) {
overlay.addClass('hidden');
const message = `Can not start ReID merge. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
} finally {
cancelButton.prop('disabled', false);
}
}
async function cancel(overlay, cancelButton) {
cancelButton.prop('disabled', true);
try {
await $.get(`/reid/cancel/${window.cvat.job.id}`);
overlay.addClass('hidden');
cancelButton.text('Cancel ReID Merge (0%)');
} catch (errorData) {
const message = `Can not cancel ReID process. Code: ${errorData.status}. Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
} finally {
cancelButton.prop('disabled', false);
}
}
const buttonsUI = $('#engineMenuButtons');
@ -121,13 +120,13 @@ document.addEventListener('DOMContentLoaded', () => {
<table>
<tr>
<td> <label class="regular h2"> Threshold: </label> </td>
<td> <input id="${reidThresholdValueId}" class="regular h1" type="number"` +
`title="Maximum cosine distance between embeddings of objects" min="0.05" max="0.95" value="0.5" step="0.05"> </td>
<td> <input id="${reidThresholdValueId}" class="regular h1" type="number"`
+ `title="Maximum cosine distance between embeddings of objects" min="0.05" max="0.95" value="0.5" step="0.05"> </td>
</tr>
<tr>
<td> <label class="regular h2"> Max Pixel Distance </label> </td>
<td> <input id="${reidDistanceValueId}" class="regular h1" type="number"` +
`title="Maximum radius that an object can diverge between neighbor frames" min="10" max="1000" value="50" step="10"> </td>
<td> <input id="${reidDistanceValueId}" class="regular h1" type="number"`
+ `title="Maximum radius that an object can diverge between neighbor frames" min="10" max="1000" value="50" step="10"> </td>
</tr>
<tr>
<td colspan="2"> <label class="regular h2" style="color: red;"> All boxes will be translated to box paths. Continue? </label> </td>
@ -165,6 +164,11 @@ document.addEventListener('DOMContentLoaded', () => {
$(`#${reidSubmitMergeId}`).on('click', () => {
$(`#${reidWindowId}`).addClass('hidden');
run($(`#${reidOverlay}`), $(`#${reidCancelButtonId}`),
$(`#${reidThresholdValueId}`), $(`#${reidDistanceValueId}`));
$(`#${reidThresholdValueId}`), $(`#${reidDistanceValueId}`))
.catch((error) => {
setTimeout(() => {
throw error;
});
});
});
});

@ -5,5 +5,5 @@
from cvat.settings.base import JS_3RDPARTY
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['tf_annotation/js/tf_annotation.js']
JS_3RDPARTY['dashboard'] = JS_3RDPARTY.get('dashboard', []) + ['tf_annotation/js/dashboardPlugin.js']

@ -0,0 +1,112 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
userConfirm:false
showMessage:false
*/
window.addEventListener('dashboardReady', () => {
function checkProcess(tid, button) {
function checkCallback() {
$.get(`/tensorflow/annotation/check/task/${tid}`).done((statusData) => {
if (['started', 'queued'].includes(statusData.status)) {
const progress = Math.round(statusData.progress) || '0';
button.text(`Cancel TF Annotation (${progress}%)`);
setTimeout(checkCallback, 5000);
} else {
button.text('Run TF Annotation');
button.removeClass('tfAnnotationProcess');
button.prop('disabled', false);
if (statusData.status === 'failed') {
const message = `Tensorflow annotation failed. Error: ${statusData.stderr}`;
showMessage(message);
} else if (statusData.status !== 'finished') {
const message = `Tensorflow annotation check request returned status "${statusData.status}"`;
showMessage(message);
}
}
}).fail((errorData) => {
const message = `Can not sent tensorflow annotation check request. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
setTimeout(checkCallback, 5000);
}
function runProcess(tid, button) {
$.get(`/tensorflow/annotation/create/task/${tid}`).done(() => {
showMessage('Process has started');
button.text('Cancel TF Annotation (0%)');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}).fail((errorData) => {
const message = `Can not run tf annotation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function cancelProcess(tid, button) {
$.get(`/tensorflow/annotation/cancel/task/${tid}`).done(() => {
button.prop('disabled', true);
}).fail((errorData) => {
const message = `Can not cancel tf annotation. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
}
function setupDashboardItem(item, metaData) {
const tid = +item.attr('tid');
const button = $('<button> Run TF Annotation </button>');
button.on('click', () => {
if (button.hasClass('tfAnnotationProcess')) {
userConfirm('The process will be canceled. Continue?', () => {
cancelProcess(tid, button);
});
} else {
userConfirm('The current annotation will be lost. Are you sure?', () => {
runProcess(tid, button);
});
}
});
button.addClass('dashboardTFAnnotationButton regular dashboardButtonUI');
button.appendTo(item.find('div.dashboardButtonsUI'));
if ((tid in metaData) && (metaData[tid].active)) {
button.text('Cancel TF Annotation');
button.addClass('tfAnnotationProcess');
checkProcess(tid, button);
}
}
const elements = $('.dashboardItem');
const tids = Array.from(elements, el => +el.getAttribute('tid'));
$.ajax({
type: 'POST',
url: '/tensorflow/annotation/meta/get',
data: JSON.stringify(tids),
contentType: 'application/json; charset=utf-8',
}).done((metaData) => {
elements.each(function setupDashboardItemWrapper() {
setupDashboardItem($(this), metaData);
});
}).fail((errorData) => {
const message = `Can not get tf annotation meta info. Code: ${errorData.status}. `
+ `Message: ${errorData.responseText || errorData.statusText}`;
showMessage(message);
});
});

@ -1,134 +0,0 @@
/*
* Copyright (C) 2018 Intel Corporation
*
* SPDX-License-Identifier: MIT
*/
/* global
userConfirm:false
showMessage:false
*/
"use strict";
function CheckTFAnnotationRequest(taskId, tfAnnotationButton) {
let frequence = 5000;
let errorCount = 0;
let interval = setInterval(function() {
$.ajax ({
url: '/tensorflow/annotation/check/task/' + taskId,
success: function(jsonData) {
let status = jsonData["status"];
if (status == "started" || status == "queued") {
let progress = Math.round(jsonData["progress"]) || "0";
tfAnnotationButton.text(`Cancel TF Annotation (${progress}%)`);
}
else {
tfAnnotationButton.text("Run TF Annotation");
tfAnnotationButton.removeClass("tfAnnotationProcess");
tfAnnotationButton.prop("disabled", false);
clearInterval(interval);
}
},
error: function() {
errorCount ++;
if (errorCount > 5) {
clearInterval(interval);
tfAnnotationButton.prop("disabled", false);
tfAnnotationButton.text("Status Check Error");
throw Error(`TF annotation check request error for task ${window.cvat.dashboard.taskID}:${window.cvat.dashboard.taskName}`);
}
}
});
}, frequence);
}
function RunTFAnnotationRequest() {
let tfAnnotationButton = this;
let taskID = window.cvat.dashboard.taskID;
$.ajax ({
url: '/tensorflow/annotation/create/task/' + taskID,
success: function() {
showMessage('Process started.');
tfAnnotationButton.text(`Cancel TF Annotation (0%)`);
tfAnnotationButton.addClass("tfAnnotationProcess");
CheckTFAnnotationRequest(taskID, tfAnnotationButton);
},
error: function(response) {
let message = 'Abort. Reason: ' + response.responseText;
showMessage(message);
}
});
}
function CancelTFAnnotationRequest() {
let tfAnnotationButton = this;
$.ajax ({
url: '/tensorflow/annotation/cancel/task/' + window.cvat.dashboard.taskID,
success: function() {
tfAnnotationButton.prop("disabled", true);
},
error: function(data) {
let message = `TF annotation cancel error: ${data.responseText}`;
showMessage(message);
}
});
}
function onTFAnnotationClick() {
let button = this;
let uiElem = button.closest('div.dashboardTaskUI');
let taskId = +uiElem.attr('id').split('_')[1];
let taskName = $.trim($( uiElem.find('label.dashboardTaskNameLabel')[0] ).text());
window.cvat.dashboard.taskID = taskId;
window.cvat.dashboard.taskName = taskName;
if (button.hasClass("tfAnnotationProcess")) {
userConfirm('The process will be canceled. Continue?', CancelTFAnnotationRequest.bind(button));
}
else {
userConfirm('The current annotation will be lost. Are you sure?', RunTFAnnotationRequest.bind(button));
}
}
window.cvat = window.cvat || {};
window.cvat.dashboard = window.cvat.dashboard || {};
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
window.cvat.dashboard.uiCallbacks.push(function(newElements) {
let tids = [];
for (let el of newElements) {
tids.push(el.id.split('_')[1]);
}
$.ajax({
type: 'POST',
url: '/tensorflow/annotation/meta/get',
data: JSON.stringify(tids),
contentType: "application/json; charset=utf-8",
success: (data) => {
newElements.each(function(idx) {
let elem = $(newElements[idx]);
let tid = +elem.attr('id').split('_')[1];
let buttonsUI = elem.find('div.dashboardButtonsUI')[0];
let tfAnnotationButton = $('<button> Run TF Annotation </button>');
tfAnnotationButton.on('click', onTFAnnotationClick.bind(tfAnnotationButton));
tfAnnotationButton.addClass('dashboardTFAnnotationButton regular dashboardButtonUI');
tfAnnotationButton.appendTo(buttonsUI);
if ((tid in data) && (data[tid].active)) {
tfAnnotationButton.text("Cancel TF Annotation");
tfAnnotationButton.addClass("tfAnnotationProcess");
CheckTFAnnotationRequest(tid, tfAnnotationButton);
}
});
},
error: (data) => {
let message = `Can not get tf annotation meta info. Code: ${data.status}. Message: ${data.responseText || data.statusText}`;
showMessage(message);
throw Error(message);
}
});
});

@ -10,7 +10,8 @@ from rules.contrib.views import permission_required, objectgetter
from cvat.apps.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine import annotation, task
from cvat.apps.engine.serializers import LabeledDataSerializer
from cvat.apps.engine.annotation import put_task_data
import django_rq
import fnmatch
@ -168,44 +169,30 @@ def make_image_list(path_to_data):
def convert_to_cvat_format(data):
def create_anno_container():
return {
"boxes": [],
"polygons": [],
"polylines": [],
"points": [],
"box_paths": [],
"polygon_paths": [],
"polyline_paths": [],
"points_paths": [],
}
result = {
'create': create_anno_container(),
'update': create_anno_container(),
'delete': create_anno_container(),
"tracks": [],
"shapes": [],
"tags": [],
"version": 0,
}
for label in data:
boxes = data[label]
for box in boxes:
result['create']['boxes'].append({
result['shapes'].append({
"type": "rectangle",
"label_id": label,
"frame": box[0],
"xtl": box[1],
"ytl": box[2],
"xbr": box[3],
"ybr": box[4],
"points": [box[1], box[2], box[3], box[4]],
"z_order": 0,
"group_id": 0,
"group": None,
"occluded": False,
"attributes": [],
"id": -1,
})
return result
def create_thread(tid, labels_mapping):
def create_thread(tid, labels_mapping, user):
try:
TRESHOLD = 0.5
# Init rq job
@ -228,14 +215,16 @@ def create_thread(tid, labels_mapping):
# Modify data format and save
result = convert_to_cvat_format(result)
annotation.clear_task(tid)
annotation.save_task(tid, result)
serializer = LabeledDataSerializer(data = result)
if serializer.is_valid(raise_exception=True):
put_task_data(tid, user, result)
slogger.glob.info('tf annotation for task {} done'.format(tid))
except:
except Exception as ex:
try:
slogger.task[tid].exception('exception was occured during tf annotation of the task', exc_info=True)
except:
slogger.glob.exception('exception was occured during tf annotation of the task {}'.format(tid), exc_into=True)
raise ex
@login_required
def get_meta_info(request):
@ -301,7 +290,7 @@ def create(request, tid):
# Run tf annotation job
queue.enqueue_call(func=create_thread,
args=(tid, labels_mapping),
args=(tid, labels_mapping, request.user),
job_id='tf_annotation.create/{}'.format(tid),
timeout=604800) # 7 days
@ -338,6 +327,7 @@ def check(request, tid):
job.delete()
else:
data['status'] = 'failed'
data['stderr'] = job.exc_info
job.delete()
except Exception:

@ -0,0 +1,4 @@
-r development.txt
-r production.txt
-r staging.txt
-r testing.txt

@ -1,14 +1,14 @@
click==6.7
Django==2.1.5
Django==2.1.7
django-appconf==1.0.2
django-auth-ldap==1.4.0
django-cacheops==4.0.6
django-compressor==2.2
django-rq==1.3.0
django-rq==2.0.0
EasyProcess==0.2.3
ffmpy==0.2.2
Pillow==5.1.0
numpy==1.14.2
numpy==1.16.2
patool==1.12
python-ldap==3.0.0
pytz==2018.3
@ -17,8 +17,8 @@ rcssmin==1.0.6
redis==3.2.0
requests==2.20.0
rjsmin==1.0.12
rq==0.13.0
scipy==1.0.1
rq==1.0.0
scipy==1.2.1
sqlparse==0.2.4
django-sendfile==0.3.11
dj-pagination==2.4.0
@ -26,3 +26,10 @@ python-logstash==0.4.6
django-revproxy==0.9.15
rules==2.0
GitPython==2.1.11
coreapi==2.3.3
django-filter==2.0.0
Markdown==3.0.1
djangorestframework==3.9.0
Pygments==2.3.1
drf-yasg==1.15.0
Shapely==1.6.4.post2

@ -0,0 +1,2 @@
-f development.txt
fakeredis==1.0.3

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save