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

@@ -4,7 +4,6 @@ from enum import StrEnum
import bpy
from ...common.image import color_data_name, configure_color_alpha
from ...common.depth import camera_depth_values, depth_uniform_scale
from ...common.material import (
IMAGE_MATERIAL_NODE_GROUP_NAME,
@@ -97,37 +96,6 @@ def _assign_float_point_attribute(mesh, name, values):
attribute.data.foreach_set("value", np.asarray(values, dtype=np.float32))
def create_cropped_color_image(source_object, crop_bounds, source_rgba):
"""Create a packed color image for one local Cutout bounds rectangle."""
import numpy as np
source = source_object.data
rgba = np.asarray(source_rgba)
left, top, right, bottom = crop_bounds
width = right - left
height = bottom - top
if width <= 0 or height <= 0:
raise ValueError("The Cutout crop dimensions must be positive")
source_height, source_width = rgba.shape[:2]
if left < 0 or top < 0 or right > source_width or bottom > source_height:
raise ValueError("The Cutout crop is outside the source RGBA")
cropped = rgba[top:bottom, left:right]
image = bpy.data.images.new(
color_data_name(source_object),
width=width,
height=height,
alpha=True,
)
try:
image.colorspace_settings.name = source.colorspace_settings.name
configure_color_alpha(image)
image.pixels.foreach_set(np.flipud(cropped).ravel())
image.pack()
image.update()
except Exception:
bpy.data.images.remove(image, do_unlink=True)
raise
return image
def load_cutout_node_group(shape):

View File

@@ -31,17 +31,19 @@ from ...common.ai import (
require_model,
)
from ...common.image import (
cleanup_image_input,
color_data_name,
image_rgba,
load_normal_result_image,
load_packed_rgba_image,
prepare_image_bounds_input,
require_image_empty,
source_start_frame,
video_total_frames,
)
from ...common.depth import load_depth_metadata, load_depth_result_image
from ...common.color_image import (
cleanup_material_color_input,
load_material_color_image,
material_analysis_input,
material_color_image,
prepare_material_color_input,
)
from ...common.viewport import (
ImageGesture,
image_edit_drag_keymap,
@@ -61,7 +63,6 @@ from .interaction import open_shape_pie
from .object import (
CutoutInwardAxis,
NormalMode,
create_cropped_color_image,
create_shape_object,
orient_object_normal_image,
)
@@ -101,36 +102,31 @@ def create_cutout_shape(
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
color_image,
):
try:
base_shape = build_base_shape(
context,
source_object,
point_spacing,
content_values,
content_bounds,
alpha_threshold=alpha_threshold,
fine_outline=fine_outline,
)
result = create_shape_object(
context,
source_object,
shape,
point_spacing,
content_values,
content_bounds,
base_shape,
alpha_threshold=alpha_threshold,
depth_image=depth_image,
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
color_image=color_image,
)
except Exception:
if color_image.users == 0:
bpy.data.images.remove(color_image, do_unlink=True)
raise
base_shape = build_base_shape(
context,
source_object,
point_spacing,
content_values,
content_bounds,
alpha_threshold=alpha_threshold,
fine_outline=fine_outline,
)
result = create_shape_object(
context,
source_object,
shape,
point_spacing,
content_values,
content_bounds,
base_shape,
alpha_threshold=alpha_threshold,
depth_image=depth_image,
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
color_image=color_image,
)
undo_message = f"Convert to {shape.replace('_', ' ').title()}"
if "FINISHED" not in bpy.ops.ed.undo_push(message=undo_message):
raise RuntimeError(f"Unable to create the {undo_message} Undo step")
@@ -160,12 +156,8 @@ def _cutout_content_values(
return values
def _generated_color_result(input_path, source_object, source_bounds):
image = load_packed_rgba_image(
input_path,
source_object.data,
color_data_name(source_object),
)
def _generated_color_result(input_path, source_bounds):
image = load_material_color_image(input_path)
try:
alpha = image_rgba(image)[:, :, 3]
except Exception:
@@ -193,6 +185,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
starting_message = "Generating Cutout artifacts"
input_path: bpy.props.StringProperty(options=_HIDDEN)
color_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
selection_path_json: bpy.props.StringProperty(options=_HIDDEN)
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
@@ -265,25 +258,15 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
selection_mask.values,
alpha_threshold=self.alpha_threshold,
)
color_image = create_cropped_color_image(
source_object,
selection_mask.bounds,
source_rgba,
)
create_cutout_shape(
context,
source_object,
self.shape,
self.point_spacing,
content_values,
selection_mask.bounds,
inward_axis=self.inward_axis,
alpha_threshold=self.alpha_threshold,
fine_outline=self.fine_outline,
color_image=color_image,
)
with material_color_image(source_object.data, bounds=selection_mask.bounds, rgba=source_rgba) as color_image:
create_cutout_shape(
context, source_object, self.shape, self.point_spacing,
content_values, selection_mask.bounds,
inward_axis=self.inward_axis, alpha_threshold=self.alpha_threshold,
fine_outline=self.fine_outline, color_image=color_image,
)
return {"FINISHED"}
setup_result = invoke_ai_setup_if_needed()
setup_result = invoke_ai_setup_if_needed(self)
if setup_result is not None:
return setup_result
self.model, self.resolution_level = cutout_geometry_settings(context)
@@ -292,13 +275,10 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
separators=(",", ":"),
)
self._selection_mask = selection_mask
self.input_path = str(
prepare_image_bounds_input(
source_object.data,
source_rgba,
selection_mask.bounds,
)
)
self.color_path = str(prepare_material_color_input(
source_object.data, selection_mask.bounds, source_rgba,
))
self.input_path = str(material_analysis_input(self.color_path))
except (
ImageEditWarning,
OSError,
@@ -306,11 +286,12 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
TypeError,
ValueError,
) as error:
cleanup_material_color_input(getattr(self, "color_path", None))
report_image_edit_exception(self, error)
return {"CANCELLED"}
result = super().execute(context)
if "CANCELLED" in result:
cleanup_image_input(self.input_path, True)
cleanup_material_color_input(self.color_path)
return result
def request(self, context):
@@ -329,7 +310,6 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
"alpha_threshold": float(self.alpha_threshold),
}
if generate_depth or normal_mode != NormalMode.NONE:
start_frame = source_start_frame(self.source_object_name)
parameters.update(
model=self.model,
resolution_level=self.resolution_level,
@@ -340,7 +320,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
source_path,
self.model,
self.resolution_level,
start_frame,
1,
)
)
return parameters
@@ -354,14 +334,12 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
source_object = require_image_empty(self.source_object_name)
source_bounds = tuple(json.loads(self.selection_bounds_json))
color_image, source_alpha, color_bounds = _generated_color_result(
self.input_path,
source_object,
self.color_path,
source_bounds,
)
value = result.value if isinstance(result.value, dict) else {}
normal_image = None
depth_image = None
shape_started = False
try:
selection_mask = getattr(self, "_selection_mask", None)
if selection_mask is None:
@@ -389,7 +367,6 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
source_object,
depth_metadata,
)
shape_started = True
create_cutout_shape(
context,
source_object,
@@ -411,13 +388,13 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
bpy.data.images.remove(normal_image, do_unlink=True)
if depth_image is not None and depth_image.users == 0:
bpy.data.images.remove(depth_image, do_unlink=True)
if not shape_started and color_image.users == 0:
if color_image.users == 0:
bpy.data.images.remove(color_image, do_unlink=True)
raise
return f"{self.shape.replace('_', ' ').title()} is ready"
def cleanup(self):
cleanup_image_input(self.input_path, True)
cleanup_material_color_input(self.color_path)
class SelectCutoutSelection(ImageGesture, bpy.types.Operator):