Randomizer Nodes
Each randomizer node changes one aspect of the scene. A node receives the Context when the tree executes and mutates it in place. Nodes are appended to a Randomizer in the order they should run: instances first, then poses, then appearance, then the camera.
Import
from telekinesis.illusion.randomizer.randomizer_node import (
RandomizerNode,
ObjectInstanceRandomizer,
ObjectPoseRandomizer,
MaterialRandomizer,
BackgroundRandomizer,
CameraPoseRandomizer,
)target_objects is matched by prefix throughout, so passing "part_1" selects every instance registered under that name.
ObjectInstanceRandomizer
Samples how many instances of each model are visible in the scene, and reveals them.
ObjectInstanceRandomizer(
target_objects=[], # model names to randomize
min_num_total_objects=None, # lower bound across all targets
max_num_total_objects=None, # upper bound across all targets
)Parameters
| Parameter | Type | Description |
|---|---|---|
target_objects | list[str] | Names of the models whose instance count is randomized. |
min_num_total_objects | int | None | Minimum number of visible objects across all targets. None uses the sum of each model's min_number_instances. |
max_num_total_objects | int | None | Maximum number of visible objects across all targets. None uses the sum of each model's max_number_instances, and the value is capped at that sum in any case. |
A total is drawn uniformly between the two bounds. Each model first receives its own minimum, then the remaining slots are distributed across the models with the lowest fill ratio, so clutter stays balanced rather than concentrating on one part. Revealed instances also get their rigid body enabled, so they participate in the physics simulation.
Raises a ValueError when the minimum exceeds the maximum, and a RuntimeError when target_objects is empty.
Usage
randomizer.add_randomizer(
randomizer_node=ObjectInstanceRandomizer(
target_objects=["part_1", "part_2"],
min_num_total_objects=2,
max_num_total_objects=4,
),
node_name="instance_randomizer_objects",
)ObjectPoseRandomizer
Samples 6-DoF poses for the visible target objects, rejecting poses that collide.
ObjectPoseRandomizer(
pose_sampling_function, # callable applied to each object
target_objects=[], # model names to place
max_tries=10, # collision-free attempts per object
sample_on_surface=None, # name of a surface object
surface_name_resolver=None, # callable resolving that name at runtime
)Parameters
| Parameter | Type | Description |
|---|---|---|
pose_sampling_function | Callable[[Object], None] | Sets the location and rotation of the object passed to it. Called once per attempt. |
target_objects | list[str] | Names of the models to place. Empty places every visible object in the scene. |
max_tries | int | Attempts to place an object without collision before it is hidden for the current scene. |
sample_on_surface | str | None | Name of the object whose surface the poses are sampled on, for example a bin. |
surface_name_resolver | Callable[[Context], str] | None | Resolves the surface name from the context at randomization time. Used when the surface differs per scene, and only consulted when sample_on_surface is None. |
The sampling function defines the distribution; the node handles collision checking. When an object cannot be placed collision-free within max_tries, it is hidden for that scene, its rigid body is disabled, and it is removed from the visible objects – so the annotations never reference an object that is not there.
When a surface is given, poses are sampled onto it with additional spacing and ray-cast checks, and objects are dropped onto the surface geometry. Combine this with simulate_physics=True for realistic settling.
Usage
import numpy as np
from telekinesis.illusion.types.object import Object
def sample_pose(obj: Object):
obj.set_location(np.random.uniform((-0.1, -0.1, 0.1), (0.1, 0.1, 0.1)))
obj.set_rotation(np.random.uniform((-180, -180, -180), (180, 180, 180)))
randomizer.add_randomizer(
randomizer_node=ObjectPoseRandomizer(
pose_sampling_function=sample_pose,
target_objects=["part_1", "part_2"],
),
node_name="pose_randomizer",
)MaterialRandomizer
Assigns a random material to each target object.
MaterialRandomizer(
target_objects, # model names to re-material
context, # the Context whose materials are loaded
types=None, # material type tags to sample from
)Parameters
| Parameter | Type | Description |
|---|---|---|
target_objects | list[str] | Names of the models whose material is randomized. |
context | Context | The context whose material manager loads the requested types. |
types | list[str] | None | Material type tags to sample from, matching sub-directories of materials/ in the asset directory, for example ["metal"]. None samples from every available type. |
The requested types are loaded into the context when the node is constructed, so materials are read from disk once rather than per scene. Use one node per material family – parts in metal, bins in plastic – rather than one node for the whole scene.
Usage
randomizer.add_randomizer(
randomizer_node=MaterialRandomizer(
target_objects=["part_1", "part_2"], types=["metal"], context=context
),
node_name="material_randomizer_parts",
)
randomizer.add_randomizer(
randomizer_node=MaterialRandomizer(
target_objects=["crate_2"], types=["plastic"], context=context
),
node_name="material_randomizer_crate",
)BackgroundRandomizer
Samples an HDRI background, which also drives the scene lighting.
BackgroundRandomizer(
hdris_root=None, # directory holding the HDRIs
categories=None, # HDRI categories to sample from
)Parameters
| Parameter | Type | Description |
|---|---|---|
hdris_root | Path | None | Root directory of the HDRI assets. None uses the hdris/ sub-directory of the resolved asset directory. |
categories | list[str] | None | Categories to sample from. None samples from every category. |
Available categories:
| Category | Description |
|---|---|
indoor/industrial | Factory and workshop environments. |
indoor/studio | Neutral studio lighting. |
indoor/misc | Other indoor environments. |
outdoor/day | Daylight. |
outdoor/morning | Morning light. |
outdoor/evening | Evening light. |
outdoor/night | Night. |
The catalog is built when the node is constructed. A FileNotFoundError is raised when no HDRI matches the requested categories.
Usage
randomizer.add_randomizer(
randomizer_node=BackgroundRandomizer(
categories=["indoor/industrial", "indoor/studio"]
),
node_name="background_randomizer",
)CameraPoseRandomizer
Samples one camera pose per view and keyframes it, so a single randomized scene yields several images.
CameraPoseRandomizer(
pose_sampling_function, # callable returning (location, euler_angles)
number_of_views=0, # rendered images per scene
**kwargs, # forwarded to the sampling function
)Parameters
| Parameter | Type | Description |
|---|---|---|
pose_sampling_function | Callable[..., tuple[np.ndarray, np.ndarray]] | Returns a location and XYZ Euler angles. Receives the Context as context plus every keyword argument passed to the node. |
number_of_views | int | Number of camera poses sampled per scene, and therefore images rendered per scene. |
**kwargs | Forwarded verbatim to the sampling function on every call. |
Camera Pose Samplers
Ready-made samplers live in telekinesis.illusion.sampler.camera_pose_sampler:
from telekinesis.illusion.sampler.camera_pose_sampler import (
shell_sampler,
volume_sampler,
)shell_sampler places the camera on a spherical shell around a center point:
| Parameter | Type | Default | Description |
|---|---|---|---|
center | np.ndarray | list[str] | None | None | The point the shell is centered on, or the names of the objects to center on. |
radius_min / radius_max | float | 0.4 / 0.6 | Bounds on the camera distance from the center, in meters. |
elevation_min / elevation_max | float | -90.0 / 90.0 | Bounds on the elevation angle, in degrees. |
azimuth_min / azimuth_max | float | -180.0 / 180.0 | Bounds on the azimuth angle, in degrees. |
inplane_rot_min / inplane_rot_max | float | -60.0 / 60.0 | Bounds on the in-plane camera rotation, in degrees. |
volume_sampler places the camera inside a volume and orients it towards a point of interest:
| Parameter | Type | Default | Description |
|---|---|---|---|
point_of_interst | np.ndarray | list[str] | None | None | The point the camera looks at, or the names of the objects to look at. |
volume_size | tuple[float, float, float] | None | None | Size of the sampling volume. Derived from the scene when omitted. |
volume_center | np.ndarray | None | None | Center of the sampling volume. |
distance_range | tuple[float, float] | None | None | Bounds on the camera distance from the point of interest. |
inplane_rot_min / inplane_rot_max | float | -60.0 / 60.0 | Bounds on the in-plane camera rotation, in degrees. |
Usage
randomizer.add_randomizer(
randomizer_node=CameraPoseRandomizer(
pose_sampling_function=shell_sampler,
number_of_views=2,
radius_min=0.5,
radius_max=0.7,
),
node_name="camera_pose_randomizer",
)Custom Nodes
Any other axis of variation can be added by subclassing RandomizerNode and implementing randomize():
import numpy as np
from telekinesis.illusion.randomizer.randomizer_node import RandomizerNode
from telekinesis.illusion.core.context import Context
class ScaleRandomizer(RandomizerNode):
def __init__(self, target_objects, scale_range):
self.target_objects = target_objects
self.scale_range = scale_range
def randomize(self, context: Context) -> None:
for name, obj in context.get_objects().items():
if name.startswith(tuple(self.target_objects)) and not obj.is_hidden():
obj.set_scale(np.random.uniform(*self.scale_range))
randomizer.add_randomizer(
randomizer_node=ScaleRandomizer(["part_1"], (0.9, 1.1)),
node_name="scale_randomizer",
)
