From 6f5b720d61c4ac3c63de074eed86c993cbe6d83b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Lind=C3=A9n?= Date: Mon, 26 Jun 2023 08:14:16 -0700 Subject: [PATCH 1/2] Restructured tests and shemas --- .../schemas/comment.py | 54 +++++++- .../schemas/shapes.py | 131 ++++++++++++++++++ .../schemas/tag.py | 25 ++++ .../schemas/video_schema.py | 73 ++++++++++ .../vector.py | 36 +++-- tests/test_data/vector/expected_bbox.json | 41 ++++++ tests/test_data/vector/expected_cuboid.json | 46 ++++++ tests/test_data/vector/expected_ellipse.json | 42 ++++++ .../test_data/vector/expected_instances.json | 8 -- tests/test_data/vector/expected_point.json | 32 +++++ tests/test_data/vector/expected_polygon.json | 60 ++++++++ tests/test_data/vector/expected_polyline.json | 42 ++++++ tests/test_data/vector/expected_rbbox.json | 38 +++++ tests/test_data/vector/expected_tag.json | 18 +++ tests/test_vector.py | 14 +- 15 files changed, 622 insertions(+), 38 deletions(-) create mode 100644 src/superannotate_databricks_connector/schemas/shapes.py create mode 100644 src/superannotate_databricks_connector/schemas/tag.py create mode 100644 src/superannotate_databricks_connector/schemas/video_schema.py create mode 100644 tests/test_data/vector/expected_bbox.json create mode 100644 tests/test_data/vector/expected_cuboid.json create mode 100644 tests/test_data/vector/expected_ellipse.json delete mode 100644 tests/test_data/vector/expected_instances.json create mode 100644 tests/test_data/vector/expected_point.json create mode 100644 tests/test_data/vector/expected_polygon.json create mode 100644 tests/test_data/vector/expected_polyline.json create mode 100644 tests/test_data/vector/expected_rbbox.json create mode 100644 tests/test_data/vector/expected_tag.json diff --git a/src/superannotate_databricks_connector/schemas/comment.py b/src/superannotate_databricks_connector/schemas/comment.py index 7a9f16c..339e659 100644 --- a/src/superannotate_databricks_connector/schemas/comment.py +++ b/src/superannotate_databricks_connector/schemas/comment.py @@ -5,12 +5,15 @@ FloatType, BooleanType, MapType, - ArrayType + ArrayType, + IntegerType ) +from .shapes import get_bbox_schema -def get_comment_schema(): - comment_schema = StructType([ + +def get_vector_comment_schema(): + return StructType([ StructField("correspondence", ArrayType(MapType( StringType(), @@ -23,12 +26,53 @@ def get_comment_schema(): StructField("createdBy", MapType( StringType(), StringType()), + True), + StructField("creationType", StringType(), True), + StructField("updatedAt", StringType(), True), + StructField("updatedBy", MapType( + StringType(), + StringType()), + True) + ]) + + +def get_video_timestamp_schema(): + return StructType([ + StructField("timestamp", IntegerType(), True), + StructField("points", get_bbox_schema(), True) + ]) + + +def get_video_comment_parameter_schema(): + return StructType([ + StructField("start", IntegerType(), True), + StructField("end", IntegerType, True), + StructField("timestamps", ArrayType( + get_video_timestamp_schema()), True) + ]) + + +def get_video_comment_schema(): + return StructType([ + StructField("correspondence", + ArrayType(MapType( + StringType(), + StringType())), True), + StructField("start", IntegerType(), True), + StructField("end", IntegerType(), True), + StructField("createdAt", StringType(), True), + StructField("createdBy", MapType( + StringType(), + StringType()), + True), StructField("creationType", StringType(), True), StructField("updatedAt", StringType(), True), StructField("updatedBy", MapType( StringType(), StringType()), - True) + True), + StructField("parameters", + ArrayType(get_video_comment_parameter_schema()), True) + ]) - return comment_schema diff --git a/src/superannotate_databricks_connector/schemas/shapes.py b/src/superannotate_databricks_connector/schemas/shapes.py new file mode 100644 index 0000000..599244b --- /dev/null +++ b/src/superannotate_databricks_connector/schemas/shapes.py @@ -0,0 +1,131 @@ +from pyspark.sql.types import ( + StructType, + StructField, + FloatType, + ArrayType +) + + +def get_bbox_schema(): + """ + Defines the schema of a bounding box + + Args: + None + + Returns: + StructType: Schema of a bbox + """ + return StructType([ + StructField("x1", FloatType(), True), + StructField("y1", FloatType(), True), + StructField("x2", FloatType(), True), + StructField("y2", FloatType(), True) + ]) + + +def get_rbbox_schema(): + """ + Defines the schema of a rotated bounding box + this contains one point for each corned + + Args: + None + + Returns: + StructType: Schema of a bbox + """ + return StructType([ + StructField("x1", FloatType(), True), + StructField("y1", FloatType(), True), + StructField("x2", FloatType(), True), + StructField("y2", FloatType(), True), + StructField("x3", FloatType(), True), + StructField("y3", FloatType(), True), + StructField("x4", FloatType(), True), + StructField("y5", FloatType(), True) + ]) + + +def get_point_schema(): + """ + Defines the schema of a point + + Args: + None + + Returns: + StructType: Schema of a point + """ + return StructType([ + StructField("x", FloatType(), True), + StructField("y", FloatType(), True) + ]) + + +def get_cuboid_schema(): + """ + Defines the schema of a cuboid (3d bounding box) + + Args: + None + + Returns: + StructType: Schema of a cuboid + """ + return StructType([ + StructField("f1", get_point_schema(), True), + StructField("f2", get_point_schema(), True), + StructField("r1", get_point_schema(), True), + StructField("r2", get_point_schema(), True) + ]) + + +def get_ellipse_schema(): + """ + Defines the schema of an ellipse + + Args: + None + + Returns: + StructType: Schema of an ellipse + """ + return StructType([ + StructField("cx", FloatType(), True), + StructField("cy", FloatType(), True), + StructField("rx", FloatType(), True), + StructField("ty", FloatType(), True), + StructField("angle", FloatType(), True) + ]) + + +def get_polygon_schema(): + """ + Defines the schema of a polygon. It contains a shell as well + as excluded points + + Args: + None + + Returns: + StructType: Schema of a polygon with holes + """ + return StructType([ + StructField("points", ArrayType(FloatType()), True), + StructField("exclude", ArrayType(ArrayType(FloatType)), True) + ]) + + +def get_polyline_schema(): + """ + Defines the schema of a polyline + A simple array of float + + Args: + None + + Returns: + ArrayType: Schema of a polygon with holes + """ + return ArrayType(FloatType()) diff --git a/src/superannotate_databricks_connector/schemas/tag.py b/src/superannotate_databricks_connector/schemas/tag.py new file mode 100644 index 0000000..3a9c476 --- /dev/null +++ b/src/superannotate_databricks_connector/schemas/tag.py @@ -0,0 +1,25 @@ +from pyspark.sql.types import ( + StructType, + StructField, + StringType, + IntegerType, + MapType, + ArrayType, +) + + +def get_tag_schema(): + schema = StructType([ + StructField("instance_type", StringType(), True), + StructField("classId", IntegerType(), True), + StructField("probability", IntegerType(), True), + StructField("attributes", ArrayType(MapType(StringType(), + StringType())), + True), + StructField("createdAt", StringType(), True), + StructField("createdBy", MapType(StringType(), StringType()), True), + StructField("creationType", StringType(), True), + StructField("updatedAt", StringType(), True), + StructField("updatedBy", MapType(StringType(), StringType()), True), + StructField("className", StringType(), True)]) + return schema \ No newline at end of file diff --git a/src/superannotate_databricks_connector/schemas/video_schema.py b/src/superannotate_databricks_connector/schemas/video_schema.py new file mode 100644 index 0000000..57706c6 --- /dev/null +++ b/src/superannotate_databricks_connector/schemas/video_schema.py @@ -0,0 +1,73 @@ +from pyspark.sql.types import ( + StructType, + StructField, + StringType, + IntegerType, + ArrayType, + MapType +) + +from .shapes import ( + get_point_schema, + get_bbox_schema, + get_polygon_schema, + get_polyline_schema, +) +from .comment import get_video_comment_schema +from .tag import get_tag_schema + + +def get_instance_metadata_schema(): + return StructType([ + StructField("type", StringType(), True), + StructField("className", StringType(), True), + StructField("start", IntegerType(), True), + StructField("end", IntegerType(), True), + StructField("creationType", StringType, True) + ]) + + +def get_timestamp_schema(): + return StructType([ + StructField("timestamp", IntegerType(), True), + StructField("bbox", get_bbox_schema(), True), + StructField("polygon", get_polygon_schema()), + StructField("polyline", get_polyline_schema(), True), + StructField("point", get_point_schema(), True), + StructField("attributes", ArrayType(MapType(StringType(), + StringType())), + True) + ]) + + +def get_instance_parameter_schema(): + return StructType([ + StructField("start", IntegerType(), True), + StructField("end", IntegerType(), True), + StructField("timestamp", get_timestamp_schema(), True) + ]) + + +def get_instance_schema(): + StructType([ + StructField("meta", get_instance_metadata_schema(), True), + StructField("parameters", get_instance_parameter_schema(), True) + ]) + + +def get_video_schema(): + return StructType([ + StructField("video_height", IntegerType(), True), + StructField("video_width", IntegerType(), True), + StructField("video_name", StringType(), True), + StructField("url", StringType(), True), + StructField("projectId", IntegerType(), True), + StructField("duration", IntegerType(), True), + StructField("status", StringType(), True), + StructField("annotatorEmail", StringType(), True), + StructField("qaEmail", StringType(), True), + StructField("instances", ArrayType(get_instance_schema()), + True), + StructField("comments", ArrayType(get_video_comment_schema()), True), + StructField("tags", ArrayType(get_tag_schema()), True) + ]) diff --git a/src/superannotate_databricks_connector/vector.py b/src/superannotate_databricks_connector/vector.py index ae4f68b..250ec4c 100644 --- a/src/superannotate_databricks_connector/vector.py +++ b/src/superannotate_databricks_connector/vector.py @@ -24,25 +24,29 @@ def process_vector_object(instance, custom_id_map=None): 'classId': instance["classId"] if custom_id_map is None else custom_id_map.get(instance["className"]), 'probability': instance.get('probability'), - 'bbox_points': {k: float(v) for k, v in instance['points'].items()} + 'bbox': {k: float(v) for k, v in instance['points'].items()} if instance["type"] == "bbox" else None, - 'polygon_points': [float(p) for p in instance['points']] + 'rbbox': {k: float(v) for k, v in instance['points'].items()} + if instance["type"] == "rbbox" else None, + 'polygon': {"points": [float(p) for p in instance['points']] + if instance["type"] == "polygon" else None, + 'exclude': instance.get("exclude")} if instance["type"] == "polygon" else None, - 'polygon_exclude': instance["exclude"] - if instance["type"] == "polygon" else None, - 'point_points': {"x": float(instance["x"]), - "y": float(instance["y"]) - } if instance["type"] == "point" else None, - 'ellipse_points': {"cx": float(instance["cx"]), - "cy": float(instance["cy"]), - "rx": float(instance["rx"]), - "ry": float(instance["ry"]), - "angle": float(instance["angle"])} + 'point': {"x": float(instance.get("x")), + "y": float(instance.get("y")) + } if instance["type"] == "point" else None, + 'ellipse': {"cx": float(instance["cx"]), + "cy": float(instance["cy"]), + "rx": float(instance["rx"]), + "ry": float(instance["ry"]), + "angle": float(instance["angle"])} if instance["type"] == "ellipse" else None, - 'cuboid_points': {outer_k: {inner_k: float(inner_v) - for inner_k, inner_v in outer_v.items()} - for outer_k, outer_v in instance['points'].items()} + 'cuboid': {outer_k: {inner_k: float(inner_v) + for inner_k, inner_v in outer_v.items()} + for outer_k, outer_v in instance['points'].items()} if instance["type"] == "cuboid" else None, + "polyline": [float(p) for p in instance['points']] + if instance["type"] == "polyline" else None, 'groupId': instance.get('groupId'), 'locked': instance.get('locked'), 'attributes': instance['attributes'], @@ -150,7 +154,9 @@ def get_vector_dataframe(annotations, spark, custom_id_map=None): "comments": [process_comment(comment) for comment in item["comments"]] } + rows.append(flattened_item) schema = get_vector_schema() + spark_df = spark.createDataFrame(rows, schema=schema) return spark_df diff --git a/tests/test_data/vector/expected_bbox.json b/tests/test_data/vector/expected_bbox.json new file mode 100644 index 0000000..821ee25 --- /dev/null +++ b/tests/test_data/vector/expected_bbox.json @@ -0,0 +1,41 @@ +{ + "instance_type": "bbox", + "classId": 1622610, + "probability": 100, + "bbox": { + "x1": 434.53, + "x2": 568.32, + "y1": 712.81, + "y2": 848.69 + }, + "rbbox": null, + "polygon": null, + "cuboid": null, + "ellipse": null, + "polyline": null, + "point": null, + "groupId": 0, + "locked": false, + "attributes": [ + { + "id": 2994312, + "groupId": 736603, + "name": "Bbox", + "groupName": "Shape Type" + } + ], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:43:44.705Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:53:45.639Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_cuboid.json b/tests/test_data/vector/expected_cuboid.json new file mode 100644 index 0000000..f9ae30e --- /dev/null +++ b/tests/test_data/vector/expected_cuboid.json @@ -0,0 +1,46 @@ +{ + "instance_type": "cuboid", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": null, + "polygon": null, + "cuboid": { + "f1": { + "x": 1563.33, + "y": 900.95 + }, + "f2": { + "x": 1674.12, + "y": 781.8 + }, + "r1": { + "x": 1536.16, + "y": 777.62 + }, + "r2": { + "x": 1749.37, + "y": 558.13 + } + }, + "ellipse": null, + "polyline": null, + "point": null, + "groupId": 0, + "locked": false, + "attributes": [], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:44:46.908Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:45:00.764Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_ellipse.json b/tests/test_data/vector/expected_ellipse.json new file mode 100644 index 0000000..8f4f7db --- /dev/null +++ b/tests/test_data/vector/expected_ellipse.json @@ -0,0 +1,42 @@ +{ + "instance_type": "ellipse", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": null, + "polygon": null, + "cuboid": null, + "ellipse": { + "cx": 1401.33, + "cy": 687.73, + "rx": 109.74, + "ry": 110.79, + "angle": 0.0 + }, + "polyline": null, + "point": null, + "groupId": 0, + "locked": false, + "attributes": [ + { + "id": 2994313, + "groupId": 736603, + "name": "Ellipse", + "groupName": "Shape Type" + } + ], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:44:26.389Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:53:51.841Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_instances.json b/tests/test_data/vector/expected_instances.json deleted file mode 100644 index 1e94218..0000000 --- a/tests/test_data/vector/expected_instances.json +++ /dev/null @@ -1,8 +0,0 @@ -{"instance_type": "bbox", "classId": 1622610, "probability": 100, "bbox_points": {"x1": 434.53, "x2": 568.32, "y1": 712.81, "y2": 848.69}, "polygon_points": null, "polygon_exclude": null, "point_points": null, "ellipse_points": null, "cuboid_points": null, "groupId": 0, "locked": false, "attributes": [{"id": 2994312, "groupId": 736603, "name": "Bbox", "groupName": "Shape Type"}], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:43:44.705Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:53:45.639Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "polygon", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": [716.73, 909.31, 677.02, 827.78, 674.93, 675.19, 725.09, 637.56, 773.17, 673.1, 804.53, 760.89, 812.89, 857.05, 810.8, 890.49, 796.17, 890.49, 754.36, 911.4], "polygon_exclude": [], "point_points": null, "ellipse_points": null, "cuboid_points": null, "groupId": 0, "locked": true, "attributes": [{"id": 2994314, "groupId": 736603, "name": "Other", "groupName": "Shape Type"}], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:43:52.256Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:53:47.883Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "point", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": null, "polygon_exclude": null, "point_points": {"x": 1038.65, "y": 974.11}, "ellipse_points": null, "cuboid_points": null, "groupId": 0, "locked": false, "attributes": [], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:44:06.914Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:44:08.929Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "rbbox", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": null, "polygon_exclude": null, "point_points": null, "ellipse_points": null, "cuboid_points": null, "groupId": 0, "locked": false, "attributes": [], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:44:14.227Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:44:20.613Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "ellipse", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": null, "polygon_exclude": null, "point_points": null, "ellipse_points": {"cx": 1401.33, "cy": 687.73, "rx": 109.74, "ry": 110.79, "angle": 0.0}, "cuboid_points": null, "groupId": 0, "locked": false, "attributes": [{"id": 2994313, "groupId": 736603, "name": "Ellipse", "groupName": "Shape Type"}], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:44:26.389Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:53:51.841Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "polyline", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": null, "polygon_exclude": null, "point_points": null, "ellipse_points": null, "cuboid_points": null, "groupId": 0, "locked": false, "attributes": [], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:44:36.428Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:44:42.828Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "cuboid", "classId": 1622610, "probability": 100, "bbox_points": null, "polygon_points": null, "polygon_exclude": null, "point_points": null, "ellipse_points": null, "cuboid_points": {"f1": {"x": 1563.33, "y": 900.95}, "f2": {"x": 1674.12, "y": 781.8}, "r1": {"x": 1536.16, "y": 777.62}, "r2": {"x": 1749.37, "y": 558.13}}, "groupId": 0, "locked": false, "attributes": [], "trackingId": null, "error": null, "createdAt": "2023-06-12T11:44:46.908Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:45:00.764Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Grape"} -{"instance_type": "tag", "classId": 1626900, "probability":100, "attributes": [], "createdAt": "2023-06-12T11:53:41.951Z", "createdBy": {"email": "leo@superannotate.com", "role": "Admin"}, "creationType": "Manual", "updatedAt": "2023-06-12T11:53:41.951Z", "updatedBy": {"email": "leo@superannotate.com", "role": "Admin"}, "className": "Tag 2"} \ No newline at end of file diff --git a/tests/test_data/vector/expected_point.json b/tests/test_data/vector/expected_point.json new file mode 100644 index 0000000..ba74715 --- /dev/null +++ b/tests/test_data/vector/expected_point.json @@ -0,0 +1,32 @@ +{ + "instance_type": "point", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": null, + "polygon": null, + "cuboid": null, + "ellipse": null, + "polyline": null, + "point": { + "x": 1038.65, + "y": 974.11 + }, + "groupId": 0, + "locked": false, + "attributes": [], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:44:06.914Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:44:08.929Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_polygon.json b/tests/test_data/vector/expected_polygon.json new file mode 100644 index 0000000..68c4940 --- /dev/null +++ b/tests/test_data/vector/expected_polygon.json @@ -0,0 +1,60 @@ +{ + "instance_type": "polygon", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": null, + "polygon": { + "points": [ + 716.73, + 909.31, + 677.02, + 827.78, + 674.93, + 675.19, + 725.09, + 637.56, + 773.17, + 673.1, + 804.53, + 760.89, + 812.89, + 857.05, + 810.8, + 890.49, + 796.17, + 890.49, + 754.36, + 911.4 + ], + "exclude": [] + }, + "cuboid": null, + "ellipse": null, + "polyline": null, + "point": null, + "groupId": 0, + "locked": true, + "attributes": [ + { + "id": 2994314, + "groupId": 736603, + "name": "Other", + "groupName": "Shape Type" + } + ], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:43:52.256Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:53:47.883Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_polyline.json b/tests/test_data/vector/expected_polyline.json new file mode 100644 index 0000000..d514b6a --- /dev/null +++ b/tests/test_data/vector/expected_polyline.json @@ -0,0 +1,42 @@ +{ + "instance_type": "polyline", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": null, + "polygon": null, + "cuboid": null, + "ellipse": null, + "polyline": [ + 869.33, + 1130.89, + 965.49, + 1041, + 1141.08, + 1034.73, + 1471.35, + 1030.55, + 1603.05, + 1149.7, + 1598.87, + 1365.01 + ], + "point": null, + "groupId": 0, + "locked": false, + "attributes": [], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:44:36.428Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:44:42.828Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_rbbox.json b/tests/test_data/vector/expected_rbbox.json new file mode 100644 index 0000000..157f670 --- /dev/null +++ b/tests/test_data/vector/expected_rbbox.json @@ -0,0 +1,38 @@ +{ + "instance_type": "rbbox", + "classId": 1622610, + "probability": 100, + "bbox": null, + "rbbox": { + "x1": 955.95, + "y1": 734.45, + "x2": 1177.53, + "y2": 586.03, + "x3": 1296.95, + "y3": 764.33, + "x4": 1075.37, + "y4": 912.75 + }, + "polygon": null, + "cuboid": null, + "ellipse": null, + "polyline": null, + "point": null, + "groupId": 0, + "locked": false, + "attributes": [], + "trackingId": null, + "error": null, + "createdAt": "2023-06-12T11:44:14.227Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:44:20.613Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Grape" +} \ No newline at end of file diff --git a/tests/test_data/vector/expected_tag.json b/tests/test_data/vector/expected_tag.json new file mode 100644 index 0000000..5afa84b --- /dev/null +++ b/tests/test_data/vector/expected_tag.json @@ -0,0 +1,18 @@ +{ + "instance_type": "tag", + "classId": 1626900, + "probability": 100, + "attributes": [], + "createdAt": "2023-06-12T11:53:41.951Z", + "createdBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "creationType": "Manual", + "updatedAt": "2023-06-12T11:53:41.951Z", + "updatedBy": { + "email": "leo@superannotate.com", + "role": "Admin" + }, + "className": "Tag 2" +} \ No newline at end of file diff --git a/tests/test_vector.py b/tests/test_vector.py index b388242..fd178b9 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -20,22 +20,16 @@ def __init__(self, *args): with open(os.path.join(DATA_SET_PATH, "vector/example_annotation.json"), "r") as f: data = json.load(f) - - target_data = [] - with open(os.path.join(DATA_SET_PATH, - 'vector/expected_instances.json'), "r") as f: - for line in f: - target_data.append(json.loads(line)) - self.test_data = data - self.target_data = target_data self.spark = SparkSession.builder.appName('test').getOrCreate() def __get_test_pair(self, instance_type): test = [data for data in self.test_data["instances"] if data["type"] == instance_type][0] - target = [data for data in self.target_data - if data["instance_type"] == instance_type][0] + with open(os.path.join(DATA_SET_PATH, + f'vector/expected_{instance_type}.json'), + "r") as f: + target = json.load(f) return test, target def __assert_equal(self, instance_type): From 041d68db300091ebf37781c54d0211c30e6418c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leo=20Lind=C3=A9n?= Date: Mon, 26 Jun 2023 08:32:33 -0700 Subject: [PATCH 2/2] Bufixes --- .../schemas/shapes.py | 2 +- .../schemas/vector_schema.py | 70 ++++++------------ ...-9ba1-d594b0852c2c-c000.snappy.parquet.crc | Bin 0 -> 220 bytes ...-b51c-4d2461300ccf-c000.snappy.parquet.crc | Bin 192 -> 0 bytes ...4b61-9ba1-d594b0852c2c-c000.snappy.parquet | Bin 0 -> 26799 bytes ...4d94-b51c-4d2461300ccf-c000.snappy.parquet | Bin 23169 -> 0 bytes 6 files changed, 22 insertions(+), 50 deletions(-) create mode 100644 tests/test_data/vector/expected_df.parquet/.part-00000-297a9061-4979-4b61-9ba1-d594b0852c2c-c000.snappy.parquet.crc delete mode 100644 tests/test_data/vector/expected_df.parquet/.part-00000-fc80ae53-f845-4d94-b51c-4d2461300ccf-c000.snappy.parquet.crc create mode 100644 tests/test_data/vector/expected_df.parquet/part-00000-297a9061-4979-4b61-9ba1-d594b0852c2c-c000.snappy.parquet delete mode 100644 tests/test_data/vector/expected_df.parquet/part-00000-fc80ae53-f845-4d94-b51c-4d2461300ccf-c000.snappy.parquet diff --git a/src/superannotate_databricks_connector/schemas/shapes.py b/src/superannotate_databricks_connector/schemas/shapes.py index 599244b..9ea9200 100644 --- a/src/superannotate_databricks_connector/schemas/shapes.py +++ b/src/superannotate_databricks_connector/schemas/shapes.py @@ -113,7 +113,7 @@ def get_polygon_schema(): """ return StructType([ StructField("points", ArrayType(FloatType()), True), - StructField("exclude", ArrayType(ArrayType(FloatType)), True) + StructField("exclude", ArrayType(ArrayType(FloatType())), True) ]) diff --git a/src/superannotate_databricks_connector/schemas/vector_schema.py b/src/superannotate_databricks_connector/schemas/vector_schema.py index 7aaea90..0831505 100644 --- a/src/superannotate_databricks_connector/schemas/vector_schema.py +++ b/src/superannotate_databricks_connector/schemas/vector_schema.py @@ -3,45 +3,35 @@ StructField, StringType, IntegerType, - FloatType, BooleanType, MapType, ArrayType ) -from .comment import get_comment_schema - - -def get_point_schema(): - point_schema = StructType([ - StructField("x", FloatType(), True), - StructField("y", FloatType(), True) - ]) - return point_schema - - -def get_cuboid_schema(): - cuboid_points_schema = StructType([ - StructField("f1", get_point_schema(), True), - StructField("f2", get_point_schema(), True), - StructField("r1", get_point_schema(), True), - StructField("r2", get_point_schema(), True) - ]) - return cuboid_points_schema +from .comment import get_vector_comment_schema +from .shapes import ( + get_point_schema, + get_cuboid_schema, + get_bbox_schema, + get_ellipse_schema, + get_polygon_schema, + get_polyline_schema, + get_rbbox_schema +) +from .tag import get_tag_schema def get_vector_instance_schema(): - instance_schema = StructType([ + return StructType([ StructField("instance_type", StringType(), True), StructField("classId", IntegerType(), True), StructField("probability", IntegerType(), True), - StructField("bbox_points", MapType(StringType(), FloatType()), True), - StructField("polygon_points", ArrayType(FloatType()), True), - StructField("polygon_exclude", ArrayType(ArrayType(FloatType())), - True), - StructField("cuboid_points", get_cuboid_schema(), True), - StructField("ellipse_points", MapType(StringType(), FloatType()), - True), - StructField("point_points", MapType(StringType(), FloatType()), True), + StructField("bbox", get_bbox_schema(), True), + StructField("rbbox", get_rbbox_schema(), True), + StructField("polygon", get_polygon_schema()), + StructField("cuboid", get_cuboid_schema(), True), + StructField("ellipse", get_ellipse_schema(), True), + StructField("polyline", get_polyline_schema(), True), + StructField("point", get_point_schema(), True), StructField("groupId", IntegerType(), True), StructField("locked", BooleanType(), True), StructField("attributes", ArrayType(MapType(StringType(), @@ -56,24 +46,6 @@ def get_vector_instance_schema(): StructField("updatedBy", MapType(StringType(), StringType()), True), StructField("className", StringType(), True) ]) - return instance_schema - - -def get_vector_tag_schema(): - schema = StructType([ - StructField("instance_type", StringType(), True), - StructField("classId", IntegerType(), True), - StructField("probability", IntegerType(), True), - StructField("attributes", ArrayType(MapType(StringType(), - StringType())), - True), - StructField("createdAt", StringType(), True), - StructField("createdBy", MapType(StringType(), StringType()), True), - StructField("creationType", StringType(), True), - StructField("updatedAt", StringType(), True), - StructField("updatedBy", MapType(StringType(), StringType()), True), - StructField("className", StringType(), True)]) - return schema def get_vector_schema(): @@ -90,7 +62,7 @@ def get_vector_schema(): StructField("instances", ArrayType(get_vector_instance_schema()), True), StructField("bounding_boxes", ArrayType(IntegerType()), True), - StructField("comments", ArrayType(get_comment_schema()), True), - StructField("tags", ArrayType(get_vector_tag_schema()), True) + StructField("comments", ArrayType(get_vector_comment_schema()), True), + StructField("tags", ArrayType(get_tag_schema()), True) ]) return schema diff --git a/tests/test_data/vector/expected_df.parquet/.part-00000-297a9061-4979-4b61-9ba1-d594b0852c2c-c000.snappy.parquet.crc b/tests/test_data/vector/expected_df.parquet/.part-00000-297a9061-4979-4b61-9ba1-d594b0852c2c-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..26164a9dad0503e2b911ec1df48730889a39703f GIT binary patch literal 220 zcmV<203-ina$^7h00ICdS2lA7T}KhSq$;AlFN2oYbU*s^I`}ZI|3af14WJA7{C3Gt z67=RKBdYU=9z9NN=uTF(u;vB5UGQv4n^q||&O;B2)BJoLg7HB#xJYuX2Fpg+BE zy=r!FVYJenTYDTQUwAaLIo(wsBa#2(L`cG?wxRDpmjSV*9oqB`Yh+|%P5P7z@{2q} WP_!!93Y*0&Y`yy!H<>A_fHMxrx-_c_o<2J1&e--Hfy$&d3#%v^#tBiDO;K>_$V!9JU`_5%U;~9 z8Y2?g?36!K(9sA;s8o%cs;dWHS|ZL%AV8VCKrGAEq#Sp>l-TU}Z}03Y?;Ds;>5GAk uRcg9KQ7Sc?KZPtezXFsSNn)>f#WV zNk)+ie>(6{2B%L-HWh|ggFJpoL)L4^w@>_NUs?|6jg^=p2&Iy;k0fhS(#Fpmtx0p0 zm)A6;f@-xj{>I9hYMpLGT2*yWlP(0Vy&=cv_gB@{`OHmL#7K_DOnJbGw1EqqiRMno*a1hiI+1GIaxv;<)WHD0(?ReKnqE-ggvjn zzIz0dPyvtQmmmO89{J>wAQM^Ne|GabDO`PovD$5Jnf0mm zU%Yuu4AvFdBNf(v|H-O9kBq@OadN4`y74#PKAshg^~BDsUtO+NSo_{RcPc#=>oWxk z>)GRf*dA2@kb1%LJ%#o8*7ZlhqnJm8)Bv!ad33bG+WhecK6YHyzUvdO?036;~e*6ujjcZ z9>{aQaeKb|sCS(E!&$evmrT!dpSf|S`$FLq`1@tIecF7t{l~N13x2TB{b9x(^f%)v zmwVIuUQ`v`y|ekb9v!FaC|44yP)Z-{&n>F?=&2p_Kw1Ode>V&OOBy{y>6Pq`t!@JA4PYo zPQy6e9)^3dL*ms)0-96_Dk=-6Wy#MSs#A$Lkt}c*41f^3H)zPIH=kG!4KN-E)Ujttj$Qh~Xl;^40LMOc!g*qJ zN)q_mxO>0qUa{Z__ku+axCP@X_uBSW_kyoH>bC#xHTR}>R=PKN_PLjIu5}+h@s@ku z<_^WcxBu<(243qBAN$&CS-(2`sKVO+=l71nXdBscfi>ehb#6JPKlSEs9@DazE}Mki zB#u!*AY(}`dA$44&v@6nrjYCVFnqv(LuQk?;Hf_X7p8j!XFN_67kVGrjuo(NQ?(%2vEy7`LRBBTH=ik%n z`uqC@VCm41%^Gs_XPxV=alyb0NcSE>xDpsYacIA1si)RwEN!gyA(E}A$YHnIP)>o} z>9p8U^j*QFzCbD&D>yV0-M33atIBK#wF-%geD=#VFM#DttsFm73zh$f)vrezEjIft7UwNi zTdCDr?64O*9OeQ`e%ZVnAv@n@w&gqLN=O0Nb~w9uXDjY)bX`bu?Bq0Jym|J@j#8=2lklABy!^UHSa<9 z%u$-~x*4>tHa`7yI<|U_PNVCX1P|S025JB3r(HT(V(w}JkNv)ysde?WzJRB?x+dre z`pn*%fE<`-Km<^reVi`ojKiAbJlfBl_IxfRll*q(O;pl>G92qoY# z>o@P+4?}7Oyhp>sjZzO7o1{r8{XFT%8bNp)dO&~=Te;8z{sx}93mrX}Zp`}X%C?u^Fn@O{n3sNRc#Z=TN^ z^i)^+edBdmi|cA@sw=9hD~-XWo}jS`{?r*g#`2o_ba93}^Pelt{Il^@-JG-vW68BQ z2_@+_<;Wo_Ez3L#=l(ifrUjM;=+@JuC4dx1pX_dFf+ZFUSaKX?mZT-?Mj0(r=Ps^Y z3AUG#`Q*31{?V)a7&s|3xCqAIPZL%ayFct}c8|Na!@b~#o7`*5R>4$lKaNExVcTEZ z{=`rD2S~_(*#=JTwyZ05<7kcp;o7#K`=~U%#{uS*pFYvYN4S9l3`SPyi~BX?Q19x` z_#txezUY8%Ht2cI0w84;nC;H8QbBLG zn2QREzNr20Zx{s+Xvn*7biB?l75JKeDDo(jk5SO!EGpAOILo%$%}$3MYrN2GE3(iU zx0)UHe5`SYxzM_hJ}Put=NCfN+hP22ele?FfYH!QUi;;De#sId&{4@;j3r3|4x34E z!8J+HgUJq@imrP6yPYt|NEsI3mgjzqJNsR_nn%{-Zyr7dbET-P6(AU-V#j)_52>IgAF789o)k zp%D69zO@V_OeH1cLjQlf3)&|`k%n?(4pk`)M5NBXMM4}ny(lLYWZlY_FG8GR?^(I` z);ZDQI3^Jmm~3u(skaTn6|+?@1w1st0w#pa6s`i3&5(F|ZpUW_U_Hi6zFswXGMJ2w zTof4fd8MuYY?G8$(YCkGR^0ylWIC?a-x@hWutS% zG%_86n_$Qh=de3S)h?8Y=QNDkr6p5k>P0OnlW%*H7_}{#Op&RF)Z11a>DI{Prc^Rd zzHLckx4mg(zD(Vo&TgAC*zHO6cE<=tZOLS}r_@`>3@XCbjAXZc>TTCGjM_Si%$9+V zk0LEv`M!TNQ?2tl#=It*-S(-sU4&6vMRwb--tN4fQP=6&?P>LPmw{2+#<1Iu$B;R) zI-Or))HOL|pHi2HZ(!t(vFvvHIHvn*Bbh6U*ncCbkZ&*EMDCYw`^GbjZ4=n-l?kL- zsfj~*)Z7nQw3!vK#DnsPs=B!WUqzKS=mQGAm{JG%;`%z}C1gset*Wk8o{yAM z$~A%8F>S5z;j5_+cz9Yqz*SX)xN5Jj4wg$Pvt6J@ezHER;POXp1L~J8%ra=j&e_VmA@+3h;rz<1kO^BG zjv_n`la(T$VMs&4+9g)dhVp0{7{+9kFq4+7EN2t;`U4523}NQPV+UKvU2ZImei zg#v9wmC?~8E^3&Dm4+$UNVbO=r=yh`L38R8%A8#C>=b{EcNsV>fMj@r!9Z1ceQRsC z^&=WU(*U~0|DX~bl{UOu+7)Up8D{B&wnX;u!uQ{=hhA+BM91u~jwfmjIfl#gOqiK9 zd5O)Shw%C6cF$<` z!9>mhk(kz$t|(G1@gfhR$hNsCay@LaWy*ST6BjD7Ok4=hU7@+q&=O#TWJ*Jy@|b-A zB8q$Fk~xCu9G;z&G1XZSv*H{;()_A)MKdtlDwTMV7g1#E-DpM^Y)}ZYNPc2Pg}TJ4 zz~F)eE&Ks-^p_XZS_gu5-A!@@(?t-Z)Q3DDqcY};W$?_!r*ipwkjf=_`6oQi7Q{Ju zH<>P&dSDk=QCObjFNvk88wkjj2`V zate9UBv6_<^2%MVBYJVwJ;WlIns6HjFp{HUDAQ6Z{ULF+2Hr6pPRWtdQPLHi2a{7$ zNia-?eV1hDR?xTbM-bNs!K&@|pbxdsE*lGxpZcn(Eor7>sep=^cIk?!YAFJ02cn*$ zsJ(!iM(gsvD0N9E*f6oCi1Wnxrd|vkn825=VrCv!;;VBH!nBs6aob_DIUbls6=r)n z!oY@rbfqw?#KRmxm|Ya68TQoTfoZTYYnn3v1~(X_E5fKH9_9?fd_rMP#tGA?!kk1H z+^~?Y2&0yGm{s)vv+-W6v>mWd7uy=ik@?2iF#>RK2SmCej#}bzHX=?p#lcfiap7>S z(~^leu%#khDGn>~I9n0tJjFQ`FAmo{rw|ACXrwFRs3jif7l_k7AM33Xwrb54dNiN|Sr5O7}k3f5Z}>}|$pkAvCMbq(O)K9_Vw9JR#bJcBsLDNgICIB^>D zDb5UWwyAZL%L)5t>Ois`*tK!RJ_$SK3yr#6w*|sHU%>RqON#K&e)((*qQ4NK03Q zQcFD4x(0ySL7`4FsAw}GCE=y@s@>`|BH=FsRxmEQ>2gMM+!G-ub0?b(fNgEc|qS3Dk$lFs?)4KU7~W@#x!l)%4W-r35Ux<~secYgHCgp1d94hFV zY5O?S%5ga%?S***_)nrmSiKxW#Jv=8wGo%Gu~vd|GMGBYY;mq>wGkG#8hIg(zq~D< zqU5s+adErqHqj5~@ZQt=P3YHq7n4UIwUMW^TC{MYs8P}w<}W$oCXLvx0U@TQ zyd2Prp6Ss`CleyH&UrL{j|a0B8zv0}m-$DIxSM84HcT?TG!c;_pBmEZ=%Jzrs{=Z$|vR<-{qN zuE_Y&lSJukOGNR6_23CNNALvoSW}oMu(MFSCv>g=PqoT>usN>?<#psT^ z4UIpxlt!v4Lx5#&stj4}Q%pPlHHMo;9kRMnljbT6R9&|!nnY=aXd_nFt)@#;hBjjL zBK+|x9I;xa4(0`61J)G{ycO9Cq zC2@E`%OhZX>oW9$jqV}9hIzq8x2a>=P`qF>YO;G7dcj#)6X=X|13V;Rhixor#rwns z@Q#a~X{8s4S9rda-t(2*i5TnraNx;w;C5wXih#jwENN-#?!fI3t?+EDbj5h6mUx5% zh_KU-(W-rV41@t2NfJu?bU>+~3opDPidy1P`Vi$~igFQ9q@B3t@Vbh7fut2NSYDhy zV5{&V;%r!sYlMAw3=4<(iB`ny>b^S$AhmuAkakd{O*0gv2rUy5hP9$JmTa2g8raFy zlfBWOK>ZF_Z^ZK!-ws>81)a8MIdj+^xS*#Xo;cW~dp{oov(Y^~S z=TJ3v($-lC!mx@@hKaq{2K%qG5NeQRg^JySCa$c(Zn^o(3U-8s85o$g;zDsTyy8;U z3d%3kde|yLh5H0e?Ww_FymEF7xR8~D;s+~d{~!6m%9R?ZrRG|6gw=N{YDaQ}=#g7; zgw=P(a)gb51B>n**?b5LcIqT2q?Xdl!61ZkVjs*90ofo>~u_$u-mCx#kCb zfjT&Br`Ord@NKl9pBTnb_kiCFCwU*J_XW*?ns1nw`+^=gi09GobFRq6ldieNxpl!n zy*HRUId@5w&tFlOTl}>Zxp;gR9`Of$fkA!&&X@WseE}d;*ZcjRazC(w4BzBjMSO1Y ziiaPbJU|RRBs^>>R5PXu=njV-d9qQN_h=TI6c+zYa*R?Rs|$%2|XIE zzMrrHp~tCJL5V6NpS>Qs0OjO$9TUM^(-R1I8i6dI(Zo}w;980w#&TTw_%}Vx&E%r1 zC*&^j(Qj2i!miI97axWc{OFav9*_MER>q#Y#9!mV?j2`mZ5;Zm4Yr}9Ci?2vACB81 z`pq!Y?4Q*a8k`&KtZEW=m7%}d;21h;euA$i;41P#4F(X8vq&FYgufCoc(E$>)>H>Q z5c=l85GoCy@jJA9k|Rz?X2TE^N?gkXDqH$uB;1M^aeavzoS0cL*wvuP>OI~l$kdAO z%>>L|qK3LJtPO#ypdA8V*mg+3+2oI;m^a7cao(YK8!yh*WAsELQy6AWm4){e2lk!;&W=L+rb8qQGJXJvAOC%S3W3VRx(cPk0bt0Wnz{-Xwo*3mygp!#QzzZ!eIIw-Jz3W7OjoRi+61&yyiEa+#+ zJ8ZWs4;bYwIiuBVwVCb4yz=@ge}!>LVTIRMVJ)GhV|t%dpd_JZ9d(R24HT#Immx`~6p~Sa(v;FjX+&T$GlbwIlTId< zJbxCol%|v-D~%MX@6}TJ9uIkFBadc@Qi_(^NLdsys34;9e68iRKI&sx8tvQr+HVU4|wCG>0ZLh?88{^zPdt*FbxnU z$=}=@TT=+Eb;P3GEwOf^ae}F}Js}ne@ySS~j z{sR*jg)n>`Jq3X*A&bdhwr$%4V_dEZIb$lAh+y0amQpAd&1l=-_HjW#^lq22!0E=f zH7@#eLzlT^J~_7U`2`$-sYwJH_dSUOPI}ydX)_uFC+lCVzq7nF09{Xrq)L{1nScZTcI3P;I*Nx4(V`W@=WdC+mN!HhtrXU;W&Wqp7E}UTyk| z;Z3iX=4g72@u1rDX#datI59`lJuP)=(@p)a|1g&ZPCopS+VuDX58R)N*F`-Ou<2^D ziVQq^`X@zJB0-T(vnttu8vk8F3oJi1wC`m>5X&or?Jdy=sywttEbzwH(ARE!#=D{K zLefC`|M`^%Va793l}V(^POUt3Mlg#0`=&n5E7B4p%NwJ3u4-51wrM&55rEioewv zP-g4*fBE0v)UnwLZStC}SCb0zox@*$mmk+j)tn82V*pn|7Jw?=`R3N`20u6%@EGjWE4aN6ZR#1m9{Od*b{dphH}bLRwTvP&cniR9$Z_idi!2h9P}Eyob94m=||?Jungc0_EA-5n7`a@W*& zJuWxWnd$NQoE{|og~X~zypUWic*SXf8)c`dHTD2o)x=NUeRktdKyt>GHOkn+#$_gb zprV%x({4Lr%u!%{D&u2cIaj=tFrJsA?o>Hj>tfv0y|)Ru%bpnfyT z=yZE-aQbd=xf@-sTCb

vha@Ry8fM3guO9hr7zxU=^(1TBpxZ5yzIZAQ{^x`3UBM`j z8s%aq5YYc?BKgse9()hxH*=OF>%R*0U*+(5YMMZoWiuVs)t>pNe$aolqsHA(P2b&4 zhtCD?HfUbua#VX=iyMVS0URbsk@a!gl_U5yQ< zu&r+LbwXXybymeinI<~QFsd^eOPmn&p;_0Mm1--tSCNsFRa+k;9^LkL@ z)ed)!lWN@M@Or9H<6cL#YZ?7i?Q<=z2Gx6D{qlV@RWHD5=pa9P_FK=gtu9PdvJicV zUchBj4-bsm5<%R?V(@fb6tSl z=^CJ!9*W>n2y?E=)dUn~lR9$o+6w7sADJw^K|MKBR_w%t?uCvUB~Gt&4pQg;PR(ajK7Pq!`>Gs(EO z;p8fi{f)(}yl|gVSyOfpHBD)(u}wr1*L~cq8ELo<(?<^5HQ@{xbc4(((}w}l=vr%D zAV;aAP=)~`P9RJJOpSm6Lr18_=#djJ*%@RQOBVg5DGUNbHNMke82=x@SXR|4AyFpQ zl*ow_b=<7!A=d+WZHlpKOMjYx+9!^caSJ&5d8FZg zK&%<%_0=r@DneT&6S)bzonWy_4eSlh0G2707erP&pd+)D+IMuMNqIf4XSKZsGE1pF zroC=5lEb1>y}pnvQeHQh*z0K%S*Nx*TSV?qS_~DF9-Z=jri8VAbRt=-k}GAer?uDq zNvyV~jJ=-GUiVIBwHv0e*RxZ|7L|*OSCG3Dj0@$YS$RD{$O`544TDJlE5~ao9we0n51;gmSj`XRj*lcGlhCabw->hVf zpO{AC3bjkqS%Xau#@hvlrtgcN(%vsQnIwZQGOQ|NvzHMWs$#F_eGKrxO!A)EczrEv zapcqNb%&oURajd48K&_e?REDo20c8B5kC4^jk)8qNryt>&>YgM;+?2tW3sniYdsKP zt#{t2wSN32*|5R-hr8#Nban*ew=eGut%Bu~d<+);>viUm*0x|vWcjK{Ys;#HTs1+i z`dVu^u?mjRpi!}0gCW56!U8J|8lbwUBOY4~>6Uq6*b6{;LTl&3cqH5!N<^RrI~H_8 zVzH}J?SzdAI$GP?)$gTB?lBg>5%ZgBJM3Ec)gWJ21#qqHK(0L$>4dntV1Xals4|!h z(bmoc(FxF?FcOWlMcNajQbwPsc37T(#I#Brr7jFbgPonoHSz)@W;@8|h5I zz;$^?4AMOarCTsVhAO%}(!KnSV6-a&i71dyFzca4rE3C8M*0nI#t&OWCmfzfX+o7k9+qZjm&0_+tbcj%lxdCQWr8yboZ=(8fGOCzG!%=&MFLC$ zN5F54JfB86)zej*GMA`3Gy!xhdWY)k)G@qV#ufByfXH^lktiRZGydRa=;by*bOlbE zc#_UywG0RBn2@tn6$mCsA@MN)(MdX~PP)l%^2>=S`6&sNG)*O!Aby5yBCMW*AiW(X z<Zzc_b{Q_QwQZTg`he>e(AZc1nexewdWRpug$$lhxXfA4I z0P@m;LXr=YC{e#O8yff_F9-jC9R1}9J%fbanoBAK`#V5L)rS&YQ5uV-Civ#(GkE+v zP~~!a{1+bQ4C3_8BR2{5<8b(=GOUDbjMwA(9K61bG+vm8y!OJ)5S7=INRQI0m1aR3 zzYq4dnmKJ)3H>k4gL?w@V_bhV9Aq7jrFMJ>J8qqi9XG*|b&2Kr0>Ls@3mR#lsD@j~ zp&<}0m0l91!yoI!|7>2AEYLH2(-zBXRp^yDU zfmKf7$WJsLOz6lZ!7>BV?FN`uFt_kWkdC5MZ_Gy<+CZmlE<`?%)lfH>>@O4oDkfOv zC!%Vl45;T2weNF?dKyq=U9QTaOFmrILSzh$froNwy@;ZpHu-%em5|RhLt>X#=_OR@ zq0bS!U_T4zj3zp4`9aCS76nU4cK{ifpO_=Y^zDoFQb{FO;-Pz6K+4}yDTm;QB{y_R zK%Zny0PU65>7*W=eW)0~F%u2okd{Vpt;B=xYyr8qEI<{XfsRX#w2T|Im7vTJO6i%peOPjoHfDMBPnS7Wei=@TS8$`r3y8J{DwGxjw zgov+E5!b_Ef4+!lJ~HC^Qb5EEz5GN(t;8dqN5svar*@k+qMMWPG$La1Uw$H@R^k!w zT?L2-C}KYx_~a*}8*}xFjQx`U5ziImCn9Pk9&sxoo~MXCaHlda#5g~WJ!OE1M;7uE z5w#MJxCarpHBeI>1EMD58ARLzha&P55w#MJ_%tFuLlO5*&R0f$9QRJfafE{w`ANr- zm3YLL5b+{K+%P47L`}vGQveaqaO5W~~m3Zty#6G$Zhx83;#PC49kBJ_Dh6is*_Pvr-R-~d%43X)wjn51V z^;tw5ricSqV#Jm!=uz?>)a5LY7TO1{gg8!AVovbr4l=e#LiG+{a1~|p0?wq1c&q(!Sr8+5pc@M5?rKIj$yN(-SXCRZZ-It z;YH*g!M@I>j#{>;PGzI6JI!9K(l$}rE&>tzxKW#(g%!kTmBRzA@z)J7!z{;*hvIzWph001cLrF@@(ZT2urOn{a zE9~1Vtm-kf`U~;9a`L9uWWe8v_`{9FC)oS18_^P2_D{8hKCp!AGFXCk>YZi@?5Lc# zgafyOCG_8dmay5bsv)Z-u!Lh)OW16;AHIGRmN10kKe`w#VdxWM!A-M-p-ryo?#sC5U7D^6*TeF(Nz_yF zu}R<32HYk+C`vm-plaVUoentOaL7LO_aGy^OK`w9Io=mWm<8CRgL>&9J5UIW&`LbQ1w?q7B3yE^SR=CxEcZ=~7=w*V&Kyaj z4Q-&F3tz!tW6&j!9pr8@lU7%YIdJL+krOOh!y@$Z`_dOJcHbn;Rk2Gdynqk2fq;EY z)Kz&^3tE+%iyUj#X0QFQhZ-(kZOLx911Nm&t<+OhjRBUKsk&qhRoQ>%&0(flm#m}6 z$)Q`(QJwdxoMah>Y%A7zpM8&SY%A6|gzsC5E7pOTBUwS(g7uyVy)s*YdMP?JsA2bk zcq_PsRxr2}tzdnvs)TG-F!Fk}zSjOk%_yv(w;hCk<~Fo~BcC1vY?>7u`Lz8^?O3c} zCvx)6ZD<8M{A?KLSx=J3PRpU{q$Sc)=;NnXp6CO-30{Aq@4S)^AjaS_GF`B*{fxRY z%}vMPa;Q3*d|LY%@K$(rN`9iZ(n>tS2}F2%8T!MaTnH&SQyFClQM3#xqG%-^K1(E(pk&b>= zl_|a2?DN}Paz4&q19fL``aRpA=cvI~fUPn!$KH*bUXnRV2JT1*WCi zVJ^c}?kv(Ph>R|pdS2$D+*i~H2Ap1OCBdM>M%CFOgX>D*)-vI-SG^gbXf*EB~7&D1mo#>T^K z(go0TUUS&)N>^UZ*Ra-1%cFG5II|R&u8vR6p0|zON>;iVZ8iWoME+395Wft@A^wpa zhdhie`Ob5?;eF5!5*7*N>h#~67v-#zu~Cut`X%4l)(GSuX0V&yj0p1x8C^2_I;vq~ zD|+^YZ9HCFJ4j^QYwK%HXQ@TM^s4#TyR&=g<0et3IsErs$)nT-e+_=@^02+{xJi5% zRDypKgIDzP6$U-zcx*fQ?*JG&GmVV1lk*_s!B+pCV3eqgryMav@*f@KOZda!Y(BUs zNOMl=A4@QORpjmA|KXq#!IXr@W#FL3>s|0~RG3k*`J2(Fu&+60x0)olOg8k|-@oOn zqYs0l`p1mF9A~_?D6qd8WX3tlpm%Iem6^L!;seM4K^rlhrPE{DtQ@YpJwyIP}R z+sf*2C=zy6yF=cPYh`md67q#Y%^p{ERh4IEb5+pkiufX7U-LBhGb{L8>C5JyaPurd KSP1{~>Hi17{d}ST