Add new functionality for image rectification and depth metadata handling

- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

View File

@@ -13,6 +13,7 @@ from ...preferences import (
from ...properties import (
cutout_alpha_threshold_property,
cutout_fine_outline_property,
cutout_gesture_property,
)
from ...runtime import JobOperatorBase
from ...server.model_catalog import DEFAULT_GEOMETRY_MODEL_KEY
@@ -35,7 +36,7 @@ from ...common.image import (
image_rgba,
load_normal_result_image,
require_image_empty,
video_total_frames,
is_animated_image,
)
from ...common.depth import load_depth_metadata, load_depth_result_image
from ...common.color_image import (
@@ -47,7 +48,7 @@ from ...common.color_image import (
)
from ...common.viewport import (
ImageGesture,
image_edit_drag_keymap,
image_edit_point_keymap,
report_image_edit_exception,
)
from ...common.selection import (
@@ -62,10 +63,8 @@ from .geometry import (
)
from .interaction import open_shape_pie
from .object import (
CutoutInwardAxis,
NormalMode,
create_shape_object,
orient_object_normal_image,
)
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
@@ -100,8 +99,8 @@ def create_cutout_shape(
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
color_image,
gesture="LASSO",
):
base_shape = build_base_shape(
context,
@@ -125,8 +124,8 @@ def create_cutout_shape(
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
color_image=color_image,
gesture=gesture,
)
undo_message = f"Convert to {shape.replace('_', ' ').title()}"
if "FINISHED" not in bpy.ops.ed.undo_push(message=undo_message):
@@ -191,6 +190,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
selection_path_json: bpy.props.StringProperty(options=_HIDDEN)
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
shape: bpy.props.StringProperty(default="SOLID", options=_HIDDEN)
gesture: bpy.props.StringProperty(default="LASSO", options=_HIDDEN)
point_spacing: bpy.props.FloatProperty(
default=32.0,
min=0.01,
@@ -215,23 +215,13 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
alpha_threshold: cutout_alpha_threshold_property()
fine_outline: cutout_fine_outline_property()
inward_axis: bpy.props.StringProperty(
default=CutoutInwardAxis.NEGATIVE_Z.value,
options=_HIDDEN,
)
def _needs_generation(self):
return self.shape in DEPTH_CUTOUT_SHAPES or self.generate_normal
def execute(self, context):
try:
self.inward_axis = getattr(
self,
"inward_axis",
CutoutInwardAxis.NEGATIVE_Z.value,
)
source_object = require_image_empty(self.source_object_name)
if video_total_frames(source_object.data) != 1:
if is_animated_image(source_object.data):
raise ValueError("Cutout currently supports still images")
if self.shape not in CUTOUT_SHAPES:
raise ValueError(f"Unsupported Cutout shape: {self.shape}")
@@ -263,8 +253,9 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
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,
alpha_threshold=self.alpha_threshold,
fine_outline=self.fine_outline, color_image=color_image,
gesture=self.gesture,
)
return {"FINISHED"}
setup_result = invoke_ai_setup_if_needed(self)
@@ -317,21 +308,14 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
)
parameters.update(
moge2_parameters(
context.scene.anyimage_settings,
source_path,
self.model,
self.resolution_level,
1,
)
)
return parameters
def response(self, context, result):
inward_axis = getattr(
self,
"inward_axis",
CutoutInwardAxis.NEGATIVE_Z.value,
)
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(
@@ -358,8 +342,6 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
result.file(normal_key),
source_object,
)
if normal_mode == NormalMode.OBJECT:
orient_object_normal_image(normal_image, inward_axis)
depth_metadata = None
if shape_uses_depth(self.shape):
depth_metadata = load_depth_metadata(result.file("depth_metadata"))
@@ -379,10 +361,10 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
alpha_threshold=self.alpha_threshold,
fine_outline=self.fine_outline,
color_image=color_image,
gesture=self.gesture,
)
except Exception:
if normal_image is not None and normal_image.users == 0:
@@ -401,22 +383,32 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
class SelectCutoutSelection(ImageGesture, bpy.types.Operator):
bl_idname = "anyimage.select_cutout_selection"
bl_label = "Start Cutout Selection"
bl_description = "Lasso an Image Empty selection, then choose its mesh shape"
bl_description = "Select an Image Empty area with a lasso or polyline, then choose its mesh shape"
bl_options = {"INTERNAL"}
active_tool_id = "anyimage.cutout_lasso"
gesture = "LASSO"
resolve_on_press = True
gesture: cutout_gesture_property()
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
_pie_event = None
_polyline_confirm_type = None
def _read_gesture_settings(self, context):
self.gesture = context.scene.anyimage_settings.cutout_gesture
def _set_status(self, context):
action = (
"click points; click start, double-click, or Enter for shapes; Backspace removes"
if self.gesture == "POLYLINE" else "drag and release for shapes"
)
context.workspace.status_text_set(
"Cutout Lasso: drag and release for shapes; RMB or Esc cancels"
f"Cutout {self.gesture.title()}: {action}; RMB or Esc cancels"
)
def invoke(self, context, event):
self._polyline_confirm_type = None
self._pie_event = event
return super().invoke(context, event)
@@ -424,6 +416,19 @@ class SelectCutoutSelection(ImageGesture, bpy.types.Operator):
self._pie_event = event
return ImageGesture.modal(self, context, event)
def _confirm_polyline(self, context):
if len(self._path) >= 3:
self._polyline_confirm_type = self._pie_event.type
return {"RUNNING_MODAL"}
def _modal_polyline(self, context, event):
if self._polyline_confirm_type is not None:
if event.type == self._polyline_confirm_type and event.value == "RELEASE":
self._polyline_confirm_type = None
return ImageGesture._confirm_polyline(self, context)
return {"RUNNING_MODAL"}
return ImageGesture._modal_polyline(self, context, event)
def _submit_selection_path(self, context, selection_path):
try:
source_object = require_image_empty(self.source_object_name)
@@ -435,6 +440,7 @@ class SelectCutoutSelection(ImageGesture, bpy.types.Operator):
self._pie_event,
source_object,
selection_path,
gesture=self.gesture,
)
return {"FINISHED"}
@@ -444,19 +450,18 @@ class CutoutTool(bpy.types.WorkSpaceTool):
bl_context_mode = "OBJECT"
bl_idname = "anyimage.cutout_lasso"
bl_label = "Cutout"
bl_description = "Lasso an Image Empty and choose a mesh shape"
bl_description = "Select an Image Empty area with a lasso or polyline and choose a mesh shape"
bl_icon = "ops.mesh.primitive_sphere_add_gizmo"
bl_operator = SelectCutoutSelection.bl_idname
bl_keymap = image_edit_drag_keymap(
bl_keymap = image_edit_point_keymap(
SelectCutoutSelection.bl_idname,
deselect_on_empty=False,
)
def draw_settings(context, layout, _tool):
settings = context.scene.anyimage_settings
status = draw_ai_setup(layout)
draw_ai_property(layout, settings, "cutout_generate_normal", status)
layout.prop(settings, "cutout_gesture")
layout.prop(settings, "cutout_mesh_detail")
layout.prop(settings, "cutout_fine_outline")
layout.prop(settings, "cutout_alpha_threshold")
layout.prop(settings, "cutout_inward_axis")
status = draw_ai_setup(layout)
draw_ai_property(layout, settings, "cutout_generate_normal", status)