For the complete documentation index, see llms.txt. This page is also available as Markdown.

World Coordinates

Reading a prediction's positions in meters, and the point clouds that come with them

With a depth map and a calibration in place, predictions carry a 3D position in meters alongside their 2D one.

Opting in

Two things have to be true: the Pop names a depthMap, and the components whose predictions should be translated set toWorld.

from eyepop.worker.camera import Camera
from eyepop.worker.worker_types import InferenceComponent, Pop, PopDepthMap, SourceDefaults

pop = Pop(
    components=[InferenceComponent(ability="eyepop.person:latest", toWorld=True)],
    depthMap=PopDepthMap(ability="eyepop.depth.metric.large:latest"),
    defaults=SourceDefaults(camera=Camera(hfovDegrees=72.0)),
)
const pop = {
    components: [{ type: PopComponentType.INFERENCE, ability: 'eyepop.person:latest', toWorld: true }],
    depthMap: { ability: 'eyepop.depth.metric.large:latest' },
    defaults: { camera: { hfovDegrees: 72 } },
}

toWorld only means something on a component that runs its own inference — inference and tracking. Asking for it on a forward, contour_finder or component_finder is rejected when the Pop is compiled. A contour finder's points do get enriched, but they belong to the object that fed it, so the request goes on the inference component upstream.

What carries coordinates

worldX, worldY and worldZ appear on every point-shaped member:

  • key points

  • outline points

  • contour points, including cutouts

Bounding boxes are not enriched. A box is not a point, and any single anchor — centre, nearest corner, base — would be an arbitrary choice made on your behalf.

z and worldZ are unrelated. z is model-relative depth in whatever convention the model uses — what a 3D body-points model emits, for instance. worldZ is meters. They are not convertible into one another.

Which frame the meters are in depends on whether the calibration supplied a pose — see Which frame the meters are in. Nothing in the prediction records it, so your code has to know which it asked for.

Point clouds

An object with a segmentation mask carries a dense cloud too: one xyz triple per mask pixel, laid out row-major on the bitmap's own grid, with NaN marking a pixel that could not be placed.

placed_points / placedPoints is usually what you want: a mask covers its object's whole bounding box, so anything not rectangular is mostly NaN.

The whole scene

Asking for depthMap.toWorld returns a cloud of the entire scene rather than one per segmented object, on the depth map's own grid.

The scene cloud is indexed exactly like the depth map's values: same grid, same order, so a pixel's point and the depth it came from share an index. Both are sent, because a NaN point says only that the pixel could not be placed — the value at that index says why, whether +Infinity for sky or a reading that was never a distance.

A prediction can hold many clouds — one per masked object, plus the scene — which is why from_prediction / cloudsOfPrediction return a list while DepthMap.from_prediction returns a single map. A depth map is frame-level; a cloud belongs to one object.

Next steps

Last updated