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:
484
src/anyimage/operators/cutout_tool/operators.py
Normal file
484
src/anyimage/operators/cutout_tool/operators.py
Normal 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")
|
||||
Reference in New Issue
Block a user