Skip to content

Commit

Permalink
feat: add key_line_3d and key_point_3d annotation objects and structu…
Browse files Browse the repository at this point in the history
…res (#135)
nehalmamgain authored Dec 7, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 1791c88 commit 5fa6002
Showing 12 changed files with 1,668 additions and 0 deletions.
4 changes: 4 additions & 0 deletions dgp/annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
from dgp.annotations.panoptic_segmentation_2d_annotation import PanopticSegmentation2DAnnotation # isort:skip
from dgp.annotations.semantic_segmentation_2d_annotation import SemanticSegmentation2DAnnotation # isort:skip
from dgp.annotations.key_line_2d_annotation import KeyLine2DAnnotationList # isort:skip
from dgp.annotations.key_line_3d_annotation import KeyLine3DAnnotationList # isort:skip
from dgp.annotations.key_point_2d_annotation import KeyPoint2DAnnotationList # isort:skip
from dgp.annotations.depth_annotation import DenseDepthAnnotation # isort:skip

@@ -31,7 +32,9 @@
"instance_segmentation_2d": InstanceSegmentationOntology,
"instance_segmentation_3d": InstanceSegmentationOntology,
"key_point_2d": KeyPointOntology,
"key_point_3d": KeyPointOntology,
"key_line_2d": KeyLineOntology,
"key_line_3d": KeyLineOntology,
"agent_behavior": AgentBehaviorOntology,
"depth": None,
"surface_normals_2d": None,
@@ -48,6 +51,7 @@
"instance_segmentation_2d": PanopticSegmentation2DAnnotation,
"key_point_2d": KeyPoint2DAnnotationList,
"key_line_2d": KeyLine2DAnnotationList,
"key_line_3d": KeyLine3DAnnotationList,
"depth": DenseDepthAnnotation
}

140 changes: 140 additions & 0 deletions dgp/annotations/key_line_3d_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Copyright 2022 Woven Planet. All rights reserved.
import numpy as np

from dgp.annotations.base_annotation import Annotation
from dgp.annotations.ontology import KeyLineOntology
from dgp.proto.annotations_pb2 import KeyLine3DAnnotation, KeyLine3DAnnotations
from dgp.utils.protobuf import (
generate_uid_from_pbobject,
open_pbobject,
save_pbobject_as_json,
)
from dgp.utils.structures.key_line_3d import KeyLine3D
from dgp.utils.structures.key_point_3d import KeyPoint3D


class KeyLine3DAnnotationList(Annotation):
"""Container for 3D keyline annotations.
Parameters
----------
ontology: KeyLineOntology
Ontology for 3D keyline tasks.
linelist: list[KeyLine3D]
List of KeyLine3D objects. See `dgp/utils/structures/key_line_3d` for more details.
"""
def __init__(self, ontology, linelist):
super().__init__(ontology)
assert isinstance(self._ontology, KeyLineOntology), "Trying to load annotation with wrong type of ontology!"
for line in linelist:
assert isinstance(
line, KeyLine3D
), f"Can only instantate an annotation from a list of KeyLine3D, not {type(line)}"
self._linelist = linelist

@classmethod
def load(cls, annotation_file, ontology):
"""Load annotation from annotation file and ontology.
Parameters
----------
annotation_file: str
Full path to annotation
ontology: KeyLineOntology
Ontology for 3D keyline tasks.
Returns
-------
KeyLine3DAnnotationList
Annotation object instantiated from file.
"""
_annotation_pb2 = open_pbobject(annotation_file, KeyLine3DAnnotations)
linelist = [
KeyLine3D(
line=np.float32([[vertex.x, vertex.y, vertex.z] for vertex in ann.vertices]),
class_id=ontology.class_id_to_contiguous_id[ann.class_id],
color=ontology.colormap[ann.class_id],
attributes=getattr(ann, "attributes", {}),
) for ann in _annotation_pb2.annotations
]
return cls(ontology, linelist)

def to_proto(self):
"""Return annotation as pb object.
Returns
-------
KeyLine3DAnnotations
Annotation as defined in `proto/annotations.proto`
"""
return KeyLine3DAnnotations(
annotations=[
KeyLine3DAnnotation(
class_id=self._ontology.contiguous_id_to_class_id[line.class_id],
vertices=[
KeyPoint3D(
point=np.float32([x, y, z]),
class_id=line.class_id,
instance_id=line.instance_id,
color=line.color,
attributes=line.attributes
).to_proto() for x, y, z in zip(line.x, line.y, line.z)
],
attributes=line.attributes
) for line in self._linelist
]
)

def save(self, save_dir):
"""Serialize Annotation object and saved to specified directory. Annotations are saved in format <save_dir>/<sha>.<ext>
Parameters
----------
save_dir: str
Directory in which annotation is saved.
Returns
-------
output_annotation_file: str
Full path to saved annotation.
"""
return save_pbobject_as_json(self.to_proto(), save_path=save_dir)

def __len__(self):
return len(self._linelist)

def __getitem__(self, index):
"""Return a single 3D keyline"""
return self._linelist[index]

def render(self):
"""Batch rendering function for keylines."""
raise NotImplementedError

@property
def xyz(self):
"""Return lines as (N, 3) np.ndarray in format ([x, y, z])"""
return np.array([line.xyz.tolist() for line in self._linelist], dtype=np.float32)

@property
def class_ids(self):
"""Return class ID for each line, with ontology applied:
class IDs mapped to a contiguous set.
"""
return np.array([line.class_id for line in self._linelist], dtype=np.int64)

@property
def attributes(self):
"""Return a list of dictionaries of attribute name to value."""
return [line.attributes for line in self._linelist]

@property
def instance_ids(self):
return np.array([line.instance_id for line in self._linelist], dtype=np.int64)

@property
def hexdigest(self):
"""Reproducible hash of annotation."""
return generate_uid_from_pbobject(self.to_proto())
134 changes: 134 additions & 0 deletions dgp/annotations/key_point_3d_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright 2022 Woven Planet. All rights reserved.
import numpy as np

from dgp.annotations.base_annotation import Annotation
from dgp.annotations.ontology import KeyPointOntology
from dgp.proto.annotations_pb2 import (
KeyPoint3DAnnotation,
KeyPoint3DAnnotations,
)
from dgp.utils.protobuf import (
generate_uid_from_pbobject,
open_pbobject,
save_pbobject_as_json,
)
from dgp.utils.structures.key_point_3d import KeyPoint3D


class KeyPoint3DAnnotationList(Annotation):
"""Container for 3D keypoint annotations.
Parameters
----------
ontology: KeyPointOntology
Ontology for 3D keypoint tasks.
pointlist: list[KeyPoint3D]
List of KeyPoint3D objects. See `dgp/utils/structures/key_point_3d` for more details.
"""
def __init__(self, ontology, pointlist):
super().__init__(ontology)
assert isinstance(self._ontology, KeyPointOntology), "Trying to load annotation with wrong type of ontology!"
for point in pointlist:
assert isinstance(
point, KeyPoint3D
), f"Can only instantate an annotation from a list of KeyPoint3D, not {type(point)}"
self._pointlist = pointlist

@classmethod
def load(cls, annotation_file, ontology):
"""Load annotation from annotation file and ontology.
Parameters
----------
annotation_file: str
Full path to annotation
ontology: KeyPointOntology
Ontology for 3D keypoint tasks.
Returns
-------
KeyPoint3DAnnotationList
Annotation object instantiated from file.
"""
_annotation_pb2 = open_pbobject(annotation_file, KeyPoint3DAnnotations)
pointlist = [
KeyPoint3D(
point=np.float32([ann.point.x, ann.point.y, ann.point.z]),
class_id=ontology.class_id_to_contiguous_id[ann.class_id],
color=ontology.colormap[ann.class_id],
attributes=getattr(ann, "attributes", {}),
) for ann in _annotation_pb2.annotations
]
return cls(ontology, pointlist)

def to_proto(self):
"""Return annotation as pb object.
Returns
-------
KeyPoint3DAnnotations
Annotation as defined in `proto/annotations.proto`
"""
return KeyPoint3DAnnotations(
annotations=[
KeyPoint3DAnnotation(
class_id=self._ontology.contiguous_id_to_class_id[point.class_id],
point=point.to_proto(),
attributes=point.attributes
) for point in self._pointlist
]
)

def save(self, save_dir):
"""Serialize Annotation object and saved to specified directory. Annotations are saved in format <save_dir>/<sha>.<ext>
Parameters
----------
save_dir: str
Directory in which annotation is saved.
Returns
-------
output_annotation_file: str
Full path to saved annotation
"""
return save_pbobject_as_json(self.to_proto(), save_path=save_dir)

def __len__(self):
return len(self._pointlist)

def __getitem__(self, index):
"""Return a single 3D keypoint"""
return self._pointlist[index]

def render(self):
"""Batch rendering function for keypoints."""
raise NotImplementedError

@property
def xyz(self):
"""Return points as (N, 3) np.ndarray in format ([x, y, z])"""
return np.array([point.xyz for point in self._pointlist], dtype=np.float32)

@property
def class_ids(self):
"""Return class ID for each point, with ontology applied:
0 is background, class IDs mapped to a contiguous set.
"""
return np.array([point.class_id for point in self._pointlist], dtype=np.int64)

@property
def attributes(self):
"""Return a list of dictionaries of attribut name to value."""
return [point.attributes for point in self._pointlist]

@property
def instance_ids(self):
return np.array([point.instance_id for point in self._pointlist], dtype=np.int64)

@property
def hexdigest(self):
"""Reproducible hash of annotation."""
return generate_uid_from_pbobject(self.to_proto())
100 changes: 100 additions & 0 deletions dgp/utils/structures/key_line_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright 2022 Woven Planet. All rights reserved.
import hashlib

import numpy as np

import dgp.proto.annotations_pb2 as annotations_pb2

GENERIC_OBJECT_CLASS_ID = 1


class KeyLine3D:
"""3D keyline object.
Parameters
----------
line: np.ndarray[np.float32]
Array of (N,3) floats describing keyline coordinates [x, y, z].
class_id: int, default: GENERIC_OBJECT_CLASS_ID
Integer class ID
instance_id: int, default: None
Unique instance ID for keyline. If None provided, the ID is a hash of the keyline
location and class.
color: tuple, default: (0, 0, 0)
RGB tuple for keyline color
attributes: dict, default: None
Dictionary of attributes associated with keyline. If None provided,
defaults to empty dict.
"""
def __init__(self, line, class_id=GENERIC_OBJECT_CLASS_ID, instance_id=None, color=(0, 0, 0), attributes=None):
assert line.dtype in (np.float32, np.float64)
assert line.shape[1] == 3

self._point = line
self.x = []
self.y = []
self.z = []
for point in line:
self.x.append(point[0])
self.y.append(point[1])
self.z.append(point[2])

self._class_id = class_id
self._instance_id = instance_id
self._color = color
self._attributes = dict(attributes) if attributes is not None else {}

@property
def xyz(self):
return np.array([self.x, self.y, self.z], dtype=np.float32)

@property
def class_id(self):
return self._class_id

@property
def instance_id(self):
if self._instance_id is None:
return self.hexdigest
return self._instance_id

@property
def color(self):
return self._color

@property
def attributes(self):
return self._attributes

@property
def hexdigest(self):
return hashlib.md5(self.xyz.tobytes() + bytes(self._class_id)).hexdigest()

def __repr__(self):
return "{}[{}, Class: {}, Attributes: {}]".format(
self.__class__.__name__, list(self.xyz), self.class_id, self.attributes
)

def __eq__(self, other):
return self.hexdigest == other.hexdigest

def to_proto(self):
"""Serialize keyline to proto object.
Does not serialize class or instance information, just line geometry.
To serialize a complete annotation, see `dgp/annotations/key_line_3d_annotation.py`
Returns
-------
KeyLine3D.pb2
As defined in `proto/annotations.proto`
"""
return [
annotations_pb2.KeyPoint3D(x=int(self.x[j]), y=int(self.y[j]), z=int(self.z[j]))
for j, _ in enumerate(self.x)
]
94 changes: 94 additions & 0 deletions dgp/utils/structures/key_point_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright 2022 Woven Planet. All rights reserved.
import hashlib

import numpy as np

import dgp.proto.annotations_pb2 as annotations_pb2

GENERIC_OBJECT_CLASS_ID = 1


class KeyPoint3D:
"""3D keypoint object.
Parameters
----------
point: np.ndarray[np.float32]
Array of 3 floats describing keypoint coordinates [x, y, z].
class_id: int, default: GENERIC_OBJECT_CLASS_ID
Integer class ID (0 reserved for background).
instance_id: int, default: None
Unique instance ID for keypoint. If None provided, the ID is a hash of the keypoint
location and class.
color: tuple, default: (0, 0, 0)
RGB tuple for keypoint color
attributes: dict, default: None
Dictionary of attributes associated with keypoint. If None provided,
defaults to empty dict.
"""
def __init__(self, point, class_id=GENERIC_OBJECT_CLASS_ID, instance_id=None, color=(0, 0, 0), attributes=None):
assert point.dtype in (np.float32, np.float64)
assert point.shape[0] == 3

self._point = point

self.x = point[0]
self.y = point[1]
self.z = point[2]

self._class_id = class_id
self._instance_id = instance_id
self._color = color
self._attributes = dict(attributes) if attributes is not None else {}

@property
def xyz(self):
return np.array([self.x, self.y, self.z], dtype=np.float32)

@property
def class_id(self):
return self._class_id

@property
def instance_id(self):
if self._instance_id is None:
return self.hexdigest
return self._instance_id

@property
def color(self):
return self._color

@property
def attributes(self):
return self._attributes

@property
def hexdigest(self):
return hashlib.md5(self.xyz.tobytes() + bytes(self._class_id)).hexdigest()

def __repr__(self):
return "{}[{}, Class: {}, Attributes: {}]".format(
self.__class__.__name__, list(self.xyz), self.class_id, self.attributes
)

def __eq__(self, other):
return self.hexdigest == other.hexdigest

def to_proto(self):
"""Serialize keypoint to proto object.
Does not serialize class or instance information, just point geometry.
To serialize a complete annotation, see `dgp/annotations/key_point_3d_annotation.py`
Returns
-------
KeyPoint3D.pb2
As defined in `proto/annotations.proto`
"""
return annotations_pb2.KeyPoint3D(x=int(self.x), y=int(self.y), z=int(self.z))
68 changes: 68 additions & 0 deletions tests/annotation/test_key_line_3d_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os

import numpy as np
import pytest

from dgp.annotations.key_line_3d_annotation import KeyLine3DAnnotationList
from dgp.datasets.synchronized_dataset import SynchronizedSceneDataset
from dgp.utils.structures.key_line_3d import KeyLine3D
from tests import TEST_DATA_DIR


def get_ontology_kl(scene_dataset_json, annotation_type):
dataset = SynchronizedSceneDataset(
scene_dataset_json,
split='train',
datum_names=['lcm_25tm'],
backward_context=0,
requested_annotations=("key_line_3d", )
)
return dataset.dataset_metadata.ontology_table.get(annotation_type, None)


@pytest.fixture
def kl_ontology():
DGP_TEST_DATASET_DIR = os.path.join(TEST_DATA_DIR, "dgp")
scenes_dataset_json = os.path.join(DGP_TEST_DATASET_DIR, "key_line_3d", "scene_dataset.json")
return get_ontology_kl(scene_dataset_json=scenes_dataset_json, annotation_type="key_line_3d")


def test_kl3d_annotation(kl_ontology):
keylines = [KeyLine3D(np.array([[i + j, i + 1, i + 2] for i in range(5)], dtype=np.float32)) for j in range(5)]
annotation_list = KeyLine3DAnnotationList(kl_ontology, keylines)
assert len(annotation_list.xyz) == 5


def test_kl3d_load(kl_ontology):
DGP_TEST_DATASET_DIR = os.path.join(TEST_DATA_DIR, "dgp")
expected_output = "ac354"
scenes_dataset_json = os.path.join(
DGP_TEST_DATASET_DIR,
"key_line_3d/scene_000000/key_line_3d/lcm_25tm/000000000000000005_21e2436af96fb6388eb0c64cc029cfdc928a3e95.json"
)
kl3d_list = KeyLine3DAnnotationList.load(scenes_dataset_json, kl_ontology)
assert kl3d_list.hexdigest[0:5] == expected_output


def test_kl3d_proto(kl_ontology):
DGP_TEST_DATASET_DIR = os.path.join(TEST_DATA_DIR, "dgp")
scenes_dataset_json = os.path.join(
DGP_TEST_DATASET_DIR,
"key_line_3d/scene_000000/key_line_3d/lcm_25tm/000000000000000005_21e2436af96fb6388eb0c64cc029cfdc928a3e95.json"
)
kl3d_list = KeyLine3DAnnotationList.load(scenes_dataset_json, kl_ontology)
output_proto = kl3d_list.to_proto()
assert output_proto.__sizeof__() == 80


def test_kl3d_save(kl_ontology):
DGP_TEST_DATASET_DIR = os.path.join(TEST_DATA_DIR, "dgp")
scenes_dataset_json = os.path.join(
DGP_TEST_DATASET_DIR,
"key_line_3d/scene_000000/key_line_3d/lcm_25tm/000000000000000005_21e2436af96fb6388eb0c64cc029cfdc928a3e95.json"
)
kl3d_list = KeyLine3DAnnotationList.load(scenes_dataset_json, kl_ontology)
kl3d_list.save(".")
filepath = "./ac35449091ebdd374aaa743be74794db561ec86a.json"
assert os.path.exists(filepath)
os.remove(filepath)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"names": [
"lcm_25tm"
],
"intrinsics": [
{
"fx": 1283.641357421875,
"fy": 1283.641357421875,
"cx": 639.5,
"cy": 383.5,
"skew": 0.0
}
],
"extrinsics": [
{
"translation": {
"x": 0.4555000066757202,
"y": -0.003000000026077032,
"z": 1.3170000314712524
},
"rotation": {
"qx": -0.48292216221687323,
"qy": 0.48292216221687323,
"qz": -0.5165134898913871,
"qw": 0.516513489891387
}
}
]
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"items": [
{
"name": "ego_line",
"color": {
"r": 255,
"g": 153,
"b": 0
},
"isthing": true,
"id": 0,
"supercategory": ""
},
{
"name": "ego_line_extrapolated_to_horizon",
"id": 1,
"color": {
"r": 255,
"g": 0,
"b": 0
},
"isthing": true,
"supercategory": ""
},
{
"name": "adjacent1_left",
"id": 2,
"color": {
"g": 255,
"r": 0,
"b": 0
},
"isthing": true,
"supercategory": ""
},
{
"name": "adjacent1_right",
"id": 3,
"color": {
"g": 255,
"r": 0,
"b": 0
},
"isthing": true,
"supercategory": ""
},
{
"name": "adjacent2_left",
"id": 4,
"color": {
"r": 255,
"g": 255,
"b": 0
},
"isthing": true,
"supercategory": ""
},
{
"name": "adjacent2_right",
"id": 5,
"color": {
"r": 255,
"g": 255,
"b": 0
},
"isthing": true,
"supercategory": ""
},
{
"name": "other_ego_line",
"id": 6,
"color": {
"r": 46,
"g": 181,
"b": 140
},
"isthing": true,
"supercategory": ""
},
{
"name": "other_line",
"id": 7,
"color": {
"r": 244,
"g": 204,
"b": 204
},
"isthing": true,
"supercategory": ""
},
{
"name": "line_break_due_to_grad",
"id": 8,
"color": {
"b": 255,
"r": 0,
"g": 0
},
"isthing": true,
"supercategory": ""
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"name": "scene_000000",
"samples": [
{
"id": {
"timestamp": "2022-11-10T01:00:27.752Z",
"log": "",
"name": "",
"index": "0"
},
"datum_keys": [
"2ea9448ddf12dd58062335f3ec1889d87b0ef6202211707cf351f90b4a6ab83b"
],
"calibration_key": "0a6700143aaff8364a37e018e0768085b0564e47",
"metadata": {}
}
],
"metadata": {
"TOTORO": {
"@type": "type.googleapis.com/dgp.proto.ParallelDomainSceneMetadata",
"location": "motorway_37_673810_-122_389930_0_006000",
"time_of_day": "DAYTIME",
"fog_intensity": 0.45,
"rain_intensity": 0.9,
"wetness": 0.9,
"version": 1,
"cloud_cover": 0.8,
"attributes": {
"is_curved_highway": "True"
},
"region_type": "SYNTHETIC_REGION",
"scene_type": "SYNTHETIC_SCENE",
"sun_elevation": 0.0,
"sun_azimuth": 0.0,
"street_lights": 0.0,
"batch_id": 0
}
},
"data": [
{
"id": {
"name": "lcm_25tm",
"timestamp": "2022-11-10T01:00:27.752Z",
"log": "",
"index": "0"
},
"key": "2ea9448ddf12dd58062335f3ec1889d87b0ef6202211707cf351f90b4a6ab83b",
"datum": {
"image": {
"filename": "rgb/lcm_25tm/000000000000000005.png",
"height": 768,
"width": 1280,
"channels": 4,
"annotations": {
"17": "key_line_3d/lcm_25tm/000000000000000005_21e2436af96fb6388eb0c64cc029cfdc928a3e95.json"
},
"metadata": {
"TOTORO": {
"@type": "type.googleapis.com/dgp.proto.ParallelDomainSampleMetadata",
"attributes": {
"type_of_scene": "NORMAL",
"lane_change": "False",
"elevation_variation": "False",
"road": "HIGHWAY",
"sensor_id": "lcm_25tm",
"branching_in_view": "False",
"merging_in_view": "False",
"ego_merging": "False",
"road_radius": "0.0",
"partition_0": "PD"
}
}
},
"pose": {
"translation": {
"x": -110.28236389160156,
"y": -1078.1033935546875,
"z": 8.57644271850586
},
"rotation": {
"qx": -0.6510899207948903,
"qy": 0.1869702528681094,
"qz": -0.2021379216584355,
"qw": 0.7072936449660401
}
}
}
},
"next_key": "",
"prev_key": ""
}
],
"ontologies": {
"17": "a5b601b09ac109191fdbdc4a21fa18dfbe69b1a1"
},
"description": "",
"log": ""
}
22 changes: 22 additions & 0 deletions tests/data/dgp/key_line_3d/scene_dataset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"metadata": {
"name": "nehal-mamgain",
"version": "1.0",
"creation_date": "2022-12-06T00:44:56.237604Z",
"creator": "Woven Planet",
"origin": "INTERNAL",
"available_annotation_types": [
17
],
"metadata": {},
"description": "",
"frame_per_second": 0.0
},
"scene_splits": {
"0": {
"filenames": [
"scene_000000/scene_23d1725bfb52a43491ce3609073ce4d75b7785a5.json"
]
}
}
}
35 changes: 35 additions & 0 deletions tests/utils/structures/test_key_line_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import numpy as np
import pytest
from numpy.lib.ufunclike import fix

from dgp.utils.structures.key_line_3d import KeyLine3D


@pytest.fixture
def keyline():
k = np.float32([[.5, 2, -1], [-4, 0, 3], [0, -1, 2], [.25, 1.25, -.25], [100, 1, 200]])
return KeyLine3D(k)


def test_keyline_class_id(keyline):
assert keyline.class_id == 1


def test_keyline_instance_id(keyline):
assert keyline.instance_id == '6b144d77fb6c1f915f56027b4fe34f5e'


def test_keyline_color(keyline):
assert keyline.color == (0, 0, 0)


def test_keyline_attributes(keyline):
assert keyline.attributes == {}


def test_keyline_hexdigest(keyline):
assert keyline.hexdigest == '6b144d77fb6c1f915f56027b4fe34f5e'


def test_keyline_to_proto(keyline):
assert len(keyline.to_proto()) == 5

0 comments on commit 5fa6002

Please sign in to comment.