Repository: comic/grand-challenge.org

Issue:
Add support for point annotations



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/reader_studies/migrations/0009_auto_20200319_0752.py b/app/grandchallenge/reader_studies/migrations/0009_auto_20200319_0752.py
new file mode 100644
--- /dev/null
+++ b/app/grandchallenge/reader_studies/migrations/0009_auto_20200319_0752.py
@@ -0,0 +1,34 @@
+# Generated by Django 3.0.2 on 2020-03-19 07:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("reader_studies", "0008_auto_20200123_0931"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="question",
+            name="answer_type",
+            field=models.CharField(
+                choices=[
+                    ("STXT", "Single line text"),
+                    ("MTXT", "Multi line text"),
+                    ("BOOL", "Bool"),
+                    ("HEAD", "Heading"),
+                    ("2DBB", "2D bounding box"),
+                    ("DIST", "Distance measurement"),
+                    ("MDIS", "Multiple distance measurements"),
+                    ("POIN", "Point"),
+                    ("MPOI", "Multiple points"),
+                    ("POLY", "Polygon"),
+                    ("MPOL", "Multiple polygons"),
+                ],
+                default="STXT",
+                max_length=4,
+            ),
+        ),
+    ]
diff --git a/app/grandchallenge/reader_studies/models.py b/app/grandchallenge/reader_studies/models.py
--- a/app/grandchallenge/reader_studies/models.py
+++ b/app/grandchallenge/reader_studies/models.py
@@ -627,11 +627,6 @@ def delete_reader_study_groups_hook(*_, instance: ReaderStudy, using, **__):
         "2DBB": {
             "type": "object",
             "properties": {
-                "version": {
-                    "type": "object",
-                    "additionalProperties": {"type": "number"},
-                    "required": ["major", "minor"],
-                },
                 "type": {"enum": ["2D bounding box"]},
                 "corners": {
                     "type": "array",
@@ -667,6 +662,49 @@ def delete_reader_study_groups_hook(*_, instance: ReaderStudy, using, **__):
             },
             "required": ["start", "end"],
         },
+        "point-object": {
+            "type": "object",
+            "properties": {
+                "name": {"type": "string"},
+                "point": {
+                    "type": "array",
+                    "items": {"type": "number"},
+                    "minItems": 3,
+                    "maxItems": 3,
+                },
+            },
+            "required": ["point"],
+        },
+        "polygon-object": {
+            "type": "object",
+            "properties": {
+                "name": {"type": "string"},
+                "seed_point": {
+                    "type": "array",
+                    "items": {"type": "number"},
+                    "minItems": 3,
+                    "maxItems": 3,
+                },
+                "path_points": {
+                    "type": "array",
+                    "items": {
+                        "type": "array",
+                        "items": {"type": "number"},
+                        "minItems": 3,
+                        "maxItems": 3,
+                    },
+                },
+                "sub_type": {"type": "string"},
+                "groups": {"type": "array", "items": {"type": "string"}},
+            },
+            "required": [
+                "name",
+                "seed_point",
+                "path_points",
+                "sub_type",
+                "groups",
+            ],
+        },
         "DIST": {
             "type": "object",
             "properties": {
@@ -701,6 +739,77 @@ def delete_reader_study_groups_hook(*_, instance: ReaderStudy, using, **__):
             },
             "required": ["version", "type", "lines"],
         },
+        "POIN": {
+            "type": "object",
+            "properties": {
+                "name": {"type": "string"},
+                "type": {"enum": ["Point"]},
+                "point": {
+                    "type": "array",
+                    "items": {"type": "number"},
+                    "minItems": 3,
+                    "maxItems": 3,
+                },
+            },
+            "required": ["version", "type", "point"],
+        },
+        "MPOI": {
+            "type": "object",
+            "properties": {
+                "name": {"type": "string"},
+                "type": {"enum": ["Multiple points"]},
+                "points": {
+                    "type": "array",
+                    "items": {
+                        "allOf": [{"$ref": "#/definitions/point-object"}]
+                    },
+                },
+            },
+            "required": ["version", "type", "points"],
+        },
+        "POLY": {
+            "type": "object",
+            "properties": {
+                "name": {"type": "string"},
+                "seed_point": {
+                    "type": "array",
+                    "items": {"type": "number"},
+                    "minItems": 3,
+                    "maxItems": 3,
+                },
+                "path_points": {
+                    "type": "array",
+                    "items": {
+                        "type": "array",
+                        "items": {"type": "number"},
+                        "minItems": 3,
+                        "maxItems": 3,
+                    },
+                },
+                "sub_type": {"type": "string"},
+                "groups": {"type": "array", "items": {"type": "string"}},
+            },
+            "required": [
+                "name",
+                "seed_point",
+                "path_points",
+                "sub_type",
+                "groups",
+                "version",
+            ],
+        },
+        "MPOL": {
+            "type": "object",
+            "properties": {
+                "type": {"enum": ["Multiple polygons"]},
+                "name": {"type": "string"},
+                "polygons": {
+                    "type": "array",
+                    "items": {"$ref": "#/definitions/polygon-object"},
+                },
+            },
+            "required": ["type", "version", "polygons"],
+        },
     },
     "properties": {
         "version": {
@@ -718,6 +827,10 @@ def delete_reader_study_groups_hook(*_, instance: ReaderStudy, using, **__):
         {"$ref": "#/definitions/2DBB"},
         {"$ref": "#/definitions/DIST"},
         {"$ref": "#/definitions/MDIS"},
+        {"$ref": "#/definitions/POIN"},
+        {"$ref": "#/definitions/MPOI"},
+        {"$ref": "#/definitions/POLY"},
+        {"$ref": "#/definitions/MPOL"},
     ],
 }
 
@@ -730,6 +843,10 @@ class Question(UUIDModel):
     ANSWER_TYPE_2D_BOUNDING_BOX = "2DBB"
     ANSWER_TYPE_DISTANCE_MEASUREMENT = "DIST"
     ANSWER_TYPE_MULTIPLE_DISTANCE_MEASUREMENTS = "MDIS"
+    ANSWER_TYPE_POINT = "POIN"
+    ANSWER_TYPE_MULTIPLE_POINTS = "MPOI"
+    ANSWER_TYPE_POLYGON = "POLY"
+    ANSWER_TYPE_MULTIPLE_POLYGONS = "MPOL"
     # WARNING: Do not change the display text, these are used in the front end
     ANSWER_TYPE_CHOICES = (
         (ANSWER_TYPE_SINGLE_LINE_TEXT, "Single line text"),
@@ -742,6 +859,10 @@ class Question(UUIDModel):
             ANSWER_TYPE_MULTIPLE_DISTANCE_MEASUREMENTS,
             "Multiple distance measurements",
         ),
+        (ANSWER_TYPE_POINT, "Point"),
+        (ANSWER_TYPE_MULTIPLE_POINTS, "Multiple points"),
+        (ANSWER_TYPE_POLYGON, "Polygon"),
+        (ANSWER_TYPE_MULTIPLE_POLYGONS, "Multiple polygons"),
     )
 
     # What is the orientation of the question form when presented on the
@@ -879,6 +1000,10 @@ def clean(self):
                 self.ANSWER_TYPE_2D_BOUNDING_BOX,
                 self.ANSWER_TYPE_DISTANCE_MEASUREMENT,
                 self.ANSWER_TYPE_MULTIPLE_DISTANCE_MEASUREMENTS,
+                self.ANSWER_TYPE_POINT,
+                self.ANSWER_TYPE_MULTIPLE_POINTS,
+                self.ANSWER_TYPE_POLYGON,
+                self.ANSWER_TYPE_MULTIPLE_POLYGONS,
             ]
         ) != bool(self.image_port):
             raise ValidationError(


Test patch:
diff --git a/app/tests/reader_studies_tests/test_api.py b/app/tests/reader_studies_tests/test_api.py
--- a/app/tests/reader_studies_tests/test_api.py
+++ b/app/tests/reader_studies_tests/test_api.py
@@ -277,6 +277,142 @@ def test_answer_creator_is_reader(client):
             },
             400,
         ),
+        (
+            Question.ANSWER_TYPE_POINT,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Point",
+                "name": "test",
+                "point": (1, 2, 3),
+            },
+            201,
+        ),
+        (
+            Question.ANSWER_TYPE_POINT,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Point",
+                "name": "test",
+                "point": (1, 2,),
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_MULTIPLE_POINTS,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Multiple points",
+                "name": "test",
+                "points": [{"point": (1, 2, 3)}, {"point": (4, 5, 6)}],
+            },
+            201,
+        ),
+        (
+            Question.ANSWER_TYPE_MULTIPLE_POINTS,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Multiple points",
+                "name": "test",
+                "points": [{"point": (1, 2)}, {"point": (4, 5, 6)}],
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_POLYGON,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Polygon",
+                "name": "test",
+                "seed_point": (1, 2, 3),
+                "path_points": [(1, 2, 3), (4, 5, 6)],
+                "sub_type": "poly",
+                "groups": ["a", "b"],
+            },
+            201,
+        ),
+        (
+            Question.ANSWER_TYPE_POLYGON,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Polygon",
+                "name": "test",
+                "path_points": [(1, 2, 3), (4, 5, 6)],
+                "sub_type": "poly",
+                "groups": ["a", "b"],
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_POLYGON,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Polygon",
+                "name": "test",
+                "seed_point": (1, 2, 3),
+                "sub_type": "poly",
+                "groups": ["a", "b"],
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_POLYGON,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Polygon",
+                "name": "test",
+                "seed_point": (1, 2, 3),
+                "path_points": [(1, 2, 3), (4, 5, 6)],
+                "sub_type": "poly",
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_POLYGON,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Polygon",
+                "name": "test",
+                "seed_point": (1, 2, 3),
+                "path_points": [(1, 2, 3), (4, 5, 6)],
+                "groups": ["a", "b"],
+            },
+            400,
+        ),
+        (
+            Question.ANSWER_TYPE_MULTIPLE_POLYGONS,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Multiple polygons",
+                "name": "test",
+                "polygons": [
+                    {
+                        "name": "test",
+                        "seed_point": (1, 2, 3),
+                        "path_points": [(1, 2, 3), (4, 5, 6)],
+                        "sub_type": "poly",
+                        "groups": ["a", "b"],
+                    }
+                ],
+            },
+            201,
+        ),
+        (
+            Question.ANSWER_TYPE_MULTIPLE_POLYGONS,
+            {
+                "version": {"major": 1, "minor": 0},
+                "type": "Multiple polygons",
+                "name": "test",
+                "polygons": [
+                    {
+                        "seed_point": (1, 2, 3),
+                        "path_points": [(1, 2, 3), (4, 5, 6)],
+                        "sub_type": "poly",
+                        "groups": ["a", "b"],
+                    }
+                ],
+            },
+            400,
+        ),
     ),
 )
 def test_answer_is_correct_type(client, answer_type, answer, expected):
diff --git a/app/tests/reader_studies_tests/validation_tests.py b/app/tests/reader_studies_tests/validation_tests.py
--- a/app/tests/reader_studies_tests/validation_tests.py
+++ b/app/tests/reader_studies_tests/validation_tests.py
@@ -173,6 +173,41 @@ def test_hanging_list_shuffle_per_user(client):
             {"start": [0, 0, 0], "end": [10, 0, 0]},
         ],
     },
+    "POIN": {
+        "version": {"major": 1, "minor": 0},
+        "type": "Point",
+        "name": "test_name",
+        "point": [0, 0, 0],
+    },
+    "MPOI": {
+        "version": {"major": 1, "minor": 0},
+        "type": "Multiple points",
+        "name": "test_name",
+        "points": [{"point": [0, 0, 0]}],
+    },
+    "POLY": {
+        "version": {"major": 1, "minor": 0},
+        "type": "Polygon",
+        "name": "test_name",
+        "seed_point": [0, 0, 0],
+        "path_points": [[0, 0, 0], [0, 0, 0]],
+        "sub_type": "poly",
+        "groups": ["a", "b"],
+    },
+    "MPOL": {
+        "version": {"major": 1, "minor": 0},
+        "type": "Multiple polygons",
+        "name": "test_name",
+        "polygons": [
+            {
+                "name": "test_name",
+                "seed_point": [0, 0, 0],
+                "path_points": [[0, 0, 0], [0, 0, 0]],
+                "sub_type": "poly",
+                "groups": ["a", "b"],
+            }
+        ],
+    },
 }
 
 
@@ -192,31 +227,18 @@ def test_answer_type_annotation_header_schema_fails(
     assert not q.is_answer_valid(answer=answer)
 
 
-@pytest.mark.parametrize(
-    "answer_type, answer_type_check",
-    [
-        ("STXT", "2DBB"),
-        ("STXT", "DIST"),
-        ("MTXT", "MDIS"),
-        ("BOOL", "2DBB"),
-        ("BOOL", "STXT"),
-        ("2DBB", "DIST"),
-        ("2DBB", "STXT"),
-        ("DIST", "MDIS"),
-        ("DIST", "2DBB"),
-        ("MDIS", "2DBB"),
-        ("MDIS", "DIST"),
-    ],
-)
-def test_answer_type_annotation_schema_mismatch(
-    answer_type, answer_type_check
-):
-    a = ANSWER_TYPE_NAMES_AND_ANSWERS[answer_type]
-    assert Question(answer_type=answer_type).is_answer_valid(answer=a) is True
-    assert (
-        Question(answer_type=answer_type_check).is_answer_valid(answer=a)
-        is False
-    )
+def test_answer_type_annotation_schema_mismatch():
+    # Answers to STXT are valid for MTXT as well, that's why STXT is excluded
+    # Other than that, each answer is only valid for a single answer type
+    unique_answer_types = [
+        key for key in ANSWER_TYPE_NAMES_AND_ANSWERS.keys() if key != "STXT"
+    ]
+    for answer_type in unique_answer_types:
+        answer = ANSWER_TYPE_NAMES_AND_ANSWERS[answer_type]
+        for answer_type_check in unique_answer_types:
+            assert Question(answer_type=answer_type_check).is_answer_valid(
+                answer=answer
+            ) == (answer_type == answer_type_check)
 
 
 def test_new_answer_type_listed():