Add comprehensive tests for image processing and mask functionalities
- Introduced tests for color image handling, ensuring source preservation and premultiplied alpha usage. - Implemented tests for brush stroke operations, validating segment boundaries and brush anti-aliasing. - Added tests for frame anti-aliasing, confirming correct rendering behavior with various alpha values and depth settings. - Created tests for frame projection, ensuring accurate overlap calculations and half-plane clipping. - Developed tests for image premultiplied retention, verifying behavior during loading and packing processes. - Added undo functionality tests for mask operations, ensuring single transaction behavior during edits.
This commit is contained in:
@@ -1213,7 +1213,7 @@ def depth_balloon_projection_parameters(
|
||||
)
|
||||
if not usable.any():
|
||||
raise ValueError("MoGe-2 found no valid Depth Balloon samples")
|
||||
local_coordinates = vertices[:, 1:]
|
||||
local_coordinates = vertices[:, :2]
|
||||
metric_depth = sampled[usable] * float(uniform_scale)
|
||||
plane = _stable_plane(local_coordinates[usable], metric_depth)
|
||||
reference = metric_depth_reference(
|
||||
@@ -1221,7 +1221,7 @@ def depth_balloon_projection_parameters(
|
||||
calibration_valid,
|
||||
uniform_scale,
|
||||
)
|
||||
direction = np.asarray((-plane[0], plane[1], 1.0), dtype=np.float64)
|
||||
direction = np.asarray((plane[0], plane[1], 1.0), dtype=np.float64)
|
||||
direction /= np.linalg.norm(direction)
|
||||
return reference, tuple(float(value) for value in direction)
|
||||
|
||||
@@ -1238,19 +1238,6 @@ class BaseShape:
|
||||
balloon: object
|
||||
|
||||
|
||||
def orient_cutout_vertices(vertices):
|
||||
import numpy as np
|
||||
|
||||
vertices = np.asarray(vertices, dtype=np.float64)
|
||||
return np.column_stack(
|
||||
(
|
||||
-vertices[:, 2],
|
||||
-vertices[:, 0],
|
||||
vertices[:, 1],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def cutout_domain(
|
||||
selection_contours,
|
||||
bounds,
|
||||
@@ -1433,7 +1420,7 @@ def build_base_shape(
|
||||
)
|
||||
source_vertices, faces, uv, _boundary, planar, balloon = domain
|
||||
return BaseShape(
|
||||
vertices=orient_cutout_vertices(source_vertices),
|
||||
vertices=np.asarray(source_vertices, dtype=np.float64),
|
||||
faces=counter_clockwise_faces(planar, faces),
|
||||
uv=np.asarray(uv, dtype=np.float32),
|
||||
balloon=np.asarray(balloon, dtype=np.float64) / world_z_scale,
|
||||
|
||||
@@ -29,7 +29,6 @@ def _pie_button(
|
||||
icon,
|
||||
source_name,
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
point_spacing,
|
||||
inward_axis,
|
||||
@@ -42,7 +41,6 @@ def _pie_button(
|
||||
operator.source_object_name = source_name
|
||||
operator.selection_path_json = selection_path_json
|
||||
operator.shape = shape
|
||||
operator.refine_selection = refine_selection
|
||||
operator.generate_normal = generate_normal
|
||||
operator.point_spacing = point_spacing
|
||||
operator.inward_axis = inward_axis
|
||||
@@ -53,14 +51,12 @@ def open_shape_pie(
|
||||
event,
|
||||
source_object,
|
||||
selection_path,
|
||||
refine_selection=False,
|
||||
):
|
||||
source_name = source_object.name
|
||||
selection_path_json = selection_path.to_json()
|
||||
settings = context.scene.anyimage_settings
|
||||
point_spacing = cutout_mesh_detail_spacing(settings.cutout_mesh_detail)
|
||||
ready = ai_ready()
|
||||
refine_selection = ready and refine_selection
|
||||
generate_normal = ready and bool(
|
||||
getattr(settings, "cutout_generate_normal", False)
|
||||
)
|
||||
@@ -77,7 +73,6 @@ def open_shape_pie(
|
||||
icon,
|
||||
source_name,
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
point_spacing,
|
||||
inward_axis,
|
||||
|
||||
@@ -9,11 +9,7 @@ from ...preferences import (
|
||||
configured_max_ai_input_size,
|
||||
configured_max_cutout_mesh_resolution,
|
||||
)
|
||||
from ...properties import (
|
||||
BEN2_MODEL_KEY,
|
||||
DEFAULT_MOGE2_MODEL_KEY,
|
||||
refine_selection_property,
|
||||
)
|
||||
from ...properties import DEFAULT_MOGE2_MODEL_KEY
|
||||
from ...runtime import JobOperatorBase
|
||||
from ...server.media.resolution import (
|
||||
DEFAULT_MAX_AI_INPUT_SIZE,
|
||||
@@ -43,9 +39,8 @@ from ...common.image import (
|
||||
)
|
||||
from ...common.depth import load_depth_metadata, load_depth_result_image
|
||||
from ...common.viewport import (
|
||||
CropSelectionGesture,
|
||||
apply_crop_settings,
|
||||
gesture_property,
|
||||
ImageGesture,
|
||||
image_edit_drag_keymap,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
serialize_matrix,
|
||||
@@ -158,14 +153,9 @@ def _cutout_content_values(source_alpha, selection_values=None):
|
||||
return values
|
||||
|
||||
|
||||
def _generated_color_result(result, input_path, source_object, source_bounds):
|
||||
value = result.value if isinstance(result.value, dict) else {}
|
||||
if "selection_image" not in value:
|
||||
path = input_path
|
||||
else:
|
||||
path = result.file("selection_image")
|
||||
def _generated_color_result(input_path, source_object, source_bounds):
|
||||
image = load_packed_rgba_image(
|
||||
path,
|
||||
input_path,
|
||||
source_object.data,
|
||||
color_data_name(source_object),
|
||||
)
|
||||
@@ -222,18 +212,13 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
options=_HIDDEN,
|
||||
)
|
||||
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
refine_selection: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
inward_axis: bpy.props.StringProperty(
|
||||
default=CutoutInwardAxis.NEGATIVE_Z.value,
|
||||
options=_HIDDEN,
|
||||
)
|
||||
|
||||
def _needs_generation(self):
|
||||
return (
|
||||
self.refine_selection
|
||||
or self.shape in DEPTH_CUTOUT_SHAPES
|
||||
or self.generate_normal
|
||||
)
|
||||
return self.shape in DEPTH_CUTOUT_SHAPES or self.generate_normal
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
@@ -321,20 +306,15 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
source_path = require_input_path(self.input_path)
|
||||
depth_mode = depth_mode_for_shape(self.shape)
|
||||
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
|
||||
if self.refine_selection:
|
||||
require_model(BEN2_MODEL_KEY)
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
require_model(self.model)
|
||||
parameters = {
|
||||
"input": str(source_path),
|
||||
"device": production_device().lower(),
|
||||
"refine_selection": self.refine_selection,
|
||||
"depth_mode": depth_mode,
|
||||
"normal_mode": normal_mode.value,
|
||||
"max_input_size": int(self.max_ai_input_size),
|
||||
}
|
||||
if self.refine_selection:
|
||||
parameters["refine_model"] = BEN2_MODEL_KEY
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
start_frame = source_start_frame(self.source_object_name)
|
||||
parameters.update(
|
||||
@@ -361,7 +341,6 @@ 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(
|
||||
result,
|
||||
self.input_path,
|
||||
source_object,
|
||||
source_bounds,
|
||||
@@ -426,19 +405,16 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
|
||||
class SelectCutoutSelection(CropSelectionGesture, 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_options = {"INTERNAL"}
|
||||
active_tool_id = "anyimage.cutout_lasso"
|
||||
refine_selection_settings = {"LASSO": "cutout_refine_selection"}
|
||||
gesture = "LASSO"
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
refine_selection: refine_selection_property()
|
||||
invert: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
gesture: gesture_property()
|
||||
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
_pie_event = None
|
||||
@@ -462,8 +438,6 @@ class SelectCutoutSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
self.report({"WARNING"}, "Cutout only supports a still image")
|
||||
return {"CANCELLED"}
|
||||
apply_crop_settings(self, context)
|
||||
self.invert = False
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
width, height = (int(value) for value in source_object.data.size)
|
||||
@@ -474,7 +448,7 @@ class SelectCutoutSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
|
||||
def modal(self, context, event):
|
||||
self._pie_event = event
|
||||
return CropSelectionGesture.modal(self, context, event)
|
||||
return ImageGesture.modal(self, context, event)
|
||||
|
||||
def _submit_selection_path(self, context, selection_path):
|
||||
try:
|
||||
@@ -487,7 +461,6 @@ class SelectCutoutSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
self._pie_event,
|
||||
source_object,
|
||||
selection_path,
|
||||
self.refine_selection,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -504,19 +477,17 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
(
|
||||
SelectCutoutSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
|
||||
{"properties": [("gesture", "LASSO"), ("full_image", True)]},
|
||||
{"properties": [("full_image", True)]},
|
||||
),
|
||||
(
|
||||
*image_edit_drag_keymap(
|
||||
SelectCutoutSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "LASSO")]},
|
||||
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_refine_selection", status)
|
||||
draw_ai_property(layout, settings, "cutout_generate_normal", status)
|
||||
layout.prop(settings, "cutout_mesh_detail")
|
||||
layout.prop(settings, "cutout_inward_axis")
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import StrEnum
|
||||
|
||||
import bpy
|
||||
|
||||
from ...common.image import color_data_name
|
||||
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,
|
||||
@@ -31,7 +31,7 @@ from .geometry import (
|
||||
from .shape import (
|
||||
BALLOON_ATTRIBUTE_NAME,
|
||||
BALLOON_CUTOUT_SHAPES,
|
||||
CUTOUT_AXIS_TO_SOURCE,
|
||||
CUTOUT_X_TO_Z,
|
||||
CUTOUT_NODE_GROUP_NAME,
|
||||
CUTOUT_SHAPE_INDICES,
|
||||
DEPTH_CUTOUT_SHAPES,
|
||||
@@ -55,13 +55,13 @@ def _cutout_world_matrix(source_matrix, crop_center, inward_axis):
|
||||
center_x, center_y = (float(value) for value in crop_center)
|
||||
matrix = source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
|
||||
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
|
||||
matrix @= Matrix(CUTOUT_AXIS_TO_SOURCE)
|
||||
matrix @= Matrix(CUTOUT_X_TO_Z)
|
||||
return matrix
|
||||
|
||||
|
||||
def orient_object_normal_image(image, inward_axis):
|
||||
"""Convert internal +X Cutout normals to the selected object axes."""
|
||||
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
|
||||
"""Convert native Z-axis Cutout normals to the selected object axes."""
|
||||
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.NEGATIVE_Z:
|
||||
return image
|
||||
import numpy as np
|
||||
|
||||
@@ -70,7 +70,7 @@ def orient_object_normal_image(image, inward_axis):
|
||||
rgba = pixels.reshape((-1, 4))
|
||||
normal = rgba[:, :3] * 2.0 - 1.0
|
||||
rgba[:, :3] = np.column_stack(
|
||||
(-normal[:, 1], normal[:, 2], -normal[:, 0])
|
||||
(-normal[:, 2], -normal[:, 0], normal[:, 1])
|
||||
) * 0.5 + 0.5
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
@@ -120,7 +120,7 @@ def create_cropped_color_image(source_object, crop_bounds, source_rgba):
|
||||
)
|
||||
try:
|
||||
image.colorspace_settings.name = source.colorspace_settings.name
|
||||
image.alpha_mode = source.alpha_mode
|
||||
configure_color_alpha(image)
|
||||
image.pixels.foreach_set(np.flipud(cropped).ravel())
|
||||
image.pack()
|
||||
image.update()
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
|
||||
BALLOON_ATTRIBUTE_NAME = "o_balloon"
|
||||
CUTOUT_NODE_GROUP_NAME = "O Image Cutout"
|
||||
CUTOUT_AXIS_TO_SOURCE = (
|
||||
CUTOUT_X_TO_Z = (
|
||||
(0.0, -1.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 1.0, 0.0),
|
||||
(-1.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
CUTOUT_Z_TO_X = (
|
||||
(0.0, 0.0, -1.0, 0.0),
|
||||
(-1.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 1.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
CUTOUT_NODE_GROUP_NAMES = {
|
||||
"SURFACE": "O Image Surface",
|
||||
"BALLOON": "O Image Balloon",
|
||||
|
||||
Reference in New Issue
Block a user