LeRobot documentation

Processadores de Ambiente

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v0.6.1).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Processadores de Ambiente

Os processadores de ambiente são uma camada crítica na arquitetura de processamento de dados do LeRobot, responsável por transformações específicas do ambiente, separadas do processamento específico da política. Essa separação de responsabilidades permite um código mais limpo, melhor modularidade e experimentação mais fácil com diferentes ambientes e políticas.

Por que Processadores de Ambiente?

Ao trabalhar com diferentes ambientes de robótica (LIBERO, MetaWorld, Aloha, etc.), cada ambiente frequentemente possui formatos de dados, sistemas de coordenadas e convenções únicos que precisam de padronização antes do processamento da política. Sem os processadores de ambiente, essas transformações seriam:

  1. Fixo no código do ambiente - Dificulta a experimentação com diferentes representações de estado
  2. Duplicado entre políticas - Cada política precisaria lidar com peculiaridades específicas do ambiente
  3. Misturado com a lógica de política - Viola a separação de responsabilidades e torna o depuramento mais difícil

Os processadores de ambiente resolvem isso ao fornecer uma camada de processamento dedicada entre as observações brutas do ambiente e as entradas da política.

O Pipeline de Processamento

É assim que os dados fluem pelo pipeline de processamento completo durante a avaliação:

# In lerobot_eval.py rollout() function:

# 1. Raw environment observation (numpy arrays, various formats)
raw_observation = env.step(action)

# 2. Convert numpy to torch, normalize images [0,1]
observation = preprocess_observation(raw_observation)

# 3. Add the task description (for multi-task environments)
observation["task"] = list(env.call("task_description"))

# 4. ENVIRONMENT-SPECIFIC preprocessing (NEW!)
#    - Flatten robot states
#    - Rotate images to match dataset conventions
#    - Handle environment-specific coordinate systems
observation = env_preprocessor(observation)

# 5. POLICY-SPECIFIC preprocessing
#    - Normalize with dataset statistics
#    - Add batch dimensions
#    - Move to GPU
#    - Tokenize language instructions
observation = preprocessor(observation)

# 6. Policy inference
action = policy.select_action(observation)

# 7. POLICY-SPECIFIC postprocessing
#    - Unnormalize actions
#    - Remove batch dimensions
action = postprocessor(action)

# 8. ENVIRONMENT-SPECIFIC postprocessing (NEW!)
#    - Convert action formats if needed
#    - Apply environment-specific constraints
action_transition = {"action": action}
action_transition = env_postprocessor(action_transition)
action = action_transition["action"]

# 9. Execute in environment
env.step(action)

Os Benefícios

1. Separação de responsabilidades

Os processadores de ambiente lidam com transformações específicas do formato de dados do ambiente, enquanto os processadores de política lidam com transformações específicas dos requisitos do modelo.

# ❌ Before: Mixed concerns
class LiberoVLAPolicy:
    def preprocess(self, obs):
        # Environment-specific: Flatten robot state (shouldn't be in policy!)
        state = self._flatten_robot_state(obs["robot_state"])
        # Policy-specific: Normalize with dataset stats
        state = self.normalizer(state)
        return state

# ✅ After: Clear separation
# Environment processor: Handles LIBERO's nested robot state
env_preprocessor = LiberoProcessorStep()  # Flattens robot_state

# Policy processor: Handles model requirements
policy_preprocessor = NormalizerProcessorStep(stats=dataset_stats)

2. Flexibilidade e Reutilização

A mesma política pode funcionar com diferentes processadores de ambiente, e o mesmo processador de ambiente pode funcionar com diferentes políticas:

# Use SmolVLA policy with LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
    env_cfg=libero_cfg,
    policy_cfg=smolvla_cfg,
)
smolvla_preprocessor, smolvla_postprocessor = make_pre_post_processors(smolvla_cfg)

# Or use ACT policy with the same LIBERO environment
libero_preprocessor, libero_postprocessor = make_env_pre_post_processors(
    env_cfg=libero_cfg,
    policy_cfg=act_cfg,
)
act_preprocessor, act_postprocessor = make_pre_post_processors(act_cfg)

3. Experimentação mais fácil

Quer tentar diferentes representações de estado para o LIBERO? Basta criar um novo processador:

# Original: 8D state (pos + quat→axisangle + gripper)
@ProcessorStepRegistry.register("libero_processor")
class LiberoProcessorStep(ObservationProcessorStep):
    def _process_observation(self, obs):
        eef_pos = robot_state["eef"]["pos"]          # 3D
        eef_axisangle = quat2axisangle(quat)         # 3D
        gripper = robot_state["gripper"]["qpos"]     # 2D
        state = torch.cat([eef_pos, eef_axisangle, gripper], dim=-1)  # 8D
        return state

# Experiment: Add velocity for better control
@ProcessorStepRegistry.register("libero_velocity_processor")
class LiberoVelocityProcessorStep(ObservationProcessorStep):
    def _process_observation(self, obs):
        # Include velocities for 14D state
        eef_pos = robot_state["eef"]["pos"]          # 3D
        eef_axisangle = quat2axisangle(quat)         # 3D
        eef_vel = robot_state["eef"]["vel"]          # 3D  (NEW)
        gripper_pos = robot_state["gripper"]["qpos"] # 2D
        gripper_vel = robot_state["gripper"]["qvel"] # 3D  (NEW)
        state = torch.cat([eef_pos, eef_axisangle, eef_vel,
                          gripper_pos, gripper_vel], dim=-1)  # 14D
        return state

4. Código de ambiente mais limpo

Os ambientes expõem todos os dados disponíveis sem a necessidade de saber o que os modelos downstream usarão:

# LIBERO environment exposes full robot state
observation = {
    "pixels": {"image": img, "image2": img2},
    "robot_state": {
        "eef": {"pos": ..., "quat": ..., "vel": ..., "mat": ..., "axisangle": ...},
        "gripper": {"qpos": ..., "qvel": ...},
        "joints": {"pos": ..., "vel": ...}
    }
}

# Environment processor decides what to use
# Policy processor handles model-specific transformations

Usando Processadores de Ambiente

Função de Fábrica

A função make_env_pre_post_processors segue o mesmo padrão que make_pre_post_processors para políticas:

from lerobot.envs import make_env_pre_post_processors, PushtEnv
from lerobot.envs.configs import LiberoEnv

# policy_cfg is your policy's config (e.g. cfg.policy in lerobot-eval); a few policies,
# such as XVLA, provide their own environment processors

# For LIBERO: Returns LiberoProcessorStep in preprocessor
libero_cfg = LiberoEnv(task="libero_spatial")
env_preprocessor, env_postprocessor = make_env_pre_post_processors(libero_cfg, policy_cfg=policy_cfg)

# For other environments: Returns identity processors (no-op)
pusht_cfg = PushtEnv()
env_preprocessor, env_postprocessor = make_env_pre_post_processors(pusht_cfg, policy_cfg=policy_cfg)

Implementação em envs/factory.py

def make_env_pre_post_processors(
    env_cfg: EnvConfig,
) -> tuple[
    PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
    PolicyProcessorPipeline[dict[str, Any], dict[str, Any]],
]:
    """
    Create preprocessor and postprocessor pipelines for environment observations.

    Args:
        env_cfg: The configuration of the environment.

    Returns:
        A tuple containing:
            - preprocessor: Pipeline that processes environment observations
            - postprocessor: Pipeline that processes environment outputs
    """
    # For LIBERO environments, add the LiberoProcessorStep to preprocessor
    if isinstance(env_cfg, LiberoEnv) or "libero" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[LiberoProcessorStep()])
    else:
        # For all other environments, return an identity preprocessor
        preprocessor = PolicyProcessorPipeline(steps=[])

    # Postprocessor is currently identity for all environments
    # Future: Could add environment-specific action transformations
    postprocessor = PolicyProcessorPipeline(steps=[])

    return preprocessor, postprocessor

Integração na Avaliação

Em lerobot_eval.py, os processadores de ambiente são criados uma vez e usados ao longo de todo o processo:

def eval_main(cfg: EvalPipelineConfig):
    # Create environment
    envs = make_env(cfg.env, n_envs=cfg.eval.batch_size)

    # Create policy
    policy = make_policy(cfg=cfg.policy, env_cfg=cfg.env)

    # Create policy processors
    preprocessor, postprocessor = make_pre_post_processors(
        policy_cfg=cfg.policy,
        pretrained_path=cfg.policy.pretrained_path,
    )

    # Create environment processors (NEW!)
    env_preprocessor, env_postprocessor = make_env_pre_post_processors(env_cfg=cfg.env, policy_cfg=cfg.policy)

    # Run evaluation with both processor types
    eval_policy_all(
        envs=envs,
        policy=policy,
        env_preprocessor=env_preprocessor,      # Environment-specific
        env_postprocessor=env_postprocessor,    # Environment-specific
        preprocessor=preprocessor,              # Policy-specific
        postprocessor=postprocessor,            # Policy-specific
        n_episodes=cfg.eval.n_episodes,
    )

Exemplo: Processador de Ambiente LIBERO

O LiberoProcessorStep demonstra um processador de ambiente do mundo real:

from lerobot.processor import ObservationProcessorStep

@dataclass
@ProcessorStepRegistry.register(name="libero_processor")
class LiberoProcessorStep(ObservationProcessorStep):
    """
    Processes LIBERO observations into the LeRobot format.

    **State Processing:**
    - Extracts end-effector position (3D)
    - Converts quaternion to axis-angle representation (3D)
    - Extracts gripper joint positions (2D)
    - Concatenates into 8D state vector

    **Image Processing:**
    - Rotates images 180° to match HuggingFaceVLA/libero convention
    """

    def _process_observation(self, observation):
        processed_obs = observation.copy()

        # Process images: Flip 180° for camera convention
        for key in list(processed_obs.keys()):
            if key.startswith("observation.images."):
                img = processed_obs[key]
                img = torch.flip(img, dims=[2, 3])  # Flip H and W
                processed_obs[key] = img

        # Process robot_state: Flatten to 8D vector
        if "observation.robot_state" in processed_obs:
            robot_state = processed_obs.pop("observation.robot_state")

            eef_pos = robot_state["eef"]["pos"]           # (B, 3)
            eef_quat = robot_state["eef"]["quat"]         # (B, 4)
            gripper_qpos = robot_state["gripper"]["qpos"] # (B, 2)

            # Convert quaternion to axis-angle
            eef_axisangle = self._quat2axisangle(eef_quat)  # (B, 3)

            # Concatenate into single state vector
            state = torch.cat((eef_pos, eef_axisangle, gripper_qpos), dim=-1)
            state = state.float()

            processed_obs["observation.state"] = state

        return processed_obs

Por que essas transformações?

  1. Rotação de imagem: O conjunto de dados HuggingFaceVLA/libero possui imagens rotacionadas em 180° em relação ao simulador LIBERO bruto. O processador trata essa incompatibilidade de convenção para que as políticas treinadas no conjunto de dados funcionem perfeitamente.

  2. Achatar o estado: O ambiente bruto do LIBERO expõe dicionários aninhados com todas as informações de estado disponíveis (posição, quaternião, velocidade, representação matricial, etc.). O processador:

    • Seleciona os componentes relevantes (pos, quat, gripper)
    • Converte quaternions para eixo-ângulo (mais adequado para aprendizado)
    • Achata em um único vetor 8D que as políticas esperam
  3. Flexibilidade: o ambiente ainda expõe todos os dados brutos. Se você quiser tentar diferentes representações de estado (por exemplo, incluindo velocidades, usando representação matricial em vez de eixo-ângulo), você pode criar um novo processador sem modificar o código do ambiente.

Adicionando Processadores de Ambiente para Novos Ambientes

Para adicionar processadores de ambiente para um novo ambiente:

1. Crie a Etapa do Processor

# In src/lerobot/processor/env_processor.py

@dataclass
@ProcessorStepRegistry.register(name="myenv_processor")
class MyEnvProcessorStep(ObservationProcessorStep):
    """Process observations from MyEnv."""

    def _process_observation(self, observation):
        processed = observation.copy()

        # Your environment-specific transformations
        if "myenv.specific.state" in processed:
            state = processed.pop("myenv.specific.state")
            # Transform to standard format
            processed["observation.state"] = self._transform_state(state)

        return processed

2. Atualize o Subclasse do EnvConfig

# In src/lerobot/envs/factory.py

def make_env_pre_post_processors(env_cfg: EnvConfig):
    if isinstance(env_cfg, LiberoEnv) or "libero" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[LiberoProcessorStep()])
    elif isinstance(env_cfg, MyEnvConfig) or "myenv" in env_cfg.type:
        preprocessor = PolicyProcessorPipeline(steps=[MyEnvProcessorStep()])
    else:
        preprocessor = PolicyProcessorPipeline(steps=[])

    postprocessor = PolicyProcessorPipeline(steps=[])
    return preprocessor, postprocessor

3. Uso na Avaliação

Nenhuma alteração é necessária! O script de avaliação usa automaticamente o processador apropriado:

lerobot-eval \
    --policy.path=lerobot/my_policy \
    --env.type=myenv \  # Automatically uses MyEnvProcessorStep
    --eval.n_episodes=10

Futuro: Pós-processadores de Ambiente

Atualmente, os pós-processadores são identidade (no-op) para todos os ambientes. Casos de uso futuros incluem:

Transformações de Espaço de Ações

@dataclass
class MyEnvActionPostprocessor(ProcessorStep):
    """Convert policy actions to environment-specific format."""

    def __call__(self, transition: EnvTransition) -> EnvTransition:
        action = transition["action"]

        # Example: Convert from Cartesian to joint space
        if self.action_space == "joint":
            action = self.ik_solver(action)

        # Example: Apply environment-specific safety limits
        action = torch.clamp(action, self.min_action, self.max_action)

        transition["action"] = action
        return transition

Conversões de Sistemas de Coordenadas

@dataclass
class CoordinateTransformPostprocessor(ProcessorStep):
    """Transform actions between coordinate systems."""

    def __call__(self, transition: EnvTransition) -> EnvTransition:
        action = transition["action"]

        # Example: Policy outputs in world frame, env expects base frame
        action = self.world_to_base_transform(action)

        transition["action"] = action
        return transition

Boas Práticas

  1. Mantenha os processadores de ambiente simples: eles devem lidar apenas com problemas de formato de dados específicos do ambiente, e não com transformações complexas relacionadas à aprendizagem.

  2. Use processadores de política para requisitos do modelo: normalização, loteamento, posicionamento de dispositivo e tokenização pertencem aos processadores de política.

  3. Expor todos os dados dos ambientes: Permitir que os processadores decidam o que usar, em vez de codificar as escolhas diretamente no ambiente.

  4. Convenções de documentação: Documente claramente as convenções de sistema de coordenadas, orientações de câmera ou formatos de dados que seu processador manipula.

  5. Teste de forma independente: os processadores de ambiente devem ser testáveis sem carregar políticas ou ambientes completos.

Resumo

Os processadores de ambiente fornecem uma separação limpa entre as transformações de dados específicas do ambiente e os requisitos de modelo específicos da política. Essa arquitetura:

  • ✅ Permite experimentação fácil com diferentes representações de estado
  • ✅ Permite que as políticas funcionem de forma transparente em diferentes ambientes
  • ✅ Mantém o código do ambiente focado na interface de simulação/hardware
  • ✅ Torna os pipelines de processadores mais mantíveis e depuráveis
  • ✅ Segue o princípio da responsabilidade única

O insight principal: os ambientes definem os formatos de dados, os processadores os padronizam e as políticas consomem os dados padronizados. Cada camada tem uma responsabilidade clara e focada.

Update on GitHub