From 4584b2384b394c18b1c3d313a5458772d2e586b5 Mon Sep 17 00:00:00 2001 From: Anastasia Yasakova Date: Fri, 4 Jun 2021 18:03:49 +0300 Subject: [PATCH] add test for bug with duplicate points in polygon (#3278) --- .../tests/assets/annotations.json | 46 +++++ .../dataset_manager/tests/assets/tasks.json | 57 ++++++ .../tests/test_rest_api_formats.py | 191 ++++++++++++++++++ 3 files changed, 294 insertions(+) create mode 100644 cvat/apps/dataset_manager/tests/assets/annotations.json create mode 100644 cvat/apps/dataset_manager/tests/assets/tasks.json create mode 100644 cvat/apps/dataset_manager/tests/test_rest_api_formats.py diff --git a/cvat/apps/dataset_manager/tests/assets/annotations.json b/cvat/apps/dataset_manager/tests/assets/annotations.json new file mode 100644 index 00000000..cc82dbcd --- /dev/null +++ b/cvat/apps/dataset_manager/tests/assets/annotations.json @@ -0,0 +1,46 @@ +{ + "CVAT for video 1.1 polygon": { + "version": 0, + "tags": [], + "shapes": [], + "tracks": [ + { + "frame": 0, + "label_id": null, + "group": 1, + "source": "manual", + "shapes": [ + { + "type": "polygon", + "occluded": false, + "z_order": 0, + "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], + "frame": 0, + "outside": false, + "attributes": [] + }, + { + "type": "polygon", + "occluded": false, + "z_order": 0, + "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], + "frame": 1, + "outside": true, + "attributes": [] + }, + { + "type": "polygon", + "occluded": false, + "z_order": 0, + "points": [24.62, 13.01, 34.88, 20.03, 18.14, 18.08], + "frame": 2, + "outside": false, + "keyframe": true, + "attributes": [] + } + ], + "attributes": [] + } + ] + } +} diff --git a/cvat/apps/dataset_manager/tests/assets/tasks.json b/cvat/apps/dataset_manager/tests/assets/tasks.json new file mode 100644 index 00000000..2fd5f663 --- /dev/null +++ b/cvat/apps/dataset_manager/tests/assets/tasks.json @@ -0,0 +1,57 @@ +{ + "main": { + "name": "main task", + "overlap": 0, + "segment_size": 100, + "owner_id": 1, + "assignee_id": 2, + "labels": [ + { + "name": "car", + "color": "#2080c0", + "attributes": [ + { + "name": "select_name", + "mutable": false, + "input_type": "select", + "default_value": "bmw", + "values": ["bmw", "mazda", "renault"] + }, + { + "name": "radio_name", + "mutable": false, + "input_type": "radio", + "default_value": "x1", + "values": ["x1", "x2", "x3"] + }, + { + "name": "check_name", + "mutable": true, + "input_type": "checkbox", + "default_value": "false", + "values": ["false"] + }, + { + "name": "text_name", + "mutable": false, + "input_type": "text", + "default_value": "qwerty", + "values": ["qwerty"] + }, + { + "name": "number_name", + "mutable": false, + "input_type": "number", + "default_value": "-4", + "values": ["-4", "4", "1"] + } + ] + }, + { + "name": "person", + "color": "#c06060", + "attributes": [] + } + ] + } +} diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py new file mode 100644 index 00000000..0abbf889 --- /dev/null +++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py @@ -0,0 +1,191 @@ + +# Copyright (C) 2021 Intel Corporation +# +# SPDX-License-Identifier: MIT + +import copy +import json +import os.path as osp +import random +import xml.etree.ElementTree as ET +import zipfile +from io import BytesIO + +from datumaro.util.test_utils import TestDir +from django.contrib.auth.models import Group, User +from PIL import Image +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +path = osp.join(osp.dirname(__file__), 'assets', 'tasks.json') +with open(path) as f: + tasks = json.load(f) + +path = osp.join(osp.dirname(__file__), 'assets', 'annotations.json') +with open(path) as f: + annotations = json.load(f) + +def generate_image_file(filename, size=(100, 50)): + f = BytesIO() + image = Image.new('RGB', size=size) + image.save(f, 'jpeg') + f.name = filename + f.seek(0) + return f + +class ForceLogin: + def __init__(self, user, client): + self.user = user + self.client = client + + def __enter__(self): + if self.user: + self.client.force_login(self.user, + backend='django.contrib.auth.backends.ModelBackend') + + return self + + def __exit__(self, exception_type, exception_value, traceback): + if self.user: + self.client.logout() + +class _DbTestBase(APITestCase): + def setUp(self): + self.client = APIClient() + + @classmethod + def setUpTestData(cls): + cls.create_db_users() + + @classmethod + def create_db_users(cls): + (group_admin, _) = Group.objects.get_or_create(name="admin") + (group_user, _) = Group.objects.get_or_create(name="user") + + user_admin = User.objects.create_superuser(username="admin", email="", + password="admin") + user_admin.groups.add(group_admin) + user_dummy = User.objects.create_user(username="user", password="user") + user_dummy.groups.add(group_user) + + cls.admin = user_admin + cls.user = user_dummy + + def _put_api_v1_task_id_annotations(self, tid, data): + with ForceLogin(self.admin, self.client): + response = self.client.put("/api/v1/tasks/%s/annotations" % tid, + data=data, format="json") + + return response + + @staticmethod + def _generate_task_images(count): # pylint: disable=no-self-use + images = {"client_files[%d]" % i: generate_image_file("image_%d.jpg" % i) for i in range(count)} + images["image_quality"] = 75 + return images + + def _create_task(self, data, image_data): + with ForceLogin(self.user, self.client): + response = self.client.post('/api/v1/tasks', data=data, format="json") + assert response.status_code == status.HTTP_201_CREATED, response.status_code + tid = response.data["id"] + + response = self.client.post("/api/v1/tasks/%s/data" % tid, + data=image_data) + assert response.status_code == status.HTTP_202_ACCEPTED, response.status_code + + response = self.client.get("/api/v1/tasks/%s" % tid) + task = response.data + + return task + + def _get_request_with_data(self, path, data, user): + with ForceLogin(user, self.client): + response = self.client.get(path, data) + return response + + def _create_annotations(self, task, name_ann, key_get_values): + tmp_annotations = copy.deepcopy(annotations[name_ann]) + + # change attributes in all annotations + for item in tmp_annotations: + if item in ["tags", "shapes", "tracks"]: + for index_elem, _ in enumerate(tmp_annotations[item]): + tmp_annotations[item][index_elem]["label_id"] = task["labels"][0]["id"] + + for index_attribute, attribute in enumerate(task["labels"][0]["attributes"]): + spec_id = task["labels"][0]["attributes"][index_attribute]["id"] + + if key_get_values == "random": + if attribute["input_type"] == "number": + start = int(attribute["values"][0]) + stop = int(attribute["values"][1]) + 1 + step = int(attribute["values"][2]) + value = str(random.randrange(start, stop, step)) + else: + value = random.choice(task["labels"][0]["attributes"][index_attribute]["values"]) + elif key_get_values == "default": + value = attribute["default_value"] + + if item == "tracks" and attribute["mutable"]: + for index_shape, _ in enumerate(tmp_annotations[item][index_elem]["shapes"]): + tmp_annotations[item][index_elem]["shapes"][index_shape]["attributes"].append({ + "spec_id": spec_id, + "value": value, + }) + else: + tmp_annotations[item][index_elem]["attributes"].append({ + "spec_id": spec_id, + "value": value, + }) + + response = self._put_api_v1_task_id_annotations(task["id"], tmp_annotations) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def _download_file(self, url, data, user, file_name): + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + response = self._get_request_with_data(url, data, user) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + content = BytesIO(b"".join(response.streaming_content)) + with open(file_name, "wb") as f: + f.write(content.getvalue()) + + def _check_downloaded_file(self, file_name): + if not osp.exists(file_name): + raise FileNotFoundError(f"File '{file_name}' was not downloaded") + + def _generate_url_dump_tasks_annotations(self, task_id): + return f"/api/v1/tasks/{task_id}/annotations" + +class TaskDumpUploadTest(_DbTestBase): + def test_api_v1_check_duplicated_polygon_points(self): + test_name = self._testMethodName + images = self._generate_task_images(10) + task = self._create_task(tasks["main"], images) + task_id = task["id"] + data = { + "format": "CVAT for video 1.1", + "action": "download", + } + annotation_name = "CVAT for video 1.1 polygon" + self._create_annotations(task, annotation_name, "default") + annotation_points = annotations[annotation_name]["tracks"][0]["shapes"][0]['points'] + + with TestDir() as test_dir: + url = self._generate_url_dump_tasks_annotations(task_id) + file_zip_name = osp.join(test_dir, f'{test_name}.zip') + self._download_file(url, data, self.admin, file_zip_name) + self._check_downloaded_file(file_zip_name) + + folder_name = osp.join(test_dir, f'{test_name}') + with zipfile.ZipFile(file_zip_name, 'r') as zip_ref: + zip_ref.extractall(folder_name) + + tree = ET.parse(osp.join(folder_name, 'annotations.xml')) + root = tree.getroot() + for polygon in root.findall("./track[@id='0']/polygon"): + polygon_points = polygon.attrib["points"].replace(",", ";") + polygon_points = [float(p) for p in polygon_points.split(";")] + self.assertEqual(polygon_points, annotation_points)