all lessons/synthetic_vision/05 · Blender image pipelinelesson 5 / 7

Build a Blender pipeline for an image model

Turn the contracts from Lessons 01–04 into an executable dataset system: compile scenarios, evaluate one authoritative Blender state, render model inputs and identity passes, export visible labels, reject invalid samples, train a baseline, and let held-out real evidence decide what to generate next.

The implementation principle
Blender is the forward executor, not the definition of the experiment. The task contract defines what the image model must infer. The scenario manifest defines what world to instantiate. The camera contract defines the measurement. The label contract defines supervision. Independent QA defines whether a sample may enter the dataset. A locked real evaluation defines whether the pipeline was useful.

0 · What we will build—and why the task is deliberately narrow

We will build a synthetic dataset for a single-image pedestrian detector and visible-instance segmenter. The running scene remains the dusk shuttle crossing: a pedestrian emerges from behind a parked van while the shuttle's front camera approaches. The model receives one processed RGB image and produces a pedestrian confidence, a visible mask, and a visible two-dimensional box. A temporal system may later combine those outputs to decide when to brake.

Why not train one image directly to output “brake” or “coast”? Before the pedestrian becomes visible, a world with a hidden pedestrian and an otherwise identical world without one can produce the same current image. The correct braking action may differ even though the available evidence is identical. A deterministic single-frame action label would ask the network to infer information absent from its input. It would reward accidental correlations—perhaps a particular van asset appears only when the hidden pedestrian is present. Visible segmentation is narrower but identifiable: the label answers which pixels currently contain observable pedestrian evidence. Lessons 06 and 07 will reconnect perception to real transfer and action-conditioned futures.

Definition of done
The lab is complete when one manifest record can reproduce one accepted sample bundle; camera projections agree with an analytic check; RGB, masks, and metadata refer to the same evaluated state and time; label semantics are explicit; causal lineages do not cross splits; a training loader consumes only released artifacts; nuisance interventions expose shortcuts; and a frozen real slice determines whether the synthetic data improved the image model.

The released dataset will look approximately like this:

blender_lab/
  base_scene.blend
  assets/asset_manifest.json
  configs/task.json
  manifests/scenarios.jsonl
  scripts/
    make_manifest.py
    blender_generate.py
    sensor_postprocess.py
    export_coco.py
    validate_dataset.py
    train_baseline.py
  dataset/run_0007/
    run.json
    samples/000042/
      rgb.png
      linear.exr
      passes.exr
      sample.json
      qa.json
    annotations/instances_train.json
    splits/train.txt
    splits/validation.txt
    splits/real_test_reference.txt
    qa/report.json

This layout is not sacred. The separation of responsibilities is. The model input, privileged render products, labels, manifests, and QA decisions must not be collapsed into anonymous neighboring files whose relationship is inferred from matching filenames.

1 · The fundamental problem: privileged Blender state is not model evidence

A Blender scene exposes geometry, object identity, transforms, materials, animation curves, lights, and render settings. The deployed image model sees none of those objects. It receives a finite array of numbers after light transport, optics, exposure, sensor noise, color processing, resizing, and compression. The generator has privileged access to a hidden world state s; the learner receives an observation x and an exported target :

m ∼ pdesign(m) → s=B(m) → r=R(s,c) → x=P(r,η) → ỹ=L(s,r,c) → fω(x)

Read the chain from left to right:

The intended semantic target y and exported target coincide only if every convention in that chain is correct. If an instance pass comes from the previous frame, the network learns the time-skewed mask. If all positive images use one pedestrian asset, the network may learn its texture. If the color pipeline adds a bright halo at mask boundaries, the network may detect the exporter. If fully hidden pedestrians are assigned visible boxes, the optimizer is punished for not recovering inaccessible state. More rendering makes each wrong relationship more precisely estimated.

Three obligations follow

ObligationFundamental reasonFailure if omitted
Identify the intended ruleMany predictors fit the same finite training files.The model uses a cheaper simulator-only shortcut.
Keep measurement and truth synchronizedThe learner sees exported arrays, not design intent.Systematic label error becomes supervision.
Test outside the generatorA simulator cannot establish its own agreement with reality.Perfect synthetic metrics certify only an internally consistent hypothesis.

This is why a dataset pipeline is not equivalent to a render loop. A render loop maps scene state to pictures. A dataset pipeline creates a controlled joint distribution over model inputs, targets, metadata, and splits, then measures whether training on that distribution changes real performance.

2 · Start with the tempting generator—and break it

The first attempt usually resembles this:

import bpy
import random

for sample_id in range(10_000):
    randomize_objects(random)
    randomize_lights(random)
    bpy.context.scene.render.filepath = f"images/{sample_id:06d}.png"
    bpy.ops.render.render(write_still=True)

It is short, and it can produce convincing images. It is not yet a trustworthy training pipeline. Follow the missing information linearly:

  1. No consuming task. The script does not say whether the model predicts visible masks, amodal boxes, depth, pose, or action. Therefore “correct output” is undefined.
  2. No frozen proposal. Randomness occurs inside an opaque worker. A failed sample cannot be reconstructed without recovering global random state and every call order.
  3. No structured distribution. Independent sliders can make wet roads under a dry sky, headlights off at night, pedestrians floating outside walkable regions, or only one valid critical case in thousands of draws.
  4. No authoritative identity. Object names and selection order can change when an artist edits the file. A filename does not say which visible pixels belong to which semantic instance.
  5. No camera contract. Resolution, crop, lens, sensor fit, pose convention, exposure time, color transform, and resize are implicit.
  6. No synchronized label path. Even a later mask exporter can evaluate another dependency-graph state, time, camera, or visibility definition.
  7. No provenance or replay level. Blender version, renderer, GPU, scene hash, asset revision, code revision, and seed streams are absent.
  8. No acceptance gate. Empty, clipped, mislabeled, overwritten, or numerically invalid samples silently enter training.
  9. No lineage split. Adjacent frames and near-identical variants may be randomly scattered across training and validation.
  10. No external objective. Throughput and synthetic accuracy can improve while held-out real detection becomes worse.
Scaling law for pipeline bugs
If a systematic exporter fault affects every sample, rendering ten million images does not average it away. It increases the effective sample size supporting the wrong rule. Verify one end-to-end sample before optimizing images per second.

3 · Derive the minimum architecture

Each component below exists because it owns a different claim. Merging them is possible in software, but their logical authority should remain distinct.

ComponentWhat it ownsWhy it must be inspectable
task.jsonInput bytes, outputs, label semantics, deployment envelope, metricsWithout it, the generator has no criterion for relevant fidelity.
asset_manifest.jsonStable identities, families, units, licenses, allowed splitsBlender names and file locations are not durable semantic identity.
make_manifest.pyConditional distribution, constraints, split lineage, named seedsA proposal must be reviewed and replayed before expensive rendering.
base_scene.blendReusable geometry, materials, collections, camera rig, world templateArt state must be certified as metric and simulation-ready.
blender_generate.pyManifest application, scene evaluation, render passes, raw metadataThe forward execution boundary must be deterministic enough to diagnose.
sensor_postprocess.pyDeclared noise, tone mapping, resize, compressionCycles radiance is not automatically the deployed camera tensor.
export_coco.pyVisible mask, box, category, COCO serializationFormat conversion must not silently change semantic truth.
validate_dataset.pyIndependent invariants and release statusThe producer must not grade itself using the same assumptions.
train_baseline.pyOnly released inputs/labels and declared mixtureTraining must be unable to read privileged simulator fields.

The resulting state machine is:

planned → proposal-valid → rendered → postprocessed → label-valid → QA-accepted → released │ │ │ │ │ └─reject reason and immutable evidence at the first failed boundary──────┘

Do not silently redraw a failed proposal with a new seed. Suppose severe occlusions fail more often because the mask exporter mishandles them. Retrying with a fresh random scene selectively removes difficult examples and changes the accepted distribution. A retry should repeat the same manifest after a transient system failure. A semantic rejection should remain recorded with its reason, so acceptance probability can be measured by slice.

4 · Write the image task contract before opening Blender

The lab's contract can be expressed in a small versioned JSON document:

{
  "task_version": "pedestrian_visible_v1",
  "input": {
    "file": "rgb.png",
    "width": 1280,
    "height": 720,
    "channels": "sRGB_8bit",
    "camera": "rgb_front",
    "measurement": "global_shutter_mid_exposure_baseline"
  },
  "targets": {
    "category_ids": {"pedestrian": 1},
    "instance_mask": "visible_rendered_surface",
    "bbox_xywh": "tight_rectangle_of_visible_mask",
    "min_labeled_pixels": 4,
    "crowd_policy": "separate_instances_when_identity_is_authoritative"
  },
  "deployment": {
    "range_m": [3.0, 60.0],
    "illumination": ["day", "dusk", "night"],
    "occlusion_fraction": [0.0, 1.0]
  },
  "acceptance": {
    "primary": "held_out_real_AP",
    "slices": ["dusk", "partial_occlusion", "small_object"],
    "downstream": ["first_detection_time", "stopping_margin"]
  }
}

Every field eliminates an ambiguity. The dimensions determine intrinsics and resize. The channel declaration determines whether training loads display-referred bytes or scene-linear values. “Visible rendered surface” says a fully occluded pedestrian has no mask even if Blender knows the actor exists. The box is derived from the mask, not from projected three-dimensional bounds. The measurement declaration begins with global shutter because it is the smallest testable camera model; rolling shutter will be added only after the baseline is correct.

Why detection and segmentation share a label source

If a visible box is computed from projected 3D vertices while the mask is computed after occlusion, the two targets can disagree. A pedestrian behind the van may have a large projected amodal box and a two-pixel visible mask. Derive both visible targets from one authoritative instance raster:

Mi(u,v)=1 if the first task-visible surface at pixel (u,v) belongs to instance i;   bi=bounds{(u,v):Mi(u,v)=1}

An amodal box can be useful, but it is a separate target requiring a separate name and evaluation policy. Do not substitute it for the visible box merely because Blender makes hidden geometry easy to access.

5 · Convert the prebuilt .blend file into a world template

A prebuilt scene often contains excellent art but weak contracts. Before procedural sampling, organize it into explicit collections:

WORLD_STATIC
  road
  curb
  buildings
ACTOR_SOCKETS
  van_anchor
  pedestrian_path
SENSORS
  rgb_front
CALIBRATION_TARGETS
  projection_cube
LIGHTING_RIGS
  sun
  sky
  practical_lights

Collection names help humans navigate. Persistent custom properties carry machine semantics:

ped = bpy.data.objects["PedestrianRoot"]
ped["semantic_class"] = "pedestrian"
ped["instance_uuid"] = "actor:pedestrian:0042"
ped["asset_family_id"] = "human_pack_03"
ped["label_policy"] = "visible_surface"
ped["source_license_id"] = "asset_license_017"

for mesh_part in pedestrian_mesh_parts:
    mesh_part["instance_uuid"] = ped["instance_uuid"]
    mesh_part.pass_index = 17

Why not use Pedestrian.001? Blender may append suffixes when objects are duplicated, linked, or reimported. Names are useful handles, not guaranteed identities. One logical pedestrian can also contain body, clothing, hair, and accessories as several mesh objects. All task-visible parts need one instance identity or a declared part-to-instance mapping.

Asset certification checklist

Build tiny calibration scenes before trusting the street: one fronto-parallel plane for depth, one cube for projection and normals, two overlapping objects for visibility, one object crossing the image boundary, and one moving vertical pole for temporal alignment. A complex street is a poor first debugger because too many mechanisms can explain one wrong pixel.

6 · Compile conditional scenario manifests outside Blender

A manifest is a frozen proposal for one experiment. Generate it with ordinary Python before opening Blender. This makes distribution review cheap, separates sampling from rendering, and prevents accidental dependence on the order in which Blender happens to call random functions.

{
  "schema_version": "scenario_v3",
  "sample_id": "000042",
  "scenario_family_id": "van_occlusion_dusk_009",
  "split": "train",
  "template_hash": "sha256:...",
  "time_s": 0.0,
  "timeline": {"fps": 30.0, "frame": 1, "subframe": 0.0},
  "pedestrian": {
    "present": true,
    "asset_family_id": "human_pack_03",
    "instance_id": 17,
    "emergence_time_s": -0.08,
    "speed_mps": 1.7,
    "path_id": "crosswalk_path_A"
  },
  "van": {
    "asset_family_id": "delivery_van_02",
    "anchor_id": "curb_slot_03",
    "lateral_offset_m": 0.15
  },
  "camera": {
    "id": "rgb_front",
    "exposure_ms": 4.0,
    "readout_ms": 0.0
  },
  "environment": {
    "condition": "dusk",
    "road_wetness": 0.2,
    "headlights_on": true
  },
  "seeds": {
    "scenario": 348621,
    "appearance": 991021,
    "renderer": 662817,
    "sensor": 181220
  }
}

Sample causes, not independent sliders

At dusk, sun elevation, sky luminance, artificial lights, headlight state, exposure, noise, and pedestrian visibility are related. Sampling them independently can create a bright noon sky with maximum sensor gain and headlights off on a pitch-black road. Some rare combinations are real and should remain possible; contradictions should not appear merely because the generator forgot dependencies.

Represent the generator as a conditional program:

condition = sample(["day", "dusk", "night"], weights)
sun = sample_sun_given(condition)
sky = sample_sky_given(condition, weather)
headlights_on = sample_headlights_given(condition, vehicle_policy)
exposure = camera_controller(scene_luminance, controller_version)
road_state = sample_road_given(weather, recent_weather)
pedestrian_path = sample_reachable_path(map_affordances)
van_pose = sample_valid_curb_pose(map_affordances)
accept only if geometry, visibility family, and task constraints hold

The manifest generator should calculate cheap constraints before rendering: the van lies on a legal curb anchor; the pedestrian path begins on a walkable region; actor bounding volumes do not interpenetrate; camera distance is inside the contract; requested asset families belong to the declared split; and the coarse projected pedestrian region intersects the image. Expensive visibility and sensor checks occur after rendering.

Separate random streams

One global seed is not enough. Adding a harmless extra draw for cloud texture can shift every subsequent actor pose. Derive named seeds from an immutable root, sample ID, and stream name using a stable cryptographic hash—not Python's process-randomized hash():

import hashlib

def derive_seed(root_seed, sample_id, stream):
    payload = f"{root_seed}:{sample_id}:{stream}".encode("utf-8")
    digest = hashlib.blake2b(payload, digest_size=8).digest()
    return int.from_bytes(digest, "little")

scenario_seed = derive_seed(20260718, "000042", "scenario")
appearance_seed = derive_seed(20260718, "000042", "appearance")
render_seed = derive_seed(20260718, "000042", "renderer")
sensor_seed = derive_seed(20260718, "000042", "sensor")

Named streams let paired counterfactuals share geometry while changing clothing, or share the entire world while changing only sensor noise. Record the choice. “Same seed” has no causal meaning unless the stream's ownership and timing are defined.

Assign splits before rendering

Split by causal lineage, not by image. A scenario family, trajectory, pedestrian asset family, van family, or environment template can create many near-duplicate images. If siblings cross training and validation, memorization looks like generalization. Hash the highest required holdout unit to a split before rendering, and verify that every descendant inherits it.

7 · Run Blender headlessly with a narrow command boundary

Pin a Blender version and invoke the worker with the template already loaded:

blender --background base_scene.blend \
  --python scripts/blender_generate.py -- \
  --manifest manifests/scenarios.jsonl \
  --sample-id 000042 \
  --output dataset/run_0007

Blender interprets arguments before --; the script owns arguments after it. A minimal parser is:

import argparse
import sys

def parse_args():
    argv = sys.argv
    argv = argv[argv.index("--") + 1:] if "--" in argv else []
    parser = argparse.ArgumentParser()
    parser.add_argument("--manifest", required=True)
    parser.add_argument("--sample-id", required=True)
    parser.add_argument("--output", required=True)
    return parser.parse_args(argv)

Prefer Blender's data API for deterministic edits. Operators under bpy.ops often depend on active object, selection, mode, editor area, or other UI context. Rendering itself is normally an operator, but setting transforms, materials, custom properties, and node links can usually use direct data access.

Configure the render contract explicitly

import bpy

scene = bpy.context.scene
scene.render.engine = "BLENDER_EEVEE_NEXT"  # or CYCLES, chosen by the contract
scene.render.resolution_x = 1920  # native render resolution
scene.render.resolution_y = 1080
scene.render.resolution_percentage = 100
scene.render.film_transparent = False

# The minimal baseline is one evaluated instant, not motion-integrated RGB.
scene.render.use_motion_blur = False

The raw worker writes scene-linear Combined color and privileged passes to a multilayer EXR. It does not treat Blender's display preview as rgb.png. The external sensor postprocessor is the single authority that turns Combined linear color into the released display-referred training image. Blender preview view-transform and look settings are still recorded for visual audits, but they cannot silently alter training bytes. This also avoids relying on color-look names that can change between Blender/OCIO versions.

Apply one manifest, then force evaluation

from mathutils import Vector

van_xyz = resolve_curb_anchor(
    manifest["van"]["anchor_id"],
    manifest["van"]["lateral_offset_m"]
)
pedestrian_xyz = evaluate_path(
    manifest["pedestrian"]["path_id"],
    manifest["time_s"] - manifest["pedestrian"]["emergence_time_s"],
    manifest["pedestrian"]["speed_mps"]
)
frame = manifest["timeline"]["frame"]
subframe = manifest["timeline"].get("subframe", 0.0)

# Evaluate animation first; it may overwrite object transforms.
scene.frame_set(frame, subframe=subframe)
bpy.context.view_layer.update()

def set_world_translation(object_name, xyz):
    obj = bpy.data.objects[object_name]
    world = obj.matrix_world.copy()
    world.translation = Vector(xyz)
    obj.matrix_world = world  # meaningful even when the object has a parent

def set_actor_present(root_name, present):
    root = bpy.data.objects[root_name]
    for obj in [root, *root.children_recursive]:
        obj.hide_render = not present
        obj["label_enabled"] = bool(present)

set_world_translation("VanRoot", van_xyz)
set_actor_present("PedestrianRoot", manifest["pedestrian"]["present"])
if manifest["pedestrian"]["present"]:
    set_world_translation("PedestrianRoot", pedestrian_xyz)

# Resolve constraints, armatures, geometry nodes, and the final camera state.
bpy.context.view_layer.update()

resolve_curb_anchor and evaluate_path are task-specific functions that turn semantic manifest choices into metric poses using the certified map and path definitions. This is preferable to placing actors at arbitrary world coordinates inside the render worker: the manifest remains readable as an experiment, while the template adapter owns engine-specific geometry.

The order is deliberate. Frame evaluation occurs first because animation and drivers can overwrite pose. The adapter then applies manifest-controlled state through designated roots or controller inputs and updates the dependency graph again. obj.location is parent-local, so a helper claiming to set world state must use matrix_world or an explicit parent transform. If a root is constrained, write the constraint/controller input rather than fighting its evaluated output. An absent actor must be disabled consistently in beauty rendering, label enumeration, and any collision/physics components; merely moving it off camera can leave shadows, reflections, or contacts.

What reproducibility can honestly promise

Record bpy.app.version_string, the full build hash when available, render engine, device, sample count, denoiser, template and asset hashes, script revision, manifest hash, operating system, and relevant driver/GPU identifiers. A fixed seed may replay the semantic scene exactly while producing small floating-point or path-tracing differences across hardware. Define levels:

Most production pipelines need semantic replay and stable model behavior more than cross-device bitwise equality. The claim must nevertheless be executable.

8 · Make camera geometry explicit and test it twice

A Blender camera looks along its local −Z axis with local +Y up. A common computer-vision camera frame uses +Z forward, +X right, and +Y down. Confusing the conventions can mirror labels, invert depth, or produce projections that look plausible near the center and fail elsewhere.

Bind and configure the active camera from the contract rather than trusting an artist's last saved UI state:

camera = bpy.data.objects["rgb_front"]
assert camera.type == "CAMERA"
scene.camera = camera

camera.data.type = "PERSP"
camera.data.lens = 12.0
camera.data.sensor_width = 11.52
camera.data.sensor_height = 6.48
camera.data.sensor_fit = "HORIZONTAL"
camera.data.shift_x = 0.0
camera.data.shift_y = 0.0
scene.render.pixel_aspect_x = 1.0
scene.render.pixel_aspect_y = 1.0

assert scene.render.resolution_x == 1920
assert scene.render.resolution_y == 1080
assert scene.render.resolution_percentage == 100

In a production adapter, values may be asserted against a calibrated prebuilt rig rather than overwritten. Either approach must fail loudly on disagreement. The active scene camera, projection type, clipping planes, lens, sensor dimensions and fit, shifts, pixel aspect, crop, resolution percentage, and final resize all belong to the measurement contract.

For a simple uncropped horizontal sensor with square pixels, image width W, height H, focal length fmm, sensor width sw, and zero camera shift:

fx = W fmm/sw,   fy=fx,   cx=W/2,   cy=H/2

Suppose the calibration exercise uses 1920×1080 pixels, 12 mm focal length, an 11.52×6.48 mm sensor, square pixels, and zero shift. Then fx=12×1920/11.52=2000 px and fy=12×1080/6.48=2000 px, so:

K = [[2000, 0, 960], [0, 2000, 540], [0, 0, 1]]

Real Blender configurations may include sensor_fit, pixel aspect ratio, camera shift, render percentage, border crop, and later resize. Those operations change the effective intrinsics. Export both the native-render intrinsics and the final-training-image intrinsics. If an image is resized by sx,sy, then focal lengths and principal point coordinates scale by the corresponding factor. A crop also subtracts its origin before resize.

Transform Blender camera coordinates to the CV convention

For the convention above, the axis conversion from Blender camera coordinates to CV camera coordinates is:

Rbcam→cv = diag(1, −1, −1)

If Tworld←bcam is the evaluated Blender camera world transform, invert it to map world points into Blender camera coordinates, then apply the axis conversion. State the direction of every transform in the serialized name; a generic field called camera_pose invites inversion mistakes.

import numpy as np

depsgraph = bpy.context.evaluated_depsgraph_get()
camera_eval = camera.evaluated_get(depsgraph)
T_world_from_bcam = np.asarray(camera_eval.matrix_world, dtype=np.float64)
T_bcam_from_world = np.linalg.inv(T_world_from_bcam)

T_cv_from_bcam = np.eye(4)
T_cv_from_bcam[:3, :3] = np.diag([1.0, -1.0, -1.0])
T_cv_from_world = T_cv_from_bcam @ T_bcam_from_world

Independent projection test

Do not validate projection using only the same function that exports it. Place calibration points at known camera-frame coordinates. Project analytically with u=fxX/Z+cx, v=fyY/Z+cy. Independently project their Blender world locations using bpy_extras.object_utils.world_to_camera_view, converting its normalized bottom-up coordinates to the declared image convention. Serialize the evaluated camera transform shown above so constraints or animation cannot leave metadata stale.

This lab uses continuous coordinates measured from the top-left image edge: pixel (i,j) has center (i+0.5,j+0.5), and the even-sized image center is (W/2,H/2). Thus normalized Blender coordinates map as u=W xndc, v=H(1-yndc). A library using integer-centered pixels requires a −0.5 conversion. Pin this convention in both exporter and QA; otherwise a half-pixel disagreement can masquerade as lens error. Require subpixel agreement and repeat after resolution percentage, crop, and training resize.

9 · Define the measurement time before enabling motion effects

For the first working pipeline, use a global-shutter mid-exposure state with Blender motion blur explicitly disabled, as configured above. If the manifest specifies exposure interval [t0,t1], evaluate RGB and truth at the declared reference time, often (t0+t1)/2. Record exposure start, end, reference time, frame, subframe, and interpolation policy. When motion blur is later enabled, define whether the supervision is a reference-time mask or time-integrated coverage; an instantaneous Object Index mask is not automatically the support of blurred RGB.

Ordinary Blender motion blur is not a rolling shutter. Motion blur integrates motion over an exposure interval. Rolling shutter additionally changes the interval by row: lower rows may observe a later pose than upper rows. If deployment depends on this effect, introduce it as a tested extension:

  1. Divide the image into row bands.
  2. Assign each band its acquisition interval from the readout direction and total readout time.
  3. Evaluate the actor and camera state for that band's reference time.
  4. Render or otherwise approximate only that band.
  5. Construct labels using the same band-wise time policy.
  6. Stitch bands and convergence-test 8, 16, and 32 bands against cost and geometry metrics.

Why delay this feature? A wrong global-shutter pipeline plus a rolling-shutter approximation creates two entangled timing hypotheses. First prove projection, visibility, and labels at one time. Then add the minimum temporal complexity whose downstream sensitivity justifies it.

Worked consequence
Suppose an ideal render permits reliable pedestrian detection at 10.5 m, while nominal stopping requires 9.83 m. The apparent margin is +0.67 m. At 10 m/s, a 100 ms measurement or processing delay consumes 1.0 m, changing the margin to −0.33 m. A visually small timing simplification can reverse the safety conclusion. Measure fidelity through detection time and margin, not only image similarity.

10 · Export RGB and privileged passes from one evaluated scene

The safest boundary is one evaluated scene state feeding one render result with multiple passes. Enable the passes explicitly:

scene = bpy.context.scene
view_layer = bpy.context.view_layer

view_layer.use_pass_z = True
view_layer.use_pass_normal = True
view_layer.use_pass_object_index = True

scene.render.image_settings.file_format = "OPEN_EXR_MULTILAYER"
scene.render.image_settings.color_depth = "32"
scene.render.filepath = str(sample_dir / "passes.exr")
bpy.ops.render.render(write_still=True)

passes.exr is the one raw render authority and includes the Combined scene-linear channels plus enabled privileged passes. The external postprocessor extracts Combined RGB into linear.exr, applies the declared sensor/ISP/preprocessing chain, and writes the only released model input, rgb.png. It then records hashes linking all three. Do not also save an AgX/Filmic preview under the training filename.

linear_rgb = read_multilayer_exr(
    sample_dir / "passes.exr",
    channels=["Combined.R", "Combined.G", "Combined.B"]
)
write_linear_exr(sample_dir / "linear.exr", linear_rgb)
training_rgb = camera_postprocess(linear_rgb, sensor_seed, camera_contract)
write_png(sample_dir / "rgb.png", training_rgb)

In practice, compositor file-output nodes can also write products in one render. Test exact filenames: compositor nodes may append frame suffixes, relative paths beginning with // are relative to the .blend file, and careless configuration can overwrite neighboring workers. Write to a sample-specific temporary directory and enumerate the expected outputs before atomic release.

Identity choices: object index, Cryptomatte, or dedicated label render

MethodStrengthRisk and policy
Object Index passSimple integer-like per-object identity through pass_indexLogical instances with several objects need shared mapping; boundary antialiasing semantics differ from RGB; store float EXR, not color-managed PNG.
CryptomatteStable name-derived mattes with fractional edge coverage and compositing supportExtraction is more involved; identities still require durable semantic mapping; coverage is not automatically a hard categorical mask.
Dedicated emission/flat label renderFull control over class and instance encodingA second render can drift in state or visibility; color management, transparency, antialiasing, and material overrides must be isolated and tested.

For the minimal lab, use an object-index EXR and a sample-local map from numeric pass index to stable instance UUID and category. Treat the pass as privileged evidence, never as a model input. If the target requires antialiased soft coverage, declare how fractional boundaries become training labels. If it requires a hard mask, declare the sampling rule. There is no format-neutral notion of “the exact edge” once pixels integrate subpixel coverage.

Depth, range, normals, and transparency

The Blender Z pass must be tested against the lesson's depth definition rather than assumed. For a CV camera point (X,Y,Z), camera-z is Z; Euclidean range is √(X²+Y²+Z²). They agree only on the principal ray. If a render product encodes ray distance, convert it only with the known intrinsics and pixel ray. Store metric values in EXR or another documented floating-point encoding; an eight-bit PNG is not metric depth unless a scale, offset, saturation policy, and invalid code are defined.

Normals require a named frame—world, Blender camera, or CV camera—and a direction convention. Test a fronto-parallel plane with a known normal. Transparent windows, alpha-masked hair, volumetric fog, and refractive objects need task-specific visibility rules. The first surface affecting RGB is not always identical to the first opaque geometric surface. Do not claim universal ground truth where the task contract has not resolved the ambiguity.

Keep beauty RGB and diagnostic radiance separate

Write at least:

Training must not discover the EXR or instance pass by directory convention. A dataset loader should receive an allowlist of released input fields. Privileged products are for supervision, audits, and future tasks, not accidental extra channels.

11 · Turn rendered radiance into the camera tensor

Cycles approximates light transport. Eevee approximates a real-time rendering pipeline. Neither is automatically the shuttle's camera, sensor electronics, ISP, or deployment preprocessing. The smallest useful postprocess starts with a declared input and adds only effects whose omission changes the task.

# Illustrative camera-noise boundary; parameters require calibration.
mu_e = np.maximum(linear_rgb * electrons_per_linear_unit, 0.0)
shot_e = rng.poisson(mu_e)  # sample arrivals before saturation
charge_e = shot_e + rng.normal(0.0, read_noise_e, shot_e.shape)
charge_e = np.clip(charge_e, 0.0, full_well_e)  # sensor saturation
normalized_linear = charge_e / full_well_e
display_rgb = srgb_encode(normalized_linear)  # exactly one display encoding
resized_rgb = resize_display_rgb(display_rgb, width=1280, height=720)
training_rgb = quantize_u8(resized_rgb)

Shot noise variance grows with expected electrons; read noise adds approximately signal-independent variance. Saturation occurs after stochastic charge accumulation: clipping the Poisson mean first would let severely overexposed pixels fluctuate below full well incorrectly. The display encoding occurs exactly once. But electrons_per_linear_unit is not an absolute physical constant supplied by Blender. Calibrate the mapping with real flat fields or a reference-target experiment, and represent uncertainty if exposure, aperture, transmittance, quantum efficiency, or ISP gain are not identified.

This minimal code omits spectral sensitivity, Bayer sampling, demosaicing, lens distortion, point-spread function, flare, auto-exposure dynamics, rolling shutter, denoising, sharpening, white balance, clipping behavior, JPEG artifacts, and thermal effects. That is acceptable only when the task is insensitive to them or when a staged experiment has not yet justified the cost. List omissions explicitly so that a failure can be traced to the observation model rather than misdiagnosed as insufficient scene diversity.

Order matters

Adding noise after downsampling is not equivalent to adding sensor noise before resize. JPEG before resize is not equivalent to JPEG after resize. Auto-exposure computed from a full image may leak information unavailable to a cropped model. The task contract should name the exact order used for both synthetic and real preprocessing:

scene-linear render → optics/PSF → exposure and sensor noise → ISP/color transform → resize/crop → compression → training tensor

Where a stage is omitted, it should be an identity transformation in the conceptual chain, not a forgotten unknown.

Play: Cycles radiance is not the camera tensor
This is a clean intensity ramp (shadows on the left, highlights on the right) after a real sensor model: each pixel collects electrons, shot noise grows like √(signal), a fixed read-noise floor sits under everything, and the well saturates (clips) past full capacity. Shadows are read-noise limited (grainy, low SNR); highlights are shot-noise limited. Raise exposure to lift shadow SNR — and watch the highlights blow out. This is the gap between a renderer's radiance and the bytes your model actually trains on; §11 is where you calibrate it.
SNR in shadows
SNR at mid-gray
Highlights clipped
Verdict
Show the core JS
e      = signal · exposure                 // electrons collected (0..full-well FW)
noise  = √(e + read²)                       // shot (√e) + read-noise floor
meas   = clip(e + N(0, noise), 0, FW)       // saturation at full well
SNR(s) = e / √(e + read²);  clip = e > FW    // shadows read-limited, highs shot-limited

12 · Derive visible labels and COCO records from the identity raster

Let the decoded instance image contain numeric identity i at each task-visible pixel. For every released instance:

  1. Construct a binary visible mask Mi.
  2. Count visible pixels and apply the declared minimum-size policy.
  3. Find xmin,ymin,xmax,ymax over positive pixels.
  4. Export COCO bbox=[x_min,y_min,width,height], using one documented inclusive/exclusive convention.
  5. Encode the visible mask as polygons or run-length encoding, recording the library and version.
  6. Map stable instance UUID to the task's category ID.
  7. Store area, visibility fraction if defined, crowd policy, scenario ID, camera/time ID, and provenance link.
def mask_to_xywh(mask):
    ys, xs = np.nonzero(mask)
    if len(xs) == 0:
        return None
    x0, x1 = int(xs.min()), int(xs.max())
    y0, y1 = int(ys.min()), int(ys.max())
    return [x0, y0, x1 - x0 + 1, y1 - y0 + 1]

A fully hidden pedestrian has present=true in privileged state and an empty visible mask. For this task, that actor does not become a positive visible instance. Keep present, projected amodal extent, and occlusion diagnostics in privileged metadata if useful, but do not punish the image model for failing to see through the van.

What exactly is visibility fraction?

A common definition is visible mask area divided by an amodal projected area. But “amodal area” can mean projected mesh raster with occluders removed, a silhouette from 3D bounds, or a canonical unposed area. These denominators differ. Define one. For example:

visibilityi = visible_surface_pixelsi / amodal_surface_pixelsi under the same camera, pose, sampling rule, and image bounds

Rendering an amodal silhouette by temporarily hiding occluders is a second scene query. Freeze time and camera, restore visibility flags afterward, and test that it cannot mutate the beauty render. If the denominator is zero because the actor is behind the camera or outside the image, use a declared invalid value rather than dividing silently.

Minimum COCO structure

{
  "images": [{
    "id": 42,
    "file_name": "samples/000042/rgb.png",
    "width": 1280,
    "height": 720
  }],
  "annotations": [{
    "id": 42017,
    "image_id": 42,
    "category_id": 1,
    "bbox": [612, 281, 19, 48],
    "area": 603,
    "segmentation": {"counts": "...", "size": [720, 1280]},
    "iscrowd": 0
  }],
  "categories": [{"id": 1, "name": "pedestrian"}]
}

COCO is a serialization contract, not a truth authority. Two valid COCO files can encode different visibility policies, category mappings, crowd definitions, or polygon approximations. Publish the semantic contract beside the JSON.

13 · Make the sample bundle authoritative

sample.json should join all products by identifiers and hashes rather than filename guesses:

{
  "sample_id": "000042",
  "scenario_family_id": "van_occlusion_dusk_009",
  "split": "train",
  "status": "qa_accepted",
  "measurement": {
    "camera_id": "rgb_front",
    "reference_time_s": 0.0,
    "exposure_start_s": -0.002,
    "exposure_end_s": 0.002,
    "width": 1280,
    "height": 720,
    "K_final": [[1333.3, 0, 640], [0, 1333.3, 360], [0, 0, 1]]
  },
  "files": {
    "rgb": {"path": "rgb.png", "sha256": "..."},
    "linear": {"path": "linear.exr", "sha256": "..."},
    "passes": {"path": "passes.exr", "sha256": "..."}
  },
  "instance_map": {
    "17": {
      "instance_uuid": "actor:pedestrian:0042",
      "category_id": 1,
      "asset_family_id": "human_pack_03"
    }
  },
  "provenance": {
    "blender_version": "...",
    "blender_build_hash": "...",
    "template_sha256": "...",
    "asset_manifest_sha256": "...",
    "generator_revision": "...",
    "task_version": "pedestrian_visible_v1",
    "scenario_manifest_sha256": "..."
  }
}

The metadata should also include every rejection and repair decision, coordinate convention, native and final intrinsics, evaluated transforms, render settings, seed tree, and label-contract version. A sample is not reproducible merely because its RGB can be recreated; the supervision and acceptance decision must be replayable too.

14 · Build an independent QA gate before batching

QA is not a gallery of attractive images. It is a set of falsifiable invariants that connect contracts across boundaries.

BoundaryInvariantBug it catches
FilesExpected files exist, hashes match, dimensions/channels are correct, values are finitePartial writes, stale files, wrong encoding, NaNs
IdentityEvery nonzero instance ID maps to exactly one allowed UUID/categoryUnlabeled objects, reused pass indices, name drift
Mask ↔ boxEvery visible box tightly encloses exactly its mask; empty masks have no visible boxAmodal/visible confusion, off-by-one conversion
ProjectionKnown 3D points project within tolerance in native and resized imagesAxis flips, inverse poses, crop/resize errors
DepthCalibration plane and cube agree with analytic camera-z/rangeWrong pass meaning, units, invalid encoding
TimeRGB, pass, transform, and label use one declared measurement eventPrevious-frame labels, stale dependency graph
CounterfactualChanging clothing or background seed does not change geometric labelsState coupling, exporter mutation
ReplayManifest replay meets semantic or bitwise contractHidden randomness, version drift
LineageNo held-out family has descendants in trainingNear-duplicate leakage

Metamorphic tests when exact answers are hard

A metamorphic test changes one input and predicts which outputs may change:

These tests expose accidental coupling more effectively than manual inspection. They also state the intended causal graph in executable form.

Dataset-level acceptance

After per-sample checks, audit distributions: accepted/rejected count by cause, class and empty-frame balance, mask area, distance, visibility, truncation, illumination, asset family, background family, color and contrast, camera parameters, and joint critical slices such as small + occluded + dusk. A marginal histogram can look diverse while the important joint event is absent.

Compare proposal and accepted distributions. If 40% of severe-occlusion proposals fail but 2% of clear proposals fail, the released dataset is selected toward easy visibility. Fix the pipeline or explicitly reweight; do not report only the accepted count.

15 · Scale as a resumable transaction, not one giant render loop

Once calibration scenes and a 32–100 sample pilot pass, separate planning from execution. A controller reads immutable manifest records, claims sample IDs, launches isolated workers, records statuses, and releases only atomically completed bundles.

for record in planned_manifest:
    if ledger[record.sample_id].status == "released":
        continue

    claim(record.sample_id, worker_id)
    run_blender(record, temporary_sample_dir)
    run_sensor_postprocess(record, temporary_sample_dir)
    run_label_export(record, temporary_sample_dir)
    qa = validate(record, temporary_sample_dir)

    if qa.accepted:
        fsync_and_atomic_rename(temporary_sample_dir, released_sample_dir)
        mark_released(record.sample_id, hashes=qa.hashes)
    else:
        preserve_failure_evidence(record, temporary_sample_dir, qa)
        mark_rejected(record.sample_id, reason=qa.first_failure)

Atomicity

Never let a training job observe a directory while a renderer is still populating it. Write to samples/.partial/000042.worker_7, compute expected hashes, flush files, then atomically rename within the same filesystem to samples/000042. The ledger should move to released only after the rename. A crash then leaves either the previous released bundle or a clearly temporary bundle, not a half-valid sample with a final name.

Process lifetime and cleanup

Starting Blender for every sample maximizes isolation but pays startup and template-load cost. Rendering thousands of samples in one process reduces startup cost but can accumulate orphaned meshes, materials, images, node groups, caches, and GPU memory. A practical compromise is a bounded shard—perhaps 20–200 samples depending on scene complexity—followed by a clean process restart. Measure resident memory and render latency across a shard; do not assume deletion of objects frees every data block.

If a worker reuses the template, it must restore all mutable state: object transforms and visibility, materials, light parameters, animation state, compositor paths, camera configuration, world nodes, render settings, pass indices, random seeds, and temporary objects. Reloading the base file between samples is slower but simpler. Begin with correctness; optimize only after equivalence tests prove the faster reset path.

Parallel workers

Storage and render budget

Scene-linear multilayer EXR can dominate storage. Keep it during pilot and calibration. At scale, decide which privileged products remain online, compressed, archived, or regenerable. Deleting all diagnostic passes saves cost but can make future failures impossible to localize. A tiered policy might retain every final RGB and label, full passes for all validation samples and high-risk training slices, and a reproducible subset for the rest.

Renderer sample count should be selected by downstream sensitivity. Render the same manifests at 16, 64, and 256 samples per pixel; compare small-pedestrian evidence, label stability, training metrics, and cost. If denoising at 16 samples shifts thin limbs or removes four-pixel pedestrians, a low average image error does not justify it. If 64 and 256 produce indistinguishable real detection at four times the cost, 256 is waste.

16 · Split by causal lineage, then audit shortcuts

Define split groups before generation. For this lab, the validation set can hold out complete scenario families and pedestrian asset families. A stronger real-transfer experiment also holds out van family, background neighborhood, and part of the dusk/occlusion range. The split answers a specific question:

HoldoutWhat success would showWhat it would not show
Frames from known scenariosInterpolation across sampled timesNew scenario or asset generalization
Scenario familiesGeneralization to new causal configurationsUnseen appearance families
Pedestrian asset familiesLess reliance on one mesh/clothing libraryReality transfer
Background/van familiesReduced context memorizationCorrect sensor statistics
Locked real sliceSome evidence of deployment transferAll future environments or closed-loop safety

Construct a shortcut on purpose

Create an educational failure: in generator version A, positive images use pedestrian family human_pack_03 with saturated red clothing, while empty scenes and other actors use muted colors. A detector can achieve high random-split synthetic accuracy by using red texture. Evaluate paired interventions:

  1. Same positive scene, replace red clothing with gray.
  2. Same empty scene, place an irrelevant red sign.
  3. Same geometry and label, change only background illumination.
  4. Hold out the entire pedestrian family.

If confidence follows red rather than pedestrian geometry, the paired test identifies the shortcut more directly than a lower validation score. Repair the generator by crossing clothing color with presence and asset family, then preserve the pairs in a regression suite. Do not simply add more random colors; measure whether the intended invariance was learned.

17 · Train the smallest baseline that can test the pipeline

The purpose of the first model is to test data, not win an architecture competition. Use a conventional small detector or segmenter with a fixed training recipe. Freeze preprocessing and record whether the backbone uses real-image pretraining. A pretrained backbone contains real visual knowledge, so “trained on synthetic data” does not mean “synthetic-only information.” Report it honestly.

Minimum experiment matrix

RunTraining evidenceQuestion
RTrusted real training set onlyWhat is the current baseline?
SSynthetic onlyWhat transfers without real task supervision?
S→RSynthetic pretraining, then real fine-tuningDoes synthetic evidence improve initialization or label efficiency?
R+SControlled real/synthetic batch mixtureDoes continuing synthetic coverage help or cause negative transfer?
Snaive→R vs Svalidated→REqual-volume generator ablationDid the pipeline improvements matter, or merely sample count?

Control total optimizer steps or report both compute and samples. If synthetic pretraining receives ten times more compute, an improvement cannot be attributed to data source alone. Plot real-label learning curves: performance using 1%, 5%, 20%, and 100% of trusted real labels after the same synthetic stage. A useful result may be equal held-out real risk with fewer real labels rather than a higher maximum score.

Metrics from pixels to decision

A detector may improve aggregate AP while detecting the critical emerging pedestrian 100 ms later because its small-object confidence changed. The decision proxy reveals why the camera approximation and occlusion distribution matter.

Keep generation feedback away from the locked test

Use a real development set to identify broad failure hypotheses such as small dusk pedestrians or reflective jackets. Convert a hypothesis into a parameterized generator family and test it on synthetic validation plus a separate real validation set. Do not repeatedly inspect every locked-test failure and generate replicas; that turns the test into training data through the generator. When the locked set influences design, retire and replace it.

18 · Diagnose results through causal ablations

Keep scenario manifests fixed and change one pipeline mechanism at a time:

  1. Ideal sharp RGB versus calibrated noise and exposure.
  2. Global shutter versus banded rolling shutter.
  3. One pedestrian family versus multiple crossed families.
  4. Random frame split versus scenario-family split.
  5. Default Blender color settings versus pinned color contract.
  6. Denoising off versus on at equal renderer samples.
  7. Visible mask labels versus mistakenly projected amodal boxes.
  8. Unstructured independent randomization versus conditional manifests.
  9. Object-index masks versus independently verified Cryptomatte extraction.
  10. Equal synthetic volume with and without shortcut-breaking paired interventions.

For every ablation, predict which artifact and metric should change before running it. If changing sensor-noise seed changes a geometry label, the pipeline violates its own authority boundary. If a more photoreal render improves synthetic validation but hurts locked real dusk recall, appearance quality did not solve the relevant gap. If the family split causes a large collapse while the random split does not, the model memorized assets rather than learning the promised rule.

Failure triage order

Observed failureFirst questionDo not begin with
Mask offsetDo RGB/pass/time/crop identifiers and projection tests agree?A bigger network
Excellent synthetic, weak realWhich content, sensor, label, or shortcut slice differs?More random textures everywhere
Poor small-pedestrian recallAre scale, PSF, resize, denoising, noise, and mask policy correct?Only class reweighting
Validation much better than family holdoutWhich ancestor or asset leaked?Celebrating the random split
Low accepted yieldWhich proposal families fail and how does selection change density?Silently resampling until enough images exist
Nondeterministic replayWhich named stream, version, device, or mutable state differs?Assuming the root seed is sufficient

19 · Raw bpy, BlenderProc, Kubric, Infinigen, and other engines

The reasoning does not require writing every layer from raw Blender Python. Frameworks can remove boilerplate, but they do not own your deployment contract.

ToolUse it whenIt gives youYou still own
Raw bpyYou need unusual scene logic, exact Blender control, or want to learn every boundaryDirect access to scene data, render passes, compositor, and automationSampling, identity, writers, QA, versioning, batching, semantics
BlenderProcYou want a dataset-oriented Blender abstractionLoaders, samplers, rendering helpers, segmentation maps, physics placement, COCO/BOP/HDF5-style writersTask distribution, valid scenario logic, sensor fidelity, split lineage, real acceptance
KubricYou want Python scene specifications and research-oriented controlled scenesSeparation of scene construction, simulation/rendering, and rich metadataMaintenance fit, asset contracts, deployment distribution, measurement validity
InfinigenYou need procedural natural worlds and geometry-rich variationLarge procedural Blender scenes and assetsYour task, controllable event support, camera calibration, labels, transfer test
Unreal/Unity/Isaac Sim/ReplicatorYour existing scene, physics, robotics, or real-time stack lives thereDifferent asset ecosystems, physics, sensors, runtime, and orchestration toolsThe same contracts, causal coverage, QA, lineage, and held-out real evidence

A small BlenderProc translation

BlenderProc can replace much of the pass and COCO plumbing. The following uses the current documented main-branch pattern as accessed in July 2026: configure camera intrinsics and at least one camera-to-world pose, enable segmentation before one render, then write COCO. Pin a released BlenderProc version or commit because APIs and output keys evolve, and invoke the file through blenderproc run rather than ordinary system Python or raw blender --python.

import blenderproc as bproc

bproc.init()
objects = bproc.loader.load_blend(args.scene)

for obj in objects:
    obj.set_cp("category_id", category_for(obj))

# This lab stores edge-origin coordinates; BlenderProc's K helper uses
# integer-centered OpenCV pixels, so convert the principal point by -0.5.
K_bproc = K_native_edge.copy()
K_bproc[0, 2] -= 0.5
K_bproc[1, 2] -= 0.5
bproc.camera.set_intrinsics_from_K_matrix(
    K_bproc, image_width=1920, image_height=1080
)
T_world_from_bcam = bproc.math.change_source_coordinate_frame_of_transformation_matrix(
    T_world_from_cv_camera, ["X", "-Y", "-Z"]
)
bproc.camera.add_camera_pose(T_world_from_bcam)

bproc.renderer.enable_segmentation_output(
    map_by=["category_id", "instance", "name"]
)
data = bproc.renderer.render()

bproc.writer.write_coco_annotations(
    args.output,
    instance_segmaps=data["instance_segmaps"],
    instance_attribute_maps=data["instance_attribute_maps"],
    colors=data["colors"],
    color_file_format="JPEG"
)
blenderproc run scripts/blenderproc_generate.py +  --scene base_scene.blend --manifest manifests/scenarios.jsonl

This is valuable infrastructure, not proof that the generated data answers the task. category_for, the CV-camera-to-world pose conversion, object identity, timing configuration, conditional scenario manifest, visible-label policy, rejection rules, lineage split, and real evaluation remain yours. Wrap framework outputs in the same authoritative bundle and independent QA.

Why not convert RGB or depth afterward into every sensor?

A Blender depth image is not a device-accurate automotive LiDAR scan: LiDAR samples rays at beam-specific times and wavelengths, with material-dependent returns, dropout, multipath, and electronics. It is not radar: radar measures delay, Doppler, angle, interference, and material response. Choose a framework with the required sensor mechanism or implement a calibrated one. Do not rename a convenient raster and treat the label as evidence.

20 · A linear build schedule

The recommended order is intentionally conservative:

  1. Day 1—contract: freeze input bytes, visible mask/box semantics, camera convention, real slices, and metrics.
  2. Day 2—template: certify units, identities, collections, camera rig, and calibration scenes.
  3. Day 3—one manifest: instantiate exactly one static global-shutter sample with no randomization.
  4. Day 4—geometry proof: validate projection, visible identity, box-mask equality, depth/range, and replay.
  5. Day 5—postprocess: create the exact training RGB and test color/noise/resize order.
  6. Day 6—structured proposals: add conditional dusk, van, pedestrian, camera, and named seed streams.
  7. Day 7—pilot: generate 32–100 samples, inspect every rejection, and audit joint distributions.
  8. Week 2—baseline: train R, S, and S→R under equal budgets; run family and paired shortcut tests.
  9. Then scale: add resumable workers, bounded shards, atomic release, and storage policy.
  10. Only after evidence: add rolling shutter, richer sensor effects, generative refinement, or another engine when a measured bottleneck justifies it.
Why this order is minimal
Each stage creates an invariant needed to interpret the next. Without a contract, a label cannot be declared correct. Without a certified template, a manifest does not identify metric state. Without one evaluated state, passes cannot be synchronized. Without synchronized passes, COCO is only well-formatted uncertainty. Without QA, batching multiplies unknown errors. Without lineage splits, validation is contaminated. Without a real anchor, synthetic success cannot establish transfer.

21 · Production release checklist

Task and distribution

Blender state and camera

Outputs and supervision

Operations and evidence

22 · Exercises and self-test

  1. Unobservable target. A pedestrian is completely behind the van, but the manifest says present=true. Should the single-image visible detector receive a positive box? No. The current RGB contains no visible pedestrian evidence. Preserve presence as privileged state or a separate belief target, not a visible box.
  2. Axis conversion. Blender camera coordinates report a point as (1, 2, −10). What are its coordinates under the stated CV convention? (1, −2, 10), because the conversion is diag(1,−1,−1).
  3. Resize. A native 1920×1080 image with fx=fy=2000 and principal point (960,540) is resized uniformly to 960×540. What is the new calibration? Focal lengths become 1000; principal point becomes (480,270), assuming no crop and aligned pixel convention.
  4. Visible box. Positive mask pixels span x=12…20 and y=7…15 inclusive. What COCO xywh does the helper emit? [12,7,9,9]. State the inclusive pixel-support convention.
  5. Random streams. You add one random cloud draw and every pedestrian changes. What design error caused it? One sequential global stream coupled unrelated processes. Derive named seeds by sample and mechanism.
  6. Selection. Severe occlusions fail QA ten times more often than clear views. Why is rendering more replacement samples unsafe? The accepted distribution becomes selected toward clear cases. Preserve rejects, fix the mechanism, or model acceptance and restore the target density.
  7. Motion. Can Cycles motion blur be presented as rolling-shutter fidelity? No. Exposure integration and row-dependent readout are different mechanisms. Implement and validate row/band timing explicitly.
  8. Pass semantics. Why should identity remain EXR or structured data instead of a display PNG? Color management, quantization, interpolation, and antialiasing can change categorical IDs. Use a declared decoding path and test boundaries.
  9. Leakage. Frames 100–120 of one emergence are in training and frames 121–140 in validation. What is wrong? They share scene, assets, trajectory, and causal state. Keep the entire lineage in one split.
  10. Shortcut test. Confidence falls when red clothing becomes gray and rises on empty scenes containing red signs. What did the model learn? A color/context shortcut. Cross color with labels and preserve paired interventions in evaluation.
  11. Reproducibility. Identical manifests give slightly different Cycles RGB on two GPUs while masks and geometry match. Is replay broken? Bitwise replay is, but semantic replay may satisfy the declared tolerance. Record hardware and never claim more reproducibility than tested.
  12. Framework choice. Does using BlenderProc remove the need for a label contract? No. It can implement segmentation and writers, but only the task defines visibility, ontology, timing, splits, and acceptance.
  13. Model perspective. Why must the training loader be unable to see present=true, asset ID, or instance EXR as input? The deployed model will not receive them. Exposing privileged fields changes the task and can create impossible shortcuts.
  14. Value. Synthetic AP increases by 12 points, but locked real AP and dusk detection time do not change. Was the pipeline successful? Not for the stated deployment objective. It may have improved fit to the synthetic process without reducing the relevant real gap.

Capstone implementation prompt

Design review
Starting from a prebuilt base_scene.blend, design a 10,000-image pedestrian dataset. Submit: (1) the task contract; (2) asset and collection contract; (3) conditional scenario graph; (4) manifest schema and named seed derivation; (5) Blender headless boundary; (6) camera matrix and axis conversion; (7) global-shutter measurement event; (8) RGB, EXR, identity, visible-mask, and COCO paths; (9) five calibration scenes and ten invariants; (10) causal split groups; (11) atomic batch state machine; (12) real/synthetic experiment matrix; and (13) a feedback policy that does not leak the locked test. For every component, state what ambiguity it removes and what uncertainty remains.

Primary sources and practical references

Final takeaway
A synthetic image pipeline is an executable argument about evidence. Begin with what the model can observe and what decision its prediction serves. Compile structured scenarios before rendering. Let Blender instantiate and measure one authoritative state. Derive visible labels from the same event. Prove camera, time, identity, and serialization with independent invariants. Scale only accepted immutable bundles, split by lineage, train under controlled mixtures, and let untouched real performance—not Blender beauty or frame count—decide what to improve.