Add comprehensive tests for cutout and mesh functionalities
- Introduced `test_cutout.py` to validate cutout pie menu behavior, image lasso options, and mesh detail calculations. - Added `test_mesh.py` to ensure correct tangent normal rasterization, plane source topology, and depth surface evaluations. - Created `test_mesh_blender.py` to verify material node groups, tangent baking, and local cutout building from Blender images. - Implemented tests for handling alpha regions and ensuring geometry nodes maintain expected behavior.
This commit is contained in:
@@ -8,7 +8,6 @@ from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
|
||||
@@ -94,27 +93,18 @@ class AddonTest(unittest.TestCase):
|
||||
cls.depth_build = importlib.import_module(
|
||||
"anyimage.operators.depth_plane.build"
|
||||
)
|
||||
cls.mesh_plane = importlib.import_module(
|
||||
"anyimage.operators.mesh_plane"
|
||||
)
|
||||
cls.mesh_plane = importlib.import_module("anyimage.operators.mesh_plane")
|
||||
cls.mesh = importlib.import_module("anyimage.operators.mesh")
|
||||
cls.image_background = importlib.import_module("anyimage.operators.background")
|
||||
cls.image_upscale = importlib.import_module("anyimage.operators.upscale")
|
||||
cls.image_mesh = importlib.import_module("anyimage.operators.surface")
|
||||
cls.image_edit_common = importlib.import_module(
|
||||
"anyimage.operators.image_edit.common"
|
||||
)
|
||||
cls.image_mask = importlib.import_module(
|
||||
"anyimage.operators.image_edit.mask"
|
||||
)
|
||||
cls.image_box = importlib.import_module(
|
||||
"anyimage.operators.image_edit.box"
|
||||
)
|
||||
cls.image_lasso = importlib.import_module(
|
||||
"anyimage.operators.image_edit.lasso"
|
||||
)
|
||||
cls.image_line = importlib.import_module(
|
||||
"anyimage.operators.image_edit.line"
|
||||
)
|
||||
cls.image_mask = importlib.import_module("anyimage.operators.image_edit.mask")
|
||||
cls.image_box = importlib.import_module("anyimage.operators.image_edit.box")
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.image_edit.lasso")
|
||||
cls.image_line = importlib.import_module("anyimage.operators.image_edit.line")
|
||||
cls.image_polyline = importlib.import_module(
|
||||
"anyimage.operators.image_edit.polyline"
|
||||
)
|
||||
@@ -251,8 +241,8 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertFalse(name.startswith("anyimage_"), (operator, name))
|
||||
|
||||
def test_concrete_job_operator_is_the_blender_operator(self):
|
||||
operator = self.addon.operators.GenerateMoge2Depth
|
||||
self.assertEqual(operator.bl_idname, "anyimage.generate_moge2_depth")
|
||||
operator = self.addon.operators.GenerateMesh
|
||||
self.assertEqual(operator.bl_idname, "anyimage.generate_mesh")
|
||||
self.assertIn(self.job_runtime.JobOperatorBase, operator.__mro__)
|
||||
self.assertIn(self.fake_bpy.types.Operator, operator.__mro__)
|
||||
|
||||
@@ -265,15 +255,15 @@ class AddonTest(unittest.TestCase):
|
||||
device="CPU",
|
||||
video_frames=3,
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=settings)
|
||||
)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
||||
for model_key in self.addon.properties.shared_model_catalog.UPSCALE_MODELS:
|
||||
operator = self.debug.DebugUpscale()
|
||||
operator.model = model_key
|
||||
with self.subTest(model=model_key), patch.object(
|
||||
self.debug, "require_environment"
|
||||
), patch.object(self.debug, "require_model") as require_model:
|
||||
with (
|
||||
self.subTest(model=model_key),
|
||||
patch.object(self.debug, "require_environment"),
|
||||
patch.object(self.debug, "require_model") as require_model,
|
||||
):
|
||||
request = operator.request(context)
|
||||
require_model.assert_called_once_with(model_key)
|
||||
self.assertEqual(request["model"], model_key)
|
||||
@@ -618,18 +608,23 @@ class AddonTest(unittest.TestCase):
|
||||
):
|
||||
status = properties.ai_status()
|
||||
self.assertFalse(status["ready"])
|
||||
self.assertEqual(properties.ai_setup_label(status), "Set Up AI Environment…")
|
||||
self.assertEqual(
|
||||
properties.ai_setup_label(status), "Set Up AI Environment…"
|
||||
)
|
||||
|
||||
def test_setup_ai_environment_selects_the_needed_install_step(self):
|
||||
calls = []
|
||||
ops = SimpleNamespace(
|
||||
anyimage=SimpleNamespace(
|
||||
install_environment=lambda *_args: calls.append("official")
|
||||
or {"FINISHED"},
|
||||
install_environment_mirror=lambda *_args: calls.append("mirror")
|
||||
or {"FINISHED"},
|
||||
download_required_models=lambda *_args: calls.append("models")
|
||||
or {"FINISHED"},
|
||||
install_environment=lambda *_args: (
|
||||
calls.append("official") or {"FINISHED"}
|
||||
),
|
||||
install_environment_mirror=lambda *_args: (
|
||||
calls.append("mirror") or {"FINISHED"}
|
||||
),
|
||||
download_required_models=lambda *_args: (
|
||||
calls.append("models") or {"FINISHED"}
|
||||
),
|
||||
)
|
||||
)
|
||||
operator = self.model_operators.SetupAIEnvironment()
|
||||
@@ -696,12 +691,20 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
catalog = tuple({
|
||||
"key": key, "label": key, "family": "upscale",
|
||||
"enabled": True, "ready": False,
|
||||
} for key in (
|
||||
"REALESRGAN_GENERAL_WDN_X4V3", "SAFMN_REAL_X4", "REALESRGAN_X4PLUS",
|
||||
))
|
||||
catalog = tuple(
|
||||
{
|
||||
"key": key,
|
||||
"label": key,
|
||||
"family": "upscale",
|
||||
"enabled": True,
|
||||
"ready": False,
|
||||
}
|
||||
for key in (
|
||||
"REALESRGAN_GENERAL_WDN_X4V3",
|
||||
"SAFMN_REAL_X4",
|
||||
"REALESRGAN_X4PLUS",
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
self.addon.properties,
|
||||
@@ -728,19 +731,21 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn("upscale_model", properties)
|
||||
self.assertIn("upscale_max_output_size", properties)
|
||||
self.assertEqual(
|
||||
preference_module.AnyImagePreferences.__annotations__[
|
||||
"upscale_model"
|
||||
]["name"],
|
||||
preference_module.AnyImagePreferences.__annotations__["upscale_model"][
|
||||
"name"
|
||||
],
|
||||
"Default Upscale Model",
|
||||
)
|
||||
|
||||
def test_mesh_plane_operator_is_registered_with_material_options(self):
|
||||
operator = self.mesh_plane.ConvertToMeshPlane
|
||||
def test_semantic_mesh_operators_are_registered_without_dialog_options(self):
|
||||
operator = self.mesh.ConvertToPlane
|
||||
self.assertIn(operator, self.addon.CLASSES)
|
||||
self.assertEqual(operator.bl_idname, "anyimage.convert_to_mesh_plane")
|
||||
self.assertFalse(operator.__annotations__["shadeless"]["default"])
|
||||
self.assertFalse(operator.__annotations__["keep_original"]["default"])
|
||||
self.assertIn("invoke", operator.__dict__)
|
||||
self.assertIn(self.mesh.ConvertToDepthPlane, self.addon.CLASSES)
|
||||
self.assertIn(self.mesh.ConvertCutoutShape, self.addon.CLASSES)
|
||||
self.assertEqual(operator.bl_idname, "anyimage.convert_to_plane")
|
||||
self.assertNotIn("shadeless", operator.__annotations__)
|
||||
self.assertNotIn("keep_original", operator.__annotations__)
|
||||
self.assertNotIn("invoke", operator.__dict__)
|
||||
self.assertNotIn("draw", operator.__dict__)
|
||||
self.assertIn("REGISTER", operator.bl_options)
|
||||
self.assertIn("UNDO", operator.bl_options)
|
||||
@@ -758,9 +763,246 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
plain_mesh = SimpleNamespace(type="MESH", modifiers=[])
|
||||
self.assertTrue(operator.poll(SimpleNamespace(object=image_empty)))
|
||||
self.assertTrue(operator.poll(SimpleNamespace(object=mesh_plane)))
|
||||
self.assertFalse(operator.poll(SimpleNamespace(object=mesh_plane)))
|
||||
self.assertFalse(operator.poll(SimpleNamespace(object=plain_mesh)))
|
||||
|
||||
def test_plane_depth_off_accepts_animated_image_without_server(self):
|
||||
operator = SimpleNamespace(
|
||||
depth_mode="OFF",
|
||||
remove_background=False,
|
||||
shadeless=False,
|
||||
profile="SURFACE",
|
||||
mesh_detail=64,
|
||||
thickness=0.1,
|
||||
inflation=0.1,
|
||||
algorithm="POISSON",
|
||||
keep_original=False,
|
||||
)
|
||||
source = SimpleNamespace(data=SimpleNamespace(name="Sequence"))
|
||||
context = SimpleNamespace(object=source, scene=object())
|
||||
|
||||
with (
|
||||
patch.object(self.mesh, "video_total_frames", return_value=24),
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"image_user_settings",
|
||||
return_value=(1, 0, 24, True),
|
||||
),
|
||||
patch.object(self.mesh, "create_material", return_value="Material"),
|
||||
patch.object(self.mesh, "_copy_color_image", return_value="Color"),
|
||||
patch.object(self.mesh, "_build_mesh_object") as build_mesh,
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"invoke_ai_setup_if_needed",
|
||||
side_effect=AssertionError("local conversion must not start AI setup"),
|
||||
),
|
||||
):
|
||||
result = self.mesh.ConvertToPlane.execute(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
build_mesh.assert_called_once()
|
||||
|
||||
def test_cutout_and_balloon_without_refine_build_locally(self):
|
||||
source = SimpleNamespace(select_set=lambda _selected: None)
|
||||
context = SimpleNamespace(
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None))
|
||||
)
|
||||
|
||||
for shape in (self.mesh.CUTOUT, self.mesh.BALLOON):
|
||||
with self.subTest(shape=shape):
|
||||
operator = SimpleNamespace(
|
||||
source_object_name="Source",
|
||||
selection_data="[[0,0],[10,0],[0,10]]",
|
||||
shape=shape,
|
||||
mesh_detail=64,
|
||||
refine_selection=False,
|
||||
normal_analysis=False,
|
||||
invert=True,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
self.mesh.bpy.data.objects,
|
||||
"get",
|
||||
return_value=source,
|
||||
),
|
||||
patch.object(self.mesh, "is_image_empty", return_value=True),
|
||||
patch.object(self.mesh, "_build_local_cutout") as build_local,
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"_start_job",
|
||||
side_effect=AssertionError("local Cutout must not start a Job"),
|
||||
),
|
||||
):
|
||||
result = self.mesh.ConvertCutoutShape.execute(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
build_local.assert_called_once_with(
|
||||
operator,
|
||||
context,
|
||||
source,
|
||||
shape,
|
||||
operator.selection_data,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_refined_cutout_and_balloon_submit_blendjob(self):
|
||||
source = SimpleNamespace(select_set=lambda _selected: None)
|
||||
context = SimpleNamespace(
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None))
|
||||
)
|
||||
|
||||
for shape in (self.mesh.CUTOUT, self.mesh.BALLOON):
|
||||
with self.subTest(shape=shape):
|
||||
operator = SimpleNamespace(
|
||||
source_object_name="Source",
|
||||
selection_data="[[0,0],[10,0],[0,10]]",
|
||||
shape=shape,
|
||||
mesh_detail=64,
|
||||
refine_selection=True,
|
||||
normal_analysis=False,
|
||||
invert=False,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
self.mesh.bpy.data.objects,
|
||||
"get",
|
||||
return_value=source,
|
||||
),
|
||||
patch.object(self.mesh, "is_image_empty", return_value=True),
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"_build_local_cutout",
|
||||
side_effect=AssertionError("Refine must run through the Job"),
|
||||
),
|
||||
patch.object(
|
||||
self.mesh, "_start_job", return_value={"FINISHED"}
|
||||
) as start_job,
|
||||
):
|
||||
result = self.mesh.ConvertCutoutShape.execute(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
start_job.assert_called_once_with(
|
||||
operator,
|
||||
context,
|
||||
shape,
|
||||
operator.selection_data,
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_plain_balloon_job_requests_moge_normal(self):
|
||||
source = SimpleNamespace(
|
||||
name="Source",
|
||||
data=SimpleNamespace(size=(2048, 1024)),
|
||||
)
|
||||
context = SimpleNamespace(object=source, scene=SimpleNamespace())
|
||||
operator = SimpleNamespace(mesh_detail=64, report=lambda *_args: None)
|
||||
|
||||
with (
|
||||
patch.object(self.mesh, "video_total_frames", return_value=1),
|
||||
patch.object(
|
||||
self.mesh, "invoke_ai_setup_if_needed", return_value=None
|
||||
) as setup,
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"prepare_image_analysis_inputs",
|
||||
return_value=(
|
||||
Path("input.png"),
|
||||
False,
|
||||
Path("input.png"),
|
||||
False,
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
self.mesh,
|
||||
"_depth_settings",
|
||||
return_value=("MOGE2_VITL", 100.0, 5),
|
||||
),
|
||||
patch.object(
|
||||
self.mesh.bpy.ops.anyimage,
|
||||
"generate_mesh",
|
||||
return_value={"FINISHED"},
|
||||
create=True,
|
||||
) as generate_mesh,
|
||||
):
|
||||
result = self.mesh._start_job(
|
||||
operator, context, self.mesh.BALLOON, normal_analysis=True
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
setup.assert_called_once_with()
|
||||
self.assertEqual(generate_mesh.call_args.kwargs["shape"], self.mesh.BALLOON)
|
||||
self.assertEqual(generate_mesh.call_args.kwargs["mesh_detail"], 64)
|
||||
self.assertEqual(
|
||||
generate_mesh.call_args.kwargs["normal_model"],
|
||||
self.addon.properties.DEFAULT_MOGE2_MODEL_KEY,
|
||||
)
|
||||
self.assertEqual(generate_mesh.call_args.kwargs["normal_resolution_level"], 5)
|
||||
|
||||
def test_all_cutout_shapes_request_moge_normal_when_enabled(self):
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1))
|
||||
)
|
||||
for shape in self.mesh.CUTOUT_SHAPES:
|
||||
with self.subTest(shape=shape):
|
||||
operator = self.mesh.GenerateMesh()
|
||||
operator.input_path = __file__
|
||||
operator.source_object_name = ""
|
||||
operator.shape = shape
|
||||
operator.selection_data = "[[0,0],[10,0],[0,10]]"
|
||||
operator.model = self.addon.properties.DEFAULT_MOGE2_MODEL_KEY
|
||||
operator.mesh_detail = 64
|
||||
operator.process_res = 1
|
||||
operator.resolution_level = 5
|
||||
operator.normal_model = self.addon.properties.DEFAULT_MOGE2_MODEL_KEY
|
||||
operator.normal_resolution_level = 5
|
||||
operator.normal_analysis = True
|
||||
operator.refine_selection = False
|
||||
operator.invert = False
|
||||
with (
|
||||
patch.object(self.mesh, "require_environment"),
|
||||
patch.object(self.mesh, "require_model") as require_model,
|
||||
patch.object(self.mesh, "production_device", return_value="CPU"),
|
||||
patch.object(self.mesh, "source_start_frame", return_value=1),
|
||||
):
|
||||
parameters = operator.request(context)
|
||||
|
||||
require_model.assert_called_once_with(
|
||||
self.addon.properties.DEFAULT_MOGE2_MODEL_KEY
|
||||
)
|
||||
self.assertEqual(
|
||||
parameters["normal_model"],
|
||||
self.addon.properties.DEFAULT_MOGE2_MODEL_KEY,
|
||||
)
|
||||
self.assertEqual(parameters["normal_resolution_level"], 5)
|
||||
self.assertEqual(parameters["mesh_detail"], 64)
|
||||
|
||||
def test_cutout_job_skips_moge_normal_model_when_disabled(self):
|
||||
operator = self.mesh.GenerateMesh()
|
||||
operator.input_path = __file__
|
||||
operator.source_object_name = ""
|
||||
operator.shape = self.mesh.CUTOUT
|
||||
operator.selection_data = "[[0,0],[10,0],[0,10]]"
|
||||
operator.mesh_detail = 64
|
||||
operator.normal_analysis = False
|
||||
operator.refine_selection = False
|
||||
operator.invert = False
|
||||
|
||||
with (
|
||||
patch.object(self.mesh, "require_environment"),
|
||||
patch.object(self.mesh, "require_model") as require_model,
|
||||
patch.object(self.mesh, "production_device", return_value="CPU"),
|
||||
patch.object(self.mesh, "source_start_frame", return_value=1),
|
||||
):
|
||||
parameters = operator.request(SimpleNamespace(scene=SimpleNamespace()))
|
||||
|
||||
require_model.assert_not_called()
|
||||
self.assertFalse(parameters["normal_analysis"])
|
||||
self.assertNotIn("normal_model", parameters)
|
||||
self.assertNotIn("normal_resolution_level", parameters)
|
||||
def test_mesh_plane_redo_keeps_the_original_world_matrix(self):
|
||||
operator = self.mesh_plane.ConvertToMeshPlane()
|
||||
operator.shadeless = False
|
||||
@@ -818,9 +1060,7 @@ class AddonTest(unittest.TestCase):
|
||||
import inspect
|
||||
|
||||
mesh_source = inspect.getsource(self.mesh_plane.create_mesh_plane_mesh)
|
||||
processing_source = inspect.getsource(
|
||||
self.mesh_plane.add_mesh_plane_processing
|
||||
)
|
||||
processing_source = inspect.getsource(self.mesh_plane.add_mesh_plane_processing)
|
||||
self.assertIn("((0, 1, 2, 3),)", mesh_source)
|
||||
self.assertIn('("Subdivide", max(0, int(subdivisions)))', processing_source)
|
||||
self.assertIn('("Thickness", max(0.0, float(thickness)))', processing_source)
|
||||
@@ -865,9 +1105,8 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
calls,
|
||||
[
|
||||
("anyimage.convert_to_mesh_plane", True),
|
||||
("anyimage.convert_to_plane", True),
|
||||
("anyimage.convert_to_depth_plane", True),
|
||||
("anyimage.convert_to_surface", True),
|
||||
("separator", True),
|
||||
("anyimage.remove_image_background", True),
|
||||
("anyimage.upscale_image", True),
|
||||
@@ -884,6 +1123,7 @@ class AddonTest(unittest.TestCase):
|
||||
labels["anyimage.upscale_image"],
|
||||
"Upscale (640 × 480)",
|
||||
)
|
||||
|
||||
def test_image_menu_opens_environment_settings_when_ai_is_ready(self):
|
||||
calls = []
|
||||
|
||||
@@ -933,9 +1173,7 @@ class AddonTest(unittest.TestCase):
|
||||
operator.model = "SAFMN_REAL_X4"
|
||||
operator.max_output_size = 6144
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(
|
||||
anyimage_settings=SimpleNamespace(video_frames=7)
|
||||
)
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=7))
|
||||
)
|
||||
with (
|
||||
patch.object(self.image_upscale, "require_environment"),
|
||||
@@ -943,12 +1181,8 @@ class AddonTest(unittest.TestCase):
|
||||
patch.object(
|
||||
self.image_upscale, "input_path", return_value=Path("input.png")
|
||||
),
|
||||
patch.object(
|
||||
self.image_upscale, "production_device", return_value="CUDA"
|
||||
),
|
||||
patch.object(
|
||||
self.image_upscale, "source_start_frame", return_value=4
|
||||
),
|
||||
patch.object(self.image_upscale, "production_device", return_value="CUDA"),
|
||||
patch.object(self.image_upscale, "source_start_frame", return_value=4),
|
||||
):
|
||||
request = operator.request(context)
|
||||
|
||||
@@ -979,9 +1213,7 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
object=source_object,
|
||||
scene=SimpleNamespace(
|
||||
anyimage_settings=SimpleNamespace(video_frames=1)
|
||||
),
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)),
|
||||
)
|
||||
operator = self.image_upscale.UpscaleImage()
|
||||
with (
|
||||
@@ -1109,6 +1341,22 @@ class AddonTest(unittest.TestCase):
|
||||
):
|
||||
operator.response(None, SimpleNamespace(directory=Path("result")))
|
||||
|
||||
def test_depth_analysis_follows_the_oldest_upscale_source(self):
|
||||
original = SimpleNamespace(name="Original")
|
||||
first_upscale = SimpleNamespace(
|
||||
name="First",
|
||||
anyimage_upscale_source=original,
|
||||
)
|
||||
second_upscale = SimpleNamespace(
|
||||
name="Second",
|
||||
anyimage_upscale_source=first_upscale,
|
||||
)
|
||||
|
||||
self.assertIs(
|
||||
self.image_data.analysis_source_image(second_upscale),
|
||||
original,
|
||||
)
|
||||
|
||||
def test_repeated_upscale_uses_original_image_and_reports_warning(self):
|
||||
original_image = SimpleNamespace(name="Original", source="FILE")
|
||||
current_image = SimpleNamespace(
|
||||
@@ -1123,9 +1371,7 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
object=source_object,
|
||||
scene=SimpleNamespace(
|
||||
anyimage_settings=SimpleNamespace(video_frames=1)
|
||||
),
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)),
|
||||
)
|
||||
reports = []
|
||||
operator = self.image_upscale.UpscaleImage()
|
||||
@@ -1287,14 +1533,53 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertGreater(self.balloon_geometry.polygon_area(contour), 0.0)
|
||||
self.assertLess(float(lengths.std() / lengths.mean()), 0.15)
|
||||
|
||||
def test_balloon_contour_smooths_pixel_stairs_before_coarse_resampling(self):
|
||||
import numpy as np
|
||||
|
||||
staircase = [(0.0, 0.0)]
|
||||
for value in range(16):
|
||||
staircase.append((value + 1.0, float(value)))
|
||||
staircase.append((value + 1.0, value + 1.0))
|
||||
staircase.append((0.0, 16.0))
|
||||
smooth = self.balloon_geometry.smooth_lasso_contour(staircase, 2.0)
|
||||
|
||||
def diagonal_noise(points):
|
||||
points = np.asarray(points)
|
||||
selected = (
|
||||
(points[:, 0] > 2.0)
|
||||
& (points[:, 0] < 14.0)
|
||||
& (np.abs(points[:, 1] - points[:, 0]) < 2.0)
|
||||
)
|
||||
return float(np.std(points[selected, 1] - points[selected, 0]))
|
||||
|
||||
self.assertLess(diagonal_noise(smooth), 0.05)
|
||||
|
||||
def test_cutout_polygon_mask_keeps_nonzero_winding_overlap(self):
|
||||
polygon = (
|
||||
(1, 1),
|
||||
(7, 1),
|
||||
(7, 7),
|
||||
(1, 7),
|
||||
(1, 1),
|
||||
(4, 1),
|
||||
(10, 1),
|
||||
(10, 7),
|
||||
(4, 7),
|
||||
(4, 1),
|
||||
(1, 1),
|
||||
)
|
||||
|
||||
even_odd = self.image_mask.polygon_mask((12, 9), polygon)
|
||||
nonzero = self.image_mask.polygon_mask((12, 9), polygon, "NONZERO")
|
||||
|
||||
self.assertFalse(even_odd[3, 5])
|
||||
self.assertTrue(nonzero[3, 5])
|
||||
def test_image_mask_preserves_source_rgb_outside_the_selection(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.full((3, 3, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
|
||||
source = np.flipud(top_down).ravel()
|
||||
selection = self.image_mask.polygon_mask(
|
||||
(3, 3), ((0, 0), (3, 0), (0, 3))
|
||||
)
|
||||
selection = self.image_mask.polygon_mask((3, 3), ((0, 0), (3, 0), (0, 3)))
|
||||
pixels, bounds = self.image_mask.edit_image_mask_pixels(
|
||||
source,
|
||||
(3, 3),
|
||||
@@ -1558,9 +1843,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn(tool, self.addon.CLASSES)
|
||||
self.assertEqual(tool.bl_operator, operator.bl_idname)
|
||||
self.assertEqual(tool.bl_icon, icon)
|
||||
self.assertEqual(
|
||||
tool.bl_keymap[0][2]["properties"], [("gesture", gesture)]
|
||||
)
|
||||
self.assertEqual(tool.bl_keymap[0][2]["properties"], [("gesture", gesture)])
|
||||
self.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_lasso.ImageLassoTool.__dict__)
|
||||
@@ -1574,10 +1857,16 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
settings = self.addon.properties.AnyImageSettings.__annotations__
|
||||
self.assertFalse(settings["image_edit_refine_selection"]["default"])
|
||||
self.assertEqual(settings["cutout_pixel_size"]["default"], 32)
|
||||
self.assertEqual(settings["cutout_pixel_size"]["min"], 4)
|
||||
self.assertEqual(settings["cutout_pixel_size"]["max"], 32)
|
||||
self.assertFalse(settings["cutout_normal_analysis"]["default"])
|
||||
self.assertNotIn("image_extract_continuous", settings)
|
||||
self.assertNotIn("image_extract_padding", settings)
|
||||
self.assertNotIn("image_extract_remove_background", settings)
|
||||
source_picker = inspect.getsource(self.image_edit_common.resolve_image_edit_click)
|
||||
source_picker = inspect.getsource(
|
||||
self.image_edit_common.resolve_image_edit_click
|
||||
)
|
||||
self.assertIn("bpy.ops.view3d.select(", source_picker)
|
||||
self.assertIn("Click an Image Empty", source_picker)
|
||||
self.assertIn("_restore_selection(", source_picker)
|
||||
@@ -1585,6 +1874,11 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn('event.type == "F"', modal_source)
|
||||
self.assertIn("self.invert = not self.invert", modal_source)
|
||||
|
||||
self.assertIn("math.dist(point, self._path[-1]) >= 2.0", modal_source)
|
||||
mask_source = inspect.getsource(self.image_mask.polygon_mask)
|
||||
self.assertNotIn("np.indices", mask_source)
|
||||
self.assertIn("range(top, bottom, 128)", mask_source)
|
||||
|
||||
def test_image_line_and_polyline_disable_refine_selection(self):
|
||||
settings = SimpleNamespace(image_edit_refine_selection=True)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
||||
@@ -1614,9 +1908,7 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_area_click_is_cancelled_without_a_warning(self):
|
||||
import inspect
|
||||
|
||||
completion_source = inspect.getsource(
|
||||
self.image_mask.EditImageMask._complete
|
||||
)
|
||||
completion_source = inspect.getsource(self.image_mask.EditImageMask._complete)
|
||||
|
||||
self.assertIn("if len(path) < 3", completion_source)
|
||||
self.assertIn('return {"CANCELLED"}', completion_source)
|
||||
@@ -1625,12 +1917,8 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_polyline_closes_from_start_or_enter(self):
|
||||
import inspect
|
||||
|
||||
modal_source = inspect.getsource(
|
||||
self.image_mask.EditImageMask._modal_polyline
|
||||
)
|
||||
draw_source = inspect.getsource(
|
||||
self.image_mask.EditImageMask._draw_selection
|
||||
)
|
||||
modal_source = inspect.getsource(self.image_mask.EditImageMask._modal_polyline)
|
||||
draw_source = inspect.getsource(self.image_mask.EditImageMask._draw_selection)
|
||||
self.assertIn("return self._confirm_polyline(context)", modal_source)
|
||||
self.assertNotIn("DOUBLE_CLICK", modal_source)
|
||||
self.assertIn('event.type in {"RET", "NUMPAD_ENTER"}', modal_source)
|
||||
@@ -1671,10 +1959,8 @@ class AddonTest(unittest.TestCase):
|
||||
clicked.name: clicked,
|
||||
}
|
||||
try:
|
||||
source, should_edit = (
|
||||
self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
source, should_edit = self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
finally:
|
||||
self.fake_bpy.data.objects = previous_objects
|
||||
@@ -1718,10 +2004,8 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
self.fake_bpy.data.objects = {active.name: active, clicked.name: clicked}
|
||||
try:
|
||||
source, should_edit = (
|
||||
self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
source, should_edit = self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
finally:
|
||||
self.fake_bpy.data.objects = previous_objects
|
||||
@@ -1781,6 +2065,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
def test_image_perspective_corner_order_is_click_order_independent(self):
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
corners = ((10, 20), (80, 15), (90, 70), (5, 75))
|
||||
@@ -1794,15 +2079,9 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_perspective_detects_source_image_overlap(self):
|
||||
overlaps = self.image_perspective.perspective_quad_overlaps_image
|
||||
|
||||
self.assertFalse(
|
||||
overlaps(((20, 20), (30, 20), (30, 30), (20, 30)), (10, 10))
|
||||
)
|
||||
self.assertTrue(
|
||||
overlaps(((5, 5), (15, 5), (15, 15), (5, 15)), (10, 10))
|
||||
)
|
||||
self.assertTrue(
|
||||
overlaps(((-5, -5), (15, -5), (15, 15), (-5, 15)), (10, 10))
|
||||
)
|
||||
self.assertFalse(overlaps(((20, 20), (30, 20), (30, 30), (20, 30)), (10, 10)))
|
||||
self.assertTrue(overlaps(((5, 5), (15, 5), (15, 15), (5, 15)), (10, 10)))
|
||||
self.assertTrue(overlaps(((-5, -5), (15, -5), (15, 15), (-5, 15)), (10, 10)))
|
||||
|
||||
def test_image_perspective_builds_polyline_mask_fill_triangles(self):
|
||||
self.assertEqual(
|
||||
@@ -1825,9 +2104,7 @@ class AddonTest(unittest.TestCase):
|
||||
1.0,
|
||||
)
|
||||
|
||||
self.assertLessEqual(
|
||||
width * height, self.image_perspective.MAX_OUTPUT_PIXELS
|
||||
)
|
||||
self.assertLessEqual(width * height, self.image_perspective.MAX_OUTPUT_PIXELS)
|
||||
|
||||
def test_image_perspective_mouse_adjusts_preview_aspect_exponentially(self):
|
||||
adjust = self.image_perspective.aspect_ratio_from_mouse
|
||||
@@ -1866,9 +2143,7 @@ class AddonTest(unittest.TestCase):
|
||||
def remove(*_args, **_options):
|
||||
raise ReferenceError("StructRNA of type Image has been removed")
|
||||
|
||||
self.fake_bpy.data = SimpleNamespace(
|
||||
images=SimpleNamespace(remove=remove)
|
||||
)
|
||||
self.fake_bpy.data = SimpleNamespace(images=SimpleNamespace(remove=remove))
|
||||
operator._remove_preview()
|
||||
|
||||
self.assertIsNone(operator._preview_image)
|
||||
@@ -1961,18 +2236,12 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
endpoint = self.image_mask.snapped_line_endpoint((0, 0), (10, 1), True)
|
||||
self.assertAlmostEqual(math.degrees(math.atan2(endpoint[1], endpoint[0])), 5.0)
|
||||
preview = self.image_mask.line_half_plane_polygon(
|
||||
(0, 5), (10, 5), (10, 10)
|
||||
)
|
||||
preview = self.image_mask.line_half_plane_polygon((0, 5), (10, 5), (10, 10))
|
||||
image = self.image_mask.image_line_half_plane_polygon(
|
||||
((0, 5), (10, 5)), (10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_edit_common.polygon_bounds(preview), (0, 5, 10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_edit_common.polygon_bounds(image), (0, 0, 10, 5)
|
||||
)
|
||||
self.assertEqual(self.image_edit_common.polygon_bounds(preview), (0, 5, 10, 10))
|
||||
self.assertEqual(self.image_edit_common.polygon_bounds(image), (0, 0, 10, 5))
|
||||
|
||||
def test_balloon_interior_points_have_uniform_spacing(self):
|
||||
import numpy as np
|
||||
@@ -2087,6 +2356,64 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
self.assertGreater(wide_height, narrow_height)
|
||||
|
||||
def test_balloon_keeps_overlapping_regions_disconnected(self):
|
||||
import numpy as np
|
||||
|
||||
regions = (
|
||||
(
|
||||
((20, 0), (90, 0), (90, 70), (20, 70)),
|
||||
((50, 20), (50, 50), (70, 50), (70, 20)),
|
||||
),
|
||||
(
|
||||
((0, 20), (40, 20), (40, 45), (0, 45)),
|
||||
),
|
||||
)
|
||||
vertices, faces, uv = self.balloon_geometry.triangulate_balloon(
|
||||
regions,
|
||||
(100, 100),
|
||||
(0.0, 1.0, 0.0, 1.0),
|
||||
spacing=0.1,
|
||||
world_metric=((1.0, 0.0), (0.0, 1.0)),
|
||||
)
|
||||
centers = uv[np.asarray(faces)].mean(axis=1)
|
||||
in_hole = (
|
||||
(centers[:, 0] > 0.55)
|
||||
& (centers[:, 0] < 0.65)
|
||||
& (centers[:, 1] > 0.57)
|
||||
& (centers[:, 1] < 0.73)
|
||||
)
|
||||
in_left_region = (
|
||||
(centers[:, 0] > 0.02)
|
||||
& (centers[:, 0] < 0.12)
|
||||
& (centers[:, 1] > 0.55)
|
||||
& (centers[:, 1] < 0.8)
|
||||
)
|
||||
heights = np.asarray(vertices)[:, 2]
|
||||
|
||||
self.assertFalse(in_hole.any())
|
||||
self.assertTrue(in_left_region.any())
|
||||
self.assertGreater(float(heights.max()), 0.0)
|
||||
self.assertLess(float(heights.min()), 0.0)
|
||||
adjacency = {}
|
||||
for face in faces:
|
||||
for vertex in face:
|
||||
adjacency.setdefault(int(vertex), set())
|
||||
for start, end in zip(face, np.roll(face, -1)):
|
||||
adjacency[int(start)].add(int(end))
|
||||
adjacency[int(end)].add(int(start))
|
||||
remaining = set(adjacency)
|
||||
component_count = 0
|
||||
while remaining:
|
||||
component_count += 1
|
||||
pending = [remaining.pop()]
|
||||
while pending:
|
||||
vertex = pending.pop()
|
||||
connected = adjacency[vertex] & remaining
|
||||
remaining.difference_update(connected)
|
||||
pending.extend(connected)
|
||||
|
||||
self.assertEqual(component_count, 2)
|
||||
|
||||
def test_poisson_balloon_supports_resolution_002(self):
|
||||
import numpy as np
|
||||
|
||||
@@ -2228,19 +2555,16 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertTrue(edges)
|
||||
self.assertTrue(all(users == 2 for users in edges.values()))
|
||||
|
||||
def test_balloon_tool_operators_are_registered(self):
|
||||
self.assertIn(self.balloon.ExtractBalloon, self.addon.CLASSES)
|
||||
self.assertIn(self.balloon.ActivateBalloonTool, self.addon.CLASSES)
|
||||
def test_balloon_tool_operators_are_not_registered(self):
|
||||
self.assertNotIn(self.balloon.ExtractBalloon, self.addon.CLASSES)
|
||||
self.assertNotIn(self.balloon.ActivateBalloonTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.balloon.BalloonTool, self.addon.CLASSES)
|
||||
self.assertLess(
|
||||
self.addon.CLASSES.index(self.balloon.ExtractBalloon),
|
||||
self.addon.CLASSES.index(self.balloon.ActivateBalloonTool),
|
||||
)
|
||||
self.assertIn(self.mesh.GenerateMesh, self.addon.CLASSES)
|
||||
|
||||
def test_balloon_ai_analysis_is_registered_and_forwarded(self):
|
||||
import inspect
|
||||
|
||||
self.assertIn(self.balloon.AnalyzeBalloon, self.addon.CLASSES)
|
||||
self.assertNotIn(self.balloon.AnalyzeBalloon, self.addon.CLASSES)
|
||||
operator = self.balloon.ExtractBalloon
|
||||
self.assertIn("contour_analysis", operator.__annotations__)
|
||||
self.assertIn("depth_analysis", operator.__annotations__)
|
||||
@@ -2333,6 +2657,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
def test_balloon_depth_fields_and_shape_nodes(self):
|
||||
import inspect
|
||||
|
||||
import numpy as np
|
||||
|
||||
depth_data = {
|
||||
@@ -2379,8 +2704,12 @@ class AddonTest(unittest.TestCase):
|
||||
controller_source = inspect.getsource(
|
||||
self.balloon_depth.create_balloon_base_plane
|
||||
)
|
||||
modifier_source = inspect.getsource(
|
||||
self.balloon_depth.add_balloon_shape_modifier
|
||||
)
|
||||
self.assertIn('"Thickness"', source)
|
||||
self.assertIn('"Depth Strength"', source)
|
||||
self.assertIn('"Depth Strength": 1.0', modifier_source)
|
||||
self.assertNotIn('"Far Clamp"', source)
|
||||
self.assertIn('"Double Sided Depth"', source)
|
||||
self.assertIn('"Base Plane"', source)
|
||||
@@ -2399,7 +2728,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("o_balloon_depth_mix", source)
|
||||
self.assertIn('front.operation = "LESS_THAN"', source)
|
||||
self.assertIn('depth_residual.operation = "ADD"', source)
|
||||
self.assertIn('depth_calibration.inputs[1].default_value = 0.5', source)
|
||||
self.assertIn("depth_calibration.inputs[1].default_value = 0.5", source)
|
||||
self.assertIn('corrected_depth.operation = "ADD"', source)
|
||||
self.assertIn(
|
||||
'profile_statistics.outputs["Max"], corrected_depth.inputs[1]',
|
||||
@@ -2410,7 +2739,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn('double_choice.outputs["Output"], combine.inputs["X"]', source)
|
||||
self.assertIn('depth_mix = nodes.new("ShaderNodeMix")', source)
|
||||
self.assertIn('depth_mix.data_type = "FLOAT"', source)
|
||||
self.assertIn('depth_mix.clamp_factor = True', source)
|
||||
self.assertIn("depth_mix.clamp_factor = True", source)
|
||||
self.assertIn('base_magnitude.outputs[0], depth_mix.inputs["A"]', source)
|
||||
self.assertIn('depth_scale.outputs[0], depth_mix.inputs["B"]', source)
|
||||
self.assertNotIn("profile_inverse", source)
|
||||
@@ -2438,7 +2767,6 @@ class AddonTest(unittest.TestCase):
|
||||
"o_balloon_depth",
|
||||
)
|
||||
|
||||
|
||||
def test_balloon_redo_properties_use_mode_and_meter_resolution(self):
|
||||
import inspect
|
||||
|
||||
@@ -2595,11 +2923,11 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn("color_image = image.copy()", source)
|
||||
self.assertIn("color_image.name = color_name", source)
|
||||
self.assertIn(
|
||||
"material_name,\n color_image,\n 1.0,",
|
||||
"material_name,\n color_image,\n 0.0,",
|
||||
source,
|
||||
)
|
||||
self.assertIn(
|
||||
"else TEXTURE_MATERIAL_NODE_GROUP_NAME",
|
||||
"surface_group_name=IMAGE_MATERIAL_NODE_GROUP_NAME",
|
||||
source,
|
||||
)
|
||||
self.assertIn("owns_material", source)
|
||||
@@ -2608,14 +2936,13 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn("assign_balloon_shape_attributes(", source)
|
||||
self.assertIn("inflation=INFLATION", source)
|
||||
self.assertIn("create_balloon_base_plane(", source)
|
||||
self.assertIn(
|
||||
"add_balloon_shape_modifier(balloon, thickness, base_plane)", source
|
||||
)
|
||||
self.assertIn("BALLOON_MATERIAL_NODE_GROUP_NAME", source)
|
||||
self.assertIn("add_balloon_shape_modifier(", source)
|
||||
self.assertNotIn("depth_strength", source)
|
||||
self.assertIn("IMAGE_MATERIAL_NODE_GROUP_NAME", source)
|
||||
self.assertIn("normal_path=", source)
|
||||
self.assertEqual(
|
||||
self.image_depth.TEXTURE_MATERIAL_NODE_GROUP_NAME,
|
||||
"O Texture Layer",
|
||||
"O Image Layer",
|
||||
)
|
||||
|
||||
def test_balloon_material_reuse_uses_explicit_source_markers(self):
|
||||
@@ -2858,9 +3185,12 @@ class AddonTest(unittest.TestCase):
|
||||
},
|
||||
),
|
||||
(
|
||||
self.balloon.BalloonTool,
|
||||
self.addon.CutoutTool,
|
||||
{
|
||||
"separator": False,
|
||||
"after": {
|
||||
self.image_perspective.ImagePerspectiveTool.bl_idname
|
||||
},
|
||||
"separator": True,
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -2875,9 +3205,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.outliner_menu_draws[0],
|
||||
self.addon.draw_image_context_menu,
|
||||
)
|
||||
self.assertTrue(
|
||||
hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source")
|
||||
)
|
||||
self.assertTrue(hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"))
|
||||
|
||||
self.addon.unregister()
|
||||
self.assertEqual(self.registered, [])
|
||||
@@ -2885,9 +3213,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(self.status_bar_draws, [])
|
||||
self.assertEqual(self.context_menu_draws, [existing_context_draw])
|
||||
self.assertEqual(self.outliner_menu_draws, [existing_outliner_draw])
|
||||
self.assertFalse(
|
||||
hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source")
|
||||
)
|
||||
self.assertFalse(hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"))
|
||||
|
||||
def test_blender_image_exports_do_not_use_scene_render_color_management(self):
|
||||
source_root = PROJECT_ROOT / "src" / "anyimage"
|
||||
@@ -2922,7 +3248,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(calls[1], ("operator", "anyimage.cancel_job"))
|
||||
|
||||
def test_moge_depth_request_is_written_on_the_operator(self):
|
||||
operator = self.addon.operators.GenerateMoge2Depth()
|
||||
operator = self.depth_operator.GenerateMoge2Depth()
|
||||
operator.input_path = __file__
|
||||
operator.source_object_name = ""
|
||||
operator.model = "MOGE2_VITS_NORMAL"
|
||||
@@ -2943,7 +3269,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("job_type", parameters)
|
||||
|
||||
def test_moge_depth_response_calls_an_ordinary_function(self):
|
||||
operator = self.addon.operators.GenerateMoge2Depth()
|
||||
operator = self.depth_operator.GenerateMoge2Depth()
|
||||
operator.source_object_name = "Reference"
|
||||
operator.remove_background = False
|
||||
operator.delete_source = True
|
||||
@@ -2990,8 +3316,13 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
with patch.object(
|
||||
self.depth_operator,
|
||||
"prepare_image_input",
|
||||
return_value=(Path(__file__), False),
|
||||
"prepare_image_analysis_inputs",
|
||||
return_value=(
|
||||
Path(__file__),
|
||||
False,
|
||||
Path(__file__),
|
||||
False,
|
||||
),
|
||||
):
|
||||
self.assertEqual(operator.execute(context), {"FINISHED"})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user