Skip to content

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

python
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.

python
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

ParameterTypeDescription
target_objectslist[str]Names of the models whose instance count is randomized.
min_num_total_objectsint | NoneMinimum number of visible objects across all targets. None uses the sum of each model's min_number_instances.
max_num_total_objectsint | NoneMaximum 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

python
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.

python
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

ParameterTypeDescription
pose_sampling_functionCallable[[Object], None]Sets the location and rotation of the object passed to it. Called once per attempt.
target_objectslist[str]Names of the models to place. Empty places every visible object in the scene.
max_triesintAttempts to place an object without collision before it is hidden for the current scene.
sample_on_surfacestr | NoneName of the object whose surface the poses are sampled on, for example a bin.
surface_name_resolverCallable[[Context], str] | NoneResolves 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

python
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.

python
MaterialRandomizer(
    target_objects,   # model names to re-material
    context,          # the Context whose materials are loaded
    types=None,       # material type tags to sample from
)

Parameters

ParameterTypeDescription
target_objectslist[str]Names of the models whose material is randomized.
contextContextThe context whose material manager loads the requested types.
typeslist[str] | NoneMaterial 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

python
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.

python
BackgroundRandomizer(
    hdris_root=None,   # directory holding the HDRIs
    categories=None,   # HDRI categories to sample from
)

Parameters

ParameterTypeDescription
hdris_rootPath | NoneRoot directory of the HDRI assets. None uses the hdris/ sub-directory of the resolved asset directory.
categorieslist[str] | NoneCategories to sample from. None samples from every category.

Available categories:

CategoryDescription
indoor/industrialFactory and workshop environments.
indoor/studioNeutral studio lighting.
indoor/miscOther indoor environments.
outdoor/dayDaylight.
outdoor/morningMorning light.
outdoor/eveningEvening light.
outdoor/nightNight.

The catalog is built when the node is constructed. A FileNotFoundError is raised when no HDRI matches the requested categories.

Usage

python
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.

python
CameraPoseRandomizer(
    pose_sampling_function,   # callable returning (location, euler_angles)
    number_of_views=0,        # rendered images per scene
    **kwargs,                 # forwarded to the sampling function
)

Parameters

ParameterTypeDescription
pose_sampling_functionCallable[..., 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_viewsintNumber of camera poses sampled per scene, and therefore images rendered per scene.
**kwargsForwarded verbatim to the sampling function on every call.

Camera Pose Samplers

Ready-made samplers live in telekinesis.illusion.sampler.camera_pose_sampler:

python
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:

ParameterTypeDefaultDescription
centernp.ndarray | list[str] | NoneNoneThe point the shell is centered on, or the names of the objects to center on.
radius_min / radius_maxfloat0.4 / 0.6Bounds on the camera distance from the center, in meters.
elevation_min / elevation_maxfloat-90.0 / 90.0Bounds on the elevation angle, in degrees.
azimuth_min / azimuth_maxfloat-180.0 / 180.0Bounds on the azimuth angle, in degrees.
inplane_rot_min / inplane_rot_maxfloat-60.0 / 60.0Bounds on the in-plane camera rotation, in degrees.

volume_sampler places the camera inside a volume and orients it towards a point of interest:

ParameterTypeDefaultDescription
point_of_interstnp.ndarray | list[str] | NoneNoneThe point the camera looks at, or the names of the objects to look at.
volume_sizetuple[float, float, float] | NoneNoneSize of the sampling volume. Derived from the scene when omitted.
volume_centernp.ndarray | NoneNoneCenter of the sampling volume.
distance_rangetuple[float, float] | NoneNoneBounds on the camera distance from the point of interest.
inplane_rot_min / inplane_rot_maxfloat-60.0 / 60.0Bounds on the in-plane camera rotation, in degrees.

Usage

python
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():

python
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",
)
Workers
Drive a complete, sharded dataset run from a spec YAML instead of assembling the tree in Python.
Read more →