From 3c3041e14c6de9c46eb700db2eff3622ef976934 Mon Sep 17 00:00:00 2001 From: Tobias Klockau Date: Mon, 28 Oct 2024 15:25:36 +0100 Subject: [PATCH 01/12] test: add tests for rail side validation --- .../validation/validate_rail_side/__init__.py | 3 + .../validate_rail_side/validate_rail_side.py | 22 ++ .../test_validate_rail_side.py | 205 ++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 raillabel_providerkit/validation/validate_rail_side/__init__.py create mode 100644 raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py create mode 100644 tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py diff --git a/raillabel_providerkit/validation/validate_rail_side/__init__.py b/raillabel_providerkit/validation/validate_rail_side/__init__.py new file mode 100644 index 0000000..3f71fbc --- /dev/null +++ b/raillabel_providerkit/validation/validate_rail_side/__init__.py @@ -0,0 +1,3 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 +"""Package for validating a scene for empty frames.""" diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py new file mode 100644 index 0000000..a2111ed --- /dev/null +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -0,0 +1,22 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from typing import List + +import raillabel + + +def validate_rail_side(scene: raillabel.Scene) -> List[str]: + """TODO. + + Parameters + ---------- + scene : raillabel.Scene + Scene, that should be validated. + + Returns + ------- + list[str] + list of all rail side errors in the scene. If an empty list is returned, then there are no + errors present. + """ diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py new file mode 100644 index 0000000..9792561 --- /dev/null +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -0,0 +1,205 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import raillabel + +from raillabel_providerkit.validation.validate_rail_side.validate_rail_side import ( + validate_rail_side, +) + + +def test_validate_rail_side__no_errors(empty_scene, empty_frame): + scene = empty_scene + scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + sensor = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={ + "railSide": "leftRail" + } + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={ + "railSide": "rightRail" + } + ) + + actual = validate_rail_side(scene) + assert len(actual) == 0 + + +def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): + scene = empty_scene + scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + sensor = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={ + "railSide": "rightRail" + } + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={ + "railSide": "leftRail" + } + ) + + actual = validate_rail_side(scene) + assert len(actual) == 1 + + +def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): + scene = empty_scene + scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + sensor = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={ + "railSide": "leftRail" + } + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={ + "railSide": "leftRail" + } + ) + + actual = validate_rail_side(scene) + assert len(actual) == 1 + + +def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): + scene = empty_scene + scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", + name="track0001", + type="track" + ) + sensor = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={ + "railSide": "rightRail" + } + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={ + "railSide": "rightRail" + } + ) + + actual = validate_rail_side(scene) + assert len(actual) == 1 + + +if __name__ == "__main__": + pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) From 91d68a1a21030187a85c218b2cbde04bd375098a Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Mon, 4 Nov 2024 11:18:22 +0100 Subject: [PATCH 02/12] lint: apply changes from new ruff pre-commit settings --- .../validate_rail_side/validate_rail_side.py | 5 +- .../test_validate_rail_side.py | 64 +++++-------------- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index a2111ed..04c7c87 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -1,12 +1,12 @@ # Copyright DB Netz AG and contributors # SPDX-License-Identifier: Apache-2.0 -from typing import List +from __future__ import annotations import raillabel -def validate_rail_side(scene: raillabel.Scene) -> List[str]: +def validate_rail_side(scene: raillabel.Scene) -> list[str]: """TODO. Parameters @@ -19,4 +19,5 @@ def validate_rail_side(scene: raillabel.Scene) -> List[str]: list[str] list of all rail side errors in the scene. If an empty list is returned, then there are no errors present. + """ diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index 9792561..6992e30 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -12,14 +12,10 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): scene = empty_scene scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) sensor = raillabel.format.Sensor( uid="rgb_center", @@ -35,9 +31,7 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): raillabel.format.Point2d(0, 1), ], closed=False, - attributes={ - "railSide": "leftRail" - } + attributes={"railSide": "leftRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", @@ -48,9 +42,7 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): raillabel.format.Point2d(1, 1), ], closed=False, - attributes={ - "railSide": "rightRail" - } + attributes={"railSide": "rightRail"}, ) actual = validate_rail_side(scene) @@ -60,14 +52,10 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): scene = empty_scene scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) sensor = raillabel.format.Sensor( uid="rgb_center", @@ -83,9 +71,7 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): raillabel.format.Point2d(0, 1), ], closed=False, - attributes={ - "railSide": "rightRail" - } + attributes={"railSide": "rightRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", @@ -96,9 +82,7 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): raillabel.format.Point2d(1, 1), ], closed=False, - attributes={ - "railSide": "leftRail" - } + attributes={"railSide": "leftRail"}, ) actual = validate_rail_side(scene) @@ -108,14 +92,10 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): scene = empty_scene scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) sensor = raillabel.format.Sensor( uid="rgb_center", @@ -131,9 +111,7 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): raillabel.format.Point2d(0, 1), ], closed=False, - attributes={ - "railSide": "leftRail" - } + attributes={"railSide": "leftRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", @@ -144,9 +122,7 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): raillabel.format.Point2d(1, 1), ], closed=False, - attributes={ - "railSide": "leftRail" - } + attributes={"railSide": "leftRail"}, ) actual = validate_rail_side(scene) @@ -156,14 +132,10 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): scene = empty_scene scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", - name="track0001", - type="track" + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) sensor = raillabel.format.Sensor( uid="rgb_center", @@ -179,9 +151,7 @@ def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): raillabel.format.Point2d(0, 1), ], closed=False, - attributes={ - "railSide": "rightRail" - } + attributes={"railSide": "rightRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", @@ -192,9 +162,7 @@ def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): raillabel.format.Point2d(1, 1), ], closed=False, - attributes={ - "railSide": "rightRail" - } + attributes={"railSide": "rightRail"}, ) actual = validate_rail_side(scene) From ce2548a4d4bda6c10c0628c2dbca1cce0c79f2c5 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Thu, 7 Nov 2024 10:12:33 +0100 Subject: [PATCH 03/12] feat: Implement filter_sensor_uids_by_type --- raillabel_providerkit/_util/_filters.py | 27 +++++++++ .../_util/test_filters.py | 59 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 raillabel_providerkit/_util/_filters.py create mode 100644 tests/test_raillabel_providerkit/_util/test_filters.py diff --git a/raillabel_providerkit/_util/_filters.py b/raillabel_providerkit/_util/_filters.py new file mode 100644 index 0000000..5b4a4b2 --- /dev/null +++ b/raillabel_providerkit/_util/_filters.py @@ -0,0 +1,27 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import raillabel + + +def filter_sensor_uids_by_type( + sensors: list[raillabel.format.Sensor], sensor_type: raillabel.format.SensorType +) -> set[str]: + """Get the uids of all given sensors matching the given SensorType. + + Parameters + ---------- + sensors : list[raillabel.format.Sensor] + The sensors to filter. + sensor_type : raillabel.format.SensorType + The SensorType to filter by. + + Returns + ------- + set[str] + The list of uids of matching sensors. + + """ + return {sensor.uid for sensor in sensors if sensor.type == sensor_type} diff --git a/tests/test_raillabel_providerkit/_util/test_filters.py b/tests/test_raillabel_providerkit/_util/test_filters.py new file mode 100644 index 0000000..b9bc5ab --- /dev/null +++ b/tests/test_raillabel_providerkit/_util/test_filters.py @@ -0,0 +1,59 @@ +# Copyright DB Netz AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +import raillabel + +from raillabel_providerkit._util._filters import filter_sensor_uids_by_type + + +@pytest.fixture +def sensor_types() -> list[raillabel.format.SensorType]: + return [sensor_type for sensor_type in raillabel.format.SensorType] + + +def test_filter_sensor_uids_by_type__empty(sensor_types): + sensors = [] + for sensor_type in sensor_types: + assert len(filter_sensor_uids_by_type(sensors, sensor_type)) == 0 + + +def test_filter_sensor_uids_by_type__exactly_one_match(sensor_types): + # Create a list of sensors where each sensor type occurs exactly once + sensors = [] + for i in range(len(sensor_types)): + sensors.append(raillabel.format.Sensor(uid=f"test_{i}", type=sensor_types[i])) + + # Ensure the filter works for each sensor type + for sensor_type in sensor_types: + results = filter_sensor_uids_by_type(sensors, sensor_type) + assert len(results) == 1 + # Assert the result is of correct type + matches = 0 + for sensor in sensors: + if sensor.uid in results: + assert sensor.type == sensor_type + matches += 1 + assert matches == len(results) + + +def test_filter_sensor_uids_by_type__multiple_matches(sensor_types): + # Create a list of sensors where each sensor type occurs three times + sensors = [] + i = 0 + for sensor_type in sensor_types: + for j in range(3): + sensors.append(raillabel.format.Sensor(uid=f"test_{i}", type=sensor_type)) + i += 1 + + # Ensure the filter works for each sensor type + for sensor_type in sensor_types: + results = filter_sensor_uids_by_type(sensors, sensor_type) + assert len(results) == 3 + # Assert the results are of correct type + matches = 0 + for sensor in sensors: + if sensor.uid in results: + assert sensor.type == sensor_type + matches += 1 + assert matches == len(results) From 637f80927e52e987f02786cdeebb65f37c53d62f Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Thu, 7 Nov 2024 11:28:30 +0100 Subject: [PATCH 04/12] test: Fix test_validate_rail_side scene creation --- .../test_validate_rail_side.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index 6992e30..9c1f2a9 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -11,16 +11,15 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): scene = empty_scene - scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) object = raillabel.format.Object( uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) + scene.objects[object.uid] = object sensor = raillabel.format.Sensor( uid="rgb_center", type=raillabel.format.SensorType.CAMERA, ) + scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", @@ -44,6 +43,7 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): closed=False, attributes={"railSide": "rightRail"}, ) + scene.frames[frame.uid] = frame actual = validate_rail_side(scene) assert len(actual) == 0 @@ -51,16 +51,15 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): scene = empty_scene - scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) object = raillabel.format.Object( uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) + scene.objects[object.uid] = object sensor = raillabel.format.Sensor( uid="rgb_center", type=raillabel.format.SensorType.CAMERA, ) + scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", @@ -84,6 +83,7 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): closed=False, attributes={"railSide": "leftRail"}, ) + scene.frames[frame.uid] = frame actual = validate_rail_side(scene) assert len(actual) == 1 @@ -91,16 +91,15 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): scene = empty_scene - scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) object = raillabel.format.Object( uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) + scene.objects[object.uid] = object sensor = raillabel.format.Sensor( uid="rgb_center", type=raillabel.format.SensorType.CAMERA, ) + scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", @@ -124,6 +123,7 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): closed=False, attributes={"railSide": "leftRail"}, ) + scene.frames[frame.uid] = frame actual = validate_rail_side(scene) assert len(actual) == 1 @@ -131,16 +131,15 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): scene = empty_scene - scene.objects["a1082ef9-555b-4b69-a888-7da531d8a2eb"] = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) object = raillabel.format.Object( uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" ) + scene.objects[object.uid] = object sensor = raillabel.format.Sensor( uid="rgb_center", type=raillabel.format.SensorType.CAMERA, ) + scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", @@ -164,6 +163,7 @@ def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): closed=False, attributes={"railSide": "rightRail"}, ) + scene.frames[frame.uid] = frame actual = validate_rail_side(scene) assert len(actual) == 1 From 13f85b3b513627c743be1f4efa4ade1138ca0597 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Thu, 7 Nov 2024 11:37:15 +0100 Subject: [PATCH 05/12] feat: Implement rail side count validation in validate_rail_side --- .../validate_rail_side/validate_rail_side.py | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index 04c7c87..60b71bb 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -5,9 +5,11 @@ import raillabel +from raillabel_providerkit._util._filters import filter_sensor_uids_by_type + def validate_rail_side(scene: raillabel.Scene) -> list[str]: - """TODO. + """Validate whether all tracks have <= one left and right rail, and that they have correct order. Parameters ---------- @@ -21,3 +23,65 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: errors present. """ + errors: list[str] = [] + + # Get a list of camera uids + cameras = filter_sensor_uids_by_type( + list(scene.sensors.values()), raillabel.format.SensorType.CAMERA + ) + + # Filter scene for track annotations in camera sensors + filtered_scene = raillabel.filter(scene, include_object_types=["track"], include_sensors=cameras) + + # Check per frame + for frame_uid, frame in filtered_scene.frames.items(): + # Count rails per track + counts_per_track = _count_rails_per_track_in_frame(frame) + + # Add errors if there is more than one left or right rail + for object_uid, (left_count, right_count) in counts_per_track.items(): + if left_count > 1: + errors.append( + f"In frame {frame_uid}, the track with object_uid {object_uid} " + f"has more than one ({left_count}) left rail." + ) + if right_count > 1: + errors.append( + f"In frame {frame_uid}, the track with object_uid {object_uid} " + f"has more than one ({right_count}) right rail." + ) + + return errors + + +def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, tuple[int, int]]: + # For each track, the left and right rail counts are stored as a tuple (left, right) + counts: dict[str, tuple[int, int]] = {} + + # For each track, count the left and right rails + for object_uid, _annotations in frame.object_data.items(): + # Ensure we work only on Poly2d annotations + poly2ds: list[raillabel.format.Poly2d] = [ + annotation + for annotation in _annotations.values() + if isinstance(annotation, raillabel.format.Poly2d) + ] + + # Count left and right rails + left_count: int = 0 + right_count: int = 0 + for poly2d in poly2ds: + match poly2d.attributes["railSide"]: + case "leftRail": + left_count += 1 + case "rightRail": + right_count += 1 + case _: + # NOTE: This is ignored because it is covered by validate_onthology + continue + + # Store counts of current track + counts[object_uid] = (left_count, right_count) + + # Return results + return counts From 168a3e3792cd2bfa06eb6b7736b3aae3783b5720 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Fri, 8 Nov 2024 11:26:36 +0100 Subject: [PATCH 06/12] test: make sure validate_rail_side differentiates sensors --- .../test_validate_rail_side.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index 9c1f2a9..e46cff5 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -169,5 +169,117 @@ def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): assert len(actual) == 1 +def test_validate_rail_side__two_sensors_with_two_right_rails_each(empty_scene, empty_frame): + scene = empty_scene + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" + ) + scene.objects[object.uid] = object + sensor1 = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + sensor2 = raillabel.format.Sensor( + uid="ir_center", + type=raillabel.format.SensorType.CAMERA, + ) + for sensor in [sensor1, sensor2]: + scene.sensors[sensor.uid] = sensor + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor1, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor1, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + frame.annotations["f6db5b28-bdcd-437f-bf39-c044bb516de8"] = raillabel.format.Poly2d( + uid="f6db5b28-bdcd-437f-bf39-c044bb516de8", + object=object, + sensor=sensor2, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + frame.annotations["89f8cf2c-1dc9-4956-9661-f1054ff069f9"] = raillabel.format.Poly2d( + uid="89f8cf2c-1dc9-4956-9661-f1054ff069f9", + object=object, + sensor=sensor2, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + scene.frames[frame.uid] = frame + + actual = validate_rail_side(scene) + assert len(actual) == 2 + + +def test_validate_rail_side__two_sensors_with_one_right_rail_each(empty_scene, empty_frame): + scene = empty_scene + object = raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" + ) + scene.objects[object.uid] = object + sensor1 = raillabel.format.Sensor( + uid="rgb_center", + type=raillabel.format.SensorType.CAMERA, + ) + sensor2 = raillabel.format.Sensor( + uid="ir_center", + type=raillabel.format.SensorType.CAMERA, + ) + for sensor in [sensor1, sensor2]: + scene.sensors[sensor.uid] = sensor + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor1, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + frame.annotations["f6db5b28-bdcd-437f-bf39-c044bb516de8"] = raillabel.format.Poly2d( + uid="f6db5b28-bdcd-437f-bf39-c044bb516de8", + object=object, + sensor=sensor2, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + scene.frames[frame.uid] = frame + + actual = validate_rail_side(scene) + assert len(actual) == 0 + + if __name__ == "__main__": pytest.main([__file__, "--disable-pytest-warnings", "--cache-clear", "-v"]) From 826617b47565b9710019a745afa2d61ba71c674c Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Fri, 8 Nov 2024 11:31:14 +0100 Subject: [PATCH 07/12] fix: validate_rail_side differentiates between sensors --- .../validate_rail_side/validate_rail_side.py | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index 60b71bb..1ec3478 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -30,26 +30,30 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: list(scene.sensors.values()), raillabel.format.SensorType.CAMERA ) - # Filter scene for track annotations in camera sensors - filtered_scene = raillabel.filter(scene, include_object_types=["track"], include_sensors=cameras) - - # Check per frame - for frame_uid, frame in filtered_scene.frames.items(): - # Count rails per track - counts_per_track = _count_rails_per_track_in_frame(frame) - - # Add errors if there is more than one left or right rail - for object_uid, (left_count, right_count) in counts_per_track.items(): - if left_count > 1: - errors.append( - f"In frame {frame_uid}, the track with object_uid {object_uid} " - f"has more than one ({left_count}) left rail." - ) - if right_count > 1: - errors.append( - f"In frame {frame_uid}, the track with object_uid {object_uid} " - f"has more than one ({right_count}) right rail." - ) + # Check per camera + for camera in cameras: + # Filter scene for track annotations in the selected camera sensor + filtered_scene = raillabel.filter( + scene, include_object_types=["track"], include_sensors=[camera] + ) + + # Check per frame + for frame_uid, frame in filtered_scene.frames.items(): + # Count rails per track + counts_per_track = _count_rails_per_track_in_frame(frame) + + # Add errors if there is more than one left or right rail + for object_uid, (left_count, right_count) in counts_per_track.items(): + if left_count > 1: + errors.append( + f"In sensor {camera} frame {frame_uid}, the track with" + f" object_uid {object_uid} has more than one ({left_count}) left rail." + ) + if right_count > 1: + errors.append( + f"In sensor {camera} frame {frame_uid}, the track with" + f" object_uid {object_uid} has more than one ({right_count}) right rail." + ) return errors From 6aee2b8eadcc213015fdc55f835ff85c605de810 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Fri, 8 Nov 2024 11:57:15 +0100 Subject: [PATCH 08/12] test: check _count_rails_per_track_in_frame --- .../test_validate_rail_side.py | 213 +++++++++++++----- 1 file changed, 158 insertions(+), 55 deletions(-) diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index e46cff5..dc5b26f 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -6,19 +6,143 @@ from raillabel_providerkit.validation.validate_rail_side.validate_rail_side import ( validate_rail_side, + _count_rails_per_track_in_frame, ) -def test_validate_rail_side__no_errors(empty_scene, empty_frame): - scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) - scene.objects[object.uid] = object - sensor = raillabel.format.Sensor( +@pytest.fixture +def example_camera_1() -> raillabel.format.Sensor: + return raillabel.format.Sensor( uid="rgb_center", type=raillabel.format.SensorType.CAMERA, ) + + +@pytest.fixture +def example_camera_2() -> raillabel.format.Sensor: + return raillabel.format.Sensor( + uid="ir_center", + type=raillabel.format.SensorType.CAMERA, + ) + + +@pytest.fixture +def example_track_1() -> raillabel.format.Object: + return raillabel.format.Object( + uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" + ) + + +@pytest.fixture +def example_track_2() -> raillabel.format.Object: + return raillabel.format.Object( + uid="6e92e7af-3bc8-4225-b538-16d19e3f8aa7", name="track0002", type="track" + ) + + +def test_count_rails_per_track_in_frame__empty(empty_frame): + frame = empty_frame + results = _count_rails_per_track_in_frame(frame) + assert len(results) == 0 + + +def test_count_rails_per_track_in_frame__many_rails_for_one_track( + empty_frame, example_camera_1, example_track_1 +): + frame = empty_frame + sensor = example_camera_1 + object = example_track_1 + + LEFT_COUNT = 32 + RIGHT_COUNT = 42 + + for i in range(LEFT_COUNT): + uid = f"test_left_{i}" + frame.annotations[uid] = raillabel.format.Poly2d( + uid=uid, + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "leftRail"}, + ) + + for i in range(RIGHT_COUNT): + uid = f"test_right_{i}" + frame.annotations[uid] = raillabel.format.Poly2d( + uid=uid, + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + + results = _count_rails_per_track_in_frame(frame) + assert len(results) == 1 + assert object.uid in results.keys() + assert results[object.uid] == (LEFT_COUNT, RIGHT_COUNT) + + +def test_count_rails_per_track_in_frame__many_rails_for_two_tracks( + empty_frame, example_camera_1, example_track_1, example_track_2 +): + frame = empty_frame + sensor = example_camera_1 + object1 = example_track_1 + object2 = example_track_2 + + LEFT_COUNT = 32 + RIGHT_COUNT = 42 + + for object in [object1, object2]: + for i in range(LEFT_COUNT): + uid = f"test_left_{i}_object_{object.uid}" + frame.annotations[uid] = raillabel.format.Poly2d( + uid=uid, + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(0, 1), + ], + closed=False, + attributes={"railSide": "leftRail"}, + ) + + for i in range(RIGHT_COUNT): + uid = f"test_right_{i}_object_{object.uid}" + frame.annotations[uid] = raillabel.format.Poly2d( + uid=uid, + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + + results = _count_rails_per_track_in_frame(frame) + assert len(results) == 2 + assert object1.uid in results.keys() + assert object2.uid in results.keys() + assert results[object1.uid] == (LEFT_COUNT, RIGHT_COUNT) + assert results[object2.uid] == (LEFT_COUNT, RIGHT_COUNT) + + +def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_1, example_track_1): + scene = empty_scene + object = example_track_1 + scene.objects[object.uid] = object + sensor = example_camera_1 scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( @@ -49,16 +173,13 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame): assert len(actual) == 0 -def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): +def test_validate_rail_side__rail_sides_switched( + empty_scene, empty_frame, example_camera_1, example_track_1 +): scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) + object = example_track_1 scene.objects[object.uid] = object - sensor = raillabel.format.Sensor( - uid="rgb_center", - type=raillabel.format.SensorType.CAMERA, - ) + sensor = example_camera_1 scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( @@ -89,16 +210,13 @@ def test_validate_rail_side__rail_sides_switched(empty_scene, empty_frame): assert len(actual) == 1 -def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): +def test_validate_rail_side__two_left_rails( + empty_scene, empty_frame, example_camera_1, example_track_1 +): scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) + object = example_track_1 scene.objects[object.uid] = object - sensor = raillabel.format.Sensor( - uid="rgb_center", - type=raillabel.format.SensorType.CAMERA, - ) + sensor = example_camera_1 scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( @@ -129,16 +247,13 @@ def test_validate_rail_side__two_left_rails(empty_scene, empty_frame): assert len(actual) == 1 -def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): +def test_validate_rail_side__two_right_rails( + empty_scene, empty_frame, example_camera_1, example_track_1 +): scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) + object = example_track_1 scene.objects[object.uid] = object - sensor = raillabel.format.Sensor( - uid="rgb_center", - type=raillabel.format.SensorType.CAMERA, - ) + sensor = example_camera_1 scene.sensors[sensor.uid] = sensor frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( @@ -169,20 +284,14 @@ def test_validate_rail_side__two_right_rails(empty_scene, empty_frame): assert len(actual) == 1 -def test_validate_rail_side__two_sensors_with_two_right_rails_each(empty_scene, empty_frame): +def test_validate_rail_side__two_sensors_with_two_right_rails_each( + empty_scene, empty_frame, example_camera_1, example_camera_2, example_track_1 +): scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) + object = example_track_1 scene.objects[object.uid] = object - sensor1 = raillabel.format.Sensor( - uid="rgb_center", - type=raillabel.format.SensorType.CAMERA, - ) - sensor2 = raillabel.format.Sensor( - uid="ir_center", - type=raillabel.format.SensorType.CAMERA, - ) + sensor1 = example_camera_1 + sensor2 = example_camera_2 for sensor in [sensor1, sensor2]: scene.sensors[sensor.uid] = sensor frame = empty_frame @@ -236,20 +345,14 @@ def test_validate_rail_side__two_sensors_with_two_right_rails_each(empty_scene, assert len(actual) == 2 -def test_validate_rail_side__two_sensors_with_one_right_rail_each(empty_scene, empty_frame): +def test_validate_rail_side__two_sensors_with_one_right_rail_each( + empty_scene, empty_frame, example_camera_1, example_camera_2, example_track_1 +): scene = empty_scene - object = raillabel.format.Object( - uid="a1082ef9-555b-4b69-a888-7da531d8a2eb", name="track0001", type="track" - ) + object = example_track_1 scene.objects[object.uid] = object - sensor1 = raillabel.format.Sensor( - uid="rgb_center", - type=raillabel.format.SensorType.CAMERA, - ) - sensor2 = raillabel.format.Sensor( - uid="ir_center", - type=raillabel.format.SensorType.CAMERA, - ) + sensor1 = example_camera_1 + sensor2 = example_camera_2 for sensor in [sensor1, sensor2]: scene.sensors[sensor.uid] = sensor frame = empty_frame From 6102dd115bb7ef22acfae5e73677da36d51eddd1 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Mon, 11 Nov 2024 13:56:06 +0100 Subject: [PATCH 09/12] feat: Implement rail side order check --- .../validate_rail_side/validate_rail_side.py | 69 +++++++++++++++++-- .../test_validate_rail_side.py | 47 +++++++++++++ 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index 1ec3478..94f4a09 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -3,6 +3,7 @@ from __future__ import annotations +import numpy as np import raillabel from raillabel_providerkit._util._filters import filter_sensor_uids_by_type @@ -42,6 +43,9 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: # Count rails per track counts_per_track = _count_rails_per_track_in_frame(frame) + # Find rail x limits per track + track_limits_per_track = _get_track_limits_in_frame(frame) + # Add errors if there is more than one left or right rail for object_uid, (left_count, right_count) in counts_per_track.items(): if left_count > 1: @@ -55,6 +59,18 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: f" object_uid {object_uid} has more than one ({right_count}) right rail." ) + # If left and right rails exist, check if the track has its rails swapped + if left_count >= 1 and right_count >= 1: + # Add errors if any track has its rails swapped + (max_x_of_left, min_x_of_right) = track_limits_per_track[object_uid] + if max_x_of_left > min_x_of_right: + errors.append( + f"In sensor {camera} frame {frame_uid}, the track with" + f" object_uid {object_uid} has its rails swapped." + f" The right-most left rail has x={max_x_of_left} while" + f" the left-most right rail has x={min_x_of_right}." + ) + return errors @@ -63,13 +79,11 @@ def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, counts: dict[str, tuple[int, int]] = {} # For each track, count the left and right rails - for object_uid, _annotations in frame.object_data.items(): + for object_uid, unfiltered_annotations in frame.object_data.items(): # Ensure we work only on Poly2d annotations - poly2ds: list[raillabel.format.Poly2d] = [ - annotation - for annotation in _annotations.values() - if isinstance(annotation, raillabel.format.Poly2d) - ] + poly2ds: list[raillabel.format.Poly2d] = _filter_for_poly2ds( + list(unfiltered_annotations.values()) + ) # Count left and right rails left_count: int = 0 @@ -89,3 +103,46 @@ def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, # Return results return counts + + +def _get_track_limits_in_frame(frame: raillabel.format.Frame) -> dict[str, tuple[float, float]]: + # For each track, the largest x of any left rail and the smallest x of any right rail is stored + # as a tuple (max_x_of_left, min_x_of_right) + track_limits: dict[str, tuple[float, float]] = {} + + for object_uid, unfiltered_annotations in frame.object_data.items(): + # Ensure we work only on Poly2d annotations + poly2ds: list[raillabel.format.Poly2d] = _filter_for_poly2ds( + list(unfiltered_annotations.values()) + ) + + # Get the largest x of any left rail and the smallest x of any right rail + max_x_of_left: float = float("-inf") + min_x_of_right: float = float("inf") + for poly2d in poly2ds: + rail_x_values: list[float] = [point.x for point in poly2d.points] + match poly2d.attributes["railSide"]: + case "leftRail": + max_x_of_rail_points: float = np.max(rail_x_values) + max_x_of_left = max(max_x_of_rail_points, max_x_of_left) + case "rightRail": + min_x_of_rail_points: float = np.min(rail_x_values) + min_x_of_right = min(min_x_of_rail_points, min_x_of_right) + case _: + # NOTE: This is ignored because it is covered by validate_onthology + continue + + # Store the calculated limits of current track + track_limits[object_uid] = (max_x_of_left, min_x_of_right) + + return track_limits + + +def _filter_for_poly2ds( + unfiltered_annotations: list[type[raillabel.format._ObjectAnnotation]], +) -> list[raillabel.format.Poly2d]: + return [ + annotation + for annotation in unfiltered_annotations + if isinstance(annotation, raillabel.format.Poly2d) + ] diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index dc5b26f..06f5e09 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -7,6 +7,8 @@ from raillabel_providerkit.validation.validate_rail_side.validate_rail_side import ( validate_rail_side, _count_rails_per_track_in_frame, + _get_track_limits_in_frame, + _filter_for_poly2ds, ) @@ -138,6 +140,51 @@ def test_count_rails_per_track_in_frame__many_rails_for_two_tracks( assert results[object2.uid] == (LEFT_COUNT, RIGHT_COUNT) +def test_get_track_limits_in_frame__empty(empty_frame): + frame = empty_frame + results = _get_track_limits_in_frame(frame) + assert len(results) == 0 + + +def test_get_track_limits_in_frame__one_track_two_rails( + empty_frame, example_camera_1, example_track_1 +): + frame = empty_frame + sensor = example_camera_1 + object = example_track_1 + + MAX_X_OF_LEFT = 42 + MIN_X_OF_RIGHT = 73 + + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(0, 0), + raillabel.format.Point2d(MAX_X_OF_LEFT, 1), + ], + closed=False, + attributes={"railSide": "leftRail"}, + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(1000, 0), + raillabel.format.Point2d(MIN_X_OF_RIGHT, 1), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + + results = _get_track_limits_in_frame(frame) + assert len(results) == 1 + assert object.uid in results.keys() + assert results[object.uid] == (MAX_X_OF_LEFT, MIN_X_OF_RIGHT) + + def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_1, example_track_1): scene = empty_scene object = example_track_1 From 4840ffda80df59e59012fcdd45f4013e2554578f Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Mon, 18 Nov 2024 10:55:13 +0100 Subject: [PATCH 10/12] feat: Implement more thorough rail side validation --- .../validate_rail_side/validate_rail_side.py | 263 ++++++++++++++---- .../test_validate_rail_side.py | 104 ++++--- 2 files changed, 285 insertions(+), 82 deletions(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index 94f4a09..e791bd1 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -43,9 +43,6 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: # Count rails per track counts_per_track = _count_rails_per_track_in_frame(frame) - # Find rail x limits per track - track_limits_per_track = _get_track_limits_in_frame(frame) - # Add errors if there is more than one left or right rail for object_uid, (left_count, right_count) in counts_per_track.items(): if left_count > 1: @@ -59,21 +56,66 @@ def validate_rail_side(scene: raillabel.Scene) -> list[str]: f" object_uid {object_uid} has more than one ({right_count}) right rail." ) - # If left and right rails exist, check if the track has its rails swapped - if left_count >= 1 and right_count >= 1: - # Add errors if any track has its rails swapped - (max_x_of_left, min_x_of_right) = track_limits_per_track[object_uid] - if max_x_of_left > min_x_of_right: - errors.append( - f"In sensor {camera} frame {frame_uid}, the track with" - f" object_uid {object_uid} has its rails swapped." - f" The right-most left rail has x={max_x_of_left} while" - f" the left-most right rail has x={min_x_of_right}." - ) + # If exactly one left and right rail exists, check if the track has its rails swapped + # or intersects with itself + if left_count == 1 == right_count: + # Get the two annotations in question + left_rail: raillabel.format.Poly2d | None = _get_track_from_frame( + frame, object_uid, "leftRail" + ) + right_rail: raillabel.format.Poly2d | None = _get_track_from_frame( + frame, object_uid, "rightRail" + ) + if left_rail is None or right_rail is None: + continue + + swap_error: str | None = _check_rails_for_swap(left_rail, right_rail, frame_uid) + if swap_error is not None: + errors.append(swap_error) return errors +def _check_rails_for_swap( + left_rail: raillabel.format.Poly2d, + right_rail: raillabel.format.Poly2d, + frame_uid: str | int = "unknown", +) -> str | None: + # Ensure the rails belong to the same track + if left_rail.object.uid != right_rail.object.uid: + return None + + max_common_y = _find_max_common_y(left_rail, right_rail) + if max_common_y is None: + return None + + left_x = _find_x_by_y(max_common_y, left_rail) + right_x = _find_x_by_y(max_common_y, right_rail) + if left_x is None or right_x is None: + return None + + object_uid = left_rail.object.uid + sensor_uid = left_rail.sensor.uid if left_rail.sensor is not None else "unknown" + + if left_x >= right_x: + return ( + f"In sensor {sensor_uid} frame {frame_uid}, the track with" + f" object_uid {object_uid} has its rails swapped." + f" At the maximum common y={max_common_y}, the left rail has x={left_x}" + f" while the right rail has x={right_x}." + ) + + intersect_interval = _find_intersect_interval(left_rail, right_rail) + if intersect_interval is not None: + return ( + f"In sensor {sensor_uid} frame {frame_uid}, the track with" + f" object_uid {object_uid} intersects with itself." + f" The left and right rail intersect in y interval {intersect_interval}." + ) + + return None + + def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, tuple[int, int]]: # For each track, the left and right rail counts are stored as a tuple (left, right) counts: dict[str, tuple[int, int]] = {} @@ -105,39 +147,6 @@ def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, return counts -def _get_track_limits_in_frame(frame: raillabel.format.Frame) -> dict[str, tuple[float, float]]: - # For each track, the largest x of any left rail and the smallest x of any right rail is stored - # as a tuple (max_x_of_left, min_x_of_right) - track_limits: dict[str, tuple[float, float]] = {} - - for object_uid, unfiltered_annotations in frame.object_data.items(): - # Ensure we work only on Poly2d annotations - poly2ds: list[raillabel.format.Poly2d] = _filter_for_poly2ds( - list(unfiltered_annotations.values()) - ) - - # Get the largest x of any left rail and the smallest x of any right rail - max_x_of_left: float = float("-inf") - min_x_of_right: float = float("inf") - for poly2d in poly2ds: - rail_x_values: list[float] = [point.x for point in poly2d.points] - match poly2d.attributes["railSide"]: - case "leftRail": - max_x_of_rail_points: float = np.max(rail_x_values) - max_x_of_left = max(max_x_of_rail_points, max_x_of_left) - case "rightRail": - min_x_of_rail_points: float = np.min(rail_x_values) - min_x_of_right = min(min_x_of_rail_points, min_x_of_right) - case _: - # NOTE: This is ignored because it is covered by validate_onthology - continue - - # Store the calculated limits of current track - track_limits[object_uid] = (max_x_of_left, min_x_of_right) - - return track_limits - - def _filter_for_poly2ds( unfiltered_annotations: list[type[raillabel.format._ObjectAnnotation]], ) -> list[raillabel.format.Poly2d]: @@ -146,3 +155,163 @@ def _filter_for_poly2ds( for annotation in unfiltered_annotations if isinstance(annotation, raillabel.format.Poly2d) ] + + +def _find_intersect_interval( + line1: raillabel.format.Poly2d, line2: raillabel.format.Poly2d +) -> tuple[float, float] | None: + # If the two polylines intersect anywhere, return the y interval where they intersect. + + # Get all y values where either polyline has points + y_values: list[float] = sorted( + _get_y_of_all_points_of_poly2d(line1).union(_get_y_of_all_points_of_poly2d(line2)) + ) + + order: bool | None = None + last_y: float | None = None + for y in y_values: + x1 = _find_x_by_y(y, line1) + x2 = _find_x_by_y(y, line2) + + if x1 is None or x2 is None: + order = None + continue + + if x1 == x2: + return (y, y) + + new_order = x1 < x2 + + if order is not None and new_order != order and last_y is not None: + # The order has flipped. There is an intersection between previous and current y + return (last_y, y) + + order = new_order + last_y = y + + return None + + +def _find_max_y(poly2d: raillabel.format.Poly2d) -> float: + return np.max([point.y for point in poly2d.points]) + + +def _find_max_common_y( + line1: raillabel.format.Poly2d, line2: raillabel.format.Poly2d +) -> float | None: + if len(line1.points) == 0 or len(line2.points) == 0: + # One of the lines is empty + return None + + max_y_of_line1: float = _find_max_y(line1) + if _y_in_poly2d(max_y_of_line1, line2): + # The highest y is the bottom of line 1 + return max_y_of_line1 + + max_y_of_line2: float = _find_max_y(line2) + if _y_in_poly2d(max_y_of_line2, line1): + # The highest y is the bottom of line 2 + return max_y_of_line2 + + # There is no y overlap + return None + + +def _find_x_by_y(y: float, poly2d: raillabel.format.Poly2d) -> float | None: + """Find the x value of the first point where the polyline passes through y. + + Parameters + ---------- + y : float + The y value to check. + poly2d : raillabel.format.Poly2d + The Poly2D whose points will be checked against. + + Returns + ------- + float | None + The x value of a point (x,y) that poly2d passes through, + or None if poly2d doesn't go through y. + + """ + # 1. Find the first two points between which y is located + points = poly2d.points + p1: raillabel.format.Point2d | None = None + p2: raillabel.format.Point2d | None = None + for i in range(len(points) - 1): + current = points[i] + next_ = points[i + 1] + if (current.y >= y >= next_.y) or (current.y <= y <= next_.y): + p1 = current + p2 = next_ + break + + # 2. Abort if no valid points have been found + if not (p1 and p2): + return None + + # 3. Return early if p1=p2 (to avoid division by zero) + if p1.x == p2.x: + return p1.x + + # 4. Calculate m and n for the line g(x)=mx+n connecting p1 and p2 + m = (p2.y - p1.y) / (p2.x - p1.x) + n = p1.y - (m * p1.x) + + # 5. Return early if m is 0, as that means p2.y=p1.y, which implies p2.y=p1.y=y + if m == 0: + return p1.x + + # 6. Calculate the x we were searching for and return it + return (y - n) / m + + +def _get_track_from_frame( + frame: raillabel.format.Frame, object_uid: str, rail_side: str +) -> raillabel.format.Poly2d | None: + if object_uid not in frame.object_data: + return None + + for annotation in frame.object_data[object_uid].values(): + if not isinstance(annotation, raillabel.format.Poly2d): + continue + + if "railSide" not in annotation.attributes: + continue + + if annotation.attributes["railSide"] == rail_side: + return annotation + + return None + + +def _get_y_of_all_points_of_poly2d(poly2d: raillabel.format.Poly2d) -> set[float]: + y_values: set[float] = set() + for point in poly2d.points: + y_values.add(point.y) + return y_values + + +def _y_in_poly2d(y: float, poly2d: raillabel.format.Poly2d) -> bool: + """Check whether the polyline created by the given Poly2d passes through the given y value. + + Parameters + ---------- + y : float + The y value to check. + poly2d : raillabel.format.Poly2d + The Poly2D whose points will be checked against. + + Returns + ------- + bool + Does the Poly2d pass through the given y value? + + """ + # For every point (except the last), check if the y is between them + for i in range(len(poly2d.points) - 1): + current = poly2d.points[i] + next_ = poly2d.points[i + 1] + if (current.y >= y >= next_.y) or (current.y <= y <= next_.y): + return True + return False diff --git a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py index 06f5e09..eb053be 100644 --- a/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py +++ b/tests/test_raillabel_providerkit/validation/validate_rail_side/test_validate_rail_side.py @@ -7,8 +7,6 @@ from raillabel_providerkit.validation.validate_rail_side.validate_rail_side import ( validate_rail_side, _count_rails_per_track_in_frame, - _get_track_limits_in_frame, - _filter_for_poly2ds, ) @@ -140,29 +138,20 @@ def test_count_rails_per_track_in_frame__many_rails_for_two_tracks( assert results[object2.uid] == (LEFT_COUNT, RIGHT_COUNT) -def test_get_track_limits_in_frame__empty(empty_frame): - frame = empty_frame - results = _get_track_limits_in_frame(frame) - assert len(results) == 0 - - -def test_get_track_limits_in_frame__one_track_two_rails( - empty_frame, example_camera_1, example_track_1 -): - frame = empty_frame - sensor = example_camera_1 +def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_1, example_track_1): + scene = empty_scene object = example_track_1 - - MAX_X_OF_LEFT = 42 - MIN_X_OF_RIGHT = 73 - + scene.objects[object.uid] = object + sensor = example_camera_1 + scene.sensors[sensor.uid] = sensor + frame = empty_frame frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", object=object, sensor=sensor, points=[ raillabel.format.Point2d(0, 0), - raillabel.format.Point2d(MAX_X_OF_LEFT, 1), + raillabel.format.Point2d(0, 1), ], closed=False, attributes={"railSide": "leftRail"}, @@ -172,20 +161,21 @@ def test_get_track_limits_in_frame__one_track_two_rails( object=object, sensor=sensor, points=[ - raillabel.format.Point2d(1000, 0), - raillabel.format.Point2d(MIN_X_OF_RIGHT, 1), + raillabel.format.Point2d(1, 0), + raillabel.format.Point2d(1, 1), ], closed=False, attributes={"railSide": "rightRail"}, ) + scene.frames[frame.uid] = frame - results = _get_track_limits_in_frame(frame) - assert len(results) == 1 - assert object.uid in results.keys() - assert results[object.uid] == (MAX_X_OF_LEFT, MIN_X_OF_RIGHT) + actual = validate_rail_side(scene) + assert len(actual) == 0 -def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_1, example_track_1): +def test_validate_rail_side__rail_sides_switched( + empty_scene, empty_frame, example_camera_1, example_track_1 +): scene = empty_scene object = example_track_1 scene.objects[object.uid] = object @@ -201,7 +191,7 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_ raillabel.format.Point2d(0, 1), ], closed=False, - attributes={"railSide": "leftRail"}, + attributes={"railSide": "rightRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", @@ -212,15 +202,15 @@ def test_validate_rail_side__no_errors(empty_scene, empty_frame, example_camera_ raillabel.format.Point2d(1, 1), ], closed=False, - attributes={"railSide": "rightRail"}, + attributes={"railSide": "leftRail"}, ) scene.frames[frame.uid] = frame actual = validate_rail_side(scene) - assert len(actual) == 0 + assert len(actual) == 1 -def test_validate_rail_side__rail_sides_switched( +def test_validate_rail_side__rail_sides_intersect_at_top( empty_scene, empty_frame, example_camera_1, example_track_1 ): scene = empty_scene @@ -234,22 +224,26 @@ def test_validate_rail_side__rail_sides_switched( object=object, sensor=sensor, points=[ - raillabel.format.Point2d(0, 0), - raillabel.format.Point2d(0, 1), + raillabel.format.Point2d(20, 0), + raillabel.format.Point2d(20, 10), + raillabel.format.Point2d(10, 20), + raillabel.format.Point2d(10, 100), ], closed=False, - attributes={"railSide": "rightRail"}, + attributes={"railSide": "leftRail"}, ) frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( uid="be7d136a-8364-4fbd-b098-6f4a21205d22", object=object, sensor=sensor, points=[ - raillabel.format.Point2d(1, 0), - raillabel.format.Point2d(1, 1), + raillabel.format.Point2d(10, 0), + raillabel.format.Point2d(10, 10), + raillabel.format.Point2d(20, 20), + raillabel.format.Point2d(20, 100), ], closed=False, - attributes={"railSide": "leftRail"}, + attributes={"railSide": "rightRail"}, ) scene.frames[frame.uid] = frame @@ -257,6 +251,46 @@ def test_validate_rail_side__rail_sides_switched( assert len(actual) == 1 +def test_validate_rail_side__rail_sides_correct_with_early_end_of_one_side( + empty_scene, empty_frame, example_camera_1, example_track_1 +): + scene = empty_scene + object = example_track_1 + scene.objects[object.uid] = object + sensor = example_camera_1 + scene.sensors[sensor.uid] = sensor + frame = empty_frame + frame.annotations["325b1f55-a2ef-475f-a780-13e1a9e823c3"] = raillabel.format.Poly2d( + uid="325b1f55-a2ef-475f-a780-13e1a9e823c3", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(70, 0), + raillabel.format.Point2d(30, 20), + raillabel.format.Point2d(15, 40), + raillabel.format.Point2d(10, 50), + raillabel.format.Point2d(10, 100), + ], + closed=False, + attributes={"railSide": "leftRail"}, + ) + frame.annotations["be7d136a-8364-4fbd-b098-6f4a21205d22"] = raillabel.format.Poly2d( + uid="be7d136a-8364-4fbd-b098-6f4a21205d22", + object=object, + sensor=sensor, + points=[ + raillabel.format.Point2d(20, 50), + raillabel.format.Point2d(20, 100), + ], + closed=False, + attributes={"railSide": "rightRail"}, + ) + scene.frames[frame.uid] = frame + + actual = validate_rail_side(scene) + assert len(actual) == 0 + + def test_validate_rail_side__two_left_rails( empty_scene, empty_frame, example_camera_1, example_track_1 ): From 1342aae28fab8279a7ae62a6e2d892045ae727cc Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Mon, 18 Nov 2024 11:22:18 +0100 Subject: [PATCH 11/12] docs: Correct validate_rail_side package description --- raillabel_providerkit/validation/validate_rail_side/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/raillabel_providerkit/validation/validate_rail_side/__init__.py b/raillabel_providerkit/validation/validate_rail_side/__init__.py index 3f71fbc..ea61820 100644 --- a/raillabel_providerkit/validation/validate_rail_side/__init__.py +++ b/raillabel_providerkit/validation/validate_rail_side/__init__.py @@ -1,3 +1,3 @@ # Copyright DB Netz AG and contributors # SPDX-License-Identifier: Apache-2.0 -"""Package for validating a scene for empty frames.""" +"""Package for validating a scene for rail side errors.""" From a702f6cdd62b54d3b6fbe3b71e80bb64cab346b8 Mon Sep 17 00:00:00 2001 From: Niklas Freund Date: Mon, 18 Nov 2024 11:42:41 +0100 Subject: [PATCH 12/12] fix: compatibility with Python 3.8 --- pyproject.toml | 3 ++- .../validate_rail_side/validate_rail_side.py | 16 ++++++++-------- .../_util/test_filters.py | 2 ++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d89728e..49e3b29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ dependencies = [ "jsonschema>=4.4.0", "fastjsonschema>=2.16.2", "raillabel>=3.1.0, <4.0.0", - "pyyaml>=6.0.0" + "pyyaml>=6.0.0", + "numpy>=1.24.4", ] [project.urls] diff --git a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py index e791bd1..d01bd76 100644 --- a/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py +++ b/raillabel_providerkit/validation/validate_rail_side/validate_rail_side.py @@ -131,14 +131,14 @@ def _count_rails_per_track_in_frame(frame: raillabel.format.Frame) -> dict[str, left_count: int = 0 right_count: int = 0 for poly2d in poly2ds: - match poly2d.attributes["railSide"]: - case "leftRail": - left_count += 1 - case "rightRail": - right_count += 1 - case _: - # NOTE: This is ignored because it is covered by validate_onthology - continue + rail_side = poly2d.attributes["railSide"] + if rail_side == "leftRail": + left_count += 1 + elif rail_side == "rightRail": + right_count += 1 + else: + # NOTE: This is ignored because it is covered by validate_onthology + continue # Store counts of current track counts[object_uid] = (left_count, right_count) diff --git a/tests/test_raillabel_providerkit/_util/test_filters.py b/tests/test_raillabel_providerkit/_util/test_filters.py index b9bc5ab..c6c812b 100644 --- a/tests/test_raillabel_providerkit/_util/test_filters.py +++ b/tests/test_raillabel_providerkit/_util/test_filters.py @@ -1,6 +1,8 @@ # Copyright DB Netz AG and contributors # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import pytest import raillabel