Skip to content

Commit ba0fa45

Browse files
authored
Add rendering wrapper with noise (#1243)
1 parent ff66b08 commit ba0fa45

File tree

3 files changed

+221
-2
lines changed

3 files changed

+221
-2
lines changed

Diff for: gymnasium/wrappers/__init__.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,13 @@
5858
RecordEpisodeStatistics,
5959
TimeLimit,
6060
)
61-
from gymnasium.wrappers.rendering import HumanRendering, RecordVideo, RenderCollection
61+
from gymnasium.wrappers.rendering import (
62+
AddWhiteNoise,
63+
HumanRendering,
64+
ObstructView,
65+
RecordVideo,
66+
RenderCollection,
67+
)
6268
from gymnasium.wrappers.stateful_action import StickyAction
6369
from gymnasium.wrappers.stateful_observation import (
6470
DelayObservation,
@@ -122,6 +128,8 @@
122128
"OrderEnforcing",
123129
"RecordEpisodeStatistics",
124130
# --- Rendering ---
131+
"AddWhiteNoise",
132+
"ObstructView",
125133
"RenderCollection",
126134
"RecordVideo",
127135
"HumanRendering",

Diff for: gymnasium/wrappers/rendering.py

+177-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* ``RenderCollection`` - Collects rendered frames into a list
44
* ``RecordVideo`` - Records a video of the environments
55
* ``HumanRendering`` - Provides human rendering of environments with ``"rgb_array"``
6+
* ``AddWhiteNoise`` - Randomly replaces pixels with white noise
7+
* ``ObstructView`` - Randomly places patches of white noise to obstruct the pixel rendering
68
"""
79

810
from __future__ import annotations
@@ -16,13 +18,15 @@
1618
import gymnasium as gym
1719
from gymnasium import error, logger
1820
from gymnasium.core import ActType, ObsType, RenderFrame
19-
from gymnasium.error import DependencyNotInstalled
21+
from gymnasium.error import DependencyNotInstalled, InvalidProbability
2022

2123

2224
__all__ = [
2325
"RenderCollection",
2426
"RecordVideo",
2527
"HumanRendering",
28+
"AddWhiteNoise",
29+
"ObstructView",
2630
]
2731

2832

@@ -562,3 +566,175 @@ def close(self):
562566
pygame.display.quit()
563567
pygame.quit()
564568
super().close()
569+
570+
571+
class AddWhiteNoise(
572+
gym.Wrapper[ObsType, ActType, ObsType, ActType], gym.utils.RecordConstructorArgs
573+
):
574+
"""Randomly replaces pixels with white noise.
575+
576+
If used with ``render_mode="rgb_array"`` and ``AddRenderObservation``, it will
577+
make observations noisy.
578+
The environment may also become partially-observable, turning the MDP into a POMDP.
579+
580+
Example - Every pixel will be replaced by white noise with probability 0.5:
581+
>>> env = gym.make("LunarLander-v3", render_mode="rgb_array")
582+
>>> env = AddWhiteNoise(env, probability_of_noise_per_pixel=0.5)
583+
>>> env = HumanRendering(env)
584+
>>> obs, _ = env.reset(seed=123)
585+
>>> obs, *_ = env.step(env.action_space.sample())
586+
"""
587+
588+
def __init__(
589+
self,
590+
env: gym.Env[ObsType, ActType],
591+
probability_of_noise_per_pixel: float,
592+
is_noise_grayscale: bool = False,
593+
):
594+
"""Wrapper replaces random pixels with white noise.
595+
596+
Args:
597+
env: The environment that is being wrapped
598+
probability_of_noise_per_pixel: the probability that a pixel is white noise
599+
is_noise_grayscale: if True, RGB noise is converted to grayscale
600+
"""
601+
if not 0 <= probability_of_noise_per_pixel < 1:
602+
raise InvalidProbability(
603+
f"probability_of_noise_per_pixel should be in the interval [0,1). Received {probability_of_noise_per_pixel}"
604+
)
605+
606+
gym.utils.RecordConstructorArgs.__init__(
607+
self,
608+
probability_of_noise_per_pixel=probability_of_noise_per_pixel,
609+
is_noise_grayscale=is_noise_grayscale,
610+
)
611+
gym.Wrapper.__init__(self, env)
612+
613+
self.probability_of_noise_per_pixel = probability_of_noise_per_pixel
614+
self.is_noise_grayscale = is_noise_grayscale
615+
616+
def render(self) -> RenderFrame:
617+
"""Compute the render frames as specified by render_mode attribute during initialization of the environment, then add white noise."""
618+
render_out = super().render()
619+
620+
if self.is_noise_grayscale:
621+
noise = (
622+
self.np_random.integers(
623+
(0, 0, 0),
624+
255 * np.array([0.2989, 0.5870, 0.1140]),
625+
size=render_out.shape,
626+
dtype=np.uint8,
627+
)
628+
.sum(-1, keepdims=True)
629+
.repeat(3, -1)
630+
)
631+
else:
632+
noise = self.np_random.integers(
633+
0,
634+
255,
635+
size=render_out.shape,
636+
dtype=np.uint8,
637+
)
638+
639+
mask = (
640+
self.np_random.random(render_out.shape[0:2])
641+
< self.probability_of_noise_per_pixel
642+
)
643+
644+
return np.where(mask[..., None], noise, render_out)
645+
646+
647+
class ObstructView(
648+
gym.Wrapper[ObsType, ActType, ObsType, ActType], gym.utils.RecordConstructorArgs
649+
):
650+
"""Randomly obstructs rendering with white noise patches.
651+
652+
If used with ``render_mode="rgb_array"`` and ``AddRenderObservation``, it will
653+
make observations noisy.
654+
The number of patches depends on how many pixels we want to obstruct.
655+
Depending on the size of the patches, the environment may become
656+
partially-observable, turning the MDP into a POMDP.
657+
658+
Example - Obstruct 50% of the pixels with patches of size 50x50 pixels:
659+
>>> env = gym.make("LunarLander-v3", render_mode="rgb_array")
660+
>>> env = ObstructView(env, obstructed_pixels_ratio=0.5, obstruction_width=50)
661+
>>> env = HumanRendering(env)
662+
>>> obs, _ = env.reset(seed=123)
663+
>>> obs, *_ = env.step(env.action_space.sample())
664+
"""
665+
666+
def __init__(
667+
self,
668+
env: gym.Env[ObsType, ActType],
669+
obstructed_pixels_ratio: float,
670+
obstruction_width: int,
671+
is_noise_grayscale: bool = False,
672+
):
673+
"""Wrapper obstructs pixels with white noise patches.
674+
675+
Args:
676+
env: The environment that is being wrapped
677+
obstructed_pixels_ratio: the percentage of pixels obstructed with white noise
678+
obstruction_width: the width of the obstruction patches
679+
is_noise_grayscale: if True, RGB noise is converted to grayscale
680+
"""
681+
if not 0 <= obstructed_pixels_ratio < 1:
682+
raise ValueError(
683+
f"obstructed_pixels_ratio should be in the interval [0,1). Received {obstructed_pixels_ratio}"
684+
)
685+
686+
if obstruction_width < 1:
687+
raise ValueError(
688+
f"obstruction_width should be larger or equal than 1. Received {obstruction_width}"
689+
)
690+
691+
gym.utils.RecordConstructorArgs.__init__(
692+
self,
693+
obstructed_pixels_ratio=obstructed_pixels_ratio,
694+
obstruction_width=obstruction_width,
695+
is_noise_grayscale=is_noise_grayscale,
696+
)
697+
gym.Wrapper.__init__(self, env)
698+
699+
self.obstruction_centers_ratio = obstructed_pixels_ratio / obstruction_width**2
700+
self.obstruction_width = obstruction_width
701+
self.is_noise_grayscale = is_noise_grayscale
702+
703+
def render(self) -> RenderFrame:
704+
"""Compute the render frames as specified by render_mode attribute during initialization of the environment, then add white noise patches."""
705+
render_out = super().render()
706+
707+
render_shape = render_out.shape
708+
n_pixels = render_shape[0] * render_shape[1]
709+
n_obstructions = int(n_pixels * self.obstruction_centers_ratio)
710+
centers = self.np_random.integers(0, n_pixels, n_obstructions)
711+
centers = np.unravel_index(centers, (render_shape[0], render_shape[1]))
712+
mask = np.zeros((render_shape[0], render_shape[1]), dtype=bool)
713+
low = self.obstruction_width // 2
714+
high = self.obstruction_width - low
715+
for x, y in zip(*centers):
716+
mask[
717+
max(x - low, 0) : min(x + high, render_shape[0]),
718+
max(y - low, 0) : min(y + high, render_shape[1]),
719+
] = True
720+
721+
if self.is_noise_grayscale:
722+
noise = (
723+
self.np_random.integers(
724+
(0, 0, 0),
725+
255 * np.array([0.2989, 0.5870, 0.1140]),
726+
size=render_out.shape,
727+
dtype=np.uint8,
728+
)
729+
.sum(-1, keepdims=True)
730+
.repeat(3, -1)
731+
)
732+
else:
733+
noise = self.np_random.integers(
734+
0,
735+
255,
736+
size=render_out.shape,
737+
dtype=np.uint8,
738+
)
739+
740+
return np.where(mask[..., None], noise, render_out)

Diff for: tests/wrappers/test_white_noise_rendering.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Test suite of AddWhiteNoise and ObstructView wrapper."""
2+
3+
import gymnasium as gym
4+
from gymnasium.wrappers import AddWhiteNoise, HumanRendering, ObstructView
5+
6+
7+
def test_white_noise_rendering():
8+
for mode in ["rgb_array"]:
9+
env = gym.make("CartPole-v1", render_mode=mode, disable_env_checker=True)
10+
env = AddWhiteNoise(env, probability_of_noise_per_pixel=0.5)
11+
env = HumanRendering(env)
12+
13+
assert env.render_mode == "human"
14+
env.reset()
15+
16+
for _ in range(75):
17+
_, _, terminated, truncated, _ = env.step(env.action_space.sample())
18+
if terminated or truncated:
19+
env.reset()
20+
21+
env.close()
22+
23+
env = gym.make("CartPole-v1", render_mode=mode, disable_env_checker=True)
24+
env = ObstructView(env, obstructed_pixels_ratio=0.5, obstruction_width=100)
25+
env = HumanRendering(env)
26+
27+
assert env.render_mode == "human"
28+
env.reset()
29+
30+
for _ in range(75):
31+
_, _, terminated, truncated, _ = env.step(env.action_space.sample())
32+
if terminated or truncated:
33+
env.reset()
34+
35+
env.close()

0 commit comments

Comments
 (0)