From 9dc17f9354fb6154375b64105308b4b10ca863bf Mon Sep 17 00:00:00 2001 From: Brandon Shrewsbury Date: Tue, 14 Jul 2026 10:49:18 -0600 Subject: [PATCH 1/2] Fix the orientation vector model across frame-system pages An orientation vector's (x, y, z) is the direction the component's +z axis points in the parent frame; th spins the component about that direction. Several pages taught and used it as an axis-angle rotation, so every 'tilt the camera by rotating about x' config example aimed the lens horizontally instead. Confirmed against a live viam-server v1.0.0: ov_degrees {x:1,y:0,z:0,th:180} resolves to pointing along +x with a 180-degree roll, not straight down. - orientation-vectors.md: fix the field bullets and two Common orientations table rows that applied the axis-angle reading; scope the five-format type selection to JSON config (API Pose messages are always orientation-vector degrees); correct the gimbal-lock claim (theta is discontinuous at the +/-z poles per the RDK source); distinguish axis_angles from ov_radians instead of calling them equivalent. - arm-fixed-camera.md, arm-gripper-camera.md, mobile-base-arm.md, mobile-base-sensors.md, quickstarts/frame-system.md: replace every axis-angle-style camera orientation with a pointing-vector value and a one-line statement of the rule; state the +z-out-of-the-lens convention consistently (mobile-base-sensors claimed cameras default to looking along +y). - Mobile-base pages: the frame system holds configured relationships only; it does not track the base as it drives (only arm, gantry, and gripper feed live inputs). Rewrote the step text, a verification step that asked readers to drive the base and watch frames move, and a troubleshooting expand describing a symptom the frame system cannot produce. Confirmed live: move_straight leaves the base frame's world pose unchanged. - quickstarts/frame-system.md: correct the expected print-status output for the zero-joint ur5e (live-confirmed values); a frame parented to an arm attaches to the end effector, not the arm origin. - Consistency: gripper frame origin at the tool center point, lidar casing, table-surface tree entries annotated. --- .../frame-system/arm-fixed-camera.md | 35 ++++++---- .../frame-system/arm-gripper-camera.md | 46 ++++++------ .../frame-system/mobile-base-arm.md | 59 ++++++++-------- .../frame-system/mobile-base-sensors.md | 70 ++++++++++--------- .../quickstarts/frame-system.md | 46 +++++++----- .../reference/orientation-vectors.md | 47 +++++++------ 6 files changed, 167 insertions(+), 136 deletions(-) diff --git a/docs/motion-planning/frame-system/arm-fixed-camera.md b/docs/motion-planning/frame-system/arm-fixed-camera.md index 7a88514c4b..03d9b6d9b6 100644 --- a/docs/motion-planning/frame-system/arm-fixed-camera.md +++ b/docs/motion-planning/frame-system/arm-fixed-camera.md @@ -29,6 +29,7 @@ world Unlike a wrist-mounted camera, this camera is a child of the world frame, not the arm. The camera frame stays fixed in space when the arm moves. +`table-surface` is workspace obstacle geometry, configured separately; see [Define obstacles](/motion-planning/obstacles/). ## Steps @@ -96,17 +97,17 @@ For a camera mounted 200 mm to the right, 300 mm forward, and 800 mm above the w "translation": { "x": 200, "y": 300, "z": 800 }, "orientation": { "type": "ov_degrees", - "value": { "x": 1, "y": 0, "z": 0, "th": 180 } + "value": { "x": 0, "y": 0, "z": -1, "th": 0 } } } ``` -An overhead camera's lens points at the floor. In the default camera -frame, +z points out of the lens, so an unrotated overhead camera has -its +z pointing up rather than down at the workspace. The 180-degree -rotation around x flips the camera's +z to point downward, matching the -lens's actual aim, so 2D image coordinates map intuitively to world -positions. +In an orientation vector, `(x, y, z)` is the direction the camera's +z +axis points in the parent frame, and the camera's +z axis is the lens. +An overhead camera's lens points at the floor, so set the pointing +vector to `(0, 0, -1)`: straight down at the workspace. Use `th` to +spin the camera about the lens axis if the image needs to rotate to +line up with your world axes. **Tripod-mounted camera at an angle:** For a camera on a tripod 500 mm to the left, 600 mm forward, and 700 mm above the origin, tilted 45 degrees downward: @@ -117,12 +118,16 @@ For a camera on a tripod 500 mm to the left, 600 mm forward, and 700 mm above th "translation": { "x": -500, "y": 600, "z": 700 }, "orientation": { "type": "ov_degrees", - "value": { "x": 1, "y": 0, "z": 0, "th": -45 } + "value": { "x": 0.7, "y": 0, "z": -0.7, "th": 0 } } } ``` -A negative angle around the x axis tilts the camera's view downward from the horizontal. +Set `(x, y, z)` to the direction the lens points; Viam normalizes the +vector for you. `(0.7, 0, -0.7)` aims the lens 45 degrees below +horizontal, facing the +x direction. To aim at a different part of the +bench, change the horizontal components: for example, +`(0.5, -0.5, -0.7)` faces between +x and -y. Click **Save**. @@ -131,8 +136,10 @@ Click **Save**. For the arm, jog in **CONTROL** along +x, +y, +z and watch the physical direction. For the camera, open the camera stream in **CONTROL** and move a known object (a pen, a ruler) in the physical +x direction; the object should -move in the +x direction in the camera's image coordinates. If either is -wrong, rotate the relevant component's `orientation` until it matches. +move along one consistent axis in the camera's image. If it moves along the +wrong image axis or in the wrong direction, adjust `th` (which spins the +image about the lens axis) or the pointing vector until image motion matches +physical motion. ### 6. Visualize the frame system @@ -160,9 +167,9 @@ Change the parent to `"world"` so the camera frame remains fixed in space. {{< expand "Overhead camera orientation looks wrong in the visualizer" >}} -For a camera pointing straight down, the rotation should be 180 degrees around the x axis: `"value": { "x": 1, "y": 0, "z": 0, "th": 180 }`. -This flips the camera's z axis to point downward. -If the camera is rotated in the horizontal plane as well (not aligned with the x or y axis), you may need to combine rotations or use a different orientation type. +For a camera pointing straight down, set the pointing vector to `(0, 0, -1)`: `"value": { "x": 0, "y": 0, "z": -1, "th": 0 }`. +In an orientation vector, `(x, y, z)` is the direction the lens points, so point it wherever the lens actually aims. +If the image appears rotated in the visualizer, adjust `th`, which spins the camera about the lens axis. {{< /expand >}} diff --git a/docs/motion-planning/frame-system/arm-gripper-camera.md b/docs/motion-planning/frame-system/arm-gripper-camera.md index a57ac5c498..e11bdd13fa 100644 --- a/docs/motion-planning/frame-system/arm-gripper-camera.md +++ b/docs/motion-planning/frame-system/arm-gripper-camera.md @@ -10,11 +10,11 @@ aliases: --- A table-mounted arm, a gripper bolted to the end effector, and a camera -clamped near the wrist is the most common manipulation setup. The camera moves +clamped near the wrist is a common manipulation setup. The camera moves with the arm, so its view is always centered on wherever the arm is reaching. -The motion service and vision pipelines do not know that on their own; you -have to declare three frame relationships: arm-to-world, gripper-to-arm, and -camera-to-arm. This guide walks through all three. +You declare three frame relationships (arm-to-world, gripper-to-arm, and +camera-to-arm) so the frame system can compute where the gripper and camera +sit as the arm moves. This guide walks through all three. ## Frame hierarchy @@ -28,6 +28,7 @@ world The arm is a direct child of the world frame. The gripper and camera are both children of the arm, so they move with the arm automatically as it changes position. +`table-surface` is workspace obstacle geometry, configured separately; see [Define obstacles](/motion-planning/obstacles/). ## Steps @@ -39,7 +40,7 @@ base means the arm's translation is `(0, 0, 0)` and every measurement starts from the arm. Using a table corner gives you a visible landmark but requires measuring arm-base-to-corner first. -Every frame on the machine is defined relative to this point, so you +Every frame on the machine ultimately resolves to this point, so you will refer to it every time you add or adjust a frame. ### 2. Add a frame to the arm @@ -100,10 +101,10 @@ In the sidebar, click your gripper component to open its card. On the card, clic #### Pick where the gripper frame origin sits -A point near the center of the gripper jaws is usually the most -convenient frame origin. The point you pick here is what `translation` -measures _to_, and what gets moved to a target pose when you later call -the motion service. +The tool center point, between the tips of the gripper jaws, is usually +the most convenient frame origin. The point you pick here is what +`translation` measures _to_, and what gets moved to a target pose when +you later call the motion service. #### Configure the frame @@ -112,8 +113,8 @@ the offset in mm from the arm's end effector to the gripper frame origin, and `orientation` to the gripper's rotation relative to the arm. For a parallel-jaw gripper that bolts directly to the arm's end effector -with the frame origin at the jaw tip, the offset is the gripper's body -length. 120 mm is typical: +with the frame origin at the tool center point between the jaw tips, the +offset is the gripper's body length. 120 mm is typical: ```json { @@ -132,17 +133,17 @@ gripper above, use `z: 170`. A wrong `translation.z` produces silent failures: motion plans validate, then the physical gripper tip lands short or long of the target, or the -planner returns "outside workspace" for poses the arm can clearly reach. +planner fails to find a plan for poses the arm can clearly reach. Click **Save**. #### Check whether the gripper has a kinematics file -Some grippers ship with a kinematics file that describes the position of -the jaws as they open and close, along with collision geometry for the -jaw linkages. If the gripper has one, the motion planner already knows -the gripper's volume and you do not need to add collision geometry to -the gripper frame. +Some gripper modules include a kinematics file that describes the position +of the jaws as they open and close, along with collision geometry for the +jaw linkages. If the gripper has one, the motion planner already has the +gripper's collision geometry and you do not need to add it to the gripper +frame. Call `GetKinematics` on the gripper (or check the module source). If the call returns kinematics data, verify the gripper renders as expected @@ -173,7 +174,7 @@ For a camera mounted 30 mm to the side and 60 mm above the end effector: } ``` -If the camera is tilted downward (for example, angled 30 degrees toward the gripper), add a rotation around the x axis: +If the camera is tilted (for example, angled 30 degrees toward the gripper jaws), set `(x, y, z)` to the direction the lens points in the arm's end effector frame: ```json { @@ -181,11 +182,14 @@ If the camera is tilted downward (for example, angled 30 degrees toward the grip "translation": { "x": 0, "y": 30, "z": 60 }, "orientation": { "type": "ov_degrees", - "value": { "x": 1, "y": 0, "z": 0, "th": -30 } + "value": { "x": 0, "y": -0.5, "z": 0.87, "th": 0 } } } ``` +`(0, -0.5, 0.87)` tips the lens 30 degrees away from the tool axis, back +toward the gripper jaws. Viam normalizes the vector for you. + Click **Save**. ### 6. Visualize the frame system @@ -217,8 +221,8 @@ If the parent is `"world"`, the frame stays fixed in space when the arm moves. {{< expand "Camera image appears rotated or flipped" >}} The camera's orientation in the frame system must match its physical mounting orientation. -If the camera is mounted upside down, add a 180-degree rotation around the z axis. -If the image is mirrored, check whether you need to rotate around the x or y axis. +If the image appears rotated (for example, the camera is mounted upside down), adjust `th`, which spins the camera about the lens axis: `th: 180` corrects an upside-down mounting. +If image motion runs along the wrong axis, re-check the pointing vector `(x, y, z)` against the direction the lens actually aims. {{< /expand >}} diff --git a/docs/motion-planning/frame-system/mobile-base-arm.md b/docs/motion-planning/frame-system/mobile-base-arm.md index 70422c2742..868b15491a 100644 --- a/docs/motion-planning/frame-system/mobile-base-arm.md +++ b/docs/motion-planning/frame-system/mobile-base-arm.md @@ -15,8 +15,7 @@ moves within reach of the base. The frame system represents this by parenting the arm to the base rather than to the world. Every arm-attached component (the gripper, a wrist camera) then inherits the base's motion, while navigation sensors mounted directly on the base -form a parallel subtree. This guide builds the full hierarchy: world to -base to (arm to (gripper, wrist camera), navigation sensors). +form a parallel subtree. This guide builds that full hierarchy. ## Frame hierarchy @@ -30,21 +29,21 @@ world └── my-lidar ``` -The hierarchy reflects what physically moves with what. The base's parent is -the world, so when the base drives, every frame below it shifts with it. The -arm's parent is the base, so arm motion moves the gripper and wrist camera -but not the navigation sensors. This separation keeps the lidar's reading -about the floor consistent while the arm reaches for something. +The hierarchy reflects what physically moves with what. The arm's parent is +the base, so arm motion moves the gripper and wrist camera but not the +navigation sensors. This separation keeps lidar readings fixed to the base +while the arm reaches for something. ## Steps ### 1. Choose your world frame -For a mobile base, the world frame origin is the base center at machine -start. Unlike a table-mounted arm where you pick a fixed physical -landmark, the frame system tracks the base's motion from this initial -origin, so you do not mark anything physically. All component positions -are defined relative to this point. +For a mobile base, configure the base with zero translation so the +world frame origin sits at the base center. The frame system holds this +configured relationship; tracking the base's actual position as it +drives is the job of SLAM or a movement sensor through the motion +service, not the frame system. All component offsets on the machine are +measured from this point. ### 2. Add a frame to the base @@ -119,17 +118,19 @@ If there is an adapter plate between the arm and gripper, set the z translation ``` Adjust the translation values to match the camera's actual mounting position relative to the arm's end effector. -If the camera is tilted, add a rotation. For example, tilted 30 degrees downward: +If the camera is tilted, set `(x, y, z)` to the direction the lens points in the arm's end effector frame. For example, tilted 30 degrees from the tool axis back toward the gripper jaws: ```json { "orientation": { "type": "ov_degrees", - "value": { "x": 1, "y": 0, "z": 0, "th": -30 } + "value": { "x": 0, "y": -0.5, "z": 0.87, "th": 0 } } } ``` +In an orientation vector, `(x, y, z)` is the direction the camera's +z axis (the lens) points; Viam normalizes the vector for you. + Click **Save** after adding each frame. ### 5. Add frames to navigation sensors @@ -144,12 +145,16 @@ Navigation sensors are children of the base, not the arm. "translation": { "x": 0, "y": 200, "z": 120 }, "orientation": { "type": "ov_degrees", - "value": { "x": 0, "y": 0, "z": 1, "th": 0 } + "value": { "x": 0, "y": 1, "z": 0, "th": 0 } } } ``` -**LIDAR mounted on top of the base.** In the sidebar, click your LIDAR component to open its card, then click **Frame**: +Point `(x, y, z)` where the lens aims. A base's forward direction is +y, +so `(0, 1, 0)` faces the camera forward; the identity orientation +`(0, 0, 1)` would aim the lens at the ceiling. + +**Lidar mounted on top of the base.** In the sidebar, click your lidar component to open its card, then click **Frame**: ```json { @@ -170,9 +175,8 @@ Click **Save** after adding each frame. 1. Open the **3D SCENE** tab. 2. Confirm the tree structure: arm, gripper, and wrist camera under the base; navigation sensors as direct children of the base. -3. **Jog the arm from the CONTROL tab — the single clearest test that your hierarchy is correct.** Only the arm subtree should move; the base and navigation sensors should stay put. -4. **Drive the base a short distance.** Every frame should shift together. -5. Measure a known physical offset (base center to lidar, for example) and compare to the translation values in your config. +3. **Jog the arm from the CONTROL tab.** This is the clearest test that your hierarchy is correct: only the arm subtree should move; the base and navigation sensors should stay put. (Frames parented to the base keep the base's configured pose in the scene; the visualizer does not follow the base as it drives.) +4. Measure a known physical offset (base center to lidar, for example) and compare to the translation values in your config. ### 7. Verify with TransformPose @@ -201,21 +205,20 @@ If the gripper's parent is the base, its position will not account for the arm's {{< expand "Navigation camera and wrist camera report conflicting object positions" >}} -These cameras have different parents (base and arm respectively), so their raw coordinates are in different reference frames. +The navigation camera's parent is the base and the wrist camera's parent is the arm, so their raw coordinates are in different reference frames. Always use `TransformPose` to convert positions to a common frame (such as the world frame) before comparing them. Also verify that each camera's translation and orientation offsets are accurate. {{< /expand >}} -{{< expand "Arm subtree shifts unexpectedly when base rotates" >}} +{{< expand "Arm sits in the wrong place relative to the base in the 3D scene" >}} -This is expected. When the base rotates in place, anything mounted off-center -sweeps through an arc; the further off-center, the longer the arc. An arm -mounted forward of the base's rotation center will end up 100 mm to the side -after a 90-degree base turn if it is 100 mm forward of center. If the shift -does not match that geometry, the arm's `translation` offset from the base is -probably wrong; measure from the base's rotation center (usually the wheel -axis midpoint) rather than from a corner of the base chassis. +The arm's `translation` offset from the base is probably measured from the +wrong point. Measure from the base's rotation center (usually the wheel axis +midpoint) rather than from a corner of the base chassis. This matters on the +physical robot too: when the base turns in place, anything mounted off the +rotation center sweeps through an arc, so an offset measured from the wrong +point puts every downstream frame in the wrong place by that same error. {{< /expand >}} diff --git a/docs/motion-planning/frame-system/mobile-base-sensors.md b/docs/motion-planning/frame-system/mobile-base-sensors.md index 27fbcf3aa8..4edff7206c 100644 --- a/docs/motion-planning/frame-system/mobile-base-sensors.md +++ b/docs/motion-planning/frame-system/mobile-base-sensors.md @@ -4,19 +4,17 @@ title: "Mobile base with sensors" weight: 30 layout: "docs" type: "docs" -description: "Configure frames for a mobile base with mounted cameras and LIDAR sensors." +description: "Configure frames for a mobile base with mounted cameras and lidar sensors." aliases: - /motion-planning/frame-system-how-to/mobile-base-sensors/ --- -A mobile base's sense of its own position comes from its sensors, -nothing else. Lidar sweeps tell the base what is around it; front and -rear cameras feed detection and SLAM. Every sensor reading is in the -sensor's own reference frame: a lidar point at `(2.0 m, 0, 0)` is two -meters in front of the lidar, not in front of the base. The frame -system converts sensor frames to the base frame and on to the world -frame, so the navigation stack sees consistent positions. Configuring -it is a one-time setup; this guide walks through it. +A lidar point at `(2.0 m, 0, 0)` is two meters in front of the _lidar_, +not two meters in front of the base. Every sensor reports positions in +its own reference frame. The frame system converts each sensor's +readings to the base frame and on to the world frame, so object +detections, mapping data, and camera images line up with each other. +This guide configures frames for a base with a lidar and two cameras. ## Frame hierarchy @@ -35,11 +33,12 @@ All sensors are children of the base, so the entire sensor subtree moves with th ### 1. Choose your world frame origin -For a mobile base, the world frame origin is the base center at machine -start: not a fixed point in the room, but wherever the base was when it -came online. Sensor positions are measured relative to the base center, -so you do not need to mark anything physically. The frame system tracks -the base's motion from that initial origin. +For a mobile base, configure the base with zero translation so the +world frame origin sits at the base center. Sensor positions are +measured relative to the base center, so you do not need to mark +anything physically. The frame system holds these configured +relationships; tracking the base's position as it drives is the job of +SLAM or a movement sensor, not the frame system. ### 2. Add a frame to the base @@ -60,12 +59,12 @@ Since the world frame origin is at the base center, the translation is zero: Click **Save** in the top-right of the page (or press ⌘/Ctrl+S). -### 3. Add a frame to the LIDAR +### 3. Add a frame to the lidar -In the sidebar, click your LIDAR component to open its card. On the card, click **Frame**. -Measure the offset from the center of the base to the LIDAR's sensor origin. +In the sidebar, click your lidar component to open its card. On the card, click **Frame**. +Measure the offset from the center of the base to the lidar's sensor origin. -For a LIDAR mounted on top of the base, centered horizontally and 150 mm above the base center: +For a lidar mounted on top of the base, centered horizontally and 150 mm above the base center: ```json { @@ -78,8 +77,8 @@ For a LIDAR mounted on top of the base, centered horizontally and 150 mm above t } ``` -If the LIDAR is offset forward or to one side, include x and y values. -For example, a LIDAR mounted 50 mm forward of center and 150 mm above: +If the lidar is offset forward or to one side, include x and y values. +For example, a lidar mounted 50 mm forward of center and 150 mm above: ```json { @@ -108,19 +107,22 @@ For a camera mounted at the front of the base, 200 mm forward and 120 mm above t "translation": { "x": 0, "y": 200, "z": 120 }, "orientation": { "type": "ov_degrees", - "value": { "x": 0, "y": 0, "z": 1, "th": 0 } + "value": { "x": 0, "y": 1, "z": 0, "th": 0 } } } ``` -The default orientation has the camera looking along the +y axis (forward). -If your camera is mounted rotated, set `value.th` to the rotation angle in degrees. +In an orientation vector, `(x, y, z)` is the direction the camera's +z +axis (the lens) points. A base's forward direction is +y, so `(0, 1, 0)` +faces the camera forward; the identity orientation `(0, 0, 1)` would aim +the lens at the ceiling. If the image appears rotated, adjust `th`, +which spins the camera about the lens axis. **Rear-facing camera:** The rear camera's configuration mirrors the front camera's, with two changes: -a negative y translation (it is behind the base center) and a 180-degree -rotation around z (so it faces backward). +a negative y translation (it is behind the base center) and a pointing +vector aimed backward. For a camera mounted at the back of the base, 200 mm backward and 120 mm above the base center, facing backward: @@ -130,12 +132,12 @@ For a camera mounted at the back of the base, 200 mm backward and 120 mm above t "translation": { "x": 0, "y": -200, "z": 120 }, "orientation": { "type": "ov_degrees", - "value": { "x": 0, "y": 0, "z": 1, "th": 180 } + "value": { "x": 0, "y": -1, "z": 0, "th": 0 } } } ``` -The 180-degree rotation around the z axis points the camera backward (along -y). +`(0, -1, 0)` points the lens backward, along -y. Click **Save** after adding each camera frame. @@ -158,9 +160,9 @@ For details on the TransformPose API, see [Frame system: TransformPose](/motion- ## Troubleshooting -{{< expand "LIDAR data does not align with camera data" >}} +{{< expand "Lidar data does not align with camera data" >}} -Check that both the LIDAR and camera frames have the correct translation offsets from the base. +Check that both the lidar and camera frames have the correct translation offsets from the base. Even small errors in the offsets can cause the two data sources to disagree about object positions. Verify by placing an object at a known distance and checking that both sensors report consistent positions after frame transformation. @@ -168,15 +170,15 @@ Verify by placing an object at a known distance and checking that both sensors r {{< expand "Front and rear camera frames overlap in the visualizer" >}} -Make sure the y translations have opposite signs. The front camera should have a positive y offset and the rear camera a negative y offset (or vice versa, depending on your axis convention). -Also verify that the rear camera has a 180-degree rotation to face backward. +Make sure the y translations have opposite signs. A base's forward direction is +y, so the front camera has a positive y offset and the rear camera a negative y offset. +Also verify that the rear camera's pointing vector is `(0, -1, 0)` so it faces backward. {{< /expand >}} -{{< expand "Sensor frames are not updating as the base moves" >}} +{{< expand "A sensor frame sits at the world origin instead of on the base" >}} -Confirm that each sensor's `parent` field is set to the base component name (for example, `"my-base"`), not `"world"`. -Frames parented to the world stay fixed in space. +Confirm that the sensor's `parent` field is set to the base component name (for example, `"my-base"`), not `"world"`. +A frame parented to `"world"` ignores the base entirely, and its readings no longer transform through the base frame. {{< /expand >}} diff --git a/docs/motion-planning/quickstarts/frame-system.md b/docs/motion-planning/quickstarts/frame-system.md index b819cfd420..03b334a842 100644 --- a/docs/motion-planning/quickstarts/frame-system.md +++ b/docs/motion-planning/quickstarts/frame-system.md @@ -42,7 +42,7 @@ In the [Viam app](https://app.viam.com), go to your machine's 3. Name it **my-arm** and click **Add to machine**. 4. In the arm's attributes, set `arm-model` to `"ur5e"`. 5. Click **Frame** on the arm's card. The Frame section is a JSON - editor with no form, parent dropdown, or geometry-type picker. + editor: edit the frame's fields directly as JSON. Edit the JSON so the frame sits at the world origin: ```json @@ -65,7 +65,8 @@ Click **Save** in the top-right of the page (or press ⌘/Ctrl+S). 3. Name it **my-gripper** and click **Add to machine**. 4. Click **Frame** on the gripper's card. Edit the JSON so the parent is the arm and the origin is offset 110 mm along the arm's - z axis (the end of a typical gripper mount): + end effector's z axis (a typical gripper body length). A frame + parented to an arm attaches to the arm's end effector: ```json { @@ -88,7 +89,7 @@ pose moves with the arm automatically. 3. Name it **my-camera** and click **Add to machine**. 4. Click **Frame** on the camera's card. Edit the JSON so the camera sits 30 mm to the side of the arm's end effector and 60 mm above it, - tilted 30 degrees downward about the x axis: + tilted 30 degrees away from the arm's tool axis: ```json { @@ -96,18 +97,21 @@ pose moves with the arm automatically. "translation": { "x": 0, "y": 30, "z": 60 }, "orientation": { "type": "ov_degrees", - "value": { "x": 1, "y": 0, "z": 0, "th": -30 } + "value": { "x": 0, "y": -0.5, "z": 0.87, "th": 0 } } } ``` -In `ov_degrees`, `(x, y, z)` names the rotation axis and `th` is the -angle. `(1, 0, 0), -30` rotates 30 degrees about the camera's x axis, -which tilts the camera downward when its parent's z is up. +In `ov_degrees`, `(x, y, z)` is the direction the frame's z axis points +in the parent frame, and `th` spins the frame about that direction. A +camera's z axis is its lens, so `(0, -0.5, 0.87)` tips the lens +30 degrees away from the arm's tool axis. Viam normalizes the vector +for you. For the full format, see +[orientation vectors](/motion-planning/reference/orientation-vectors/). Click **Save**. You now have three components in a frame tree: the arm -at the world origin, the gripper 110 mm above its end effector, and the -camera offset to the side and tilted down. +based at the world origin, the gripper 110 mm out from its end +effector, and the camera offset to the side and tilted. ## 4. Verify with the CLI @@ -129,14 +133,19 @@ viam machines part motion print-status --part "YOUR-PART-NAME" You should see something like: ```text - my-arm : X: 0.00 Y: 0.00 Z: 0.00 OX: 0.00 OY: 0.00 OZ: 1.00 Theta: 0.00 - my-gripper : X: 0.00 Y: 0.00 Z: 110.00 OX: 0.00 OY: 0.00 OZ: 1.00 Theta: 0.00 - my-camera : X: 0.00 Y: 30.00 Z: 60.00 OX: ... + my-arm : X: -817.20 Y: -232.90 Z: 62.80 OX: 0.00 OY: -1.00 OZ: 0.00 Theta: 90.00 + my-gripper : X: -817.20 Y: -342.90 Z: 62.80 OX: 0.00 OY: -1.00 OZ: 0.00 Theta: 90.00 + my-camera : X: ... ``` -The gripper sits 110 mm above the arm origin. The camera sits 30 mm -to the side and 60 mm above, with the downward tilt reflected in the -orientation vector. +Each line is that component's frame origin expressed in the world +frame. An arm's printed pose is its end effector, the end of the +kinematic chain, so `my-arm` prints where the UR5e's end effector sits +at its zero joint configuration rather than the world origin where you +placed the arm's base. The gripper prints 110 mm from the end effector +along the end effector's z axis (which points along world -y at this +configuration), and the camera prints at its own offset from the same +end effector. ## 5. Transform a detected object to the world frame @@ -191,10 +200,11 @@ translation, downward tilt, and the arm's current pose. Change the arm's joint configuration (through the **CONTROL** tab or by calling `move_to_joint_positions`), then re-run `print-status`. -You will see `my-camera` and `my-gripper` poses change while -`my-arm` stays at the world origin. That is the parent-child +You will see all three poses change together: `my-arm` prints the end +effector's new pose, and the gripper and camera stay at their fixed +offsets from it wherever it moves. That is the parent-child propagation in action: the camera and gripper frames are fixed -relative to the arm, so they move wherever the arm moves. +relative to the arm's end effector, so they move wherever it moves. ## What you learned diff --git a/docs/motion-planning/reference/orientation-vectors.md b/docs/motion-planning/reference/orientation-vectors.md index 419a7191a3..90d5deff20 100644 --- a/docs/motion-planning/reference/orientation-vectors.md +++ b/docs/motion-planning/reference/orientation-vectors.md @@ -12,7 +12,7 @@ aliases: - /operate/reference/orientation-vector/ --- -When you write a `Pose` payload (in frame system configuration, a motion planning destination, or anywhere else), the orientation format you choose affects validation and gimbal-lock behavior. The sections below cover the five supported formats, their schemas, common orientations, and validation rules. +In machine JSON configuration (frame definitions and world state files), you choose one of five orientation formats with the `type` field. API `Pose` messages always express orientation as an orientation vector in degrees (`o_x`, `o_y`, `o_z`, `theta`); there is no format field to choose. The format you pick in configuration affects validation and singularity behavior. The sections below cover the five supported formats, their schemas, common orientations, and validation rules. Viam's default format, the orientation vector (OV), is structured similarly to the @@ -29,7 +29,7 @@ orientation configuration: ### `ov_degrees` (default) -The orientation vector format (axis plus angle in degrees). Use this for most configurations; the other formats exist to interoperate with Euler, axis-angle, or quaternion inputs. +The orientation vector format: a pointing direction plus a spin in degrees. Use this for most configurations; the other formats exist to interoperate with Euler, axis-angle, or quaternion inputs. ```json { @@ -38,15 +38,16 @@ The orientation vector format (axis plus angle in degrees). Use this for most co } ``` -- `x`, `y`, `z`: components of the rotation axis vector (must be non-zero; - normalized internally) -- `th`: rotation angle in degrees. Values wrap every 360 degrees, so - `th: 370` and `th: 10` describe the same orientation. +- `x`, `y`, `z`: the direction the component's +z axis points, as a point + on the unit sphere (must be non-zero; normalized internally) +- `th`: rotation in degrees around that same pointing direction. Values + wrap every 360 degrees, so `th: 370` and `th: 10` describe the same + orientation. -Default (identity): `{"x": 0, "y": 0, "z": 1, "th": 0}`. This represents no rotation. +Default (identity): `{"x": 0, "y": 0, "z": 1, "th": 0}`. This points the component along +z with no spin, which is no rotation. -The axis must be a non-zero vector. `(x, y, z) = (0, 0, 0)` is a -singularity: the rotation axis is undefined and validation rejects it. +The pointing vector must be non-zero. `(x, y, z) = (0, 0, 0)` is a +singularity: the pointing direction is undefined and validation rejects it. ### `ov_radians` @@ -78,7 +79,7 @@ Default: `{"roll": 0, "pitch": 0, "yaw": 0}`. ### `axis_angles` -Rotation axis plus angle in radians, using the R4AA (Rotation 4 Axis Angle) representation. Choose `axis_angles` when your input already comes from an R4AA source; otherwise `ov_radians` is equivalent and more common in Viam configs. +Rotation axis plus angle in radians, using the R4 axis-angle representation. Choose `axis_angles` when your input is a true axis-angle rotation. The value keys match an orientation vector's, but the meaning differs: here `(x, y, z)` is the axis to rotate around, while in `ov_radians` it is the direction the component points. ```json { @@ -108,21 +109,25 @@ Unit quaternion. Values are auto-normalized. ## Common orientations -| Description | `ov_degrees` value | -| ----------------------------------- | ------------------------------------- | -| No rotation (identity) | `{"x": 0, "y": 0, "z": 1, "th": 0}` | -| 90 degrees around z | `{"x": 0, "y": 0, "z": 1, "th": 90}` | -| 180 degrees around z (flip x and y) | `{"x": 0, "y": 0, "z": 1, "th": 180}` | -| 90 degrees around x | `{"x": 1, "y": 0, "z": 0, "th": 90}` | -| Pointing straight down (-z) | `{"x": 0, "y": 0, "z": -1, "th": 0}` | -| 30 degree tilt around y | `{"x": 0, "y": 1, "z": 0, "th": 30}` | +| Description | `ov_degrees` value | +| --------------------------------------------- | ----------------------------------------- | +| No rotation (identity) | `{"x": 0, "y": 0, "z": 1, "th": 0}` | +| Pointing up, spun 90 degrees | `{"x": 0, "y": 0, "z": 1, "th": 90}` | +| Pointing up, spun 180 degrees (flips x and y) | `{"x": 0, "y": 0, "z": 1, "th": 180}` | +| Pointing horizontally along +x | `{"x": 1, "y": 0, "z": 0, "th": 0}` | +| Pointing straight down (-z) | `{"x": 0, "y": 0, "z": -1, "th": 0}` | +| Tilted 30 degrees from vertical toward +x | `{"x": 0.5, "y": 0, "z": 0.866, "th": 0}` | ## Gimbal lock When using `euler_angles`, certain pitch values (near +/- 90 degrees or pi/2 -radians) cause gimbal lock, where roll and yaw become ambiguous. If you need -orientations near these values, use `ov_degrees`, `axis_angles`, or `quaternion` -instead. These formats do not suffer from gimbal lock. +radians) cause gimbal lock, where roll and yaw become ambiguous. `quaternion` +and `axis_angles` represent every orientation without this singularity. + +An orientation vector has one discontinuity of its own: when the component +points exactly along +z or -z, `th` behaves like a gimbal-locked Euler angle, +and orientations near straight up or straight down can produce large jumps in +`th`. If you need smooth behavior near those poles, use `quaternion`. ## Validation From deaeddc88c1d7fb99b121a9bfe976b61e8ca5b44 Mon Sep 17 00:00:00 2001 From: Brandon Shrewsbury Date: Tue, 14 Jul 2026 12:05:36 -0600 Subject: [PATCH 2/2] Apply doc-review polish to the orientation vector fix Style-review pass over the PR's changed pages (viam-doc-review): - orientation-vectors.md: drop the redundant negative clause after the API Pose statement; state the identity orientation positively; split the pole-discontinuity sentence; the Validation section now says 'pointing vector' instead of 'axis' for ov formats, matching the terminology this PR corrects. - arm-fixed-camera.md: put the condition before the instruction in the th guidance (if the image needs to rotate, use th). - mobile-base-arm.md: state the visualizer behavior positively (shows base-parented frames at the configured pose even while the base drives); align the SLAM/movement-sensor sentence with the wording on mobile-base-sensors.md. - arm-gripper-camera.md: replace 'you do not need to add it' with the positive instruction to leave the gripper frame's geometry field empty. - quickstarts/frame-system.md: an arm's printed pose is the pose of its end effector (not 'is its end effector'); split the 44-word sentence. prettier, markdownlint, vale (0 errors), make build-prod all pass. --- .../motion-planning/frame-system/arm-fixed-camera.md | 6 +++--- .../frame-system/arm-gripper-camera.md | 4 ++-- docs/motion-planning/frame-system/mobile-base-arm.md | 6 +++--- docs/motion-planning/quickstarts/frame-system.md | 8 ++++---- .../motion-planning/reference/orientation-vectors.md | 12 ++++++------ 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/motion-planning/frame-system/arm-fixed-camera.md b/docs/motion-planning/frame-system/arm-fixed-camera.md index 03d9b6d9b6..b42f8c3eae 100644 --- a/docs/motion-planning/frame-system/arm-fixed-camera.md +++ b/docs/motion-planning/frame-system/arm-fixed-camera.md @@ -105,9 +105,9 @@ For a camera mounted 200 mm to the right, 300 mm forward, and 800 mm above the w In an orientation vector, `(x, y, z)` is the direction the camera's +z axis points in the parent frame, and the camera's +z axis is the lens. An overhead camera's lens points at the floor, so set the pointing -vector to `(0, 0, -1)`: straight down at the workspace. Use `th` to -spin the camera about the lens axis if the image needs to rotate to -line up with your world axes. +vector to `(0, 0, -1)`: straight down at the workspace. If the image +needs to rotate to line up with your world axes, use `th` to spin the +camera about the lens axis. **Tripod-mounted camera at an angle:** For a camera on a tripod 500 mm to the left, 600 mm forward, and 700 mm above the origin, tilted 45 degrees downward: diff --git a/docs/motion-planning/frame-system/arm-gripper-camera.md b/docs/motion-planning/frame-system/arm-gripper-camera.md index e11bdd13fa..f0a9bc3583 100644 --- a/docs/motion-planning/frame-system/arm-gripper-camera.md +++ b/docs/motion-planning/frame-system/arm-gripper-camera.md @@ -142,8 +142,8 @@ Click **Save**. Some gripper modules include a kinematics file that describes the position of the jaws as they open and close, along with collision geometry for the jaw linkages. If the gripper has one, the motion planner already has the -gripper's collision geometry and you do not need to add it to the gripper -frame. +gripper's collision geometry, so leave the gripper frame's `geometry` +field empty. Call `GetKinematics` on the gripper (or check the module source). If the call returns kinematics data, verify the gripper renders as expected diff --git a/docs/motion-planning/frame-system/mobile-base-arm.md b/docs/motion-planning/frame-system/mobile-base-arm.md index 868b15491a..3c97deb65e 100644 --- a/docs/motion-planning/frame-system/mobile-base-arm.md +++ b/docs/motion-planning/frame-system/mobile-base-arm.md @@ -41,8 +41,8 @@ while the arm reaches for something. For a mobile base, configure the base with zero translation so the world frame origin sits at the base center. The frame system holds this configured relationship; tracking the base's actual position as it -drives is the job of SLAM or a movement sensor through the motion -service, not the frame system. All component offsets on the machine are +drives is the job of SLAM or a movement sensor, not the frame system. +All component offsets on the machine are measured from this point. ### 2. Add a frame to the base @@ -175,7 +175,7 @@ Click **Save** after adding each frame. 1. Open the **3D SCENE** tab. 2. Confirm the tree structure: arm, gripper, and wrist camera under the base; navigation sensors as direct children of the base. -3. **Jog the arm from the CONTROL tab.** This is the clearest test that your hierarchy is correct: only the arm subtree should move; the base and navigation sensors should stay put. (Frames parented to the base keep the base's configured pose in the scene; the visualizer does not follow the base as it drives.) +3. **Jog the arm from the CONTROL tab.** This is the clearest test that your hierarchy is correct: only the arm subtree should move; the base and navigation sensors should stay put. (The visualizer shows frames parented to the base at the base's configured pose, even while the base drives.) 4. Measure a known physical offset (base center to lidar, for example) and compare to the translation values in your config. ### 7. Verify with TransformPose diff --git a/docs/motion-planning/quickstarts/frame-system.md b/docs/motion-planning/quickstarts/frame-system.md index 03b334a842..374de28d14 100644 --- a/docs/motion-planning/quickstarts/frame-system.md +++ b/docs/motion-planning/quickstarts/frame-system.md @@ -139,10 +139,10 @@ You should see something like: ``` Each line is that component's frame origin expressed in the world -frame. An arm's printed pose is its end effector, the end of the -kinematic chain, so `my-arm` prints where the UR5e's end effector sits -at its zero joint configuration rather than the world origin where you -placed the arm's base. The gripper prints 110 mm from the end effector +frame. An arm's printed pose is the pose of its end effector, the end +of the kinematic chain. `my-arm` therefore prints where the UR5e's end +effector sits at its zero joint configuration rather than the world +origin where you placed the arm's base. The gripper prints 110 mm from the end effector along the end effector's z axis (which points along world -y at this configuration), and the camera prints at its own offset from the same end effector. diff --git a/docs/motion-planning/reference/orientation-vectors.md b/docs/motion-planning/reference/orientation-vectors.md index 90d5deff20..a24e08478b 100644 --- a/docs/motion-planning/reference/orientation-vectors.md +++ b/docs/motion-planning/reference/orientation-vectors.md @@ -12,7 +12,7 @@ aliases: - /operate/reference/orientation-vector/ --- -In machine JSON configuration (frame definitions and world state files), you choose one of five orientation formats with the `type` field. API `Pose` messages always express orientation as an orientation vector in degrees (`o_x`, `o_y`, `o_z`, `theta`); there is no format field to choose. The format you pick in configuration affects validation and singularity behavior. The sections below cover the five supported formats, their schemas, common orientations, and validation rules. +In machine JSON configuration (frame definitions and world state files), you choose one of five orientation formats with the `type` field. API `Pose` messages always express orientation as an orientation vector in degrees (`o_x`, `o_y`, `o_z`, `theta`). The format you pick in configuration affects validation and singularity behavior. The sections below cover the five supported formats, their schemas, common orientations, and validation rules. Viam's default format, the orientation vector (OV), is structured similarly to the @@ -44,7 +44,7 @@ The orientation vector format: a pointing direction plus a spin in degrees. Use wrap every 360 degrees, so `th: 370` and `th: 10` describe the same orientation. -Default (identity): `{"x": 0, "y": 0, "z": 1, "th": 0}`. This points the component along +z with no spin, which is no rotation. +Default (identity): `{"x": 0, "y": 0, "z": 1, "th": 0}`. This points the component along +z with zero spin, leaving it unrotated. The pointing vector must be non-zero. `(x, y, z) = (0, 0, 0)` is a singularity: the pointing direction is undefined and validation rejects it. @@ -124,14 +124,14 @@ When using `euler_angles`, certain pitch values (near +/- 90 degrees or pi/2 radians) cause gimbal lock, where roll and yaw become ambiguous. `quaternion` and `axis_angles` represent every orientation without this singularity. -An orientation vector has one discontinuity of its own: when the component -points exactly along +z or -z, `th` behaves like a gimbal-locked Euler angle, -and orientations near straight up or straight down can produce large jumps in +An orientation vector has one discontinuity of its own. When the component +points exactly along +z or -z, `th` behaves like a gimbal-locked Euler angle: +orientations near straight up or straight down can produce large jumps in `th`. If you need smooth behavior near those poles, use `quaternion`. ## Validation -For `ov_degrees` and `ov_radians`, the axis `(x, y, z)` must have a non-zero magnitude. Passing an all-zero axis returns the error `has a normal of 0, probably X, Y, and Z are all 0`. +For `ov_degrees` and `ov_radians`, the pointing vector `(x, y, z)` must have a non-zero magnitude. Passing an all-zero axis returns the error `has a normal of 0, probably X, Y, and Z are all 0`. For `axis_angles`, a zero-norm axis causes a panic during normalization. Always provide a non-zero axis vector.