feat(cutout_tool): Implement cutout region to shape functionality
- Added interaction helpers for cutout region validation and shape menu interaction. - Created main operator for converting image regions into cutout shapes, including AI artifact generation. - Introduced object creation logic for cutout shapes, handling depth and normal textures. - Defined cutout shape identifiers and node groups for various shape types. - Implemented depth texture writing and prediction artifact generation for MoGe-2. - Developed server jobs for generating cutout artifacts, depth planes, and refining image regions. - Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
@@ -84,21 +84,27 @@ class AddonTest(unittest.TestCase):
|
||||
cls.addon = importlib.import_module("anyimage")
|
||||
cls.job_runtime = importlib.import_module("anyimage.runtime")
|
||||
cls.model_operators = importlib.import_module("anyimage.operators.ai_setup")
|
||||
cls.image_depth = importlib.import_module("anyimage.operators.common.mesh_result")
|
||||
cls.image_depth = importlib.import_module(
|
||||
"anyimage.operators.common.mesh_result"
|
||||
)
|
||||
cls.image_data = importlib.import_module("anyimage.operators.common.image_data")
|
||||
cls.object_data = importlib.import_module("anyimage.operators.common.mesh_result")
|
||||
cls.object_data = importlib.import_module(
|
||||
"anyimage.operators.common.mesh_result"
|
||||
)
|
||||
cls.debug = importlib.import_module("anyimage.operators.debug")
|
||||
cls.mesh_plane = importlib.import_module("anyimage.operators.convert_to_plane.mesh_plane")
|
||||
cls.mesh = importlib.import_module("anyimage.operators.convert_to_plane.conversion")
|
||||
cls.mesh_plane = importlib.import_module(
|
||||
"anyimage.operators.convert_to_plane.mesh_plane"
|
||||
)
|
||||
cls.mesh = importlib.import_module(
|
||||
"anyimage.operators.convert_to_plane.conversion"
|
||||
)
|
||||
cls.cutout_conversion = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.region_to_shape"
|
||||
"anyimage.operators.cutout_tool.main"
|
||||
)
|
||||
cls.cutout_build = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.region_to_shape"
|
||||
)
|
||||
cls.mesh_shapes = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.region_to_shape"
|
||||
"anyimage.operators.cutout_tool.main"
|
||||
)
|
||||
cls.mesh_shapes = importlib.import_module("anyimage.operators.cutout_tool.main")
|
||||
cls.image_background = importlib.import_module(
|
||||
"anyimage.operators.remove_background"
|
||||
)
|
||||
@@ -116,9 +122,7 @@ class AddonTest(unittest.TestCase):
|
||||
"anyimage.operators.common.image_interaction"
|
||||
)
|
||||
cls.image_box = importlib.import_module("anyimage.operators.image_tool.box")
|
||||
cls.image_lasso = importlib.import_module(
|
||||
"anyimage.operators.image_tool.lasso"
|
||||
)
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.image_tool.lasso")
|
||||
cls.image_line = importlib.import_module("anyimage.operators.image_tool.line")
|
||||
cls.image_polyline = importlib.import_module(
|
||||
"anyimage.operators.image_tool.polyline"
|
||||
@@ -127,10 +131,10 @@ class AddonTest(unittest.TestCase):
|
||||
"anyimage.operators.perspective_tool.edit_perspective"
|
||||
)
|
||||
cls.cutout_geometry = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.geometry.triangulation"
|
||||
"anyimage.operators.cutout_tool.geometry"
|
||||
)
|
||||
cls.cutout_depth = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.geometry.depth_calibration"
|
||||
"anyimage.operators.cutout_tool.geometry"
|
||||
)
|
||||
cls.image_interaction = importlib.import_module(
|
||||
"anyimage.operators.common.image_interaction"
|
||||
@@ -525,7 +529,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("stage", controller.post_status)
|
||||
self.assertNotIn("stages", controller.post_status)
|
||||
|
||||
def test_conversion_model_lists_moge2_before_da3(self):
|
||||
def test_production_geometry_models_exclude_da3(self):
|
||||
properties = importlib.import_module("anyimage.properties")
|
||||
catalog = (
|
||||
{
|
||||
@@ -571,14 +575,12 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
with patch.object(properties, "model_catalog", return_value=catalog):
|
||||
depth_items = properties.depth_plane_model_enum_items(None, None)
|
||||
mesh_depth_items = properties.mesh_depth_model_enum_items(None, None)
|
||||
mesh_depth_items = properties.geometry_model_enum_items(None, None)
|
||||
|
||||
expected = (
|
||||
"MOGE2_VITS_NORMAL",
|
||||
"MOGE2_VITB_NORMAL",
|
||||
"MOGE2_VITL_NORMAL",
|
||||
"SMALL",
|
||||
"BASE",
|
||||
)
|
||||
self.assertEqual(tuple(item[0] for item in depth_items[:-1]), expected)
|
||||
self.assertEqual(tuple(item[0] for item in mesh_depth_items), expected)
|
||||
@@ -687,11 +689,10 @@ class AddonTest(unittest.TestCase):
|
||||
self.model_operators.OpenAIEnvironmentSettings,
|
||||
self.addon.CLASSES,
|
||||
)
|
||||
|
||||
def test_global_ai_input_size_defaults_and_clamps(self):
|
||||
preference_module = self.addon.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons[
|
||||
"anyimage"
|
||||
].preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
@@ -708,6 +709,25 @@ class AddonTest(unittest.TestCase):
|
||||
256,
|
||||
)
|
||||
|
||||
def test_cutout_mesh_resolution_defaults_and_clamps_independently(self):
|
||||
preference_module = self.addon.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 2048
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 1
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
4,
|
||||
)
|
||||
|
||||
def test_preferences_draws_upscale_model_download(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
downloads = []
|
||||
@@ -742,7 +762,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
preferences.mesh_depth_model = "BASE"
|
||||
preferences.geometry_model = "MOGE2_VITS_NORMAL"
|
||||
preferences.upscale_model = "SAFMN_REAL_X4"
|
||||
catalog = tuple(
|
||||
{
|
||||
@@ -781,7 +801,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
[download.model for download in downloads],
|
||||
["BASE", "BEN2_BASE", "SAFMN_REAL_X4"],
|
||||
["MOGE2_VITS_NORMAL", "BEN2_BASE", "SAFMN_REAL_X4"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
@@ -815,10 +835,10 @@ class AddonTest(unittest.TestCase):
|
||||
("anyimage_preferences_cutout", False),
|
||||
],
|
||||
)
|
||||
self.assertIn("DA3 models cannot generate normals", labels)
|
||||
self.assertNotIn("Normal Model", labels)
|
||||
self.assertIn("upscale_model", properties)
|
||||
self.assertIn("max_ai_input_size", properties)
|
||||
self.assertIn("max_cutout_mesh_resolution", properties)
|
||||
self.assertIn("balloon_profile_algorithm", properties)
|
||||
balloon_profile = preference_module.AnyImagePreferences.__annotations__[
|
||||
"balloon_profile_algorithm"
|
||||
@@ -887,8 +907,8 @@ class AddonTest(unittest.TestCase):
|
||||
return_value=(1, 0, 24, True),
|
||||
),
|
||||
patch.object(self.mesh, "create_image_material", return_value="Material"),
|
||||
patch.object(self.mesh, "copy_color_image", return_value="Color"),
|
||||
patch.object(self.mesh, "build_mesh_plane") as build_mesh,
|
||||
patch.object(self.mesh, "create_color_image", return_value="Color"),
|
||||
patch.object(self.mesh, "create_plane_object") as build_mesh,
|
||||
):
|
||||
result = self.mesh.ConvertToPlane.execute(operator, context)
|
||||
|
||||
@@ -905,9 +925,7 @@ class AddonTest(unittest.TestCase):
|
||||
operator.resolution_level = 5
|
||||
operator.max_input_size = 1536
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(
|
||||
anyimage_settings=SimpleNamespace(video_frames=1)
|
||||
)
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1))
|
||||
)
|
||||
with (
|
||||
patch.object(self.mesh, "require_environment"),
|
||||
@@ -924,8 +942,12 @@ class AddonTest(unittest.TestCase):
|
||||
def test_surface_and_balloon_without_ai_build_locally(self):
|
||||
import numpy as np
|
||||
|
||||
source = SimpleNamespace(data=SimpleNamespace(size=(10, 10)), select_set=lambda _value: None)
|
||||
context = SimpleNamespace(view_layer=SimpleNamespace(objects=SimpleNamespace(active=None)))
|
||||
source = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(10, 10)), select_set=lambda _value: None
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None))
|
||||
)
|
||||
region_mask = self.cutout_conversion.RegionMask(
|
||||
np.ones((8, 8), dtype=np.float32), (1, 1, 9, 9)
|
||||
)
|
||||
@@ -938,25 +960,46 @@ class AddonTest(unittest.TestCase):
|
||||
{"points": [[1, 1], [9, 1], [9, 9], [1, 9]]}
|
||||
),
|
||||
shape=shape,
|
||||
mesh_detail=64,
|
||||
normal_mode="NONE",
|
||||
refine_selection=False,
|
||||
mesh_resolution=64,
|
||||
generate_normal=False,
|
||||
refine_region=False,
|
||||
report=lambda *_args: None,
|
||||
_needs_generation=lambda: False,
|
||||
)
|
||||
with (
|
||||
patch.object(self.cutout_conversion, "require_image_empty", return_value=source),
|
||||
patch.object(self.cutout_conversion, "video_total_frames", return_value=1),
|
||||
patch.object(self.cutout_conversion, "image_rgba", return_value=source_rgba),
|
||||
patch.object(
|
||||
self.cutout_conversion,
|
||||
"require_image_empty",
|
||||
return_value=source,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_conversion, "video_total_frames", return_value=1
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_conversion, "image_rgba", return_value=source_rgba
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_conversion,
|
||||
"rasterize_region_path",
|
||||
return_value=region_mask,
|
||||
),
|
||||
patch.object(self.cutout_conversion, "configured_max_ai_input_size", return_value=2048),
|
||||
patch.object(self.cutout_conversion, "region_to_shape") as convert,
|
||||
patch.object(
|
||||
self.cutout_conversion,
|
||||
"configured_max_ai_input_size",
|
||||
return_value=2048,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_conversion,
|
||||
"configured_max_cutout_mesh_resolution",
|
||||
return_value=1024,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_conversion, "create_cutout_shape"
|
||||
) as convert,
|
||||
):
|
||||
result = self.cutout_conversion.CutoutRegionToShape.execute(operator, context)
|
||||
result = self.cutout_conversion.CutoutRegionToShape.execute(
|
||||
operator, context
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
convert.assert_called_once_with(
|
||||
@@ -975,16 +1018,24 @@ class AddonTest(unittest.TestCase):
|
||||
operator.shape = "DEPTH_SURFACE"
|
||||
operator.model = self.addon.properties.DEFAULT_MOGE2_MODEL_KEY
|
||||
operator.resolution_level = 5
|
||||
operator.max_input_size = 2048
|
||||
operator.normal_mode = "OBJECT"
|
||||
operator.refine_selection = True
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=SimpleNamespace()))
|
||||
operator.max_ai_input_size = 2048
|
||||
operator.generate_normal = True
|
||||
operator.refine_region = True
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
|
||||
)
|
||||
with (
|
||||
patch.object(self.cutout_conversion, "require_environment"),
|
||||
patch.object(self.cutout_conversion, "require_model") as require_model,
|
||||
patch.object(self.cutout_conversion, "production_device", return_value="CPU"),
|
||||
patch.object(
|
||||
self.cutout_conversion, "production_device", return_value="CPU"
|
||||
),
|
||||
patch.object(self.cutout_conversion, "source_start_frame", return_value=1),
|
||||
patch.object(self.cutout_conversion, "moge2_parameters", return_value={"model": operator.model, "resolution_level": 5}),
|
||||
patch.object(
|
||||
self.cutout_conversion,
|
||||
"moge2_parameters",
|
||||
return_value={"model": operator.model, "resolution_level": 5},
|
||||
),
|
||||
):
|
||||
parameters = operator.request(context)
|
||||
|
||||
@@ -993,27 +1044,27 @@ class AddonTest(unittest.TestCase):
|
||||
[self.addon.properties.BEN2_MODEL_KEY, operator.model],
|
||||
)
|
||||
self.assertTrue(parameters["refine_region"])
|
||||
self.assertTrue(parameters["output_depth"])
|
||||
self.assertEqual(parameters["depth_mode"], "VECTOR")
|
||||
self.assertEqual(parameters["normal_mode"], "OBJECT")
|
||||
self.assertNotIn("polygon", parameters)
|
||||
self.assertNotIn("shape", parameters)
|
||||
self.assertNotIn("normal_analysis", parameters)
|
||||
|
||||
def test_cutout_moge_settings_falls_back_from_da3(self):
|
||||
def test_cutout_geometry_settings_use_configured_moge(self):
|
||||
preferences = SimpleNamespace(
|
||||
mesh_depth_model="DA3_BASE",
|
||||
mesh_resolution_level=7,
|
||||
geometry_model="MOGE2_VITB_NORMAL",
|
||||
geometry_resolution_level=7,
|
||||
)
|
||||
with patch.object(
|
||||
self.cutout_conversion,
|
||||
"addon_preferences",
|
||||
return_value=preferences,
|
||||
):
|
||||
settings = self.cutout_conversion.cutout_moge_settings(None)
|
||||
settings = self.cutout_conversion.cutout_geometry_settings(None)
|
||||
|
||||
self.assertEqual(
|
||||
settings,
|
||||
(self.addon.properties.DEFAULT_MOGE2_MODEL_KEY, 7),
|
||||
("MOGE2_VITB_NORMAL", 7),
|
||||
)
|
||||
|
||||
def test_image_menu_keeps_ai_features_clickable_and_offers_models(self):
|
||||
@@ -1321,9 +1372,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertTrue(nonzero[3, 5])
|
||||
|
||||
def preview_area(fill_rule):
|
||||
triangles = self.image_edit_common.scanline_fill_triangles(
|
||||
path, fill_rule
|
||||
)
|
||||
triangles = self.image_edit_common.scanline_fill_triangles(path, fill_rule)
|
||||
return sum(
|
||||
abs(
|
||||
self.image_edit_common._triangle_cross(
|
||||
@@ -1336,8 +1385,8 @@ class AddonTest(unittest.TestCase):
|
||||
for index in range(0, len(triangles), 3)
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(preview_area('EVEN_ODD'), 36.0)
|
||||
self.assertAlmostEqual(preview_area('NONZERO'), 54.0)
|
||||
self.assertAlmostEqual(preview_area("EVEN_ODD"), 36.0)
|
||||
self.assertAlmostEqual(preview_area("NONZERO"), 54.0)
|
||||
|
||||
def test_expected_empty_image_edits_are_reported_as_warnings(self):
|
||||
reports = []
|
||||
@@ -1370,7 +1419,7 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_edit_tools_and_operators_are_registered(self):
|
||||
operator = self.image_mask.EditImageMask
|
||||
self.assertIn(operator, self.addon.CLASSES)
|
||||
self.assertIn(self.image_mask.RefineImageMask, self.addon.CLASSES)
|
||||
self.assertIn(self.image_mask.RefineImageRegion, self.addon.CLASSES)
|
||||
self.assertIn(self.image_mask.ActivateImageTool, self.addon.CLASSES)
|
||||
tool_cases = (
|
||||
(self.image_box.ImageBoxTool, "BOX", "ops.sculpt.border_mask"),
|
||||
@@ -1391,24 +1440,36 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn("draw_settings", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_lasso.ImageLassoTool.__dict__)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=object()))
|
||||
layout = SimpleNamespace(prop=lambda *_args: None)
|
||||
drawn_settings = []
|
||||
layout = SimpleNamespace(
|
||||
prop=lambda _settings, name: drawn_settings.append(name)
|
||||
)
|
||||
with patch.object(self.image_box, "draw_region_settings") as draw:
|
||||
self.image_box.ImageBoxTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(
|
||||
context, layout, "image_box_refine_selection"
|
||||
)
|
||||
draw.assert_called_once_with(context, layout, "image_box_refine_region")
|
||||
with patch.object(self.image_lasso, "draw_region_settings") as draw:
|
||||
self.image_lasso.ImageLassoTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(
|
||||
context, layout, "image_lasso_refine_selection"
|
||||
)
|
||||
draw.assert_called_once_with(context, layout, "image_lasso_refine_region")
|
||||
cutout_tool = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool"
|
||||
).CutoutTool
|
||||
cutout_module = sys.modules[cutout_tool.__module__]
|
||||
with patch.object(cutout_module, "draw_region_settings") as draw:
|
||||
drawn_settings.clear()
|
||||
with patch.object(
|
||||
cutout_module,
|
||||
"draw_region_settings",
|
||||
side_effect=lambda _context, _layout, name: drawn_settings.append(name),
|
||||
) as draw:
|
||||
cutout_tool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(context, layout, "cutout_refine_selection")
|
||||
draw.assert_called_once_with(context, layout, "cutout_refine_region")
|
||||
self.assertEqual(
|
||||
drawn_settings,
|
||||
[
|
||||
"cutout_refine_region",
|
||||
"cutout_point_spacing",
|
||||
"cutout_generate_normal",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("draw_settings", self.image_line.ImageLineTool.__dict__)
|
||||
self.assertNotIn(
|
||||
"draw_settings", self.image_polyline.ImagePolylineTool.__dict__
|
||||
@@ -1419,51 +1480,69 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
settings = self.addon.properties.AnyImageSettings.__annotations__
|
||||
for name in (
|
||||
"cutout_refine_selection",
|
||||
"image_box_refine_selection",
|
||||
"image_lasso_refine_selection",
|
||||
"cutout_refine_region",
|
||||
"image_box_refine_region",
|
||||
"image_lasso_refine_region",
|
||||
):
|
||||
self.assertFalse(settings[name]["default"])
|
||||
self.assertNotIn("refine_selection", settings)
|
||||
self.assertNotIn("image_edit_refine_selection", settings)
|
||||
self.assertNotIn("refine_region", settings)
|
||||
self.assertNotIn("image_edit_refine_region", settings)
|
||||
self.assertEqual(settings["cutout_point_spacing"]["default"], 32)
|
||||
self.assertEqual(settings["cutout_point_spacing"]["min"], 4)
|
||||
self.assertEqual(settings["cutout_point_spacing"]["max"], 32)
|
||||
self.assertEqual(settings["cutout_normal_mode"]["default"], "NONE")
|
||||
self.assertFalse(settings["cutout_generate_normal"]["default"])
|
||||
self.assertEqual(settings["cutout_refine_region"]["name"], "Fit to Foreground")
|
||||
self.assertEqual(
|
||||
settings["cutout_point_spacing"]["name"], "Mesh Sampling Spacing"
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["cutout_generate_normal"]["name"],
|
||||
"Generate Normal Map",
|
||||
)
|
||||
self.assertNotIn("image_extract_continuous", settings)
|
||||
self.assertNotIn("image_extract_padding", settings)
|
||||
self.assertNotIn("image_extract_remove_background", settings)
|
||||
def test_refine_selection_is_independent_for_each_area_tool(self):
|
||||
|
||||
def test_refine_region_is_independent_for_each_area_tool(self):
|
||||
settings = SimpleNamespace(
|
||||
cutout_refine_selection=True,
|
||||
image_box_refine_selection=False,
|
||||
image_lasso_refine_selection=True,
|
||||
cutout_refine_region=True,
|
||||
image_box_refine_region=False,
|
||||
image_lasso_refine_region=True,
|
||||
)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
||||
with patch.object(self.image_edit_common, "ai_ready", return_value=True):
|
||||
for gesture in ("LINE", "POLYLINE"):
|
||||
operator = SimpleNamespace(
|
||||
gesture=gesture,
|
||||
refine_selection=True,
|
||||
refine_region=True,
|
||||
)
|
||||
self.image_edit_common.apply_region_settings(operator, context)
|
||||
self.assertFalse(operator.refine_selection)
|
||||
self.assertFalse(operator.refine_region)
|
||||
|
||||
box = SimpleNamespace(gesture="BOX", refine_selection=True)
|
||||
box = SimpleNamespace(
|
||||
gesture="BOX",
|
||||
refine_region=True,
|
||||
refine_region_settings={"BOX": "image_box_refine_region"},
|
||||
)
|
||||
self.image_edit_common.apply_region_settings(box, context)
|
||||
self.assertFalse(box.refine_selection)
|
||||
self.assertFalse(box.refine_region)
|
||||
|
||||
lasso = SimpleNamespace(gesture="LASSO", refine_selection=False)
|
||||
lasso = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
refine_region=False,
|
||||
refine_region_settings={"LASSO": "image_lasso_refine_region"},
|
||||
)
|
||||
self.image_edit_common.apply_region_settings(lasso, context)
|
||||
self.assertTrue(lasso.refine_selection)
|
||||
self.assertTrue(lasso.refine_region)
|
||||
|
||||
cutout = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
active_tool_id="anyimage.cutout_lasso",
|
||||
refine_selection=False,
|
||||
refine_region=False,
|
||||
refine_region_settings={"LASSO": "cutout_refine_region"},
|
||||
)
|
||||
self.image_edit_common.apply_region_settings(cutout, context)
|
||||
self.assertTrue(cutout.refine_selection)
|
||||
self.assertTrue(cutout.refine_region)
|
||||
|
||||
def test_image_edit_first_click_selects_another_image_without_editing(self):
|
||||
selected = []
|
||||
@@ -1652,14 +1731,14 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(area, 10.0)
|
||||
|
||||
def test_image_mask_fill_uses_blender_geometry_without_stopping_the_draw(self):
|
||||
mathutils = ModuleType('mathutils')
|
||||
mathutils = ModuleType("mathutils")
|
||||
mathutils.Vector = lambda values: values
|
||||
mathutils.geometry = SimpleNamespace(
|
||||
tessellate_polygon=lambda _loops: (
|
||||
((0.0, 0.0, 0.0), (2.0, 0.0, 0.0), (0.0, 2.0, 0.0)),
|
||||
)
|
||||
)
|
||||
with patch.dict(sys.modules, {'mathutils': mathutils}):
|
||||
with patch.dict(sys.modules, {"mathutils": mathutils}):
|
||||
triangles = self.image_edit_common.triangulated_polygon_vertices(
|
||||
((0, 0), (2, 0), (0, 2))
|
||||
)
|
||||
@@ -1670,12 +1749,12 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_image_mask_fill_falls_back_when_blender_tessellation_rejects_path(self):
|
||||
mathutils = ModuleType('mathutils')
|
||||
mathutils = ModuleType("mathutils")
|
||||
mathutils.Vector = lambda values: values
|
||||
mathutils.geometry = SimpleNamespace(
|
||||
tessellate_polygon=lambda _loops: (_ for _ in ()).throw(TypeError())
|
||||
)
|
||||
with patch.dict(sys.modules, {'mathutils': mathutils}):
|
||||
with patch.dict(sys.modules, {"mathutils": mathutils}):
|
||||
triangles = self.image_edit_common.triangulated_polygon_vertices(
|
||||
((0, 0), (2, 0), (0, 2))
|
||||
)
|
||||
@@ -1684,9 +1763,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
def test_image_mask_preview_fills_a_self_intersecting_lasso(self):
|
||||
polygon = ((0, 0), (4, 4), (0, 4), (4, 0))
|
||||
triangles = self.image_edit_common.preview_fill_triangles(
|
||||
polygon, 'NONZERO'
|
||||
)
|
||||
triangles = self.image_edit_common.preview_fill_triangles(polygon, "NONZERO")
|
||||
area = sum(
|
||||
abs(
|
||||
self.image_edit_common._triangle_cross(
|
||||
@@ -1788,9 +1865,10 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertNotIn("image_perspective_aspect_ratio", settings)
|
||||
self.assertNotIn("aspect_mode", operator.__annotations__)
|
||||
self.assertIn("aspect_ratio", operator.__annotations__)
|
||||
self.assertNotIn("refine_selection", operator.__annotations__)
|
||||
self.assertNotIn("refine_region", operator.__annotations__)
|
||||
self.assertNotIn("continuous", operator.__annotations__)
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
|
||||
def test_image_box_builds_a_rectangular_screen_path(self):
|
||||
self.assertEqual(
|
||||
self.image_gesture.box_screen_path((2, 3), (8, 9)),
|
||||
@@ -1830,9 +1908,7 @@ class AddonTest(unittest.TestCase):
|
||||
return_value=polygon,
|
||||
),
|
||||
):
|
||||
result = self.image_gesture.ImageRegionGesture._complete(
|
||||
operator, context
|
||||
)
|
||||
result = self.image_gesture.ImageRegionGesture._complete(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
self.assertEqual(
|
||||
@@ -1854,12 +1930,12 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_lasso_preview_uses_fill_and_a_closed_dashed_outline(self):
|
||||
polygon = ((1.0, 2.0), (8.0, 3.0), (4.0, 9.0))
|
||||
operator = SimpleNamespace(
|
||||
gesture='LASSO',
|
||||
gesture="LASSO",
|
||||
invert=False,
|
||||
_path=[polygon[0]],
|
||||
_area_pointer=1,
|
||||
_region_pointer=2,
|
||||
preview_fill_rule='NONZERO',
|
||||
preview_fill_rule="NONZERO",
|
||||
_fill_polygon=None,
|
||||
_fill_triangles=(),
|
||||
_outline_polygon=None,
|
||||
@@ -1867,33 +1943,33 @@ class AddonTest(unittest.TestCase):
|
||||
_screen_path=lambda: polygon,
|
||||
)
|
||||
with (
|
||||
patch.object(self.image_gesture, 'drawing_in_region', return_value=True),
|
||||
patch.object(self.image_gesture, "drawing_in_region", return_value=True),
|
||||
patch.object(
|
||||
self.image_gesture,
|
||||
'dashed_line_segments',
|
||||
return_value=('segments',),
|
||||
"dashed_line_segments",
|
||||
return_value=("segments",),
|
||||
) as build_outline,
|
||||
patch.object(
|
||||
self.image_gesture,
|
||||
'preview_fill_triangles',
|
||||
return_value=('triangles',),
|
||||
"preview_fill_triangles",
|
||||
return_value=("triangles",),
|
||||
) as build_fill,
|
||||
patch.object(self.image_gesture, 'draw_polygon_fill') as draw_fill,
|
||||
patch.object(self.image_gesture, 'draw_dashed_line') as draw_outline,
|
||||
patch.object(self.image_gesture, "draw_polygon_fill") as draw_fill,
|
||||
patch.object(self.image_gesture, "draw_dashed_line") as draw_outline,
|
||||
):
|
||||
self.image_gesture.ImageRegionGesture._draw_region(operator)
|
||||
self.image_gesture.ImageRegionGesture._draw_region(operator)
|
||||
|
||||
build_fill.assert_called_once_with(polygon, 'NONZERO')
|
||||
build_fill.assert_called_once_with(polygon, "NONZERO")
|
||||
build_outline.assert_called_once_with((*polygon, polygon[0]))
|
||||
self.assertEqual(draw_fill.call_count, 2)
|
||||
draw_fill.assert_called_with(
|
||||
polygon,
|
||||
('triangles',),
|
||||
("triangles",),
|
||||
(1.0, 1.0, 1.0, 0.16),
|
||||
)
|
||||
self.assertEqual(draw_outline.call_count, 2)
|
||||
draw_outline.assert_called_with((), segments=('segments',))
|
||||
draw_outline.assert_called_with((), segments=("segments",))
|
||||
|
||||
def test_image_lasso_does_not_add_stationary_or_nearly_collinear_points(self):
|
||||
path = [(0.0, 0.0)]
|
||||
@@ -1912,8 +1988,7 @@ class AddonTest(unittest.TestCase):
|
||||
previous_ops = getattr(self.fake_bpy, "ops", None)
|
||||
self.fake_bpy.ops = SimpleNamespace(
|
||||
wm=SimpleNamespace(
|
||||
tool_set_by_id=lambda **options: calls.append(options)
|
||||
or {"FINISHED"}
|
||||
tool_set_by_id=lambda **options: calls.append(options) or {"FINISHED"}
|
||||
)
|
||||
)
|
||||
try:
|
||||
@@ -1935,9 +2010,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
endpoint = self.image_gesture.snapped_line_endpoint((0, 0), (10, 1), True)
|
||||
self.assertAlmostEqual(math.degrees(math.atan2(endpoint[1], endpoint[0])), 5.0)
|
||||
preview = self.image_gesture.line_half_plane_polygon(
|
||||
(0, 5), (10, 5), (10, 10)
|
||||
)
|
||||
preview = self.image_gesture.line_half_plane_polygon((0, 5), (10, 5), (10, 10))
|
||||
image = self.image_gesture.image_line_half_plane_polygon(
|
||||
((0, 5), (10, 5)), (10, 10)
|
||||
)
|
||||
@@ -2191,5 +2264,6 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(calls[0][1]["text"], "Working")
|
||||
self.assertEqual(calls[1], ("operator", "anyimage.cancel_job"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import MethodType, SimpleNamespace
|
||||
@@ -9,8 +8,9 @@ SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators.common.image_interaction import ImageRegionGesture
|
||||
from anyimage.operators.cutout_tool import select_region as cutout
|
||||
from anyimage.operators.common.image_region import RegionPath
|
||||
from anyimage.operators.cutout_tool import interaction
|
||||
from anyimage.operators.cutout_tool import main as cutout
|
||||
|
||||
|
||||
class Pie:
|
||||
@@ -32,10 +32,10 @@ class WindowManager:
|
||||
draw(menu, None)
|
||||
|
||||
|
||||
def cutout_context(window_manager, point_spacing=32, normal_mode="NONE"):
|
||||
def cutout_context(window_manager, point_spacing=32, generate_normal=False):
|
||||
settings = SimpleNamespace(
|
||||
cutout_point_spacing=point_spacing,
|
||||
cutout_normal_mode=normal_mode,
|
||||
cutout_generate_normal=generate_normal,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
window_manager=window_manager,
|
||||
@@ -52,7 +52,7 @@ def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
|
||||
context = cutout_context(window_manager)
|
||||
source = cutout_source()
|
||||
|
||||
cutout._open_shape_pie(
|
||||
interaction.open_shape_pie(
|
||||
context,
|
||||
object(),
|
||||
source,
|
||||
@@ -75,19 +75,17 @@ def test_cutout_pie_uses_four_semantic_shapes_in_cardinal_order():
|
||||
call[0] == "anyimage.cutout_region_to_shape"
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert not any(call[2].refine_selection for call in window_manager.pie.calls)
|
||||
assert all(call[2].normal_mode == "NONE" for call in window_manager.pie.calls)
|
||||
assert all(call[2].mesh_detail == 64 for call in window_manager.pie.calls)
|
||||
assert not any(call[2].refine_region for call in window_manager.pie.calls)
|
||||
assert not any(call[2].generate_normal for call in window_manager.pie.calls)
|
||||
assert all(call[2].mesh_resolution == 64 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_pie_passes_image_lasso_options_to_every_shape():
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(
|
||||
window_manager, point_spacing=16, normal_mode="OBJECT"
|
||||
)
|
||||
context = cutout_context(window_manager, point_spacing=16, generate_normal=True)
|
||||
source = cutout_source(size=(4096, 2048))
|
||||
|
||||
cutout._open_shape_pie(
|
||||
interaction.open_shape_pie(
|
||||
context,
|
||||
object(),
|
||||
source,
|
||||
@@ -95,22 +93,30 @@ def test_cutout_pie_passes_image_lasso_options_to_every_shape():
|
||||
points=((0, 0), (10, 0), (0, 10)),
|
||||
invert=True,
|
||||
),
|
||||
refine_selection=True,
|
||||
refine_region=True,
|
||||
)
|
||||
|
||||
assert all(call[2].refine_selection for call in window_manager.pie.calls)
|
||||
assert all(call[2].normal_mode == "OBJECT" for call in window_manager.pie.calls)
|
||||
assert all(call[2].refine_region for call in window_manager.pie.calls)
|
||||
assert all(call[2].generate_normal for call in window_manager.pie.calls)
|
||||
assert all(
|
||||
RegionPath.from_json(call[2].region_path_json).invert
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert all(call[2].mesh_detail == 256 for call in window_manager.pie.calls)
|
||||
assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_normal_space_is_derived_from_shape():
|
||||
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", False) == "NONE"
|
||||
assert cutout.normal_mode_for_shape("SURFACE", True) == "TANGENT"
|
||||
assert cutout.normal_mode_for_shape("BALLOON", True) == "TANGENT"
|
||||
assert cutout.normal_mode_for_shape("DEPTH_BALLOON", True) == "TANGENT"
|
||||
assert cutout.normal_mode_for_shape("DEPTH_SURFACE", True) == "OBJECT"
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
|
||||
assert cutout.cutout_mesh_detail((4096, 2048), 32) == 128
|
||||
assert cutout.cutout_mesh_detail((16, 8), 4) == 4
|
||||
assert cutout.cutout_mesh_detail((65536, 32768), 4) == 1024
|
||||
assert interaction.cutout_mesh_resolution((4096, 2048), 32) == 128
|
||||
assert interaction.cutout_mesh_resolution((16, 8), 4) == 4
|
||||
assert interaction.cutout_mesh_resolution((65536, 32768), 4) == 1024
|
||||
|
||||
|
||||
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
|
||||
@@ -128,11 +134,11 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
)
|
||||
monkeypatch.setattr(cutout, "video_total_frames", lambda _image: 1)
|
||||
monkeypatch.setattr(cutout, "apply_region_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout, "_validate_cutout_region", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout, "validate_cutout_region", lambda *_args: None)
|
||||
|
||||
operator = SimpleNamespace(
|
||||
full_image=True,
|
||||
refine_selection=False,
|
||||
refine_region=False,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
operator.execute = MethodType(cutout.SelectCutoutRegion.execute, operator)
|
||||
@@ -142,11 +148,9 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
|
||||
assert issubclass(cutout.SelectCutoutRegion, ImageRegionGesture)
|
||||
assert result == {"FINISHED"}
|
||||
region_path = RegionPath.from_json(
|
||||
window_manager.pie.calls[0][2].region_path_json
|
||||
)
|
||||
region_path = RegionPath.from_json(window_manager.pie.calls[0][2].region_path_json)
|
||||
assert region_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
|
||||
assert window_manager.pie.calls[0][2].mesh_detail == 10
|
||||
assert window_manager.pie.calls[0][2].mesh_resolution == 10
|
||||
assert not hasattr(cutout, "pending_selection")
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@ from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@@ -44,15 +41,6 @@ def load_image_modules():
|
||||
/ "common"
|
||||
/ "mesh_result.py",
|
||||
),
|
||||
(
|
||||
"anyimage.operators.convert_to_plane.resolution",
|
||||
PROJECT_ROOT
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "operators"
|
||||
/ "convert_to_plane"
|
||||
/ "resolution.py",
|
||||
),
|
||||
)
|
||||
packages = {
|
||||
name: ModuleType(name)
|
||||
@@ -85,6 +73,7 @@ def load_image_modules():
|
||||
setattr(namespace, name, value)
|
||||
return namespace
|
||||
|
||||
|
||||
class ImageTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -95,6 +84,7 @@ class ImageTest(unittest.TestCase):
|
||||
self.image_depth.node_asset_path(),
|
||||
PROJECT_ROOT / "src" / "anyimage" / "assets" / "O_AnyImage.blend",
|
||||
)
|
||||
|
||||
def test_material_color_space_matches_scene_view_transform(self):
|
||||
mappings = {
|
||||
"ACES 1.3": "ACES 1.3 sRGB",
|
||||
@@ -143,19 +133,13 @@ class ImageTest(unittest.TestCase):
|
||||
|
||||
def test_is_animated_image_accepts_movie_and_sequence(self):
|
||||
self.assertTrue(
|
||||
self.image_depth.is_animated_image(
|
||||
SimpleNamespace(source="MOVIE")
|
||||
)
|
||||
self.image_depth.is_animated_image(SimpleNamespace(source="MOVIE"))
|
||||
)
|
||||
self.assertTrue(
|
||||
self.image_depth.is_animated_image(
|
||||
SimpleNamespace(source="SEQUENCE")
|
||||
)
|
||||
self.image_depth.is_animated_image(SimpleNamespace(source="SEQUENCE"))
|
||||
)
|
||||
self.assertFalse(
|
||||
self.image_depth.is_animated_image(
|
||||
SimpleNamespace(source="FILE")
|
||||
)
|
||||
self.image_depth.is_animated_image(SimpleNamespace(source="FILE"))
|
||||
)
|
||||
|
||||
def test_video_total_frames_reads_movie_duration(self):
|
||||
@@ -327,9 +311,7 @@ class ImageTest(unittest.TestCase):
|
||||
|
||||
def test_image_user_settings_defaults_without_user(self):
|
||||
self.assertEqual(
|
||||
self.image_depth.image_user_settings(
|
||||
SimpleNamespace(image_user=None)
|
||||
),
|
||||
self.image_depth.image_user_settings(SimpleNamespace(image_user=None)),
|
||||
(1, 0, 0, False),
|
||||
)
|
||||
|
||||
@@ -411,31 +393,6 @@ class ImageTest(unittest.TestCase):
|
||||
["frame-1.jpg", "frame-2.jpg", "frame-10.jpg"],
|
||||
)
|
||||
|
||||
def test_process_resolution_scales_long_edge_and_aligns_to_patch_size(self):
|
||||
image = SimpleNamespace(size=(1080, 1920))
|
||||
|
||||
self.assertEqual(
|
||||
self.image_depth.scaled_process_resolution(image, 25.0),
|
||||
476,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_depth.scaled_process_resolution(image, 50.0),
|
||||
966,
|
||||
)
|
||||
|
||||
def test_process_resolution_clamps_to_supported_range(self):
|
||||
large_image = SimpleNamespace(size=(4000, 3000))
|
||||
small_image = SimpleNamespace(size=(200, 100))
|
||||
|
||||
self.assertEqual(
|
||||
self.image_depth.scaled_process_resolution(large_image, 100.0),
|
||||
1008,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_depth.scaled_process_resolution(small_image, 1.0),
|
||||
252,
|
||||
)
|
||||
|
||||
def test_depth_data_uses_source_image_name(self):
|
||||
source_object = SimpleNamespace(
|
||||
name="obj_img",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import builtins
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -9,53 +10,53 @@ import numpy as np
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators.cutout_tool import region_to_shape as mesh
|
||||
from anyimage.operators.cutout_tool import region_to_shape as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import region_to_shape as shapes
|
||||
from anyimage.operators.cutout_tool.geometry import balloon as cutout_profile
|
||||
from anyimage.operators.cutout_tool.geometry import depth_calibration as cutout_depth
|
||||
from anyimage.operators.cutout_tool.geometry.balloon import poisson_balloon_profile
|
||||
from anyimage.operators.common.mesh_result import ImageGeometry, load_image_geometry
|
||||
from anyimage.operators.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
)
|
||||
from anyimage.operators.convert_to_plane.normal_map import rasterize_tangent_normal
|
||||
from anyimage.operators.cutout_tool.geometry.region_contours import (
|
||||
from anyimage.operators.common.depth_data import (
|
||||
depth_uniform_scale,
|
||||
load_depth_metadata,
|
||||
)
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_depth
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_profile
|
||||
from anyimage.operators.cutout_tool import geometry as mesh
|
||||
from anyimage.operators.cutout_tool import object as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import shape as shapes
|
||||
from anyimage.operators.cutout_tool.geometry import (
|
||||
RegionContours,
|
||||
extract_region_contours,
|
||||
poisson_balloon_profile,
|
||||
)
|
||||
from anyimage.server.models import moge2 as moge2_model
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_uses_the_cropped_analysis_scale():
|
||||
assert mesh.limited_cutout_mesh_detail(256, (4096, 1024), 2048) == 128
|
||||
assert mesh.limited_cutout_mesh_detail(256, (1024, 512), 2048) == 256
|
||||
assert mesh.limited_cutout_mesh_detail(8, (8192, 4096), 2048) == 4
|
||||
assert mesh.limit_cutout_mesh_resolution(256, (4096, 1024), 2048) == 128
|
||||
assert mesh.limit_cutout_mesh_resolution(256, (1024, 512), 2048) == 256
|
||||
assert mesh.limit_cutout_mesh_resolution(8, (8192, 4096), 2048) == 4
|
||||
|
||||
|
||||
def test_image_geometry_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
|
||||
path = tmp_path / "image_geometry.npz"
|
||||
points = np.ones((2, 3, 3), dtype=np.float32)
|
||||
valid = np.ones((2, 3), dtype=bool)
|
||||
np.savez_compressed(
|
||||
path,
|
||||
points=points,
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
valid=valid,
|
||||
calibration_valid=valid,
|
||||
image_size=np.asarray((3, 2), dtype=np.int32),
|
||||
reference_depth=np.asarray(1.0, dtype=np.float32),
|
||||
region_transparent_fraction=np.asarray(0.25, dtype=np.float32),
|
||||
def test_depth_metadata_loader_uses_one_schema_for_plane_and_cutout(tmp_path):
|
||||
path = tmp_path / "depth.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"image_size": [3, 2],
|
||||
"intrinsics": np.eye(3).tolist(),
|
||||
"reference_depth": 1.0,
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
field = load_image_geometry(path)
|
||||
metadata = load_depth_metadata(path)
|
||||
|
||||
assert metadata["image_size"] == (3, 2)
|
||||
assert metadata["intrinsics"] == ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
|
||||
assert metadata["reference_depth"] == 1.0
|
||||
|
||||
assert field.points.shape == (2, 3, 3)
|
||||
assert field.image_size == (3, 2)
|
||||
assert field.reference_depth == 1.0
|
||||
assert field.region_transparent_fraction == 0.25
|
||||
|
||||
def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
loaded = object()
|
||||
@@ -63,7 +64,7 @@ def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
cutout_nodes, "load_asset_node_group", return_value=loaded
|
||||
) as load:
|
||||
for shape in shapes.CUTOUT_NODE_GROUP_NAMES:
|
||||
assert cutout_nodes.cutout_node_group(shape) is loaded
|
||||
assert cutout_nodes.load_cutout_node_group() is loaded
|
||||
load.assert_called_with(shapes.CUTOUT_NODE_GROUP_NAME)
|
||||
assert load.call_count == 4
|
||||
|
||||
@@ -81,9 +82,7 @@ def test_base_plane_fit_is_stable_for_a_slender_diagonal_domain():
|
||||
|
||||
plane = cutout_depth._stable_plane(coordinates, values)
|
||||
selected_plane = coordinates @ plane[:2] + plane[2]
|
||||
corners = np.asarray(
|
||||
((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0))
|
||||
)
|
||||
corners = np.asarray(((-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)))
|
||||
corner_plane = corners @ plane[:2] + plane[2]
|
||||
|
||||
assert np.ptp(corner_plane) <= np.ptp(selected_plane) * 1.25
|
||||
@@ -166,63 +165,29 @@ def mesh_component_count(faces):
|
||||
return count
|
||||
|
||||
|
||||
def test_tangent_normal_pixels_use_the_final_geometry_frame():
|
||||
object_normal = np.zeros((3, 3, 3), dtype=np.float32)
|
||||
object_normal[..., 2] = 1.0
|
||||
triangle_uv = np.asarray((((0.0, 0.0), (1.0, 0.0), (0.0, 1.0)),))
|
||||
triangle_normal = np.asarray((((1.0, 0.0, 0.0),) * 3,))
|
||||
triangle_tangent = np.asarray((((0.0, 1.0, 0.0),) * 3,))
|
||||
triangle_sign = np.ones((1, 3), dtype=np.float32)
|
||||
|
||||
tangent = rasterize_tangent_normal(
|
||||
object_normal,
|
||||
triangle_uv,
|
||||
triangle_normal,
|
||||
triangle_tangent,
|
||||
triangle_sign,
|
||||
)
|
||||
|
||||
assert np.allclose(tangent[0, 0], (0.0, 1.0, 0.0))
|
||||
assert not np.allclose(tangent[0, 0], object_normal[0, 0])
|
||||
assert np.allclose(tangent[2, 2], (0.0, 0.0, 1.0))
|
||||
|
||||
|
||||
def test_depth_surface_discontinuity_map_uses_local_three_by_three_range():
|
||||
depth = np.ones((5, 5), dtype=np.float32)
|
||||
depth[:, 3:] = 2.0
|
||||
valid = np.ones_like(depth, dtype=bool)
|
||||
|
||||
discontinuity = mesh._depth_discontinuity_map(depth, valid)
|
||||
|
||||
assert discontinuity[2, 0] == 0.0
|
||||
assert discontinuity[2, 2] == 1.0
|
||||
|
||||
|
||||
def test_depth_surface_projection_uses_one_scale_for_all_camera_axes():
|
||||
points = np.zeros((51, 101, 3), dtype=np.float32)
|
||||
points[..., 2] = np.linspace(1.0, 3.0, 51 * 101).reshape(51, 101)
|
||||
field = ImageGeometry(
|
||||
points=points,
|
||||
intrinsics=np.asarray(
|
||||
((100.0, 0.0, 50.0), (0.0, 100.0, 25.0), (0.0, 0.0, 1.0))
|
||||
metadata = {
|
||||
"intrinsics": (
|
||||
(100.0, 0.0, 50.0),
|
||||
(0.0, 100.0, 25.0),
|
||||
(0.0, 0.0, 1.0),
|
||||
),
|
||||
valid=np.ones((51, 101), dtype=bool),
|
||||
calibration_valid=np.ones((51, 101), dtype=bool),
|
||||
image_size=(101, 51),
|
||||
reference_depth=2.0,
|
||||
region_transparent_fraction=0.0,
|
||||
)
|
||||
"image_size": (101, 51),
|
||||
"reference_depth": 2.0,
|
||||
}
|
||||
|
||||
uniform_scale = mesh._depth_uniform_scale(
|
||||
uniform_scale = depth_uniform_scale(
|
||||
SimpleNamespace(),
|
||||
field,
|
||||
metadata,
|
||||
bounds=(-2.0, 2.0, -1.0, 1.0),
|
||||
)
|
||||
|
||||
assert uniform_scale == 2.0
|
||||
reference_depth = cutout_depth.metric_depth_reference(
|
||||
points[..., 2],
|
||||
field.calibration_valid,
|
||||
np.ones((51, 101), dtype=bool),
|
||||
uniform_scale,
|
||||
)
|
||||
assert np.isclose(reference_depth, 5.8, atol=1e-6)
|
||||
@@ -245,6 +210,21 @@ def test_moge_surface_field_uses_the_model_validity_mask():
|
||||
assert np.array_equal(frame.valid_mask, prediction.mask[0])
|
||||
|
||||
|
||||
def test_moge_z_depth_frame_does_not_require_points():
|
||||
prediction = moge2_model.Prediction(
|
||||
depth=np.ones((1, 2, 2), dtype=np.float32),
|
||||
normal=np.zeros((1, 2, 2, 3), dtype=np.float32),
|
||||
mask=np.ones((1, 2, 2), dtype=bool),
|
||||
intrinsics=np.eye(3, dtype=np.float32)[None],
|
||||
points=None,
|
||||
)
|
||||
|
||||
frame = moge2_model.geometry_frame(prediction)
|
||||
|
||||
assert frame.points is None
|
||||
assert frame.valid_mask.all()
|
||||
|
||||
|
||||
def test_region_contours_group_holes_with_their_connected_component():
|
||||
alpha = np.zeros((120, 200), dtype=np.uint8)
|
||||
alpha[20:100, 50:160] = 255
|
||||
@@ -259,13 +239,10 @@ def test_region_contours_group_holes_with_their_connected_component():
|
||||
assert region_contours.image_size == (200, 120)
|
||||
assert sorted(len(component) for component in region_contours.components) == [1, 2]
|
||||
contours = [
|
||||
contour
|
||||
for component in region_contours.components
|
||||
for contour in component
|
||||
contour for component in region_contours.components for contour in component
|
||||
]
|
||||
bounds = [
|
||||
(*np.min(contour, axis=0), *np.max(contour, axis=0))
|
||||
for contour in contours
|
||||
(*np.min(contour, axis=0), *np.max(contour, axis=0)) for contour in contours
|
||||
]
|
||||
assert (50.0, 20.0, 160.0, 100.0) in bounds
|
||||
assert (80.0, 45.0, 120.0, 75.0) in bounds
|
||||
@@ -294,7 +271,7 @@ def test_cutout_crop_bounds_map_to_the_source_plane():
|
||||
empty_image_offset=(-0.5, -0.5),
|
||||
)
|
||||
|
||||
bounds = mesh._crop_plane_bounds(
|
||||
bounds = mesh.crop_plane_bounds(
|
||||
source,
|
||||
(50, 25, 150, 75),
|
||||
(200, 100),
|
||||
@@ -308,7 +285,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
|
||||
alpha[2:10, 2:14] = 0.49
|
||||
alpha[4:8, 5:11] = 0.51
|
||||
|
||||
region_contours = mesh._extract_cutout_contours(
|
||||
region_contours = mesh.extract_cutout_contours(
|
||||
RegionMask(alpha, (0, 0, 16, 12)),
|
||||
64,
|
||||
threshold=0.5,
|
||||
@@ -319,6 +296,7 @@ def test_depth_surface_cutout_uses_half_alpha_boundary():
|
||||
np.testing.assert_allclose(contour.min(axis=0), (5.0, 4.0))
|
||||
np.testing.assert_allclose(contour.max(axis=0), (11.0, 8.0))
|
||||
|
||||
|
||||
def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
original_import = builtins.__import__
|
||||
|
||||
@@ -343,6 +321,7 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
assert region_contours.components
|
||||
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
|
||||
|
||||
|
||||
def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
expected = RegionContours(
|
||||
components=(((((0, 0), (1, 0), (0, 1))),),),
|
||||
@@ -350,37 +329,38 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
)
|
||||
region_mask = RegionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
|
||||
with patch.object(mesh, "extract_region_contours", return_value=expected) as trace:
|
||||
assert mesh._extract_cutout_contours(region_mask, 32) == expected
|
||||
assert mesh._extract_cutout_contours(region_mask, 256) == expected
|
||||
assert mesh.extract_cutout_contours(region_mask, 32) == expected
|
||||
assert mesh.extract_cutout_contours(region_mask, 256) == expected
|
||||
|
||||
assert trace.call_args_list[0].kwargs["max_resolution"] == 512
|
||||
assert trace.call_args_list[1].kwargs["max_resolution"] == 1024
|
||||
|
||||
|
||||
def test_cutout_point_spacing_uses_the_complete_source_plane():
|
||||
source = SimpleNamespace(matrix_world=object())
|
||||
with (
|
||||
patch.object(mesh, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
|
||||
patch.object(
|
||||
mesh,
|
||||
"_world_plane_metric",
|
||||
"world_plane_metric",
|
||||
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
|
||||
),
|
||||
):
|
||||
spacing = mesh._cutout_point_spacing(source, 200)
|
||||
spacing = mesh.cutout_point_spacing(source, 200)
|
||||
|
||||
assert spacing == 0.02
|
||||
|
||||
|
||||
def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_value(
|
||||
mesh.cutout_cleanup_value(
|
||||
0.25,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
),
|
||||
6.25,
|
||||
)
|
||||
assert np.isclose(
|
||||
mesh._cutout_cleanup_value(
|
||||
mesh.cutout_cleanup_value(
|
||||
0.25,
|
||||
((4.0, 0.0), (0.0, 9.0)),
|
||||
),
|
||||
@@ -437,6 +417,8 @@ def test_poisson_balloon_profile_makes_wide_regions_thicker():
|
||||
narrow_profile = poisson_balloon_profile(narrow, faces, boundary)
|
||||
|
||||
assert wide_profile[4] > narrow_profile[4]
|
||||
|
||||
|
||||
def test_teddy_balloon_profile_uses_chordal_axis_radius():
|
||||
points = np.asarray(
|
||||
((0, 0), (4, 0), (4, 2), (0, 2), (2, 1)),
|
||||
@@ -463,14 +445,10 @@ def test_teddy_balloon_profile_uses_chordal_axis_radius():
|
||||
assert np.isclose(profile[4], 0.45, atol=1e-6)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
|
||||
regions = (
|
||||
(((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),
|
||||
)
|
||||
regions = ((((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),)
|
||||
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -484,7 +462,7 @@ def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_region():
|
||||
def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
regions = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),)
|
||||
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -494,10 +472,11 @@ def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
assert len(domain[0]) >= 4
|
||||
assert len(domain[1]) >= 2
|
||||
|
||||
|
||||
def test_cutout_domain_centers_vertices_on_crop_bounds():
|
||||
regions = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
|
||||
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = mesh._cutout_domain(
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = mesh.cutout_domain(
|
||||
RegionContours(regions, (100, 50)),
|
||||
(2.0, 6.0, -3.0, 1.0),
|
||||
0.5,
|
||||
@@ -516,11 +495,9 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
((20, 0), (100, 0), (100, 80), (20, 80)),
|
||||
((50, 20), (50, 60), (70, 60), (70, 20)),
|
||||
),
|
||||
(
|
||||
((0, 20), (40, 20), (40, 50), (0, 50)),
|
||||
),
|
||||
(((0, 20), (40, 20), (40, 50), (0, 50)),),
|
||||
)
|
||||
domain = mesh._cutout_domain(
|
||||
domain = mesh.cutout_domain(
|
||||
RegionContours(regions, (120, 80)),
|
||||
(0.0, 1.0, 0.0, 1.0),
|
||||
0.025,
|
||||
@@ -544,3 +521,15 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
assert not in_hole.any()
|
||||
assert in_left_region.any()
|
||||
assert mesh_component_count(domain[1]) == 2
|
||||
|
||||
|
||||
def test_region_calibration_uses_the_final_region_mask():
|
||||
region_mask = RegionMask(
|
||||
np.asarray(((1.0, 1.0), (1.0, 0.0)), dtype=np.float32),
|
||||
(0, 0, 2, 2),
|
||||
)
|
||||
|
||||
valid, transparent_fraction = mesh.region_calibration(region_mask, (2, 2))
|
||||
|
||||
assert np.array_equal(valid, ((True, True), (True, False)))
|
||||
assert transparent_fraction == 0.25
|
||||
|
||||
@@ -5,27 +5,26 @@ from unittest.mock import patch
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Matrix, Vector
|
||||
from mathutils import Vector
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators.common import image_data as common_image
|
||||
from anyimage.operators.common import depth_data as common_depth
|
||||
from anyimage.operators.common import mesh_result as material
|
||||
from anyimage.operators.common.mesh_result import ImageGeometry
|
||||
from anyimage.operators.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
)
|
||||
from anyimage.operators.cutout_tool import region_to_shape as mesh
|
||||
from anyimage.operators.cutout_tool import region_to_shape as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import region_to_shape as shapes
|
||||
from anyimage.operators.convert_to_plane import nodes as plane_nodes
|
||||
from anyimage.operators.convert_to_plane import build as plane_build
|
||||
from anyimage.operators.convert_to_plane.normal_map import (
|
||||
bake_tangent_normal_image,
|
||||
)
|
||||
from anyimage.operators.convert_to_plane import object as plane_object
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool import main as cutout_main
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
from anyimage.operators.cutout_tool import shape as shapes
|
||||
from anyimage.server.geometry.depth_texture import write_vector_depth_texture
|
||||
|
||||
|
||||
def _inputs(group):
|
||||
@@ -44,93 +43,101 @@ def _group_node(group, node_tree):
|
||||
)
|
||||
|
||||
|
||||
def _create_cutout_shape(source, shape, detail, *, bounds=None, field=None):
|
||||
def _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
*,
|
||||
bounds=None,
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
region_values=None,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
left, top, right, bottom = bounds
|
||||
region_mask = RegionMask(
|
||||
np.ones((bottom - top, right - left), dtype=np.float32),
|
||||
bounds,
|
||||
)
|
||||
base_shape = mesh.build_base_shape(
|
||||
if region_values is None:
|
||||
region_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
region_mask = RegionMask(region_values, bounds)
|
||||
base_shape = cutout_geometry.build_base_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
detail,
|
||||
region_mask,
|
||||
)
|
||||
return mesh.create_shape_object(
|
||||
return cutout_object.create_shape_object(
|
||||
bpy.context,
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
region_mask,
|
||||
base_shape,
|
||||
field=field,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
)
|
||||
|
||||
|
||||
def test_depth_surface_image_packs_metric_points_and_edge_validity():
|
||||
def _depth_image(points, valid, name="Depth"):
|
||||
points = np.asarray(points, dtype=np.float32)
|
||||
valid = np.asarray(valid, dtype=bool)
|
||||
height, width = valid.shape
|
||||
pixels = np.zeros((height, width, 4), dtype=np.float32)
|
||||
pixels[valid, :3] = points[valid]
|
||||
pixels[..., 3] = valid
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=True,
|
||||
float_buffer=True,
|
||||
)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
image.pixels.foreach_set(np.flipud(pixels).ravel())
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def _depth_metadata(intrinsics, image_size, reference_depth):
|
||||
return {
|
||||
"intrinsics": tuple(tuple(float(value) for value in row) for row in intrinsics),
|
||||
"image_size": tuple(int(value) for value in image_size),
|
||||
"reference_depth": float(reference_depth),
|
||||
}
|
||||
|
||||
|
||||
def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Source.png", width=2, height=2)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.data = source_image
|
||||
points = np.arange(12, dtype=np.float32).reshape((2, 2, 3))
|
||||
depth = np.asarray(((1.0, 1.0), (1.0, 2.0)), dtype=np.float32)
|
||||
valid = np.asarray(((True, False), (True, True)))
|
||||
path = tmp_path / "vector-depth.exr"
|
||||
write_vector_depth_texture(points, valid, path)
|
||||
metadata = _depth_metadata(np.eye(3), (2, 2), 1.0)
|
||||
|
||||
image = mesh._cutout_depth_image(source, points, valid, depth)
|
||||
image = common_depth.load_depth_result_image(path, source, metadata)
|
||||
|
||||
packed = np.empty(16, dtype=np.float32)
|
||||
image.pixels.foreach_get(packed)
|
||||
packed = np.flipud(packed.reshape((2, 2, 4)))
|
||||
assert np.array_equal(packed[:, :, :3], points)
|
||||
assert packed[0, 1, 3] == -1.0
|
||||
assert np.all(packed[valid, 3] >= 0.0)
|
||||
assert np.array_equal(packed[valid, :3], points[valid])
|
||||
assert np.array_equal(packed[..., 3] > 0.5, valid)
|
||||
assert image.alpha_mode == "CHANNEL_PACKED"
|
||||
assert image.name == "Source_depth.exr"
|
||||
|
||||
|
||||
def test_tangent_bake_changes_pixels_using_evaluated_mesh_tbn():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
bpy.context.collection.objects.link(source)
|
||||
target_mesh = bpy.data.meshes.new("Target")
|
||||
target_mesh.from_pydata(
|
||||
((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
(),
|
||||
((0, 1, 2),),
|
||||
)
|
||||
target_mesh.polygons[0].use_smooth = True
|
||||
mesh._assign_uv(
|
||||
target_mesh,
|
||||
np.asarray(((0.0, 0.0), (1.0, 0.0), (0.0, 1.0))),
|
||||
)
|
||||
target = bpy.data.objects.new("Target", target_mesh)
|
||||
bpy.context.collection.objects.link(target)
|
||||
target.matrix_world = Matrix.Rotation(np.radians(-90.0), 4, "Y")
|
||||
image = bpy.data.images.new("Object Normal", width=3, height=3, alpha=True)
|
||||
pixels = np.ones((3, 3, 4), dtype=np.float32)
|
||||
pixels[..., :3] = (1.0, 0.5, 0.5)
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
|
||||
bake_tangent_normal_image(bpy.context, source, target, image)
|
||||
|
||||
baked = np.empty(3 * 3 * 4, dtype=np.float32)
|
||||
image.pixels.foreach_get(baked)
|
||||
baked = baked.reshape((3, 3, 4))
|
||||
assert not np.allclose(baked[0, 0, :3], pixels[0, 0, :3])
|
||||
|
||||
|
||||
def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
plane = plane_nodes.plane_node_group("PLANE")
|
||||
depth_plane = plane_nodes.plane_node_group("DEPTH_PLANE")
|
||||
cutout = cutout_nodes.cutout_node_group("SURFACE")
|
||||
cutout = cutout_object.load_cutout_node_group()
|
||||
assert plane.name == "O Mesh Plane"
|
||||
assert cutout.name == shapes.CUTOUT_NODE_GROUP_NAME
|
||||
assert all(
|
||||
cutout_nodes.cutout_node_group(shape) == cutout
|
||||
cutout_object.load_cutout_node_group() == cutout
|
||||
for shape in shapes.CUTOUT_SHAPES
|
||||
)
|
||||
assert _inputs(plane) == ["Geometry", "Subdivide", "Thickness", "Material"]
|
||||
@@ -139,6 +146,8 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"Subdivide",
|
||||
"Thickness",
|
||||
"Depth Amount",
|
||||
"Reference Depth",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
"Material",
|
||||
]
|
||||
@@ -156,9 +165,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
]
|
||||
panels = [
|
||||
item for item in cutout.interface.items_tree if item.item_type == "PANEL"
|
||||
]
|
||||
panels = [item for item in cutout.interface.items_tree if item.item_type == "PANEL"]
|
||||
assert [panel.name for panel in panels] == ["Options", "Data"]
|
||||
assert [item.name for item in panels[0].interface_items] == [
|
||||
"Cleanup",
|
||||
@@ -258,33 +265,24 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"Depth Image": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
}
|
||||
|
||||
assert sum(
|
||||
node.bl_idname == "GeometryNodeInputMeshIsland" for node in cutout.nodes
|
||||
) == 1
|
||||
assert (
|
||||
sum(node.bl_idname == "GeometryNodeInputMeshIsland" for node in cutout.nodes)
|
||||
== 1
|
||||
)
|
||||
assert not any(
|
||||
node.bl_idname == "GeometryNodeAttributeStatistic"
|
||||
for node in cutout.nodes
|
||||
node.bl_idname == "GeometryNodeAttributeStatistic" for node in cutout.nodes
|
||||
)
|
||||
assert all(
|
||||
not any(
|
||||
node.bl_idname == "GeometryNodeInputMeshIsland"
|
||||
for node in group.nodes
|
||||
)
|
||||
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
|
||||
for group in child_groups.values()
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeBlurAttribute" for node in cutout.nodes
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in cutout.nodes)
|
||||
assert all(
|
||||
not any(
|
||||
node.bl_idname == "GeometryNodeBlurAttribute" for node in group.nodes
|
||||
)
|
||||
not any(node.bl_idname == "GeometryNodeBlurAttribute" for node in group.nodes)
|
||||
for group in child_groups.values()
|
||||
)
|
||||
repeat_counts = {
|
||||
shape: sum(
|
||||
node.bl_idname == "GeometryNodeRepeatInput" for node in group.nodes
|
||||
)
|
||||
shape: sum(node.bl_idname == "GeometryNodeRepeatInput" for node in group.nodes)
|
||||
for shape, group in child_groups.items()
|
||||
}
|
||||
assert repeat_counts == {
|
||||
@@ -293,9 +291,9 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
"DEPTH_BALLOON": 0,
|
||||
"DEPTH_SURFACE": 1,
|
||||
}
|
||||
assert sum(
|
||||
node.bl_idname == "GeometryNodeRepeatInput" for node in cutout.nodes
|
||||
) == 1
|
||||
assert (
|
||||
sum(node.bl_idname == "GeometryNodeRepeatInput" for node in cutout.nodes) == 1
|
||||
)
|
||||
|
||||
smooth = next(
|
||||
item
|
||||
@@ -338,15 +336,12 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
|
||||
for group in child_groups.values():
|
||||
extrudes = [
|
||||
node
|
||||
for node in group.nodes
|
||||
if node.bl_idname == "GeometryNodeExtrudeMesh"
|
||||
node for node in group.nodes if node.bl_idname == "GeometryNodeExtrudeMesh"
|
||||
]
|
||||
assert extrudes
|
||||
assert all(not node.inputs["Individual"].default_value for node in extrudes)
|
||||
assert all(
|
||||
node.inputs["Offset Scale"].default_value == 1.0
|
||||
for node in extrudes
|
||||
node.inputs["Offset Scale"].default_value == 1.0 for node in extrudes
|
||||
)
|
||||
for group in (
|
||||
child_groups["SURFACE"],
|
||||
@@ -371,6 +366,7 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
assert thickness.subtype == "NONE"
|
||||
assert thickness.default_value == 1.0
|
||||
|
||||
|
||||
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
@@ -409,9 +405,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
)
|
||||
assert object_space.default_value is False
|
||||
normal_maps = {
|
||||
node.space
|
||||
for node in group.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
node.space for node in group.nodes if node.bl_idname == "ShaderNodeNormalMap"
|
||||
}
|
||||
assert normal_maps == {"TANGENT", "OBJECT"}
|
||||
assert all(
|
||||
@@ -442,9 +436,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
reused = material.material_node_group()
|
||||
assert {
|
||||
node.space
|
||||
for node in reused.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
node.space for node in reused.nodes if node.bl_idname == "ShaderNodeNormalMap"
|
||||
} == {"TANGENT", "OBJECT"}
|
||||
|
||||
|
||||
@@ -458,7 +450,7 @@ def test_build_plane_keeps_one_quad_and_generates_topology_in_geometry_nodes():
|
||||
bpy.context.collection.objects.link(source)
|
||||
material = bpy.data.materials.new("Source.material")
|
||||
|
||||
result = plane_build.build_mesh_plane(
|
||||
result = plane_object.create_plane_object(
|
||||
bpy.context,
|
||||
source,
|
||||
2,
|
||||
@@ -517,7 +509,7 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
with patch.object(
|
||||
mesh,
|
||||
cutout_geometry,
|
||||
"addon_preferences",
|
||||
return_value=SimpleNamespace(balloon_profile_algorithm="TEDDY"),
|
||||
):
|
||||
@@ -533,6 +525,24 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
|
||||
assert profile.min() == 0.0 and profile.max() > 0.0
|
||||
|
||||
|
||||
def test_cutout_material_uses_ior_1_2():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Cutout", width=16, height=16, alpha=True)
|
||||
source = bpy.data.objects.new("Cutout", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
|
||||
result = _create_cutout_shape(source, "SURFACE", 8)
|
||||
|
||||
shader = next(
|
||||
node
|
||||
for node in result.data.materials[0].node_tree.nodes
|
||||
if node.bl_idname == "ShaderNodeBsdfPrincipled"
|
||||
)
|
||||
assert np.isclose(shader.inputs["IOR"].default_value, 1.2)
|
||||
|
||||
def test_cutout_material_uses_flat_tangent_normal():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
@@ -553,18 +563,6 @@ def test_cutout_material_uses_flat_tangent_normal():
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
|
||||
|
||||
def test_internal_depth_plane_image_uses_standard_data_name():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
plane_source = bpy.data.objects.new("Plane Source", None)
|
||||
plane_source.data = bpy.data.images.new(
|
||||
"Plane Source.png", width=2, height=2, alpha=True
|
||||
)
|
||||
anchored = np.zeros((2, 2, 3), dtype=np.float32)
|
||||
plane_depth = plane_build.create_depth_image(plane_source, anchored)
|
||||
assert plane_depth.name == "Plane Source_depth.exr"
|
||||
|
||||
|
||||
|
||||
def test_depth_surface_protected_smooth_evaluates():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Surface", width=64, height=64)
|
||||
@@ -584,54 +582,52 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
points[..., 0] = (x - intrinsics[0, 2]) / intrinsics[0, 0] * depth
|
||||
points[..., 1] = (y - intrinsics[1, 2]) / intrinsics[1, 1] * depth
|
||||
points[..., 2] = depth
|
||||
field = ImageGeometry(
|
||||
points=points,
|
||||
intrinsics=intrinsics,
|
||||
valid=np.ones((size, size), dtype=bool),
|
||||
calibration_valid=np.ones((size, size), dtype=bool),
|
||||
image_size=(size, size),
|
||||
reference_depth=1.0,
|
||||
region_transparent_fraction=0.0,
|
||||
)
|
||||
valid = np.ones((size, size), dtype=bool)
|
||||
depth_image = _depth_image(points, valid)
|
||||
depth_metadata = _depth_metadata(intrinsics, (size, size), 1.0)
|
||||
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_SURFACE",
|
||||
8,
|
||||
field=field,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
assert shapes.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Shape")
|
||||
] == shapes.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
|
||||
expected_cleanup = mesh._cutout_cleanup_value(
|
||||
mesh._cutout_point_spacing(source, 8),
|
||||
mesh._world_plane_metric(source.matrix_world)[0],
|
||||
assert (
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Shape")]
|
||||
== shapes.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 8),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
modifier[mesh.modifier_input_identifier(modifier.node_group, "Cleanup")],
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Cleanup")],
|
||||
expected_cleanup,
|
||||
)
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth")
|
||||
] == 1.0
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(
|
||||
modifier.node_group, "Smooth Iterations"
|
||||
)
|
||||
] == 0
|
||||
reference_id = mesh.modifier_input_identifier(
|
||||
assert (
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Smooth")]
|
||||
== 1.0
|
||||
)
|
||||
assert (
|
||||
modifier[
|
||||
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
|
||||
]
|
||||
== 0
|
||||
)
|
||||
reference_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Base Plane Depth",
|
||||
)
|
||||
assert modifier[reference_id] == 8.0
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
0.0,
|
||||
)
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
@@ -643,9 +639,9 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
1.0,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
@@ -660,16 +656,14 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(
|
||||
modifier.node_group, "Smooth Iterations"
|
||||
),
|
||||
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations"),
|
||||
4,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
1.0,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
@@ -688,7 +682,6 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
assert np.isfinite(internal_positions).all()
|
||||
|
||||
|
||||
|
||||
def _evaluated_positions(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
@@ -699,38 +692,35 @@ def _evaluated_positions(obj):
|
||||
|
||||
|
||||
def _add_cutout_modifier(obj, shape):
|
||||
group = cutout_nodes.cutout_node_group(shape)
|
||||
group = cutout_object.load_cutout_node_group()
|
||||
modifier = obj.modifiers.new(group.name, "NODES")
|
||||
modifier.node_group = group
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(group, "Shape"),
|
||||
material.modifier_input_identifier(group, "Shape"),
|
||||
shapes.CUTOUT_SHAPE_INDICES[shape],
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(group, "Cleanup"),
|
||||
material.modifier_input_identifier(group, "Cleanup"),
|
||||
0.0,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(group, "Smooth"),
|
||||
material.modifier_input_identifier(group, "Smooth"),
|
||||
0.0,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(group, "Smooth Iterations"),
|
||||
material.modifier_input_identifier(group, "Smooth Iterations"),
|
||||
1,
|
||||
)
|
||||
return modifier
|
||||
|
||||
|
||||
def test_cutout_thickness_keeps_one_connected_shell():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [
|
||||
(0.0, float(y), float(z))
|
||||
for z in range(3)
|
||||
for y in range(3)
|
||||
]
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
@@ -742,14 +732,14 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "SURFACE")
|
||||
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Cleanup"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Cleanup"),
|
||||
0.0,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
1.0,
|
||||
)
|
||||
|
||||
@@ -764,9 +754,7 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
front_faces = [
|
||||
polygon for polygon in result.polygons if polygon.normal.x < -0.9
|
||||
]
|
||||
back_faces = [
|
||||
polygon for polygon in result.polygons if polygon.normal.x > 0.9
|
||||
]
|
||||
back_faces = [polygon for polygon in result.polygons if polygon.normal.x > 0.9]
|
||||
side_faces = [
|
||||
polygon for polygon in result.polygons if abs(polygon.normal.x) < 0.1
|
||||
]
|
||||
@@ -780,9 +768,9 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
2.4,
|
||||
)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
@@ -809,11 +797,7 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
|
||||
def test_balloon_uses_injected_profile_at_default_height():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [
|
||||
(0.0, float(y), float(z))
|
||||
for z in range(3)
|
||||
for y in range(3)
|
||||
]
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
@@ -821,7 +805,7 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
data = bpy.data.meshes.new("Balloon Profile")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
mesh._assign_float_point_attribute(
|
||||
cutout_object._assign_float_point_attribute(
|
||||
data,
|
||||
shapes.BALLOON_ATTRIBUTE_NAME,
|
||||
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
|
||||
@@ -830,14 +814,14 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "BALLOON")
|
||||
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Cleanup"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Cleanup"),
|
||||
0.0,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
1.0,
|
||||
)
|
||||
|
||||
@@ -856,9 +840,9 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
baseline_face_count = len(result.polygons)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
2.0,
|
||||
)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
@@ -872,9 +856,9 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
assert np.isclose(x.max(), 2.0, atol=1e-6)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
material.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
0.0,
|
||||
)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
@@ -886,11 +870,7 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
|
||||
def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [
|
||||
(0.0, float(y), float(z))
|
||||
for z in range(3)
|
||||
for y in range(3)
|
||||
]
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
@@ -899,9 +879,7 @@ def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
|
||||
for island in range(12):
|
||||
start = len(vertices)
|
||||
y = 5.0 + island * 0.1
|
||||
vertices.extend(
|
||||
((0.0, y, 5.0), (0.0, y + 0.01, 5.0), (0.0, y, 5.01))
|
||||
)
|
||||
vertices.extend(((0.0, y, 5.0), (0.0, y + 0.01, 5.0), (0.0, y, 5.01)))
|
||||
faces.append((start, start + 1, start + 2))
|
||||
data = bpy.data.meshes.new("Cleanup")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
@@ -909,13 +887,13 @@ def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "SURFACE")
|
||||
|
||||
cleanup_id = mesh.modifier_input_identifier(modifier.node_group, "Cleanup")
|
||||
cleanup_id = material.modifier_input_identifier(modifier.node_group, "Cleanup")
|
||||
|
||||
mesh.set_modifier_input(modifier, cleanup_id, 0.0)
|
||||
material.set_modifier_input(modifier, cleanup_id, 0.0)
|
||||
bpy.context.view_layer.update()
|
||||
assert len(_evaluated_positions(obj)) == 45
|
||||
|
||||
mesh.set_modifier_input(modifier, cleanup_id, 0.1)
|
||||
material.set_modifier_input(modifier, cleanup_id, 0.1)
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
assert len(_evaluated_positions(obj)) == 9
|
||||
@@ -930,8 +908,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
crop_bounds = (50, 20, 90, 80)
|
||||
crop = ((0, 0), (40, 0), (40, 60), (0, 60))
|
||||
local_bounds = mesh._crop_plane_bounds(
|
||||
local_bounds = cutout_geometry.crop_plane_bounds(
|
||||
source,
|
||||
crop_bounds,
|
||||
(100, 100),
|
||||
@@ -948,22 +925,21 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
((50.0, 0.0, 20.0), (0.0, 50.0, 30.0), (0.0, 0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
field = ImageGeometry(
|
||||
points=points,
|
||||
intrinsics=intrinsics,
|
||||
valid=np.ones((61, 41), dtype=bool),
|
||||
calibration_valid=np.ones((61, 41), dtype=bool),
|
||||
image_size=(41, 61),
|
||||
reference_depth=2.0,
|
||||
region_transparent_fraction=0.05,
|
||||
)
|
||||
valid = np.ones((61, 41), dtype=bool)
|
||||
z_depth = np.repeat(points[..., 2, None], 3, axis=2)
|
||||
depth_image = _depth_image(z_depth, valid)
|
||||
depth_metadata = _depth_metadata(intrinsics, (41, 61), 2.0)
|
||||
|
||||
region_values = np.ones((60, 40), dtype=np.float32)
|
||||
region_values[:, :2] = 0.0
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_BALLOON",
|
||||
8,
|
||||
field=field,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
bounds=crop_bounds,
|
||||
region_values=region_values,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
@@ -976,7 +952,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
result.name,
|
||||
}
|
||||
modifier = result.modifiers[0]
|
||||
depth_image_id = mesh.modifier_input_identifier(
|
||||
depth_image_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Image",
|
||||
)
|
||||
@@ -996,39 +972,39 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
atol=1e-5,
|
||||
)
|
||||
|
||||
depth_reference_id = mesh.modifier_input_identifier(
|
||||
depth_reference_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Base Plane Depth",
|
||||
)
|
||||
assert not np.isclose(modifier[depth_reference_id], 8.0)
|
||||
depth_direction_id = mesh.modifier_input_identifier(
|
||||
depth_direction_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Direction",
|
||||
)
|
||||
direction = np.asarray(modifier[depth_direction_id], dtype=np.float64)
|
||||
assert not np.allclose(direction, (0.0, 0.0, 1.0))
|
||||
uniform_scale_id = mesh.modifier_input_identifier(
|
||||
uniform_scale_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Uniform Scale",
|
||||
)
|
||||
assert modifier[uniform_scale_id] > 0.0
|
||||
thickness_id = mesh.modifier_input_identifier(
|
||||
thickness_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Thickness",
|
||||
)
|
||||
smooth_id = mesh.modifier_input_identifier(
|
||||
smooth_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Smooth",
|
||||
)
|
||||
assert modifier[smooth_id] == 1.0
|
||||
mesh.set_modifier_input(modifier, smooth_id, 0.0)
|
||||
material.set_modifier_input(modifier, smooth_id, 0.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(result)
|
||||
center_distance = np.linalg.norm(positions[:, 1:] - expected_center, axis=1)
|
||||
nearest_center = center_distance <= center_distance.min() + 1e-6
|
||||
depth_at_thickness_one = positions[nearest_center, 0].min()
|
||||
mesh.set_modifier_input(modifier, thickness_id, 2.0)
|
||||
material.set_modifier_input(modifier, thickness_id, 2.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(result)
|
||||
@@ -1040,12 +1016,12 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
depth_at_thickness_two,
|
||||
atol=1e-5,
|
||||
)
|
||||
mesh.set_modifier_input(modifier, thickness_id, 1.0)
|
||||
material.set_modifier_input(modifier, thickness_id, 1.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
before_positions = _evaluated_positions(result)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier[depth_reference_id] + 0.2,
|
||||
@@ -1059,31 +1035,31 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
atol=1e-5,
|
||||
)
|
||||
|
||||
depth_scale_id = mesh.modifier_input_identifier(
|
||||
depth_scale_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Scale",
|
||||
)
|
||||
mesh.set_modifier_input(modifier, depth_scale_id, 0.0)
|
||||
material.set_modifier_input(modifier, depth_scale_id, 0.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
zero_scale_before = _evaluated_positions(result)
|
||||
mesh.set_modifier_input(
|
||||
material.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier[depth_reference_id] + 0.15,
|
||||
)
|
||||
mesh.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
|
||||
material.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
zero_scale_after = _evaluated_positions(result)
|
||||
assert np.allclose(zero_scale_after, zero_scale_before, atol=1e-5)
|
||||
mesh.set_modifier_input(modifier, depth_scale_id, 1.0)
|
||||
material.set_modifier_input(modifier, depth_scale_id, 1.0)
|
||||
|
||||
double_sided_id = mesh.modifier_input_identifier(
|
||||
double_sided_id = material.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Double Sided",
|
||||
)
|
||||
mesh.set_modifier_input(modifier, double_sided_id, True)
|
||||
material.set_modifier_input(modifier, double_sided_id, True)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
mirrored = _evaluated_positions(result)
|
||||
@@ -1115,6 +1091,7 @@ def test_loaded_normal_image_uses_normal_texture_name(tmp_path):
|
||||
assert image.colorspace_settings.name == "Non-Color"
|
||||
assert image.packed_file is not None
|
||||
|
||||
|
||||
def test_converted_color_images_are_new_and_use_blender_duplicate_suffix():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Poster", width=16, height=16)
|
||||
@@ -1123,8 +1100,8 @@ def test_converted_color_images_are_new_and_use_blender_duplicate_suffix():
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
|
||||
first = plane_build.copy_color_image(source)
|
||||
second = plane_build.copy_color_image(source)
|
||||
first = plane_object.create_color_image(source)
|
||||
second = plane_object.create_color_image(source)
|
||||
|
||||
assert first is not image
|
||||
assert second is not image
|
||||
@@ -1151,11 +1128,13 @@ def test_local_cutout_selection_matches_uint8_tenth_alpha_boundary():
|
||||
assert selection[1, 1] == 0.0
|
||||
assert selection[1, 2] > 0.1
|
||||
|
||||
|
||||
def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Local Cutout", width=64, height=48, alpha=True)
|
||||
pixels = np.ones((48, 64, 4), dtype=np.float32)
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
source_rgba = np.ones((48, 64, 4), dtype=np.float32)
|
||||
source_rgba[:, :, 3] = np.linspace(0.25, 1.0, 48, dtype=np.float32)[:, None]
|
||||
image.pixels.foreach_set(np.flipud(source_rgba).ravel())
|
||||
source = bpy.data.objects.new("Local Cutout", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
@@ -1166,30 +1145,29 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
points=((-8, 8), (72, 8), (72, 40), (-8, 40)),
|
||||
fill_rule="NONZERO",
|
||||
)
|
||||
source_bounds = mesh.image_empty_bounds(source)
|
||||
source_bounds = common_image.image_empty_bounds(source)
|
||||
|
||||
with patch.object(
|
||||
mesh,
|
||||
common_image,
|
||||
"image_rgba",
|
||||
wraps=mesh.image_rgba,
|
||||
wraps=common_image.image_rgba,
|
||||
) as read_rgba:
|
||||
rgba = mesh.image_rgba(image)
|
||||
rgba = common_image.image_rgba(image)
|
||||
region_mask = rasterize_region_path(
|
||||
tuple(image.size),
|
||||
region_path,
|
||||
source_alpha=rgba[:, :, 3],
|
||||
alpha_threshold=mesh.CUTOUT_ALPHA_THRESHOLD,
|
||||
alpha_threshold=cutout_geometry.CUTOUT_ALPHA_THRESHOLD,
|
||||
antialias=True,
|
||||
)
|
||||
with patch.object(mesh, "configured_max_ai_input_size", return_value=2048):
|
||||
mesh.region_to_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
"SURFACE",
|
||||
16,
|
||||
region_mask,
|
||||
source_rgba=rgba,
|
||||
)
|
||||
cutout_main.create_cutout_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
"SURFACE",
|
||||
16,
|
||||
region_mask,
|
||||
source_rgba=rgba,
|
||||
)
|
||||
|
||||
read_rgba.assert_called_once_with(image)
|
||||
|
||||
@@ -1199,25 +1177,28 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
assert result["anyimage_mesh_shape"] == "SURFACE"
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == shapes.CUTOUT_NODE_GROUP_NAME
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Shape")
|
||||
] == shapes.CUTOUT_SHAPE_INDICES["SURFACE"]
|
||||
expected_cleanup = mesh._cutout_cleanup_value(
|
||||
mesh._cutout_point_spacing(source, 16),
|
||||
mesh._world_plane_metric(source.matrix_world)[0],
|
||||
assert (
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Shape")]
|
||||
== shapes.CUTOUT_SHAPE_INDICES["SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 16),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
modifier[mesh.modifier_input_identifier(modifier.node_group, "Cleanup")],
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Cleanup")],
|
||||
expected_cleanup,
|
||||
)
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth")
|
||||
] == 1.0
|
||||
assert modifier[
|
||||
mesh.modifier_input_identifier(
|
||||
modifier.node_group, "Smooth Iterations"
|
||||
)
|
||||
] == 0
|
||||
assert (
|
||||
modifier[material.modifier_input_identifier(modifier.node_group, "Smooth")]
|
||||
== 1.0
|
||||
)
|
||||
assert (
|
||||
modifier[
|
||||
material.modifier_input_identifier(modifier.node_group, "Smooth Iterations")
|
||||
]
|
||||
== 0
|
||||
)
|
||||
assert len(result.data.vertices) > 4
|
||||
assert len(result.data.polygons) > 1
|
||||
assert all(polygon.use_smooth for polygon in result.data.polygons)
|
||||
@@ -1242,4 +1223,8 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
copied = np.empty(34 * 64 * 4, dtype=np.float32)
|
||||
color_texture.image.pixels.foreach_get(copied)
|
||||
copied = copied.reshape((34, 64, 4))
|
||||
assert np.allclose(copied, 1.0)
|
||||
assert np.allclose(copied[:, :, :3], 1.0)
|
||||
left, top, right, bottom = region_mask.bounds
|
||||
assert np.allclose(
|
||||
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
|
||||
)
|
||||
|
||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@@ -40,24 +39,17 @@ class PackTest(unittest.TestCase):
|
||||
pack.DIST_DIR = original_dist
|
||||
|
||||
version = tomllib.loads(
|
||||
(pack.SOURCE_DIR / "blender_manifest.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
(pack.SOURCE_DIR / "blender_manifest.toml").read_text(encoding="utf-8")
|
||||
)["version"]
|
||||
self.assertEqual(
|
||||
{path.name for path in output_paths},
|
||||
{
|
||||
f"AnyImage.v{version}.{platform}.zip"
|
||||
for platform in pack.PLATFORMS
|
||||
},
|
||||
{f"AnyImage.v{version}.{platform}.zip" for platform in pack.PLATFORMS},
|
||||
)
|
||||
for output_path in output_paths:
|
||||
platform = output_path.stem.split(".")[-1]
|
||||
with ZipFile(output_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
manifest = archive.read("blender_manifest.toml").decode(
|
||||
"utf-8"
|
||||
)
|
||||
manifest = archive.read("blender_manifest.toml").decode("utf-8")
|
||||
|
||||
self.assertIn("__init__.py", names)
|
||||
self.assertIn("operators/clipboard_image/__init__.py", names)
|
||||
@@ -72,18 +64,14 @@ class PackTest(unittest.TestCase):
|
||||
"operators/common/image_interaction.py",
|
||||
"operators/common/image_region.py",
|
||||
"operators/common/mesh_result.py",
|
||||
"operators/convert_to_plane/normal_map.py",
|
||||
"operators/convert_to_plane/resolution.py",
|
||||
"operators/cutout_tool/__init__.py",
|
||||
"operators/cutout_tool/select_region.py",
|
||||
"operators/cutout_tool/region_to_shape.py",
|
||||
"operators/cutout_tool/geometry/__init__.py",
|
||||
"operators/cutout_tool/geometry/balloon.py",
|
||||
"operators/cutout_tool/geometry/depth_calibration.py",
|
||||
"operators/cutout_tool/geometry/region_contours.py",
|
||||
"operators/cutout_tool/geometry/triangulation.py",
|
||||
"operators/cutout_tool/main.py",
|
||||
"operators/cutout_tool/interaction.py",
|
||||
"operators/cutout_tool/geometry.py",
|
||||
"operators/cutout_tool/object.py",
|
||||
"operators/cutout_tool/shape.py",
|
||||
"operators/convert_to_plane/__init__.py",
|
||||
"operators/convert_to_plane/build.py",
|
||||
"operators/convert_to_plane/object.py",
|
||||
"operators/convert_to_plane/conversion.py",
|
||||
"operators/convert_to_plane/mesh_plane.py",
|
||||
"operators/convert_to_plane/nodes.py",
|
||||
@@ -109,15 +97,20 @@ class PackTest(unittest.TestCase):
|
||||
names,
|
||||
)
|
||||
self.assertIn("server/media/input.py", names)
|
||||
self.assertIn("operators/common/depth_data.py", names)
|
||||
self.assertIn("server/geometry/projection.py", names)
|
||||
self.assertIn("server/models/geometry.py", names)
|
||||
self.assertIn("server/jobs/remove_background.py", names)
|
||||
self.assertIn("server/jobs/refine_image_mask.py", names)
|
||||
self.assertIn("server/region/generate_selection.py", names)
|
||||
self.assertIn("server/jobs/generate_region_data.py", names)
|
||||
self.assertIn("server/jobs/generate_depth_plane.py", names)
|
||||
self.assertIn("server/geometry/generate_depth.py", names)
|
||||
self.assertIn("server/jobs/refine_image_region.py", names)
|
||||
self.assertIn("server/region/refine_region.py", names)
|
||||
self.assertIn("server/jobs/cutout.py", names)
|
||||
self.assertIn("server/jobs/depth_plane.py", names)
|
||||
self.assertIn("server/geometry/depth_texture.py", names)
|
||||
self.assertIn("server/geometry/prediction_artifacts.py", names)
|
||||
self.assertIn("server/geometry/normal_texture.py", names)
|
||||
self.assertNotIn(
|
||||
"server/geometry/generate_geometry_and_normal.py", names
|
||||
)
|
||||
self.assertNotIn("operators/common/image_selection.py", names)
|
||||
self.assertNotIn("server/selection/cutout_images.py", names)
|
||||
self.assertNotIn("server/selection/polygon_mask.py", names)
|
||||
@@ -139,21 +132,13 @@ class PackTest(unittest.TestCase):
|
||||
"wheels/blendjob-0.1.14-py3-none-any.whl",
|
||||
names,
|
||||
)
|
||||
self.assertFalse(
|
||||
any(name.startswith("blendjob/") for name in names)
|
||||
)
|
||||
self.assertFalse(any(name.startswith("blendjob/") for name in names))
|
||||
self.assertNotIn("anyimage/__init__.py", names)
|
||||
self.assertFalse(
|
||||
any(name.startswith("sources/") for name in names)
|
||||
)
|
||||
self.assertFalse(
|
||||
any(name.startswith("vendor/") for name in names)
|
||||
)
|
||||
self.assertFalse(any(name.startswith("sources/") for name in names))
|
||||
self.assertFalse(any(name.startswith("vendor/") for name in names))
|
||||
self.assertIn(f'platforms = ["{platform}"]', manifest)
|
||||
self.assertNotIn(
|
||||
"windows-x64"
|
||||
if platform != "windows-x64"
|
||||
else "macos-arm64",
|
||||
"windows-x64" if platform != "windows-x64" else "macos-arm64",
|
||||
manifest.split("platforms =", 1)[-1],
|
||||
)
|
||||
|
||||
@@ -168,9 +153,7 @@ class PackTest(unittest.TestCase):
|
||||
|
||||
def fake_run(command, check):
|
||||
self.assertTrue(check)
|
||||
(destination / "blendjob-0.1.14-py3-none-any.whl").write_bytes(
|
||||
b"wheel"
|
||||
)
|
||||
(destination / "blendjob-0.1.14-py3-none-any.whl").write_bytes(b"wheel")
|
||||
|
||||
with patch.object(pack.subprocess, "run", side_effect=fake_run) as run:
|
||||
wheels = pack.download_wheels("linux-x64", destination)
|
||||
@@ -185,6 +168,7 @@ class PackTest(unittest.TestCase):
|
||||
[wheel.name for wheel in wheels],
|
||||
["blendjob-0.1.14-py3-none-any.whl"],
|
||||
)
|
||||
|
||||
def test_rejects_unknown_platform(self):
|
||||
pack = load_pack()
|
||||
with self.assertRaisesRegex(ValueError, "Unsupported platform"):
|
||||
@@ -193,9 +177,7 @@ class PackTest(unittest.TestCase):
|
||||
def test_platforms_match_manifest(self):
|
||||
pack = load_pack()
|
||||
manifest = tomllib.loads(
|
||||
(pack.SOURCE_DIR / "blender_manifest.toml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
(pack.SOURCE_DIR / "blender_manifest.toml").read_text(encoding="utf-8")
|
||||
)
|
||||
self.assertEqual(set(pack.PLATFORMS), set(manifest["platforms"]))
|
||||
self.assertEqual(
|
||||
@@ -203,5 +185,6 @@ class PackTest(unittest.TestCase):
|
||||
{"windows-x64", "macos-arm64", "linux-x64"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -41,8 +42,8 @@ class ServerTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server_module = load_server_module()
|
||||
importlib.import_module("server.geometry.generate_depth")
|
||||
importlib.import_module("server.jobs.generate_region_data")
|
||||
importlib.import_module("server.geometry.prediction_artifacts")
|
||||
importlib.import_module("server.jobs.cutout")
|
||||
from blendjob import JobServer
|
||||
from server.model_manager import ModelManager, ModelSessionCache
|
||||
|
||||
@@ -258,11 +259,11 @@ class ServerTest(unittest.TestCase):
|
||||
{
|
||||
"download-model",
|
||||
"download-required-models",
|
||||
"refine-image-mask",
|
||||
"refine-image-region",
|
||||
"remove-background",
|
||||
"upscale-image",
|
||||
"generate-depth-plane",
|
||||
"generate-region-data",
|
||||
"generate-depth-plane-geometry",
|
||||
"generate-cutout-artifacts",
|
||||
"debug-da3",
|
||||
"debug-moge2",
|
||||
"debug-ben2",
|
||||
@@ -386,23 +387,12 @@ class ServerTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(events, ["clear", "close"])
|
||||
|
||||
def test_region_transparent_fraction_uses_visible_alpha_threshold(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
depth = sys.modules["server.geometry.generate_depth"]
|
||||
output_path = self.storage_root / "region.png"
|
||||
pixels = np.full((10, 10, 4), 255, dtype=np.uint8)
|
||||
pixels.reshape(-1, 4)[:5, 3] = 25
|
||||
Image.fromarray(pixels, mode="RGBA").save(output_path)
|
||||
|
||||
self.assertEqual(depth.transparent_fraction(output_path), 0.05)
|
||||
|
||||
def test_image_geometry_keeps_model_and_region_validity_separate(self):
|
||||
def test_depth_artifacts_store_texture_and_compact_metadata(self):
|
||||
import numpy as np
|
||||
|
||||
depth = sys.modules["server.geometry.generate_depth"]
|
||||
output_path = self.storage_root / "image_geometry.npz"
|
||||
depth = importlib.import_module("server.geometry.depth_texture")
|
||||
texture_path = self.storage_root / "z-depth.exr"
|
||||
metadata_path = self.storage_root / "depth.json"
|
||||
valid = np.asarray(((True, True, False), (True, True, True)))
|
||||
calibration = np.asarray(((False, True, False), (False, False, True)))
|
||||
frame = SimpleNamespace(
|
||||
@@ -413,19 +403,60 @@ class ServerTest(unittest.TestCase):
|
||||
model_mask=None,
|
||||
)
|
||||
|
||||
depth._write_image_geometry_archive(frame, output_path, calibration, 0.05)
|
||||
depth.write_z_depth_texture(frame.depth, frame.valid_mask, texture_path)
|
||||
depth.write_depth_metadata(frame, calibration, metadata_path)
|
||||
|
||||
with np.load(output_path) as archive:
|
||||
np.testing.assert_array_equal(archive["valid"], valid)
|
||||
np.testing.assert_array_equal(archive["calibration_valid"], calibration)
|
||||
self.assertAlmostEqual(
|
||||
float(archive["region_transparent_fraction"]),
|
||||
0.05,
|
||||
places=6,
|
||||
)
|
||||
self.assertTrue(texture_path.is_file())
|
||||
self.assertEqual(texture_path.read_bytes()[:4], b"v/1\x01")
|
||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(metadata["image_size"], [3, 2])
|
||||
self.assertEqual(metadata["intrinsics"], np.eye(3).tolist())
|
||||
self.assertAlmostEqual(metadata["reference_depth"], 4.0)
|
||||
|
||||
def test_generate_region_data_only_returns_requested_outputs(self):
|
||||
job = sys.modules["server.jobs.generate_region_data"]
|
||||
def test_object_normal_texture_matches_depth_surface_axes(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
normal_texture = importlib.import_module("server.geometry.normal_texture")
|
||||
output_path = self.storage_root / "object-normal.png"
|
||||
camera_normals = np.asarray(
|
||||
[[
|
||||
(0.0, 0.0, -1.0),
|
||||
(1.0, 0.0, 0.0),
|
||||
(0.0, 1.0, 0.0),
|
||||
(0.0, 0.0, -1.0),
|
||||
]],
|
||||
dtype=np.float32,
|
||||
)
|
||||
valid = np.asarray([[True, True, True, False]], dtype=bool)
|
||||
|
||||
normal_texture.write_camera_normal_texture(
|
||||
camera_normals,
|
||||
valid,
|
||||
(0, 0),
|
||||
(4, 1),
|
||||
(4, 1),
|
||||
output_path,
|
||||
normal_mode="OBJECT",
|
||||
)
|
||||
|
||||
with Image.open(output_path) as opened:
|
||||
encoded = np.asarray(opened, dtype=np.uint8)
|
||||
np.testing.assert_array_equal(
|
||||
encoded,
|
||||
np.asarray(
|
||||
[[
|
||||
(0, 127, 127),
|
||||
(127, 0, 127),
|
||||
(127, 127, 0),
|
||||
(0, 128, 128),
|
||||
]],
|
||||
dtype=np.uint8,
|
||||
),
|
||||
)
|
||||
|
||||
def test_generate_cutout_artifacts_only_returns_requested_outputs(self):
|
||||
job = sys.modules["server.jobs.cutout"]
|
||||
input_path = self.storage_root / "input.png"
|
||||
input_path.write_bytes(b"region")
|
||||
context = SimpleNamespace(
|
||||
@@ -433,40 +464,49 @@ class ServerTest(unittest.TestCase):
|
||||
progress=lambda *_args: None,
|
||||
check_cancelled=lambda: None,
|
||||
)
|
||||
archive = context.directory / "image_geometry.npz"
|
||||
normal = context.directory / "normal.png"
|
||||
vector_depth = context.directory / "vector-depth.exr"
|
||||
metadata = context.directory / "depth.json"
|
||||
normal = context.directory / "object-normal.png"
|
||||
with patch.object(
|
||||
job,
|
||||
"generate_depth",
|
||||
return_value=(archive, normal),
|
||||
"generate_moge2_artifacts",
|
||||
return_value={
|
||||
"vector_depth": vector_depth,
|
||||
"depth_metadata": metadata,
|
||||
"object_normal": normal,
|
||||
},
|
||||
) as generate:
|
||||
result = job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
"output_depth": True,
|
||||
"depth_mode": "VECTOR",
|
||||
"normal_mode": "OBJECT",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
{"image_geometry": "image_geometry.npz", "normal": "normal.png"},
|
||||
{
|
||||
"vector_depth": "vector-depth.exr",
|
||||
"depth_metadata": "depth.json",
|
||||
"object_normal": "object-normal.png",
|
||||
},
|
||||
)
|
||||
generate.assert_called_once_with(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
"output_depth": True,
|
||||
"depth_mode": "VECTOR",
|
||||
"normal_mode": "OBJECT",
|
||||
},
|
||||
input_path,
|
||||
output_depth=True,
|
||||
depth_mode="VECTOR",
|
||||
normal_mode="OBJECT",
|
||||
)
|
||||
|
||||
def test_generate_region_data_refines_an_already_rasterized_region(self):
|
||||
job = sys.modules["server.jobs.generate_region_data"]
|
||||
def test_generate_cutout_artifacts_refine_an_already_rasterized_region(self):
|
||||
job = sys.modules["server.jobs.cutout"]
|
||||
input_path = self.storage_root / "input.png"
|
||||
input_path.write_bytes(b"region")
|
||||
directory = self.storage_root / "job"
|
||||
@@ -480,7 +520,7 @@ class ServerTest(unittest.TestCase):
|
||||
output.write_bytes(b"refined")
|
||||
return output, (2, 3, 8, 9)
|
||||
|
||||
with patch.object(job, "generate_selection", side_effect=generate):
|
||||
with patch.object(job, "refine_region", side_effect=generate):
|
||||
result = job.run(
|
||||
context,
|
||||
{
|
||||
@@ -493,7 +533,7 @@ class ServerTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
{"region": "region.png", "region_bounds": "region_bounds.json"},
|
||||
{"region_image": "region.png", "region_bounds": "region_bounds.json"},
|
||||
)
|
||||
self.assertEqual(
|
||||
(directory / "region_bounds.json").read_text(encoding="utf-8"),
|
||||
|
||||
Reference in New Issue
Block a user