You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

706 lines
24 KiB
Python

# Copyright (C) 2018-2019 Intel Corporation
#
# SPDX-License-Identifier: MIT
import os
import re
import shutil
from enum import Enum
from django.conf import settings
from django.contrib.auth.models import User
from django.core.files.storage import FileSystemStorage
from django.db import models
from django.db.models.fields import FloatField
from django.core.serializers.json import DjangoJSONEncoder
from cvat.apps.engine.utils import parse_specific_attributes
from cvat.apps.organizations.models import Organization
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 DimensionType(str, Enum):
DIM_3D = '3d'
DIM_2D = '2d'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class StatusChoice(str, Enum):
ANNOTATION = 'annotation'
VALIDATION = 'validation'
COMPLETED = 'completed'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
@classmethod
def list(cls):
return list(map(lambda x: x.value, cls))
def __str__(self):
return self.value
class StageChoice(str, Enum):
ANNOTATION = 'annotation'
VALIDATION = 'validation'
ACCEPTANCE = 'acceptance'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class StateChoice(str, Enum):
NEW = 'new'
IN_PROGRESS = 'in progress'
COMPLETED = 'completed'
REJECTED = 'rejected'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class DataChoice(str, Enum):
VIDEO = 'video'
IMAGESET = 'imageset'
LIST = 'list'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class StorageMethodChoice(str, Enum):
CACHE = 'cache'
FILE_SYSTEM = 'file_system'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class StorageChoice(str, Enum):
CLOUD_STORAGE = 'cloud_storage'
LOCAL = 'local'
SHARE = 'share'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class SortingMethod(str, Enum):
LEXICOGRAPHICAL = 'lexicographical'
NATURAL = 'natural'
PREDEFINED = 'predefined'
RANDOM = 'random'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class Data(models.Model):
chunk_size = models.PositiveIntegerField(null=True)
size = models.PositiveIntegerField(default=0)
image_quality = models.PositiveSmallIntegerField(default=50)
start_frame = models.PositiveIntegerField(default=0)
stop_frame = models.PositiveIntegerField(default=0)
frame_filter = models.CharField(max_length=256, default="", blank=True)
compressed_chunk_type = models.CharField(max_length=32, choices=DataChoice.choices(),
default=DataChoice.IMAGESET)
original_chunk_type = models.CharField(max_length=32, choices=DataChoice.choices(),
default=DataChoice.IMAGESET)
storage_method = models.CharField(max_length=15, choices=StorageMethodChoice.choices(), default=StorageMethodChoice.FILE_SYSTEM)
storage = models.CharField(max_length=15, choices=StorageChoice.choices(), default=StorageChoice.LOCAL)
cloud_storage = models.ForeignKey('CloudStorage', on_delete=models.SET_NULL, null=True, related_name='data')
sorting_method = models.CharField(max_length=15, choices=SortingMethod.choices(), default=SortingMethod.LEXICOGRAPHICAL)
class Meta:
default_permissions = ()
def get_frame_step(self):
match = re.search("step\s*=\s*([1-9]\d*)", self.frame_filter)
return int(match.group(1)) if match else 1
def get_data_dirname(self):
return os.path.join(settings.MEDIA_DATA_ROOT, str(self.id))
def get_upload_dirname(self):
return os.path.join(self.get_data_dirname(), "raw")
def get_compressed_cache_dirname(self):
return os.path.join(self.get_data_dirname(), "compressed")
def get_original_cache_dirname(self):
return os.path.join(self.get_data_dirname(), "original")
@staticmethod
def _get_chunk_name(chunk_number, chunk_type):
if chunk_type == DataChoice.VIDEO:
ext = 'mp4'
elif chunk_type == DataChoice.IMAGESET:
ext = 'zip'
else:
ext = 'list'
return '{}.{}'.format(chunk_number, ext)
def _get_compressed_chunk_name(self, chunk_number):
return self._get_chunk_name(chunk_number, self.compressed_chunk_type)
def _get_original_chunk_name(self, chunk_number):
return self._get_chunk_name(chunk_number, self.original_chunk_type)
def get_original_chunk_path(self, chunk_number):
return os.path.join(self.get_original_cache_dirname(),
self._get_original_chunk_name(chunk_number))
def get_compressed_chunk_path(self, chunk_number):
return os.path.join(self.get_compressed_cache_dirname(),
self._get_compressed_chunk_name(chunk_number))
def get_preview_path(self):
return os.path.join(self.get_data_dirname(), 'preview.jpeg')
def get_manifest_path(self):
return os.path.join(self.get_upload_dirname(), 'manifest.jsonl')
def get_index_path(self):
return os.path.join(self.get_upload_dirname(), 'index.json')
def make_dirs(self):
data_path = self.get_data_dirname()
if os.path.isdir(data_path):
shutil.rmtree(data_path)
os.makedirs(self.get_compressed_cache_dirname())
os.makedirs(self.get_original_cache_dirname())
os.makedirs(self.get_upload_dirname())
def get_uploaded_files(self):
upload_dir = self.get_upload_dirname()
uploaded_files = [os.path.join(upload_dir, file) for file in os.listdir(upload_dir) if os.path.isfile(os.path.join(upload_dir, file))]
represented_files = [{'file':f} for f in uploaded_files]
return represented_files
class Video(models.Model):
data = models.OneToOneField(Data, on_delete=models.CASCADE, related_name="video", null=True)
path = models.CharField(max_length=1024, default='')
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
class Meta:
default_permissions = ()
class Image(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, related_name="images", null=True)
path = models.CharField(max_length=1024, default='')
frame = models.PositiveIntegerField()
width = models.PositiveIntegerField()
height = models.PositiveIntegerField()
class Meta:
default_permissions = ()
class Project(models.Model):
name = SafeCharField(max_length=256)
owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="+")
assignee = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="+")
bug_tracker = models.CharField(max_length=2000, blank=True, default="")
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
organization = models.ForeignKey(Organization, null=True, default=None,
blank=True, on_delete=models.SET_NULL, related_name="projects")
def get_project_dirname(self):
return os.path.join(settings.PROJECTS_ROOT, str(self.id))
def get_project_logs_dirname(self):
return os.path.join(self.get_project_dirname(), 'logs')
def get_tmp_dirname(self):
return os.path.join(self.get_project_dirname(), "tmp")
def get_client_log_path(self):
return os.path.join(self.get_project_logs_dirname(), "client.log")
def get_log_path(self):
return os.path.join(self.get_project_logs_dirname(), "project.log")
# Extend default permission model
class Meta:
default_permissions = ()
def __str__(self):
return self.name
class Task(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE,
null=True, blank=True, related_name="tasks",
related_query_name="task")
name = SafeCharField(max_length=256)
mode = models.CharField(max_length=32)
owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="owners")
assignee = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="assignees")
bug_tracker = models.CharField(max_length=2000, blank=True, default="")
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
overlap = models.PositiveIntegerField(null=True)
# Zero means that there are no limits (default)
segment_size = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name="tasks")
dimension = models.CharField(max_length=2, choices=DimensionType.choices(), default=DimensionType.DIM_2D)
subset = models.CharField(max_length=64, blank=True, default="")
organization = models.ForeignKey(Organization, null=True, default=None,
blank=True, on_delete=models.SET_NULL, related_name="tasks")
# Extend default permission model
class Meta:
default_permissions = ()
def get_task_dirname(self):
return os.path.join(settings.TASKS_ROOT, str(self.id))
def get_task_logs_dirname(self):
return os.path.join(self.get_task_dirname(), 'logs')
def get_client_log_path(self):
return os.path.join(self.get_task_logs_dirname(), "client.log")
def get_log_path(self):
return os.path.join(self.get_task_logs_dirname(), "task.log")
def get_task_artifacts_dirname(self):
return os.path.join(self.get_task_dirname(), 'artifacts')
def get_tmp_dirname(self):
return os.path.join(self.get_task_dirname(), "tmp")
def __str__(self):
return self.name
# Redefined a couple of operation for FileSystemStorage to avoid renaming
# or other side effects.
class MyFileSystemStorage(FileSystemStorage):
def get_valid_name(self, name):
return name
def get_available_name(self, name, max_length=None):
if self.exists(name) or (max_length and len(name) > max_length):
raise IOError('`{}` file already exists or its name is too long'.format(name))
return name
def upload_path_handler(instance, filename):
# relative path is required since Django 3.1.11
return os.path.join(os.path.relpath(instance.data.get_upload_dirname(), settings.BASE_DIR), filename)
# For client files which the user is uploaded
class ClientFile(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name='client_files')
file = models.FileField(upload_to=upload_path_handler,
max_length=1024, storage=MyFileSystemStorage())
class Meta:
default_permissions = ()
unique_together = ("data", "file")
# For server files on the mounted share
class ServerFile(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name='server_files')
file = models.CharField(max_length=1024)
class Meta:
default_permissions = ()
# For URLs
class RemoteFile(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name='remote_files')
file = models.CharField(max_length=1024)
class Meta:
default_permissions = ()
class RelatedFile(models.Model):
data = models.ForeignKey(Data, on_delete=models.CASCADE, related_name="related_files", default=1, null=True)
path = models.FileField(upload_to=upload_path_handler,
max_length=1024, storage=MyFileSystemStorage())
primary_image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="related_files", null=True)
class Meta:
default_permissions = ()
unique_together = ("data", "path")
class Segment(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
start_frame = models.IntegerField()
stop_frame = models.IntegerField()
class Meta:
default_permissions = ()
class Job(models.Model):
segment = models.ForeignKey(Segment, on_delete=models.CASCADE)
assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
# TODO: it has to be deleted in Job, Task, Project and replaced by (stage, state)
# The stage field cannot be changed by an assignee, but state field can be. For
# now status is read only and it will be updated by (stage, state). Thus we don't
# need to update Task and Project (all should work as previously).
status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION)
stage = models.CharField(max_length=32, choices=StageChoice.choices(),
default=StageChoice.ANNOTATION)
state = models.CharField(max_length=32, choices=StateChoice.choices(),
default=StateChoice.NEW)
def get_project_id(self):
project = self.segment.task.project
return project.id if project else None
def get_bug_tracker(self):
task = self.segment.task
project = task.project
return task.bug_tracker or getattr(project, 'bug_tracker', None)
def get_labels(self):
task = self.segment.task
project = task.project
return project.label_set if project else task.label_set
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
db_commit = JobCommit(job=self, scope='create',
owner=self.segment.task.owner, data={
'stage': self.stage, 'state': self.state, 'assignee': self.assignee
})
db_commit.save()
class Meta:
default_permissions = ()
class Label(models.Model):
task = models.ForeignKey(Task, null=True, blank=True, on_delete=models.CASCADE)
project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.CASCADE)
name = SafeCharField(max_length=64)
color = models.CharField(default='', max_length=8)
def __str__(self):
return self.name
class Meta:
default_permissions = ()
unique_together = ('task', 'name')
class AttributeType(str, Enum):
CHECKBOX = 'checkbox'
RADIO = 'radio'
NUMBER = 'number'
TEXT = 'text'
SELECT = 'select'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class AttributeSpec(models.Model):
label = models.ForeignKey(Label, on_delete=models.CASCADE)
name = models.CharField(max_length=64)
mutable = models.BooleanField()
input_type = models.CharField(max_length=16,
choices=AttributeType.choices())
default_value = models.CharField(max_length=128)
values = models.CharField(max_length=4096)
class Meta:
default_permissions = ()
unique_together = ('label', 'name')
def __str__(self):
return self.name
class AttributeVal(models.Model):
# TODO: add a validator here to be sure that it corresponds to self.label
id = models.BigAutoField(primary_key=True)
spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE)
value = SafeCharField(max_length=4096)
class Meta:
abstract = True
default_permissions = ()
class ShapeType(str, Enum):
RECTANGLE = 'rectangle' # (x0, y0, x1, y1)
POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn)
ELLIPSE = 'ellipse' # (cx, cy, rx, ty)
CUBOID = 'cuboid' # (x0, y0, ..., x7, y7)
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
def __str__(self):
return self.value
class SourceType(str, Enum):
AUTO = 'auto'
MANUAL = 'manual'
@classmethod
def choices(self):
return tuple((x.value, x.name) for x in self)
def __str__(self):
return self.value
class Annotation(models.Model):
id = models.BigAutoField(primary_key=True)
job = models.ForeignKey(Job, on_delete=models.CASCADE)
label = models.ForeignKey(Label, on_delete=models.CASCADE)
frame = models.PositiveIntegerField()
group = models.PositiveIntegerField(null=True)
source = models.CharField(max_length=16, choices=SourceType.choices(),
default=str(SourceType.MANUAL), null=True)
class Meta:
abstract = True
default_permissions = ()
class Commit(models.Model):
class JSONEncoder(DjangoJSONEncoder):
def default(self, o):
if isinstance(o, User):
data = {'user': {'id': o.id, 'username': o.username}}
return data
else:
return super().default(o)
id = models.BigAutoField(primary_key=True)
scope = models.CharField(max_length=32, default="")
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now=True)
data = models.JSONField(default=dict, encoder=JSONEncoder)
class Meta:
abstract = True
default_permissions = ()
class JobCommit(Commit):
job = models.ForeignKey(Job, on_delete=models.CASCADE, related_name="commits")
class FloatArrayField(models.TextField):
separator = ","
def from_db_value(self, value, expression, connection):
if not value:
return value
if value.startswith('[') and value.endswith(']'):
value = value[1:-1]
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()
rotation = FloatField(default=0)
class Meta:
abstract = True
default_permissions = ()
class LabeledImage(Annotation):
pass
class LabeledImageAttributeVal(AttributeVal):
image = models.ForeignKey(LabeledImage, on_delete=models.CASCADE)
class LabeledShape(Annotation, Shape):
pass
class LabeledShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(LabeledShape, on_delete=models.CASCADE)
class LabeledTrack(Annotation):
pass
class LabeledTrackAttributeVal(AttributeVal):
track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
class TrackedShape(Shape):
id = models.BigAutoField(primary_key=True)
track = models.ForeignKey(LabeledTrack, on_delete=models.CASCADE)
frame = models.PositiveIntegerField()
outside = models.BooleanField(default=False)
class TrackedShapeAttributeVal(AttributeVal):
shape = models.ForeignKey(TrackedShape, on_delete=models.CASCADE)
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
rating = models.FloatField(default=0.0)
class Issue(models.Model):
frame = models.PositiveIntegerField()
position = FloatArrayField()
job = models.ForeignKey(Job, related_name='issues', on_delete=models.CASCADE)
owner = models.ForeignKey(User, null=True, blank=True, related_name='+',
on_delete=models.SET_NULL)
assignee = models.ForeignKey(User, null=True, blank=True, related_name='+',
on_delete=models.SET_NULL)
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(null=True, blank=True)
resolved = models.BooleanField(default=False)
class Comment(models.Model):
issue = models.ForeignKey(Issue, related_name='comments', on_delete=models.CASCADE)
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
message = models.TextField(default='')
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
class CloudProviderChoice(str, Enum):
AWS_S3 = 'AWS_S3_BUCKET'
AZURE_CONTAINER = 'AZURE_CONTAINER'
GOOGLE_DRIVE = 'GOOGLE_DRIVE'
GOOGLE_CLOUD_STORAGE = 'GOOGLE_CLOUD_STORAGE'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
@classmethod
def list(cls):
return list(map(lambda x: x.value, cls))
def __str__(self):
return self.value
class CredentialsTypeChoice(str, Enum):
# ignore bandit issues because false positives
KEY_SECRET_KEY_PAIR = 'KEY_SECRET_KEY_PAIR' # nosec
ACCOUNT_NAME_TOKEN_PAIR = 'ACCOUNT_NAME_TOKEN_PAIR' # nosec
KEY_FILE_PATH = 'KEY_FILE_PATH'
ANONYMOUS_ACCESS = 'ANONYMOUS_ACCESS'
@classmethod
def choices(cls):
return tuple((x.value, x.name) for x in cls)
@classmethod
def list(cls):
return list(map(lambda x: x.value, cls))
def __str__(self):
return self.value
class Manifest(models.Model):
filename = models.CharField(max_length=1024, default='manifest.jsonl')
cloud_storage = models.ForeignKey('CloudStorage', on_delete=models.CASCADE, null=True, related_name='manifests')
def __str__(self):
return '{}'.format(self.filename)
class CloudStorage(models.Model):
# restrictions:
# AWS bucket name, Azure container name - 63, Google bucket name - 63 without dots and 222 with dots
# https://cloud.google.com/storage/docs/naming-buckets#requirements
# AWS access key id - 20
# AWS secret access key - 40
# AWS temporary session tocken - None
# The size of the security token that AWS STS API operations return is not fixed.
# We strongly recommend that you make no assumptions about the maximum size.
# The typical token size is less than 4096 bytes, but that can vary.
# specific attributes:
# location - max 23
# project ID: 6 - 30 (https://cloud.google.com/resource-manager/docs/creating-managing-projects#before_you_begin)
provider_type = models.CharField(max_length=20, choices=CloudProviderChoice.choices())
resource = models.CharField(max_length=222)
display_name = models.CharField(max_length=63)
owner = models.ForeignKey(User, null=True, blank=True,
on_delete=models.SET_NULL, related_name="cloud_storages")
created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True)
credentials = models.CharField(max_length=500)
credentials_type = models.CharField(max_length=29, choices=CredentialsTypeChoice.choices())#auth_type
specific_attributes = models.CharField(max_length=1024, blank=True)
description = models.TextField(blank=True)
organization = models.ForeignKey(Organization, null=True, default=None,
blank=True, on_delete=models.SET_NULL, related_name="cloudstorages")
class Meta:
default_permissions = ()
unique_together = ('provider_type', 'resource', 'credentials')
def __str__(self):
return "{} {} {}".format(self.provider_type, self.display_name, self.id)
def get_storage_dirname(self):
return os.path.join(settings.CLOUD_STORAGE_ROOT, str(self.id))
def get_storage_logs_dirname(self):
return os.path.join(self.get_storage_dirname(), 'logs')
def get_log_path(self):
return os.path.join(self.get_storage_logs_dirname(), "storage.log")
def get_preview_path(self):
return os.path.join(self.get_storage_dirname(), 'preview.jpeg')
def get_specific_attributes(self):
return parse_specific_attributes(self.specific_attributes)
def get_key_file_path(self):
return os.path.join(self.get_storage_dirname(), 'key.json')