blender-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. blender_cli/__init__.py +151 -0
  2. blender_cli/__init__.pyi +55 -0
  3. blender_cli/alignment/__init__.py +37 -0
  4. blender_cli/alignment/_fal.py +74 -0
  5. blender_cli/alignment/depth.py +250 -0
  6. blender_cli/alignment/generation.py +248 -0
  7. blender_cli/alignment/integration.py +80 -0
  8. blender_cli/alignment/pipeline.py +200 -0
  9. blender_cli/alignment/pose.py +1453 -0
  10. blender_cli/alignment/types.py +165 -0
  11. blender_cli/alignment/viz.py +62 -0
  12. blender_cli/animation/__init__.py +15 -0
  13. blender_cli/animation/codegen.py +134 -0
  14. blender_cli/animation/keyframes.py +211 -0
  15. blender_cli/assets/__init__.py +14 -0
  16. blender_cli/assets/image.py +36 -0
  17. blender_cli/assets/material.py +578 -0
  18. blender_cli/assets/prefab.py +72 -0
  19. blender_cli/assets/registry.py +189 -0
  20. blender_cli/blenvy.py +214 -0
  21. blender_cli/blenvy_registry.py +237 -0
  22. blender_cli/build/__init__.py +18 -0
  23. blender_cli/build/build_types.py +9 -0
  24. blender_cli/build/context.py +179 -0
  25. blender_cli/build/generation_step.py +97 -0
  26. blender_cli/build/runner.py +74 -0
  27. blender_cli/cli/__init__.py +5 -0
  28. blender_cli/cli/__main__.py +5 -0
  29. blender_cli/cli/commands/__init__.py +65 -0
  30. blender_cli/cli/commands/align.py +524 -0
  31. blender_cli/cli/commands/anchor_cmd.py +47 -0
  32. blender_cli/cli/commands/animation_cmd.py +187 -0
  33. blender_cli/cli/commands/assets.py +94 -0
  34. blender_cli/cli/commands/blenvy_cmd.py +228 -0
  35. blender_cli/cli/commands/camera_cmd.py +120 -0
  36. blender_cli/cli/commands/candidates.py +289 -0
  37. blender_cli/cli/commands/inspect.py +124 -0
  38. blender_cli/cli/commands/instance_cmd.py +72 -0
  39. blender_cli/cli/commands/manifest.py +90 -0
  40. blender_cli/cli/commands/material_cmd.py +65 -0
  41. blender_cli/cli/commands/measure.py +58 -0
  42. blender_cli/cli/commands/modifier_cmd.py +179 -0
  43. blender_cli/cli/commands/object_cmd.py +80 -0
  44. blender_cli/cli/commands/op.py +2353 -0
  45. blender_cli/cli/commands/project.py +343 -0
  46. blender_cli/cli/commands/raycast.py +85 -0
  47. blender_cli/cli/commands/render.py +503 -0
  48. blender_cli/cli/commands/render_settings_cmd.py +114 -0
  49. blender_cli/cli/commands/repl_cmd.py +25 -0
  50. blender_cli/cli/commands/run.py +34 -0
  51. blender_cli/cli/commands/select.py +22 -0
  52. blender_cli/cli/commands/session_cmd.py +90 -0
  53. blender_cli/cli/commands/stats.py +20 -0
  54. blender_cli/cli/commands/terrain_cmd.py +116 -0
  55. blender_cli/cli/commands/world_cmd.py +91 -0
  56. blender_cli/cli/common.py +339 -0
  57. blender_cli/cli/main.py +74 -0
  58. blender_cli/cli/repl.py +121 -0
  59. blender_cli/constants.py +6 -0
  60. blender_cli/core/__init__.py +39 -0
  61. blender_cli/core/diagnostics.py +9 -0
  62. blender_cli/core/metadata.py +70 -0
  63. blender_cli/geometry/__init__.py +34 -0
  64. blender_cli/geometry/_erosion.py +114 -0
  65. blender_cli/geometry/field2d.py +306 -0
  66. blender_cli/geometry/heightfield.py +586 -0
  67. blender_cli/geometry/mask.py +232 -0
  68. blender_cli/geometry/pointset.py +603 -0
  69. blender_cli/geometry/spline.py +279 -0
  70. blender_cli/geometry/spline_ops.py +320 -0
  71. blender_cli/modifiers/__init__.py +15 -0
  72. blender_cli/modifiers/codegen.py +94 -0
  73. blender_cli/modifiers/modifier.py +185 -0
  74. blender_cli/modifiers/registry.py +346 -0
  75. blender_cli/project/__init__.py +6 -0
  76. blender_cli/project/project_file.py +2207 -0
  77. blender_cli/project/session.py +124 -0
  78. blender_cli/py.typed +0 -0
  79. blender_cli/render/__init__.py +46 -0
  80. blender_cli/render/camera.py +397 -0
  81. blender_cli/render/camera_path.py +321 -0
  82. blender_cli/render/context.py +1897 -0
  83. blender_cli/render/settings.py +286 -0
  84. blender_cli/render/world.py +170 -0
  85. blender_cli/scene/__init__.py +27 -0
  86. blender_cli/scene/anchor.py +65 -0
  87. blender_cli/scene/entity.py +542 -0
  88. blender_cli/scene/instances.py +524 -0
  89. blender_cli/scene/primitives.py +574 -0
  90. blender_cli/scene/scene.py +1613 -0
  91. blender_cli/scene/selection.py +273 -0
  92. blender_cli/snap/__init__.py +26 -0
  93. blender_cli/snap/axis.py +80 -0
  94. blender_cli/snap/objects.py +628 -0
  95. blender_cli/snap/results.py +190 -0
  96. blender_cli/types.py +292 -0
  97. blender_cli/utils/__init__.py +29 -0
  98. blender_cli/utils/placement.py +511 -0
  99. blender_cli/utils/spline_strip.py +233 -0
  100. blender_cli/utils/strings.py +30 -0
  101. blender_cli/utils/sweep.py +154 -0
  102. blender_cli-0.1.0.dist-info/METADATA +281 -0
  103. blender_cli-0.1.0.dist-info/RECORD +105 -0
  104. blender_cli-0.1.0.dist-info/WHEEL +4 -0
  105. blender_cli-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,151 @@
1
+ """
2
+ maps-creation SDK — agent-friendly procedural map generation.
3
+
4
+ Core types and primitives are re-exported here for convenience::
5
+
6
+ from blender_cli import Scene, plane, box
7
+
8
+ For less common utilities, import from submodules directly::
9
+
10
+ from blender_cli.utils.placement import perimeter_points
11
+ from blender_cli.types import AddResult
12
+ """
13
+
14
+ # Scene & entity primitives — the most-used API surface.
15
+ # Alignment
16
+ from blender_cli.alignment import (
17
+ AlignmentPipelineResult,
18
+ ComposeOptions,
19
+ ComposeResult,
20
+ GenerationOptions,
21
+ GenerationResult,
22
+ PoseEstimationOptions,
23
+ PoseEstimationResult,
24
+ compose_from_directory,
25
+ estimate_pose_from_directory,
26
+ generate_alignment_assets,
27
+ integrate_pose_result,
28
+ load_pose_result,
29
+ run_alignment_pipeline,
30
+ )
31
+
32
+ # Assets
33
+ from blender_cli.assets import Material
34
+
35
+ # Build
36
+ from blender_cli.build import BuildContext
37
+
38
+ # Geometry
39
+ from blender_cli.geometry import (
40
+ WILDCARD,
41
+ Field2D,
42
+ Heightfield,
43
+ Mask,
44
+ PointSet,
45
+ Spline,
46
+ SplineOp,
47
+ )
48
+
49
+ # Project
50
+ from blender_cli.project import ProjectFile, Session
51
+
52
+ # Render
53
+ from blender_cli.render import (
54
+ Camera,
55
+ CameraKeyframe,
56
+ CameraPath,
57
+ RenderContext,
58
+ focus,
59
+ still,
60
+ )
61
+ from blender_cli.scene import (
62
+ Anchor,
63
+ Entity,
64
+ Instances,
65
+ Scene,
66
+ Selection,
67
+ SnapSpec,
68
+ Transform,
69
+ as_entity,
70
+ box,
71
+ cone,
72
+ cylinder,
73
+ plane,
74
+ sphere,
75
+ torus,
76
+ )
77
+
78
+ # Blenvy
79
+ from blender_cli.blenvy import apply_bevy_components, to_ron
80
+ from blender_cli.blenvy_registry import BevyRegistry, ComponentInfo
81
+
82
+ # Snap
83
+ from blender_cli.snap import SnapPolicy
84
+ from blender_cli.snap import snap as snap_points
85
+
86
+ # Types
87
+ from blender_cli.types import Vec3
88
+
89
+ __all__ = [
90
+ "WILDCARD",
91
+ # alignment
92
+ "AlignmentPipelineResult",
93
+ # scene
94
+ "Anchor",
95
+ # blenvy registry
96
+ "BevyRegistry",
97
+ # build
98
+ "BuildContext",
99
+ # render
100
+ "Camera",
101
+ "CameraKeyframe",
102
+ "CameraPath",
103
+ "ComponentInfo",
104
+ "ComposeOptions",
105
+ "ComposeResult",
106
+ "Entity",
107
+ # geometry
108
+ "Field2D",
109
+ "GenerationOptions",
110
+ "GenerationResult",
111
+ "Heightfield",
112
+ "Instances",
113
+ "Mask",
114
+ # assets
115
+ "Material",
116
+ "PointSet",
117
+ "PoseEstimationOptions",
118
+ "PoseEstimationResult",
119
+ # project
120
+ "ProjectFile",
121
+ "Session",
122
+ "RenderContext",
123
+ "Scene",
124
+ "Selection",
125
+ # snap
126
+ "SnapPolicy",
127
+ "SnapSpec",
128
+ "Spline",
129
+ "SplineOp",
130
+ "Transform",
131
+ # types
132
+ "Vec3",
133
+ "apply_bevy_components",
134
+ "as_entity",
135
+ "box",
136
+ "compose_from_directory",
137
+ "cone",
138
+ "cylinder",
139
+ "estimate_pose_from_directory",
140
+ "focus",
141
+ "generate_alignment_assets",
142
+ "integrate_pose_result",
143
+ "load_pose_result",
144
+ "plane",
145
+ "run_alignment_pipeline",
146
+ "snap_points",
147
+ "sphere",
148
+ "still",
149
+ "to_ron",
150
+ "torus",
151
+ ]
@@ -0,0 +1,55 @@
1
+ from blender_cli.alignment import AlignmentPipelineResult as AlignmentPipelineResult
2
+ from blender_cli.alignment import ComposeOptions as ComposeOptions
3
+ from blender_cli.alignment import ComposeResult as ComposeResult
4
+ from blender_cli.alignment import GenerationOptions as GenerationOptions
5
+ from blender_cli.alignment import GenerationResult as GenerationResult
6
+ from blender_cli.alignment import PoseEstimationOptions as PoseEstimationOptions
7
+ from blender_cli.alignment import PoseEstimationResult as PoseEstimationResult
8
+ from blender_cli.alignment import compose_from_directory as compose_from_directory
9
+ from blender_cli.alignment import (
10
+ estimate_pose_from_directory as estimate_pose_from_directory,
11
+ )
12
+ from blender_cli.alignment import (
13
+ generate_alignment_assets as generate_alignment_assets,
14
+ )
15
+ from blender_cli.alignment import integrate_pose_result as integrate_pose_result
16
+ from blender_cli.alignment import load_pose_result as load_pose_result
17
+ from blender_cli.alignment import run_alignment_pipeline as run_alignment_pipeline
18
+ from blender_cli.assets import Material as Material
19
+ from blender_cli.blenvy_registry import BevyRegistry as BevyRegistry
20
+ from blender_cli.blenvy_registry import ComponentInfo as ComponentInfo
21
+ from blender_cli.build import BuildContext as BuildContext
22
+ from blender_cli.geometry import WILDCARD as WILDCARD
23
+ from blender_cli.geometry import Field2D as Field2D
24
+ from blender_cli.geometry import Heightfield as Heightfield
25
+ from blender_cli.geometry import Mask as Mask
26
+ from blender_cli.geometry import PointSet as PointSet
27
+ from blender_cli.geometry import Spline as Spline
28
+ from blender_cli.geometry import SplineOp as SplineOp
29
+ from blender_cli.project import ProjectFile as ProjectFile
30
+ from blender_cli.project import Session as Session
31
+ from blender_cli.render import Camera as Camera
32
+ from blender_cli.render import CameraKeyframe as CameraKeyframe
33
+ from blender_cli.render import CameraPath as CameraPath
34
+ from blender_cli.render import RenderContext as RenderContext
35
+ from blender_cli.render import focus as focus
36
+ from blender_cli.render import still as still
37
+ from blender_cli.scene import Anchor as Anchor
38
+ from blender_cli.scene import Entity as Entity
39
+ from blender_cli.scene import Instances as Instances
40
+ from blender_cli.scene import Scene as Scene
41
+ from blender_cli.scene import Selection as Selection
42
+ from blender_cli.scene import SnapSpec as SnapSpec
43
+ from blender_cli.scene import Transform as Transform
44
+ from blender_cli.scene import as_entity as as_entity
45
+ from blender_cli.scene import box as box
46
+ from blender_cli.scene import cone as cone
47
+ from blender_cli.scene import cylinder as cylinder
48
+ from blender_cli.scene import plane as plane
49
+ from blender_cli.scene import sphere as sphere
50
+ from blender_cli.scene import torus as torus
51
+ from blender_cli.snap import SnapPolicy as SnapPolicy
52
+ from blender_cli.snap import snap as snap_points
53
+ from blender_cli.types import Vec3 as Vec3
54
+
55
+ __all__: list[str] # noqa: PYI035
@@ -0,0 +1,37 @@
1
+ """Alignment pipeline public API."""
2
+
3
+ from blender_cli.alignment.generation import generate_alignment_assets
4
+ from blender_cli.alignment.integration import (
5
+ compose_from_directory,
6
+ integrate_pose_result,
7
+ load_pose_result,
8
+ )
9
+ from blender_cli.alignment.pipeline import (
10
+ estimate_pose_from_directory,
11
+ run_alignment_pipeline,
12
+ )
13
+ from blender_cli.alignment.types import (
14
+ AlignmentPipelineResult,
15
+ ComposeOptions,
16
+ ComposeResult,
17
+ GenerationOptions,
18
+ GenerationResult,
19
+ PoseEstimationOptions,
20
+ PoseEstimationResult,
21
+ )
22
+
23
+ __all__ = [
24
+ "AlignmentPipelineResult",
25
+ "ComposeOptions",
26
+ "ComposeResult",
27
+ "GenerationOptions",
28
+ "GenerationResult",
29
+ "PoseEstimationOptions",
30
+ "PoseEstimationResult",
31
+ "compose_from_directory",
32
+ "estimate_pose_from_directory",
33
+ "generate_alignment_assets",
34
+ "integrate_pose_result",
35
+ "load_pose_result",
36
+ "run_alignment_pipeline",
37
+ ]
@@ -0,0 +1,74 @@
1
+ """Shared fal.ai client helpers for alignment workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ import logging
11
+
12
+ _INSTALL_HINT = "Install it with `uv add fal-client` (or `pip install fal-client`)."
13
+
14
+
15
+ def require_fal_client(feature: str = "Alignment fal.ai integration") -> Any:
16
+ """Import ``fal_client`` with a consistent error message."""
17
+ try:
18
+ return importlib.import_module("fal_client")
19
+ except ImportError as exc: # pragma: no cover - dependency issue
20
+ msg = f"{feature} requires fal-client. {_INSTALL_HINT}"
21
+ raise RuntimeError(msg) from exc
22
+
23
+
24
+ def upload_file(path: str | Path) -> str:
25
+ """Upload a local file to Fal storage and return the remote URL."""
26
+ client = require_fal_client("Alignment generation")
27
+ return str(client.upload_file(str(Path(path))))
28
+
29
+
30
+ def upload_image(image: Any, *, fmt: str = "png") -> str:
31
+ """Upload a PIL image to Fal storage and return the remote URL."""
32
+ client = require_fal_client("Alignment fal.ai image upload")
33
+ return str(client.upload_image(image, fmt))
34
+
35
+
36
+ def subscribe(
37
+ application: str,
38
+ *,
39
+ arguments: dict[str, Any],
40
+ with_logs: bool = False,
41
+ logger: logging.Logger | None = None,
42
+ log_prefix: str = "",
43
+ client_timeout: float | None = None,
44
+ ) -> dict[str, Any]:
45
+ """Call ``fal_client.subscribe`` with optional queue log forwarding."""
46
+ client = require_fal_client("Alignment fal.ai inference")
47
+ subscribe_kwargs: dict[str, Any] = {
48
+ "arguments": arguments,
49
+ "with_logs": with_logs,
50
+ }
51
+
52
+ if with_logs and logger is not None:
53
+ in_progress_type = getattr(client, "InProgress", None)
54
+
55
+ def _on_queue_update(update: object) -> None:
56
+ if in_progress_type is not None and not isinstance(
57
+ update, in_progress_type
58
+ ):
59
+ return
60
+ entries = getattr(update, "logs", None) or []
61
+ for entry in entries:
62
+ if isinstance(entry, dict):
63
+ message = entry.get("message")
64
+ else:
65
+ message = getattr(entry, "message", None)
66
+ if message:
67
+ logger.info("%s%s", log_prefix, message)
68
+
69
+ subscribe_kwargs["on_queue_update"] = _on_queue_update
70
+
71
+ if client_timeout is not None:
72
+ subscribe_kwargs["client_timeout"] = client_timeout
73
+
74
+ return client.subscribe(application, **subscribe_kwargs)
@@ -0,0 +1,250 @@
1
+ """Marigold depth helpers for silhouette/CMA-ES pose initialization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import logging
7
+ import urllib.request
8
+
9
+ import cv2
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+ import pyrender # pyright: ignore[reportMissingImports]
13
+ import trimesh
14
+ from PIL import Image # pyright: ignore[reportMissingImports]
15
+
16
+ from ._fal import require_fal_client
17
+ from ._fal import subscribe as fal_subscribe
18
+ from ._fal import upload_image as fal_upload_image
19
+ from .pose import ( # pyright: ignore[reportPrivateUsage]
20
+ M_SCENE_TO_GLB,
21
+ CameraParams,
22
+ _build_camera_pose_opengl,
23
+ _camera_intrinsics,
24
+ )
25
+
26
+ log = logging.getLogger("blender_cli.alignment.depth")
27
+
28
+ _FAL_MODEL_ID = "fal-ai/imageutils/marigold-depth"
29
+
30
+
31
+ class DepthEstimator:
32
+ """Monocular depth estimation using Marigold via fal.ai."""
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ ensemble_size: int = 10,
38
+ num_inference_steps: int = 10,
39
+ ) -> None:
40
+ require_fal_client("Depth initialization")
41
+ self._ensemble_size = ensemble_size
42
+ self._num_inference_steps = num_inference_steps
43
+ log.info(
44
+ "DepthEstimator ready (Marigold via fal.ai, ensemble=%d, steps=%d)",
45
+ ensemble_size,
46
+ num_inference_steps,
47
+ )
48
+
49
+ def estimate(self, image_np: npt.NDArray[np.uint8]) -> npt.NDArray[np.float32]:
50
+ """
51
+ Estimate relative depth from an RGB image.
52
+
53
+ Returns a depth map (H, W) in [0, 1] float range where
54
+ higher values = farther from camera.
55
+
56
+ """
57
+ h, w = image_np.shape[:2]
58
+
59
+ pil_img = Image.fromarray(image_np)
60
+ image_url = fal_upload_image(pil_img, fmt="png")
61
+ log.info("Uploaded query image to fal CDN, calling Marigold...")
62
+
63
+ result = fal_subscribe(
64
+ _FAL_MODEL_ID,
65
+ arguments={
66
+ "image_url": image_url,
67
+ "ensemble_size": self._ensemble_size,
68
+ "num_inference_steps": self._num_inference_steps,
69
+ },
70
+ with_logs=True,
71
+ logger=log,
72
+ log_prefix=" fal: ",
73
+ )
74
+
75
+ # Download and decode the depth map PNG, preserving bit depth.
76
+ # Marigold may return 8-bit or 16-bit PNG.
77
+ depth_url: str = result["image"]["url"]
78
+ with urllib.request.urlopen(depth_url) as resp: # noqa: S310
79
+ depth_bytes = resp.read()
80
+ depth_img = Image.open(io.BytesIO(depth_bytes))
81
+
82
+ # Convert to float preserving full dynamic range
83
+ if depth_img.mode == "I;16":
84
+ depth_np = np.asarray(depth_img, dtype=np.float32) / 65535.0
85
+ elif depth_img.mode in {"I", "F"}:
86
+ depth_np = np.asarray(depth_img, dtype=np.float32)
87
+ dmax = depth_np.max()
88
+ if dmax > 0:
89
+ depth_np /= dmax
90
+ else:
91
+ # 8-bit (L or RGB)
92
+ depth_np = np.asarray(depth_img.convert("L"), dtype=np.float32) / 255.0
93
+
94
+ # Resize to original resolution
95
+ resized: npt.NDArray[np.float32] = np.asarray(
96
+ cv2.resize(depth_np, (w, h), interpolation=cv2.INTER_LINEAR),
97
+ dtype=np.float32,
98
+ )
99
+ log.info(
100
+ "Marigold depth map received (%dx%d, range=[%.3f, %.3f])",
101
+ w,
102
+ h,
103
+ resized.min(),
104
+ resized.max(),
105
+ )
106
+ return resized
107
+
108
+
109
+ def render_scene_depth(
110
+ scene_mesh: trimesh.Scene | trimesh.Trimesh,
111
+ cam_config: CameraParams,
112
+ width: int,
113
+ height: int,
114
+ ) -> npt.NDArray[np.float32]:
115
+ """Render depth buffer of the scene (without object) using pyrender."""
116
+ pos_scene = np.array(cam_config["position"], dtype=np.float64)
117
+ look_scene = np.array(cam_config["look_at"], dtype=np.float64)
118
+ pos_glb = M_SCENE_TO_GLB[:3, :3] @ pos_scene + M_SCENE_TO_GLB[:3, 3]
119
+ tgt_glb = M_SCENE_TO_GLB[:3, :3] @ look_scene + M_SCENE_TO_GLB[:3, 3]
120
+ K = _camera_intrinsics(cam_config, width, height)
121
+ cam_pose = _build_camera_pose_opengl(pos_glb, tgt_glb)
122
+
123
+ pr_scene = pyrender.Scene(ambient_light=[0.3, 0.3, 0.3])
124
+
125
+ if isinstance(scene_mesh, trimesh.Scene):
126
+ for node_name in scene_mesh.graph.nodes_geometry:
127
+ transform, geom_name = scene_mesh.graph[node_name]
128
+ geom = scene_mesh.geometry[geom_name]
129
+ try:
130
+ pr_scene.add(
131
+ pyrender.Mesh.from_trimesh(geom, smooth=False),
132
+ pose=transform,
133
+ )
134
+ except Exception: # noqa: BLE001 - pyrender raises generic errors for unsupported geometry
135
+ log.debug("Skipped scene depth geometry node %s", node_name)
136
+
137
+ camera = pyrender.IntrinsicsCamera(
138
+ fx=float(K[0, 0]),
139
+ fy=float(K[1, 1]),
140
+ cx=float(K[0, 2]),
141
+ cy=float(K[1, 2]),
142
+ znear=0.05,
143
+ zfar=50.0,
144
+ )
145
+ pr_scene.add(camera, pose=cam_pose)
146
+
147
+ r = pyrender.OffscreenRenderer(width, height)
148
+ result = r.render(pr_scene)
149
+ assert result is not None
150
+ _, depth = result
151
+ r.delete()
152
+ return depth
153
+
154
+
155
+ def align_depth(
156
+ scene_depth: npt.NDArray[np.float32],
157
+ mono_depth: npt.NDArray[np.float32],
158
+ mask: npt.NDArray[np.bool_],
159
+ ) -> tuple[npt.NDArray[np.float64], float, float]:
160
+ """
161
+ Align monocular (relative) depth to absolute scene depth.
162
+
163
+ Fits an affine model ``scene = a * mono + b`` on background pixels
164
+ (where the rendered scene depth is valid and the object mask is off).
165
+ Automatically tries both depth and inverse-depth conventions and
166
+ picks the one with lower residual.
167
+
168
+ """
169
+ valid = (scene_depth > 0.1) & (~mask)
170
+ if valid.sum() < 100:
171
+ log.warning("Too few valid background pixels for depth alignment")
172
+ return mono_depth.astype(np.float64), 1.0, 0.0
173
+
174
+ sd = scene_depth[valid].astype(np.float64)
175
+ md = mono_depth[valid].astype(np.float64)
176
+
177
+ # Fit 1: direct linear scene = a*mono + b
178
+ A_lin = np.column_stack([md, np.ones_like(md)])
179
+ res_lin = np.linalg.lstsq(A_lin, sd, rcond=None)
180
+ a_lin, b_lin = res_lin[0]
181
+ pred_lin = a_lin * md + b_lin
182
+ err_lin = float(np.mean((pred_lin - sd) ** 2))
183
+
184
+ # Fit 2: inverse scene = a/mono + b (handles disparity-like maps)
185
+ md_inv = 1.0 / (md + 1e-6)
186
+ A_inv = np.column_stack([md_inv, np.ones_like(md_inv)])
187
+ res_inv = np.linalg.lstsq(A_inv, sd, rcond=None)
188
+ a_inv, b_inv = res_inv[0]
189
+ pred_inv = a_inv * md_inv + b_inv
190
+ err_inv = float(np.mean((pred_inv - sd) ** 2))
191
+
192
+ # Pick the better fit
193
+ if a_lin > 0 and err_lin <= err_inv:
194
+ aligned = a_lin * mono_depth.astype(np.float64) + b_lin
195
+ log.info(
196
+ "Depth alignment: linear (a=%.4f, b=%.4f, mse=%.6f)", a_lin, b_lin, err_lin
197
+ )
198
+ return np.clip(aligned, 0, 50), float(a_lin), float(b_lin)
199
+
200
+ aligned = a_inv / (mono_depth.astype(np.float64) + 1e-6) + b_inv
201
+ log.info(
202
+ "Depth alignment: inverse (a=%.4f, b=%.4f, mse=%.6f)", a_inv, b_inv, err_inv
203
+ )
204
+ return np.clip(aligned, 0, 50), float(a_inv), float(b_inv)
205
+
206
+
207
+ def estimate_object_depth(
208
+ aligned_depth: npt.NDArray[np.float64],
209
+ mask: npt.NDArray[np.bool_],
210
+ ) -> float:
211
+ """Estimate the depth of the object using the aligned depth map."""
212
+ object_depths = aligned_depth[mask]
213
+ if len(object_depths) == 0:
214
+ return 1.0
215
+ return float(np.median(object_depths))
216
+
217
+
218
+ def depth_to_position(
219
+ pixel_u: float,
220
+ pixel_v: float,
221
+ depth: float,
222
+ cam_config: CameraParams,
223
+ width: int,
224
+ height: int,
225
+ ) -> npt.NDArray[np.float64]:
226
+ """Back-project a pixel at given depth to 3D scene coordinates."""
227
+ K = _camera_intrinsics(cam_config, width, height)
228
+ K_inv = np.linalg.inv(K)
229
+
230
+ p_h = np.array([pixel_u, pixel_v, 1.0], dtype=np.float64)
231
+ d_cam = K_inv @ p_h
232
+ d_cam /= np.linalg.norm(d_cam)
233
+
234
+ ray_distance = depth / abs(d_cam[2])
235
+
236
+ pos = np.array(cam_config["position"], dtype=np.float64)
237
+ target = np.array(cam_config["look_at"], dtype=np.float64)
238
+ fwd = target - pos
239
+ fwd /= np.linalg.norm(fwd) + 1e-12
240
+ up_scene = np.array([0.0, 0.0, 1.0])
241
+ right = np.cross(fwd, up_scene)
242
+ if np.linalg.norm(right) < 1e-6:
243
+ up_scene = np.array([0.0, 1.0, 0.0])
244
+ right = np.cross(fwd, up_scene)
245
+ right /= np.linalg.norm(right) + 1e-12
246
+ down = np.cross(fwd, right)
247
+ down /= np.linalg.norm(down) + 1e-12
248
+ R_wc = np.column_stack([right, down, fwd])
249
+
250
+ return pos + R_wc @ (d_cam * ray_distance)