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

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

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The ReID application for automatic bounding box merging has been added (#299) - 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) - 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 - Semi-automatic semantic segmentation with the [Deep Extreme Cut](http://www.vision.ee.ethz.ch/~cvlsegmentation/dextr/) work
### Changed ### Changed

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

@ -3,3 +3,8 @@
# #
# SPDX-License-Identifier: MIT # 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 import os
from django.conf import settings from django.conf import settings
from django.db.models import Q
import rules import rules
from . import AUTH_ROLE from . import AUTH_ROLE
from rest_framework.permissions import BasePermission
def register_signals(): def register_signals():
from django.db.models.signals import post_migrate, post_save 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 return has_rights
# AUTH PERMISSIONS RULES # 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.create', has_admin_role | has_user_role)
rules.add_perm('engine.task.access', has_admin_role | has_observer_role | rules.add_perm('engine.task.access', has_admin_role | has_observer_role |
is_task_owner | is_task_annotator) 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) is_job_owner | is_job_annotator)
rules.add_perm('engine.job.change', has_admin_role | is_job_owner | rules.add_perm('engine.job.change', has_admin_role | is_job_owner |
is_job_annotator) 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.log import slogger
from cvat.apps.engine.models import Task as TaskModel 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 .models import AnnotationModel, FrameworkChoice
from .model_loader import ModelLoader from .model_loader import ModelLoader
@ -208,71 +209,42 @@ def get_image_data(path_to_data):
image_list.sort(key=get_image_key) image_list.sort(key=get_image_key)
return ImageLoader(image_list) return ImageLoader(image_list)
def create_anno_container():
return {
"boxes": [],
"polygons": [],
"polylines": [],
"points": [],
"box_paths": [],
"polygon_paths": [],
"polyline_paths": [],
"points_paths": [],
}
class Results(): class Results():
def __init__(self): 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): def add_box(self, xtl, ytl, xbr, ybr, label, frame_number, attributes=None):
self.get_boxes().append({ self.get_shapes().append({
"label": label, "label": label,
"frame": frame_number, "frame": frame_number,
"xtl": xtl, "points": [xtl, ytl, xbr, ybr],
"ytl": ytl, "type": "rectangle",
"xbr": xbr,
"ybr": ybr,
"attributes": attributes or {}, "attributes": attributes or {},
}) })
def add_points(self, points, label, frame_number, attributes=None): def add_points(self, points, label, frame_number, attributes=None):
self.get_points().append( points = self._create_polyshape(points, label, frame_number, attributes)
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): def add_polygon(self, points, label, frame_number, attributes=None):
self.get_polygons().append( polygon = self._create_polyshape(points, label, frame_number, attributes)
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): def add_polyline(self, points, label, frame_number, attributes=None):
self.get_polylines().append( polyline = self._create_polyshape(points, label, frame_number, attributes)
self._create_polyshape(points, label, frame_number, attributes) polyline["type"] = "polyline"
) self.get_shapes().append(polyline)
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"]
def get_polyline_paths(self): def get_shapes(self):
return self._results["polyline_paths"] return self._results["shapes"]
def get_points_paths(self): def get_tracks(self):
return self._results["points_paths"] return self._results["tracks"]
@staticmethod @staticmethod
def _create_polyshape(self, points, label, frame_number, attributes=None): 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 return attributes
def add_polyshapes(shapes, target_container): def add_shapes(shapes, target_container):
for shape in shapes: for shape in shapes:
if shape["label"] not in labels_mapping: if shape["label"] not in labels_mapping:
continue continue
@ -325,35 +297,18 @@ def _run_inference_engine_annotation(data, model_file, weights_file,
"label_id": db_label, "label_id": db_label,
"frame": shape["frame"], "frame": shape["frame"],
"points": shape["points"], "points": shape["points"],
"type": shape["type"],
"z_order": 0, "z_order": 0,
"group_id": 0, "group": None,
"occluded": False, "occluded": False,
"attributes": process_attributes(shape["attributes"], attribute_spec[db_label]), "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 = { result = {
"create": create_anno_container(), "shapes": [],
"update": create_anno_container(), "tracks": [],
"delete": create_anno_container(), "tags": [],
"version": 0
} }
data_len = len(data) data_len = len(data)
@ -375,16 +330,15 @@ def _run_inference_engine_annotation(data, model_file, weights_file,
frame_counter += 1 frame_counter += 1
if job and update_progress and not update_progress(job, frame_counter * 100 / data_len): if job and update_progress and not update_progress(job, frame_counter * 100 / data_len):
return None return None
processed_detections = _process_detections(detections, convertation_file) processed_detections = _process_detections(detections, convertation_file)
add_boxes(processed_detections.get_boxes(), result["create"]["boxes"]) add_shapes(processed_detections.get_shapes(), result["shapes"])
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"])
return result 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): def update_progress(job, progress):
job.refresh() job.refresh()
if "cancel" in job.meta: 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)) slogger.glob.info("auto annotation for task {} canceled by user".format(tid))
return return
if reset: serializer = LabeledDataSerializer(data = result)
annotation.clear_task(tid) if serializer.is_valid(raise_exception=True):
annotation.save_task(tid, result) 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)) slogger.glob.info("auto annotation for task {} done".format(tid))
except Exception as e: except Exception as e:
try: try:

@ -11,8 +11,6 @@
*/ */
window.cvat = window.cvat || {}; window.cvat = window.cvat || {};
window.cvat.dashboard = window.cvat.dashboard || {};
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || [];
const AutoAnnotationServer = { const AutoAnnotationServer = {
start(modelId, taskId, data, success, error, progress, check) { start(modelId, taskId, data, success, error, progress, check) {
@ -602,7 +600,7 @@ class AutoAnnotationModelRunnerView {
this.id = null; this.id = null;
this.initButton = initButton; this.initButton = initButton;
this.tid = data.taskid; this.tid = data.id;
this.modelsTable.empty(); this.modelsTable.empty();
this.labelsTable.empty(); this.labelsTable.empty();
this.active = null; this.active = null;
@ -617,7 +615,7 @@ class AutoAnnotationModelRunnerView {
this.active.style.color = 'darkblue'; this.active.style.color = 'darkblue';
this.labelsTable.empty(); 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); const intersection = labels.filter(el => event.data.model.labels.indexOf(el) !== -1);
intersection.forEach((label) => { intersection.forEach((label) => {
const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector'); const dlSelect = labelsSelect(event.data.model.labels, 'annotatorDlLabelSelector');
@ -705,47 +703,37 @@ window.cvat.autoAnnotation = {
managerButtonId: 'annotatorManagerButton', managerButtonId: 'annotatorManagerButton',
}; };
window.cvat.dashboard.uiCallbacks.push((newElements) => { window.addEventListener('DOMContentLoaded', () => {
window.cvat.autoAnnotation.server = AutoAnnotationServer; window.cvat.autoAnnotation.server = AutoAnnotationServer;
window.cvat.autoAnnotation.manager = new AutoAnnotationModelManagerView(); window.cvat.autoAnnotation.manager = new AutoAnnotationModelManagerView();
window.cvat.autoAnnotation.runner = new AutoAnnotationModelRunnerView(); 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.server.meta(tids, (data) => {
window.cvat.autoAnnotation.data = 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>`) elements.each(function setupDashboardItem() {
.on('click', () => { const elem = $(this);
const overlay = showOverlay('The manager are being setup..'); const tid = +elem.attr('tid');
window.cvat.autoAnnotation.manager.reset().show();
overlay.remove();
}).appendTo('#dashboardManageButtons');
newElements.each((_, element) => {
const elem = $(element);
const tid = +elem.attr('id').split('_')[1];
const button = $('<button> Run Auto Annotation </button>').addClass('regular dashboardButtonUI'); const button = $('<button> Run Auto Annotation </button>').addClass('regular dashboardButtonUI');
button[0].setupRun = function setupRun() { button[0].setupRun = function setupRun() {
const self = $(this); const self = $(this);
const taskInfo = event.detail.filter(task => task.id === tid)[0];
self.text('Run Auto Annotation').off('click').on('click', () => { self.text('Run Auto Annotation').off('click').on('click', () => {
const overlay = showOverlay('Task date are being recieved from the server..'); window.cvat.autoAnnotation.runner.reset(taskInfo, self).show();
$.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();
},
});
}); });
}; };

@ -155,7 +155,7 @@ def get_meta_info(request):
job = queue.fetch_job(rq_id) job = queue.fetch_job(rq_id)
if job is not None: if job is not None:
response["run"][tid] = { response["run"][tid] = {
"status": job.status, "status": job.get_status(),
"rq_id": rq_id, "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_labels = db_task.label_set.prefetch_related("attributespec_set").all()
db_attributes = {db_label.id: 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} 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()} 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, db_attributes,
convertation_file_path, convertation_file_path,
should_reset, should_reset,
request.user,
), ),
job_id = rq_id, job_id = rq_id,
timeout=604800) # 7 days 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 from django.apps import AppConfig
class DashboardConfig(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 * SPDX-License-Identifier: MIT
*/ */
"use strict";
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
$(`<button class="menuButton semiBold h2"> Open Task </button>`).on('click', () => { $('<button class="menuButton semiBold h2"> Open Task </button>').on('click', () => {
let win = window.open(`${window.location.origin }/dashboard/?jid=${window.cvat.job.id}`, '_blank'); const win = window.open(`${window.location.origin}/dashboard/?id=${window.cvat.job.task_id}`, '_blank');
win.focus(); win.focus();
}).prependTo('#engineMenuButtons'); }).prependTo('#engineMenuButtons');
}); });

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

@ -3,6 +3,7 @@
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
{% extends 'engine/base.html' %} {% extends 'engine/base.html' %}
{% load static %} {% load static %}
{% load pagination_tags %} {% load pagination_tags %}
@ -14,7 +15,8 @@
{% block head_css %} {% block head_css %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static 'dashboard/stylesheet.css' %}"> <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 %} {% for css_file in css_3rdparty %}
<link rel="stylesheet" type="text/css" href="{% static css_file %}"> <link rel="stylesheet" type="text/css" href="{% static css_file %}">
{% endfor %} {% endfor %}
@ -22,7 +24,8 @@
{% block head_js_3rdparty %} {% block head_js_3rdparty %}
{{ block.super }} {{ 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 %} {% for js_file in js_3rdparty %}
<script type="text/javascript" src="{% static js_file %}" defer></script> <script type="text/javascript" src="{% static js_file %}" defer></script>
{% endfor %} {% endfor %}
@ -36,17 +39,13 @@
<script type="text/javascript" src="{% static 'engine/js/shapes.js' %}"></script> <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/annotationParser.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/server.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 %} {% endblock %}
{% block content %} {% block content %}
<div id="content"> <div id="content">
<div style="width: 100%; display: flex;"> <div style="width: 100%; display: flex;">
<div style="width: 50%; display: flex;"> </div> <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;"> <div id="dashboardManageButtons" style="display: flex;">
<button id="dashboardCreateTaskButton" class="regular h1" style="padding: 7px;"> Create New Task </button> <button id="dashboardCreateTaskButton" class="regular h1" style="padding: 7px;"> Create New Task </button>
</div> </div>
@ -58,13 +57,9 @@
<div style="width: 50%; display: flex;"> </div> <div style="width: 50%; display: flex;"> </div>
</div> </div>
{% autopaginate data %} <div id="dashboardPagination">
<div style="float: left; width: 100%"> <div id="dashboardList" style="float: left; width: 100%"> </div>
{% for item in data %}
{% include "dashboard/task.html" %}
{% endfor %}
</div> </div>
<center>{% paginate %}</center>
</div> </div>
<div id="dashboardCreateModal" class="modal hidden"> <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> <br> <input id="dashboardShareSource" type="radio" name="sourceType" value="share"/> <label for="dashboardShareSource" class="regular h2" for="shareSource"> Share </label>
</td> </td>
</tr> </tr>
<tr>
<td>
<label class="regular h2"> Flip images </label>
</td>
<td>
<input type="checkbox" id="dashboardFlipImages"/>
</td>
</tr>
<tr> <tr>
<td> <td>
<label class="regular h2"> Z-Order </label> <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 id="dashboardShareBrowseModal" class="modal hidden">
<div style="width: 600px; height: 400px;" class="modal-content noSelect"> <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> <div id="dashboardShareBrowser"> </div>
<center> <center>
<button id="dashboardCancelBrowseServer" class="regular h2" style="margin: 0px 10px"> Cancel </button> <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> </div>
<div id="dashboardUpdateModal" class="modal hidden"> <template id="dashboardUpdateTemplate">
<div id="dashboardUpdateContent" class="modal-content"> <div id="dashboardUpdateModal" class="modal">
<input id="dashboardOldLabels" type="text" readonly=true placeholder="Please Wait.." class="regular h2"> <div id="dashboardUpdateContent" class="modal-content">
<input id="dashboardNewLabels" type="text" placeholder="New Labels" class="regular h2"> <input id="dashboardOldLabels" type="text" readonly=true class="regular h2">
<center> <input id="dashboardNewLabels" type="text" placeholder="expand the specification here" class="regular h2">
<button id="dashboardCancelUpdate" class="regular h2"> Cancel </button> <center>
<button id="dashboardSubmitUpdate" class="regular h2"> Update </button> <button id="dashboardCancelUpdate" class="regular h2"> Cancel </button>
</center> <button id="dashboardSubmitUpdate" class="regular h2"> Update </button>
</center>
</div>
</div> </div>
</div> </template>
{% endblock %} {% 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 from . import views
urlpatterns = [ urlpatterns = [
path('get_share_nodes', views.JsTreeView),
path('', views.DashboardView), path('', views.DashboardView),
path('meta', views.DashboardMeta),
] ]

@ -9,73 +9,22 @@ from django.shortcuts import render
from django.conf import settings from django.conf import settings
from cvat.apps.authentication.decorators import login_required 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 from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
import os 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 @login_required
def DashboardView(request): 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', { 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_size': settings.LOCAL_LOAD_MAX_FILES_SIZE,
'max_upload_count': settings.LOCAL_LOAD_MAX_FILES_COUNT, 'max_upload_count': settings.LOCAL_LOAD_MAX_FILES_COUNT,
'base_url': "{0}://{1}/".format(request.scheme, request.get_host()), 'base_url': "{0}://{1}/".format(request.scheme, request.get_host()),
'share_path': os.getenv('CVAT_SHARE_URL', default=r'${cvat_root}/share'), '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 = np.concatenate((resized, heatmap[:, :, np.newaxis].astype(resized.dtype)), axis=2)
input_dextr = input_dextr.transpose((2,0,1)) 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 = 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) pred = cv2.resize(pred, tuple(reversed(numpy_cropped.shape[:2])), interpolation = cv2.INTER_CUBIC)
result = np.zeros(numpy_image.shape[:2]) 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.models import Job
from cvat.apps.engine.log import slogger 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 from cvat.apps.dextr_segmentation.dextr import DEXTR_HANDLER
import django_rq import django_rq
@ -39,8 +38,8 @@ def create(request, jid):
slogger.job[jid].info("create dextr request for the JOB: {} ".format(jid) slogger.job[jid].info("create dextr request for the JOB: {} ".format(jid)
+ "by the USER: {} on the FRAME: {}".format(username, frame)) + "by the USER: {} on the FRAME: {}".format(username, frame))
tid = Job.objects.select_related("segment__task").get(id=jid).segment.task.id db_task = Job.objects.select_related("segment__task").get(id=jid).segment.task
im_path = os.path.realpath(get_frame_path(tid, frame)) im_path = os.path.realpath(db_task.get_frame_path(frame))
queue = django_rq.get_queue(__RQ_QUEUE_NAME) queue = django_rq.get_queue(__RQ_QUEUE_NAME)
rq_id = "dextr.create/{}/{}".format(jid, username) rq_id = "dextr.create/{}/{}".format(jid, username)

@ -5,5 +5,4 @@
from cvat.settings.base import JS_3RDPARTY 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). __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. __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: __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:

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

@ -56,8 +56,7 @@ class SegmentAdmin(admin.ModelAdmin):
class TaskAdmin(admin.ModelAdmin): class TaskAdmin(admin.ModelAdmin):
date_hierarchy = 'updated_date' date_hierarchy = 'updated_date'
readonly_fields = ('size', 'path', 'created_date', 'updated_date', readonly_fields = ('size', 'created_date', 'updated_date', 'overlap', 'flipped')
'overlap', 'flipped')
list_display = ('name', 'mode', 'owner', 'assignee', 'created_date', 'updated_date') list_display = ('name', 'mode', 'owner', 'assignee', 'created_date', 'updated_date')
search_fields = ('name', 'mode', 'owner__username', 'owner__first_name', search_fields = ('name', 'mode', 'owner__username', 'owner__first_name',
'owner__last_name', 'owner__email', 'assignee__username', 'assignee__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 from django.apps import AppConfig
class EngineConfig(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({ clogger = dotdict({
'task': TaskClientLoggerStorage(), 'task': TaskClientLoggerStorage(),
'job': JobClientLoggerStorage() 'job': JobClientLoggerStorage(),
'glob': logging.getLogger('cvat.client'),
}) })
slogger = dotdict({ 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 # SPDX-License-Identifier: MIT
from enum import Enum
import shlex
import os
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
from io import StringIO class SafeCharField(models.CharField):
from enum import Enum def get_prep_value(self, value):
value = super().get_prep_value(value)
import shlex if value:
import csv return value[:self.max_length]
import re return value
import os
class StatusChoice(Enum): class StatusChoice(str, Enum):
ANNOTATION = 'annotation' ANNOTATION = 'annotation'
VALIDATION = 'validation' VALIDATION = 'validation'
COMPLETED = 'completed' COMPLETED = 'completed'
@classmethod @classmethod
def choices(self): 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): def __str__(self):
return self.value 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): class Task(models.Model):
name = SafeCharField(max_length=256) name = SafeCharField(max_length=256)
size = models.PositiveIntegerField() size = models.PositiveIntegerField()
path = models.CharField(max_length=256)
mode = models.CharField(max_length=32) mode = models.CharField(max_length=32)
owner = models.ForeignKey(User, null=True, blank=True, owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="owners") 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="") bug_tracker = models.CharField(max_length=2000, blank=True, default="")
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_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) z_order = models.BooleanField(default=False)
flipped = models.BooleanField(default=False) flipped = models.BooleanField(default=False)
source = SafeCharField(max_length=256, default="unknown") image_quality = models.PositiveSmallIntegerField(default=50)
status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
# Extend default permission model # Extend default permission model
class Meta: class Meta:
default_permissions = () 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): 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): def get_data_dirname(self):
return os.path.join(self.path, "data") return os.path.join(self.get_task_dirname(), "data")
def get_dump_path(self):
name = re.sub(r'[\\/*?:"<>|]', '_', self.name)
return os.path.join(self.path, "{}.xml".format(name))
def get_log_path(self): 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): 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): def get_image_meta_cache_path(self):
return os.path.join(self.path, "image_meta.cache") return os.path.join(self.get_task_dirname(), "image_meta.cache")
def set_task_dirname(self, path):
self.path = path
self.save(update_fields=['path'])
def get_task_dirname(self): def get_task_dirname(self):
return self.path return os.path.join(settings.DATA_ROOT, str(self.id))
def __str__(self): def __str__(self):
return self.name 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): class Segment(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE)
start_frame = models.IntegerField() start_frame = models.IntegerField()
@ -96,8 +158,8 @@ class Segment(models.Model):
class Job(models.Model): class Job(models.Model):
segment = models.ForeignKey(Segment, on_delete=models.CASCADE) segment = models.ForeignKey(Segment, on_delete=models.CASCADE)
assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
status = models.CharField(max_length=32, default=StatusChoice.ANNOTATION) status = models.CharField(max_length=32, choices=StatusChoice.choices(),
max_shape_id = models.BigIntegerField(default=-1) default=StatusChoice.ANNOTATION)
class Meta: class Meta:
default_permissions = () default_permissions = ()
@ -111,53 +173,37 @@ class Label(models.Model):
class Meta: class Meta:
default_permissions = () default_permissions = ()
unique_together = ('task', 'name')
class AttributeType(str, Enum):
CHECKBOX = 'checkbox'
RADIO = 'radio'
NUMBER = 'number'
TEXT = 'text'
SELECT = 'select'
def parse_attribute(text): @classmethod
match = re.match(r'^([~@])(\w+)=(\w+):(.+)?$', text) def choices(self):
prefix = match.group(1) return tuple((x.value, x.name) for x in self)
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':type, 'name':name, 'values':values} def __str__(self):
return self.value
class AttributeSpec(models.Model): class AttributeSpec(models.Model):
label = models.ForeignKey(Label, on_delete=models.CASCADE) 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: class Meta:
default_permissions = () default_permissions = ()
unique_together = ('label', 'name')
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']
def __str__(self): def __str__(self):
return self.get_attribute()['name'] return self.name
class AttributeVal(models.Model): class AttributeVal(models.Model):
# TODO: add a validator here to be sure that it corresponds to self.label # TODO: add a validator here to be sure that it corresponds to self.label
@ -169,103 +215,112 @@ class AttributeVal(models.Model):
abstract = True abstract = True
default_permissions = () 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): 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) label = models.ForeignKey(Label, on_delete=models.CASCADE)
frame = models.PositiveIntegerField() frame = models.PositiveIntegerField()
group_id = models.PositiveIntegerField(default=0) group = models.PositiveIntegerField(null=True)
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)
class Meta: class Meta:
abstract = True abstract = True
default_permissions = () default_permissions = ()
class BoundingBox(Shape): class Commit(models.Model):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
xtl = models.FloatField() author = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
ytl = models.FloatField() version = models.PositiveIntegerField(default=0)
xbr = models.FloatField() timestamp = models.DateTimeField(auto_now=True)
ybr = models.FloatField() message = models.CharField(max_length=4096, default="")
class Meta: class Meta:
abstract = True abstract = True
default_permissions = () default_permissions = ()
class PolyShape(Shape): class JobCommit(Commit):
id = models.BigAutoField(primary_key=True) job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="commits")
points = models.TextField()
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: class Meta:
abstract = True abstract = True
default_permissions = () default_permissions = ()
class LabeledBox(Annotation, BoundingBox): class LabeledImage(Annotation):
pass pass
class LabeledBoxAttributeVal(AttributeVal): class LabeledImageAttributeVal(AttributeVal):
box = models.ForeignKey(LabeledBox, on_delete=models.CASCADE) image = models.ForeignKey(LabeledImage, on_delete=models.CASCADE)
class LabeledPolygon(Annotation, PolyShape): class LabeledShape(Annotation, Shape):
pass pass
class LabeledPolygonAttributeVal(AttributeVal): class LabeledShapeAttributeVal(AttributeVal):
polygon = models.ForeignKey(LabeledPolygon, on_delete=models.CASCADE) shape = models.ForeignKey(LabeledShape, on_delete=models.CASCADE)
class LabeledPolyline(Annotation, PolyShape): class LabeledTrack(Annotation):
pass pass
class LabeledPolylineAttributeVal(AttributeVal): class LabeledTrackAttributeVal(AttributeVal):
polyline = models.ForeignKey(LabeledPolyline, on_delete=models.CASCADE) track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
class LabeledPoints(Annotation, PolyShape):
pass
class LabeledPointsAttributeVal(AttributeVal): class TrackedShape(Shape):
points = models.ForeignKey(LabeledPoints, on_delete=models.CASCADE)
class ObjectPath(Annotation):
id = models.BigAutoField(primary_key=True) id = models.BigAutoField(primary_key=True)
shapes = models.CharField(max_length=10, default='boxes') track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
class ObjectPathAttributeVal(AttributeVal):
track = models.ForeignKey(ObjectPath, on_delete=models.CASCADE)
class TrackedObject(models.Model):
track = models.ForeignKey(ObjectPath, on_delete=models.CASCADE)
frame = models.PositiveIntegerField() frame = models.PositiveIntegerField()
outside = models.BooleanField(default=False) outside = models.BooleanField(default=False)
class Meta:
abstract = True
default_permissions = ()
class TrackedBox(TrackedObject, BoundingBox): class TrackedShapeAttributeVal(AttributeVal):
pass shape = models.ForeignKey(TrackedShape, on_delete=models.CASCADE)
class TrackedBoxAttributeVal(AttributeVal):
box = models.ForeignKey(TrackedBox, on_delete=models.CASCADE)
class TrackedPolygon(TrackedObject, PolyShape): class Plugin(models.Model):
pass name = models.SlugField(max_length=32, primary_key=True)
description = SafeCharField(max_length=8192)
class TrackedPolygonAttributeVal(AttributeVal): maintainer = models.ForeignKey(User, null=True, blank=True,
polygon = models.ForeignKey(TrackedPolygon, on_delete=models.CASCADE) 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): # Extend default permission model
pass class Meta:
default_permissions = ()
class TrackedPolylineAttributeVal(AttributeVal):
polyline = models.ForeignKey(TrackedPolyline, on_delete=models.CASCADE)
class TrackedPoints(TrackedObject, PolyShape):
pass
class TrackedPointsAttributeVal(AttributeVal): class PluginOption(models.Model):
points = models.ForeignKey(TrackedPoints, on_delete=models.CASCADE) 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 /* global
PolyShapeModel:false PolyShapeModel:false
LabelsInfo:false
*/ */
"use strict";
class AnnotationParser { class AnnotationParser {
constructor(job, labelsInfo, idGenerator) { constructor(job, labelsInfo) {
this._parser = new DOMParser(); this._parser = new DOMParser();
this._startFrame = job.start; this._startFrame = job.start;
this._stopFrame = job.stop; this._stopFrame = job.stop;
this._flipped = job.flipped; this._flipped = job.flipped;
this._im_meta = job.image_meta_data; this._im_meta = job.image_meta_data;
this._labelsInfo = labelsInfo; this._labelsInfo = labelsInfo;
this._idGen = idGenerator;
} }
_xmlParseError(parsedXML) { _xmlParseError(parsedXML) {
return parsedXML.getElementsByTagName("parsererror"); return parsedXML.getElementsByTagName('parsererror');
} }
_getBoxPosition(box, frame) { _getBoxPosition(box, frame) {
frame = Math.min(frame - this._startFrame, this._im_meta['original_size'].length - 1); frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
let im_w = this._im_meta['original_size'][frame].width; const imWidth = this._im_meta[frame].width;
let im_h = this._im_meta['original_size'][frame].height; const imHeight = this._im_meta[frame].height;
let xtl = +box.getAttribute('xtl'); let xtl = +box.getAttribute('xtl');
let ytl = +box.getAttribute('ytl'); let ytl = +box.getAttribute('ytl');
let xbr = +box.getAttribute('xbr'); let xbr = +box.getAttribute('xbr');
let ybr = +box.getAttribute('ybr'); let ybr = +box.getAttribute('ybr');
if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0 || if (xtl < 0 || ytl < 0 || xbr < 0 || ybr < 0
xtl > im_w || ytl > im_h || xbr > im_w || ybr > im_h) { || xtl > imWidth || ytl > imHeight || xbr > imWidth || ybr > imHeight) {
let message = `Incorrect bb found in annotation file: xtl=${xtl} ytl=${ytl} xbr=${xbr} ybr=${ybr}. `; const message = `Incorrect bb found in annotation file: xtl=${xtl} `
message += `Box out of range: ${im_w}x${im_h}`; + `ytl=${ytl} xbr=${xbr} ybr=${ybr}. `
+ `Box out of range: ${imWidth}x${imHeight}`;
throw Error(message); throw Error(message);
} }
if (this._flipped) { if (this._flipped) {
let _xtl = im_w - xbr; [xtl, ytl, xbr, ybr] = [
let _xbr = im_w - xtl; imWidth - xbr,
let _ytl = im_h - ybr; imWidth - xtl,
let _ybr = im_h - ytl; imHeight - ybr,
xtl = _xtl; imHeight - ytl,
ytl = _ytl; ];
xbr = _xbr;
ybr = _ybr;
} }
let occluded = +box.getAttribute('occluded'); const occluded = box.getAttribute('occluded');
let z_order = box.getAttribute('z_order') || '0'; const zOrder = box.getAttribute('z_order') || '0';
return [xtl, ytl, xbr, ybr, occluded, +z_order]; return [[xtl, ytl, xbr, ybr], +occluded, +zOrder];
} }
_getPolyPosition(shape, frame) { _getPolyPosition(shape, frame) {
frame = Math.min(frame - this._startFrame, this._im_meta['original_size'].length - 1); frame = Math.min(frame - this._startFrame, this._im_meta.length - 1);
let im_w = this._im_meta['original_size'][frame].width; const imWidth = this._im_meta[frame].width;
let im_h = this._im_meta['original_size'][frame].height; const imHeight = this._im_meta[frame].height;
let points = shape.getAttribute('points').split(';').join(' '); let points = shape.getAttribute('points').split(';').join(' ');
points = PolyShapeModel.convertStringToNumberArray(points); points = PolyShapeModel.convertStringToNumberArray(points);
for (let point of points) { for (const point of points) {
if (point.x < 0 || point.y < 0 || point.x > im_w || point.y > im_h) { if (point.x < 0 || point.y < 0 || point.x > imWidth || point.y > imHeight) {
let message = `Incorrect point found in annotation file x=${point.x} y=${point.y}. `; const message = `Incorrect point found in annotation file x=${point.x} `
message += `Point out of range ${im_w}x${im_h}`; + `y=${point.y}. Point out of range ${imWidth}x${imHeight}`;
throw Error(message); throw Error(message);
} }
if (this._flipped) { if (this._flipped) {
point.x = im_w - point.x; point.x = imWidth - point.x;
point.y = im_h - point.y; point.y = imHeight - point.y;
} }
} }
points = PolyShapeModel.convertNumberArrayToString(points);
let occluded = +shape.getAttribute('occluded'); points = points.reduce((acc, el) => {
let z_order = shape.getAttribute('z_order') || '0'; acc.push(el.x, el.y);
return [points, occluded, +z_order]; return acc;
}, []);
const occluded = shape.getAttribute('occluded');
const zOrder = shape.getAttribute('z_order') || '0';
return [points, +occluded, +zOrder];
} }
_getAttribute(labelId, attrTag) { _getAttribute(labelId, attrTag) {
let name = attrTag.getAttribute('name'); const name = attrTag.getAttribute('name');
let attrId = this._labelsInfo.attrIdOf(labelId, name); const attrId = this._labelsInfo.attrIdOf(labelId, name);
if (attrId === null) { 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); const attrInfo = this._labelsInfo.attrInfo(attrId);
let value = this._labelsInfo.strToValues(attrInfo.type, attrTag.textContent)[0]; const value = LabelsInfo.normalize(attrInfo.type, attrTag.textContent);
if (['select', 'radio'].includes(attrInfo.type) && !attrInfo.values.includes(value)) { if (['select', 'radio'].includes(attrInfo.type) && !attrInfo.values.includes(value)) {
throw Error('Incorrect attribute value found for "' + name + '" attribute: ' + value); throw Error(`Incorrect attribute value found for "${name}" + attribute: "${value}"`);
} } else if (attrInfo.type === 'number') {
else if (attrInfo.type === 'number') { if (Number.isNaN(+value)) {
if (isNaN(+value)) { throw Error(`Incorrect attribute value found for "${name}" attribute: "${value}". Value must be a number.`);
throw Error('Incorrect attribute value found for "' + name + '" attribute: ' + value + '. Value must be a number.'); } else {
} const min = +attrInfo.values[0];
else { const max = +attrInfo.values[1];
let min = +attrInfo.values[0];
let max = +attrInfo.values[1];
if (+value < min || +value > max) { 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) { _getAttributeList(shape, labelId) {
let attributeDict = {}; const attributeDict = {};
let attributes = shape.getElementsByTagName('attribute'); const attributes = shape.getElementsByTagName('attribute');
for (let attribute of attributes ) { for (const attribute of attributes) {
let [id, value] = this._getAttribute(labelId, attribute); const [id, value] = this._getAttribute(labelId, attribute);
attributeDict[id] = value; attributeDict[id] = value;
} }
let attributeList = []; const attributeList = [];
for (let attrId in attributeDict) { for (const attrId in attributeDict) {
attributeList.push({ if (Object.prototype.hasOwnProperty.call(attributeDict, attrId)) {
id: attrId, attributeList.push({
value: attributeDict[attrId], spec_id: attrId,
}); value: attributeDict[attrId],
});
}
} }
return attributeList; return attributeList;
} }
_getShapeFromPath(shape_type, tracks) { _getShapeFromPath(shapeType, tracks) {
let result = []; const result = [];
for (let track of tracks) { for (const track of tracks) {
let label = track.getAttribute('label'); const label = track.getAttribute('label');
let group_id = track.getAttribute('group_id') || '0'; const group = track.getAttribute('group_id') || '0';
let labelId = this._labelsInfo.labelIdOf(label); const labelId = this._labelsInfo.labelIdOf(label);
if (labelId === null) { if (labelId === null) {
throw Error(`An unknown label found in the annotation file: ${label}`); throw Error(`An unknown label found in the annotation file: ${label}`);
} }
let shapes = Array.from(track.getElementsByTagName(shape_type)); const shapes = Array.from(track.getElementsByTagName(shapeType));
shapes.sort((a,b) => +a.getAttribute('frame') - + b.getAttribute('frame')); shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
while (shapes.length && +shapes[0].getAttribute('outside')) { while (shapes.length && +shapes[0].getAttribute('outside')) {
shapes.shift(); shapes.shift();
} }
if (shapes.length === 2) { if (shapes.length === 2) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1 && if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
!+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) { && !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
shapes[0].setAttribute('label', label); shapes[0].setAttribute('label', label);
shapes[0].setAttribute('group_id', group_id); shapes[0].setAttribute('group_id', group);
result.push(shapes[0]); result.push(shapes[0]);
} }
} }
@ -164,87 +165,93 @@ class AnnotationParser {
} }
_parseAnnotationData(xml) { _parseAnnotationData(xml) {
let data = { const data = {
boxes: [], boxes: [],
polygons: [], polygons: [],
polylines: [], polylines: [],
points: [] points: [],
}; };
let tracks = xml.getElementsByTagName('track'); const tracks = xml.getElementsByTagName('track');
let parsed = { const parsed = {
boxes: this._getShapeFromPath('box', tracks), box: this._getShapeFromPath('box', tracks),
polygons: this._getShapeFromPath('polygon', tracks), polygon: this._getShapeFromPath('polygon', tracks),
polylines: this._getShapeFromPath('polyline', tracks), polyline: this._getShapeFromPath('polyline', tracks),
points: this._getShapeFromPath('points', tracks), points: this._getShapeFromPath('points', tracks),
}; };
const shapeTarget = {
box: 'boxes',
polygon: 'polygons',
polyline: 'polylines',
points: 'points',
};
let images = xml.getElementsByTagName('image'); const images = xml.getElementsByTagName('image');
for (let image of images) { for (const image of images) {
let frame = image.getAttribute('id'); const frame = image.getAttribute('id');
for (let box of image.getElementsByTagName('box')) { for (const box of image.getElementsByTagName('box')) {
box.setAttribute('frame', frame); 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); 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); 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); points.setAttribute('frame', frame);
parsed.points.push(points); parsed.points.push(points);
} }
} }
for (let shape_type in parsed) { for (const shapeType in parsed) {
for (let shape of parsed[shape_type]) { if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
let frame = +shape.getAttribute('frame'); for (const shape of parsed[shapeType]) {
if (frame < this._startFrame || frame > this._stopFrame) continue; const frame = +shape.getAttribute('frame');
if (frame < this._startFrame || frame > this._stopFrame) {
continue;
}
let labelId = this._labelsInfo.labelIdOf(shape.getAttribute('label')); const labelId = this._labelsInfo.labelIdOf(shape.getAttribute('label'));
let groupId = shape.getAttribute('group_id') || "0"; const group = shape.getAttribute('group_id') || '0';
if (labelId === null) { if (labelId === null) {
throw Error('An unknown label found in the annotation file: ' + shape.getAttribute('label')); throw Error(`An unknown label found in the annotation file: "${shape.getAttribute('label')}"`);
} }
let attributeList = this._getAttributeList(shape, labelId); const attributeList = this._getAttributeList(shape, labelId);
if (shape_type === 'boxes') { if (shapeType === 'box') {
let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, frame); const [points, occluded, zOrder] = this._getBoxPosition(shape, frame);
data.boxes.push({ data[shapeTarget[shapeType]].push({
label_id: labelId, label_id: labelId,
group_id: +groupId, group: +group,
frame: frame, attributes: attributeList,
occluded: occluded, type: 'rectangle',
xtl: xtl, z_order: zOrder,
ytl: ytl, frame,
xbr: xbr, occluded,
ybr: ybr, points,
z_order: z_order, });
attributes: attributeList, } else {
id: this._idGen.next(), const [points, occluded, zOrder] = this._getPolyPosition(shape, frame);
}); data[shapeTarget[shapeType]].push({
} label_id: labelId,
else { group: +group,
let [points, occluded, z_order] = this._getPolyPosition(shape, frame); attributes: attributeList,
data[shape_type].push({ type: shapeType,
label_id: labelId, z_order: zOrder,
group_id: +groupId, frame,
frame: frame, points,
points: points, occluded,
occluded: occluded, });
z_order: z_order, }
attributes: attributeList,
id: this._idGen.next(),
});
} }
} }
} }
@ -253,76 +260,81 @@ class AnnotationParser {
} }
_parseInterpolationData(xml) { _parseInterpolationData(xml) {
let data = { const data = {
box_paths: [], box_paths: [],
polygon_paths: [], polygon_paths: [],
polyline_paths: [], polyline_paths: [],
points_paths: [] points_paths: [],
}; };
let tracks = xml.getElementsByTagName('track'); const tracks = xml.getElementsByTagName('track');
for (let track of tracks) { for (const track of tracks) {
let labelId = this._labelsInfo.labelIdOf(track.getAttribute('label')); const labelId = this._labelsInfo.labelIdOf(track.getAttribute('label'));
let groupId = track.getAttribute('group_id') || '0'; const group = track.getAttribute('group_id') || '0';
if (labelId === null) { 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 = { const parsed = {
boxes: Array.from(track.getElementsByTagName('box')), box: Array.from(track.getElementsByTagName('box')),
polygons: Array.from(track.getElementsByTagName('polygon')), polygon: Array.from(track.getElementsByTagName('polygon')),
polylines: Array.from(track.getElementsByTagName('polyline')), polyline: Array.from(track.getElementsByTagName('polyline')),
points: Array.from(track.getElementsByTagName('points')), points: Array.from(track.getElementsByTagName('points')),
}; };
for (let shape_type in parsed) { for (const shapeType in parsed) {
let shapes = parsed[shape_type]; if (Object.prototype.hasOwnProperty.call(parsed, shapeType)) {
shapes.sort((a,b) => +a.getAttribute('frame') - + b.getAttribute('frame')); const shapes = parsed[shapeType];
shapes.sort((a, b) => +a.getAttribute('frame') - +b.getAttribute('frame'));
while (shapes.length && +shapes[0].getAttribute('outside')) { while (shapes.length && +shapes[0].getAttribute('outside')) {
shapes.shift(); shapes.shift();
} }
if (shapes.length === 2) { if (shapes.length === 2) {
if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1 && if (shapes[1].getAttribute('frame') - shapes[0].getAttribute('frame') === 1
!+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) { && !+shapes[0].getAttribute('outside') && +shapes[1].getAttribute('outside')) {
parsed[shape_type] = []; // pseudo interpolation track (actually is annotation) // pseudo interpolation track (actually is annotation)
parsed[shapeType] = [];
}
} }
} }
} }
let type = null, target = null; let type = null;
if (parsed.boxes.length) { let target = null;
type = 'boxes'; if (parsed.box.length) {
type = 'box';
target = 'box_paths'; target = 'box_paths';
} } else if (parsed.polygon.length) {
else if (parsed.polygons.length) { type = 'polygon';
type = 'polygons';
target = 'polygon_paths'; target = 'polygon_paths';
} } else if (parsed.polyline.length) {
else if (parsed.polylines.length) { type = 'polyline';
type = 'polylines';
target = 'polyline_paths'; target = 'polyline_paths';
} } else if (parsed.points.length) {
else if (parsed.points.length) {
type = 'points'; type = 'points';
target = 'points_paths'; target = 'points_paths';
} else {
continue;
} }
else continue;
let path = { const path = {
label_id: labelId, label_id: labelId,
group_id: +groupId, group: +group,
frame: +parsed[type][0].getAttribute('frame'), frame: +parsed[type][0].getAttribute('frame'),
attributes: [], attributes: [],
shapes: [], shapes: [],
id: this._idGen.next(),
}; };
for (let shape of parsed[type]) { if (path.frame < this._startFrame || path.frame > this._stopFrame) {
let keyFrame = +shape.getAttribute('keyframe'); continue;
let outside = +shape.getAttribute('outside'); }
let frame = +shape.getAttribute('frame');
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. All keyframes are significant.
@ -330,53 +342,53 @@ class AnnotationParser {
Ignore all frames less then start. Ignore all frames less then start.
Ignore all frames more then stop. 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) { if (significant) {
let attributeList = this._getAttributeList(shape, labelId); const attributeList = this._getAttributeList(shape, labelId);
let shapeAttributes = []; const shapeAttributes = [];
let pathAttributes = []; const pathAttributes = [];
for (let attr of attributeList) { for (const attr of attributeList) {
let attrInfo = this._labelsInfo.attrInfo(attr.id); const attrInfo = this._labelsInfo.attrInfo(attr.spec_id);
if (attrInfo.mutable) { if (attrInfo.mutable) {
shapeAttributes.push({ shapeAttributes.push({
id: attr.id, spec_id: attr.spec_id,
value: attr.value, value: attr.value,
}); });
} } else {
else {
pathAttributes.push({ pathAttributes.push({
id: attr.id, spec_id: attr.spec_id,
value: attr.value, value: attr.value,
}); });
} }
} }
path.attributes = pathAttributes; path.attributes = pathAttributes;
if (type === 'boxes') { if (type === 'box') {
let [xtl, ytl, xbr, ybr, occluded, z_order] = this._getBoxPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame)); const [points, occluded, zOrder] = this._getBoxPosition(shape,
Math.clamp(frame, this._startFrame, this._stopFrame));
path.shapes.push({ path.shapes.push({
frame: frame,
occluded: occluded,
outside: outside,
xtl: xtl,
ytl: ytl,
xbr: xbr,
ybr: ybr,
z_order: z_order,
attributes: shapeAttributes, attributes: shapeAttributes,
type: 'rectangle',
frame,
occluded,
outside,
points,
zOrder,
}); });
} } else {
else { const [points, occluded, zOrder] = this._getPolyPosition(shape,
let [points, occluded, z_order] = this._getPolyPosition(shape, Math.clamp(frame, this._startFrame, this._stopFrame)); Math.clamp(frame, this._startFrame, this._stopFrame));
path.shapes.push({ path.shapes.push({
frame: frame,
occluded: occluded,
outside: outside,
points: points,
z_order: z_order,
attributes: shapeAttributes, attributes: shapeAttributes,
type,
frame,
occluded,
outside,
points,
zOrder,
}); });
} }
} }
@ -391,14 +403,33 @@ class AnnotationParser {
} }
parse(text) { parse(text) {
let xml = this._parser.parseFromString(text, 'text/xml'); const xml = this._parser.parseFromString(text, 'text/xml');
let parseerror = this._xmlParseError(xml); const parseerror = this._xmlParseError(xml);
if (parseerror.length) { 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); const interpolationData = this._parseInterpolationData(xml);
let annotationData = this._parseAnnotationData(xml); const annotationData = this._parseAnnotationData(xml);
return Object.assign({}, annotationData, interpolationData);
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 SVG:false
*/ */
"use strict";
const AAMUndefinedKeyword = '__undefined__'; const AAMUndefinedKeyword = '__undefined__';
class AAMModel extends Listener { class AAMModel extends Listener {
@ -31,40 +29,27 @@ class AAMModel extends Listener {
this._currentShapes = []; this._currentShapes = [];
this._attrNumberByLabel = {}; this._attrNumberByLabel = {};
this._helps = {}; 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) { function getHelp(attrId) {
let attrInfo = window.cvat.labelsInfo.attrInfo(attrId); const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
let help = []; const help = [];
switch (attrInfo.type) { switch (attrInfo.type) {
case 'checkbox': case 'checkbox':
help.push('0 - ' + attrInfo.values[0]); help.push(`0 - ${attrInfo.values[0]}`);
help.push('1 - ' + !attrInfo.values[0]); help.push(`1 - ${!attrInfo.values[0]}`);
break; break;
default: default:
for (let idx = 0; idx < attrInfo.values.length; idx ++) { for (let idx = 0; idx < attrInfo.values.length; idx += 1) {
if (idx > 9) break; if (idx > 9) {
if (attrInfo.values[0] === AAMUndefinedKeyword) { break;
if (!idx) continue;
help.push(idx - 1 + ' - ' + attrInfo.values[idx]);
} }
else { if (attrInfo.values[0] === AAMUndefinedKeyword) {
help.push(idx + ' - ' + attrInfo.values[idx]); 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; 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); shapeCollection.subscribe(this);
} }
_bbRect(pos) { _bbRect(pos) {
if ('points' in pos) { if ('points' in pos) {
let points = PolyShapeModel.convertStringToNumberArray(pos.points); const points = PolyShapeModel.convertStringToNumberArray(pos.points);
let xtl = Number.MAX_SAFE_INTEGER; let xtl = Number.MAX_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER; let ytl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER; let xbr = Number.MIN_SAFE_INTEGER;
let ybr = 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); xtl = Math.min(xtl, point.x);
ytl = Math.min(ytl, point.y); ytl = Math.min(ytl, point.y);
xbr = Math.max(xbr, point.x); xbr = Math.max(xbr, point.x);
ybr = Math.max(ybr, point.y); ybr = Math.max(ybr, point.y);
} }
return [xtl, ytl, xbr, ybr]; return [xtl, ytl, xbr, ybr];
}
else {
return [pos.xtl, pos.ytl, pos.xbr, pos.ybr];
} }
return [pos.xtl, pos.ytl, pos.xbr, pos.ybr];
} }
_updateCollection() { _updateCollection() {
this._currentShapes = []; this._currentShapes = [];
for (let shape of this._shapeCollection.currentShapes) { for (const shape of this._shapeCollection.currentShapes) {
let labelAttributes = window.cvat.labelsInfo.labelAttributes(shape.model.label); const labelAttributes = window.cvat.labelsInfo.labelAttributes(shape.model.label);
if (Object.keys(labelAttributes).length && !shape.model.removed && !shape.interpolation.position.outside) { if (Object.keys(labelAttributes).length
&& !shape.model.removed && !shape.interpolation.position.outside) {
this._currentShapes.push({ this._currentShapes.push({
model: shape.model, model: shape.model,
interpolation: shape.model.interpolate(window.cvat.player.frames.current), interpolation: shape.model.interpolate(window.cvat.player.frames.current),
@ -111,8 +117,7 @@ class AAMModel extends Listener {
if (this._currentShapes.length) { if (this._currentShapes.length) {
this._activeIdx = 0; this._activeIdx = 0;
this._active = this._currentShapes[0].model; this._active = this._currentShapes[0].model;
} } else {
else {
this._activeIdx = null; this._activeIdx = null;
this._active = null; this._active = null;
} }
@ -124,15 +129,16 @@ class AAMModel extends Listener {
_activate() { _activate() {
if (this._activeAAM && this._active) { if (this._activeAAM && this._active) {
let label = this._active.label; const { label } = this._active;
let attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current); const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
let [xtl, ytl, xbr, ybr] = this._bbRect(this._currentShapes[this._activeIdx].interpolation.position); const [xtl, ytl, xbr, ybr] = this._bbRect(this._currentShapes[this._activeIdx]
this._focus(xtl - this._margin, xbr + this._margin, ytl - this._margin, ybr + this._margin); .interpolation.position);
this._focus(xtl - this._margin, xbr + this._margin,
ytl - this._margin, ybr + this._margin);
this.notify(); this.notify();
this._active.activeAttribute = attrId; this._active.activeAttribute = attrId;
} } else {
else {
this.notify(); this.notify();
} }
} }
@ -170,8 +176,7 @@ class AAMModel extends Listener {
switchAAMMode() { switchAAMMode() {
if (this._activeAAM) { if (this._activeAAM) {
this._disable(); this._disable();
} } else {
else {
this._enable(); this._enable();
} }
} }
@ -184,14 +189,13 @@ class AAMModel extends Listener {
this._deactivate(); this._deactivate();
if (Math.sign(direction) < 0) { if (Math.sign(direction) < 0) {
// next // next
this._activeIdx ++; this._activeIdx += 1;
if (this._activeIdx >= this._currentShapes.length) { if (this._activeIdx >= this._currentShapes.length) {
this._activeIdx = 0; this._activeIdx = 0;
} }
} } else {
else {
// prev // prev
this._activeIdx --; this._activeIdx -= 1;
if (this._activeIdx < 0) { if (this._activeIdx < 0) {
this._activeIdx = this._currentShapes.length - 1; this._activeIdx = this._currentShapes.length - 1;
} }
@ -206,7 +210,7 @@ class AAMModel extends Listener {
return; return;
} }
let curAttr = this._attrNumberByLabel[this._active.label]; const curAttr = this._attrNumberByLabel[this._active.label];
if (curAttr.end < 2) { if (curAttr.end < 2) {
return; return;
@ -214,14 +218,13 @@ class AAMModel extends Listener {
if (Math.sign(direction) > 0) { if (Math.sign(direction) > 0) {
// next // next
curAttr.current ++; curAttr.current += 1;
if (curAttr.current >= curAttr.end) { if (curAttr.current >= curAttr.end) {
curAttr.current = 0; curAttr.current = 0;
} }
} } else {
else {
// prev // prev
curAttr.current --; curAttr.current -= 1;
if (curAttr.current < 0) { if (curAttr.current < 0) {
curAttr.current = curAttr.end - 1; curAttr.current = curAttr.end - 1;
} }
@ -233,10 +236,10 @@ class AAMModel extends Listener {
if (!this._activeAAM || !this._active) { if (!this._activeAAM || !this._active) {
return; return;
} }
let label = this._active.label; const { label } = this._active;
let frame = window.cvat.player.frames.current; const frame = window.cvat.player.frames.current;
let attrId = this._attrIdByIdx(label, this._attrNumberByLabel[label].current); const attrId = this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
let attrInfo = window.cvat.labelsInfo.attrInfo(attrId); const attrInfo = window.cvat.labelsInfo.attrInfo(attrId);
if (key >= attrInfo.values.length) { if (key >= attrInfo.values.length) {
if (attrInfo.type === 'checkbox' && key < 2) { if (attrInfo.type === 'checkbox' && key < 2) {
this._active.updateAttribute(frame, attrId, !attrInfo.values[0]); this._active.updateAttribute(frame, attrId, !attrInfo.values[0]);
@ -247,7 +250,7 @@ class AAMModel extends Listener {
if (key >= attrInfo.values.length - 1) { if (key >= attrInfo.values.length - 1) {
return; return;
} }
key ++; key += 1;
} }
this._active.updateAttribute(frame, attrId, attrInfo.values[key]); this._active.updateAttribute(frame, attrId, attrInfo.values[key]);
} }
@ -262,13 +265,11 @@ class AAMModel extends Listener {
generateHelps() { generateHelps() {
if (this._active) { if (this._active) {
let label = this._active.label; const { label } = this._active;
let attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current); const attrId = +this._attrIdByIdx(label, this._attrNumberByLabel[label].current);
return [this._helps[attrId].title, this._helps[attrId].help, `${this._activeIdx + 1}/${this._currentShapes.length}`]; 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() { get activeAAM() {
@ -285,60 +286,57 @@ class AAMModel extends Listener {
} }
class AAMController { class AAMController {
constructor(aamModel) { constructor(aamModel) {
this._model = aamModel; this._model = aamModel;
setupAAMShortkeys.call(this);
function setupAAMShortkeys() { function setupAAMShortkeys() {
let switchAAMHandler = Logger.shortkeyLogDecorator(function() { const switchAAMHandler = Logger.shortkeyLogDecorator(() => {
this._model.switchAAMMode(); this._model.switchAAMMode();
}.bind(this)); });
let nextAttributeHandler = Logger.shortkeyLogDecorator(function(e) { const nextAttributeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveAttr(1); this._model.moveAttr(1);
e.preventDefault(); e.preventDefault();
}.bind(this)); });
let prevAttributeHandler = Logger.shortkeyLogDecorator(function(e) { const prevAttributeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveAttr(-1); this._model.moveAttr(-1);
e.preventDefault(); e.preventDefault();
}.bind(this)); });
let nextShapeHandler = Logger.shortkeyLogDecorator(function(e) { const nextShapeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveShape(1); this._model.moveShape(1);
e.preventDefault(); e.preventDefault();
}.bind(this)); });
let prevShapeHandler = Logger.shortkeyLogDecorator(function(e) { const prevShapeHandler = Logger.shortkeyLogDecorator((e) => {
this._model.moveShape(-1); this._model.moveShape(-1);
e.preventDefault(); e.preventDefault();
}.bind(this)); });
let selectAttributeHandler = Logger.shortkeyLogDecorator(function(e) { const selectAttributeHandler = Logger.shortkeyLogDecorator((e) => {
let key = e.keyCode; let key = e.keyCode;
if (key >= 48 && key <= 57) { if (key >= 48 && key <= 57) {
key -= 48; // 0 and 9 key -= 48; // 0 and 9
} } else if (key >= 96 && key <= 105) {
else if (key >= 96 && key <= 105) {
key -= 96; // num 0 and 9 key -= 96; // num 0 and 9
} } else {
else {
return; return;
} }
this._model.setupAttributeValue(key); this._model.setupAttributeValue(key);
}.bind(this)); });
let shortkeys = window.cvat.config.shortkeys; const { shortkeys } = window.cvat.config;
Mousetrap.bind(shortkeys["switch_aam_mode"].value, switchAAMHandler, 'keydown'); Mousetrap.bind(shortkeys.switch_aam_mode.value, switchAAMHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_next_attribute"].value, nextAttributeHandler, 'keydown'); Mousetrap.bind(shortkeys.aam_next_attribute.value, nextAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_prev_attribute"].value, prevAttributeHandler, 'keydown'); Mousetrap.bind(shortkeys.aam_prev_attribute.value, prevAttributeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_next_shape"].value, nextShapeHandler, 'keydown'); Mousetrap.bind(shortkeys.aam_next_shape.value, nextShapeHandler, 'keydown');
Mousetrap.bind(shortkeys["aam_prev_shape"].value, prevShapeHandler, 'keydown'); Mousetrap.bind(shortkeys.aam_prev_shape.value, prevShapeHandler, 'keydown');
Mousetrap.bind(shortkeys["select_i_attribute"].value, selectAttributeHandler, 'keydown'); Mousetrap.bind(shortkeys.select_i_attribute.value, selectAttributeHandler, 'keydown');
} }
setupAAMShortkeys.call(this);
} }
setMargin(value) { setMargin(value) {
@ -359,15 +357,15 @@ class AAMView {
this._controller = aamController; this._controller = aamController;
this._zoomMargin.on('change', (e) => { this._zoomMargin.on('change', (e) => {
let value = +e.target.value; const value = +e.target.value;
this._controller.setMargin(value); this._controller.setMargin(value);
}).trigger('change'); }).trigger('change');
aamModel.subscribe(this); aamModel.subscribe(this);
} }
_setupAAMView(active, type, pos) { _setupAAMView(active, type, pos) {
let oldRect = $('#outsideRect'); const oldRect = $('#outsideRect');
let oldMask = $('#outsideMask'); const oldMask = $('#outsideMask');
if (active) { if (active) {
if (oldRect.length) { if (oldRect.length) {
@ -375,45 +373,47 @@ class AAMView {
oldMask.remove(); oldMask.remove();
} }
let size = window.cvat.translate.box.actualToCanvas({ const size = window.cvat.translate.box.actualToCanvas({
x: 0, x: 0,
y: 0, y: 0,
width: window.cvat.player.geometry.frameWidth, 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; let includeField = null;
if (type === 'box') { if (type === 'box') {
pos = window.cvat.translate.box.actualToCanvas(pos); pos = window.cvat.translate.box.actualToCanvas(pos);
includeField = this._frameContent.rect(pos.xbr - pos.xtl, pos.ybr - pos.ytl).move(pos.xtl, pos.ytl); includeField = this._frameContent.rect(pos.xbr - pos.xtl,
} pos.ybr - pos.ytl).move(pos.xtl, pos.ytl);
else { } else {
pos.points = window.cvat.translate.points.actualToCanvas(pos.points); pos.points = window.cvat.translate.points.actualToCanvas(pos.points);
includeField = this._frameContent.polygon(pos.points); includeField = this._frameContent.polygon(pos.points);
} }
this._frameContent.mask().add(excludeField).add(includeField).fill('black').attr('id', 'outsideMask'); this._frameContent.mask().add(excludeField)
this._frameContent.rect(size.width, size.height).move(size.x, size.y).attr({ .add(includeField).fill('black')
mask: 'url(#outsideMask)', .attr('id', 'outsideMask');
id: 'outsideRect' this._frameContent.rect(size.width, size.height)
}); .move(size.x, size.y).attr({
mask: 'url(#outsideMask)',
id: 'outsideRect',
});
let content = $(this._frameContent.node); const content = $(this._frameContent.node);
let texts = content.find('.shapeText'); const texts = content.find('.shapeText');
for (let text of texts) { for (const text of texts) {
content.append(text); content.append(text);
} }
} } else {
else {
oldRect.remove(); oldRect.remove();
oldMask.remove(); oldMask.remove();
} }
} }
onAAMUpdate(aam) { onAAMUpdate(aam) {
this._setupAAMView(aam.active ? true : false, this._setupAAMView(Boolean(aam.active),
aam.active ? aam.active.type.split('_')[1] : '', aam.active ? aam.active.type.split('_')[1] : '',
aam.active ? aam.active.interpolate(window.cvat.player.frames.current).position : 0); aam.active ? aam.active.interpolate(window.cvat.player.frames.current).position : 0);
@ -423,20 +423,17 @@ class AAMView {
this._aamMenu.removeClass('hidden'); this._aamMenu.removeClass('hidden');
} }
let [title, help, counter] = aam.generateHelps(); const [title, help, counter] = aam.generateHelps();
this._aamHelpContainer.empty(); this._aamHelpContainer.empty();
this._aamCounter.text(counter); this._aamCounter.text(counter);
this._aamTitle.text(title); this._aamTitle.text(title);
for (let helpRow of help) { for (const helpRow of help) {
$(`<label> ${helpRow} <label> <br>`).appendTo(this._aamHelpContainer); $(`<label> ${helpRow} <label> <br>`).appendTo(this._aamHelpContainer);
} }
} } else if (this._trackManagement.hasClass('hidden')) {
else { this._aamMenu.addClass('hidden');
if (this._trackManagement.hasClass('hidden')) { this._trackManagement.removeClass('hidden');
this._aamMenu.addClass('hidden');
this._trackManagement.removeClass('hidden');
}
} }
} }
} }

@ -6,10 +6,7 @@
/* exported /* exported
userConfirm userConfirm
createExportContainer
dumpAnnotationRequest dumpAnnotationRequest
ExportType
getExportTargetContainer
showMessage showMessage
showOverlay showOverlay
*/ */
@ -18,54 +15,84 @@
Cookies:false Cookies:false
*/ */
"use strict";
Math.clamp = function(x, min, max) { Math.clamp = (x, min, max) => Math.min(Math.max(x, min), max);
return 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) { function userConfirm(message, onagree, ondisagree) {
let template = $('#confirmTemplate'); const template = $('#confirmTemplate');
let confirmWindow = $(template.html()).css('display', 'block'); const confirmWindow = $(template.html()).css('display', 'block');
let annotationConfirmMessage = confirmWindow.find('.templateMessage'); const annotationConfirmMessage = confirmWindow.find('.templateMessage');
let agreeConfirm = confirmWindow.find('.templateAgreeButton'); const agreeConfirm = confirmWindow.find('.templateAgreeButton');
let disagreeConfirm = confirmWindow.find('.templateDisagreeButton'); const disagreeConfirm = confirmWindow.find('.templateDisagreeButton');
function hideConfirm() {
agreeConfirm.off('click');
disagreeConfirm.off('click');
confirmWindow.remove();
}
annotationConfirmMessage.text(message); annotationConfirmMessage.text(message);
$('body').append(confirmWindow); $('body').append(confirmWindow);
agreeConfirm.on('click', function() { agreeConfirm.on('click', () => {
hideConfirm(); hideConfirm();
if (onagree) onagree(); if (onagree) {
onagree();
}
}); });
disagreeConfirm.on('click', function() { disagreeConfirm.on('click', () => {
hideConfirm(); hideConfirm();
if (ondisagree) ondisagree(); if (ondisagree) {
ondisagree();
}
}); });
disagreeConfirm.focus(); disagreeConfirm.focus();
confirmWindow.on('keydown', (e) => { confirmWindow.on('keydown', (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
function hideConfirm() {
agreeConfirm.off('click');
disagreeConfirm.off('click');
confirmWindow.remove();
}
} }
function showMessage(message) { function showMessage(message) {
let template = $('#messageTemplate'); const template = $('#messageTemplate');
let messageWindow = $(template.html()).css('display', 'block'); const messageWindow = $(template.html()).css('display', 'block');
let messageText = messageWindow.find('.templateMessage'); const messageText = messageWindow.find('.templateMessage');
let okButton = messageWindow.find('.templateOKButton'); const okButton = messageWindow.find('.templateOKButton');
messageText.text(message); messageText.text(message);
$('body').append(messageWindow); $('body').append(messageWindow);
@ -74,7 +101,7 @@ function showMessage(message) {
e.stopPropagation(); e.stopPropagation();
}); });
okButton.on('click', function() { okButton.on('click', () => {
okButton.off('click'); okButton.off('click');
messageWindow.remove(); messageWindow.remove();
}); });
@ -85,15 +112,14 @@ function showMessage(message) {
function showOverlay(message) { function showOverlay(message) {
let template = $('#overlayTemplate'); const template = $('#overlayTemplate');
let overlayWindow = $(template.html()).css('display', 'block'); const overlayWindow = $(template.html()).css('display', 'block');
let overlayText = overlayWindow.find('.templateMessage'); const overlayText = overlayWindow.find('.templateMessage');
overlayWindow[0].setMessage = function(message) {
overlayText.text(message); overlayWindow[0].getMessage = () => overlayText.html();
}; overlayWindow[0].remove = () => overlayWindow.remove();
overlayWindow[0].setMessage = (msg) => {
overlayWindow[0].remove = function() { overlayText.html(msg);
overlayWindow.remove();
}; };
$('body').append(overlayWindow); $('body').append(overlayWindow);
@ -101,151 +127,34 @@ function showOverlay(message) {
return overlayWindow[0]; return overlayWindow[0];
} }
async function dumpAnnotationRequest(tid, taskName) {
function dumpAnnotationRequest(dumpButton, taskID) { const name = encodeURIComponent(`${tid}_${taskName}`);
dumpButton = $(dumpButton); return new Promise((resolve, reject) => {
dumpButton.attr('disabled', true); const url = `/api/v1/tasks/${tid}/annotations/${name}`;
async function request() {
$.ajax({ $.get(url)
url: '/dump/annotation/task/' + taskID, .done((...args) => {
success: onDumpRequestSuccess, if (args[2].status === 202) {
error: onDumpRequestError, setTimeout(request, 3000);
}); } else {
const a = document.createElement('a');
function onDumpRequestSuccess() { a.href = `${url}?action=download`;
let requestInterval = 3000; document.body.appendChild(a);
let requestSended = false; a.click();
a.remove();
let checkInterval = setInterval(function() { resolve();
if (requestSended) return; }
requestSended = true; }).fail((errorData) => {
$.ajax({ const message = `Can not dump annotations for the task. Code: ${errorData.status}. `
url: '/check/annotation/task/' + taskID, + `Message: ${errorData.responseText || errorData.statusText}`;
success: onDumpCheckSuccess, reject(new Error(message));
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)
}); });
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() { setTimeout(request);
const container = {};
Object.keys(ExportType).forEach( action => {
container[action] = {
"boxes": [],
"box_paths": [],
"points": [],
"points_paths": [],
"polygons": [],
"polygon_paths": [],
"polylines": [],
"polyline_paths": [],
};
}); });
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 */ /* These HTTP methods do not require CSRF protection */
function csrfSafeMethod(method) { function csrfSafeMethod(method) {
@ -254,17 +163,17 @@ function csrfSafeMethod(method) {
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) { 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({ $('body').css({
width: window.screen.width + 'px', width: `${window.screen.width}px`,
height: window.screen.height * 0.95 + 'px' height: `${window.screen.height * 0.95}px`,
}); });
}); });

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

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

@ -11,8 +11,8 @@ class CoordinateTranslator {
constructor() { constructor() {
this._boxTranslator = { this._boxTranslator = {
_playerOffset: 0, _playerOffset: 0,
_convert: function(box, sign) { _convert(box, sign) {
for (let prop of ["xtl", "ytl", "xbr", "ybr", "x", "y"]) { for (const prop of ['xtl', 'ytl', 'xbr', 'ybr', 'x', 'y']) {
if (prop in box) { if (prop in box) {
box[prop] += this._playerOffset * sign; box[prop] += this._playerOffset * sign;
} }
@ -20,89 +20,118 @@ class CoordinateTranslator {
return box; return box;
}, },
actualToCanvas: function(actualBox) { actualToCanvas(actualBox) {
let canvasBox = {}; const canvasBox = {};
for (let key in actualBox) { for (const key in actualBox) {
canvasBox[key] = actualBox[key]; canvasBox[key] = actualBox[key];
} }
return this._convert(canvasBox, 1); return this._convert(canvasBox, 1);
}, },
canvasToActual: function(canvasBox) { canvasToActual(canvasBox) {
let actualBox = {}; const actualBox = {};
for (let key in canvasBox) { for (const key in canvasBox) {
actualBox[key] = canvasBox[key]; actualBox[key] = canvasBox[key];
} }
return this._convert(actualBox, -1); return this._convert(actualBox, -1);
}, },
canvasToClient: function(sourceCanvas, canvasBox) { canvasToClient(sourceCanvas, canvasBox) {
let points = [ const points = [
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y), [canvasBox.x, canvasBox.y],
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, canvasBox.y), [canvasBox.x + canvasBox.width, canvasBox.y],
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x, canvasBox.y + canvasBox.height), [canvasBox.x, canvasBox.y + canvasBox.height],
window.cvat.translate.point.canvasToClient(sourceCanvas, canvasBox.x + canvasBox.width, 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); const xes = points.map(el => el.x);
let yes = points.map((el) => el.y); const yes = points.map(el => el.y);
let xmin = Math.min(...xes); const xmin = Math.min(...xes);
let xmax = Math.max(...xes); const xmax = Math.max(...xes);
let ymin = Math.min(...yes); const ymin = Math.min(...yes);
let ymax = Math.max(...yes); const ymax = Math.max(...yes);
return { return {
x: xmin, x: xmin,
y: ymin, y: ymin,
width: xmax - xmin, 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 = { this._pointsTranslator = {
_playerOffset: 0, _playerOffset: 0,
_convert: function(points, sign) { _convert(points, sign) {
if (typeof(points) === 'string') { if (typeof (points) === 'string') {
return points.split(' ').map((coord) => coord.split(',') return points.split(' ').map(coord => coord.split(',')
.map((x) => +x + this._playerOffset * sign).join(',')).join(' '); .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;
} }
else { if (typeof (points) === 'object') {
throw Error('Unknown points type was found'); 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); return this._convert(actualPoints, 1);
}, },
canvasToActual: function(canvasPoints) { canvasToActual(canvasPoints) {
return this._convert(canvasPoints, -1); 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 = { this._pointTranslator = {
_rotation: 0, _rotation: 0,
clientToCanvas: function(targetCanvas, clientX, clientY) { clientToCanvas(targetCanvas, clientX, clientY) {
let pt = targetCanvas.createSVGPoint(); let pt = targetCanvas.createSVGPoint();
pt.x = clientX; pt.x = clientX;
pt.y = clientY; pt.y = clientY;
pt = pt.matrixTransform(targetCanvas.getScreenCTM().inverse()); pt = pt.matrixTransform(targetCanvas.getScreenCTM().inverse());
return pt; return pt;
}, },
canvasToClient: function(sourceCanvas, canvasX, canvasY) { canvasToClient(sourceCanvas, canvasX, canvasY) {
let pt = sourceCanvas.createSVGPoint(); let pt = sourceCanvas.createSVGPoint();
pt.x = canvasX; pt.x = canvasX;
pt.y = canvasY; pt.y = canvasY;
@ -110,18 +139,18 @@ class CoordinateTranslator {
return pt; return pt;
}, },
rotate(x, y, cx, cy) { rotate(x, y, cx, cy) {
cx = (typeof cx === "undefined" ? 0 : cx); cx = (typeof cx === 'undefined' ? 0 : cx);
cy = (typeof cy === "undefined" ? 0 : cy); cy = (typeof cy === 'undefined' ? 0 : cy);
let radians = (Math.PI / 180) * window.cvat.player.rotation; const radians = (Math.PI / 180) * window.cvat.player.rotation;
let cos = Math.cos(radians); const cos = Math.cos(radians);
let sin = Math.sin(radians); const sin = Math.sin(radians);
return { return {
x: (cos * (x - cx)) + (sin * (y - cy)) + cx, 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"; "use strict";
class HistoryModel extends Listener { class HistoryModel extends Listener {
constructor(playerModel, idGenerator) { constructor(playerModel) {
super('onHistoryUpdate', () => this ); super('onHistoryUpdate', () => this );
this._deep = 128; this._deep = 128;
@ -23,15 +23,10 @@ class HistoryModel extends Listener {
this._redo_stack = []; this._redo_stack = [];
this._locked = false; this._locked = false;
this._player = playerModel; this._player = playerModel;
this._idGenerator = idGenerator;
window.cvat.addAction = (name, undo, redo, frame) => this.addAction(name, undo, redo, frame); window.cvat.addAction = (name, undo, redo, frame) => this.addAction(name, undo, redo, frame);
} }
generateId() {
return this._idGenerator.next();
}
undo() { undo() {
let frame = window.cvat.player.frames.current; let frame = window.cvat.player.frames.current;
let undo = this._undo_stack.pop(); let undo = this._undo_stack.pop();
@ -47,7 +42,7 @@ class HistoryModel extends Listener {
this._player.shift(undo.frame, true); this._player.shift(undo.frame, true);
} }
this._locked = true; this._locked = true;
undo.undo(this); undo.undo();
} }
catch(err) { catch(err) {
this.notify(); this.notify();
@ -78,7 +73,7 @@ class HistoryModel extends Listener {
this._player.shift(redo.frame, true); this._player.shift(redo.frame, true);
} }
this._locked = true; this._locked = true;
redo.redo(this); redo.redo();
} }
catch(err) { catch(err) {
this.notify(); 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 */ /* exported LabelsInfo */
/* global class LabelsInfo {
showMessage:false 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 { for (const label of labels) {
constructor(job) { this._labels[label.id] = {
this._labels = new Object; name: label.name,
this._attributes = new Object;
this._colorIdxs = new Object;
for (let labelKey in job.labels) {
let label = {
name: job.labels[labelKey],
attributes: {}, attributes: {},
}; };
for (let attrKey in job.attributes[labelKey]) { for (const attr of label.attributes) {
label.attributes[attrKey] = parseAttributeRow.call(this, job.attributes[labelKey][attrKey]); this._attributes[attr.id] = convertAttribute(attr);
this._attributes[attrKey] = label.attributes[attrKey]; this._labels[label.id].attributes[attr.id] = this._attributes[attr.id];
} }
this._labels[labelKey] = label; this._colorIdxs[label.id] = +label.id;
this._colorIdxs[labelKey] = +labelKey;
} }
function parseAttributeRow(attrRow) { return this;
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]),
};
}
} }
labelColorIdx(labelId) { labelColorIdx(labelId) {
return this._colorIdxs[labelId]; return this._colorIdxs[labelId];
} }
updateLabelColorIdx(labelId) { updateLabelColorIdx(labelId) {
if (labelId in this._colorIdxs) { if (labelId in this._colorIdxs) {
this._colorIdxs[labelId] += 1; 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() { labels() {
let tempLabels = new Object(); const labels = {};
for (let labelId in this._labels) { for (const labelId in this._labels) {
tempLabels[labelId] = this._labels[labelId].name; if (Object.prototype.hasOwnProperty.call(this._labels, labelId)) {
labels[labelId] = this._labels[labelId].name;
}
} }
return tempLabels; return labels;
} }
labelAttributes(labelId) { labelAttributes(labelId) {
let attributes = new Object();
if (labelId in this._labels) { if (labelId in this._labels) {
for (let attrId in this._labels[labelId].attributes) { const attributes = {};
attributes[attrId] = this._labels[labelId].attributes[attrId].name; 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() { attributes() {
let attributes = new Object(); const attributes = {};
for (let attrId in this._attributes) { for (const attrId in this._attributes) {
attributes[attrId] = this._attributes[attrId].name; if (Object.prototype.hasOwnProperty.call(this._attributes, attrId)) {
attributes[attrId] = this._attributes[attrId].name;
}
} }
return attributes; return attributes;
} }
attrInfo(attrId) { attrInfo(attrId) {
let info = new Object();
if (attrId in this._attributes) { if (attrId in this._attributes) {
let object = this._attributes[attrId]; return JSON.parse(JSON.stringify(this._attributes[attrId]));
info.name = object.name;
info.type = object.type;
info.mutable = object.mutable;
info.values = object.values.slice();
} }
return info; throw Error('Unknown attribute ID');
} }
labelIdOf(name) { labelIdOf(name) {
for (let labelId in this._labels) { for (const labelId in this._labels) {
if (this._labels[labelId].name === name) { if (this._labels[labelId].name === name) {
return +labelId; return +labelId;
} }
} }
return null; throw Error('Unknown label name');
} }
attrIdOf(labelId, name) { attrIdOf(labelId, name) {
let attributes = this.labelAttributes(labelId); const attributes = this.labelAttributes(labelId);
for (let attrId in attributes) { for (const attrId in attributes) {
if (this._attributes[attrId].name === name) { if (this._attributes[attrId].name === name) {
return +attrId; return +attrId;
} }
} }
return null; throw Error('Unknown attribute name');
} }
strToValues(type, string) {
switch (type) { static normalize(type, attrValue) {
case 'checkbox': const value = String(attrValue);
return [string !== '0' && string !== 'false' && string !== false]; if (type === 'checkbox') {
case 'text': return value !== '0' && value.toLowerCase() !== 'false';
return [string]; }
default:
return string.toString().split(','); 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"; "use strict";
var UserActivityHandler = function() class UserActivityHandler {
{ constructor() {
this._TIME_TRESHHOLD = 100000; //ms this._TIME_TRESHHOLD = 100000; //ms
this._prevEventTime = Date.now(); this._prevEventTime = Date.now();
this._workingTime = 0;
this._workingTime = 0; }
this.updateTimer = function() updateTimer() {
{
if (document.hasFocus()) { if (document.hasFocus()) {
let now = Date.now(); let now = Date.now();
let diff = now - this._prevEventTime; let diff = now - this._prevEventTime;
this._prevEventTime = now; this._prevEventTime = now;
this._workingTime += diff < this._TIME_TRESHHOLD ? diff : 0; this._workingTime += diff < this._TIME_TRESHHOLD ? diff : 0;
} }
}; }
this.resetTimer = function() resetTimer() {
{
this._prevEventTime = Date.now(); this._prevEventTime = Date.now();
this._workingTime = 0; this._workingTime = 0;
}; }
this.getWorkingTime = function() getWorkingTime() {
{
return this._workingTime; return this._workingTime;
}; }
}; }
class LogCollection extends Array { class LogCollection extends Array {
constructor(logger, items) { constructor(logger, items) {
@ -55,134 +52,116 @@ class LogCollection extends Array {
} }
export() { export() {
return Array.from(this, log => log.toString()); return Array.from(this, log => log.serialize());
} }
} }
var LoggerHandler = function(applicationName, jobId) class LoggerHandler {
{ constructor(jobId) {
this._application = applicationName; this._clientID = Date.now().toString().substr(-6);
this._tabId = Date.now().toString().substr(-6); this._jobId = jobId;
this._jobId = jobId; this._logEvents = [];
this._username = null; this._userActivityHandler = new UserActivityHandler();
this._userActivityHandler = null; this._timeThresholds = {};
this._logEvents = []; }
this._userActivityHandler = new UserActivityHandler();
this._timeThresholds = {}; addEvent(event) {
this.isInitialized = Boolean(this._userActivityHandler);
this.addEvent = function(event)
{
this._pushEvent(event); this._pushEvent(event);
}; }
this.addContinuedEvent = function(event) addContinuedEvent(event) {
{
this._userActivityHandler.updateTimer(); this._userActivityHandler.updateTimer();
event.onCloseCallback = this._closeCallback; event.onCloseCallback = this._closeCallback;
return event; 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 = () => { sendExceptions(exception) {
if (xhr.status == 200) this._extendEvent(exception);
{ return new Promise((resolve, reject) => {
resolve(xhr.response); let retries = 3;
} let makeRequest = () => {
else { 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(); onreject();
} };
}; xhr.send(JSON.stringify(exception.serialize()));
xhr.onerror = () => {
onreject();
}; };
makeRequest();
const data = {'exceptions': Array.from(exceptions, log => log.toString())};
xhr.send(JSON.stringify(data));
}); });
}; }
this.getLogs = function() getLogs() {
{
let logs = new LogCollection(this, this._logEvents); let logs = new LogCollection(this, this._logEvents);
this._logEvents.length = 0; this._logEvents.length = 0;
return logs; return logs;
}; }
this.pushLogs = function(logEvents) pushLogs(logEvents) {
{
Array.prototype.push.apply(this._logEvents, logEvents); Array.prototype.push.apply(this._logEvents, logEvents);
}; }
this._extendEvent = function(event) _extendEvent(event) {
{ event._jobId = this._jobId;
event.addValues({ event._clientId = this._clientID;
application: this._application, }
task: this._jobId,
userid: this._username,
tabid: this._tabId,
focus: document.hasFocus()
});
};
this._pushEvent = function(event) _pushEvent(event) {
{
this._extendEvent(event); this._extendEvent(event);
if (event._type in this._timeThresholds) { if (event._type in this._timeThresholds) {
this._timeThresholds[event._type].wait(event); this._timeThresholds[event._type].wait(event);
} }
else { else {
this._logEvents.push(event); this._logEvents.push(event);
} }
this._userActivityHandler.updateTimer(); this._userActivityHandler.updateTimer();
}; }
this._closeCallback = event => { this._pushEvent(event); }; _closeCallback = event => { this._pushEvent(event); };
this.setUsername = function(username) updateTimer() {
{
this._username = username;
};
this.updateTimer = function()
{
this._userActivityHandler.updateTimer(); this._userActivityHandler.updateTimer();
}; }
this.resetTimer = function() resetTimer() {
{
this._userActivityHandler.resetTimer(); this._userActivityHandler.resetTimer();
}; }
this.getWorkingTime = function() getWorkingTime() {
{
return this._userActivityHandler.getWorkingTime(); return this._userActivityHandler.getWorkingTime();
}; }
this.setTimeThreshold = function(eventType, threshold) setTimeThreshold(eventType, threshold) {
{
this._timeThresholds[eventType] = { this._timeThresholds[eventType] = {
_threshold: threshold, _threshold: threshold,
_timeoutHandler: null, _timeoutHandler: null,
@ -191,27 +170,26 @@ var LoggerHandler = function(applicationName, jobId)
_logEvents: this._logEvents, _logEvents: this._logEvents,
wait: function(event) { wait: function(event) {
if (this._event) { if (this._event) {
if (this._timeoutHandler) clearTimeout(this._timeoutHandler); if (this._timeoutHandler) {
clearTimeout(this._timeoutHandler);
}
} }
else { else {
this._timestamp = event._timestamp; this._timestamp = event._timestamp;
} }
this._event = event; this._event = event;
this._timeoutHandler = setTimeout(() => {
this._timeoutHandler = setTimeout( () => {
if ('duration' in this._event._values) { if ('duration' in this._event._values) {
this._event._values.duration += this._event._timestamp - this._timestamp; this._event._values.duration += this._event._timestamp - this._timestamp;
} }
this._event._timestamp = this._timestamp; this._event._timestamp = this._timestamp;
this._logEvents.push(this._event); this._logEvents.push(this._event);
this._event = null; this._event = null;
}, threshold); }, threshold);
}, },
}; };
}; }
}; }
/* /*
@ -242,8 +220,30 @@ are Logger.EventType.addObject, Logger.EventType.deleteObject and
Logger.EventType.sendTaskInfo. Value of "count" property should be a number. 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 * @private
*/ */
@ -256,26 +256,25 @@ var Logger = {
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'} * @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 * @param {Function} closeCallback callback function which will be called by close method. Setted by
*/ */
LogEvent: function(type, values, closeCallback=null) LogEvent: class extends LoggerEvent {
{ constructor(type, values, message) {
this._type = type; super(type, message);
this._timestamp = Date.now();
this.onCloseCallback = closeCallback; this._timestamp = Date.now();
this.onCloseCallback = null;
this._values = values || {}; this._is_active = document.hasFocus();
this._values = values || {};
}
this.toString = function() serialize() {
{ return Object.assign(super.serialize(), {
return Object.assign({ payload: this._values,
event: Logger.eventTypeToString(this._type), is_active: this._is_active,
timestamp: this._timestamp, });
}, this._values);
}; };
this.close = function(endTimestamp) close(endTimestamp) {
{ if (this.onCloseCallback) {
if (this.onCloseCallback)
{
this.addValues({ this.addValues({
duration: endTimestamp ? endTimestamp - this._timestamp : Date.now() - this._timestamp, 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); 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. * Logger.EventType Enumeration.
*/ */
@ -375,12 +397,11 @@ var Logger = {
* @return {Bool} true if initialization was succeed * @return {Bool} true if initialization was succeed
* @static * @static
*/ */
initializeLogger: function(applicationName, taskId) initializeLogger: function(jobId) {
{
if (!this._logger) if (!this._logger)
{ {
this._logger = new LoggerHandler(applicationName, taskId); this._logger = new LoggerHandler(jobId);
return this._logger.isInitialized; return true;
} }
return false; return false;
}, },
@ -389,11 +410,11 @@ var Logger = {
* Logger.addEvent Use this method to add a log event without duration field. * Logger.addEvent Use this method to add a log event without duration field.
* @param {Logger.EventType} type Event Type * @param {Logger.EventType} type Event Type
* @param {Object} values Any event values, for example {count: 1, label: 'vehicle'} * @param {Object} values Any event values, for example {count: 1, label: 'vehicle'}
* @param {String} message Any string message. Empty by default.
* @static * @static
*/ */
addEvent: function(type, values) addEvent: function(type, values, message='') {
{ this._logger.addEvent(new Logger.LogEvent(type, values, message));
this._logger.addEvent(new Logger.LogEvent(type, values));
}, },
/** /**
@ -404,12 +425,12 @@ var Logger = {
* @param {Logger.EventType} type Event Type * @param {Logger.EventType} type Event Type
* @param {Object} values Any event values, for example {count: 1, label: * @param {Object} values Any event values, for example {count: 1, label:
* 'vehicle'} * 'vehicle'}
* @param {String} message Any string message. Empty by default.
* @return {LogEvent} instance of LogEvent * @return {LogEvent} instance of LogEvent
* @static * @static
*/ */
addContinuedEvent: function(type, values) addContinuedEvent: function(type, values, message='') {
{ return this._logger.addContinuedEvent(new Logger.LogEvent(type, values, message));
return this._logger.addContinuedEvent(new Logger.LogEvent(type, values));
}, },
/** /**
@ -420,8 +441,7 @@ var Logger = {
* @return {Function} is decorated decoredFunc * @return {Function} is decorated decoredFunc
* @static * @static
*/ */
shortkeyLogDecorator: function(decoredFunc) shortkeyLogDecorator: function(decoredFunc) {
{
let self = this; let self = this;
return function(e, combo) { return function(e, combo) {
let pressKeyEvent = self.addContinuedEvent(self.EventType.pressShortcut, {key: combo}); let pressKeyEvent = self.addContinuedEvent(self.EventType.pressShortcut, {key: combo});
@ -437,9 +457,19 @@ var Logger = {
* @param {LogEvent} exceptionEvent * @param {LogEvent} exceptionEvent
* @static * @static
*/ */
sendException: function(exceptionData)
{ sendException: function(message, filename, line, column, stack, client, system) {
return this._logger.sendExceptions([new Logger.LogEvent(Logger.EventType.sendException, exceptionData)]); return this._logger.sendExceptions(
new Logger.ExceptionEvent(
message,
filename,
line,
column,
stack,
client,
system
)
);
}, },
/** /**
@ -458,17 +488,6 @@ var Logger = {
return this._logger.getLogs(); 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 /** Logger.updateUserActivityTimer method updates internal timer for working
* time calculation logic * time calculation logic
* @static * @static
@ -514,7 +533,7 @@ var Logger = {
case this.EventType.drawObject: return 'Draw object'; case this.EventType.drawObject: return 'Draw object';
case this.EventType.changeLabel: return 'Change label'; case this.EventType.changeLabel: return 'Change label';
case this.EventType.sendTaskInfo: return 'Send task info'; 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.moveImage: return 'Move image';
case this.EventType.zoomImage: return 'Zoom image'; case this.EventType.zoomImage: return 'Zoom image';
case this.EventType.lockObject: return 'Lock object'; case this.EventType.lockObject: return 'Lock object';

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

@ -6,7 +6,6 @@
import os import os
import sys import sys
import rq import rq
import shlex
import shutil import shutil
import tempfile import tempfile
import numpy as np 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") _MEDIA_MIMETYPES_FILE = os.path.join(_SCRIPT_DIR, "media.mimetypes")
mimetypes.init(files=[_MEDIA_MIMETYPES_FILE]) mimetypes.init(files=[_MEDIA_MIMETYPES_FILE])
from cvat.apps.engine.models import StatusChoice
from cvat.apps.engine.plugins import plugin_decorator
import django_rq import django_rq
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from ffmpy import FFmpeg from ffmpy import FFmpeg
from pyunpack import Archive from pyunpack import Archive
from distutils.dir_util import copy_tree from distutils.dir_util import copy_tree
from collections import OrderedDict
from . import models from . import models
from .log import slogger from .log import slogger
############################# Low Level server API ############################# Low Level server API
@transaction.atomic def create(tid, data):
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):
"""Schedule the task""" """Schedule the task"""
q = django_rq.get_queue('default') q = django_rq.get_queue('default')
q.enqueue_call(func=_create_thread, args=(tid, params), q.enqueue_call(func=_create_thread, args=(tid, data),
job_id="task.create/{}".format(tid)) job_id="/api/v1/tasks/{}".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
@transaction.atomic @transaction.atomic
def rq_handler(job, exc_type, exc_value, traceback): 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) db_task = models.Task.objects.select_for_update().get(pk=tid)
with open(db_task.get_log_path(), "wt") as log_file: with open(db_task.get_log_path(), "wt") as log_file:
print_exception(exc_type, exc_value, traceback, file=log_file) print_exception(exc_type, exc_value, traceback, file=log_file)
db_task.delete()
return False return False
############################# Internal implementation for server API ############################# Internal implementation for server API
@ -304,14 +80,14 @@ class _FrameExtractor:
yield self[i] yield self[i]
i += 1 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: with open(db_task.get_image_meta_cache_path(), 'w') as meta_file:
cache = { cache = {
'original_size': [] 'original_size': []
} }
if db_task.mode == 'interpolation': 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({ cache['original_size'].append({
'width': image.size[0], 'width': image.size[0],
'height': image.size[1] '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: with open(db_task.get_image_meta_cache_path()) as meta_cache_file:
return literal_eval(meta_cache_file.read()) return literal_eval(meta_cache_file.read())
except Exception: 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: with open(db_task.get_image_meta_cache_path()) as meta_cache_file:
return literal_eval(meta_cache_file.read()) return literal_eval(meta_cache_file.read())
@ -362,223 +138,79 @@ def _get_mime(name):
elif mime_type.startswith('image'): elif mime_type.startswith('image'):
return 'image' return 'image'
else: else:
return 'empty' return 'unknown'
else: else:
if os.path.isdir(name): if os.path.isdir(name):
return 'directory' return 'directory'
else: else:
return 'empty' return 'unknown'
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
def _parse_labels(labels):
parsed_labels = OrderedDict()
last_label = "" def _copy_data_from_share(server_files, upload_dir):
for token in shlex.split(labels): job = rq.get_current_job()
if token[0] != "~" and token[0] != "@": job.meta['status'] = 'Data are being copied from share..'
if token in parsed_labels: job.save_meta()
raise ValueError("labels string is not corect. " +
"`{}` label is specified at least twice.".format(token))
parsed_labels[token] = {} for path in server_files:
last_label = token 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: else:
attr = models.parse_attribute(token) target_dir = os.path.dirname(target_path)
attr['text'] = token if not os.path.exists(target_dir):
if not attr['type'] in ['checkbox', 'radio', 'number', 'text', 'select']: os.makedirs(target_dir)
raise ValueError("labels string is not corect. " + shutil.copyfile(source_path, target_path)
"`{}` 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
}
share_dirs_mapping = {} def _unpack_archive(archive, upload_dir):
share_files_mapping = {} job = rq.get_current_job()
job.meta['status'] = 'Archive is being unpacked..'
if storage == 'local': job.save_meta()
# 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.')
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()
''' extractor = _FrameExtractor(video, db_task.image_quality)
Search a video in upload dir and split it by frames. Copy frames to target dirs for frame, image_orig_path in enumerate(extractor):
''' image_dest_path = db_task.get_frame_path(frame)
def _find_and_extract_video(upload_dir, output_dir, db_task, compress_quality, flip_flag, job): db_task.size += 1
video = None 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): for root, _, files in os.walk(upload_dir):
fullnames = map(lambda f: os.path.join(root, f), files) paths = map(lambda f: os.path.join(root, f), files)
videos = list(filter(lambda x: _get_mime(x) == 'video', fullnames)) paths = filter(lambda x: _get_mime(x) == 'image', paths)
if len(videos): image_paths.extend(paths)
video = videos[0] image_paths.sort()
break
db_images = []
if video: if len(image_paths):
job.meta['status'] = 'Video is being extracted..' job = rq.get_current_job()
job.save_meta() for frame, image_orig_path in enumerate(image_paths):
extractor = _FrameExtractor(video, compress_quality, flip_flag) progress = frame * 100 // len(image_paths)
for frame, image_orig_path in enumerate(extractor): job.meta['status'] = 'Images are being compressed.. {}%'.format(progress)
image_dest_path = _get_frame_path(frame, output_dir) job.save_meta()
image_dest_path = db_task.get_frame_path(frame)
db_task.size += 1 db_task.size += 1
dirname = os.path.dirname(image_dest_path) dirname = os.path.dirname(image_dest_path)
if not os.path.exists(dirname): if not os.path.exists(dirname):
os.makedirs(dirname) os.makedirs(dirname)
shutil.copyfile(image_orig_path, image_dest_path) image = Image.open(image_orig_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)
# Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion # Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion
if image.mode == "I": if image.mode == "I":
# Image mode is 32bit integer pixels. # 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()) im_data = im_data * (2**8 / im_data.max())
image = Image.fromarray(im_data.astype(np.int32)) image = Image.fromarray(im_data.astype(np.int32))
image = image.convert('RGB') image = image.convert('RGB')
if flip_flag: image.save(image_dest_path, quality=db_task.image_quality, optimize=True)
image = image.transpose(Image.ROTATE_180) db_images.append(models.Image(task=db_task, path=image_orig_path,
image.save(compressed_name, quality=compress_quality, optimize=True) frame=frame, width=image.width, height=image.height))
image.close() image.close()
if compressed_name != name:
os.remove(name) models.Image.objects.bulk_create(db_images)
# 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)
else: 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): segment_step -= db_task.overlap
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 = task_params['segment'] - db_task.overlap
for x in range(0, db_task.size, segment_step): for x in range(0, db_task.size, segment_step):
start_frame = x 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 = {}, \ slogger.glob.info("New segment for task #{}: start_frame = {}, \
stop_frame = {}".format(db_task.id, start_frame, stop_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.segment = db_segment
db_job.save() db_job.save()
parsed_labels = _parse_labels(task_params['labels']) db_task.save()
for label in parsed_labels:
db_label = models.Label() def _validate_data(data):
db_label.task = db_task share_root = settings.SHARE_ROOT
db_label.name = label server_files = {
db_label.save() '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]: server_video, server_archive = count_files(
db_attrspec = models.AttributeSpec() file_mapping={ f:os.path.abspath(os.path.join(share_root, f)) for f in data['server_files']},
db_attrspec.label = db_label counter=counter,
db_attrspec.text = parsed_labels[label][attr]['text'] )
db_attrspec.save()
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 @transaction.atomic
def _create_thread(tid, params): def _create_thread(tid, data):
slogger.glob.info("create task #{}".format(tid)) slogger.glob.info("create task #{}".format(tid))
job = rq.get_current_job()
db_task = models.Task.objects.select_for_update().get(pk=tid) 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() 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( if data['server_files']:
params['SOURCE_PATHS'], _copy_data_from_share(data['server_files'], upload_dir)
params['TARGET_PATHS'],
params['storage']
)
if (not _valid_file_set(counters)): if archive:
raise Exception('Only one archive, one video or many images can be dowloaded simultaneously. \ archive = os.path.join(upload_dir, archive)
{} image(s), {} dir(s), {} video(s), {} archive(s) found'.format( _unpack_archive(archive, upload_dir)
counters['image'],
counters['directory'], if video:
counters['video'], db_task.mode = "interpolation"
counters['archive'] video = os.path.join(upload_dir, video)
) _copy_video_to_task(video, db_task)
)
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)
else: else:
files =_find_and_compress_images(upload_dir, output_dir, db_task, db_task.mode = "annotation"
task_params['compress'], task_params['flip'], job) _copy_images_to_task(upload_dir, db_task)
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]]))
slogger.glob.info("Founded frames {} for task #{}".format(db_task.size, tid)) 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.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.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/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/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/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/3rdparty/jquery.fullscreen.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/shapeBuffer.js' %}"></script>
<script type="text/javascript" src="{% static 'engine/js/shapeGrouper.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> <script type="text/javascript" src="{% static 'engine/js/annotationUI.js' %}"></script>
{% endblock %} {% endblock %}

@ -37,7 +37,6 @@
{% compress js file cvat %} {% compress js file cvat %}
{% block head_js_cvat %} {% block head_js_cvat %}
<script type="text/javascript" src="{% static 'engine/js/base.js' %}"></script> <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> <script type="text/javascript" src="{% static 'engine/js/userConfig.js' %}"></script>
{% endblock %} {% endblock %}
{% endcompress %} {% 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 # SPDX-License-Identifier: MIT
from django.urls import path from django.urls import path, include
from . import views 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 = [ urlpatterns = [
# Entry point for a client
path('', views.dispatch_request), path('', views.dispatch_request),
path('create/task', views.create_task),
path('get/task/<int:tid>/frame/<int:frame>', views.get_frame), # documentation for API
path('check/task/<int:tid>', views.check_task), path('api/swagger.<slug:format>$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
path('delete/task/<int:tid>', views.delete_task), path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('update/task/<int:tid>', views.update_task), path('api/docs/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('get/job/<int:jid>', views.get_job),
path('get/task/<int:tid>', views.get_task), # entry point for API
path('dump/annotation/task/<int:tid>', views.dump_annotation), path('api/v1/', include((router.urls, 'cvat'), namespace='v1'))
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),
] ]

@ -1,38 +1,46 @@
# Copyright (C) 2018 Intel Corporation # Copyright (C) 2018 Intel Corporation
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os import os
import json import re
import traceback 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.shortcuts import redirect, render
from django.conf import settings 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 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 . import annotation, task, models
from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY from cvat.settings.base import JS_3RDPARTY, CSS_3RDPARTY
from cvat.apps.authentication.decorators import login_required from cvat.apps.authentication.decorators import login_required
from requests.exceptions import RequestException
import logging import logging
from .log import slogger, clogger from .log import slogger, clogger
from cvat.apps.engine.models import StatusChoice from cvat.apps.engine.models import StatusChoice, Task, Job, Plugin
from cvat.apps.engine.serializers import (TaskSerializer, UserSerializer,
############################# High Level server API ExceptionSerializer, AboutSerializer, JobSerializer, ImageMetaSerializer,
@login_required RqStatusSerializer, TaskDataSerializer, LabeledDataSerializer,
@permission_required(perm=['engine.job.access'], PluginSerializer, FileInfoSerializer, LogEventSerializer)
fn=objectgetter(models.Job, 'jid'), raise_exception=True) from django.contrib.auth.models import User
def catch_client_exception(request, jid): from cvat.apps.authentication import auth
data = json.loads(request.body.decode('utf-8')) from rest_framework.permissions import SAFE_METHODS
for event in data['exceptions']:
clogger.job[jid].error(json.dumps(event)) # Server REST API
return HttpResponse()
@login_required @login_required
def dispatch_request(request): def dispatch_request(request):
"""An entry point to dispatch legacy requests""" """An entry point to dispatch legacy requests"""
@ -45,289 +53,413 @@ def dispatch_request(request):
else: else:
return redirect('/dashboard/') return redirect('/dashboard/')
@login_required class ServerViewSet(viewsets.ViewSet):
@permission_required(perm=['engine.task.create'], raise_exception=True) serializer_class = None
def create_task(request):
"""Create a new annotation task""" # To get nice documentation about ServerViewSet actions it is necessary
# to implement the method. By default, ViewSet doesn't provide it.
db_task = None def get_serializer(self, *args, **kwargs):
params = request.POST.dict() pass
params['owner'] = request.user
slogger.glob.info("create task with params = {}".format(params)) @staticmethod
try: @action(detail=False, methods=['GET'], serializer_class=AboutSerializer)
db_task = task.create_empty(params) def about(request):
target_paths = [] from cvat import __version__ as cvat_version
source_paths = [] about = {
upload_dir = db_task.get_upload_dirname() "name": "Computer Vision Annotation Tool",
share_root = settings.SHARE_ROOT "version": cvat_version,
if params['storage'] == 'share': "description": "CVAT is completely re-designed and re-implemented " +
data_list = request.POST.getlist('data') "version of Video Annotation Tool from Irvine, California " +
data_list.sort(key=len) "tool. It is free, online, interactive video and image annotation " +
for share_path in data_list: "tool for computer vision. It is being used by our team to " +
relpath = os.path.normpath(share_path).lstrip('/') "annotate million of objects with different properties. Many UI " +
if '..' in relpath.split(os.path.sep): "and UX decisions are based on feedbacks from professional data " +
raise Exception('Permission denied') "annotation team."
abspath = os.path.abspath(os.path.join(share_root, relpath)) }
if os.path.commonprefix([share_root, abspath]) != share_root: serializer = AboutSerializer(data=about)
raise Exception('Bad file path on share: ' + abspath) if serializer.is_valid(raise_exception=True):
source_paths.append(abspath) return Response(data=serializer.data)
target_paths.append(os.path.join(upload_dir, relpath))
@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: else:
data_list = request.FILES.getlist('data') return Response("{} is an invalid directory".format(param),
status=status.HTTP_400_BAD_REQUEST)
if len(data_list) > settings.LOCAL_LOAD_MAX_FILES_COUNT:
raise Exception('Too many files. Please use download via share') class TaskFilter(filters.FilterSet):
common_size = 0 name = filters.CharFilter(field_name="name", lookup_expr="icontains")
for f in data_list: owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
common_size += f.size mode = filters.CharFilter(field_name="mode", lookup_expr="icontains")
if common_size > settings.LOCAL_LOAD_MAX_FILES_SIZE: status = filters.CharFilter(field_name="status", lookup_expr="icontains")
raise Exception('Too many size. Please use download via share') assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
for data_file in data_list: class Meta:
source_paths.append(data_file.name) model = Task
path = os.path.join(upload_dir, data_file.name) fields = ("id", "name", "owner", "mode", "status", "assignee")
target_paths.append(path)
with open(path, 'wb') as upload_file: class TaskViewSet(auth.TaskGetQuerySetMixin, viewsets.ModelViewSet):
for chunk in data_file.chunks(): queryset = Task.objects.all().prefetch_related(
upload_file.write(chunk) "label_set__attributespec_set",
"segment_set__job_set",
params['SOURCE_PATHS'] = source_paths ).order_by('-id')
params['TARGET_PATHS'] = target_paths serializer_class = TaskSerializer
search_fields = ("name", "owner__username", "mode", "status")
task.create(db_task.id, params) filterset_class = TaskFilter
ordering_fields = ("id", "name", "owner", "status", "assignee")
return JsonResponse({'tid': db_task.id})
except Exception as exc: def get_permissions(self):
slogger.glob.error("cannot create task {}".format(params['task_name']), exc_info=True) http_method = self.request.method
db_task.delete() permissions = [IsAuthenticated]
return HttpResponseBadRequest(str(exc))
if http_method in SAFE_METHODS:
return JsonResponse({'tid': db_task.id}) permissions.append(auth.TaskAccessPermission)
elif http_method in ["POST"]:
@login_required permissions.append(auth.TaskCreatePermission)
#@permission_required(perm=['engine.task.access'], elif http_method in ["PATCH", "PUT"]:
# fn=objectgetter(models.Task, 'tid'), raise_exception=True) permissions.append(auth.TaskChangePermission)
# We have commented lines above because the objectgetter() will raise 404 error in elif http_method in ["DELETE"]:
# cases when a task creating ends with an error. So an user don't get an actual reason of an error. permissions.append(auth.TaskDeletePermission)
def check_task(request, tid): else:
"""Check the status of a task""" permissions.append(auth.AdminRolePermission)
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 [perm() for perm in permissions]
@login_required def perform_create(self, serializer):
@gzip_page if self.request.data.get('owner', None):
@permission_required(perm=['engine.task.access'], serializer.save()
fn=objectgetter(models.Task, 'tid'), raise_exception=True) else:
def download_annotation(request, tid): serializer.save(owner=self.request.user)
try:
slogger.task[tid].info("get dumped annotation") def perform_destroy(self, instance):
db_task = models.Task.objects.get(pk=tid) task_dirname = instance.get_task_dirname()
response = sendfile(request, db_task.get_dump_path(), attachment=True, super().perform_destroy(instance)
attachment_filename='{}_{}.xml'.format(db_task.id, db_task.name)) shutil.rmtree(task_dirname, ignore_errors=True)
except Exception as e:
slogger.task[tid].error("cannot get dumped annotation", exc_info=True) @staticmethod
return HttpResponseBadRequest(str(e)) @action(detail=True, methods=['GET'], serializer_class=JobSerializer)
def jobs(request, pk):
return response 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 @staticmethod
@gzip_page @action(detail=False, methods=['GET'], serializer_class=UserSerializer)
@permission_required(perm=['engine.job.access'], def self(request):
fn=objectgetter(models.Job, 'jid'), raise_exception=True) serializer = UserSerializer(request.user, context={ "request": request })
def get_annotation(request, jid): return Response(serializer.data)
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)
@login_required class PluginViewSet(viewsets.ModelViewSet):
@permission_required(perm=['engine.job.change'], queryset = Plugin.objects.all()
fn=objectgetter(models.Job, 'jid'), raise_exception=True) serializer_class = PluginSerializer
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()
@login_required # @action(detail=True, methods=['GET', 'PATCH', 'PUT'], serializer_class=None)
@permission_required(perm=['engine.task.change'], # def config(self, request, name):
fn=objectgetter(models.Task, 'tid'), raise_exception=True) # pass
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()
@login_required # @action(detail=True, methods=['GET', 'POST'], serializer_class=None)
@permission_required(perm=['engine.task.change'], # def data(self, request, name):
fn=objectgetter(models.Task, 'tid'), raise_exception=True) # pass
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))
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 @action(detail=True, methods=['GET', 'POST'], serializer_class=RqStatusSerializer)
@permission_required(perm=['engine.job.change'], def requests(self, request, name):
fn=objectgetter(models.Job, 'jid'), raise_exception=True) pass
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()
@login_required @action(detail=True, methods=['GET', 'DELETE'],
def get_username(request): serializer_class=RqStatusSerializer, url_path='requests/(?P<id>\d+)')
response = {'username': request.user.username} def request_detail(self, request, name, rq_id):
return JsonResponse(response, safe=False) pass
def rq_handler(job, exc_type, exc_value, tb): 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() job.save()
module = job.id.split('.')[0] if "tasks" in job.id.split("/"):
if module == 'task':
return task.rq_handler(job, exc_type, exc_value, tb) 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 return True

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

@ -6,247 +6,262 @@
/* global /* global
showMessage:false showMessage:false
DashboardView:false
*/ */
"use strict"; // GIT ENTRYPOINT
window.addEventListener('dashboardReady', () => {
window.cvat = window.cvat || {}; const reposWindowId = 'gitReposWindow';
window.cvat.dashboard = window.cvat.dashboard || {}; const closeReposWindowButtonId = 'closeGitReposButton';
window.cvat.dashboard.uiCallbacks = window.cvat.dashboard.uiCallbacks || []; const reposURLTextId = 'gitReposURLText';
window.cvat.dashboard.uiCallbacks.push(function(newElements) { const reposSyncButtonId = 'gitReposSyncButton';
$.ajax({ const labelStatusId = 'gitReposLabelStatus';
type: "GET", const labelMessageId = 'gitReposLabelMessage';
url: "/git/repository/meta/get", const createURLInputTextId = 'gitCreateURLInputText';
success: (data) => { const lfsCheckboxId = 'gitLFSCheckbox';
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");
}
$("<button> Git Repository Sync </button>").addClass("regular dashboardButtonUI").on("click", () => { const reposWindowTemplate = `
let gitDialogWindow = $(`#${window.cvat.git.reposWindowId}`); <div id="${reposWindowId}" class="modal">
gitDialogWindow.attr("current_tid", tid); <div style="width: 700px; height: auto;" class="modal-content">
gitDialogWindow.removeClass("hidden"); <div style="width: 100%; height: 60%; overflow-y: auto;">
window.cvat.git.updateState(); <table style="width: 100%;">
}).appendTo(elem.find("div.dashboardButtonsUI")[0]); <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 = { $.get(`/git/repository/get/${tid}`).done((data) => {
reposWindowId: "gitReposWindow", reposURLText.attr('placeholder', '');
closeReposWindowButtonId: "closeGitReposButton", reposURLText.prop('value', data.url.value);
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;
}
reposURLText.attr("placeholder", ""); if (!data.status.value) {
reposURLText.prop("value", data.url.value); gitLabelStatus.css('color', 'red').text('\u26a0');
gitLabelMessage.css('color', 'red').text(data.status.error);
reposSyncButton.attr('disabled', false);
return;
}
if (!data.status.value) { if (data.status.value === '!sync') {
gitLabelStatus.css("color", "red").text("\u26a0"); gitLabelStatus.css('color', 'red').text('\u2606');
gitLabelMessage.css("color", "red").text(data.status.error); gitLabelMessage.css('color', 'red').text('Repository is not synchronized');
syncButton.attr("disabled", false); reposSyncButton.attr('disabled', false);
return; } 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") { closeReposWindowButton.on('click', () => {
gitLabelStatus.css("color", "red").text("\u2606"); gitWindow.remove();
gitLabelMessage.css("color", "red").text("Repository is not synchronized"); });
syncButton.attr("disabled", false);
} reposSyncButton.on('click', () => {
else if (data.status.value === "sync") { function badResponse(message) {
gitLabelStatus.css("color", "#cccc00").text("\u2605"); try {
gitLabelMessage.css("color", "black").text("Synchronized (merge required)"); showMessage(message);
} throw Error(message);
else if (data.status.value === "merged") { } finally {
gitLabelStatus.css("color", "darkgreen").text("\u2605"); gitWindow.remove();
gitLabelMessage.css("color", "darkgreen").text("Synchronized"); }
} }
else if (data.status.value === "syncing") {
gitLabelMessage.css("color", "#cccc00").text("Synchronization.."); gitLabelMessage.css('color', '#cccc00').text('Synchronization..');
gitLabelStatus.css("color", "#cccc00").text("\u25cc"); gitLabelStatus.css('color', '#cccc00').text('\u25cc');
} reposSyncButton.attr('disabled', true);
else {
let message = `Got unknown repository status: ${data.status.value}`; $.get(`/git/repository/push/${tid}`).done((rqData) => {
gitLabelStatus.css("color", "red").text("\u26a0"); function checkCallback() {
gitLabelMessage.css("color", "red").text(message); $.get(`/git/repository/check/${rqData.rq_id}`).done((statusData) => {
throw Error(message); 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> <tr>
<td> <label class="regular h2"> Dataset Repository: </label> </td> <td> <label class="regular h2"> Dataset Repository: </label> </td>
<td> <input type="text" id="${window.cvat.git.createURLInputTextId}" class="regular"` + <td>
`style="width: 90%", placeholder="github.com/user/repos [annotation/<dump_file_name>.zip]" ` + <input type="text" id="${createURLInputTextId}" class="regular" style="width: 90%", placeholder="${placeh}" title="${title}"/>
`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>
</tr> </tr>
<tr> <tr>
<td> <label class="regular h2" checked> Use LFS: </label> </td> <td> <label class="regular h2" checked> Use LFS: </label> </td>
<td> <input type="checkbox" checked id="${window.cvat.git.lfsCheckboxId}" </td> <td> <input type="checkbox" checked id="${lfsCheckboxId}" </td>
</tr>` </tr>`).insertAfter($('#dashboardBugTrackerInput').parent().parent());
).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");
});
repositorySyncButton.on("click", () => {
function badResponse(message) {
try {
showMessage(message);
throw Error(message);
}
finally {
window.cvat.git.updateState();
}
}
gitLabelMessage.css("color", "#cccc00").text("Synchronization.."); DashboardView.registerDecorator('createTask', (taskData, next, onFault) => {
gitLabelStatus.css("color", "#cccc00").text("\u25cc"); const taskMessage = $('#dashboardCreateTaskMessage');
repositorySyncButton.attr("disabled", true);
let tid = gitWindow.attr("current_tid"); const path = $(`#${createURLInputTextId}`).prop('value').replace(/\s/g, '');
$.get(`/git/repository/push/${tid}`).done((data) => { const lfs = $(`#${lfsCheckboxId}`).prop('checked');
setTimeout(timeoutCallback, 1000);
function timeoutCallback() { if (path.length) {
$.get(`/git/repository/check/${data.rq_id}`).done((data) => { taskMessage.css('color', 'blue');
if (["finished", "failed", "unknown"].indexOf(data.status) != -1) { taskMessage.text('Git repository is being cloned..');
if (data.status === "failed") {
let message = data.error; $.ajax({
badResponse(message); url: `/git/repository/create/${taskData.id}`,
} type: 'POST',
else if (data.status === "unknown") { data: JSON.stringify({
let message = `Request for pushing returned status "${data.status}".`; path,
badResponse(message); lfs,
} tid: taskData.id,
else { }),
window.cvat.git.updateState(); 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();
} }
} }).fail((errorData) => {
else { const message = `Can not sent a request to clone the repository. Code: ${errorData.status}. `
setTimeout(timeoutCallback, 1000); + `Message: ${errorData.responseText || errorData.statusText}`;
} taskMessage.css('color', 'red');
}).fail((data) => { taskMessage.text(message);
let message = `Error was occured during pushing an repos entry. ` + onFault();
`Code: ${data.status}, text: ${data.responseText || data.statusText}`; });
badResponse(message); }
});
} setTimeout(checkCallback, 1000);
}).fail((data) => { }).fail((errorData) => {
let message = `Error was occured during pushing an repos entry. ` + const message = `Can not sent a request to clone the repository. Code: ${errorData.status}. `
`Code: ${data.status}, text: ${data.responseText || data.statusText}`; + `Message: ${errorData.responseText || errorData.statusText}`;
badResponse(message); taskMessage.css('color', 'red');
}); taskMessage.text(message);
onFault();
});
} else {
next();
}
}); });
}); });

@ -8,6 +8,7 @@ from . import views
urlpatterns = [ urlpatterns = [
path('create/<int:tid>', views.create),
path('get/<int:tid>', views.get_repository), path('get/<int:tid>', views.get_repository),
path('push/<int:tid>', views.push_repository), path('push/<int:tid>', views.push_repository),
path('check/<str:rq_id>', views.check_process), 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 cvat.apps.git.git as CVATGit
import django_rq import django_rq
import json
@login_required @login_required
def check_process(request, rq_id): 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 not None:
if rq_job.is_queued or rq_job.is_started: 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: elif rq_job.is_finished:
return JsonResponse({"status": "finished"}) return JsonResponse({"status": rq_job.get_status()})
else: else:
return JsonResponse({"status": "failed", "error": rq_job.exc_info}) return JsonResponse({"status": rq_job.get_status(), "stderr": rq_job.exc_info})
else: else:
return JsonResponse({"status": "unknown"}) return JsonResponse({"status": "unknown"})
except Exception as ex: except Exception as ex:
@ -33,6 +34,26 @@ def check_process(request, rq_id):
return HttpResponseBadRequest(str(ex)) 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 @login_required
@permission_required(perm=['engine.task.access'], @permission_required(perm=['engine.task.access'],
fn=objectgetter(models.Task, 'tid'), raise_exception=True) fn=objectgetter(models.Task, 'tid'), raise_exception=True)

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

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

@ -5,5 +5,5 @@
from cvat.settings.base import JS_3RDPARTY 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.authentication.decorators import login_required
from cvat.apps.engine.models import Task as TaskModel from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine import annotation, task 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 django_rq
import fnmatch import fnmatch
@ -168,44 +169,30 @@ def make_image_list(path_to_data):
def convert_to_cvat_format(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 = { result = {
'create': create_anno_container(), "tracks": [],
'update': create_anno_container(), "shapes": [],
'delete': create_anno_container(), "tags": [],
"version": 0,
} }
for label in data: for label in data:
boxes = data[label] boxes = data[label]
for box in boxes: for box in boxes:
result['create']['boxes'].append({ result['shapes'].append({
"type": "rectangle",
"label_id": label, "label_id": label,
"frame": box[0], "frame": box[0],
"xtl": box[1], "points": [box[1], box[2], box[3], box[4]],
"ytl": box[2],
"xbr": box[3],
"ybr": box[4],
"z_order": 0, "z_order": 0,
"group_id": 0, "group": None,
"occluded": False, "occluded": False,
"attributes": [], "attributes": [],
"id": -1,
}) })
return result return result
def create_thread(tid, labels_mapping): def create_thread(tid, labels_mapping, user):
try: try:
TRESHOLD = 0.5 TRESHOLD = 0.5
# Init rq job # Init rq job
@ -228,14 +215,16 @@ def create_thread(tid, labels_mapping):
# Modify data format and save # Modify data format and save
result = convert_to_cvat_format(result) result = convert_to_cvat_format(result)
annotation.clear_task(tid) serializer = LabeledDataSerializer(data = result)
annotation.save_task(tid, result) if serializer.is_valid(raise_exception=True):
put_task_data(tid, user, result)
slogger.glob.info('tf annotation for task {} done'.format(tid)) slogger.glob.info('tf annotation for task {} done'.format(tid))
except: except Exception as ex:
try: try:
slogger.task[tid].exception('exception was occured during tf annotation of the task', exc_info=True) slogger.task[tid].exception('exception was occured during tf annotation of the task', exc_info=True)
except: except:
slogger.glob.exception('exception was occured during tf annotation of the task {}'.format(tid), exc_into=True) slogger.glob.exception('exception was occured during tf annotation of the task {}'.format(tid), exc_into=True)
raise ex
@login_required @login_required
def get_meta_info(request): def get_meta_info(request):
@ -301,7 +290,7 @@ def create(request, tid):
# Run tf annotation job # Run tf annotation job
queue.enqueue_call(func=create_thread, queue.enqueue_call(func=create_thread,
args=(tid, labels_mapping), args=(tid, labels_mapping, request.user),
job_id='tf_annotation.create/{}'.format(tid), job_id='tf_annotation.create/{}'.format(tid),
timeout=604800) # 7 days timeout=604800) # 7 days
@ -338,6 +327,7 @@ def check(request, tid):
job.delete() job.delete()
else: else:
data['status'] = 'failed' data['status'] = 'failed'
data['stderr'] = job.exc_info
job.delete() job.delete()
except Exception: 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 click==6.7
Django==2.1.5 Django==2.1.7
django-appconf==1.0.2 django-appconf==1.0.2
django-auth-ldap==1.4.0 django-auth-ldap==1.4.0
django-cacheops==4.0.6 django-cacheops==4.0.6
django-compressor==2.2 django-compressor==2.2
django-rq==1.3.0 django-rq==2.0.0
EasyProcess==0.2.3 EasyProcess==0.2.3
ffmpy==0.2.2 ffmpy==0.2.2
Pillow==5.1.0 Pillow==5.1.0
numpy==1.14.2 numpy==1.16.2
patool==1.12 patool==1.12
python-ldap==3.0.0 python-ldap==3.0.0
pytz==2018.3 pytz==2018.3
@ -17,8 +17,8 @@ rcssmin==1.0.6
redis==3.2.0 redis==3.2.0
requests==2.20.0 requests==2.20.0
rjsmin==1.0.12 rjsmin==1.0.12
rq==0.13.0 rq==1.0.0
scipy==1.0.1 scipy==1.2.1
sqlparse==0.2.4 sqlparse==0.2.4
django-sendfile==0.3.11 django-sendfile==0.3.11
dj-pagination==2.4.0 dj-pagination==2.4.0
@ -26,3 +26,10 @@ python-logstash==0.4.6
django-revproxy==0.9.15 django-revproxy==0.9.15
rules==2.0 rules==2.0
GitPython==2.1.11 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