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:
2026-08-31 11:36:27 +08:00
parent 08309f719d
commit 87d7721128
80 changed files with 4823 additions and 4297 deletions

View File

@@ -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()

View File

@@ -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")

View File

@@ -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",

View File

@@ -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

View File

@@ -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])
)

View File

@@ -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()

View File

@@ -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"),