Merge annotations and dataset_manager apps (#1352)
parent
6566e4aa11
commit
5ab549956f
@ -1,628 +0,0 @@
|
||||
<!--lint disable list-item-indent-->
|
||||
<!--lint disable no-duplicate-headings-->
|
||||
## Description
|
||||
|
||||
The purpose of this application is to add support for multiple annotation formats for CVAT.
|
||||
It allows to download and upload annotations in different formats and easily add support for new.
|
||||
|
||||
## How to add a new annotation format support
|
||||
|
||||
1. Write a python script that will be executed via exec() function. Following items must be defined inside at code:
|
||||
- **format_spec** - a dictionary with the following structure:
|
||||
```python
|
||||
format_spec = {
|
||||
"name": "CVAT",
|
||||
"dumpers": [
|
||||
{
|
||||
"display_name": "{name} {format} {version} for videos",
|
||||
"format": "XML",
|
||||
"version": "1.1",
|
||||
"handler": "dump_as_cvat_interpolation"
|
||||
},
|
||||
{
|
||||
"display_name": "{name} {format} {version} for images",
|
||||
"format": "XML",
|
||||
"version": "1.1",
|
||||
"handler": "dump_as_cvat_annotation"
|
||||
}
|
||||
],
|
||||
"loaders": [
|
||||
{
|
||||
"display_name": "{name} {format} {version}",
|
||||
"format": "XML",
|
||||
"version": "1.1",
|
||||
"handler": "load",
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
||||
- **name** - unique name for each format
|
||||
- **dumpers and loaders** - lists of objects that describes exposed dumpers and loaders and must
|
||||
have following keys:
|
||||
1. display_name - **unique** string used as ID for dumpers and loaders.
|
||||
Also this string is displayed in CVAT UI.
|
||||
Possible to use a named placeholders like the python format function
|
||||
(supports only name, format and version variables).
|
||||
1. format - a string, used as extension for a dumped annotation.
|
||||
1. version - just string with version.
|
||||
1. handler - function that will be called and should be defined at top scope.
|
||||
- dumper/loader handler functions. Each function should have the following signature:
|
||||
```python
|
||||
def dump_handler(file_object, annotations):
|
||||
```
|
||||
|
||||
Inside of the script environment 2 variables are available:
|
||||
- **file_object** - python's standard file object returned by open() function and exposing a file-oriented API
|
||||
(with methods such as read() or write()) to an underlying resource.
|
||||
- **annotations** - instance of [Annotation](annotation.py#L106) class.
|
||||
|
||||
Annotation class expose API and some additional pre-defined types that allow to get/add shapes inside
|
||||
a loader/dumper code.
|
||||
|
||||
Short description of the public methods:
|
||||
- **Annotation.shapes** - property, returns a generator of Annotation.LabeledShape objects
|
||||
- **Annotation.tracks** - property, returns a generator of Annotation.Track objects
|
||||
- **Annotation.tags** - property, returns a generator of Annotation.Tag objects
|
||||
- **Annotation.group_by_frame()** - method, returns an iterator on Annotation.Frame object,
|
||||
which groups annotation objects by frame. Note that TrackedShapes will be represented as Annotation.LabeledShape.
|
||||
- **Annotation.meta** - property, returns dictionary which represent a task meta information,
|
||||
for example - video source name, number of frames, number of jobs, etc
|
||||
- **Annotation.add_tag(tag)** - tag should be a instance of the Annotation.Tag class
|
||||
- **Annotation.add_shape(shape)** - shape should be a instance of the Annotation.Shape class
|
||||
- **Annotation.add_track(track)** - track should be a instance of the Annotation.Track class
|
||||
- **Annotation.Attribute** = namedtuple('Attribute', 'name, value')
|
||||
- name - String, name of the attribute
|
||||
- value - String, value of the attribute
|
||||
- **Annotation.LabeledShape** = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes,
|
||||
group, z_order')
|
||||
LabeledShape.\__new\__.\__defaults\__ = (0, None)
|
||||
- **TrackedShape** = namedtuple('TrackedShape', 'type, points, occluded, frame, attributes, outside,
|
||||
keyframe, z_order')
|
||||
TrackedShape.\__new\__.\__defaults\__ = (None, )
|
||||
- **Track** = namedtuple('Track', 'label, group, shapes')
|
||||
- **Tag** = namedtuple('Tag', 'frame, label, attributes, group')
|
||||
Tag.\__new\__.\__defaults\__ = (0, )
|
||||
- **Frame** = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags')
|
||||
|
||||
Pseudocode for a dumper script
|
||||
```python
|
||||
...
|
||||
# dump meta info if necessary
|
||||
...
|
||||
|
||||
# iterate over all frames
|
||||
for frame_annotation in annotations.group_by_frame():
|
||||
# get frame info
|
||||
image_name = frame_annotation.name
|
||||
image_width = frame_annotation.width
|
||||
image_height = frame_annotation.height
|
||||
|
||||
# iterate over all shapes on the frame
|
||||
for shape in frame_annotation.labeled_shapes:
|
||||
label = shape.label
|
||||
xtl = shape.points[0]
|
||||
ytl = shape.points[1]
|
||||
xbr = shape.points[2]
|
||||
ybr = shape.points[3]
|
||||
|
||||
# iterate over shape attributes
|
||||
for attr in shape.attributes:
|
||||
attr_name = attr.name
|
||||
attr_value = attr.value
|
||||
...
|
||||
# dump annotation code
|
||||
file_object.write(...)
|
||||
...
|
||||
```
|
||||
Pseudocode for a loader code
|
||||
```python
|
||||
...
|
||||
#read file_object
|
||||
...
|
||||
|
||||
for parsed_shape in parsed_shapes:
|
||||
shape = annotations.LabeledShape(
|
||||
type="rectangle",
|
||||
points=[0, 0, 100, 100],
|
||||
occluded=False,
|
||||
attributes=[],
|
||||
label="car",
|
||||
outside=False,
|
||||
frame=99,
|
||||
)
|
||||
|
||||
annotations.add_shape(shape)
|
||||
```
|
||||
Full examples can be found in corrseponding *.py files (cvat.py, coco.py, yolo.py, etc.).
|
||||
1. Add path to a new python script to the annotation app settings:
|
||||
|
||||
```python
|
||||
BUILTIN_FORMATS = (
|
||||
os.path.join(path_prefix, 'cvat.py'),
|
||||
os.path.join(path_prefix,'pascal_voc.py'),
|
||||
)
|
||||
```
|
||||
|
||||
## Ideas for improvements
|
||||
|
||||
- Annotation format manager like DL Model manager with which the user can add custom format support by
|
||||
writing dumper/loader scripts.
|
||||
- Often a custom loader/dumper requires additional python packages and it would be useful if CVAT provided some API
|
||||
that allows the user to install a python dependencies from their own code without changing the source code.
|
||||
Possible solutions: install additional modules via pip call to a separate directory for each Annotation Format
|
||||
to reduce version conflicts, etc. Thus, custom code can be run in an extended environment, and core CVAT modules
|
||||
should not be affected. As well, this functionality can be useful for Auto Annotation module.
|
||||
|
||||
## Format specifications
|
||||
|
||||
### CVAT
|
||||
This is native CVAT annotation format.
|
||||
[Detailed format description](cvat/apps/documentation/xml_format.md)
|
||||
|
||||
#### CVAT XML for images dumper
|
||||
- downloaded file: Single unpacked XML
|
||||
- supported shapes - Rectangles, Polygons, Polylines, Points
|
||||
|
||||
#### CVAT XML for videos dumper
|
||||
- downloaded file: Single unpacked XML
|
||||
- supported shapes - Rectangles, Polygons, Polylines, Points
|
||||
|
||||
#### CVAT XML Loader
|
||||
- uploaded file: Single unpacked XML
|
||||
- supported shapes - Rectangles, Polygons, Polylines, Points
|
||||
|
||||
### [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/)
|
||||
- [Format specification](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/devkit_doc.pdf)
|
||||
|
||||
#### Pascal dumper description
|
||||
- downloaded file: a zip archive of the following structure:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── Annotations/
|
||||
│ ├── <image_name1>.xml
|
||||
│ ├── <image_name2>.xml
|
||||
│ └── <image_nameN>.xml
|
||||
├── ImageSets/
|
||||
│ └── Main/
|
||||
│ └── default.txt
|
||||
└── labelmap.txt
|
||||
```
|
||||
|
||||
- supported shapes: Rectangles
|
||||
- additional comments: If you plan to use `truncated` and `difficult` attributes please add the corresponding
|
||||
items to the CVAT label attributes:
|
||||
`~checkbox=difficult:false ~checkbox=truncated:false`
|
||||
|
||||
#### Pascal loader description
|
||||
- uploaded file: a zip archive of the structure declared above or the following:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── <image_name1>.xml
|
||||
├── <image_name2>.xml
|
||||
├── <image_nameN>.xml
|
||||
└── labelmap.txt # optional
|
||||
```
|
||||
|
||||
The `labelmap.txt` file contains dataset labels. It **must** be included
|
||||
if dataset labels **differ** from VOC default labels. The file structure:
|
||||
```bash
|
||||
# label : color_rgb : 'body' parts : actions
|
||||
background:::
|
||||
aeroplane:::
|
||||
bicycle:::
|
||||
bird:::
|
||||
```
|
||||
|
||||
It must be possible for CVAT to match the frame (image name) and file name from annotation \*.xml
|
||||
file (the tag filename, e.g. `<filename>2008_004457.jpg</filename>`). There are 2 options:
|
||||
1. full match between image name and filename from annotation \*.xml
|
||||
(in cases when task was created from images or image archive).
|
||||
1. match by frame number (if CVAT cannot match by name). File name should
|
||||
be in the following format `<number>.jpg`.
|
||||
It should be used when task was created from a video.
|
||||
|
||||
- supported shapes: Rectangles
|
||||
- limitations: Support of Pascal VOC object detection format
|
||||
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files
|
||||
|
||||
#### How to create a task from Pascal VOC dataset
|
||||
1. Download the Pascal Voc dataset (Can be downloaded from the
|
||||
[PASCAL VOC website](http://host.robots.ox.ac.uk/pascal/VOC/))
|
||||
1. Create a CVAT task with the following labels:
|
||||
```bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
You can add `~checkbox=difficult:false ~checkbox=truncated:false` attributes for each label if you want to use them.
|
||||
|
||||
Select interesting image files
|
||||
(See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details)
|
||||
1. zip the corresponding annotation files
|
||||
1. click `Upload annotation` button, choose `Pascal VOC ZIP 1.1`
|
||||
and select the *.zip file with annotations from previous step.
|
||||
It may take some time.
|
||||
|
||||
### [YOLO](https://pjreddie.com/darknet/yolo/)
|
||||
#### Yolo dumper description
|
||||
- downloaded file: a zip archive with following structure:
|
||||
[Format specification](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)
|
||||
```bash
|
||||
archive.zip/
|
||||
├── obj.data
|
||||
├── obj.names
|
||||
├── obj_<subset>_data
|
||||
│ ├── image1.txt
|
||||
│ └── image2.txt
|
||||
└── train.txt # list of subset image paths
|
||||
|
||||
# the only valid subsets are: train, valid
|
||||
# train.txt and valid.txt:
|
||||
obj_<subset>_data/image1.jpg
|
||||
obj_<subset>_data/image2.jpg
|
||||
|
||||
# obj.data:
|
||||
classes = 3 # optional
|
||||
names = obj.names
|
||||
train = train.txt
|
||||
valid = valid.txt # optional
|
||||
backup = backup/ # optional
|
||||
|
||||
# obj.names:
|
||||
cat
|
||||
dog
|
||||
airplane
|
||||
|
||||
# image_name.txt:
|
||||
# label_id - id from obj.names
|
||||
# cx, cy - relative coordinates of the bbox center
|
||||
# rw, rh - relative size of the bbox
|
||||
# label_id cx cy rw rh
|
||||
1 0.3 0.8 0.1 0.3
|
||||
2 0.7 0.2 0.3 0.1
|
||||
```
|
||||
Each annotation `*.txt` file has a name that corresponds to the name of the image file
|
||||
(e.g. `frame_000001.txt` is the annotation for the `frame_000001.jpg` image).
|
||||
The `*.txt` file structure: each line describes label and bounding box
|
||||
in the following format `label_id cx cy w h`.
|
||||
`obj.names` contains the ordered list of label names.
|
||||
- supported shapes - Rectangles
|
||||
|
||||
#### Yolo loader description
|
||||
- uploaded file: a zip archive of the same structure as above
|
||||
It must be possible to match the CVAT frame (image name) and annotation file name
|
||||
There are 2 options:
|
||||
1. full match between image name and name of annotation `*.txt` file
|
||||
(in cases when a task was created from images or archive of images).
|
||||
1. match by frame number (if CVAT cannot match by name). File name should be in the following format `<number>.jpg`.
|
||||
It should be used when task was created from a video.
|
||||
|
||||
- supported shapes: Rectangles
|
||||
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files
|
||||
|
||||
#### How to create a task from YOLO formatted dataset (from VOC for example)
|
||||
1. Follow the official [guide](https://pjreddie.com/darknet/yolo/)(see Training YOLO on VOC section)
|
||||
and prepare the YOLO formatted annotation files.
|
||||
1. Zip train images
|
||||
```bash
|
||||
zip images.zip -j -@ < train.txt
|
||||
```
|
||||
1. Create a CVAT task with the following labels:
|
||||
```bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
Select images.zip as data. Most likely you should use `share`
|
||||
functionality because size of images.zip is more than 500Mb.
|
||||
See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details.
|
||||
1. Create `obj.names` with the following content:
|
||||
```bash
|
||||
aeroplane
|
||||
bicycle
|
||||
bird
|
||||
boat
|
||||
bottle
|
||||
bus
|
||||
car
|
||||
cat
|
||||
chair
|
||||
cow
|
||||
diningtable
|
||||
dog
|
||||
horse
|
||||
motorbike
|
||||
person
|
||||
pottedplant
|
||||
sheep
|
||||
sofa
|
||||
train
|
||||
tvmonitor
|
||||
```
|
||||
1. Zip all label files together (we need to add only label files that correspond to the train subset)
|
||||
```bash
|
||||
cat train.txt | while read p; do echo ${p%/*/*}/labels/${${p##*/}%%.*}.txt; done | zip labels.zip -j -@ obj.names
|
||||
```
|
||||
1. Click `Upload annotation` button, choose `YOLO ZIP 1.1` and select the *.zip file with labels from previous step.
|
||||
It may take some time.
|
||||
|
||||
### [MS COCO Object Detection](http://cocodataset.org/#format-data)
|
||||
#### COCO dumper description
|
||||
- downloaded file: single unpacked `json`. Detailed description of the MS COCO format can be found [here](http://cocodataset.org/#format-data)
|
||||
- supported shapes - Polygons, Rectangles (interpreted as polygons)
|
||||
|
||||
#### COCO loader description
|
||||
- uploaded file: single unpacked `*.json`.
|
||||
- supported shapes: object is interpreted as Polygon if the `segmentation` field of annotation is not empty
|
||||
else as Rectangle with coordinates from `bbox` field.
|
||||
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files
|
||||
|
||||
#### How to create a task from MS COCO dataset
|
||||
1. Download the [MS COCO dataset](http://cocodataset.org/#download).
|
||||
For example [2017 Val images](http://images.cocodataset.org/zips/val2017.zip)
|
||||
and [2017 Train/Val annotations](http://images.cocodataset.org/annotations/annotations_trainval2017.zip).
|
||||
1. Create a CVAT task with the following labels:
|
||||
```bash
|
||||
person bicycle car motorcycle airplane bus train truck boat "traffic light" "fire hydrant" "stop sign" "parking meter" bench bird cat dog horse sheep cow elephant bear zebra giraffe backpack umbrella handbag tie suitcase frisbee skis snowboard "sports ball" kite "baseball bat" "baseball glove" skateboard surfboard "tennis racket" bottle "wine glass" cup fork knife spoon bowl banana apple sandwich orange broccoli carrot "hot dog" pizza donut cake chair couch "potted plant" bed "dining table" toilet tv laptop mouse remote keyboard "cell phone" microwave oven toaster sink refrigerator book clock vase scissors "teddy bear" "hair drier" toothbrush
|
||||
```
|
||||
|
||||
Select val2017.zip as data
|
||||
(See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details)
|
||||
1. unpack annotations_trainval2017.zip
|
||||
1. click `Upload annotation` button,
|
||||
choose `COCO JSON 1.0` and select `instances_val2017.json.json` annotation file. It may take some time.
|
||||
|
||||
### [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records)
|
||||
TFRecord is a very flexible format, but we try to correspond the format that used in
|
||||
[TF object detection](https://github.com/tensorflow/models/tree/master/research/object_detection)
|
||||
with minimal modifications.
|
||||
Used feature description:
|
||||
```python
|
||||
image_feature_description = {
|
||||
'image/filename': tf.io.FixedLenFeature([], tf.string),
|
||||
'image/source_id': tf.io.FixedLenFeature([], tf.string),
|
||||
'image/height': tf.io.FixedLenFeature([], tf.int64),
|
||||
'image/width': tf.io.FixedLenFeature([], tf.int64),
|
||||
# Object boxes and classes.
|
||||
'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/class/label': tf.io.VarLenFeature(tf.int64),
|
||||
'image/object/class/text': tf.io.VarLenFeature(tf.string),
|
||||
}
|
||||
```
|
||||
#### TFRecord dumper description
|
||||
- downloaded file: a zip archive with following structure:
|
||||
```bash
|
||||
taskname.zip
|
||||
├── task2.tfrecord
|
||||
└── label_map.pbtxt
|
||||
```
|
||||
- supported shapes - Rectangles
|
||||
|
||||
#### TFRecord loader description
|
||||
- uploaded file: a zip archive with following structure:
|
||||
```bash
|
||||
taskname.zip
|
||||
└── task2.tfrecord
|
||||
```
|
||||
- supported shapes: Rectangles
|
||||
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files
|
||||
|
||||
#### How to create a task from TFRecord dataset (from VOC2007 for example)
|
||||
1. Create label_map.pbtxt file with the following content:
|
||||
```js
|
||||
item {
|
||||
id: 1
|
||||
name: 'aeroplane'
|
||||
}
|
||||
item {
|
||||
id: 2
|
||||
name: 'bicycle'
|
||||
}
|
||||
item {
|
||||
id: 3
|
||||
name: 'bird'
|
||||
}
|
||||
item {
|
||||
id: 4
|
||||
name: 'boat'
|
||||
}
|
||||
item {
|
||||
id: 5
|
||||
name: 'bottle'
|
||||
}
|
||||
item {
|
||||
id: 6
|
||||
name: 'bus'
|
||||
}
|
||||
item {
|
||||
id: 7
|
||||
name: 'car'
|
||||
}
|
||||
item {
|
||||
id: 8
|
||||
name: 'cat'
|
||||
}
|
||||
item {
|
||||
id: 9
|
||||
name: 'chair'
|
||||
}
|
||||
item {
|
||||
id: 10
|
||||
name: 'cow'
|
||||
}
|
||||
item {
|
||||
id: 11
|
||||
name: 'diningtable'
|
||||
}
|
||||
item {
|
||||
id: 12
|
||||
name: 'dog'
|
||||
}
|
||||
item {
|
||||
id: 13
|
||||
name: 'horse'
|
||||
}
|
||||
item {
|
||||
id: 14
|
||||
name: 'motorbike'
|
||||
}
|
||||
item {
|
||||
id: 15
|
||||
name: 'person'
|
||||
}
|
||||
item {
|
||||
id: 16
|
||||
name: 'pottedplant'
|
||||
}
|
||||
item {
|
||||
id: 17
|
||||
name: 'sheep'
|
||||
}
|
||||
item {
|
||||
id: 18
|
||||
name: 'sofa'
|
||||
}
|
||||
item {
|
||||
id: 19
|
||||
name: 'train'
|
||||
}
|
||||
item {
|
||||
id: 20
|
||||
name: 'tvmonitor'
|
||||
}
|
||||
```
|
||||
1. Use [create_pascal_tf_record.py](https://github.com/tensorflow/models/blob/master/research/object_detection/dataset_tools/create_pascal_tf_record.py)
|
||||
to convert VOC2007 dataset to TFRecord format.
|
||||
As example:
|
||||
```bash
|
||||
python create_pascal_tf_record.py --data_dir <path to VOCdevkit> --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt
|
||||
```
|
||||
1. Zip train images
|
||||
```bash
|
||||
cat <path to VOCdevkit>/VOC2007/ImageSets/Main/train.txt | while read p; do echo <path to VOCdevkit>/VOC2007/JPEGImages/${p}.jpg ; done | zip images.zip -j -@
|
||||
```
|
||||
1. Create a CVAT task with the following labels:
|
||||
```bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
Select images.zip as data.
|
||||
See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details.
|
||||
1. Zip pascal.tfrecord and label_map.pbtxt files together
|
||||
```bash
|
||||
zip anno.zip -j <path to pascal.tfrecord> <path to label_map.pbtxt>
|
||||
```
|
||||
1. Click `Upload annotation` button, choose `TFRecord ZIP 1.0` and select the *.zip file
|
||||
with labels from previous step. It may take some time.
|
||||
|
||||
### PNG mask
|
||||
#### Mask dumper description
|
||||
- downloaded file: a zip archive with the following structure:
|
||||
```bash
|
||||
taskname.zip
|
||||
├── labelmap.txt # optional, required for non-VOC labels
|
||||
├── ImageSets/
|
||||
│ └── Segmentation/
|
||||
│ └── default.txt # list of image names without extension
|
||||
├── SegmentationClass/ # merged class masks
|
||||
│ ├── image1.png
|
||||
│ └── image2.png
|
||||
└── SegmentationObject/ # merged instance masks
|
||||
├── image1.png
|
||||
└── image2.png
|
||||
```
|
||||
Mask is a png image with several (RGB) channels where each pixel has own color which corresponds to a label.
|
||||
Color generation correspond to the Pascal VOC color generation
|
||||
[algorithm](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/devkit_doc.html#sec:voclabelcolormap).
|
||||
(0, 0, 0) is used for background.
|
||||
`labelmap.txt` file contains the values of the used colors in RGB format. The file structure:
|
||||
```bash
|
||||
# label:color_rgb:parts:actions
|
||||
background:0,128,0::
|
||||
aeroplane:10,10,128::
|
||||
bicycle:10,128,0::
|
||||
bird:0,108,128::
|
||||
boat:108,0,100::
|
||||
bottle:18,0,8::
|
||||
bus:12,28,0::
|
||||
```
|
||||
- supported shapes - Rectangles, Polygons
|
||||
|
||||
#### Mask loader description
|
||||
- uploaded file: a zip archive of the following structure:
|
||||
```bash
|
||||
name.zip
|
||||
├── labelmap.txt # optional, required for non-VOC labels
|
||||
├── ImageSets/
|
||||
│ └── Segmentation/
|
||||
│ └── <any_subset_name>.txt
|
||||
├── SegmentationClass/
|
||||
│ ├── image1.png
|
||||
│ └── image2.png
|
||||
└── SegmentationObject/
|
||||
├── image1.png
|
||||
└── image2.png
|
||||
```
|
||||
- supported shapes: Polygons
|
||||
- additional comments: the CVAT task should be created with the full label set that may be in the annotation files
|
||||
|
||||
### [MOT sequence](https://arxiv.org/pdf/1906.04567.pdf)
|
||||
#### Dumper
|
||||
- downloaded file: a zip archive of the following structure:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── img1/
|
||||
| ├── imgage1.jpg
|
||||
| └── imgage2.jpg
|
||||
└── gt/
|
||||
├── labels.txt
|
||||
└── gt.txt
|
||||
|
||||
# labels.txt
|
||||
cat
|
||||
dog
|
||||
person
|
||||
...
|
||||
|
||||
# gt.txt
|
||||
# frame_id, track_id, x, y, w, h, "not ignored", class_id, visibility, <skipped>
|
||||
1,1,1363,569,103,241,1,1,0.86014
|
||||
...
|
||||
|
||||
```
|
||||
- supported annotations: Rectangle shapes and tracks
|
||||
- supported attributes: `visibility` (number), `ignored` (checkbox)
|
||||
|
||||
#### Loader
|
||||
- uploaded file: a zip archive of the structure above or:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── labels.txt # optional, mandatory for non-official labels
|
||||
└── gt.txt
|
||||
```
|
||||
- supported annotations: Rectangle tracks
|
||||
|
||||
### [LabelMe](http://labelme.csail.mit.edu/Release3.0)
|
||||
#### Dumper
|
||||
- downloaded file: a zip archive of the following structure:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── img1.jpg
|
||||
└── img1.xml
|
||||
```
|
||||
- supported annotations: Rectangles, Polygons (with attributes)
|
||||
|
||||
#### Loader
|
||||
- uploaded file: a zip archive of the following structure:
|
||||
```bash
|
||||
taskname.zip/
|
||||
├── Masks/
|
||||
| ├── img1_mask1.png
|
||||
| └── img1_mask2.png
|
||||
├── img1.xml
|
||||
├── img2.xml
|
||||
└── img3.xml
|
||||
```
|
||||
- supported annotations: Rectangles, Polygons, Masks (as polygons)
|
||||
@ -1,4 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
default_app_config = 'cvat.apps.annotation.apps.AnnotationConfig'
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -1,518 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import copy
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from cvat.apps.engine.data_manager import DataManager, TrackManager
|
||||
from cvat.apps.engine.serializers import LabeledDataSerializer
|
||||
|
||||
class AnnotationIR:
|
||||
def __init__(self, data=None):
|
||||
self.reset()
|
||||
if data:
|
||||
self._tags = getattr(data, 'tags', []) or data['tags']
|
||||
self._shapes = getattr(data, 'shapes', []) or data['shapes']
|
||||
self._tracks = getattr(data, 'tracks', []) or data['tracks']
|
||||
|
||||
def add_tag(self, tag):
|
||||
self._tags.append(tag)
|
||||
|
||||
def add_shape(self, shape):
|
||||
self._shapes.append(shape)
|
||||
|
||||
def add_track(self, track):
|
||||
self._tracks.append(track)
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return self._tags
|
||||
|
||||
@property
|
||||
def shapes(self):
|
||||
return self._shapes
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
return self._tracks
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@tags.setter
|
||||
def tags(self, tags):
|
||||
self._tags = tags
|
||||
|
||||
@shapes.setter
|
||||
def shapes(self, shapes):
|
||||
self._shapes = shapes
|
||||
|
||||
@tracks.setter
|
||||
def tracks(self, tracks):
|
||||
self._tracks = tracks
|
||||
|
||||
@version.setter
|
||||
def version(self, version):
|
||||
self._version = version
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return {
|
||||
'version': self.version,
|
||||
'tags': self.tags,
|
||||
'shapes': self.shapes,
|
||||
'tracks': self.tracks,
|
||||
}
|
||||
|
||||
def serialize(self):
|
||||
serializer = LabeledDataSerializer(data=self.data)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
return serializer.data
|
||||
|
||||
@staticmethod
|
||||
def _is_shape_inside(shape, start, stop):
|
||||
return start <= int(shape['frame']) <= stop
|
||||
|
||||
@staticmethod
|
||||
def _is_track_inside(track, start, stop):
|
||||
# a <= b
|
||||
def has_overlap(a, b):
|
||||
return 0 <= min(b, stop) - max(a, start)
|
||||
|
||||
prev_shape = None
|
||||
for shape in track['shapes']:
|
||||
if prev_shape and not prev_shape['outside'] and \
|
||||
has_overlap(prev_shape['frame'], shape['frame']):
|
||||
return True
|
||||
prev_shape = shape
|
||||
|
||||
if not prev_shape['outside'] and prev_shape['frame'] <= stop:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _slice_track(track_, start, stop):
|
||||
def filter_track_shapes(shapes):
|
||||
shapes = [s for s in shapes if AnnotationIR._is_shape_inside(s, start, stop)]
|
||||
drop_count = 0
|
||||
for s in shapes:
|
||||
if s['outside']:
|
||||
drop_count += 1
|
||||
else:
|
||||
break
|
||||
# Need to leave the last shape if all shapes are outside
|
||||
if drop_count == len(shapes):
|
||||
drop_count -= 1
|
||||
|
||||
return shapes[drop_count:]
|
||||
|
||||
track = copy.deepcopy(track_)
|
||||
segment_shapes = filter_track_shapes(track['shapes'])
|
||||
|
||||
if len(segment_shapes) < len(track['shapes']):
|
||||
interpolated_shapes = TrackManager.get_interpolated_shapes(track, start, stop)
|
||||
scoped_shapes = filter_track_shapes(interpolated_shapes)
|
||||
|
||||
if scoped_shapes:
|
||||
if not scoped_shapes[0]['keyframe']:
|
||||
segment_shapes.insert(0, scoped_shapes[0])
|
||||
if not scoped_shapes[-1]['keyframe']:
|
||||
segment_shapes.append(scoped_shapes[-1])
|
||||
|
||||
# Should delete 'interpolation_shapes' and 'keyframe' keys because
|
||||
# Track and TrackedShape models don't expect these fields
|
||||
del track['interpolated_shapes']
|
||||
for shape in segment_shapes:
|
||||
del shape['keyframe']
|
||||
|
||||
track['shapes'] = segment_shapes
|
||||
track['frame'] = track['shapes'][0]['frame']
|
||||
return track
|
||||
|
||||
#makes a data copy from specified frame interval
|
||||
def slice(self, start, stop):
|
||||
splitted_data = AnnotationIR()
|
||||
splitted_data.tags = [copy.deepcopy(t) for t in self.tags if self._is_shape_inside(t, start, stop)]
|
||||
splitted_data.shapes = [copy.deepcopy(s) for s in self.shapes if self._is_shape_inside(s, start, stop)]
|
||||
splitted_data.tracks = [self._slice_track(t, start, stop) for t in self.tracks if self._is_track_inside(t, start, stop)]
|
||||
|
||||
return splitted_data
|
||||
|
||||
@data.setter
|
||||
def data(self, data):
|
||||
self.version = data['version']
|
||||
self.tags = data['tags']
|
||||
self.shapes = data['shapes']
|
||||
self.tracks = data['tracks']
|
||||
|
||||
def reset(self):
|
||||
self._version = 0
|
||||
self._tags = []
|
||||
self._shapes = []
|
||||
self._tracks = []
|
||||
|
||||
class Annotation:
|
||||
Attribute = namedtuple('Attribute', 'name, value')
|
||||
LabeledShape = namedtuple('LabeledShape', 'type, frame, label, points, occluded, attributes, group, z_order')
|
||||
LabeledShape.__new__.__defaults__ = (0, 0)
|
||||
TrackedShape = namedtuple('TrackedShape', 'type, frame, points, occluded, outside, keyframe, attributes, group, z_order, label, track_id')
|
||||
TrackedShape.__new__.__defaults__ = (0, 0, None, 0)
|
||||
Track = namedtuple('Track', 'label, group, shapes')
|
||||
Tag = namedtuple('Tag', 'frame, label, attributes, group')
|
||||
Tag.__new__.__defaults__ = (0, )
|
||||
Frame = namedtuple('Frame', 'frame, name, width, height, labeled_shapes, tags')
|
||||
|
||||
def __init__(self, annotation_ir, db_task, scheme='', host='', create_callback=None):
|
||||
self._annotation_ir = annotation_ir
|
||||
self._db_task = db_task
|
||||
self._scheme = scheme
|
||||
self._host = host
|
||||
self._create_callback=create_callback
|
||||
self._MAX_ANNO_SIZE=30000
|
||||
self._frame_info = {}
|
||||
self._frame_mapping = {}
|
||||
self._frame_step = db_task.data.get_frame_step()
|
||||
|
||||
db_labels = self._db_task.label_set.all().prefetch_related('attributespec_set').order_by('pk')
|
||||
|
||||
self._label_mapping = OrderedDict((db_label.id, db_label) for db_label in db_labels)
|
||||
|
||||
self._attribute_mapping = {db_label.id: {'mutable': {}, 'immutable': {}} for db_label in db_labels}
|
||||
|
||||
for db_label in db_labels:
|
||||
for db_attribute in db_label.attributespec_set.all():
|
||||
if db_attribute.mutable:
|
||||
self._attribute_mapping[db_label.id]['mutable'][db_attribute.id] = db_attribute.name
|
||||
else:
|
||||
self._attribute_mapping[db_label.id]['immutable'][db_attribute.id] = db_attribute.name
|
||||
|
||||
self._attribute_mapping_merged = {}
|
||||
for label_id, attr_mapping in self._attribute_mapping.items():
|
||||
self._attribute_mapping_merged[label_id] = {
|
||||
**attr_mapping['mutable'],
|
||||
**attr_mapping['immutable'],
|
||||
}
|
||||
|
||||
self._init_frame_info()
|
||||
self._init_meta()
|
||||
|
||||
def _get_label_id(self, label_name):
|
||||
for db_label in self._label_mapping.values():
|
||||
if label_name == db_label.name:
|
||||
return db_label.id
|
||||
return None
|
||||
|
||||
def _get_label_name(self, label_id):
|
||||
return self._label_mapping[label_id].name
|
||||
|
||||
def _get_attribute_name(self, attribute_id):
|
||||
for attribute_mapping in self._attribute_mapping_merged.values():
|
||||
if attribute_id in attribute_mapping:
|
||||
return attribute_mapping[attribute_id]
|
||||
|
||||
def _get_attribute_id(self, label_id, attribute_name, attribute_type=None):
|
||||
if attribute_type:
|
||||
container = self._attribute_mapping[label_id][attribute_type]
|
||||
else:
|
||||
container = self._attribute_mapping_merged[label_id]
|
||||
|
||||
for attr_id, attr_name in container.items():
|
||||
if attribute_name == attr_name:
|
||||
return attr_id
|
||||
return None
|
||||
|
||||
def _get_mutable_attribute_id(self, label_id, attribute_name):
|
||||
return self._get_attribute_id(label_id, attribute_name, 'mutable')
|
||||
|
||||
def _get_immutable_attribute_id(self, label_id, attribute_name):
|
||||
return self._get_attribute_id(label_id, attribute_name, 'immutable')
|
||||
|
||||
def _init_frame_info(self):
|
||||
if hasattr(self._db_task.data, 'video'):
|
||||
self._frame_info = {
|
||||
frame: {
|
||||
"path": "frame_{:06d}".format(frame),
|
||||
"width": self._db_task.data.video.width,
|
||||
"height": self._db_task.data.video.height,
|
||||
} for frame in range(self._db_task.data.size)
|
||||
}
|
||||
else:
|
||||
self._frame_info = {db_image.frame: {
|
||||
"path": db_image.path,
|
||||
"width": db_image.width,
|
||||
"height": db_image.height,
|
||||
} for db_image in self._db_task.data.images.all()}
|
||||
|
||||
self._frame_mapping = {
|
||||
self._get_filename(info["path"]): frame for frame, info in self._frame_info.items()
|
||||
}
|
||||
|
||||
def _init_meta(self):
|
||||
db_segments = self._db_task.segment_set.all().prefetch_related('job_set')
|
||||
self._meta = OrderedDict([
|
||||
("task", OrderedDict([
|
||||
("id", str(self._db_task.id)),
|
||||
("name", self._db_task.name),
|
||||
("size", str(self._db_task.data.size)),
|
||||
("mode", self._db_task.mode),
|
||||
("overlap", str(self._db_task.overlap)),
|
||||
("bugtracker", self._db_task.bug_tracker),
|
||||
("created", str(timezone.localtime(self._db_task.created_date))),
|
||||
("updated", str(timezone.localtime(self._db_task.updated_date))),
|
||||
("start_frame", str(self._db_task.data.start_frame)),
|
||||
("stop_frame", str(self._db_task.data.stop_frame)),
|
||||
("frame_filter", self._db_task.data.frame_filter),
|
||||
("z_order", str(self._db_task.z_order)),
|
||||
|
||||
("labels", [
|
||||
("label", OrderedDict([
|
||||
("name", db_label.name),
|
||||
("attributes", [
|
||||
("attribute", OrderedDict([
|
||||
("name", db_attr.name),
|
||||
("mutable", str(db_attr.mutable)),
|
||||
("input_type", db_attr.input_type),
|
||||
("default_value", db_attr.default_value),
|
||||
("values", db_attr.values)]))
|
||||
for db_attr in db_label.attributespec_set.all()])
|
||||
])) for db_label in self._label_mapping.values()
|
||||
]),
|
||||
|
||||
("segments", [
|
||||
("segment", OrderedDict([
|
||||
("id", str(db_segment.id)),
|
||||
("start", str(db_segment.start_frame)),
|
||||
("stop", str(db_segment.stop_frame)),
|
||||
("url", "{0}://{1}/?id={2}".format(
|
||||
self._scheme, self._host, db_segment.job_set.all()[0].id))]
|
||||
)) for db_segment in db_segments
|
||||
]),
|
||||
|
||||
("owner", OrderedDict([
|
||||
("username", self._db_task.owner.username),
|
||||
("email", self._db_task.owner.email)
|
||||
]) if self._db_task.owner else ""),
|
||||
|
||||
("assignee", OrderedDict([
|
||||
("username", self._db_task.assignee.username),
|
||||
("email", self._db_task.assignee.email)
|
||||
]) if self._db_task.assignee else ""),
|
||||
])),
|
||||
("dumped", str(timezone.localtime(timezone.now())))
|
||||
])
|
||||
|
||||
if hasattr(self._db_task.data, "video"):
|
||||
self._meta["task"]["original_size"] = OrderedDict([
|
||||
("width", str(self._db_task.data.video.width)),
|
||||
("height", str(self._db_task.data.video.height))
|
||||
])
|
||||
# Add source to dumped file
|
||||
self._meta["source"] = str(os.path.basename(self._db_task.data.video.path))
|
||||
|
||||
def _export_attributes(self, attributes):
|
||||
exported_attributes = []
|
||||
for attr in attributes:
|
||||
attribute_name = self._get_attribute_name(attr["spec_id"])
|
||||
exported_attributes.append(Annotation.Attribute(
|
||||
name=attribute_name,
|
||||
value=attr["value"],
|
||||
))
|
||||
return exported_attributes
|
||||
|
||||
def _export_tracked_shape(self, shape):
|
||||
return Annotation.TrackedShape(
|
||||
type=shape["type"],
|
||||
frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step,
|
||||
label=self._get_label_name(shape["label_id"]),
|
||||
points=shape["points"],
|
||||
occluded=shape["occluded"],
|
||||
z_order=shape.get("z_order", 0),
|
||||
group=shape.get("group", 0),
|
||||
outside=shape.get("outside", False),
|
||||
keyframe=shape.get("keyframe", True),
|
||||
track_id=shape["track_id"],
|
||||
attributes=self._export_attributes(shape["attributes"]),
|
||||
)
|
||||
|
||||
def _export_labeled_shape(self, shape):
|
||||
return Annotation.LabeledShape(
|
||||
type=shape["type"],
|
||||
label=self._get_label_name(shape["label_id"]),
|
||||
frame=self._db_task.data.start_frame + shape["frame"] * self._frame_step,
|
||||
points=shape["points"],
|
||||
occluded=shape["occluded"],
|
||||
z_order=shape.get("z_order", 0),
|
||||
group=shape.get("group", 0),
|
||||
attributes=self._export_attributes(shape["attributes"]),
|
||||
)
|
||||
|
||||
def _export_tag(self, tag):
|
||||
return Annotation.Tag(
|
||||
frame=self._db_task.data.start_frame + tag["frame"] * self._frame_step,
|
||||
label=self._get_label_name(tag["label_id"]),
|
||||
group=tag.get("group", 0),
|
||||
attributes=self._export_attributes(tag["attributes"]),
|
||||
)
|
||||
|
||||
def group_by_frame(self):
|
||||
def _get_frame(annotations, shape):
|
||||
db_image = self._frame_info[shape["frame"]]
|
||||
frame = self._db_task.data.start_frame + shape["frame"] * self._frame_step
|
||||
if frame not in annotations:
|
||||
annotations[frame] = Annotation.Frame(
|
||||
frame=frame,
|
||||
name=db_image['path'],
|
||||
height=db_image["height"],
|
||||
width=db_image["width"],
|
||||
labeled_shapes=[],
|
||||
tags=[],
|
||||
)
|
||||
return annotations[frame]
|
||||
|
||||
annotations = {}
|
||||
data_manager = DataManager(self._annotation_ir)
|
||||
for shape in sorted(data_manager.to_shapes(self._db_task.data.size), key=lambda shape: shape.get("z_order", 0)):
|
||||
if 'track_id' in shape:
|
||||
exported_shape = self._export_tracked_shape(shape)
|
||||
else:
|
||||
exported_shape = self._export_labeled_shape(shape)
|
||||
_get_frame(annotations, shape).labeled_shapes.append(exported_shape)
|
||||
|
||||
for tag in self._annotation_ir.tags:
|
||||
_get_frame(annotations, tag).tags.append(self._export_tag(tag))
|
||||
|
||||
return iter(annotations.values())
|
||||
|
||||
@property
|
||||
def shapes(self):
|
||||
for shape in self._annotation_ir.shapes:
|
||||
yield self._export_labeled_shape(shape)
|
||||
|
||||
@property
|
||||
def tracks(self):
|
||||
for idx, track in enumerate(self._annotation_ir.tracks):
|
||||
tracked_shapes = TrackManager.get_interpolated_shapes(track, 0, self._db_task.data.size)
|
||||
for tracked_shape in tracked_shapes:
|
||||
tracked_shape["attributes"] += track["attributes"]
|
||||
tracked_shape["track_id"] = idx
|
||||
tracked_shape["group"] = track["group"]
|
||||
tracked_shape["label_id"] = track["label_id"]
|
||||
|
||||
yield Annotation.Track(
|
||||
label=self._get_label_name(track["label_id"]),
|
||||
group=track["group"],
|
||||
shapes=[self._export_tracked_shape(shape) for shape in tracked_shapes],
|
||||
)
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
for tag in self._annotation_ir.tags:
|
||||
yield self._export_tag(tag)
|
||||
|
||||
@property
|
||||
def meta(self):
|
||||
return self._meta
|
||||
|
||||
def _import_tag(self, tag):
|
||||
_tag = tag._asdict()
|
||||
label_id = self._get_label_id(_tag.pop('label'))
|
||||
_tag['frame'] = (int(_tag['frame']) - self._db_task.data.start_frame) // self._frame_step
|
||||
_tag['label_id'] = label_id
|
||||
_tag['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _tag['attributes']
|
||||
if self._get_attribute_id(label_id, attrib.name)]
|
||||
return _tag
|
||||
|
||||
def _import_attribute(self, label_id, attribute):
|
||||
return {
|
||||
'spec_id': self._get_attribute_id(label_id, attribute.name),
|
||||
'value': attribute.value,
|
||||
}
|
||||
|
||||
def _import_shape(self, shape):
|
||||
_shape = shape._asdict()
|
||||
label_id = self._get_label_id(_shape.pop('label'))
|
||||
_shape['frame'] = (int(_shape['frame']) - self._db_task.data.start_frame) // self._frame_step
|
||||
_shape['label_id'] = label_id
|
||||
_shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in _shape['attributes']
|
||||
if self._get_attribute_id(label_id, attrib.name)]
|
||||
return _shape
|
||||
|
||||
def _import_track(self, track):
|
||||
_track = track._asdict()
|
||||
label_id = self._get_label_id(_track.pop('label'))
|
||||
_track['frame'] = (min(int(shape.frame) for shape in _track['shapes']) - \
|
||||
self._db_task.data.start_frame) // self._frame_step
|
||||
_track['label_id'] = label_id
|
||||
_track['attributes'] = []
|
||||
_track['shapes'] = [shape._asdict() for shape in _track['shapes']]
|
||||
for shape in _track['shapes']:
|
||||
shape['frame'] = (int(shape['frame']) - self._db_task.data.start_frame) // self._frame_step
|
||||
_track['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes']
|
||||
if self._get_immutable_attribute_id(label_id, attrib.name)]
|
||||
shape['attributes'] = [self._import_attribute(label_id, attrib) for attrib in shape['attributes']
|
||||
if self._get_mutable_attribute_id(label_id, attrib.name)]
|
||||
|
||||
return _track
|
||||
|
||||
def _call_callback(self):
|
||||
if self._len() > self._MAX_ANNO_SIZE:
|
||||
self._create_callback(self._annotation_ir.serialize())
|
||||
self._annotation_ir.reset()
|
||||
|
||||
def add_tag(self, tag):
|
||||
imported_tag = self._import_tag(tag)
|
||||
if imported_tag['label_id']:
|
||||
self._annotation_ir.add_tag(imported_tag)
|
||||
self._call_callback()
|
||||
|
||||
def add_shape(self, shape):
|
||||
imported_shape = self._import_shape(shape)
|
||||
if imported_shape['label_id']:
|
||||
self._annotation_ir.add_shape(imported_shape)
|
||||
self._call_callback()
|
||||
|
||||
def add_track(self, track):
|
||||
imported_track = self._import_track(track)
|
||||
if imported_track['label_id']:
|
||||
self._annotation_ir.add_track(imported_track)
|
||||
self._call_callback()
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._annotation_ir
|
||||
|
||||
def _len(self):
|
||||
track_len = 0
|
||||
for track in self._annotation_ir.tracks:
|
||||
track_len += len(track['shapes'])
|
||||
|
||||
return len(self._annotation_ir.tags) + len(self._annotation_ir.shapes) + track_len
|
||||
|
||||
@property
|
||||
def frame_info(self):
|
||||
return self._frame_info
|
||||
|
||||
@property
|
||||
def frame_step(self):
|
||||
return self._frame_step
|
||||
|
||||
@staticmethod
|
||||
def _get_filename(path):
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
|
||||
def match_frame(self, filename):
|
||||
# try to match by filename
|
||||
_filename = self._get_filename(filename)
|
||||
if _filename in self._frame_mapping:
|
||||
return self._frame_mapping[_filename]
|
||||
|
||||
raise Exception("Cannot match filename or determinate framenumber for {} filename".format(filename))
|
||||
@ -1,18 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
from cvat.apps.annotation.settings import BUILTIN_FORMATS
|
||||
|
||||
def register_builtins_callback(sender, **kwargs):
|
||||
from .format import register_format
|
||||
for builtin_format in BUILTIN_FORMATS:
|
||||
register_format(builtin_format)
|
||||
|
||||
class AnnotationConfig(AppConfig):
|
||||
name = 'cvat.apps.annotation'
|
||||
|
||||
def ready(self):
|
||||
post_migrate.connect(register_builtins_callback, sender=self)
|
||||
@ -1,41 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from cvat.apps.annotation import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from cvat.apps.annotation.serializers import AnnotationFormatSerializer
|
||||
from django.core.files import File
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
def register_format(format_file):
|
||||
source_code = open(format_file, 'r').read()
|
||||
global_vars = {}
|
||||
exec(source_code, global_vars)
|
||||
if "format_spec" not in global_vars or not isinstance(global_vars["format_spec"], dict):
|
||||
raise Exception("Could not find 'format_spec' definition in format file specification")
|
||||
|
||||
format_spec = deepcopy(global_vars["format_spec"])
|
||||
format_spec["handler_file"] = File(open(format_file))
|
||||
for spec in format_spec["loaders"] + format_spec["dumpers"]:
|
||||
spec["display_name"] = spec["display_name"].format(
|
||||
name=format_spec["name"],
|
||||
format=spec["format"],
|
||||
version=spec["version"],
|
||||
)
|
||||
|
||||
try:
|
||||
annotation_format = models.AnnotationFormat.objects.get(name=format_spec["name"])
|
||||
serializer = AnnotationFormatSerializer(annotation_format, data=format_spec)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
except ObjectDoesNotExist:
|
||||
serializer = AnnotationFormatSerializer(data=format_spec)
|
||||
if serializer.is_valid(raise_exception=True):
|
||||
serializer.save()
|
||||
|
||||
def get_annotation_formats():
|
||||
return AnnotationFormatSerializer(
|
||||
models.AnnotationFormat.objects.all(),
|
||||
many=True).data
|
||||
@ -1,48 +0,0 @@
|
||||
# Generated by Django 2.1.9 on 2019-07-31 15:20
|
||||
|
||||
import cvat.apps.annotation.models
|
||||
import cvat.apps.engine.models
|
||||
from django.conf import settings
|
||||
import django.core.files.storage
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnnotationFormat',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', cvat.apps.engine.models.SafeCharField(max_length=256)),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now_add=True)),
|
||||
('handler_file', models.FileField(storage=django.core.files.storage.FileSystemStorage(location=settings.BASE_DIR), upload_to=cvat.apps.annotation.models.upload_file_handler)),
|
||||
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AnnotationHandler',
|
||||
fields=[
|
||||
('type', models.CharField(choices=[('dumper', 'DUMPER'), ('loader', 'LOADER')], max_length=16)),
|
||||
('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)),
|
||||
('format', models.CharField(max_length=16)),
|
||||
('version', models.CharField(max_length=16)),
|
||||
('handler', models.CharField(max_length=256)),
|
||||
('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')),
|
||||
],
|
||||
options={
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,74 +0,0 @@
|
||||
# Generated by Django 2.1.9 on 2019-08-05 06:27
|
||||
|
||||
import cvat.apps.engine.models
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
def split_handlers(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
handler_model = apps.get_model('annotation', 'AnnotationHandler')
|
||||
dumper_model = apps.get_model('annotation', "AnnotationDumper")
|
||||
loader_model = apps.get_model('annotation', 'AnnotationLoader')
|
||||
|
||||
|
||||
for db_handler in handler_model.objects.all():
|
||||
if db_handler.type == "dumper":
|
||||
new_handler = dumper_model()
|
||||
else:
|
||||
new_handler = loader_model()
|
||||
|
||||
new_handler.display_name = db_handler.display_name
|
||||
new_handler.format = db_handler.format
|
||||
new_handler.version = db_handler.version
|
||||
new_handler.handler = db_handler.handler
|
||||
new_handler.annotation_format = db_handler.annotation_format
|
||||
|
||||
new_handler.save()
|
||||
db_handler.delete()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('annotation', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AnnotationDumper',
|
||||
fields=[
|
||||
('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)),
|
||||
('format', models.CharField(max_length=16)),
|
||||
('version', models.CharField(max_length=16)),
|
||||
('handler', models.CharField(max_length=256)),
|
||||
('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AnnotationLoader',
|
||||
fields=[
|
||||
('display_name', cvat.apps.engine.models.SafeCharField(max_length=256, primary_key=True, serialize=False)),
|
||||
('format', models.CharField(max_length=16)),
|
||||
('version', models.CharField(max_length=16)),
|
||||
('handler', models.CharField(max_length=256)),
|
||||
('annotation_format', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='annotation.AnnotationFormat')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
'default_permissions': (),
|
||||
},
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=split_handlers,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='annotationhandler',
|
||||
name='annotation_format',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='AnnotationHandler',
|
||||
),
|
||||
]
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -1,46 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from cvat.apps.engine.models import SafeCharField
|
||||
|
||||
def upload_file_handler(instance, filename):
|
||||
return os.path.join('formats', str(instance.id), filename)
|
||||
|
||||
class AnnotationFormat(models.Model):
|
||||
name = SafeCharField(max_length=256)
|
||||
owner = models.ForeignKey(User, null=True, blank=True,
|
||||
on_delete=models.SET_NULL)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now_add=True)
|
||||
handler_file = models.FileField(
|
||||
upload_to=upload_file_handler,
|
||||
storage=FileSystemStorage(location=os.path.join(settings.BASE_DIR)),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
|
||||
class AnnotationHandler(models.Model):
|
||||
display_name = SafeCharField(max_length=256, primary_key=True)
|
||||
format = models.CharField(max_length=16)
|
||||
version = models.CharField(max_length=16)
|
||||
handler = models.CharField(max_length=256)
|
||||
annotation_format = models.ForeignKey(AnnotationFormat, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
abstract = True
|
||||
|
||||
class AnnotationDumper(AnnotationHandler):
|
||||
pass
|
||||
|
||||
class AnnotationLoader(AnnotationHandler):
|
||||
pass
|
||||
@ -1,81 +0,0 @@
|
||||
# Copyright (C) 2018-2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
||||
from cvat.apps.annotation import models
|
||||
|
||||
class AnnotationDumperSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.AnnotationDumper
|
||||
exclude = ('annotation_format',)
|
||||
# https://www.django-rest-framework.org/api-guide/validators/#updating-nested-serializers
|
||||
extra_kwargs = {
|
||||
'display_name': {
|
||||
'validators': [],
|
||||
},
|
||||
}
|
||||
|
||||
class AnnotationLoaderSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.AnnotationLoader
|
||||
exclude = ('annotation_format',)
|
||||
# https://www.django-rest-framework.org/api-guide/validators/#updating-nested-serializers
|
||||
extra_kwargs = {
|
||||
'display_name': {
|
||||
'validators': [],
|
||||
},
|
||||
}
|
||||
|
||||
class AnnotationFormatSerializer(serializers.ModelSerializer):
|
||||
dumpers = AnnotationDumperSerializer(many=True, source="annotationdumper_set")
|
||||
loaders = AnnotationLoaderSerializer(many=True, source="annotationloader_set")
|
||||
|
||||
class Meta:
|
||||
model = models.AnnotationFormat
|
||||
fields = "__all__"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def create(self, validated_data):
|
||||
dumpers = validated_data.pop("annotationdumper_set")
|
||||
loaders = validated_data.pop("annotationloader_set")
|
||||
|
||||
annotation_format = models.AnnotationFormat()
|
||||
annotation_format.name = validated_data["name"]
|
||||
annotation_format.handler_file = validated_data["handler_file"].name
|
||||
annotation_format.save()
|
||||
|
||||
for dumper in dumpers:
|
||||
models.AnnotationDumper(annotation_format=annotation_format, **dumper).save()
|
||||
|
||||
for loader in loaders:
|
||||
models.AnnotationLoader(annotation_format=annotation_format, **loader).save()
|
||||
|
||||
return annotation_format
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def update(self, instance, validated_data):
|
||||
dumper_names = [handler["display_name"] for handler in validated_data["annotationdumper_set"]]
|
||||
loader_names = [handler["display_name"] for handler in validated_data["annotationloader_set"]]
|
||||
instance.handler_file = validated_data.get('handler_file', instance.handler_file)
|
||||
instance.owner = validated_data.get('owner', instance.owner)
|
||||
instance.updated_date = timezone.localtime(timezone.now())
|
||||
|
||||
handlers_to_delete = [d for d in instance.annotationdumper_set.all() if d.display_name not in dumper_names] + \
|
||||
[l for l in instance.annotationloader_set.all() if l.display_name not in loader_names]
|
||||
|
||||
for db_handler in handlers_to_delete:
|
||||
db_handler.delete()
|
||||
|
||||
for dumper in validated_data["annotationdumper_set"]:
|
||||
models.AnnotationDumper(annotation_format=instance, **dumper).save()
|
||||
for loader in validated_data["annotationloader_set"]:
|
||||
models.AnnotationLoader(annotation_format=instance, **loader).save()
|
||||
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
class AnnotationFileSerializer(serializers.Serializer):
|
||||
annotation_file = serializers.FileField()
|
||||
@ -1,17 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
|
||||
path_prefix = os.path.join('cvat', 'apps', 'dataset_manager', 'formats')
|
||||
BUILTIN_FORMATS = (
|
||||
os.path.join(path_prefix, 'cvat.py'),
|
||||
os.path.join(path_prefix, 'pascal_voc.py'),
|
||||
os.path.join(path_prefix, 'yolo.py'),
|
||||
os.path.join(path_prefix, 'coco.py'),
|
||||
os.path.join(path_prefix, 'mask.py'),
|
||||
os.path.join(path_prefix, 'tfrecord.py'),
|
||||
os.path.join(path_prefix, 'mot.py'),
|
||||
os.path.join(path_prefix, 'labelme.py'),
|
||||
)
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -1,3 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
@ -0,0 +1,753 @@
|
||||
<!--lint disable list-item-indent-->
|
||||
<!--lint disable list-item-spacing-->
|
||||
<!--lint disable emphasis-marker-->
|
||||
<!--lint disable maximum-line-length-->
|
||||
<!--lint disable list-item-spacing-->
|
||||
|
||||
# Dataset and annotation formats
|
||||
|
||||
## Contents
|
||||
|
||||
- [How to add a format](#how-to-add)
|
||||
- [Format descriptions](#formats)
|
||||
- [CVAT](#cvat)
|
||||
- [LabelMe](#labelme)
|
||||
- [MOT](#mot)
|
||||
- [COCO](#coco)
|
||||
- [PASCAL VOC and mask](#voc)
|
||||
- [YOLO](#yolo)
|
||||
- [TF detection API](#tfrecord)
|
||||
|
||||
## How to add a new annotation format support<a id="how-to-add"></a>
|
||||
|
||||
1. Add a python script to `dataset_manager/formats`
|
||||
1. Add an import statement to [registry.py](./registry.py).
|
||||
1. Implement some importers and exporters as the format requires.
|
||||
|
||||
Each format is supported by an importer and exporter.
|
||||
|
||||
It can be a function or a class decorated with
|
||||
`importer` or `exporter` from [registry.py](./registry.py). Examples:
|
||||
|
||||
``` python
|
||||
@importer(name="MyFormat", version="1.0", ext="ZIP")
|
||||
def my_importer(file_object, task_data, **options):
|
||||
...
|
||||
|
||||
@importer(name="MyFormat", version="2.0", ext="XML")
|
||||
class my_importer(file_object, task_data, **options):
|
||||
def __call__(self, file_object, task_data, **options):
|
||||
...
|
||||
|
||||
@exporter(name="MyFormat", version="1.0", ext="ZIP"):
|
||||
def my_exporter(file_object, task_data, **options):
|
||||
...
|
||||
```
|
||||
|
||||
Each decorator defines format parameters such as:
|
||||
|
||||
- *name*
|
||||
|
||||
- *version*
|
||||
|
||||
- *file extension*. For the `importer` it can be a comma-separated list.
|
||||
These parameters are combined to produce a visible name. It can be
|
||||
set explicitly by the `display_name` argument.
|
||||
|
||||
Importer arguments:
|
||||
|
||||
- *file_object* - a file with annotations or dataset
|
||||
- *task_data* - an instance of `TaskData` class.
|
||||
|
||||
Exporter arguments:
|
||||
|
||||
- *file_object* - a file for annotations or dataset
|
||||
|
||||
- *task_data* - an instance of `TaskData` class.
|
||||
|
||||
- *options* - format-specific options. `save_images` is the option to
|
||||
distinguish if dataset or just annotations are requested.
|
||||
|
||||
[`TaskData`](../bindings.py) provides many task properties and interfaces
|
||||
to add and read task annotations.
|
||||
|
||||
Public members:
|
||||
|
||||
- **TaskData. Attribute** - class, `namedtuple('Attribute', 'name, value')`
|
||||
|
||||
- **TaskData. LabeledShape** - class, `namedtuple('LabeledShape',
|
||||
'type, frame, label, points, occluded, attributes, group, z_order')`
|
||||
|
||||
- **TrackedShape** - `namedtuple('TrackedShape',
|
||||
'type, points, occluded, frame, attributes, outside, keyframe, z_order')`
|
||||
|
||||
- **Track** - class, `namedtuple('Track', 'label, group, shapes')`
|
||||
|
||||
- **Tag** - class, `namedtuple('Tag', 'frame, label, attributes, group')`
|
||||
|
||||
- **Frame** - class, `namedtuple('Frame',
|
||||
'frame, name, width, height, labeled_shapes, tags')`
|
||||
|
||||
- **TaskData. shapes** - property, an iterator over `LabeledShape` objects
|
||||
|
||||
- **TaskData. tracks** - property, an iterator over `Track` objects
|
||||
|
||||
- **TaskData. tags** - property, an iterator over `Tag` objects
|
||||
|
||||
- **TaskData. meta** - property, a dictionary with task information
|
||||
|
||||
- **TaskData. group_by_frame()** - method, returns
|
||||
an iterator over `Frame` objects, which groups annotation objects by frame.
|
||||
Note that `TrackedShape` s will be represented as `LabeledShape` s.
|
||||
|
||||
- **TaskData. add_tag(tag)** - method,
|
||||
tag should be an instance of the `Tag` class
|
||||
|
||||
- **TaskData. add_shape(shape)** - method,
|
||||
shape should be an instance of the `Shape` class
|
||||
|
||||
- **TaskData. add_track(track)** - method,
|
||||
track should be an instance of the `Track` class
|
||||
|
||||
Sample exporter code:
|
||||
|
||||
``` python
|
||||
...
|
||||
# dump meta info if necessary
|
||||
...
|
||||
# iterate over all frames
|
||||
for frame_annotation in task_data.group_by_frame():
|
||||
# get frame info
|
||||
image_name = frame_annotation.name
|
||||
image_width = frame_annotation.width
|
||||
image_height = frame_annotation.height
|
||||
# iterate over all shapes on the frame
|
||||
for shape in frame_annotation.labeled_shapes:
|
||||
label = shape.label
|
||||
xtl = shape.points[0]
|
||||
ytl = shape.points[1]
|
||||
xbr = shape.points[2]
|
||||
ybr = shape.points[3]
|
||||
# iterate over shape attributes
|
||||
for attr in shape.attributes:
|
||||
attr_name = attr.name
|
||||
attr_value = attr.value
|
||||
...
|
||||
# dump annotation code
|
||||
file_object.write(...)
|
||||
...
|
||||
```
|
||||
|
||||
Sample importer code:
|
||||
|
||||
``` python
|
||||
...
|
||||
#read file_object
|
||||
...
|
||||
for parsed_shape in parsed_shapes:
|
||||
shape = task_data.LabeledShape(
|
||||
type="rectangle",
|
||||
points=[0, 0, 100, 100],
|
||||
occluded=False,
|
||||
attributes=[],
|
||||
label="car",
|
||||
outside=False,
|
||||
frame=99,
|
||||
)
|
||||
task_data.add_shape(shape)
|
||||
```
|
||||
|
||||
## Format specifications<a id="formats" />
|
||||
|
||||
### CVAT<a id="cvat" />
|
||||
|
||||
This is the native CVAT annotation format. It supports all CVAT annotations
|
||||
features, so it can be used to make data backups.
|
||||
|
||||
- supported annotations: Rectangles, Polygons, Polylines,
|
||||
Points, Cuboids, Tags, Tracks
|
||||
|
||||
- attributes are supported
|
||||
|
||||
- [Format specification](/cvat/apps/documentation/xml_format.md)
|
||||
|
||||
#### CVAT for images dumper
|
||||
|
||||
Downloaded file: a ZIP file of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── images/
|
||||
| ├── img1.png
|
||||
| └── img2.jpg
|
||||
└── annotations.xml
|
||||
```
|
||||
|
||||
- tracks are split by frames
|
||||
|
||||
#### CVAT for videos dumper
|
||||
|
||||
Downloaded file: a ZIP file of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── images/
|
||||
| ├── frame_000000.png
|
||||
| └── frame_000001.png
|
||||
└── annotations.xml
|
||||
```
|
||||
|
||||
- shapes are exported as single-frame tracks
|
||||
|
||||
#### CVAT loader
|
||||
|
||||
Uploaded file: an XML file or a ZIP file of the structures above
|
||||
|
||||
### [Pascal VOC](http://host.robots.ox.ac.uk/pascal/VOC/)<a id="voc" />
|
||||
|
||||
- [Format specification](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/devkit_doc.pdf)
|
||||
|
||||
- supported annotations:
|
||||
|
||||
- Rectangles (detection and layout tasks)
|
||||
- Tags (action- and classification tasks)
|
||||
- Polygons (segmentation task)
|
||||
|
||||
- supported attributes:
|
||||
|
||||
- `occluded`
|
||||
- `truncated` and `difficult` (should be defined for labels as `checkbox` -es)
|
||||
- action attributes (import only, should be defined as `checkbox` -es)
|
||||
|
||||
#### Pascal VOC export
|
||||
|
||||
Downloaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── JpegImages/
|
||||
│ ├── <image_name1>.jpg
|
||||
│ ├── <image_name2>.jpg
|
||||
│ └── <image_nameN>.jpg
|
||||
├── Annotations/
|
||||
│ ├── <image_name1>.xml
|
||||
│ ├── <image_name2>.xml
|
||||
│ └── <image_nameN>.xml
|
||||
├── ImageSets/
|
||||
│ └── Main/
|
||||
│ └── default.txt
|
||||
└── labelmap.txt
|
||||
|
||||
# labelmap.txt
|
||||
# label : color_rgb : 'body' parts : actions
|
||||
background:::
|
||||
aeroplane:::
|
||||
bicycle:::
|
||||
bird:::
|
||||
```
|
||||
|
||||
#### Pascal VOC import
|
||||
|
||||
Uploaded file: a zip archive of the structure declared above or the following:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── <image_name1>.xml
|
||||
├── <image_name2>.xml
|
||||
└── <image_nameN>.xml
|
||||
```
|
||||
|
||||
It must be possible for CVAT to match the frame name and file name
|
||||
from annotation `.xml` file (the `filename` tag, e. g.
|
||||
`<filename>2008_004457.jpg</filename>` ).
|
||||
|
||||
There are 2 options:
|
||||
|
||||
1. full match between frame name and file name from annotation `.xml`
|
||||
(in cases when task was created from images or image archive).
|
||||
|
||||
1. match by frame number. File name should be `<number>.jpg`
|
||||
or `frame_000000.jpg`. It should be used when task was created from video.
|
||||
|
||||
#### Segmentation mask export
|
||||
|
||||
Downloaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── labelmap.txt # optional, required for non-VOC labels
|
||||
├── ImageSets/
|
||||
│ └── Segmentation/
|
||||
│ └── default.txt # list of image names without extension
|
||||
├── SegmentationClass/ # merged class masks
|
||||
│ ├── image1.png
|
||||
│ └── image2.png
|
||||
└── SegmentationObject/ # merged instance masks
|
||||
├── image1.png
|
||||
└── image2.png
|
||||
|
||||
# labelmap.txt
|
||||
# label : color (RGB) : 'body' parts : actions
|
||||
background:0,128,0::
|
||||
aeroplane:10,10,128::
|
||||
bicycle:10,128,0::
|
||||
bird:0,108,128::
|
||||
boat:108,0,100::
|
||||
bottle:18,0,8::
|
||||
bus:12,28,0::
|
||||
```
|
||||
|
||||
Mask is a `png` image with 1 or 3 channels where each pixel
|
||||
has own color which corresponds to a label.
|
||||
Colors are generated following to Pascal VOC [algorithm](http://host.robots.ox.ac.uk/pascal/VOC/voc2012/htmldoc/devkit_doc.html#sec:voclabelcolormap).
|
||||
`(0, 0, 0)` is used for background by default.
|
||||
|
||||
- supported shapes: Rectangles, Polygons
|
||||
|
||||
#### Segmentation mask import
|
||||
|
||||
Uploaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── labelmap.txt # optional, required for non-VOC labels
|
||||
├── ImageSets/
|
||||
│ └── Segmentation/
|
||||
│ └── <any_subset_name>.txt
|
||||
├── SegmentationClass/
|
||||
│ ├── image1.png
|
||||
│ └── image2.png
|
||||
└── SegmentationObject/
|
||||
├── image1.png
|
||||
└── image2.png
|
||||
```
|
||||
|
||||
- supported shapes: Polygons
|
||||
|
||||
#### How to create a task from Pascal VOC dataset
|
||||
|
||||
1. Download the Pascal Voc dataset (Can be downloaded from the
|
||||
[PASCAL VOC website](http://host.robots.ox.ac.uk/pascal/VOC/))
|
||||
|
||||
1. Create a CVAT task with the following labels:
|
||||
|
||||
``` bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable
|
||||
dog horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
|
||||
You can add `~checkbox=difficult:false ~checkbox=truncated:false`
|
||||
attributes for each label if you want to use them.
|
||||
|
||||
Select interesting image files (See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task) guide for details)
|
||||
|
||||
1. zip the corresponding annotation files
|
||||
|
||||
1. click `Upload annotation` button, choose `Pascal VOC ZIP 1.1`
|
||||
|
||||
and select the zip file with annotations from previous step.
|
||||
It may take some time.
|
||||
|
||||
### [YOLO](https://pjreddie.com/darknet/yolo/)<a id="yolo" />
|
||||
|
||||
- [Format specification](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)
|
||||
- supported annotations: Rectangles
|
||||
|
||||
#### YOLO export
|
||||
|
||||
Downloaded file: a zip archive with following structure:
|
||||
|
||||
``` bash
|
||||
archive.zip/
|
||||
├── obj.data
|
||||
├── obj.names
|
||||
├── obj_<subset>_data
|
||||
│ ├── image1.txt
|
||||
│ └── image2.txt
|
||||
└── train.txt # list of subset image paths
|
||||
|
||||
# the only valid subsets are: train, valid
|
||||
# train.txt and valid.txt:
|
||||
obj_<subset>_data/image1.jpg
|
||||
obj_<subset>_data/image2.jpg
|
||||
|
||||
# obj.data:
|
||||
classes = 3 # optional
|
||||
names = obj.names
|
||||
train = train.txt
|
||||
valid = valid.txt # optional
|
||||
backup = backup/ # optional
|
||||
|
||||
# obj.names:
|
||||
cat
|
||||
dog
|
||||
airplane
|
||||
|
||||
# image_name.txt:
|
||||
# label_id - id from obj.names
|
||||
# cx, cy - relative coordinates of the bbox center
|
||||
# rw, rh - relative size of the bbox
|
||||
# label_id cx cy rw rh
|
||||
1 0.3 0.8 0.1 0.3
|
||||
2 0.7 0.2 0.3 0.1
|
||||
```
|
||||
|
||||
Each annotation `*.txt` file has a name that corresponds to the name of
|
||||
the image file (e. g. `frame_000001.txt` is the annotation
|
||||
for the `frame_000001.jpg` image).
|
||||
The `*.txt` file structure: each line describes label and bounding box
|
||||
in the following format `label_id cx cy w h`.
|
||||
`obj.names` contains the ordered list of label names.
|
||||
|
||||
#### YOLO import
|
||||
|
||||
Uploaded file: a zip archive of the same structure as above
|
||||
It must be possible to match the CVAT frame (image name)
|
||||
and annotation file name. There are 2 options:
|
||||
|
||||
1. full match between image name and name of annotation `*.txt` file
|
||||
(in cases when a task was created from images or archive of images).
|
||||
|
||||
1. match by frame number (if CVAT cannot match by name). File name
|
||||
should be in the following format `<number>.jpg` .
|
||||
It should be used when task was created from a video.
|
||||
|
||||
#### How to create a task from YOLO formatted dataset (from VOC for example)
|
||||
|
||||
1. Follow the official [guide](https://pjreddie.com/darknet/yolo/)(see Training YOLO on VOC section)
|
||||
and prepare the YOLO formatted annotation files.
|
||||
|
||||
1. Zip train images
|
||||
|
||||
``` bash
|
||||
zip images.zip -j -@ < train.txt
|
||||
```
|
||||
|
||||
1. Create a CVAT task with the following labels:
|
||||
|
||||
``` bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog
|
||||
horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
|
||||
Select images. zip as data. Most likely you should use `share`
|
||||
functionality because size of images. zip is more than 500Mb.
|
||||
See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details.
|
||||
|
||||
1. Create `obj.names` with the following content:
|
||||
|
||||
``` bash
|
||||
aeroplane
|
||||
bicycle
|
||||
bird
|
||||
boat
|
||||
bottle
|
||||
bus
|
||||
car
|
||||
cat
|
||||
chair
|
||||
cow
|
||||
diningtable
|
||||
dog
|
||||
horse
|
||||
motorbike
|
||||
person
|
||||
pottedplant
|
||||
sheep
|
||||
sofa
|
||||
train
|
||||
tvmonitor
|
||||
```
|
||||
|
||||
1. Zip all label files together (we need to add only label files that correspond to the train subset)
|
||||
|
||||
``` bash
|
||||
cat train.txt | while read p; do echo ${p%/*/*}/labels/${${p##*/}%%.*}.txt; done | zip labels.zip -j -@ obj.names
|
||||
```
|
||||
|
||||
1. Click `Upload annotation` button, choose `YOLO 1.1` and select the zip
|
||||
|
||||
file with labels from the previous step.
|
||||
|
||||
### [MS COCO Object Detection](http://cocodataset.org/#format-data)<a id="coco" />
|
||||
|
||||
- [Format specification](http://cocodataset.org/#format-data)
|
||||
|
||||
#### COCO dumper description
|
||||
|
||||
Downloaded file: single unpacked `json`.
|
||||
|
||||
- supported annotations: Polygons, Rectangles
|
||||
|
||||
#### COCO loader description
|
||||
|
||||
Uploaded file: single unpacked `*.json` .
|
||||
|
||||
- supported annotations: Polygons, Rectangles (if `segmentation` field is empty)
|
||||
|
||||
#### How to create a task from MS COCO dataset
|
||||
|
||||
1. Download the [MS COCO dataset](http://cocodataset.org/#download).
|
||||
|
||||
For example [2017 Val images](http://images.cocodataset.org/zips/val2017.zip)
|
||||
and [2017 Train/Val annotations](http://images.cocodataset.org/annotations/annotations_trainval2017.zip).
|
||||
|
||||
1. Create a CVAT task with the following labels:
|
||||
|
||||
``` bash
|
||||
person bicycle car motorcycle airplane bus train truck boat "traffic light" "fire hydrant" "stop sign" "parking meter" bench bird cat dog horse sheep cow elephant bear zebra giraffe backpack umbrella handbag tie suitcase frisbee skis snowboard "sports ball" kite "baseball bat" "baseball glove" skateboard surfboard "tennis racket" bottle "wine glass" cup fork knife spoon bowl banana apple sandwich orange broccoli carrot "hot dog" pizza donut cake chair couch "potted plant" bed "dining table" toilet tv laptop mouse remote keyboard "cell phone" microwave oven toaster sink refrigerator book clock vase scissors "teddy bear" "hair drier" toothbrush
|
||||
```
|
||||
|
||||
1. Select val2017.zip as data
|
||||
(See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details)
|
||||
|
||||
1. Unpack `annotations_trainval2017.zip`
|
||||
|
||||
1. click `Upload annotation` button,
|
||||
choose `COCO 1.1` and select `instances_val2017.json.json`
|
||||
annotation file. It can take some time.
|
||||
|
||||
### [TFRecord](https://www.tensorflow.org/tutorials/load_data/tf_records)<a id="tfrecord" />
|
||||
|
||||
TFRecord is a very flexible format, but we try to correspond the
|
||||
format that used in
|
||||
[TF object detection](https://github.com/tensorflow/models/tree/master/research/object_detection)
|
||||
with minimal modifications.
|
||||
|
||||
Used feature description:
|
||||
|
||||
``` python
|
||||
image_feature_description = {
|
||||
'image/filename': tf.io.FixedLenFeature([], tf.string),
|
||||
'image/source_id': tf.io.FixedLenFeature([], tf.string),
|
||||
'image/height': tf.io.FixedLenFeature([], tf.int64),
|
||||
'image/width': tf.io.FixedLenFeature([], tf.int64),
|
||||
# Object boxes and classes.
|
||||
'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32),
|
||||
'image/object/class/label': tf.io.VarLenFeature(tf.int64),
|
||||
'image/object/class/text': tf.io.VarLenFeature(tf.string),
|
||||
}
|
||||
```
|
||||
|
||||
#### TFRecord dumper description
|
||||
|
||||
Downloaded file: a zip archive with following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── task2.tfrecord
|
||||
└── label_map.pbtxt
|
||||
```
|
||||
|
||||
- supported annotations: Rectangles
|
||||
|
||||
#### TFRecord loader description
|
||||
|
||||
Uploaded file: a zip archive of following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
└── task2.tfrecord
|
||||
```
|
||||
|
||||
- supported annotations: Rectangles
|
||||
|
||||
#### How to create a task from TFRecord dataset (from VOC2007 for example)
|
||||
|
||||
1. Create `label_map.pbtxt` file with the following content:
|
||||
|
||||
``` js
|
||||
item {
|
||||
id: 1
|
||||
name: 'aeroplane'
|
||||
}
|
||||
item {
|
||||
id: 2
|
||||
name: 'bicycle'
|
||||
}
|
||||
item {
|
||||
id: 3
|
||||
name: 'bird'
|
||||
}
|
||||
item {
|
||||
id: 4
|
||||
name: 'boat'
|
||||
}
|
||||
item {
|
||||
id: 5
|
||||
name: 'bottle'
|
||||
}
|
||||
item {
|
||||
id: 6
|
||||
name: 'bus'
|
||||
}
|
||||
item {
|
||||
id: 7
|
||||
name: 'car'
|
||||
}
|
||||
item {
|
||||
id: 8
|
||||
name: 'cat'
|
||||
}
|
||||
item {
|
||||
id: 9
|
||||
name: 'chair'
|
||||
}
|
||||
item {
|
||||
id: 10
|
||||
name: 'cow'
|
||||
}
|
||||
item {
|
||||
id: 11
|
||||
name: 'diningtable'
|
||||
}
|
||||
item {
|
||||
id: 12
|
||||
name: 'dog'
|
||||
}
|
||||
item {
|
||||
id: 13
|
||||
name: 'horse'
|
||||
}
|
||||
item {
|
||||
id: 14
|
||||
name: 'motorbike'
|
||||
}
|
||||
item {
|
||||
id: 15
|
||||
name: 'person'
|
||||
}
|
||||
item {
|
||||
id: 16
|
||||
name: 'pottedplant'
|
||||
}
|
||||
item {
|
||||
id: 17
|
||||
name: 'sheep'
|
||||
}
|
||||
item {
|
||||
id: 18
|
||||
name: 'sofa'
|
||||
}
|
||||
item {
|
||||
id: 19
|
||||
name: 'train'
|
||||
}
|
||||
item {
|
||||
id: 20
|
||||
name: 'tvmonitor'
|
||||
}
|
||||
```
|
||||
|
||||
1. Use [create_pascal_tf_record.py](https://github.com/tensorflow/models/blob/master/research/object_detection/dataset_tools/create_pascal_tf_record.py)
|
||||
|
||||
to convert VOC2007 dataset to TFRecord format.
|
||||
As example:
|
||||
|
||||
``` bash
|
||||
python create_pascal_tf_record.py --data_dir <path to VOCdevkit> --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt
|
||||
```
|
||||
|
||||
1. Zip train images
|
||||
|
||||
``` bash
|
||||
cat <path to VOCdevkit>/VOC2007/ImageSets/Main/train.txt | while read p; do echo <path to VOCdevkit>/VOC2007/JPEGImages/${p}.jpg ; done | zip images.zip -j -@
|
||||
```
|
||||
|
||||
1. Create a CVAT task with the following labels:
|
||||
|
||||
``` bash
|
||||
aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor
|
||||
```
|
||||
|
||||
Select images. zip as data.
|
||||
See [Creating an annotation task](cvat/apps/documentation/user_guide.md#creating-an-annotation-task)
|
||||
guide for details.
|
||||
|
||||
1. Zip `pascal.tfrecord` and `label_map.pbtxt` files together
|
||||
|
||||
``` bash
|
||||
zip anno.zip -j <path to pascal.tfrecord> <path to label_map.pbtxt>
|
||||
```
|
||||
|
||||
1. Click `Upload annotation` button, choose `TFRecord 1.0` and select the zip file
|
||||
|
||||
with labels from the previous step. It may take some time.
|
||||
|
||||
### [MOT sequence](https://arxiv.org/pdf/1906.04567.pdf)<a id="mot" />
|
||||
|
||||
#### MOT Dumper
|
||||
|
||||
Downloaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── img1/
|
||||
| ├── imgage1.jpg
|
||||
| └── imgage2.jpg
|
||||
└── gt/
|
||||
├── labels.txt
|
||||
└── gt.txt
|
||||
|
||||
# labels.txt
|
||||
cat
|
||||
dog
|
||||
person
|
||||
...
|
||||
|
||||
# gt.txt
|
||||
# frame_id, track_id, x, y, w, h, "not ignored", class_id, visibility, <skipped>
|
||||
1,1,1363,569,103,241,1,1,0.86014
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
- supported annotations: Rectangle shapes and tracks
|
||||
- supported attributes: `visibility` (number), `ignored` (checkbox)
|
||||
|
||||
#### MOT Loader
|
||||
|
||||
Uploaded file: a zip archive of the structure above or:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── labels.txt # optional, mandatory for non-official labels
|
||||
└── gt.txt
|
||||
```
|
||||
|
||||
- supported annotations: Rectangle tracks
|
||||
|
||||
### [LabelMe](http://labelme.csail.mit.edu/Release3.0)<a id="labelme" />
|
||||
|
||||
#### LabelMe Dumper
|
||||
|
||||
Downloaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── img1.jpg
|
||||
└── img1.xml
|
||||
```
|
||||
|
||||
- supported annotations: Rectangles, Polygons (with attributes)
|
||||
|
||||
#### LabelMe Loader
|
||||
|
||||
Uploaded file: a zip archive of the following structure:
|
||||
|
||||
``` bash
|
||||
taskname.zip/
|
||||
├── Masks/
|
||||
| ├── img1_mask1.png
|
||||
| └── img1_mask2.png
|
||||
├── img1.xml
|
||||
├── img2.xml
|
||||
└── img3.xml
|
||||
```
|
||||
|
||||
- supported annotations: Rectangles, Polygons, Masks (as polygons)
|
||||
@ -0,0 +1,99 @@
|
||||
# Copyright (C) 2019 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import json
|
||||
import os
|
||||
import os.path as osp
|
||||
import shutil
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from cvat.apps.dataset_manager.bindings import (CvatTaskDataExtractor,
|
||||
import_dm_annotations)
|
||||
from cvat.apps.dataset_manager.util import make_zip_archive
|
||||
from cvat.settings.base import BASE_DIR, DATUMARO_PATH
|
||||
from datumaro.components.project import Project
|
||||
|
||||
from ..registry import dm_env, exporter
|
||||
|
||||
|
||||
@exporter(name="Datumaro", ext="ZIP", version="1.0")
|
||||
class DatumaroProjectExporter:
|
||||
_REMOTE_IMAGES_EXTRACTOR = 'cvat_rest_api_task_images'
|
||||
_TEMPLATES_DIR = osp.join(osp.dirname(__file__), 'export_templates')
|
||||
|
||||
@staticmethod
|
||||
def _save_image_info(save_dir, task_data):
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
config = {
|
||||
'server_url': task_data._host or 'localhost',
|
||||
'task_id': task_data.db_task.id,
|
||||
}
|
||||
|
||||
images = []
|
||||
images_meta = { 'images': images, }
|
||||
for frame_id, frame in task_data.frame_info.items():
|
||||
images.append({
|
||||
'id': frame_id,
|
||||
'name': osp.basename(frame['path']),
|
||||
'width': frame['width'],
|
||||
'height': frame['height'],
|
||||
})
|
||||
|
||||
with open(osp.join(save_dir, 'config.json'), 'w') as config_file:
|
||||
json.dump(config, config_file)
|
||||
with open(osp.join(save_dir, 'images_meta.json'), 'w') as images_file:
|
||||
json.dump(images_meta, images_file)
|
||||
|
||||
def _export(self, task_data, save_dir, save_images=False):
|
||||
dataset = CvatTaskDataExtractor(task_data, include_images=save_images)
|
||||
converter = dm_env.make_converter('datumaro_project',
|
||||
save_images=save_images,
|
||||
config={ 'project_name': task_data.db_task.name, }
|
||||
)
|
||||
converter(dataset, save_dir=save_dir)
|
||||
|
||||
project = Project.load(save_dir)
|
||||
target_dir = project.config.project_dir
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
shutil.copyfile(
|
||||
osp.join(self._TEMPLATES_DIR, 'README.md'),
|
||||
osp.join(target_dir, 'README.md'))
|
||||
|
||||
if not save_images:
|
||||
# add remote links to images
|
||||
source_name = 'task_%s_images' % task_data.db_task.id
|
||||
project.add_source(source_name, {
|
||||
'format': self._REMOTE_IMAGES_EXTRACTOR,
|
||||
})
|
||||
self._save_image_info(
|
||||
osp.join(save_dir, project.local_source_dir(source_name)),
|
||||
task_data)
|
||||
project.save()
|
||||
|
||||
templates_dir = osp.join(self._TEMPLATES_DIR, 'plugins')
|
||||
target_dir = osp.join(project.config.project_dir,
|
||||
project.config.env_dir, project.config.plugins_dir)
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
shutil.copyfile(
|
||||
osp.join(templates_dir, self._REMOTE_IMAGES_EXTRACTOR + '.py'),
|
||||
osp.join(target_dir, self._REMOTE_IMAGES_EXTRACTOR + '.py'))
|
||||
|
||||
# Make Datumaro and CVAT CLI modules available to the user
|
||||
shutil.copytree(DATUMARO_PATH, osp.join(save_dir, 'datumaro'),
|
||||
ignore=lambda src, names: ['__pycache__'] + [
|
||||
n for n in names
|
||||
if sum([int(n.endswith(ext)) for ext in
|
||||
['.pyx', '.pyo', '.pyd', '.pyc']])
|
||||
])
|
||||
|
||||
cvat_utils_dst_dir = osp.join(save_dir, 'cvat', 'utils')
|
||||
os.makedirs(cvat_utils_dst_dir)
|
||||
shutil.copytree(osp.join(BASE_DIR, 'utils', 'cli'),
|
||||
osp.join(cvat_utils_dst_dir, 'cli'))
|
||||
|
||||
def __call__(self, dst_file, task_data, save_images=False):
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
self._export(task_data, save_dir=temp_dir, save_images=save_images)
|
||||
make_zip_archive(temp_dir, dst_file)
|
||||
@ -0,0 +1,87 @@
|
||||
|
||||
# Copyright (C) 2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from datumaro.components.project import Environment
|
||||
|
||||
|
||||
dm_env = Environment()
|
||||
|
||||
class _Format:
|
||||
NAME = ''
|
||||
EXT = ''
|
||||
VERSION = ''
|
||||
DISPLAY_NAME = '{NAME} {VERSION}'
|
||||
|
||||
class Exporter(_Format):
|
||||
def __call__(self, dst_file, task_data, **options):
|
||||
raise NotImplementedError()
|
||||
|
||||
class Importer(_Format):
|
||||
def __call__(self, src_file, task_data, **options):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _wrap_format(f_or_cls, klass, name, version, ext, display_name):
|
||||
import inspect
|
||||
assert inspect.isclass(f_or_cls) or inspect.isfunction(f_or_cls)
|
||||
if inspect.isclass(f_or_cls):
|
||||
assert hasattr(f_or_cls, '__call__')
|
||||
target = f_or_cls
|
||||
elif inspect.isfunction(f_or_cls):
|
||||
class wrapper(klass):
|
||||
# pylint: disable=arguments-differ
|
||||
def __call__(self, *args, **kwargs):
|
||||
f_or_cls(*args, **kwargs)
|
||||
|
||||
wrapper.__name__ = f_or_cls.__name__
|
||||
wrapper.__module__ = f_or_cls.__module__
|
||||
target = wrapper
|
||||
|
||||
target.NAME = name or klass.NAME or f_or_cls.__name__
|
||||
target.VERSION = version or klass.VERSION
|
||||
target.EXT = ext or klass.EXT
|
||||
target.DISPLAY_NAME = (display_name or klass.DISPLAY_NAME).format(
|
||||
NAME=name, VERSION=version, EXT=ext)
|
||||
assert all([target.NAME, target.VERSION, target.EXT, target.DISPLAY_NAME])
|
||||
return target
|
||||
|
||||
EXPORT_FORMATS = {}
|
||||
def exporter(name, version, ext, display_name=None):
|
||||
assert name not in EXPORT_FORMATS, "Export format '%s' already registered" % name
|
||||
def wrap_with_params(f_or_cls):
|
||||
t = _wrap_format(f_or_cls, Exporter,
|
||||
name=name, ext=ext, version=version, display_name=display_name)
|
||||
key = t.DISPLAY_NAME
|
||||
assert key not in EXPORT_FORMATS, "Export format '%s' already registered" % name
|
||||
EXPORT_FORMATS[key] = t
|
||||
return t
|
||||
return wrap_with_params
|
||||
|
||||
IMPORT_FORMATS = {}
|
||||
def importer(name, version, ext, display_name=None):
|
||||
def wrap_with_params(f_or_cls):
|
||||
t = _wrap_format(f_or_cls, Importer,
|
||||
name=name, ext=ext, version=version, display_name=display_name)
|
||||
key = t.DISPLAY_NAME
|
||||
assert key not in IMPORT_FORMATS, "Import format '%s' already registered" % name
|
||||
IMPORT_FORMATS[key] = t
|
||||
return t
|
||||
return wrap_with_params
|
||||
|
||||
def make_importer(name):
|
||||
return IMPORT_FORMATS[name]()
|
||||
|
||||
def make_exporter(name):
|
||||
return EXPORT_FORMATS[name]()
|
||||
|
||||
# pylint: disable=unused-import
|
||||
import cvat.apps.dataset_manager.formats.coco
|
||||
import cvat.apps.dataset_manager.formats.cvat
|
||||
import cvat.apps.dataset_manager.formats.datumaro
|
||||
import cvat.apps.dataset_manager.formats.labelme
|
||||
import cvat.apps.dataset_manager.formats.mask
|
||||
import cvat.apps.dataset_manager.formats.mot
|
||||
import cvat.apps.dataset_manager.formats.pascal_voc
|
||||
import cvat.apps.dataset_manager.formats.tfrecord
|
||||
import cvat.apps.dataset_manager.formats.yolo
|
||||
@ -0,0 +1,15 @@
|
||||
# Copyright (C) 2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class DatasetFormatSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=64, source='DISPLAY_NAME')
|
||||
ext = serializers.CharField(max_length=64, source='EXT')
|
||||
version = serializers.CharField(max_length=64, source='VERSION')
|
||||
|
||||
class DatasetFormatsSerializer(serializers.Serializer):
|
||||
importers = DatasetFormatSerializer(many=True)
|
||||
exporters = DatasetFormatSerializer(many=True)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,107 @@
|
||||
# Copyright (C) 2019-2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import os.path as osp
|
||||
import tempfile
|
||||
from datetime import timedelta
|
||||
|
||||
import django_rq
|
||||
from django.utils import timezone
|
||||
|
||||
import cvat.apps.dataset_manager.task as task
|
||||
from cvat.apps.engine.log import slogger
|
||||
from cvat.apps.engine.models import Task
|
||||
from datumaro.cli.util import make_file_name
|
||||
from datumaro.util import to_snake_case
|
||||
|
||||
from .formats.registry import EXPORT_FORMATS, IMPORT_FORMATS
|
||||
from .util import current_function_name
|
||||
|
||||
|
||||
_MODULE_NAME = __package__ + '.' + osp.splitext(osp.basename(__file__))[0]
|
||||
def log_exception(logger=None, exc_info=True):
|
||||
if logger is None:
|
||||
logger = slogger
|
||||
logger.exception("[%s @ %s]: exception occurred" % \
|
||||
(_MODULE_NAME, current_function_name(2)),
|
||||
exc_info=exc_info)
|
||||
|
||||
|
||||
def get_export_cache_dir(db_task):
|
||||
return osp.join(db_task.get_task_dirname(), 'export_cache')
|
||||
|
||||
DEFAULT_CACHE_TTL = timedelta(hours=10)
|
||||
CACHE_TTL = DEFAULT_CACHE_TTL
|
||||
|
||||
|
||||
def export_task(task_id, dst_format, server_url=None, save_images=False):
|
||||
try:
|
||||
db_task = Task.objects.get(pk=task_id)
|
||||
|
||||
cache_dir = get_export_cache_dir(db_task)
|
||||
|
||||
exporter = EXPORT_FORMATS[dst_format]
|
||||
output_base = '%s_%s' % ('dataset' if save_images else 'task',
|
||||
make_file_name(to_snake_case(dst_format)))
|
||||
output_path = '%s.%s' % (output_base, exporter.EXT)
|
||||
output_path = osp.join(cache_dir, output_path)
|
||||
|
||||
task_time = timezone.localtime(db_task.updated_date).timestamp()
|
||||
if not (osp.exists(output_path) and \
|
||||
task_time <= osp.getmtime(output_path)):
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(dir=cache_dir) as temp_dir:
|
||||
temp_file = osp.join(temp_dir, 'result')
|
||||
task.export_task(task_id, temp_file, dst_format,
|
||||
server_url=server_url, save_images=save_images)
|
||||
os.replace(temp_file, output_path)
|
||||
|
||||
archive_ctime = osp.getctime(output_path)
|
||||
scheduler = django_rq.get_scheduler()
|
||||
cleaning_job = scheduler.enqueue_in(time_delta=CACHE_TTL,
|
||||
func=clear_export_cache,
|
||||
task_id=task_id,
|
||||
file_path=output_path, file_ctime=archive_ctime)
|
||||
slogger.task[task_id].info(
|
||||
"The task '{}' is exported as '{}' at '{}' "
|
||||
"and available for downloading for the next {}. "
|
||||
"Export cache cleaning job is enqueued, id '{}'".format(
|
||||
db_task.name, dst_format, output_path, CACHE_TTL,
|
||||
cleaning_job.id))
|
||||
|
||||
return output_path
|
||||
except Exception:
|
||||
log_exception(slogger.task[task_id])
|
||||
raise
|
||||
|
||||
def export_task_as_dataset(task_id, dst_format=None, server_url=None):
|
||||
return export_task(task_id, dst_format, server_url=server_url, save_images=True)
|
||||
|
||||
def export_task_annotations(task_id, dst_format=None, server_url=None):
|
||||
return export_task(task_id, dst_format, server_url=server_url, save_images=False)
|
||||
|
||||
def clear_export_cache(task_id, file_path, file_ctime):
|
||||
try:
|
||||
if osp.exists(file_path) and osp.getctime(file_path) == file_ctime:
|
||||
os.remove(file_path)
|
||||
slogger.task[task_id].info(
|
||||
"Export cache file '{}' successfully removed" \
|
||||
.format(file_path))
|
||||
except Exception:
|
||||
log_exception(slogger.task[task_id])
|
||||
raise
|
||||
|
||||
|
||||
def get_export_formats():
|
||||
return list(EXPORT_FORMATS.values())
|
||||
|
||||
def get_import_formats():
|
||||
return list(IMPORT_FORMATS.values())
|
||||
|
||||
def get_all_formats():
|
||||
return {
|
||||
'importers': get_import_formats(),
|
||||
'exporters': get_export_formats(),
|
||||
}
|
||||
@ -1,764 +0,0 @@
|
||||
# Copyright (C) 2018 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
from collections import OrderedDict
|
||||
from django.utils import timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from cvat.apps.profiler import silk_profile
|
||||
from cvat.apps.engine.plugins import plugin_decorator
|
||||
from cvat.apps.annotation.annotation import AnnotationIR, Annotation
|
||||
from cvat.apps.engine.utils import execute_python_code, import_modules
|
||||
|
||||
from . import models
|
||||
from .data_manager import DataManager
|
||||
from .log import slogger
|
||||
from . import serializers
|
||||
|
||||
"""dot.notation access to dictionary attributes"""
|
||||
class dotdict(OrderedDict):
|
||||
__getattr__ = OrderedDict.get
|
||||
__setattr__ = OrderedDict.__setitem__
|
||||
__delattr__ = OrderedDict.__delitem__
|
||||
__eq__ = lambda self, other: self.id == other.id
|
||||
__hash__ = lambda self: self.id
|
||||
|
||||
class PatchAction(str, Enum):
|
||||
CREATE = "create"
|
||||
UPDATE = "update"
|
||||
DELETE = "delete"
|
||||
|
||||
@classmethod
|
||||
def values(cls):
|
||||
return [item.value for item in cls]
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@silk_profile(name="GET job data")
|
||||
@transaction.atomic
|
||||
def get_job_data(pk, user):
|
||||
annotation = JobAnnotation(pk, user)
|
||||
annotation.init_from_db()
|
||||
|
||||
return annotation.data
|
||||
|
||||
@silk_profile(name="POST job data")
|
||||
@transaction.atomic
|
||||
def put_job_data(pk, user, data):
|
||||
annotation = JobAnnotation(pk, user)
|
||||
annotation.put(data)
|
||||
|
||||
return annotation.data
|
||||
|
||||
@silk_profile(name="UPDATE job data")
|
||||
@plugin_decorator
|
||||
@transaction.atomic
|
||||
def patch_job_data(pk, user, data, action):
|
||||
annotation = JobAnnotation(pk, user)
|
||||
if action == PatchAction.CREATE:
|
||||
annotation.create(data)
|
||||
elif action == PatchAction.UPDATE:
|
||||
annotation.update(data)
|
||||
elif action == PatchAction.DELETE:
|
||||
annotation.delete(data)
|
||||
|
||||
return annotation.data
|
||||
|
||||
@silk_profile(name="DELETE job data")
|
||||
@transaction.atomic
|
||||
def delete_job_data(pk, user):
|
||||
annotation = JobAnnotation(pk, user)
|
||||
annotation.delete()
|
||||
|
||||
@silk_profile(name="GET task data")
|
||||
@transaction.atomic
|
||||
def get_task_data(pk, user):
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
annotation.init_from_db()
|
||||
|
||||
return annotation.data
|
||||
|
||||
@silk_profile(name="POST task data")
|
||||
@transaction.atomic
|
||||
def put_task_data(pk, user, data):
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
annotation.put(data)
|
||||
|
||||
return annotation.data
|
||||
|
||||
@silk_profile(name="UPDATE task data")
|
||||
@transaction.atomic
|
||||
def patch_task_data(pk, user, data, action):
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
if action == PatchAction.CREATE:
|
||||
annotation.create(data)
|
||||
elif action == PatchAction.UPDATE:
|
||||
annotation.update(data)
|
||||
elif action == PatchAction.DELETE:
|
||||
annotation.delete(data)
|
||||
|
||||
return annotation.data
|
||||
|
||||
@transaction.atomic
|
||||
def load_task_data(pk, user, filename, loader):
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
annotation.upload(filename, loader)
|
||||
|
||||
@transaction.atomic
|
||||
def load_job_data(pk, user, filename, loader):
|
||||
annotation = JobAnnotation(pk, user)
|
||||
annotation.upload(filename, loader)
|
||||
|
||||
@silk_profile(name="DELETE task data")
|
||||
@transaction.atomic
|
||||
def delete_task_data(pk, user):
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
annotation.delete()
|
||||
|
||||
def dump_task_data(pk, user, filename, dumper, scheme, host):
|
||||
# For big tasks dump function may run for a long time and
|
||||
# we dont need to acquire lock after _AnnotationForTask instance
|
||||
# has been initialized from DB.
|
||||
# But there is the bug with corrupted dump file in case 2 or more dump request received at the same time.
|
||||
# https://github.com/opencv/cvat/issues/217
|
||||
with transaction.atomic():
|
||||
annotation = TaskAnnotation(pk, user)
|
||||
annotation.init_from_db()
|
||||
|
||||
annotation.dump(filename, dumper, scheme, host)
|
||||
|
||||
def bulk_create(db_model, objects, flt_param):
|
||||
if objects:
|
||||
if flt_param:
|
||||
if 'postgresql' in settings.DATABASES["default"]["ENGINE"]:
|
||||
return db_model.objects.bulk_create(objects)
|
||||
else:
|
||||
ids = list(db_model.objects.filter(**flt_param).values_list('id', flat=True))
|
||||
db_model.objects.bulk_create(objects)
|
||||
|
||||
return list(db_model.objects.exclude(id__in=ids).filter(**flt_param))
|
||||
else:
|
||||
return db_model.objects.bulk_create(objects)
|
||||
|
||||
return []
|
||||
|
||||
def _merge_table_rows(rows, keys_for_merge, field_id):
|
||||
# It is necessary to keep a stable order of original rows
|
||||
# (e.g. for tracked boxes). Otherwise prev_box.frame can be bigger
|
||||
# than next_box.frame.
|
||||
merged_rows = OrderedDict()
|
||||
|
||||
# Group all rows by field_id. In grouped rows replace fields in
|
||||
# accordance with keys_for_merge structure.
|
||||
for row in rows:
|
||||
row_id = row[field_id]
|
||||
if not row_id in merged_rows:
|
||||
merged_rows[row_id] = dotdict(row)
|
||||
for key in keys_for_merge:
|
||||
merged_rows[row_id][key] = []
|
||||
|
||||
for key in keys_for_merge:
|
||||
item = dotdict({v.split('__', 1)[-1]:row[v] for v in keys_for_merge[key]})
|
||||
if item.id is not None:
|
||||
merged_rows[row_id][key].append(item)
|
||||
|
||||
# Remove redundant keys from final objects
|
||||
redundant_keys = [item for values in keys_for_merge.values() for item in values]
|
||||
for i in merged_rows:
|
||||
for j in redundant_keys:
|
||||
del merged_rows[i][j]
|
||||
|
||||
return list(merged_rows.values())
|
||||
|
||||
class JobAnnotation:
|
||||
def __init__(self, pk, user):
|
||||
self.user = user
|
||||
self.db_job = models.Job.objects.select_related('segment__task') \
|
||||
.select_for_update().get(id=pk)
|
||||
|
||||
db_segment = self.db_job.segment
|
||||
self.start_frame = db_segment.start_frame
|
||||
self.stop_frame = db_segment.stop_frame
|
||||
self.ir_data = AnnotationIR()
|
||||
|
||||
# pylint: disable=bad-continuation
|
||||
self.logger = slogger.job[self.db_job.id]
|
||||
self.db_labels = {db_label.id:db_label
|
||||
for db_label in db_segment.task.label_set.all()}
|
||||
|
||||
self.db_attributes = {}
|
||||
for db_label in self.db_labels.values():
|
||||
self.db_attributes[db_label.id] = {
|
||||
"mutable": OrderedDict(),
|
||||
"immutable": OrderedDict(),
|
||||
"all": OrderedDict(),
|
||||
}
|
||||
for db_attr in db_label.attributespec_set.all():
|
||||
default_value = dotdict([
|
||||
('spec_id', db_attr.id),
|
||||
('value', db_attr.default_value),
|
||||
])
|
||||
if db_attr.mutable:
|
||||
self.db_attributes[db_label.id]["mutable"][db_attr.id] = default_value
|
||||
else:
|
||||
self.db_attributes[db_label.id]["immutable"][db_attr.id] = default_value
|
||||
|
||||
self.db_attributes[db_label.id]["all"][db_attr.id] = default_value
|
||||
|
||||
def reset(self):
|
||||
self.ir_data.reset()
|
||||
|
||||
def _save_tracks_to_db(self, tracks):
|
||||
db_tracks = []
|
||||
db_track_attrvals = []
|
||||
db_shapes = []
|
||||
db_shape_attrvals = []
|
||||
|
||||
for track in tracks:
|
||||
track_attributes = track.pop("attributes", [])
|
||||
shapes = track.pop("shapes")
|
||||
db_track = models.LabeledTrack(job=self.db_job, **track)
|
||||
if db_track.label_id not in self.db_labels:
|
||||
raise AttributeError("label_id `{}` is invalid".format(db_track.label_id))
|
||||
|
||||
for attr in track_attributes:
|
||||
db_attrval = models.LabeledTrackAttributeVal(**attr)
|
||||
if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["immutable"]:
|
||||
raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id))
|
||||
db_attrval.track_id = len(db_tracks)
|
||||
db_track_attrvals.append(db_attrval)
|
||||
|
||||
for shape in shapes:
|
||||
shape_attributes = shape.pop("attributes", [])
|
||||
# FIXME: need to clamp points (be sure that all of them inside the image)
|
||||
# Should we check here or implement a validator?
|
||||
db_shape = models.TrackedShape(**shape)
|
||||
db_shape.track_id = len(db_tracks)
|
||||
|
||||
for attr in shape_attributes:
|
||||
db_attrval = models.TrackedShapeAttributeVal(**attr)
|
||||
if db_attrval.spec_id not in self.db_attributes[db_track.label_id]["mutable"]:
|
||||
raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id))
|
||||
db_attrval.shape_id = len(db_shapes)
|
||||
db_shape_attrvals.append(db_attrval)
|
||||
|
||||
db_shapes.append(db_shape)
|
||||
shape["attributes"] = shape_attributes
|
||||
|
||||
db_tracks.append(db_track)
|
||||
track["attributes"] = track_attributes
|
||||
track["shapes"] = shapes
|
||||
|
||||
db_tracks = bulk_create(
|
||||
db_model=models.LabeledTrack,
|
||||
objects=db_tracks,
|
||||
flt_param={"job_id": self.db_job.id}
|
||||
)
|
||||
|
||||
for db_attrval in db_track_attrvals:
|
||||
db_attrval.track_id = db_tracks[db_attrval.track_id].id
|
||||
bulk_create(
|
||||
db_model=models.LabeledTrackAttributeVal,
|
||||
objects=db_track_attrvals,
|
||||
flt_param={}
|
||||
)
|
||||
|
||||
for db_shape in db_shapes:
|
||||
db_shape.track_id = db_tracks[db_shape.track_id].id
|
||||
|
||||
db_shapes = bulk_create(
|
||||
db_model=models.TrackedShape,
|
||||
objects=db_shapes,
|
||||
flt_param={"track__job_id": self.db_job.id}
|
||||
)
|
||||
|
||||
for db_attrval in db_shape_attrvals:
|
||||
db_attrval.shape_id = db_shapes[db_attrval.shape_id].id
|
||||
|
||||
bulk_create(
|
||||
db_model=models.TrackedShapeAttributeVal,
|
||||
objects=db_shape_attrvals,
|
||||
flt_param={}
|
||||
)
|
||||
|
||||
shape_idx = 0
|
||||
for track, db_track in zip(tracks, db_tracks):
|
||||
track["id"] = db_track.id
|
||||
for shape in track["shapes"]:
|
||||
shape["id"] = db_shapes[shape_idx].id
|
||||
shape_idx += 1
|
||||
|
||||
self.ir_data.tracks = tracks
|
||||
|
||||
def _save_shapes_to_db(self, shapes):
|
||||
db_shapes = []
|
||||
db_attrvals = []
|
||||
|
||||
for shape in shapes:
|
||||
attributes = shape.pop("attributes", [])
|
||||
# FIXME: need to clamp points (be sure that all of them inside the image)
|
||||
# Should we check here or implement a validator?
|
||||
db_shape = models.LabeledShape(job=self.db_job, **shape)
|
||||
if db_shape.label_id not in self.db_labels:
|
||||
raise AttributeError("label_id `{}` is invalid".format(db_shape.label_id))
|
||||
|
||||
for attr in attributes:
|
||||
db_attrval = models.LabeledShapeAttributeVal(**attr)
|
||||
if db_attrval.spec_id not in self.db_attributes[db_shape.label_id]["all"]:
|
||||
raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id))
|
||||
|
||||
db_attrval.shape_id = len(db_shapes)
|
||||
db_attrvals.append(db_attrval)
|
||||
|
||||
db_shapes.append(db_shape)
|
||||
shape["attributes"] = attributes
|
||||
|
||||
db_shapes = bulk_create(
|
||||
db_model=models.LabeledShape,
|
||||
objects=db_shapes,
|
||||
flt_param={"job_id": self.db_job.id}
|
||||
)
|
||||
|
||||
for db_attrval in db_attrvals:
|
||||
db_attrval.shape_id = db_shapes[db_attrval.shape_id].id
|
||||
|
||||
bulk_create(
|
||||
db_model=models.LabeledShapeAttributeVal,
|
||||
objects=db_attrvals,
|
||||
flt_param={}
|
||||
)
|
||||
|
||||
for shape, db_shape in zip(shapes, db_shapes):
|
||||
shape["id"] = db_shape.id
|
||||
|
||||
self.ir_data.shapes = shapes
|
||||
|
||||
def _save_tags_to_db(self, tags):
|
||||
db_tags = []
|
||||
db_attrvals = []
|
||||
|
||||
for tag in tags:
|
||||
attributes = tag.pop("attributes", [])
|
||||
db_tag = models.LabeledImage(job=self.db_job, **tag)
|
||||
if db_tag.label_id not in self.db_labels:
|
||||
raise AttributeError("label_id `{}` is invalid".format(db_tag.label_id))
|
||||
|
||||
for attr in attributes:
|
||||
db_attrval = models.LabeledImageAttributeVal(**attr)
|
||||
if db_attrval.spec_id not in self.db_attributes[db_tag.label_id]["all"]:
|
||||
raise AttributeError("spec_id `{}` is invalid".format(db_attrval.spec_id))
|
||||
db_attrval.tag_id = len(db_tags)
|
||||
db_attrvals.append(db_attrval)
|
||||
|
||||
db_tags.append(db_tag)
|
||||
tag["attributes"] = attributes
|
||||
|
||||
db_tags = bulk_create(
|
||||
db_model=models.LabeledImage,
|
||||
objects=db_tags,
|
||||
flt_param={"job_id": self.db_job.id}
|
||||
)
|
||||
|
||||
for db_attrval in db_attrvals:
|
||||
db_attrval.image_id = db_tags[db_attrval.tag_id].id
|
||||
|
||||
bulk_create(
|
||||
db_model=models.LabeledImageAttributeVal,
|
||||
objects=db_attrvals,
|
||||
flt_param={}
|
||||
)
|
||||
|
||||
for tag, db_tag in zip(tags, db_tags):
|
||||
tag["id"] = db_tag.id
|
||||
|
||||
self.ir_data.tags = tags
|
||||
|
||||
def _commit(self):
|
||||
db_prev_commit = self.db_job.commits.last()
|
||||
db_curr_commit = models.JobCommit()
|
||||
if db_prev_commit:
|
||||
db_curr_commit.version = db_prev_commit.version + 1
|
||||
else:
|
||||
db_curr_commit.version = 1
|
||||
db_curr_commit.job = self.db_job
|
||||
db_curr_commit.message = "Changes: tags - {}; shapes - {}; tracks - {}".format(
|
||||
len(self.ir_data.tags), len(self.ir_data.shapes), len(self.ir_data.tracks))
|
||||
db_curr_commit.save()
|
||||
self.ir_data.version = db_curr_commit.version
|
||||
|
||||
def _set_updated_date(self):
|
||||
db_task = self.db_job.segment.task
|
||||
db_task.updated_date = timezone.now()
|
||||
db_task.save()
|
||||
|
||||
def _save_to_db(self, data):
|
||||
self.reset()
|
||||
self._save_tags_to_db(data["tags"])
|
||||
self._save_shapes_to_db(data["shapes"])
|
||||
self._save_tracks_to_db(data["tracks"])
|
||||
|
||||
return self.ir_data.tags or self.ir_data.shapes or self.ir_data.tracks
|
||||
|
||||
def _create(self, data):
|
||||
if self._save_to_db(data):
|
||||
self._set_updated_date()
|
||||
self.db_job.save()
|
||||
|
||||
def create(self, data):
|
||||
self._create(data)
|
||||
self._commit()
|
||||
|
||||
def put(self, data):
|
||||
self._delete()
|
||||
self._create(data)
|
||||
self._commit()
|
||||
|
||||
def update(self, data):
|
||||
self._delete(data)
|
||||
self._create(data)
|
||||
self._commit()
|
||||
|
||||
def _delete(self, data=None):
|
||||
deleted_shapes = 0
|
||||
if data is None:
|
||||
deleted_shapes += self.db_job.labeledimage_set.all().delete()[0]
|
||||
deleted_shapes += self.db_job.labeledshape_set.all().delete()[0]
|
||||
deleted_shapes += self.db_job.labeledtrack_set.all().delete()[0]
|
||||
else:
|
||||
labeledimage_ids = [image["id"] for image in data["tags"]]
|
||||
labeledshape_ids = [shape["id"] for shape in data["shapes"]]
|
||||
labeledtrack_ids = [track["id"] for track in data["tracks"]]
|
||||
labeledimage_set = self.db_job.labeledimage_set
|
||||
labeledimage_set = labeledimage_set.filter(pk__in=labeledimage_ids)
|
||||
labeledshape_set = self.db_job.labeledshape_set
|
||||
labeledshape_set = labeledshape_set.filter(pk__in=labeledshape_ids)
|
||||
labeledtrack_set = self.db_job.labeledtrack_set
|
||||
labeledtrack_set = labeledtrack_set.filter(pk__in=labeledtrack_ids)
|
||||
|
||||
# It is not important for us that data had some "invalid" objects
|
||||
# which were skipped (not acutally deleted). The main idea is to
|
||||
# say that all requested objects are absent in DB after the method.
|
||||
self.ir_data.tags = data['tags']
|
||||
self.ir_data.shapes = data['shapes']
|
||||
self.ir_data.tracks = data['tracks']
|
||||
|
||||
deleted_shapes += labeledimage_set.delete()[0]
|
||||
deleted_shapes += labeledshape_set.delete()[0]
|
||||
deleted_shapes += labeledtrack_set.delete()[0]
|
||||
|
||||
if deleted_shapes:
|
||||
self._set_updated_date()
|
||||
|
||||
def delete(self, data=None):
|
||||
self._delete(data)
|
||||
self._commit()
|
||||
|
||||
@staticmethod
|
||||
def _extend_attributes(attributeval_set, default_attribute_values):
|
||||
shape_attribute_specs_set = set(attr.spec_id for attr in attributeval_set)
|
||||
for db_attr in default_attribute_values:
|
||||
if db_attr.spec_id not in shape_attribute_specs_set:
|
||||
attributeval_set.append(dotdict([
|
||||
('spec_id', db_attr.spec_id),
|
||||
('value', db_attr.value),
|
||||
]))
|
||||
|
||||
def _init_tags_from_db(self):
|
||||
db_tags = self.db_job.labeledimage_set.prefetch_related(
|
||||
"label",
|
||||
"labeledimageattributeval_set"
|
||||
).values(
|
||||
'id',
|
||||
'frame',
|
||||
'label_id',
|
||||
'group',
|
||||
'labeledimageattributeval__spec_id',
|
||||
'labeledimageattributeval__value',
|
||||
'labeledimageattributeval__id',
|
||||
).order_by('frame')
|
||||
|
||||
db_tags = _merge_table_rows(
|
||||
rows=db_tags,
|
||||
keys_for_merge={
|
||||
"labeledimageattributeval_set": [
|
||||
'labeledimageattributeval__spec_id',
|
||||
'labeledimageattributeval__value',
|
||||
'labeledimageattributeval__id',
|
||||
],
|
||||
},
|
||||
field_id='id',
|
||||
)
|
||||
|
||||
for db_tag in db_tags:
|
||||
self._extend_attributes(db_tag.labeledimageattributeval_set,
|
||||
self.db_attributes[db_tag.label_id]["all"].values())
|
||||
|
||||
serializer = serializers.LabeledImageSerializer(db_tags, many=True)
|
||||
self.ir_data.tags = serializer.data
|
||||
|
||||
def _init_shapes_from_db(self):
|
||||
db_shapes = self.db_job.labeledshape_set.prefetch_related(
|
||||
"label",
|
||||
"labeledshapeattributeval_set"
|
||||
).values(
|
||||
'id',
|
||||
'label_id',
|
||||
'type',
|
||||
'frame',
|
||||
'group',
|
||||
'occluded',
|
||||
'z_order',
|
||||
'points',
|
||||
'labeledshapeattributeval__spec_id',
|
||||
'labeledshapeattributeval__value',
|
||||
'labeledshapeattributeval__id',
|
||||
).order_by('frame')
|
||||
|
||||
db_shapes = _merge_table_rows(
|
||||
rows=db_shapes,
|
||||
keys_for_merge={
|
||||
'labeledshapeattributeval_set': [
|
||||
'labeledshapeattributeval__spec_id',
|
||||
'labeledshapeattributeval__value',
|
||||
'labeledshapeattributeval__id',
|
||||
],
|
||||
},
|
||||
field_id='id',
|
||||
)
|
||||
for db_shape in db_shapes:
|
||||
self._extend_attributes(db_shape.labeledshapeattributeval_set,
|
||||
self.db_attributes[db_shape.label_id]["all"].values())
|
||||
|
||||
serializer = serializers.LabeledShapeSerializer(db_shapes, many=True)
|
||||
self.ir_data.shapes = serializer.data
|
||||
|
||||
def _init_tracks_from_db(self):
|
||||
db_tracks = self.db_job.labeledtrack_set.prefetch_related(
|
||||
"label",
|
||||
"labeledtrackattributeval_set",
|
||||
"trackedshape_set__trackedshapeattributeval_set"
|
||||
).values(
|
||||
"id",
|
||||
"frame",
|
||||
"label_id",
|
||||
"group",
|
||||
"labeledtrackattributeval__spec_id",
|
||||
"labeledtrackattributeval__value",
|
||||
"labeledtrackattributeval__id",
|
||||
"trackedshape__type",
|
||||
"trackedshape__occluded",
|
||||
"trackedshape__z_order",
|
||||
"trackedshape__points",
|
||||
"trackedshape__id",
|
||||
"trackedshape__frame",
|
||||
"trackedshape__outside",
|
||||
"trackedshape__trackedshapeattributeval__spec_id",
|
||||
"trackedshape__trackedshapeattributeval__value",
|
||||
"trackedshape__trackedshapeattributeval__id",
|
||||
).order_by('id', 'trackedshape__frame')
|
||||
|
||||
db_tracks = _merge_table_rows(
|
||||
rows=db_tracks,
|
||||
keys_for_merge={
|
||||
"labeledtrackattributeval_set": [
|
||||
"labeledtrackattributeval__spec_id",
|
||||
"labeledtrackattributeval__value",
|
||||
"labeledtrackattributeval__id",
|
||||
],
|
||||
"trackedshape_set":[
|
||||
"trackedshape__type",
|
||||
"trackedshape__occluded",
|
||||
"trackedshape__z_order",
|
||||
"trackedshape__points",
|
||||
"trackedshape__id",
|
||||
"trackedshape__frame",
|
||||
"trackedshape__outside",
|
||||
"trackedshape__trackedshapeattributeval__spec_id",
|
||||
"trackedshape__trackedshapeattributeval__value",
|
||||
"trackedshape__trackedshapeattributeval__id",
|
||||
],
|
||||
},
|
||||
field_id="id",
|
||||
)
|
||||
|
||||
for db_track in db_tracks:
|
||||
db_track["trackedshape_set"] = _merge_table_rows(db_track["trackedshape_set"], {
|
||||
'trackedshapeattributeval_set': [
|
||||
'trackedshapeattributeval__value',
|
||||
'trackedshapeattributeval__spec_id',
|
||||
'trackedshapeattributeval__id',
|
||||
]
|
||||
}, 'id')
|
||||
|
||||
# A result table can consist many equal rows for track/shape attributes
|
||||
# We need filter unique attributes manually
|
||||
db_track["labeledtrackattributeval_set"] = list(set(db_track["labeledtrackattributeval_set"]))
|
||||
self._extend_attributes(db_track.labeledtrackattributeval_set,
|
||||
self.db_attributes[db_track.label_id]["immutable"].values())
|
||||
|
||||
default_attribute_values = self.db_attributes[db_track.label_id]["mutable"].values()
|
||||
for db_shape in db_track["trackedshape_set"]:
|
||||
db_shape["trackedshapeattributeval_set"] = list(
|
||||
set(db_shape["trackedshapeattributeval_set"])
|
||||
)
|
||||
# in case of trackedshapes need to interpolate attriute values and extend it
|
||||
# by previous shape attribute values (not default values)
|
||||
self._extend_attributes(db_shape["trackedshapeattributeval_set"], default_attribute_values)
|
||||
default_attribute_values = db_shape["trackedshapeattributeval_set"]
|
||||
|
||||
|
||||
serializer = serializers.LabeledTrackSerializer(db_tracks, many=True)
|
||||
self.ir_data.tracks = serializer.data
|
||||
|
||||
def _init_version_from_db(self):
|
||||
db_commit = self.db_job.commits.last()
|
||||
self.ir_data.version = db_commit.version if db_commit else 0
|
||||
|
||||
def init_from_db(self):
|
||||
self._init_tags_from_db()
|
||||
self._init_shapes_from_db()
|
||||
self._init_tracks_from_db()
|
||||
self._init_version_from_db()
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.ir_data.data
|
||||
|
||||
def upload(self, annotation_file, loader):
|
||||
annotation_importer = Annotation(
|
||||
annotation_ir=self.ir_data,
|
||||
db_task=self.db_job.segment.task,
|
||||
create_callback=self.create,
|
||||
)
|
||||
self.delete()
|
||||
db_format = loader.annotation_format
|
||||
with open(annotation_file, 'rb') as file_object:
|
||||
source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read()
|
||||
global_vars = globals()
|
||||
imports = import_modules(source_code)
|
||||
global_vars.update(imports)
|
||||
|
||||
execute_python_code(source_code, global_vars)
|
||||
|
||||
global_vars["file_object"] = file_object
|
||||
global_vars["annotations"] = annotation_importer
|
||||
|
||||
execute_python_code("{}(file_object, annotations)".format(loader.handler), global_vars)
|
||||
self.create(annotation_importer.data.slice(self.start_frame, self.stop_frame).serialize())
|
||||
|
||||
class TaskAnnotation:
|
||||
def __init__(self, pk, user):
|
||||
self.user = user
|
||||
self.db_task = models.Task.objects.prefetch_related("data__images").get(id=pk)
|
||||
|
||||
# Postgres doesn't guarantee an order by default without explicit order_by
|
||||
self.db_jobs = models.Job.objects.select_related("segment").filter(segment__task_id=pk).order_by('id')
|
||||
self.ir_data = AnnotationIR()
|
||||
|
||||
def reset(self):
|
||||
self.ir_data.reset()
|
||||
|
||||
def _patch_data(self, data, action):
|
||||
_data = data if isinstance(data, AnnotationIR) else AnnotationIR(data)
|
||||
splitted_data = {}
|
||||
jobs = {}
|
||||
for db_job in self.db_jobs:
|
||||
jid = db_job.id
|
||||
start = db_job.segment.start_frame
|
||||
stop = db_job.segment.stop_frame
|
||||
jobs[jid] = { "start": start, "stop": stop }
|
||||
splitted_data[jid] = _data.slice(start, stop)
|
||||
|
||||
for jid, job_data in splitted_data.items():
|
||||
_data = AnnotationIR()
|
||||
if action is None:
|
||||
_data.data = put_job_data(jid, self.user, job_data)
|
||||
else:
|
||||
_data.data = patch_job_data(jid, self.user, job_data, action)
|
||||
if _data.version > self.ir_data.version:
|
||||
self.ir_data.version = _data.version
|
||||
self._merge_data(_data, jobs[jid]["start"], self.db_task.overlap)
|
||||
|
||||
def _merge_data(self, data, start_frame, overlap):
|
||||
data_manager = DataManager(self.ir_data)
|
||||
data_manager.merge(data, start_frame, overlap)
|
||||
|
||||
def put(self, data):
|
||||
self._patch_data(data, None)
|
||||
|
||||
def create(self, data):
|
||||
self._patch_data(data, PatchAction.CREATE)
|
||||
|
||||
def update(self, data):
|
||||
self._patch_data(data, PatchAction.UPDATE)
|
||||
|
||||
def delete(self, data=None):
|
||||
if data:
|
||||
self._patch_data(data, PatchAction.DELETE)
|
||||
else:
|
||||
for db_job in self.db_jobs:
|
||||
delete_job_data(db_job.id, self.user)
|
||||
|
||||
def init_from_db(self):
|
||||
self.reset()
|
||||
|
||||
for db_job in self.db_jobs:
|
||||
annotation = JobAnnotation(db_job.id, self.user)
|
||||
annotation.init_from_db()
|
||||
if annotation.ir_data.version > self.ir_data.version:
|
||||
self.ir_data.version = annotation.ir_data.version
|
||||
db_segment = db_job.segment
|
||||
start_frame = db_segment.start_frame
|
||||
overlap = self.db_task.overlap
|
||||
self._merge_data(annotation.ir_data, start_frame, overlap)
|
||||
|
||||
def dump(self, filename, dumper, scheme, host):
|
||||
anno_exporter = Annotation(
|
||||
annotation_ir=self.ir_data,
|
||||
db_task=self.db_task,
|
||||
scheme=scheme,
|
||||
host=host,
|
||||
)
|
||||
db_format = dumper.annotation_format
|
||||
|
||||
with open(filename, 'wb') as dump_file:
|
||||
source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read()
|
||||
global_vars = globals()
|
||||
imports = import_modules(source_code)
|
||||
global_vars.update(imports)
|
||||
execute_python_code(source_code, global_vars)
|
||||
global_vars["file_object"] = dump_file
|
||||
global_vars["annotations"] = anno_exporter
|
||||
|
||||
execute_python_code("{}(file_object, annotations)".format(dumper.handler), global_vars)
|
||||
|
||||
def upload(self, annotation_file, loader):
|
||||
annotation_importer = Annotation(
|
||||
annotation_ir=AnnotationIR(),
|
||||
db_task=self.db_task,
|
||||
create_callback=self.create,
|
||||
)
|
||||
self.delete()
|
||||
db_format = loader.annotation_format
|
||||
with open(annotation_file, 'rb') as file_object:
|
||||
source_code = open(os.path.join(settings.BASE_DIR, db_format.handler_file.name)).read()
|
||||
global_vars = globals()
|
||||
imports = import_modules(source_code)
|
||||
global_vars.update(imports)
|
||||
execute_python_code(source_code, global_vars)
|
||||
|
||||
global_vars["file_object"] = file_object
|
||||
global_vars["annotations"] = annotation_importer
|
||||
|
||||
execute_python_code("{}(file_object, annotations)".format(loader.handler), global_vars)
|
||||
self.create(annotation_importer.data.serialize())
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.ir_data.data
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue