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

@@ -8,6 +8,8 @@ from pathlib import Path
import bpy
from ..server.media.input import validate_input_path
VIEW_TRANSFORM_COLOR_SPACES = (
("ACES 1.3", "ACES 1.3 sRGB"),
("ACES 2.0", "ACES 2.0 sRGB"),
@@ -333,11 +335,6 @@ def extract_frame_range(image, image_user):
return start_frame, frame_count
def source_start_frame(source_object_name):
source_object = bpy.data.objects.get(source_object_name)
return extract_frame_range(source_object.data, source_object.image_user)[0] if source_object else 1
def image_path(image):
filepath_from_user = getattr(image, "filepath_from_user", None)
filepath = (
@@ -395,22 +392,19 @@ def normal_data_name(source_object):
return f"{image_base_name(source_object.data)}_normal.png"
def color_data_name(source_object):
return f"{image_base_name(source_object.data)}_color.png"
def save_packed_image(image, _scene):
output_path = Path(tempfile.mkdtemp(prefix="anyimage-input-")) / "input.png"
original_file_format = image.file_format
try:
image.file_format = "PNG"
image.save(filepath=str(output_path), save_copy=True)
except (OSError, RuntimeError) as error:
output_path.unlink(missing_ok=True)
except (OSError, RuntimeError, ValueError) as error:
cleanup_image_input(output_path, True)
raise RuntimeError("Unable to export the packed image") from error
finally:
image.file_format = original_file_format
if not output_path.is_file():
cleanup_image_input(output_path, True)
raise RuntimeError("Blender did not export the packed image")
return output_path
@@ -450,9 +444,11 @@ def prepare_image_input(image, scene):
if getattr(image, "source", None) == "MOVIE":
if not source_path.is_file():
raise RuntimeError(
"The image empty must reference an existing video file"
f"Source video file does not exist: {source_path}"
)
return source_path, False
if getattr(image, "source", None) == "SEQUENCE":
validate_input_path(source_path, images_only=True)
sequence_frames = collect_sequence_frames(image)
if sequence_frames:
temporary_directory = Path(tempfile.mkdtemp(prefix="anyimage-input-"))
@@ -469,73 +465,11 @@ def prepare_image_input(image, scene):
return temporary_directory, True
if not source_path.is_file():
raise RuntimeError(
"The image empty must reference an existing image file"
f"Source image file does not exist: {source_path}"
)
return source_path, False
def prepare_image_bounds_input(image, source_rgba, bounds):
"""Write one rectangular RGBA input owned by its temporary directory."""
import numpy as np
left, top, right, bottom = (int(value) for value in bounds)
rgba = np.asarray(source_rgba, dtype=np.float32)
expected_shape = (int(image.size[1]), int(image.size[0]), 4)
if rgba.shape != expected_shape:
raise ValueError("The source RGBA does not match the Blender Image")
if (
left < 0
or top < 0
or right > expected_shape[1]
or bottom > expected_shape[0]
):
raise ValueError("The image bounds are outside the Blender Image")
if right <= left or bottom <= top:
raise ValueError("The image bounds are empty")
cropped = rgba[top:bottom, left:right]
output_directory = Path(tempfile.mkdtemp(prefix="anyimage-bounds-"))
output_path = output_directory / "input.png"
temporary_image = bpy.data.images.new(
"AnyImage Bounds Input",
width=right - left,
height=bottom - top,
alpha=True,
)
original_format = temporary_image.file_format
try:
temporary_image.colorspace_settings.name = image.colorspace_settings.name
temporary_image.alpha_mode = "STRAIGHT"
temporary_image.pixels.foreach_set(np.flipud(cropped).ravel())
temporary_image.file_format = "PNG"
temporary_image.save(filepath=str(output_path), save_copy=True)
except Exception:
cleanup_image_input(output_path, True)
raise
finally:
temporary_image.file_format = original_format
bpy.data.images.remove(temporary_image, do_unlink=True)
if not output_path.is_file():
cleanup_image_input(output_path, True)
raise RuntimeError("Blender did not export the bounds image input")
return output_path
def load_packed_rgba_image(path, source_image, name):
"""Load an encoded local RGBA file and make its Blender data self-contained."""
result_image = None
try:
result_image = bpy.data.images.load(str(path), check_existing=False)
result_image.colorspace_settings.name = source_image.colorspace_settings.name
result_image.alpha_mode = source_image.alpha_mode
result_image.name = name
result_image.pack()
except (OSError, RuntimeError, TypeError, ValueError) as error:
if result_image is not None:
bpy.data.images.remove(result_image, do_unlink=True)
raise RuntimeError("Unable to prepare the RGBA image result") from error
return result_image
def configure_image_user(
image_user,
frame_count=None,
@@ -674,6 +608,15 @@ def has_other_image_empty(source_object, *, removed_objects=()):
)
def require_conversion_source(operator):
"""Validate the object and image captured when a conversion was submitted."""
source = require_image_empty(operator.source_object_name)
if (str(source.as_pointer()) != operator.source_identity
or str(source.data.as_pointer()) != operator.image_identity):
raise ValueError("The conversion source image has changed")
return source
def image_content_state(image):
"""Capture encoded content and unsaved pixels for an Image transaction."""
import numpy as np
@@ -728,7 +671,7 @@ def restore_image_content(image, state):
def replace_empty_image(source_object, result_image, *, placement_bounds=None,
removed_objects=()):
removed_objects=(), isolate_image=False):
"""Replace one Empty image, retaining shared source images."""
try:
source_image = source_object.data
@@ -767,7 +710,7 @@ def replace_empty_image(source_object, result_image, *, placement_bounds=None,
)
original_content = None
try:
shared = has_other_image_empty(source_object, removed_objects=removed_objects)
shared = isolate_image or has_other_image_empty(source_object, removed_objects=removed_objects)
if shared:
final_image = result_image
else:
@@ -866,22 +809,6 @@ def load_image_edit_result(
)
def load_color_result_image(
source_object,
output_directory,
*,
frame_prefix="image.",
):
"""Load one packed Color texture result for a generated material."""
return _load_packed_result_image(
source_object.data,
output_directory,
frame_prefix=frame_prefix,
result_name=color_data_name(source_object),
alpha_mode=color_alpha_mode(source_object.data.colorspace_settings.name),
)
def image_empty_bounds(source_object):
image_width, image_height = source_object.data.size
if image_width <= 0 or image_height <= 0: