Source code for pettingzoo.utils.wrappers.color_reduction

from __future__ import annotations

from typing import Any

import gymnasium.spaces
import numpy as np
from typing_extensions import override

from pettingzoo.utils.env import ActionType, AECEnv, AgentID, ObsType, ParallelEnv
from pettingzoo.utils.wrappers.base import BaseWrapper
from pettingzoo.utils.wrappers.base_parallel import BaseParallelWrapper

COLOR_REDUCTION_MODES = ("full", "R", "G", "B")
GRAYSCALE_WEIGHTS = np.array([0.299, 0.587, 0.114], dtype=np.float32)
_CHANNELS = {"R": 0, "G": 1, "B": 2}


def _check_mode(mode: str) -> None:
    assert mode in COLOR_REDUCTION_MODES, (
        f"mode must be one of {list(COLOR_REDUCTION_MODES)}, got {mode!r}"
    )


def _reduce(obs: np.ndarray, mode: str) -> np.ndarray:
    if mode == "full":
        return (np.asarray(obs, dtype=np.float32) @ GRAYSCALE_WEIGHTS).astype(np.uint8)
    return np.asarray(obs)[:, :, _CHANNELS[mode]].copy()


def _reduced_space(
    space: gymnasium.spaces.Space[Any], mode: str, agent: Any
) -> gymnasium.spaces.Box:
    assert (
        isinstance(space, gymnasium.spaces.Box)
        and len(space.shape) == 3
        and space.shape[2] == 3
    ), (
        "color reduction needs a 3d Box observation space with a last dimension of "
        f"size 3, got {space} for agent {agent}"
    )
    low = _reduce(space.low, mode)
    high = _reduce(space.high, mode)
    return gymnasium.spaces.Box(low=low, high=high, dtype=low.dtype.type)


[docs] class ColorReductionObservationV1(BaseWrapper[AgentID, Any, ActionType]): """Reduces an image observation to a single channel. ``"full"`` converts to grayscale with the luminance weights [0.299, 0.587, 0.114] and returns uint8 whatever the input dtype was. ``"R"``, ``"G"`` and ``"B"`` take the named channel and keep the input dtype. Either way the trailing channel axis is dropped, so an (H, W, 3) observation becomes (H, W). :param env: The AEC environment to wrap. :param mode: One of "full", "R", "G", "B". """ def __init__(self, env: AECEnv[AgentID, ObsType, ActionType], mode: str = "full"): assert isinstance(env, AECEnv), ( "ColorReductionObservationV1 is only compatible with AEC environments, " "use ColorReductionObservationParallelV1 instead." ) _check_mode(mode) super().__init__(env) self.mode = mode self._obs_spaces: dict[AgentID, gymnasium.spaces.Box] = {} # SuperSuit checks the spaces when you wrap rather than on first use, and # skips the check for envs that have no possible_agents to check yet. for known_agent in getattr(env, "possible_agents", []): self.observation_space(known_agent) @override def observation_space(self, agent: AgentID) -> gymnasium.spaces.Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _reduced_space( self.env.observation_space(agent), self.mode, agent ) return self._obs_spaces[agent] @override def observe(self, agent: AgentID) -> Any: obs = self.env.observe(agent) if obs is None: return None return _reduce(obs, self.mode) @override def __str__(self) -> str: return f"ColorReductionObservationV1<{self.env!s}>"
[docs] class ColorReductionObservationParallelV1( BaseParallelWrapper[AgentID, Any, ActionType] ): """Reduces an image observation to a single channel. ``"full"`` converts to grayscale with the luminance weights [0.299, 0.587, 0.114] and returns uint8 whatever the input dtype was. ``"R"``, ``"G"`` and ``"B"`` take the named channel and keep the input dtype. Either way the trailing channel axis is dropped, so an (H, W, 3) observation becomes (H, W). :param env: The parallel environment to wrap. :param mode: One of "full", "R", "G", "B". """ def __init__( self, env: ParallelEnv[AgentID, ObsType, ActionType], mode: str = "full" ): _check_mode(mode) super().__init__(env) self.mode = mode self._obs_spaces: dict[AgentID, gymnasium.spaces.Box] = {} # SuperSuit checks the spaces when you wrap rather than on first use, and # skips the check for envs that have no possible_agents to check yet. for known_agent in getattr(env, "possible_agents", []): self.observation_space(known_agent) @override def observation_space(self, agent: AgentID) -> gymnasium.spaces.Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _reduced_space( self.env.observation_space(agent), self.mode, agent ) return self._obs_spaces[agent] def _reduce_all(self, observations: dict[AgentID, Any]) -> dict[AgentID, Any]: return {agent: _reduce(obs, self.mode) for agent, obs in observations.items()} @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> tuple[dict[AgentID, Any], dict[AgentID, dict[str, Any]]]: observations, infos = self.env.reset(seed=seed, options=options) return self._reduce_all(observations), infos @override def step( self, actions: dict[AgentID, ActionType] ) -> tuple[ dict[AgentID, Any], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict[str, Any]], ]: observations, rewards, terminations, truncations, infos = self.env.step(actions) return self._reduce_all(observations), rewards, terminations, truncations, infos @override def __str__(self) -> str: return f"ColorReductionObservationParallelV1<{self.env!s}>"