Machine-readable: manifest · index · 简体中文 Markdown
AR 应用定位指南
这篇教程介绍如何把一张相机图片,或一组来自同一段连续 AR 跟踪过程的 Rig.v2 相机帧(下文简称“帧”)发送给 01Spatial。系统会返回相机位姿,也就是相机在地图中的位置和朝向;应用再用这个结果把 AR 内容对齐到真实世界。
本文范围: 使用你自己的 API Key 和 Map Key(或 Project Key)完成单帧或 Rig.v2 多帧定位,并把返回位姿用于 AR 对齐。响应中除 AR 对齐必需的位姿矩阵和来源帧外,本文只说明
confidence;其他字段不属于客户端集成契约。
1. 先理解定位结果
AR 框架和 01Spatial 负责不同的工作:
- AR 框架在当前 AR Session 中平滑地跟踪设备。
- 01Spatial 找到拍摄这张图片时,相机在持久地图中的位置和朝向。
- 应用把这两个相机位姿组合起来,让地图和 AR Session 对齐。
- 保存在地图坐标系中的 AR 内容就能出现在正确位置。
拍照时的 AR 相机位姿 ─┐
├─► 地图到 AR 的对齐变换 ─► AR 内容
01Spatial 地图相机位姿 ─┘
01Spatial 返回的是相机位姿,不会决定 AR 内容应该放在哪里。每个物体都需要提前在地图坐标系中保存自己的位置和朝向。
2. 开始前需要准备什么
你需要:
- Portal 个人资料中的 API Key。
- Portal 中一张已就绪地图的 Map Key,或多地图定位使用的 Project Key。
- 一张相机图片。
- 这张图片对应的相机内参,也就是描述焦距和图像中心的
fx、fy、cx、cy。 - 与图片同一时刻的 AR 相机变换。
把 API_HOST 设置为保存定位目标并签发对应 Key 的 Portal 来源地址(origin)。Origin 只包含协议和主机名,不包含路径、查询参数和结尾的斜杠。
请把 API_HOST 当作运行时配置。不要根据设备语言猜测服务器,也不要在公共应用代码中写死某个区域的主机名。这样以后增加新区域时,同一份应用代码仍然可以使用。
请妥善保管两个 Key。把它们放在请求体中,不要放进 URL,不要提交到公开代码仓库,也不要写入日志。
3. 获取同一帧的图片、内参和 AR 位姿
请使用目标设备对应的 AR 框架。图片、内参和 AR 相机变换必须来自同一个相机帧。不要使用屏幕截图或另外调用普通相机拍照,因为这些图片无法和 AR 相机位姿可靠对应。
| 平台 | 获取相机图片 | 保存同一帧的数据 |
|---|---|---|
| iOS 或 iPadOS | ARKit ARFrame.capturedImage | ARFrame.camera.intrinsics 和 ARFrame.camera.transform |
| Android | ARCore Frame.acquireCameraImage() | frame.getCamera().getImageIntrinsics() 和 frame.getCamera().getPose() |
| 微信小程序 | XR-FRAME scene.ar.getARRawData() 中的相机数据 | 同一次结果中的 intrinsics 和 viewMatrix,并转换、保存对应的相机到 Session 矩阵 trackerSpace |
| Unity | AR Foundation ARCameraManager.TryAcquireLatestCpuImage() | 同一次相机更新中的 TryGetIntrinsics() 结果和 AR Camera 变换 |
如果图片编码或格式转换是异步的,请在开始异步任务前复制这一帧的内参和相机变换。完成像素复制或编码后,要及时释放 ARCore 的 Image 和 AR Foundation 的 XRCpuImage。
四个内参都是像素值,而且必须对应最终上传的那张图片:
| 字段 | 含义 |
|---|---|
fx | 水平方向焦距 |
fy | 垂直方向焦距 |
cx | 主点的水平位置 |
cy | 主点的垂直位置 |
如果图片缩放了,内参也要按相同比例缩放。例如,把 1920 × 1080 缩小为 960 × 540:
scale_x = 960 / 1920 = 0.5
scale_y = 540 / 1080 = 0.5
fx' = fx × scale_x
fy' = fy × scale_y
cx' = cx × scale_x
cy' = cy × scale_y
如果裁剪了图片,要从 cx 中减去裁剪区域左边的偏移量,从 cy 中减去顶部偏移量。
第一次接入时,建议保留相机帧的原始方向,不要在上传前旋转图片。如果必须旋转,只更新图片尺寸和内参还不够:返回的位姿会使用旋转后的相机轴。你还需要在第 8 节对齐相机轴时加入同一个旋转补偿。
拍照时还要保存 AR 框架的相机到 Session 变换。不要等网络响应回来后再读取,因为这时设备可能已经移动了。
4. 发送定位请求
向 POST /localize 发送 multipart/form-data 请求:
: "${API_HOST:?请把 API_HOST 设置为保存该地图的 Portal origin}"
curl -X POST "${API_HOST%/}/localize" \
-F "api_key=YOUR_API_KEY" \
-F "map_key=YOUR_MAP_KEY" \
-F "fx=1100" \
-F "fy=1100" \
-F "cx=960" \
-F "cy=540" \
-F "image_path=@frame.jpg"
在 JavaScript 中,让 FormData 自动生成 Content-Type 和 multipart boundary,不要手动设置这个请求头。
type Intrinsics = {
fx: number;
fy: number;
cx: number;
cy: number;
};
async function localizeFrame(
apiHost: string,
apiKey: string,
mapKey: string,
image: Blob,
intrinsics: Intrinsics,
) {
const localizationUrl = new URL('/localize', new URL(apiHost).origin);
const body = new FormData();
body.set('api_key', apiKey);
body.set('map_key', mapKey);
body.set('fx', String(intrinsics.fx));
body.set('fy', String(intrinsics.fy));
body.set('cx', String(intrinsics.cx));
body.set('cy', String(intrinsics.cy));
body.set('image_path', image, 'frame.jpg');
const response = await fetch(localizationUrl, {
method: 'POST',
body,
});
if (response.status === 422) return null;
if (!response.ok) {
throw new Error(`Localization request failed (${response.status})`);
}
return response.json();
}
5. 读取位姿和置信度
定位成功时,poses[0] 中会有一个位姿。下面只保留 AR 应用需要读取的字段:
{
"poses": [
{
"camera_to_world": {
"matrix_column_major": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1.2, 0.4, -3.1, 1
]
},
"metrics": {
"confidence": 0.86
}
}
]
}
matrix_column_major是完整的 4 × 4 相机到地图变换,AR 对齐时优先使用它。confidence是服务器对本次位姿可靠程度的评分,范围为0到1,数值越高表示结果越可靠。
接入初期可以先采用下面这个简单规则,再根据真实场地的验收结果调整阈值:
confidence >= 0.7 → 使用新的位姿
confidence < 0.7 → 保留当前对齐,重新拍一张图片
confidence 不是距离误差,也不代表成功概率。
HTTP 422 表示这张图片没有得到可靠位姿。不要修改当前对齐。请换一张清晰、能看到更多已建图区域细节的图片再试。
如果单帧结果经常不稳定,请使用下一节的 Rig.v2,不要连续应用多个彼此独立的单帧结果。
6. 使用 Rig.v2 完成多帧定位
POST /localize_rig 接收同一个连续跟踪周期(tracking epoch)中的 2–6 个相邻相机帧。这个周期内 AR 跟踪不能中断或重置。接口会返回其中一个采集帧对应的定位位姿,适合单帧定位容易受视角、遮挡或画面细节不足影响的场景。
当单个视角中只有白墙、画面被遮挡、存在重复纹理或眩光,或者只能看到很少的已建图区域时,Rig.v2 很有帮助。手机或平板可以在单帧失败后,引导用户缓慢转过大约半圈并采集四个清晰视角。不要发送多张位置和方向几乎相同的图片;它们无法提供更多有效视角信息。
6.1 采集一组属于同一跟踪过程的帧
采集每一帧时,都必须同时保存以下数据:
- 最终上传的 JPEG 像素。
- 对应这些最终像素的相机内参。
- 相机到 AR Session 的变换。
- 按帧单调不减的采集时间戳。
- 当前 AR 跟踪状态(tracking state)和连续跟踪周期(tracking epoch)。
同一次请求中的所有帧必须属于同一个 tracking epoch。如果 AR Session 结束、参考空间(reference space)被重置或跟踪中断,这一组帧就不再属于同一段连续跟踪过程,应立即取消旧请求。只使用跟踪状态(tracking state)为 normal 时采集的帧。
calibrated_rig 要求相机图片、内参和跟踪位姿(tracking pose)描述同一台物理相机。ARKit、ARCore 和 AR Foundation 在正确同步采样时可以提供这些数据。WebXR 只有在 Raw Camera Access 返回与同一个 XRView 关联的图片时才满足条件;独立的 getUserMedia 视频流不能用于 Rig 定位。
第一次接入时请保留编码像素的原始采集方向。把 EXIF orientation 实际应用到像素,并把 Orientation 写为 1(或移除该字段),然后把 encoded_image_rotation_cw_deg 设置为 0。这样图片、内参、相机轴和相对位姿(relative pose)的含义都是唯一、明确的。
6.2 计算帧间相对位姿
用 T_A_Ci_graphics 表示采集第 i 帧时保存的相机到 AR Session 变换。编码像素没有旋转时,计算:
T_frame0_from_frame_i = inverse(T_A_C0_graphics) × T_A_Ci_graphics
这个变换把第 i 帧的相机坐标转换到第 0 帧的相机坐标。把旋转编码为归一化的 quat_wxyz,把平移编码为 tvec:
frames[0].relative_pose必须是null。calibrated_rig中后续每一帧都必须携带relative_pose。relative_pose_semantics必须是frame0_from_frame。relative_pose_coordinate_system必须是camera_gl_encoded。
如果在 JPEG 编码前旋转了像素,还必须同步旋转内参,并把每一个相对位姿转换到编码图片对应的 OpenGL 风格相机轴(graphics-camera)。这个数学操作通常称为共轭变换。在这个转换通过测试之前,不要发送 calibrated 请求;服务端不会猜测图片旋转或坐标系含义。
6.3 构造 Rig.v2 请求头
必须设置 rig_schema_version: 2。下面的示例只包含 AR 客户端构造标定型(calibrated)Rig 请求所需的字段。
下面是两帧示例;生产客户端最多可以发送六帧。声明的宽高必须和解码后的 JPEG 完全一致,时间戳必须单调不减,frame_id 必须唯一,所有帧的 tracking_epoch 必须一致。
{
"map_key": "YOUR_MAP_KEY",
"api_key": "YOUR_API_KEY",
"rig_schema_version": 2,
"rig_capability": "calibrated_rig",
"client_platform": "ios",
"client_build": "1.0.0",
"operation_id": "rig-7f4d2c",
"relative_pose_semantics": "frame0_from_frame",
"relative_pose_coordinate_system": "camera_gl_encoded",
"intrinsics_coordinate_space": "encoded_pixels",
"frames": [
{
"frame_id": "frame-0",
"fx": 1100.0,
"fy": 1100.0,
"cx": 960.0,
"cy": 540.0,
"image_width": 1920,
"image_height": 1080,
"capture_timestamp_s": 1234.000,
"tracking_epoch": "ar-session-42",
"tracking_state": "normal",
"encoded_image_rotation_cw_deg": 0,
"capture_quality": {},
"relative_pose": null
},
{
"frame_id": "frame-1",
"fx": 1100.0,
"fy": 1100.0,
"cx": 960.0,
"cy": 540.0,
"image_width": 1920,
"image_height": 1080,
"capture_timestamp_s": 1234.620,
"tracking_epoch": "ar-session-42",
"tracking_state": "normal",
"encoded_image_rotation_cw_deg": 0,
"capture_quality": {},
"relative_pose": {
"quat_wxyz": [0.9998, 0.0, 0.0175, 0.0],
"tvec": [0.05, 0.0, -0.02]
}
}
]
}
使用 Project 进行多地图定位时,以 project_key 替代 map_key,不能同时发送两种目标。无论使用哪种模式,api_key 都必须有权访问目标地图或 Project。
6.4 发送带长度前缀的二进制请求体
请求使用 application/octet-stream,不是 multipart form data。二进制布局为:
[4 字节小端序 JSON 长度]
[UTF-8 JSON Header]
[4 字节小端序 JPEG 0 长度]
[JPEG 0 数据]
[4 字节小端序 JPEG 1 长度]
[JPEG 1 数据]
...
JPEG 的顺序必须和 frames 数组完全一致。下面的浏览器示例使用 Blob parts,避免为了组装请求再复制一份全部 JPEG 数据:
function uint32LE(value: number): Uint8Array {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, value, true);
return bytes;
}
function buildRigBody(header: object, jpegFrames: Blob[]): Blob {
const headerBytes = new TextEncoder().encode(JSON.stringify(header));
const parts: BlobPart[] = [uint32LE(headerBytes.byteLength), headerBytes];
for (const jpeg of jpegFrames) {
parts.push(uint32LE(jpeg.size), jpeg);
}
return new Blob(parts, { type: 'application/octet-stream' });
}
async function localizeRig(
apiHost: string,
header: object,
jpegFrames: Blob[],
signal: AbortSignal,
) {
const response = await fetch(
new URL('/localize_rig', new URL(apiHost).origin),
{
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: buildRigBody(header, jpegFrames),
signal,
},
);
const result = await response.json();
return { response, result };
}
一旦 AR Session 或 tracking epoch 失效,立即取消正在进行的请求,并丢弃随后才返回的响应。
6.5 应用返回位姿
Rig.v2 成功响应中,poses[0].image 是该位姿对应的上传帧:
{
"poses": [
{
"image": "frame-1",
"camera_to_world": {
"matrix_column_major": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1.2, 0.4, -3.1, 1
]
},
"metrics": {
"confidence": 0.91
}
}
]
}
只在 HTTP 状态为 200、poses[0] 存在、confidence 满足客户端策略,且 AR Session 没有发生变化时应用结果。poses[0].image 必须属于本次上传的帧。
返回的 camera_to_world 属于 poses[0].image,它不一定是第 0 帧。必须使用该帧采集时保存的相机到 Session 变换,代入第 8 节的对齐公式:
T_A_M = T_A_Cselected_graphics × D_cv_to_graphics × inverse(T_M_Cselected)
如果返回的是其他帧却仍使用第 0 帧的 AR 位姿,会产生对齐误差。HTTP 422 表示本次没有可靠位姿;保留当前对齐并重新采集,不要应用部分结果。
7. 理解坐标系
下面使用三个坐标系和一个变换符号:
M:地图坐标系。A:当前 AR Session 坐标系。C:相机坐标系。T_M_C:从相机坐标系变换到地图坐标系,也就是 API 返回的camera_to_world矩阵。
camera_to_world 中的相机轴采用计算机视觉惯例:
| 轴 | 方向 |
|---|---|
+X | 向右 |
+Y | 向下 |
+Z | 向前 |
WebXR 和 OpenGL 风格的 AR 相机使用 +X 向右、+Y 向上、-Z 向前。需要做一次相机轴转换:
D_cv_to_graphics = diag(1, -1, -1, 1)
位姿中的世界坐标使用建图时定义的地图坐标系。需要长期保存的 AR 内容也必须使用同一个坐标系。不要假设所有地图都有相同的向上轴。如果地图已经校准为真实大小,平移单位就是米。
8. 把地图内容对齐到 AR Session
拍摄定位图片时,保存 AR 相机到 Session 的变换 T_A_C_graphics。如果上传图片相对于这个相机帧没有旋转,定位成功后计算:
T_A_M = T_A_C_graphics × D_cv_to_graphics × inverse(T_M_C)
如果上传图片发生了旋转,用 R_upload_from_capture 表示“从原始计算机视觉相机轴变换到上传图片相机轴”的旋转。此时计算:
T_A_M = T_A_C_graphics × D_cv_to_graphics × inverse(R_upload_from_capture) × inverse(T_M_C)
图片没有旋转时,R_upload_from_capture 就是单位矩阵,两个公式完全相同。
T_A_M 可以把地图坐标变换到当前 AR Session。把它应用到包含全部地图内容的根节点:
AR Session
└── mapRoot 在这里应用 T_A_M
├── 信息标记 保存在地图坐标系中
├── 导航路线 保存在地图坐标系中
└── 3D 物体 保存在地图坐标系中
如果某个物体在地图中的变换是 T_M_O,它最终在 AR Session 中的变换为:
T_A_O = T_A_M × T_M_O
不要修改 AR 框架的相机变换。让本地 AR 跟踪继续平滑地更新相机。如果需要修正跟踪漂移,可以在下一次可靠定位后更新 mapRoot。
9. Three.js 和 WebXR 示例
THREE.Matrix4.fromArray() 可以直接读取 matrix_column_major,不要再转置一次。
import * as THREE from 'three';
function readTrustedPose(response: any) {
const pose = response?.poses?.[0];
const matrix = pose?.camera_to_world?.matrix_column_major;
const confidence = pose?.metrics?.confidence;
if (!Array.isArray(matrix) || matrix.length !== 16) return null;
if (!Number.isFinite(confidence) || confidence < 0.7) return null;
return pose.camera_to_world;
}
function alignMapRoot(
mapRoot: THREE.Object3D,
arCameraAtCapture: ArrayLike<number>,
cameraToWorld: { matrix_column_major: number[] },
) {
const arFromCamera = new THREE.Matrix4().fromArray(arCameraAtCapture);
const mapFromCamera = new THREE.Matrix4().fromArray(
cameraToWorld.matrix_column_major,
);
const cvToGraphics = new THREE.Matrix4().makeScale(1, -1, -1);
const arFromMap = arFromCamera
.clone()
.multiply(cvToGraphics)
.multiply(mapFromCamera.clone().invert());
mapRoot.matrixAutoUpdate = false;
mapRoot.matrix.copy(arFromMap);
mapRoot.matrixWorldNeedsUpdate = true;
}
必须使用生成上传图片的同一帧相机变换:
- WebXR:
XRView.transform.matrix。 - ARKit、RealityKit 或 App Clip:
ARFrame.camera.transform。 - 微信 XR-FRAME: 拍照时的
trackerSpace矩阵。
上面的 Three.js 示例假设上传图片没有旋转。如果图片发生了旋转,请按前面的公式加入 inverse(R_upload_from_capture)。其他 AR 引擎也使用相同的矩阵乘法顺序。
10. 常见问题
| 结果 | 需要检查什么 |
|---|---|
HTTP 400 | 图片字段和四个内参是否完整、有效。 |
HTTP 401 或 403 | API Key 和 Map Key 是否正确,是否有权访问地图。 |
HTTP 422 | 换一张更清晰、包含更多已建图细节的图片。 |
HTTP 429 | 等待一段时间后再发请求。 |
HTTP 5xx | 保留当前对齐,稍后重试。 |
| 有效的 Map Key 被拒绝 | 确认 API_HOST 指向保存这张地图的同一个 Portal 来源地址(origin)。 |
| 内容被镜像或旋转 | 确认相机轴转换只做了一次。 |
| 响应回来后内容发生移动 | 必须使用拍照时保存的 AR 相机变换。 |
| 内容始终有固定偏移 | 确认物体变换和定位位姿使用同一个地图坐标系。 |
11. 上线前检查
请确认你的应用:
- 图片、内参和 AR 相机变换来自同一个 AR 帧。
- 使用了最终上传图片对应的内参。
- 在拍照时保存了 AR 相机变换。
- 如果上传图片发生了旋转,已经补偿了相机轴。
- 读取
matrix_column_major后没有再次转置。 - 相机轴转换只执行了一次。
- 只使用满足
confidence置信度规则的位姿。 - 如果拍照后 AR Session 被重置,会丢弃过期响应。
- 使用 Rig.v2 时,所有帧属于同一个 tracking epoch,并会取消过期请求。
- 使用 Rig.v2 时,会使用
poses[0].image对应帧保存的 AR 相机变换,而不是始终使用第0帧。 - 持久内容和定位位姿使用同一个地图坐标系。
- 把对齐变换应用到地图根节点,而不是覆盖 AR 相机位姿。
- 从运行时配置读取
API_HOST,并使用保存该地图的 Portal。 - 不在 URL、日志或公开代码中暴露 API Key 和 Map Key。
本文未说明的响应字段不应成为客户端逻辑的依赖。