Skip to main content

Lab Exercise: Vision-Language-Action Integration

Objective​

In this lab exercise, you will implement a complete Vision-Language-Action (VLA) system that can interpret natural language commands, perceive the environment, and execute appropriate robotic actions. You'll build a system that combines all the concepts learned throughout the course into a functioning embodied AI agent.

Learning Objectives​

After completing this lab, you will be able to:

  • Integrate vision, language, and action components into a unified system
  • Process natural language commands to generate executable robot actions
  • Implement grounding mechanisms that connect language to perception
  • Evaluate the performance of a VLA system in simulated and/or physical environments
  • Troubleshoot common integration challenges in multi-modal systems

Prerequisites​

  • Completion of all previous modules (ROS 2, Gazebo/Unity, NVIDIA Isaac, VLA)
  • Access to a robot platform (simulated or physical) with camera and basic manipulation capabilities
  • Understanding of VLA model architectures and training approaches
  • Experience with ROS 2 messaging and action servers

Equipment Required​

  • Robot platform with camera and manipulation capability (simulated or physical)
  • Computer with NVIDIA GPU and CUDA support
  • Installed ROS 2, Isaac ROS packages, and VLA model dependencies
  • Gazebo simulation environment (if using simulation)

Lab Steps​

Step 1: Environment Setup and Verification​

  1. Verify all required software components are installed:

    # Check for ROS 2 installation
    printenv | grep ROS

    # Verify Isaac ROS packages
    ros2 pkg list | grep isaac

    # Check GPU availability
    nvidia-smi

    # Verify VLA model dependencies
    python -c "import transformers; import torch; print('Dependencies OK')"
  2. Set up the workspace for the integration project:

    mkdir -p ~/vla_integration_ws/src
    cd ~/vla_integration_ws/src
    git clone https://github.com/nvidia/cuvl_benchmark.git # Example VLA model
    cd ..
    colcon build --symlink-install
    source install/setup.bash

Step 2: Implement the VLA System Architecture​

Create the main integration node that connects language understanding to action execution:

#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from sensor_msgs.msg import Image, CameraInfo
from geometry_msgs.msg import PoseStamped
from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
from visualization_msgs.msg import Marker, MarkerArray
from cv_bridge import CvBridge
import numpy as np
import torch
from transformers import CLIPProcessor, CLIPModel
import json
from typing import List, Dict, Any, Optional
import tf_transformations


class VLASystemNode(Node):
"""
Vision-Language-Action system that interprets natural language commands,
perceives the environment, and executes appropriate robotic actions.
"""
def __init__(self):
super().__init__('vla_system_node')

# Initialize CV bridge for image processing
self.cv_bridge = CvBridge()

# Initialize perception components
self.setup_perception()

# Initialize language processing components
self.setup_language_processing()

# Initialize action execution components
self.setup_action_execution()

# Subscribe to necessary topics
self.image_subscription = self.create_subscription(
Image,
'/camera/image_raw',
self.image_callback,
10
)

self.command_subscription = self.create_subscription(
String,
'/natural_language_command',
self.command_callback,
10
)

# Publisher for system status
self.status_publisher = self.create_publisher(String, '/vla_system/status', 10)

# Publisher for markers for visualization
self.marker_publisher = self.create_publisher(MarkerArray, '/vla_system/markers', 10)

# Store latest perception data
self.latest_image = None
self.detected_objects = []

self.get_logger().info("VLA System initialized successfully")

def setup_perception(self):
"""Initialize perception components using Isaac ROS packages"""
self.get_logger().info("Setting up perception components...")

# Initialize object detection model (using Isaac ROS approach)
# For this example, we'll use a placeholder - in practice, use Isaac ROS Detection2D packages
try:
self.clip_model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
self.clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
self.get_logger().info("CLIP model loaded for vision-language understanding")
except Exception as e:
self.get_logger().warn(f"Could not load CLIP model: {e}. Using placeholder.")

def setup_language_processing(self):
"""Initialize language processing components"""
self.get_logger().info("Setting up language processing components...")

# In a full implementation, this would connect to a VLA model
# For this lab, we'll use a simplified approach
self.object_keywords = {
"water bottle": ["water", "bottle", "drink"],
"cup": ["cup", "mug", "glass"],
"book": ["book", "novel", "textbook"],
"keyboard": ["keyboard", "typing", "computer"],
"mouse": ["mouse", "computer_mouse"]
}

# Spatial relation keywords
self.spatial_keywords = {
"left": ["left", "left side", "to the left"],
"right": ["right", "right side", "to the right"],
"front": ["front", "in front", "ahead"],
"behind": ["behind", "back", "rear"],
"near": ["near", "close", "by", "next to"],
"on": ["on", "above", "atop"],
"under": ["under", "below", "beneath"]
}

def setup_action_execution(self):
"""Initialize action execution components"""
self.get_logger().info("Setting up action execution components...")

# Navigation client
self.nav_client = self.create_client(NavigateToPose, '/navigate_to_pose')

# Manipulation client (using MoveIt or similar)
# In practice, this would connect to your robot's manipulation interface

# Wait for navigation service availability
while not self.nav_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Navigation service not available, waiting again...')

def image_callback(self, msg: Image):
"""Process incoming camera images for perception"""
try:
# Convert ROS image to OpenCV format
cv_image = self.cv_bridge.imgmsg_to_cv2(msg, 'bgr8')
self.latest_image = cv_image

# Perform object detection (simplified for this example)
self.detected_objects = self.simple_object_detection(cv_image)

# In a real implementation, this would use Isaac ROS perception packages
# such as Isaac ROS Detection2D, Isaac ROS Visual Slam, etc.

self.get_logger().debug(f"Detected {len(self.detected_objects)} objects")
except Exception as e:
self.get_logger().error(f"Error in image_callback: {e}")

def command_callback(self, msg: String):
"""Process natural language command and execute appropriate action"""
command = msg.data
self.get_logger().info(f"Received command: '{command}'")

try:
# Publish status update
status_msg = String()
status_msg.data = f"Processing command: {command}"
self.status_publisher.publish(status_msg)

# Parse the command using vision-language understanding
parsed_command = self.parse_language_command(command, self.detected_objects)

if parsed_command:
# Execute the parsed action plan
success = self.execute_action_plan(parsed_command)

# Report results
result_msg = String()
if success:
result_msg.data = f"Successfully executed: {command}"
self.get_logger().info(f"Command executed successfully: {command}")
else:
result_msg.data = f"Failed to execute: {command}"
self.get_logger().error(f"Failed to execute command: {command}")

self.status_publisher.publish(result_msg)
else:
error_msg = String()
error_msg.data = f"Could not parse command: {command}"
self.status_publisher.publish(error_msg)

except Exception as e:
self.get_logger().error(f"Error processing command '{command}': {e}")
error_msg = String()
error_msg.data = f"Error processing command: {str(e)}"
self.status_publisher.publish(error_msg)

def parse_language_command(self, command: str, detected_objects: List[Dict]) -> Optional[Dict[str, Any]]:
"""
Parse a natural language command using vision-language understanding.

Args:
command: Natural language command
detected_objects: List of objects detected in the current scene

Returns:
Dictionary containing the parsed action plan, or None if parsing failed
"""
command_lower = command.lower()

# Identify action verb
action = None
if any(word in command_lower for word in ["go to", "navigate to", "move to", "approach"]):
action = "navigate"
elif any(word in command_lower for word in ["pick up", "grasp", "take", "grab"]):
action = "pick"
elif any(word in command_lower for word in ["put", "place", "drop", "set"]):
action = "place"
elif any(word in command_lower for word in ["bring", "fetch", "deliver"]):
action = "fetch"
else:
self.get_logger().warn(f"Could not identify action in command: {command}")
return None

# Extract object reference
target_object = None
for obj_name, keywords in self.object_keywords.items():
if any(keyword in command_lower for keyword in keywords):
# Find the object in the detected objects
for detected_obj in detected_objects:
if detected_obj['name'] == obj_name:
target_object = detected_obj
break
if target_object:
break

# Extract spatial reference
target_location = None
for location_keyword, synonyms in self.spatial_keywords.items():
if any(synonym in command_lower for synonym in synonyms):
target_location = location_keyword
break

# Create action plan
action_plan = {
"action": action,
"target_object": target_object,
"target_location": target_location,
"original_command": command,
"confidence": 0.8 # Placeholder confidence
}

self.get_logger().info(f"Parsed command into action plan: {action_plan}")
return action_plan

def simple_object_detection(self, image):
"""
Simple object detection for demonstration purposes.
In a real implementation, this would use Isaac ROS detection packages.
"""
# This is a placeholder - in practice, use Isaac ROS Vision packages
# like Isaac ROS Detection2D to perform object detection

# For demonstration, return some predefined objects with positions
return [
{
"name": "water bottle",
"bbox": [100, 100, 200, 200], # [x, y, width, height]
"position_3d": [1.0, 0.5, 0.0], # x, y, z in robot frame
"confidence": 0.9
},
{
"name": "cup",
"bbox": [300, 150, 150, 150],
"position_3d": [1.2, -0.2, 0.0],
"confidence": 0.85
}
]

def execute_action_plan(self, action_plan: Dict[str, Any]) -> bool:
"""
Execute a parsed action plan.

Args:
action_plan: Dictionary containing the action to execute

Returns:
True if execution was successful, False otherwise
"""
action = action_plan.get("action")

if action == "navigate":
return self.execute_navigation_action(action_plan)
elif action == "pick":
return self.execute_pick_action(action_plan)
elif action == "place":
return self.execute_place_action(action_plan)
elif action == "fetch":
return self.execute_fetch_action(action_plan)
else:
self.get_logger().error(f"Unknown action: {action}")
return False

def execute_navigation_action(self, action_plan: Dict[str, Any]) -> bool:
"""Execute navigation action."""
target_object = action_plan.get("target_object")

if target_object and "position_3d" in target_object:
target_pos = target_object["position_3d"]
self.get_logger().info(f"Navigating to object at position: {target_pos}")

# Create navigation goal
goal_msg = NavigateToPose.Goal()
goal_msg.pose.header.frame_id = "map"
goal_msg.pose.header.stamp = self.get_clock().now().to_msg()
goal_msg.pose.pose.position.x = target_pos[0]
goal_msg.pose.pose.position.y = target_pos[1]
goal_msg.pose.pose.position.z = 0.0

# Set orientation (face towards object)
quat = tf_transformations.quaternion_from_euler(0, 0, 0)
goal_msg.pose.pose.orientation.x = quat[0]
goal_msg.pose.pose.orientation.y = quat[1]
goal_msg.pose.pose.orientation.z = quat[2]
goal_msg.pose.pose.orientation.w = quat[3]

# Send navigation goal
future = self.nav_client.call_async(goal_msg)
rclpy.spin_until_future_complete(self, future)

if future.result() is not None:
self.get_logger().info("Navigation goal sent successfully")
return True
else:
self.get_logger().error("Navigation goal failed")
return False
else:
self.get_logger().warn("No target object or position found for navigation")
return False

def execute_pick_action(self, action_plan: Dict[str, Any]) -> bool:
"""Execute pick action."""
target_object = action_plan.get("target_object")

if target_object:
self.get_logger().info(f"Attempting to pick up: {target_object['name']}")

# In a real implementation, this would send a manipulation command
# such as a MoveIt pick action to the robot's manipulation interface
# For simulation purposes, we'll just return success

# Publish visualization marker for picked object
self.publish_object_marker(target_object, "picked")

self.get_logger().info(f"Successfully picked up: {target_object['name']}")
return True
else:
self.get_logger().warn("No target object specified for pick action")
return False

def execute_place_action(self, action_plan: Dict[str, Any]) -> bool:
"""Execute place action."""
target_location = action_plan.get("target_location")

if target_location:
self.get_logger().info(f"Attempting to place object at: {target_location}")

# In a real implementation, this would send a manipulation command
# such as a MoveIt place action to the robot's manipulation interface
# For simulation purposes, we'll just return success

self.get_logger().info(f"Successfully placed object at: {target_location}")
return True
else:
self.get_logger().warn("No target location specified for place action")
return False

def execute_fetch_action(self, action_plan: Dict[str, Any]) -> bool:
"""Execute fetch action (pick and place)."""
# For fetch, we need to first navigate to the object, pick it up,
# then navigate to the destination and place it
success = True

# Navigate to target object
nav_success = self.execute_navigation_action(action_plan)
if not nav_success:
self.get_logger().error("Failed to navigate to target object")
return False

# Pick up the object
pick_success = self.execute_pick_action(action_plan)
if not pick_success:
self.get_logger().error("Failed to pick up target object")
return False

# In a real implementation, we would then navigate to the delivery location
# and execute the place action. For simplicity in this lab, we'll return success.

self.get_logger().info("Fetch action completed successfully")
return True

def publish_object_marker(self, obj: Dict[str, Any], status: str):
"""Publish a visualization marker for the object."""
try:
marker_array = MarkerArray()

marker = Marker()
marker.header.frame_id = "map" # Could be changed based on robot's frame
marker.header.stamp = self.get_clock().now().to_msg()
marker.ns = "vla_objects"
marker.id = hash(obj["name"]) % 1000 # Simple ID generation
marker.type = Marker.CUBE
marker.action = Marker.ADD

# Set position
pos = obj.get("position_3d", [0, 0, 0])
marker.pose.position.x = pos[0]
marker.pose.position.y = pos[1]
marker.pose.position.z = pos[2] + 0.1 # Offset to make visible above ground

# Set orientation
marker.pose.orientation.w = 1.0

# Set dimensions
marker.scale.x = 0.1
marker.scale.y = 0.1
marker.scale.z = 0.1

# Set color based on status
if status == "picked":
marker.color.r = 1.0
marker.color.g = 0.0
marker.color.b = 0.0
elif status == "target":
marker.color.r = 0.0
marker.color.g = 1.0
marker.color.b = 0.0
else:
marker.color.r = 1.0
marker.color.g = 1.0
marker.color.b = 0.0

marker.color.a = 1.0

# Set text
marker.text = f"{obj['name']} ({status})"

marker_array.markers.append(marker)
self.marker_publisher.publish(marker_array)
except Exception as e:
self.get_logger().warn(f"Could not publish object marker: {e}")


def main(args=None):
"""Main function to run the VLA system node."""
rclpy.init(args=args)

vla_system = VLASystemNode()

try:
rclpy.spin(vla_system)
except KeyboardInterrupt:
vla_system.get_logger().info("Shutting down VLA system node...")
finally:
vla_system.destroy_node()
rclpy.shutdown()


if __name__ == '__main__':
main()

Step 3: Create a Launch File​

Create a launch file to start the complete VLA system:

<!-- vla_integration_pkg/launch/vla_system.launch.py -->
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from ament_index_python.packages import get_package_share_directory
import os


def generate_launch_description():
# Declare launch arguments
namespace = LaunchConfiguration('namespace')
use_sim_time = LaunchConfiguration('use_sim_time')

namespace_arg = DeclareLaunchArgument(
'namespace',
default_value='',
description='Namespace for the VLA system nodes'
)

use_sim_time_arg = DeclareLaunchArgument(
'use_sim_time',
default_value='false',
description='Use simulation time if true'
)

# VLA system node
vla_system_node = Node(
package='vla_integration_pkg',
executable='vla_system_node',
name='vla_system',
namespace=namespace,
parameters=[
{'use_sim_time': use_sim_time}
],
remappings=[
('/camera/image_raw', '/camera/color/image_raw'), # Remap to actual camera topic
('/natural_language_command', '/user_commands'),
('/vla_system/status', '/system_status')
],
output='screen'
)

# Perception pipeline (using Isaac ROS components)
perception_node = Node(
package='isaac_ros_detectnet',
executable='isaac_ros_detectnet',
name='perception_pipeline',
namespace=namespace,
parameters=[
{'use_sim_time': use_sim_time},
{'model_name': 'ssd_mobilenet_v2_coco'},
{'input/image_width': 640},
{'input/image_height': 480}
]
)

# Navigation system
navigation_node = Node(
package='nav2_bringup',
executable='navigation2',
name='navigation_system',
namespace=namespace,
parameters=[
{'use_sim_time': use_sim_time}
]
)

return LaunchDescription([
namespace_arg,
use_sim_time_arg,
vla_system_node,
perception_node,
navigation_node
])

Step 4: Testing Your VLA System​

  1. In a terminal, start your robot simulation (or bring up your physical robot):

    # If using simulation
    ros2 launch your_robot_gazebo your_robot_world.launch.py
  2. In another terminal, start your VLA system:

    cd ~/vla_integration_ws
    source install/setup.bash
    ros2 run vla_integration_pkg vla_system_node
  3. In a third terminal, send test commands to your system:

    # Command the robot to navigate to a detected object
    ros2 topic pub /natural_language_command std_msgs/String "data: 'Go to the water bottle'"

    # Command the robot to pick up an object
    ros2 topic pub /natural_language_command std_msgs/String "data: 'Pick up the cup'"
  4. Monitor the system's behavior:

    # Check the status of your VLA system
    ros2 topic echo /vla_system/status

    # View detected objects and their positions
    ros2 run rviz2 rviz2
    # Then add the marker topic to visualize object positions

Step 5: Evaluating Performance​

Evaluate your VLA system using the following metrics:

  1. Command Success Rate: Percentage of commands successfully executed
  2. Perception Accuracy: How accurately the system identifies objects and their positions
  3. Response Time: How quickly the system processes and responds to commands
  4. Robustness: How well the system handles ambiguous commands or challenging lighting conditions

Lab Report​

Submit a lab report including:

  1. System Design: Describe your VLA system architecture and design decisions
  2. Implementation Details: Explain key components and their interactions
  3. Results: Document the performance of your system for various commands
  4. Challenges: Describe any obstacles encountered and how you addressed them
  5. Improvements: Propose ways to enhance your VLA system

Troubleshooting​

Common Issues and Solutions​

  1. GPU Memory Issues

    • Problem: System runs out of GPU memory when running VLA models
    • Solution: Reduce batch size, use model quantization, or use CPU for some components
  2. Perception Failures

    • Problem: Objects not being detected or localized accurately
    • Solution: Improve lighting, adjust detection thresholds, or calibrate camera
  3. Language Understanding Errors

    • Problem: Commands not being parsed correctly
    • Solution: Expand keyword database or implement more sophisticated NLP processing
  4. Action Execution Failures

    • Problem: Robot not executing planned actions correctly
    • Solution: Check robot calibration, verify action server availability, adjust planning parameters

Extension Activities​

For advanced learners, consider implementing:

  1. Multi-Modal Grounding: Connect language to specific visual features
  2. Uncertainty Handling: Implement confidence-aware execution with fallback behaviors
  3. Learning from Corrections: Allow the system to improve based on user feedback
  4. Collaborative Behaviors: Implement teamwork with multiple robots or humans

Summary​

In this lab, you've implemented a complete Vision-Language-Action system that demonstrates the integration of perception, cognition, and action. You've created an embodied AI system capable of understanding natural language commands and executing appropriate robotic behaviors grounded in visual perception.

This concludes the hands-on portion of the VLA module and the entire Physical AI & Humanoid Robotics course. You now have the knowledge and skills to build sophisticated embodied AI systems that connect language to action through perception.