Skip to content

Commit 2ba87f7

Browse files
fix(roadmarkings): replace triangle list with cube primitive, add axis arrows (#183)
* fix: align example trace and tests with OSI road marking spec Closes #114 The example trace (MovingHostWithStopLine.mcap) used the standard vehicle coordinate system for the road marking, but the OSI spec defines a different local frame for road markings: - local x = surface normal (upward from ground) - local y = lateral - local z = driving direction (bottom-to-top of marking image) Fixed the trace to use spec-compliant values: - dimension: length=0.004 (protrusion), width=4.0 (lateral), height=0.3 (driving dir) - orientation: roll=0, pitch=-pi/2, yaw=pi (rotates local frame correctly) Updated tests to use realistic spec-compliant default values. Expanded code comment with full coordinate system explanation and spec link. * fix(roadmarkings): use spec-compliant trace dimensions and add rotation test * fix: use correct road marking orientation in example file * fix: add road marking orientation variation example --------- Signed-off-by: jdsika <carlo.van-driesten@bmw.de> Signed-off-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de> Co-authored-by: Thomas Sedlmayer <tsedlmayer@pmsfit.de>
1 parent 8bf208c commit 2ba87f7

6 files changed

Lines changed: 291 additions & 35 deletions

File tree

-234 Bytes
Binary file not shown.
76.1 KB
Binary file not shown.

src/converters/groundTruth/sceneUpdateConverter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function buildSceneEntities(
154154
PREFIX_ROAD_MARKING,
155155
OSI_GLOBAL_FRAME,
156156
time,
157+
panelSettings,
157158
);
158159

159160
if (result != undefined) {

src/features/roadmarkings/index.ts

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Point3 } from "@foxglove/schemas";
1+
import { GroundTruthPanelSettings } from "@converters";
2+
import { ArrowPrimitive } from "@foxglove/schemas";
23
import { RoadMarking, TrafficSign_MainSign_Classification_Type } from "@lichtblick/asam-osi-types";
34
import { Time } from "@lichtblick/suite";
4-
import { pointListToTriangleListPrimitive } from "@utils/primitives/lines";
5+
import { buildObjectAxes, objectToCubePrimitive } from "@utils/primitives/objects";
56
import { generateSceneEntityId, PartialSceneEntity } from "@utils/scene";
67
import { DeepRequired } from "ts-essentials";
78

@@ -14,6 +15,7 @@ export function buildRoadMarkingEntity(
1415
id_prefix: string,
1516
frame_id: string,
1617
time: Time,
18+
config: GroundTruthPanelSettings | undefined,
1719
): PartialSceneEntity | undefined {
1820
if (
1921
roadMarking.classification.traffic_main_sign_type !==
@@ -22,45 +24,41 @@ export function buildRoadMarkingEntity(
2224
return undefined;
2325
}
2426

25-
const roadMarkingPoints = [
26-
{
27-
position: {
28-
x: roadMarking.base.position.x,
29-
y: roadMarking.base.position.y,
30-
z: roadMarking.base.position.z,
31-
} as Point3,
32-
width: roadMarking.base.dimension.width,
33-
height: roadMarking.base.dimension.height,
34-
},
35-
{
36-
position: {
37-
x: roadMarking.base.position.x + roadMarking.base.dimension.length,
38-
y: roadMarking.base.position.y,
39-
z: roadMarking.base.position.z,
40-
} as Point3,
41-
width: roadMarking.base.dimension.width,
42-
height: roadMarking.base.dimension.height,
43-
},
44-
];
27+
const pos = roadMarking.base.position;
28+
const ori = roadMarking.base.orientation;
29+
const dim = roadMarking.base.dimension;
4530

46-
// Define color and opacity based on OSI classification
47-
const rgb = ROAD_MARKING_COLOR[roadMarking.classification.monochrome_color];
48-
const color = { r: rgb.r, g: rgb.g, b: rgb.b, a: 1 };
31+
// Road markings use BaseStationary, same as other objects. The orientation
32+
// quaternion handles whatever local-frame convention the trace author used.
33+
// See: https://opensimulationinterface.github.io/osi-antora-generator/asamosi/latest/gen/structosi3_1_1RoadMarking.html
34+
const cube = objectToCubePrimitive(
35+
pos.x,
36+
pos.y,
37+
pos.z,
38+
ori.roll,
39+
ori.pitch,
40+
ori.yaw,
41+
dim.width,
42+
dim.length,
43+
dim.height,
44+
{ ...ROAD_MARKING_COLOR[roadMarking.classification.monochrome_color], a: 1 },
45+
);
4946

50-
// Set option for dashed lines
51-
const options = {
52-
dashed: false,
53-
arrows: false,
54-
invertArrows: false,
55-
};
47+
function buildAxes(): ArrowPrimitive[] {
48+
if (!(config?.showAxes ?? false)) {
49+
return [];
50+
}
51+
return buildObjectAxes(roadMarking);
52+
}
5653

5754
return {
5855
timestamp: time,
5956
frame_id,
6057
id: generateSceneEntityId(id_prefix, roadMarking.id.value),
6158
lifetime: { sec: 0, nsec: 0 },
6259
frame_locked: true,
63-
triangles: [pointListToTriangleListPrimitive(roadMarkingPoints, color, options)],
60+
cubes: [cube],
61+
arrows: buildAxes(),
6462
metadata: buildRoadMarkingMetadata(roadMarking),
6563
};
6664
}

src/utils/primitives/objects.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
*/
88

99
import { Color, CubePrimitive, ModelPrimitive, Vector3, ArrowPrimitive } from "@foxglove/schemas";
10-
import { StationaryObject, MovingObject, TrafficLight } from "@lichtblick/asam-osi-types";
10+
import {
11+
StationaryObject,
12+
MovingObject,
13+
TrafficLight,
14+
RoadMarking,
15+
} from "@lichtblick/asam-osi-types";
1116
import { ColorCode } from "@utils/helper";
1217
import { eulerToQuaternion, quaternionMultiplication } from "@utils/math";
1318
import { DeepRequired } from "ts-essentials";
@@ -81,7 +86,8 @@ export function buildAxisArrow(
8186
osiObject:
8287
| DeepRequired<StationaryObject>
8388
| DeepRequired<MovingObject>
84-
| DeepRequired<TrafficLight>,
89+
| DeepRequired<TrafficLight>
90+
| DeepRequired<RoadMarking>,
8591
axis_color: Color,
8692
orientation: Vector3 = { x: 0, y: 0, z: 0 },
8793
shaft_length: number,
@@ -118,7 +124,8 @@ export function buildObjectAxes(
118124
osiObject:
119125
| DeepRequired<StationaryObject>
120126
| DeepRequired<MovingObject>
121-
| DeepRequired<TrafficLight>,
127+
| DeepRequired<TrafficLight>
128+
| DeepRequired<RoadMarking>,
122129
shaft_length = 0.154,
123130
shaft_diameter = 0.02,
124131
head_length = 0.046,

tests/roadmarkings.spec.ts

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import { buildRoadMarkingEntity } from "@features/roadmarkings";
2+
import {
3+
RoadMarking,
4+
RoadMarking_Classification_Color,
5+
RoadMarking_Classification_Type,
6+
TrafficSign_MainSign_Classification_Type,
7+
} from "@lichtblick/asam-osi-types";
8+
import { eulerToQuaternion } from "@utils/math";
9+
import { DeepRequired } from "ts-essentials";
10+
11+
import { GroundTruthPanelSettings } from "@/converters/groundTruth/types";
12+
13+
const DEFAULT_TEST_CONFIG: GroundTruthPanelSettings = {
14+
caching: true,
15+
showAxes: true,
16+
showPhysicalLanes: true,
17+
showLogicalLanes: false,
18+
showReferenceLines: true,
19+
showBoundingBox: true,
20+
show3dModels: false,
21+
defaultModelPath: "/opt/models/vehicles/",
22+
};
23+
24+
function createRoadMarking(
25+
overrides: Partial<{
26+
id: number;
27+
x: number;
28+
y: number;
29+
z: number;
30+
roll: number;
31+
pitch: number;
32+
yaw: number;
33+
width: number;
34+
length: number;
35+
height: number;
36+
mainSignType: number;
37+
color: number;
38+
}> = {},
39+
): DeepRequired<RoadMarking> {
40+
return {
41+
id: { value: overrides.id ?? 1 },
42+
base: {
43+
position: {
44+
x: overrides.x ?? 0,
45+
y: overrides.y ?? 0,
46+
z: overrides.z ?? 0,
47+
},
48+
orientation: {
49+
roll: overrides.roll ?? 0,
50+
pitch: overrides.pitch ?? 0,
51+
yaw: overrides.yaw ?? 0,
52+
},
53+
dimension: {
54+
width: overrides.width ?? 4,
55+
length: overrides.length ?? 0.3,
56+
height: overrides.height ?? 0.004,
57+
},
58+
},
59+
classification: {
60+
type: RoadMarking_Classification_Type.PAINTED_TRAFFIC_SIGN,
61+
monochrome_color: overrides.color ?? RoadMarking_Classification_Color.WHITE,
62+
traffic_main_sign_type:
63+
overrides.mainSignType ?? TrafficSign_MainSign_Classification_Type.STOP,
64+
value: 0,
65+
color_description: "",
66+
traffic_supplementary_sign_type: 0,
67+
},
68+
} as unknown as DeepRequired<RoadMarking>;
69+
}
70+
71+
const PREFIX = "road_marking";
72+
const FRAME = "global";
73+
const TIME = { sec: 0, nsec: 0 };
74+
75+
describe("buildRoadMarkingEntity", () => {
76+
describe("filtering", () => {
77+
it("returns undefined for non-STOP road markings", () => {
78+
const marking = createRoadMarking({ mainSignType: 0 });
79+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
80+
expect(result).toBeUndefined();
81+
});
82+
83+
it("returns entity for STOP road markings", () => {
84+
const marking = createRoadMarking();
85+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
86+
expect(result).toBeDefined();
87+
expect(result!.id).toBe("road_marking_1");
88+
});
89+
});
90+
91+
describe("position and centering", () => {
92+
it("places the cube centered on base.position", () => {
93+
const marking = createRoadMarking({ x: 100, y: 200, z: 5 });
94+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
95+
expect(result).toBeDefined();
96+
97+
const cube = result!.cubes![0]!;
98+
expect(cube.pose!.position).toEqual({ x: 100, y: 200, z: 5 });
99+
});
100+
101+
it("does not offset the cube from base.position (old bug: started at edge)", () => {
102+
const marking = createRoadMarking({ x: 50, y: 30, z: 0, length: 10 });
103+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
104+
expect(result).toBeDefined();
105+
106+
const cube = result!.cubes![0]!;
107+
// The cube must be centered at position, not shifted by length/height
108+
expect(cube.pose!.position).toEqual({ x: 50, y: 30, z: 0 });
109+
});
110+
});
111+
112+
describe("orientation", () => {
113+
it("applies orientation from base.orientation", () => {
114+
const yaw = Math.PI / 4;
115+
const marking = createRoadMarking({ yaw });
116+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
117+
expect(result).toBeDefined();
118+
119+
const cube = result!.cubes![0]!;
120+
const expected = eulerToQuaternion(0, 0, yaw);
121+
expect(cube.pose!.orientation!.w).toBeCloseTo(expected.w, 6);
122+
expect(cube.pose!.orientation!.x).toBeCloseTo(expected.x, 6);
123+
expect(cube.pose!.orientation!.y).toBeCloseTo(expected.y, 6);
124+
expect(cube.pose!.orientation!.z).toBeCloseTo(expected.z, 6);
125+
});
126+
127+
it("applies combined roll, pitch, yaw orientation", () => {
128+
const marking = createRoadMarking({
129+
roll: 0.1,
130+
pitch: -Math.PI / 2,
131+
yaw: Math.PI / 3,
132+
});
133+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
134+
expect(result).toBeDefined();
135+
136+
const cube = result!.cubes![0]!;
137+
const expected = eulerToQuaternion(0.1, -Math.PI / 2, Math.PI / 3);
138+
expect(cube.pose!.orientation!.w).toBeCloseTo(expected.w, 6);
139+
expect(cube.pose!.orientation!.x).toBeCloseTo(expected.x, 6);
140+
expect(cube.pose!.orientation!.y).toBeCloseTo(expected.y, 6);
141+
expect(cube.pose!.orientation!.z).toBeCloseTo(expected.z, 6);
142+
});
143+
});
144+
145+
describe("dimension mapping", () => {
146+
it("maps OSI dimensions correctly to cube size", () => {
147+
// objectToCubePrimitive: size.x=length, size.y=width, size.z=height
148+
const marking = createRoadMarking({
149+
length: 0.3,
150+
width: 4.0,
151+
height: 0.004,
152+
});
153+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
154+
expect(result).toBeDefined();
155+
156+
const cube = result!.cubes![0]!;
157+
expect(cube.size!.x).toBeCloseTo(0.3); // length (along local x)
158+
expect(cube.size!.y).toBeCloseTo(4.0); // width (along local y)
159+
expect(cube.size!.z).toBeCloseTo(0.004); // height (along local z)
160+
});
161+
162+
it("passes through non-trivial orientation as quaternion", () => {
163+
const marking = createRoadMarking({
164+
length: 0.3,
165+
width: 4.0,
166+
height: 0.004,
167+
roll: 0.1,
168+
pitch: 0.2,
169+
yaw: 0.3,
170+
});
171+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
172+
expect(result).toBeDefined();
173+
174+
const cube = result!.cubes![0]!;
175+
const expected = eulerToQuaternion(0.1, 0.2, 0.3);
176+
expect(cube.pose!.orientation).toEqual(expected);
177+
expect(cube.size).toEqual({ x: 0.3, y: 4.0, z: 0.004 });
178+
});
179+
});
180+
181+
describe("color", () => {
182+
it("uses the classification color", () => {
183+
const marking = createRoadMarking({ color: RoadMarking_Classification_Color.RED });
184+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
185+
expect(result).toBeDefined();
186+
187+
const cube = result!.cubes![0]!;
188+
expect(cube.color!.a).toBe(1);
189+
// RED color should have high r component
190+
expect(cube.color!.r).toBeGreaterThan(0.5);
191+
});
192+
});
193+
194+
describe("metadata", () => {
195+
it("includes type, color, width, and height in metadata", () => {
196+
const marking = createRoadMarking({ width: 5, height: 0.3 });
197+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
198+
expect(result).toBeDefined();
199+
200+
const metadata = result!.metadata!;
201+
expect(metadata).toEqual(
202+
expect.arrayContaining([
203+
{ key: "type", value: "PAINTED_TRAFFIC_SIGN" },
204+
{ key: "color", value: "WHITE" },
205+
{ key: "width", value: "5" },
206+
{ key: "height", value: "0.3" },
207+
]),
208+
);
209+
});
210+
});
211+
212+
describe("scene entity properties", () => {
213+
it("sets correct frame_id, timestamp, and frame_locked", () => {
214+
const marking = createRoadMarking({ id: 42 });
215+
const result = buildRoadMarkingEntity(marking, PREFIX, "test_frame", { sec: 10, nsec: 500 }, undefined);
216+
expect(result).toBeDefined();
217+
218+
expect(result!.frame_id).toBe("test_frame");
219+
expect(result!.timestamp).toEqual({ sec: 10, nsec: 500 });
220+
expect(result!.frame_locked).toBe(true);
221+
expect(result!.id).toBe("road_marking_42");
222+
});
223+
});
224+
225+
describe("axes display", () => {
226+
it("shows axes when config.showAxes is true", () => {
227+
const marking = createRoadMarking();
228+
const config = { ...DEFAULT_TEST_CONFIG, showAxes: true };
229+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, config);
230+
expect(result).toBeDefined();
231+
expect(result!.arrows).toBeDefined();
232+
expect(result!.arrows!.length).toBe(3);
233+
});
234+
235+
it("hides axes when config.showAxes is false", () => {
236+
const marking = createRoadMarking();
237+
const config = { ...DEFAULT_TEST_CONFIG, showAxes: false };
238+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, config);
239+
expect(result).toBeDefined();
240+
expect(result!.arrows).toEqual([]);
241+
});
242+
243+
it("hides axes when no config is provided", () => {
244+
const marking = createRoadMarking();
245+
const result = buildRoadMarkingEntity(marking, PREFIX, FRAME, TIME, undefined);
246+
expect(result).toBeDefined();
247+
expect(result!.arrows).toEqual([]);
248+
});
249+
});
250+
});

0 commit comments

Comments
 (0)