Add new tests and functionality for image processing and geometry handling

- Introduced color_image.py for preparing standalone color images for tests.
- Added hdr_image.py to create HDR textures for testing.
- Implemented test_image_depth_panorama.py to evaluate panorama topology, UVs, and depth discontinuities.
- Created test_shadeless.py to validate the opacity and HDR color endpoints of the Shadeless group.
- Developed moge3_export.py for exporting dynamic MoGe-3 backbone and ONNX graphs.
- Added material.py to inherit material slots onto generated geometry.
- Built image_depth_panorama.py to construct a quad panorama with depth separation and sampling.
This commit is contained in:
2026-09-10 14:42:17 +08:00
parent 27ca25224a
commit 489003c2cc
136 changed files with 5758 additions and 1083 deletions

View File

@@ -2,7 +2,7 @@
from pathlib import Path
from ..geometry.prediction_artifacts import generate_moge2_artifacts
from ..geometry.prediction_artifacts import generate_moge_artifacts
def run(context, parameters):
@@ -13,7 +13,7 @@ def run(context, parameters):
generate_depth = bool(parameters.get("generate_depth", False))
normal_mode = str(parameters.get("normal_mode", "NONE"))
if generate_depth or normal_mode != "NONE":
artifacts = generate_moge2_artifacts(
artifacts = generate_moge_artifacts(
context,
parameters,
input_path,

View File

@@ -1,7 +1,7 @@
"""Generate model-space depth and preserve color for one complete image."""
"""Generate model-space depth and normals for one complete image."""
from ..geometry.prediction_artifacts import generate_moge2_artifacts
from ..media import copy_color_images, input_frames
from ..geometry.prediction_artifacts import generate_moge_artifacts
from ..media import input_frames
def run(context, parameters):
@@ -13,8 +13,7 @@ def run(context, parameters):
with input_frames(context, parameters) as images:
if len(images) != 1:
raise ValueError("Depth conversion currently supports one image")
copy_color_images(images, context.directory)
artifacts = generate_moge2_artifacts(
artifacts = generate_moge_artifacts(
context,
parameters,
images[0],

View File

@@ -0,0 +1,82 @@
"""Generate a radial depth texture from the current RGBA panorama."""
import json
from pathlib import Path
from ..geometry.depth_texture import write_float_exr
from ..geometry.panorama import camera_bases, merge_distances, sample_panorama, view_rays
from ..media import VIDEO_SUFFIXES, input_frames
from ..media.input import open_image
from ..media.resolution import DEFAULT_MAX_AI_INPUT_SIZE, normalized_max_input_size
from ..models.moge import infer_image as infer
def run(context, parameters):
import numpy as np
context.check_cancelled()
input_path = Path(parameters["input"])
if input_path.is_dir() or input_path.suffix.lower() in VIDEO_SUFFIXES:
raise ValueError("Panorama conversion requires one static 2:1 image")
maximum = normalized_max_input_size(parameters.get("max_input_size", DEFAULT_MAX_AI_INPUT_SIZE))
context.directory.mkdir(parents=True, exist_ok=True)
outputs = [context.directory / name for name in ("depth.exr", "depth.json")]
try:
with input_frames(context, parameters) as images:
if len(images) != 1:
raise ValueError("Panorama conversion requires one static 2:1 image")
with open_image(images[0]) as opened:
if getattr(opened, "n_frames", 1) != 1 or opened.width != opened.height * 2:
raise ValueError("Panorama conversion requires one static 2:1 image")
source = opened.convert("RGBA")
width, height = source.size
size = min(maximum, max(512, width // 4))
image = np.asarray(source, dtype=np.float32) / 255.0
manager = context.resource("model_manager")
bases = camera_bases()
rays = view_rays(size)
distances, masks = [], []
session = None
for index, basis in enumerate(bases):
context.check_cancelled()
context.progress(0.05 + 0.65 * index / len(bases), f"Generating panorama view {index + 1}/12")
directions = rays @ basis
alpha = sample_panorama(image[..., 3], directions, order=0)
visible = alpha > 0
if not visible.any():
distances.append(np.ones((size, size), dtype=np.float32))
masks.append(visible)
continue
if session is None:
session = manager.get_moge(manager.directory(parameters["model"]), parameters["device"])
color = sample_panorama(image[..., :3], directions) * alpha[..., None]
prediction = infer(session, color, parameters["resolution_level"],
fov_x=90.0, source_valid=visible)
distances.append(np.linalg.norm(prediction["points"], axis=-1))
masks.append((prediction["mask"] > 0.5) & visible)
del prediction
context.progress(0.75, "Merging panoramic distance")
distance, valid = merge_distances(distances, masks, bases, width, height,
cancel_check=context.check_cancelled)
alpha = np.asarray(source.getchannel("A"),
dtype=np.float32) / 255.0
alpha *= valid
reference = (alpha >= 0.9) & np.isfinite(distance) & (distance > 0)
if not reference.any():
raise ValueError("The panorama contains no valid depth pixels")
if not np.isfinite(distance).all() or (distance <= 0).any():
raise ValueError("Panorama fusion produced invalid distances")
rgba = np.empty((height, width, 4), dtype=np.float32)
rgba[..., :3], rgba[..., 3] = distance[..., None], alpha
write_float_exr(rgba, outputs[0])
outputs[1].write_text(json.dumps({
"projection": "equirectangular", "image_size": [width, height],
"reference_distance": float(np.median(distance[reference])),
}, separators=(",", ":")), encoding="utf-8")
context.check_cancelled()
context.progress(1.0, "Panorama conversion data is ready")
return {"depth": outputs[0].name, "depth_metadata": outputs[1].name}
except BaseException:
for path in outputs:
path.unlink(missing_ok=True)
raise

View File

@@ -1,7 +1,8 @@
import time
from ..media import input_frames
from ..models.ben2 import infer_foreground, infer_foreground_frames
from ..media.input import open_image
from ..models.ben2 import infer_alpha, infer_foreground, infer_foreground_frames
def run(context, parameters):
@@ -15,10 +16,25 @@ def run(context, parameters):
device = parameters["device"]
model_manager = context.resource("model_manager")
model_directory = model_manager.directory(parameters["model"])
output_kind = parameters.get("output_kind", "foreground")
if output_kind not in {"foreground", "alpha"}:
raise ValueError(f"Unknown background removal output: {output_kind}")
with input_frames(context, parameters) as images:
context.progress(0.12, f"Loading BEN2 for {device.upper()}")
if len(images) == 1:
if output_kind == "alpha":
import numpy as np
if len(images) != 1:
raise ValueError("Alpha output requires one still image")
with open_image(images[0]) as source:
alpha, load_ms, inference_ms = infer_alpha(
source, model_directory, device, model_cache=model_manager,
cancel_check=context.check_cancelled,
)
context.check_cancelled()
np.save(output_directory / "alpha.npy", alpha, allow_pickle=False)
elif len(images) == 1:
context.progress(0.35, "Removing background")
foreground, load_ms, inference_ms = infer_foreground(
images[0],
@@ -62,4 +78,4 @@ def run(context, parameters):
)
context.log(message)
context.progress(1.0, message)
return {"image": "."}
return {"alpha": "alpha.npy"} if output_kind == "alpha" else {"image": "."}