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.
- blender_cli/__init__.py +151 -0
- blender_cli/__init__.pyi +55 -0
- blender_cli/alignment/__init__.py +37 -0
- blender_cli/alignment/_fal.py +74 -0
- blender_cli/alignment/depth.py +250 -0
- blender_cli/alignment/generation.py +248 -0
- blender_cli/alignment/integration.py +80 -0
- blender_cli/alignment/pipeline.py +200 -0
- blender_cli/alignment/pose.py +1453 -0
- blender_cli/alignment/types.py +165 -0
- blender_cli/alignment/viz.py +62 -0
- blender_cli/animation/__init__.py +15 -0
- blender_cli/animation/codegen.py +134 -0
- blender_cli/animation/keyframes.py +211 -0
- blender_cli/assets/__init__.py +14 -0
- blender_cli/assets/image.py +36 -0
- blender_cli/assets/material.py +578 -0
- blender_cli/assets/prefab.py +72 -0
- blender_cli/assets/registry.py +189 -0
- blender_cli/blenvy.py +214 -0
- blender_cli/blenvy_registry.py +237 -0
- blender_cli/build/__init__.py +18 -0
- blender_cli/build/build_types.py +9 -0
- blender_cli/build/context.py +179 -0
- blender_cli/build/generation_step.py +97 -0
- blender_cli/build/runner.py +74 -0
- blender_cli/cli/__init__.py +5 -0
- blender_cli/cli/__main__.py +5 -0
- blender_cli/cli/commands/__init__.py +65 -0
- blender_cli/cli/commands/align.py +524 -0
- blender_cli/cli/commands/anchor_cmd.py +47 -0
- blender_cli/cli/commands/animation_cmd.py +187 -0
- blender_cli/cli/commands/assets.py +94 -0
- blender_cli/cli/commands/blenvy_cmd.py +228 -0
- blender_cli/cli/commands/camera_cmd.py +120 -0
- blender_cli/cli/commands/candidates.py +289 -0
- blender_cli/cli/commands/inspect.py +124 -0
- blender_cli/cli/commands/instance_cmd.py +72 -0
- blender_cli/cli/commands/manifest.py +90 -0
- blender_cli/cli/commands/material_cmd.py +65 -0
- blender_cli/cli/commands/measure.py +58 -0
- blender_cli/cli/commands/modifier_cmd.py +179 -0
- blender_cli/cli/commands/object_cmd.py +80 -0
- blender_cli/cli/commands/op.py +2353 -0
- blender_cli/cli/commands/project.py +343 -0
- blender_cli/cli/commands/raycast.py +85 -0
- blender_cli/cli/commands/render.py +503 -0
- blender_cli/cli/commands/render_settings_cmd.py +114 -0
- blender_cli/cli/commands/repl_cmd.py +25 -0
- blender_cli/cli/commands/run.py +34 -0
- blender_cli/cli/commands/select.py +22 -0
- blender_cli/cli/commands/session_cmd.py +90 -0
- blender_cli/cli/commands/stats.py +20 -0
- blender_cli/cli/commands/terrain_cmd.py +116 -0
- blender_cli/cli/commands/world_cmd.py +91 -0
- blender_cli/cli/common.py +339 -0
- blender_cli/cli/main.py +74 -0
- blender_cli/cli/repl.py +121 -0
- blender_cli/constants.py +6 -0
- blender_cli/core/__init__.py +39 -0
- blender_cli/core/diagnostics.py +9 -0
- blender_cli/core/metadata.py +70 -0
- blender_cli/geometry/__init__.py +34 -0
- blender_cli/geometry/_erosion.py +114 -0
- blender_cli/geometry/field2d.py +306 -0
- blender_cli/geometry/heightfield.py +586 -0
- blender_cli/geometry/mask.py +232 -0
- blender_cli/geometry/pointset.py +603 -0
- blender_cli/geometry/spline.py +279 -0
- blender_cli/geometry/spline_ops.py +320 -0
- blender_cli/modifiers/__init__.py +15 -0
- blender_cli/modifiers/codegen.py +94 -0
- blender_cli/modifiers/modifier.py +185 -0
- blender_cli/modifiers/registry.py +346 -0
- blender_cli/project/__init__.py +6 -0
- blender_cli/project/project_file.py +2207 -0
- blender_cli/project/session.py +124 -0
- blender_cli/py.typed +0 -0
- blender_cli/render/__init__.py +46 -0
- blender_cli/render/camera.py +397 -0
- blender_cli/render/camera_path.py +321 -0
- blender_cli/render/context.py +1897 -0
- blender_cli/render/settings.py +286 -0
- blender_cli/render/world.py +170 -0
- blender_cli/scene/__init__.py +27 -0
- blender_cli/scene/anchor.py +65 -0
- blender_cli/scene/entity.py +542 -0
- blender_cli/scene/instances.py +524 -0
- blender_cli/scene/primitives.py +574 -0
- blender_cli/scene/scene.py +1613 -0
- blender_cli/scene/selection.py +273 -0
- blender_cli/snap/__init__.py +26 -0
- blender_cli/snap/axis.py +80 -0
- blender_cli/snap/objects.py +628 -0
- blender_cli/snap/results.py +190 -0
- blender_cli/types.py +292 -0
- blender_cli/utils/__init__.py +29 -0
- blender_cli/utils/placement.py +511 -0
- blender_cli/utils/spline_strip.py +233 -0
- blender_cli/utils/strings.py +30 -0
- blender_cli/utils/sweep.py +154 -0
- blender_cli-0.1.0.dist-info/METADATA +281 -0
- blender_cli-0.1.0.dist-info/RECORD +105 -0
- blender_cli-0.1.0.dist-info/WHEEL +4 -0
- blender_cli-0.1.0.dist-info/entry_points.txt +3 -0
blender_cli/__init__.py
ADDED
|
@@ -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
|
+
]
|
blender_cli/__init__.pyi
ADDED
|
@@ -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)
|