> For the complete documentation index, see [llms.txt](https://multiset.gitbook.io/multiset/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://multiset.gitbook.io/multiset/basics/localization/coordinate-systems.md).

# Coordinate Systems

### Overview

Every successful localization returns a 6-DoF pose: a `position` `(x, y, z)` in meters and a `rotation` quaternion `(x, y, z, w)`, both expressed in the map's coordinate space.

MultiSet supports **two coordinate conventions** for that pose, and both are **Y-up**:

| Convention                   | Flag                               | Ecosystem                                              |
| ---------------------------- | ---------------------------------- | ------------------------------------------------------ |
| **Left-handed, Y-up (LHS)**  | `isRightHanded: false` *(default)* | Unity, MultiSet Quest SDK, MultiSet App                |
| **Right-handed, Y-up (RHS)** | `isRightHanded: true`              | ARKit (iOS), ARCore (Android), WebXR, Three.js, OpenGL |

LHS is the map's **native** frame: it is how poses are stored internally, and it is what you get when the flag is omitted. Setting `isRightHanded: true` asks the server to convert the pose before returning it.

The two frames share the same origin, the same up axis, and the same scale. They differ by a **mirror across the YZ plane**: the X axis is negated. The same physical camera pose therefore has identical `y` and `z` values in both, with an opposite sign on `x`.

<figure><img src="/files/AfUeq6GWsPwJDXAS7iim" alt="Two coordinate frames side by side. Left: left-handed Y-up, isRightHanded false, the default, used by Unity SDK, Quest SDK and MultiSet App, with a camera at position 3.00, 1.60, 5.50 and rotation 0.00, 0.38, 0.00, 0.92. Right: right-handed Y-up, isRightHanded true, used by iOS ARKit, Android ARCore and WebXR or Three.js, with the same physical camera at position -3.00, 1.60, 5.50 and rotation 0.00, -0.38, 0.00, 0.92. The frames are mirrored across the YZ plane: negate x on position, negate y and z on the rotation quaternion."><figcaption><p>The same physical camera pose, described in both conventions</p></figcaption></figure>

***

### Which convention should I use?

| Platform                          | Frame it works in  | What to do                                                        |
| --------------------------------- | ------------------ | ----------------------------------------------------------------- |
| **Unity SDK**                     | Left-handed, Y-up  | Nothing. The SDK uses the default and places content directly.    |
| **MultiSet Quest SDK**            | Left-handed, Y-up  | Nothing. Unity based, so the default applies.                     |
| **iOS native (ARKit)**            | Right-handed, Y-up | Send `isRightHanded: true`                                        |
| **Android native (ARCore)**       | Right-handed, Y-up | Send `isRightHanded: true`                                        |
| **WebXR SDK (Three.js / Needle)** | Right-handed, Y-up | Send `isRightHanded: true` (the WebXR SDK already defaults to it) |
| **Custom REST integration**       | Your choice        | Pick the one that matches your renderer or engine                 |

{% hint style="info" %}
If your stack is right-handed but **not** Y-up (for example ROS, which is right-handed Z-up), request the RHS Y-up pose and apply your own axis swap on top of it.
{% endhint %}

***

### Setting the flag

`isRightHanded` is optional on every query endpoint and defaults to `false`.

| Endpoint                          | Body type | Value to send                 |
| --------------------------------- | --------- | ----------------------------- |
| `POST /vps/map/query`             | JSON      | `"isRightHanded": true`       |
| `POST /vps/map/query-form`        | form-data | `isRightHanded=true` (string) |
| `POST /vps/map/multi-image-query` | form-data | `isRightHanded=true` (string) |
| `POST /vps/object/query`          | form-data | `isRightHanded=true` (string) |

{% hint style="warning" %}
On the form-data endpoints every field is a string, so send the literal text `true` or `false`. Omit the field entirely to get the left-handed default.
{% endhint %}

***

### Example: the same query in both conventions

#### Left-handed (default)

```json
// POST /vps/map/query
{
  "mapCode": "MAP_RJFKKWQ1787J",
  "isRightHanded": false,
  "queryImage": "<base64>",
  "resolution": { "width": 960, "height": 720 },
  "cameraIntrinsics": { "fx": 669.53, "fy": 669.53, "px": 480.0, "py": 360.0 }
}
```

```json
// 200 OK
{
  "poseFound": true,
  "position": { "x":  3.00, "y": 1.60, "z": 5.50 },
  "rotation": { "x": 0.00, "y":  0.38, "z": 0.00, "w": 0.92 },
  "confidence": 0.87
}
```

Drop this straight into a Unity `Transform`:

```csharp
transform.position = new Vector3(3.00f, 1.60f, 5.50f);
transform.rotation = new Quaternion(0.00f, 0.38f, 0.00f, 0.92f);
```

#### Right-handed

```json
// POST /vps/map/query
{
  "mapCode": "MAP_RJFKKWQ1787J",
  "isRightHanded": true,
  "queryImage": "<base64>",
  "resolution": { "width": 960, "height": 720 },
  "cameraIntrinsics": { "fx": 669.53, "fy": 669.53, "px": 480.0, "py": 360.0 }
}
```

```json
// 200 OK, the same physical pose with a mirrored X
{
  "poseFound": true,
  "position": { "x": -3.00, "y": 1.60, "z": 5.50 },
  "rotation": { "x": 0.00, "y": -0.38, "z": 0.00, "w": 0.92 },
  "confidence": 0.87
}
```

Drop this straight into a Three.js object:

```javascript
object.position.set(-3.00, 1.60, 5.50);
object.quaternion.set(0.00, -0.38, 0.00, 0.92);
```

The same request as form-data:

```bash
curl -X POST https://api.multiset.ai/v1/vps/map/query-form \
  -H "Authorization: Bearer $TOKEN" \
  -F "mapCode=MAP_RJFKKWQ1787J" \
  -F "isRightHanded=true" \
  -F "fx=669.53" -F "fy=669.53" -F "px=480.0" -F "py=360.0" \
  -F "width=960" -F "height=720" \
  -F "queryImage=@frame.jpg"
```

***

### Converting a pose yourself

If you receive a pose in one convention and need the other, mirror it across the YZ plane: **negate `position.x`, negate `rotation.y` and `rotation.z`, and leave everything else alone**. The operation is its own inverse, so the same function converts in both directions.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
function flipHandedness(pose) {
  return {
    position: {
      x: -pose.position.x,
      y:  pose.position.y,
      z:  pose.position.z,
    },
    rotation: {
      x:  pose.rotation.x,
      y: -pose.rotation.y,
      z: -pose.rotation.z,
      w:  pose.rotation.w,
    },
  };
}
```

{% endtab %}

{% tab title="C#" %}

```csharp
public static (Vector3 position, Quaternion rotation) FlipHandedness(
    Vector3 position, Quaternion rotation)
{
    return (
        new Vector3(-position.x, position.y, position.z),
        new Quaternion(rotation.x, -rotation.y, -rotation.z, rotation.w)
    );
}
```

{% endtab %}

{% tab title="Swift" %}

```swift
func flipHandedness(position: SIMD3<Float>,
                    rotation: simd_quatf) -> (SIMD3<Float>, simd_quatf) {
    let p = SIMD3<Float>(-position.x, position.y, position.z)
    let r = simd_quatf(ix: rotation.imag.x,
                       iy: -rotation.imag.y,
                       iz: -rotation.imag.z,
                       r:  rotation.real)
    return (p, r)
}
```

{% endtab %}

{% tab title="Python" %}

```python
def flip_handedness(position, rotation):
    """position: (x, y, z), rotation: (x, y, z, w)."""
    x, y, z = position
    qx, qy, qz, qw = rotation
    return (-x, y, z), (qx, -qy, -qz, qw)
```

{% endtab %}
{% endtabs %}

***

### What `isRightHanded` does not change

The flag governs the **pose the API returns**. Several other values are always expressed in the map's native **left-handed** frame, no matter what you set:

| Value                                                                                | Frame      | Notes                                                                                   |
| ------------------------------------------------------------------------------------ | ---------- | --------------------------------------------------------------------------------------- |
| [`hintPosition`](/multiset/basics/localization/pose-prior-hintposition.md)           | Always LHS | Convert an RHS position to LHS before sending it as a hint                              |
| [`hintFloorHeight`](/multiset/basics/localization/hint-floor-height.md)              | Unaffected | A Y-axis band, and Y is identical in both conventions                                   |
| [`geoHint`](/multiset/basics/localization/geohint-in-localization.md)                | Unaffected | Latitude, longitude, and altitude. The server converts it to LHS internally             |
| [`GeoPose`](/multiset/basics/localization/geopose-support.md) response               | Unaffected | The server un-mirrors the pose before georeferencing, so GeoPose is the same either way |
| [Georeference API](/multiset/basics/rest-api-docs/georeference.md) local coordinates | Always LHS | Alignment points must be supplied in LHS                                                |
| [Simulation data](/multiset/basics/rest-api-docs/simulation-data.md) capture poses   | Always LHS | Convert RHS captures before uploading the manifest                                      |

{% hint style="warning" %}
The most common mistake is passing a right-handed `hintPosition` from a previous RHS localization result. The hint then points to the mirrored side of the map, so the search filter looks in the wrong place and localization fails or returns a poor pose. Negate `x` first.
{% endhint %}

#### Multi frame query

The [multi frame query](/multiset/basics/rest-api-docs/map-query.md#vps-multi-image-query-api) also takes a per-frame `trackingPose` from your local SLAM session. These are fused with the per-image estimates, so send them in the **same convention you declared with `isRightHanded`**: RHS tracking poses with `isRightHanded: true`, LHS tracking poses with the default. The `trackingPose` echoed back in the response is your input, returned unchanged.

#### Object Tracking query

[Object Tracking](/multiset/basics/rest-api-docs/object-query.md) uses the same flag. It applies to the detected object's pose in the camera frame: LHS by default, RHS when `isRightHanded: true`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://multiset.gitbook.io/multiset/basics/localization/coordinate-systems.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
