Refactor and clean up codebase by removing unused functions and reorganizing imports

- Removed unused model keys and functions from properties.py.
- Deleted projection.py as its functions were no longer needed.
- Cleaned up model_catalog.py by removing unused model directory function.
- Updated imports in various test files to reflect new module structure.
- Removed deprecated test cases related to image editing tools.
- Added new tests for ONNX runtime provider selection logic.
This commit is contained in:
2026-09-09 10:31:56 +08:00
parent 4c902ed7db
commit f5aff50b13
47 changed files with 460 additions and 413 deletions

View File

@@ -0,0 +1,484 @@
"""Convert one image Selection into a Cutout Shape."""
import json
import bpy
from ...preferences import (
addon_preferences,
configured_max_ai_input_size,
configured_max_cutout_mesh_resolution,
)
from ...properties import (
DEFAULT_MOGE2_MODEL_KEY,
cutout_alpha_threshold_property,
cutout_fine_outline_property,
)
from ...runtime import JobOperatorBase
from ...server.media.resolution import (
DEFAULT_MAX_AI_INPUT_SIZE,
MAX_MAX_AI_INPUT_SIZE,
MIN_MAX_AI_INPUT_SIZE,
)
from ...common.ai import (
draw_ai_property,
draw_ai_setup,
invoke_ai_setup_if_needed,
moge2_parameters,
production_device,
require_environment,
require_input_path,
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.viewport import (
ImageGesture,
image_edit_drag_keymap,
report_image_edit_exception,
)
from ...common.selection import (
ImageEditWarning,
SelectionPath,
rasterize_selection_path,
)
from .geometry import (
CUTOUT_ALPHA_THRESHOLD,
build_base_shape,
limit_cutout_point_spacing,
)
from .interaction import open_shape_pie
from .object import (
CutoutInwardAxis,
NormalMode,
create_cropped_color_image,
create_shape_object,
orient_object_normal_image,
)
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
def normal_mode_for_shape(shape, generate_normal):
if shape == "DEPTH_SOLID":
return NormalMode.OBJECT
if shape == "DEPTH_BALLOON":
return NormalMode.TANGENT
if not generate_normal:
return NormalMode.NONE
return NormalMode.TANGENT
def shape_uses_depth(shape):
return shape in {"DEPTH_SOLID", "DEPTH_BALLOON"}
def create_cutout_shape(
context,
source_object,
shape,
point_spacing,
content_values,
content_bounds,
*,
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
fine_outline=False,
depth_image=None,
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
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
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")
return result
def _cutout_content_values(
source_alpha, selection_values=None, *, alpha_threshold=CUTOUT_ALPHA_THRESHOLD
):
"""Combine local image Alpha with optional pure Selection geometry."""
import numpy as np
alpha = np.asarray(source_alpha, dtype=np.float32)
if alpha.ndim != 2:
raise ValueError("The Cutout source Alpha must be two-dimensional")
if selection_values is None:
values = alpha.copy()
else:
selection = np.asarray(selection_values, dtype=np.float32)
if alpha.shape != selection.shape:
raise ValueError("The Cutout source Alpha does not match the Selection")
values = alpha * selection
if not np.any((values > 0.0) & (values >= alpha_threshold)):
raise ImageEditWarning(
"The Cutout selection contains no visible image pixels"
)
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),
)
try:
alpha = image_rgba(image)[:, :, 3]
except Exception:
if image.users == 0:
bpy.data.images.remove(image, do_unlink=True)
raise
return image, alpha, source_bounds
def cutout_geometry_settings(context):
preferences = addon_preferences(context)
model = getattr(preferences, "geometry_model", DEFAULT_MOGE2_MODEL_KEY)
return (
model,
int(getattr(preferences, "geometry_resolution_level", 5)),
)
class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.cutout_selection_to_shape"
bl_label = "Cutout Selection to Shape"
bl_description = "Build the image selection as a Cutout shape"
bl_options = {"INTERNAL"}
job_type = "generate-cutout-artifacts"
starting_message = "Generating Cutout artifacts"
input_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)
shape: bpy.props.StringProperty(default="SOLID", options=_HIDDEN)
point_spacing: bpy.props.FloatProperty(
default=32.0,
min=0.01,
options=_HIDDEN,
)
model: bpy.props.StringProperty(
default=DEFAULT_MOGE2_MODEL_KEY,
options=_HIDDEN,
)
resolution_level: bpy.props.IntProperty(
default=5,
min=0,
max=9,
options=_HIDDEN,
)
max_ai_input_size: bpy.props.IntProperty(
default=DEFAULT_MAX_AI_INPUT_SIZE,
min=MIN_MAX_AI_INPUT_SIZE,
max=MAX_MAX_AI_INPUT_SIZE,
options=_HIDDEN,
)
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:
raise ValueError("Cutout currently supports still images")
if self.shape not in CUTOUT_SHAPES:
raise ValueError(f"Unsupported Cutout shape: {self.shape}")
selection_path = SelectionPath.from_json(self.selection_path_json)
selection_mask = rasterize_selection_path(
tuple(source_object.data.size),
selection_path,
antialias=True,
)
source_rgba = image_rgba(source_object.data)
context.view_layer.objects.active = source_object
source_object.select_set(True)
crop_width = selection_mask.bounds[2] - selection_mask.bounds[0]
crop_height = selection_mask.bounds[3] - selection_mask.bounds[1]
self.point_spacing = limit_cutout_point_spacing(
self.point_spacing,
(crop_width, crop_height),
configured_max_cutout_mesh_resolution(context),
)
self.max_ai_input_size = configured_max_ai_input_size(context)
if not self._needs_generation():
left, top, right, bottom = selection_mask.bounds
content_values = _cutout_content_values(
source_rgba[top:bottom, left:right, 3],
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,
)
return {"FINISHED"}
setup_result = invoke_ai_setup_if_needed()
if setup_result is not None:
return setup_result
self.model, self.resolution_level = cutout_geometry_settings(context)
self.selection_bounds_json = json.dumps(
selection_mask.bounds,
separators=(",", ":"),
)
self._selection_mask = selection_mask
self.input_path = str(
prepare_image_bounds_input(
source_object.data,
source_rgba,
selection_mask.bounds,
)
)
except (
ImageEditWarning,
OSError,
RuntimeError,
TypeError,
ValueError,
) as error:
report_image_edit_exception(self, error)
return {"CANCELLED"}
result = super().execute(context)
if "CANCELLED" in result:
cleanup_image_input(self.input_path, True)
return result
def request(self, context):
require_environment()
source_path = require_input_path(self.input_path)
generate_depth = shape_uses_depth(self.shape)
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
if generate_depth or normal_mode != NormalMode.NONE:
require_model(self.model)
parameters = {
"input": str(source_path),
"device": production_device().lower(),
"generate_depth": generate_depth,
"normal_mode": normal_mode.value,
"max_input_size": int(self.max_ai_input_size),
"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,
)
parameters.update(
moge2_parameters(
context.scene.anyimage_settings,
source_path,
self.model,
self.resolution_level,
start_frame,
)
)
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(
self.input_path,
source_object,
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:
raise RuntimeError("The Cutout Selection is no longer available")
content_values = _cutout_content_values(
source_alpha,
selection_mask.values,
alpha_threshold=self.alpha_threshold,
)
content_bounds = color_bounds
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
normal_key = f"{normal_mode.value.lower()}_normal"
if normal_key in value:
normal_image = load_normal_result_image(
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"))
depth_image = load_depth_result_image(
result.file("depth"),
source_object,
depth_metadata,
)
shape_started = True
create_cutout_shape(
context,
source_object,
self.shape,
self.point_spacing,
content_values,
content_bounds,
depth_image=depth_image,
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,
)
except Exception:
if normal_image is not None and normal_image.users == 0:
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:
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)
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_options = {"INTERNAL"}
active_tool_id = "anyimage.cutout_lasso"
gesture = "LASSO"
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
_pie_event = None
def _set_status(self, context):
context.workspace.status_text_set(
"Cutout Lasso: drag and release for shapes; RMB or Esc cancels"
)
def invoke(self, context, event):
self._pie_event = event
return super().invoke(context, event)
def modal(self, context, event):
self._pie_event = event
return ImageGesture.modal(self, context, event)
def _submit_selection_path(self, context, selection_path):
try:
source_object = require_image_empty(self.source_object_name)
except RuntimeError as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
open_shape_pie(
context,
self._pie_event,
source_object,
selection_path,
)
return {"FINISHED"}
class CutoutTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.cutout_lasso"
bl_label = "Cutout"
bl_description = "Lasso an Image Empty and choose a mesh shape"
bl_icon = "ops.mesh.primitive_sphere_add_gizmo"
bl_operator = SelectCutoutSelection.bl_idname
bl_keymap = image_edit_drag_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_mesh_detail")
layout.prop(settings, "cutout_fine_outline")
layout.prop(settings, "cutout_alpha_threshold")
layout.prop(settings, "cutout_inward_axis")