Add image selection refinement functionality with BEN2 integration
- Implemented viewport.py for handling 3D view projections, gestures, and overlay drawing. - Created refine_image_selection.py to manage the refinement of gesture masks and output image bounds. - Added selection module with refine_selection.py to process and crop images based on alpha channel visibility. - Introduced __init__.py for selection module to expose refine_selection function.
This commit is contained in:
@@ -84,7 +84,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
cls.anyimage = importlib.import_module("anyimage")
|
||||
cls.runtime = importlib.import_module("anyimage.runtime")
|
||||
cls.ai_setup = importlib.import_module("anyimage.operators.ai_setup")
|
||||
cls.image_data = importlib.import_module("anyimage.common.image_data")
|
||||
cls.ai = importlib.import_module("anyimage.common.ai")
|
||||
cls.image_data = importlib.import_module("anyimage.common.image")
|
||||
cls.debug_operators = importlib.import_module("anyimage.operators.debug")
|
||||
cls.convert_to_plane = importlib.import_module(
|
||||
"anyimage.operators.convert_to_plane.conversion"
|
||||
@@ -96,15 +97,11 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"anyimage.operators.remove_background"
|
||||
)
|
||||
cls.upscale = importlib.import_module("anyimage.operators.upscale")
|
||||
cls.image_interaction = importlib.import_module(
|
||||
"anyimage.common.image_interaction"
|
||||
)
|
||||
cls.image_interaction = importlib.import_module("anyimage.common.viewport")
|
||||
cls.edit_mask = importlib.import_module(
|
||||
"anyimage.operators.image_tool.edit_mask"
|
||||
)
|
||||
cls.image_region = importlib.import_module(
|
||||
"anyimage.common.image_region"
|
||||
)
|
||||
cls.image_selection = importlib.import_module("anyimage.common.selection")
|
||||
cls.image_box = importlib.import_module("anyimage.operators.image_tool.box")
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.image_tool.lasso")
|
||||
cls.image_line = importlib.import_module("anyimage.operators.image_tool.line")
|
||||
@@ -218,8 +215,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
for operator, identifier in (
|
||||
(self.anyimage.operators.GenerateDepthPlane, "anyimage.generate_depth_plane"),
|
||||
(
|
||||
self.anyimage.operators.CutoutRegionToShape,
|
||||
"anyimage.cutout_region_to_shape",
|
||||
self.anyimage.operators.CutoutSelectionToShape,
|
||||
"anyimage.cutout_selection_to_shape",
|
||||
),
|
||||
):
|
||||
self.assertEqual(operator.bl_idname, identifier)
|
||||
@@ -837,7 +834,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
operator = self.convert_to_plane.ConvertToPlane
|
||||
self.assertIn(operator, self.anyimage.CLASSES)
|
||||
self.assertIn(self.convert_to_plane.ConvertToDepthPlane, self.anyimage.CLASSES)
|
||||
self.assertIn(self.cutout_main.CutoutRegionToShape, self.anyimage.CLASSES)
|
||||
self.assertIn(self.cutout_main.CutoutSelectionToShape, self.anyimage.CLASSES)
|
||||
self.assertEqual(operator.bl_idname, "anyimage.convert_to_plane")
|
||||
self.assertNotIn("shadeless", operator.__annotations__)
|
||||
self.assertNotIn("keep_original", operator.__annotations__)
|
||||
@@ -926,7 +923,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
context = SimpleNamespace(
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None))
|
||||
)
|
||||
region_mask = self.cutout_main.RegionMask(
|
||||
selection_mask = self.cutout_main.SelectionMask(
|
||||
np.ones((8, 8), dtype=np.float32), (1, 1, 9, 9)
|
||||
)
|
||||
source_rgba = np.ones((10, 10, 4), dtype=np.float32)
|
||||
@@ -934,13 +931,13 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
with self.subTest(shape=shape):
|
||||
operator = SimpleNamespace(
|
||||
source_object_name="Source",
|
||||
region_path_json=json.dumps(
|
||||
selection_path_json=json.dumps(
|
||||
{"points": [[1, 1], [9, 1], [9, 9], [1, 9]]}
|
||||
),
|
||||
shape=shape,
|
||||
mesh_resolution=64,
|
||||
generate_normal=False,
|
||||
refine_region=False,
|
||||
refine_selection=False,
|
||||
report=lambda *_args: None,
|
||||
_needs_generation=lambda: False,
|
||||
)
|
||||
@@ -958,8 +955,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
"rasterize_region_path",
|
||||
return_value=region_mask,
|
||||
"rasterize_selection_path",
|
||||
return_value=selection_mask,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
@@ -975,7 +972,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.cutout_main, "create_cutout_shape"
|
||||
) as convert,
|
||||
):
|
||||
result = self.cutout_main.CutoutRegionToShape.execute(
|
||||
result = self.cutout_main.CutoutSelectionToShape.execute(
|
||||
operator, context
|
||||
)
|
||||
|
||||
@@ -985,12 +982,12 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
source,
|
||||
shape,
|
||||
64,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
source_rgba=source_rgba,
|
||||
)
|
||||
|
||||
def test_cutout_request_describes_ai_capabilities(self):
|
||||
operator = self.cutout_main.CutoutRegionToShape()
|
||||
operator = self.cutout_main.CutoutSelectionToShape()
|
||||
operator.input_path = __file__
|
||||
operator.source_object_name = "Source"
|
||||
operator.shape = "DEPTH_SURFACE"
|
||||
@@ -998,7 +995,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
operator.resolution_level = 5
|
||||
operator.max_ai_input_size = 2048
|
||||
operator.generate_normal = True
|
||||
operator.refine_region = True
|
||||
operator.refine_selection = True
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
|
||||
)
|
||||
@@ -1021,7 +1018,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
[call.args[0] for call in require_model.call_args_list],
|
||||
[self.anyimage.properties.BEN2_MODEL_KEY, operator.model],
|
||||
)
|
||||
self.assertTrue(parameters["refine_region"])
|
||||
self.assertTrue(parameters["refine_selection"])
|
||||
self.assertEqual(parameters["depth_mode"], "VECTOR")
|
||||
self.assertEqual(parameters["normal_mode"], "OBJECT")
|
||||
self.assertNotIn("polygon", parameters)
|
||||
@@ -1290,7 +1287,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
def test_cutout_contour_is_smooth_and_evenly_resampled(self):
|
||||
import numpy as np
|
||||
|
||||
contour = self.cutout_geometry.smooth_region_contour(
|
||||
contour = self.cutout_geometry.smooth_selection_contour(
|
||||
((0, 0), (3, 0), (4, 0), (4, 2), (0, 2)),
|
||||
0.25,
|
||||
)
|
||||
@@ -1309,7 +1306,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
staircase.append((value + 1.0, float(value)))
|
||||
staircase.append((value + 1.0, value + 1.0))
|
||||
staircase.append((0.0, 16.0))
|
||||
smooth = self.cutout_geometry.smooth_region_contour(staircase, 2.0)
|
||||
smooth = self.cutout_geometry.smooth_selection_contour(staircase, 2.0)
|
||||
|
||||
def diagonal_noise(points):
|
||||
points = np.asarray(points)
|
||||
@@ -1322,7 +1319,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
|
||||
self.assertLess(diagonal_noise(smooth), 0.05)
|
||||
|
||||
def test_region_mask_keeps_nonzero_winding_overlap(self):
|
||||
def test_selection_mask_keeps_nonzero_winding_overlap(self):
|
||||
path = (
|
||||
(1, 1),
|
||||
(7, 1),
|
||||
@@ -1337,13 +1334,13 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
(1, 1),
|
||||
)
|
||||
|
||||
even_odd = self.image_region.rasterize_region_path(
|
||||
even_odd = self.image_selection.rasterize_selection_path(
|
||||
(12, 9),
|
||||
self.image_region.RegionPath(points=path, fill_rule="EVEN_ODD"),
|
||||
self.image_selection.SelectionPath(points=path, fill_rule="EVEN_ODD"),
|
||||
).full_values((12, 9))
|
||||
nonzero = self.image_region.rasterize_region_path(
|
||||
nonzero = self.image_selection.rasterize_selection_path(
|
||||
(12, 9),
|
||||
self.image_region.RegionPath(points=path, fill_rule="NONZERO"),
|
||||
self.image_selection.SelectionPath(points=path, fill_rule="NONZERO"),
|
||||
).full_values((12, 9))
|
||||
|
||||
self.assertFalse(even_odd[3, 5])
|
||||
@@ -1397,7 +1394,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
def test_image_edit_tools_and_operators_are_registered(self):
|
||||
operator = self.edit_mask.EditImageMask
|
||||
self.assertIn(operator, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.RefineImageRegion, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.RefineImageSelection, self.anyimage.CLASSES)
|
||||
self.assertIn(self.edit_mask.ActivateImageTool, self.anyimage.CLASSES)
|
||||
tool_cases = (
|
||||
(self.image_box.ImageBoxTool, "BOX", "ops.sculpt.border_mask"),
|
||||
@@ -1422,12 +1419,12 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
layout = SimpleNamespace(
|
||||
prop=lambda _settings, name: drawn_settings.append(name)
|
||||
)
|
||||
with patch.object(self.image_box, "draw_region_settings") as draw:
|
||||
with patch.object(self.image_box, "draw_selection_settings") as draw:
|
||||
self.image_box.ImageBoxTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(context, layout, "image_box_refine_region")
|
||||
with patch.object(self.image_lasso, "draw_region_settings") as draw:
|
||||
draw.assert_called_once_with(context, layout, "image_box_refine_selection")
|
||||
with patch.object(self.image_lasso, "draw_selection_settings") as draw:
|
||||
self.image_lasso.ImageLassoTool.draw_settings(context, layout, None)
|
||||
draw.assert_called_once_with(context, layout, "image_lasso_refine_region")
|
||||
draw.assert_called_once_with(context, layout, "image_lasso_refine_selection")
|
||||
cutout_tool = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool"
|
||||
).CutoutTool
|
||||
@@ -1453,9 +1450,9 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
drawn_settings,
|
||||
[
|
||||
"cutout_refine_region",
|
||||
"cutout_point_spacing",
|
||||
"cutout_refine_selection",
|
||||
"cutout_generate_normal",
|
||||
"cutout_mesh_detail",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("draw_settings", self.image_line.ImageLineTool.__dict__)
|
||||
@@ -1468,24 +1465,33 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
settings = self.anyimage.properties.AnyImageSettings.__annotations__
|
||||
for name in (
|
||||
"cutout_refine_region",
|
||||
"image_box_refine_region",
|
||||
"image_lasso_refine_region",
|
||||
"cutout_refine_selection",
|
||||
"image_box_refine_selection",
|
||||
"image_lasso_refine_selection",
|
||||
):
|
||||
self.assertFalse(settings[name]["default"])
|
||||
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.assertFalse(settings["cutout_generate_normal"]["default"])
|
||||
self.assertEqual(settings["cutout_refine_region"]["name"], "Fit to Foreground")
|
||||
self.assertNotIn("refine_selection", settings)
|
||||
self.assertNotIn("image_edit_refine_selection", settings)
|
||||
self.assertEqual(settings["cutout_mesh_detail"]["default"], "LOW")
|
||||
self.assertEqual(
|
||||
settings["cutout_point_spacing"]["name"], "Mesh Sampling Spacing"
|
||||
[item[:2] for item in settings["cutout_mesh_detail"]["items"]],
|
||||
[
|
||||
("LOW", "Low"),
|
||||
("MEDIUM", "Medium"),
|
||||
("HIGH", "High"),
|
||||
("ULTRA", "Ultra"),
|
||||
],
|
||||
)
|
||||
self.assertFalse(settings["cutout_generate_normal"]["default"])
|
||||
self.assertEqual(
|
||||
settings["cutout_refine_selection"]["name"], "Refine Selection"
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["cutout_mesh_detail"]["name"], "Mesh Detail"
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["cutout_generate_normal"]["name"],
|
||||
"Generate Normal Map",
|
||||
"Normal Map",
|
||||
)
|
||||
self.assertNotIn("image_extract_continuous", settings)
|
||||
self.assertNotIn("image_extract_padding", settings)
|
||||
@@ -1522,10 +1528,10 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
"ready": False,
|
||||
}
|
||||
setup = ("operator", "anyimage.setup_ai_environment", True)
|
||||
with patch.object(self.image_interaction, "ai_status", return_value=missing):
|
||||
with patch.object(self.ai, "ai_status", return_value=missing):
|
||||
for tool, setting_name in (
|
||||
(self.image_box.ImageBoxTool, "image_box_refine_region"),
|
||||
(self.image_lasso.ImageLassoTool, "image_lasso_refine_region"),
|
||||
(self.image_box.ImageBoxTool, "image_box_refine_selection"),
|
||||
(self.image_lasso.ImageLassoTool, "image_lasso_refine_selection"),
|
||||
):
|
||||
layout = Layout()
|
||||
tool.draw_settings(context, layout, None)
|
||||
@@ -1543,71 +1549,71 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
layout.events,
|
||||
[
|
||||
setup,
|
||||
("prop", "cutout_refine_region", False),
|
||||
("prop", "cutout_point_spacing", True),
|
||||
("prop", "cutout_refine_selection", False),
|
||||
("prop", "cutout_generate_normal", False),
|
||||
("prop", "cutout_mesh_detail", True),
|
||||
],
|
||||
)
|
||||
|
||||
layout = Layout()
|
||||
operator = SimpleNamespace(gesture="BOX")
|
||||
self.image_interaction.draw_region_operator_settings(layout, operator)
|
||||
self.image_interaction.draw_selection_operator_settings(layout, operator)
|
||||
self.assertEqual(
|
||||
layout.events,
|
||||
[
|
||||
setup,
|
||||
("prop", "refine_region", False),
|
||||
("prop", "refine_selection", False),
|
||||
("prop", "invert", True),
|
||||
],
|
||||
)
|
||||
|
||||
ready = {"environment_ready": True, "missing_models": (), "ready": True}
|
||||
with patch.object(self.image_interaction, "ai_status", return_value=ready):
|
||||
with patch.object(self.ai, "ai_status", return_value=ready):
|
||||
layout = Layout()
|
||||
cutout_tool.draw_settings(context, layout, None)
|
||||
self.assertNotIn(setup, layout.events)
|
||||
self.assertTrue(all(event[2] for event in layout.events))
|
||||
|
||||
def test_refine_region_is_independent_for_each_area_tool(self):
|
||||
def test_refine_selection_is_independent_for_each_area_tool(self):
|
||||
settings = SimpleNamespace(
|
||||
cutout_refine_region=True,
|
||||
image_box_refine_region=False,
|
||||
image_lasso_refine_region=True,
|
||||
cutout_refine_selection=True,
|
||||
image_box_refine_selection=False,
|
||||
image_lasso_refine_selection=True,
|
||||
)
|
||||
context = SimpleNamespace(scene=SimpleNamespace(anyimage_settings=settings))
|
||||
with patch.object(self.image_interaction, "ai_ready", return_value=True):
|
||||
for gesture in ("LINE", "POLYLINE"):
|
||||
operator = SimpleNamespace(
|
||||
gesture=gesture,
|
||||
refine_region=True,
|
||||
refine_selection=True,
|
||||
)
|
||||
self.image_interaction.apply_region_settings(operator, context)
|
||||
self.assertFalse(operator.refine_region)
|
||||
self.image_interaction.apply_selection_settings(operator, context)
|
||||
self.assertFalse(operator.refine_selection)
|
||||
|
||||
box = SimpleNamespace(
|
||||
gesture="BOX",
|
||||
refine_region=True,
|
||||
refine_region_settings={"BOX": "image_box_refine_region"},
|
||||
refine_selection=True,
|
||||
refine_selection_settings={"BOX": "image_box_refine_selection"},
|
||||
)
|
||||
self.image_interaction.apply_region_settings(box, context)
|
||||
self.assertFalse(box.refine_region)
|
||||
self.image_interaction.apply_selection_settings(box, context)
|
||||
self.assertFalse(box.refine_selection)
|
||||
|
||||
lasso = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
refine_region=False,
|
||||
refine_region_settings={"LASSO": "image_lasso_refine_region"},
|
||||
refine_selection=False,
|
||||
refine_selection_settings={"LASSO": "image_lasso_refine_selection"},
|
||||
)
|
||||
self.image_interaction.apply_region_settings(lasso, context)
|
||||
self.assertTrue(lasso.refine_region)
|
||||
self.image_interaction.apply_selection_settings(lasso, context)
|
||||
self.assertTrue(lasso.refine_selection)
|
||||
|
||||
cutout = SimpleNamespace(
|
||||
gesture="LASSO",
|
||||
active_tool_id="anyimage.cutout_lasso",
|
||||
refine_region=False,
|
||||
refine_region_settings={"LASSO": "cutout_refine_region"},
|
||||
refine_selection=False,
|
||||
refine_selection_settings={"LASSO": "cutout_refine_selection"},
|
||||
)
|
||||
self.image_interaction.apply_region_settings(cutout, context)
|
||||
self.assertTrue(cutout.refine_region)
|
||||
self.image_interaction.apply_selection_settings(cutout, context)
|
||||
self.assertTrue(cutout.refine_selection)
|
||||
|
||||
def test_image_edit_first_click_selects_another_image_without_editing(self):
|
||||
selected = []
|
||||
@@ -1981,7 +1987,7 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
self.assertNotIn("image_perspective_aspect_ratio", settings)
|
||||
self.assertNotIn("aspect_mode", operator.__annotations__)
|
||||
self.assertIn("aspect_ratio", operator.__annotations__)
|
||||
self.assertNotIn("refine_region", operator.__annotations__)
|
||||
self.assertNotIn("refine_selection", operator.__annotations__)
|
||||
self.assertNotIn("continuous", operator.__annotations__)
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
|
||||
@@ -1995,14 +2001,14 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
(),
|
||||
)
|
||||
|
||||
def test_image_box_completion_serializes_the_image_region(self):
|
||||
def test_image_box_completion_serializes_the_image_selection(self):
|
||||
source = SimpleNamespace(data=SimpleNamespace(size=(64, 32)))
|
||||
finished = []
|
||||
operator = SimpleNamespace(
|
||||
source_object_name="Source",
|
||||
source_matrix_data="matrix",
|
||||
gesture="BOX",
|
||||
region_path_json="",
|
||||
selection_path_json="",
|
||||
invert=False,
|
||||
preview_fill_rule="EVEN_ODD",
|
||||
_screen_path=lambda: ((1, 1), (8, 1), (8, 6), (1, 6)),
|
||||
@@ -2024,16 +2030,16 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
return_value=polygon,
|
||||
),
|
||||
):
|
||||
result = self.image_interaction.ImageRegionGesture._complete(operator, context)
|
||||
result = self.image_interaction.ImageSelectionGesture._complete(operator, context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
self.assertEqual(
|
||||
json.loads(operator.region_path_json)["points"],
|
||||
json.loads(operator.selection_path_json)["points"],
|
||||
[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
|
||||
)
|
||||
self.assertEqual(finished, [context])
|
||||
|
||||
def test_image_region_uses_white_dashed_segments(self):
|
||||
def test_image_selection_uses_white_dashed_segments(self):
|
||||
segments = self.image_interaction.dashed_line_segments(
|
||||
((0, 0), (10, 0)), dash_length=3.0, gap_length=2.0
|
||||
)
|
||||
@@ -2073,8 +2079,8 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
patch.object(self.image_interaction, "draw_polygon_fill") as draw_fill,
|
||||
patch.object(self.image_interaction, "draw_dashed_line") as draw_outline,
|
||||
):
|
||||
self.image_interaction.ImageRegionGesture._draw_region(operator)
|
||||
self.image_interaction.ImageRegionGesture._draw_region(operator)
|
||||
self.image_interaction.ImageSelectionGesture._draw_selection(operator)
|
||||
self.image_interaction.ImageSelectionGesture._draw_selection(operator)
|
||||
|
||||
build_fill.assert_called_once_with(polygon, "NONZERO")
|
||||
build_outline.assert_called_once_with((*polygon, polygon[0]))
|
||||
@@ -2131,11 +2137,11 @@ class BlenderAddonTest(unittest.TestCase):
|
||||
((0, 5), (10, 5)), (10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_region.region_path_bounds(preview),
|
||||
self.image_selection.selection_path_bounds(preview),
|
||||
(0, 5, 10, 10),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_region.region_path_bounds(image),
|
||||
self.image_selection.selection_path_bounds(image),
|
||||
(0, 0, 10, 5),
|
||||
)
|
||||
|
||||
|
||||
@@ -453,6 +453,28 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
|
||||
self.assertIs(result, expected_reference)
|
||||
|
||||
def test_reference_image_uses_drag_and_drop_size(self):
|
||||
reference = SimpleNamespace(select_set=Mock())
|
||||
objects = SimpleNamespace(new=Mock(return_value=reference))
|
||||
context = SimpleNamespace(
|
||||
mode="OBJECT",
|
||||
collection=SimpleNamespace(objects=SimpleNamespace(link=Mock())),
|
||||
selected_objects=(),
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None)),
|
||||
region_data=None,
|
||||
)
|
||||
image = SimpleNamespace(name="Clipboard", size=(1920, 1080))
|
||||
|
||||
with patch.object(self.actions.bpy.data, "objects", objects, create=True):
|
||||
result = self.actions.add_reference_image(
|
||||
context,
|
||||
image,
|
||||
location=(1.0, 2.0, 3.0),
|
||||
)
|
||||
|
||||
self.assertIs(result, reference)
|
||||
self.assertEqual(reference.empty_display_size, 5.0)
|
||||
|
||||
def test_only_import_as_and_shadeless_are_drawn_for_plane_paste(self):
|
||||
operator = self.addon.clipboard_image.PasteClipboardImage()
|
||||
drawn_properties = []
|
||||
|
||||
@@ -10,12 +10,12 @@ import numpy as np
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
from anyimage.common.selection import (
|
||||
SelectionMask,
|
||||
SelectionPath,
|
||||
rasterize_selection_path,
|
||||
)
|
||||
from anyimage.common.depth_data import (
|
||||
from anyimage.common.depth import (
|
||||
depth_uniform_scale,
|
||||
load_depth_metadata,
|
||||
)
|
||||
@@ -25,11 +25,13 @@ from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
||||
from anyimage.operators.cutout_tool.geometry import (
|
||||
RegionContours,
|
||||
extract_region_contours,
|
||||
SelectionContours,
|
||||
extract_selection_contours,
|
||||
poisson_balloon_profile,
|
||||
)
|
||||
from anyimage.server.models import moge2 as moge2_model
|
||||
from anyimage.server.models import da3 as da3_model
|
||||
from anyimage.server.models.geometry import GeometryFrame
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_uses_the_cropped_analysis_scale():
|
||||
@@ -193,53 +195,102 @@ def test_depth_surface_projection_uses_one_scale_for_all_camera_axes():
|
||||
assert np.isclose(reference_depth, 5.8, atol=1e-6)
|
||||
|
||||
|
||||
def test_moge_surface_field_uses_the_model_validity_mask():
|
||||
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.asarray((((True, False), (True, True)),)),
|
||||
intrinsics=np.asarray(
|
||||
(((1.0, 0.0, 0.5), (0.0, 1.0, 0.5), (0.0, 0.0, 1.0)),),
|
||||
def test_geometry_frame_requires_matching_float32_fields():
|
||||
with np.testing.assert_raises_regex(ValueError, "validity"):
|
||||
GeometryFrame(
|
||||
depth=np.ones((2, 2), dtype=np.float32),
|
||||
validity=np.ones((1, 2), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
)
|
||||
with np.testing.assert_raises_regex(ValueError, "normal"):
|
||||
GeometryFrame(
|
||||
depth=np.ones((2, 2), dtype=np.float32),
|
||||
validity=np.ones((2, 2), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
normal=np.ones((2, 2, 2), dtype=np.float32),
|
||||
)
|
||||
|
||||
|
||||
def test_moge_infer_returns_geometry_frames_with_model_validity():
|
||||
mask = np.asarray(((True, False), (True, True)))
|
||||
prediction = {
|
||||
"depth": np.ones((2, 2), dtype=np.float32),
|
||||
"normal": np.zeros((2, 2, 3), dtype=np.float32),
|
||||
"mask": mask,
|
||||
"intrinsics": np.asarray(
|
||||
((1.0, 0.0, 0.5), (0.0, 1.0, 0.5), (0.0, 0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
),
|
||||
points=np.ones((1, 2, 2, 3), dtype=np.float32),
|
||||
"points": np.ones((2, 2, 3), dtype=np.float32),
|
||||
}
|
||||
|
||||
with patch.object(moge2_model, "infer_one", return_value=prediction):
|
||||
frames = moge2_model.infer(None, {}, ["image"], include_points=True)
|
||||
|
||||
assert len(frames) == 1
|
||||
frame = frames[0]
|
||||
assert np.array_equal(frame.validity, mask.astype(np.float32))
|
||||
assert np.array_equal(frame.normal, prediction["normal"])
|
||||
assert np.array_equal(frame.points, prediction["points"])
|
||||
assert np.array_equal(
|
||||
frame.intrinsics,
|
||||
np.asarray(((2.0, 0.0, 1.0), (0.0, 2.0, 1.0), (0.0, 0.0, 1.0))),
|
||||
)
|
||||
|
||||
frame = moge2_model.geometry_frame(prediction)
|
||||
|
||||
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,
|
||||
)
|
||||
prediction = {
|
||||
"depth": np.ones((2, 2), dtype=np.float32),
|
||||
"normal": np.zeros((2, 2, 3), dtype=np.float32),
|
||||
"mask": np.ones((2, 2), dtype=bool),
|
||||
"intrinsics": np.eye(3, dtype=np.float32),
|
||||
"points": np.ones((2, 2, 3), dtype=np.float32),
|
||||
}
|
||||
|
||||
frame = moge2_model.geometry_frame(prediction)
|
||||
with patch.object(moge2_model, "infer_one", return_value=prediction):
|
||||
frame = moge2_model.infer(None, {}, ["image"], include_points=False)[0]
|
||||
|
||||
assert frame.points is None
|
||||
assert frame.valid_mask.all()
|
||||
assert frame.validity.all()
|
||||
|
||||
|
||||
def test_region_contours_group_holes_with_their_connected_component():
|
||||
def test_da3_geometry_frames_use_confidence_as_validity_and_derive_points():
|
||||
prediction = SimpleNamespace(
|
||||
depth=np.ones((2, 2, 3), dtype=np.float32),
|
||||
conf=np.asarray(
|
||||
(
|
||||
((0.25, 0.5, 0.75), (1.0, 0.5, 0.25)),
|
||||
((0.1, 0.2, 0.3), (0.4, 0.5, 0.6)),
|
||||
),
|
||||
dtype=np.float32,
|
||||
),
|
||||
intrinsics=np.repeat(np.eye(3, dtype=np.float32)[None], 2, axis=0),
|
||||
)
|
||||
|
||||
frames = da3_model.geometry_frames(prediction)
|
||||
|
||||
assert len(frames) == 2
|
||||
assert frames[0].normal is None
|
||||
assert np.array_equal(frames[1].validity, prediction.conf[1])
|
||||
assert frames[0].points.shape == (2, 3, 3)
|
||||
assert frames[0].points.dtype == np.float32
|
||||
|
||||
|
||||
def test_selection_contours_group_holes_with_their_connected_component():
|
||||
alpha = np.zeros((120, 200), dtype=np.uint8)
|
||||
alpha[20:100, 50:160] = 255
|
||||
alpha[45:75, 80:120] = 0
|
||||
alpha[5:15, 5:15] = 255
|
||||
|
||||
region_contours = extract_region_contours(
|
||||
RegionMask(alpha, (0, 0, 200, 120)),
|
||||
selection_contours = extract_selection_contours(
|
||||
SelectionMask(alpha, (0, 0, 200, 120)),
|
||||
max_resolution=200,
|
||||
)
|
||||
|
||||
assert region_contours.image_size == (200, 120)
|
||||
assert sorted(len(component) for component in region_contours.components) == [1, 2]
|
||||
assert selection_contours.image_size == (200, 120)
|
||||
assert sorted(len(component) for component in selection_contours.components) == [1, 2]
|
||||
contours = [
|
||||
contour for component in region_contours.components for contour in component
|
||||
contour for component in selection_contours.components for contour in component
|
||||
]
|
||||
bounds = [
|
||||
(*np.min(contour, axis=0), *np.max(contour, axis=0)) for contour in contours
|
||||
@@ -249,19 +300,19 @@ def test_region_contours_group_holes_with_their_connected_component():
|
||||
assert (5.0, 5.0, 15.0, 15.0) in bounds
|
||||
|
||||
|
||||
def test_region_contours_accept_blender_float_mask_with_a_hole():
|
||||
def test_selection_contours_accept_blender_float_mask_with_a_hole():
|
||||
alpha = np.zeros((120, 200), dtype=np.float32)
|
||||
alpha[20:100, 50:160] = 1.0
|
||||
alpha[45:75, 80:120] = 0.0
|
||||
|
||||
region_contours = extract_region_contours(
|
||||
RegionMask(alpha, (0, 0, 200, 120)),
|
||||
selection_contours = extract_selection_contours(
|
||||
SelectionMask(alpha, (0, 0, 200, 120)),
|
||||
max_resolution=200,
|
||||
)
|
||||
|
||||
assert region_contours.image_size == (200, 120)
|
||||
assert len(region_contours.components) == 1
|
||||
assert len(region_contours.components[0]) == 2
|
||||
assert selection_contours.image_size == (200, 120)
|
||||
assert len(selection_contours.components) == 1
|
||||
assert len(selection_contours.components[0]) == 2
|
||||
|
||||
|
||||
def test_cutout_crop_bounds_map_to_the_source_plane():
|
||||
@@ -285,14 +336,14 @@ 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 = cutout_geometry.extract_cutout_contours(
|
||||
RegionMask(alpha, (0, 0, 16, 12)),
|
||||
selection_contours = cutout_geometry.extract_cutout_contours(
|
||||
SelectionMask(alpha, (0, 0, 16, 12)),
|
||||
64,
|
||||
threshold=0.5,
|
||||
)
|
||||
contour = np.asarray(region_contours.components[0][0])
|
||||
contour = np.asarray(selection_contours.components[0][0])
|
||||
|
||||
assert region_contours.image_size == (16, 12)
|
||||
assert selection_contours.image_size == (16, 12)
|
||||
np.testing.assert_allclose(contour.min(axis=0), (5.0, 4.0))
|
||||
np.testing.assert_allclose(contour.max(axis=0), (11.0, 8.0))
|
||||
|
||||
@@ -306,37 +357,37 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
return original_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=without_pillow):
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
(48, 32),
|
||||
RegionPath(points=((6, 4), (42, 4), (42, 28), (6, 28))),
|
||||
SelectionPath(points=((6, 4), (42, 4), (42, 28), (6, 28))),
|
||||
antialias=True,
|
||||
)
|
||||
antialiased = region_mask.full_values((48, 32))
|
||||
region_contours = extract_region_contours(
|
||||
RegionMask(antialiased, (0, 0, 48, 32)),
|
||||
antialiased = selection_mask.full_values((48, 32))
|
||||
selection_contours = extract_selection_contours(
|
||||
SelectionMask(antialiased, (0, 0, 48, 32)),
|
||||
max_resolution=24,
|
||||
)
|
||||
|
||||
assert region_contours.image_size == (48, 32)
|
||||
assert region_contours.components
|
||||
assert selection_contours.image_size == (48, 32)
|
||||
assert selection_contours.components
|
||||
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
|
||||
|
||||
|
||||
def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
expected = RegionContours(
|
||||
expected = SelectionContours(
|
||||
components=(((((0, 0), (1, 0), (0, 1))),),),
|
||||
image_size=(1, 1),
|
||||
)
|
||||
region_mask = RegionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
|
||||
with patch.object(cutout_geometry, "extract_region_contours", return_value=expected) as trace:
|
||||
assert cutout_geometry.extract_cutout_contours(region_mask, 32) == expected
|
||||
assert cutout_geometry.extract_cutout_contours(region_mask, 256) == expected
|
||||
selection_mask = SelectionMask(np.ones((2, 2), dtype=np.float32), (0, 0, 2, 2))
|
||||
with patch.object(cutout_geometry, "extract_selection_contours", return_value=expected) as trace:
|
||||
assert cutout_geometry.extract_cutout_contours(selection_mask, 32) == expected
|
||||
assert cutout_geometry.extract_cutout_contours(selection_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():
|
||||
def test_cutout_sampling_spacing_uses_the_complete_source_plane():
|
||||
source = SimpleNamespace(matrix_world=object())
|
||||
with (
|
||||
patch.object(cutout_geometry, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
|
||||
@@ -346,7 +397,7 @@ def test_cutout_point_spacing_uses_the_complete_source_plane():
|
||||
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
|
||||
),
|
||||
):
|
||||
spacing = cutout_geometry.cutout_point_spacing(source, 200)
|
||||
spacing = cutout_geometry.cutout_sampling_spacing(source, 200)
|
||||
|
||||
assert spacing == 0.02
|
||||
|
||||
@@ -401,7 +452,7 @@ def test_poisson_balloon_profile_scales_in_world_units():
|
||||
assert np.isclose(scaled[4], original[4] * 2.0, atol=1e-6)
|
||||
|
||||
|
||||
def test_poisson_balloon_profile_makes_wide_regions_thicker():
|
||||
def test_poisson_balloon_profile_makes_wide_selections_thicker():
|
||||
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
|
||||
boundary = np.asarray((True, True, True, True, False))
|
||||
wide = np.asarray(
|
||||
@@ -445,11 +496,11 @@ 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)),),)
|
||||
def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_selection():
|
||||
components = ((((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),)
|
||||
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
SelectionContours(components, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
@@ -460,10 +511,10 @@ 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)),),)
|
||||
components = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),)
|
||||
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
RegionContours(regions, (4096, 4096)),
|
||||
SelectionContours(components, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
@@ -474,10 +525,10 @@ def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
|
||||
|
||||
def test_cutout_domain_centers_vertices_on_crop_bounds():
|
||||
regions = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
|
||||
components = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
|
||||
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = cutout_geometry.cutout_domain(
|
||||
RegionContours(regions, (100, 50)),
|
||||
SelectionContours(components, (100, 50)),
|
||||
(2.0, 6.0, -3.0, 1.0),
|
||||
0.5,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
@@ -489,8 +540,8 @@ def test_cutout_domain_centers_vertices_on_crop_bounds():
|
||||
assert np.allclose(uv, expected_uv, atol=1e-6)
|
||||
|
||||
|
||||
def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
regions = (
|
||||
def test_cutout_domain_keeps_overlapping_selection_components_disconnected():
|
||||
components = (
|
||||
(
|
||||
((20, 0), (100, 0), (100, 80), (20, 80)),
|
||||
((50, 20), (50, 60), (70, 60), (70, 20)),
|
||||
@@ -498,7 +549,7 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
(((0, 20), (40, 20), (40, 50), (0, 50)),),
|
||||
)
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
RegionContours(regions, (120, 80)),
|
||||
SelectionContours(components, (120, 80)),
|
||||
(0.0, 1.0, 0.0, 1.0),
|
||||
0.025,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
@@ -511,7 +562,7 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
& (centers[:, 1] > 0.35)
|
||||
& (centers[:, 1] < 0.65)
|
||||
)
|
||||
in_left_region = (
|
||||
in_left_component = (
|
||||
(centers[:, 0] > 0.02)
|
||||
& (centers[:, 0] < 0.12)
|
||||
& (centers[:, 1] > 0.4)
|
||||
@@ -519,17 +570,17 @@ def test_cutout_domain_keeps_overlapping_regions_disconnected():
|
||||
)
|
||||
|
||||
assert not in_hole.any()
|
||||
assert in_left_region.any()
|
||||
assert in_left_component.any()
|
||||
assert mesh_component_count(domain[1]) == 2
|
||||
|
||||
|
||||
def test_region_calibration_uses_the_final_region_mask():
|
||||
region_mask = RegionMask(
|
||||
def test_selection_calibration_uses_the_final_selection_mask():
|
||||
selection_mask = SelectionMask(
|
||||
np.asarray(((1.0, 1.0), (1.0, 0.0)), dtype=np.float32),
|
||||
(0, 0, 2, 2),
|
||||
)
|
||||
|
||||
valid, transparent_fraction = cutout_geometry.region_calibration(region_mask, (2, 2))
|
||||
valid, transparent_fraction = cutout_geometry.selection_calibration(selection_mask, (2, 2))
|
||||
|
||||
assert np.array_equal(valid, ((True, True), (True, False)))
|
||||
assert transparent_fraction == 0.25
|
||||
|
||||
@@ -7,8 +7,8 @@ import bpy
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.image_interaction import ImageRegionGesture
|
||||
from anyimage.common.image_region import RegionPath
|
||||
from anyimage.common.selection import SelectionPath
|
||||
from anyimage.common.viewport import ImageSelectionGesture
|
||||
from anyimage.operators.cutout_tool import interaction
|
||||
from anyimage.operators.cutout_tool import main as cutout_main
|
||||
|
||||
@@ -32,9 +32,9 @@ class WindowManager:
|
||||
draw(menu, None)
|
||||
|
||||
|
||||
def cutout_context(window_manager, point_spacing=32, generate_normal=False):
|
||||
def cutout_context(window_manager, mesh_detail="LOW", generate_normal=False):
|
||||
settings = SimpleNamespace(
|
||||
cutout_point_spacing=point_spacing,
|
||||
cutout_mesh_detail=mesh_detail,
|
||||
cutout_generate_normal=generate_normal,
|
||||
)
|
||||
return SimpleNamespace(
|
||||
@@ -57,8 +57,8 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
|
||||
context,
|
||||
object(),
|
||||
source,
|
||||
RegionPath(points=((0, 0), (10, 0), (0, 10))),
|
||||
refine_region=True,
|
||||
SelectionPath(points=((0, 0), (10, 0), (0, 10))),
|
||||
refine_selection=True,
|
||||
)
|
||||
|
||||
assert [call[2].shape for call in window_manager.pie.calls] == [
|
||||
@@ -70,17 +70,17 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
|
||||
"Balloon",
|
||||
]
|
||||
assert all(
|
||||
call[0] == "anyimage.cutout_region_to_shape"
|
||||
call[0] == "anyimage.cutout_selection_to_shape"
|
||||
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].refine_selection 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_all_ai_shapes(monkeypatch):
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(window_manager, point_spacing=16, generate_normal=True)
|
||||
context = cutout_context(window_manager, mesh_detail="MEDIUM", generate_normal=True)
|
||||
source = cutout_source(size=(4096, 2048))
|
||||
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
|
||||
|
||||
@@ -88,11 +88,11 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
context,
|
||||
object(),
|
||||
source,
|
||||
RegionPath(
|
||||
SelectionPath(
|
||||
points=((0, 0), (10, 0), (0, 10)),
|
||||
invert=True,
|
||||
),
|
||||
refine_region=True,
|
||||
refine_selection=True,
|
||||
)
|
||||
|
||||
assert [call[2].shape for call in window_manager.pie.calls] == [
|
||||
@@ -101,10 +101,10 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
"DEPTH_BALLOON",
|
||||
"DEPTH_SURFACE",
|
||||
]
|
||||
assert all(call[2].refine_region for call in window_manager.pie.calls)
|
||||
assert all(call[2].refine_selection 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
|
||||
SelectionPath.from_json(call[2].selection_path_json).invert
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert all(call[2].mesh_resolution == 256 for call in window_manager.pie.calls)
|
||||
@@ -124,6 +124,13 @@ def test_cutout_mesh_detail_tracks_the_source_long_edge_and_clamps():
|
||||
assert interaction.cutout_mesh_resolution((65536, 32768), 4) == 1024
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_maps_to_denser_sampling():
|
||||
assert [
|
||||
interaction.cutout_mesh_detail_spacing(detail)
|
||||
for detail in ("LOW", "MEDIUM", "HIGH", "ULTRA")
|
||||
] == [32, 16, 8, 4]
|
||||
|
||||
|
||||
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Source", width=320, height=180)
|
||||
@@ -138,23 +145,23 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
|
||||
cutout_main, "resolve_image_edit_click", lambda *_args: (source, True)
|
||||
)
|
||||
monkeypatch.setattr(cutout_main, "video_total_frames", lambda _image: 1)
|
||||
monkeypatch.setattr(cutout_main, "apply_region_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "validate_cutout_region", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "apply_selection_settings", lambda *_args: None)
|
||||
monkeypatch.setattr(cutout_main, "validate_cutout_selection", lambda *_args: None)
|
||||
|
||||
operator = SimpleNamespace(
|
||||
full_image=True,
|
||||
refine_region=False,
|
||||
refine_selection=False,
|
||||
report=lambda *_args: None,
|
||||
)
|
||||
operator.execute = MethodType(cutout_main.SelectCutoutRegion.execute, operator)
|
||||
result = cutout_main.SelectCutoutRegion.invoke(
|
||||
operator.execute = MethodType(cutout_main.SelectCutoutSelection.execute, operator)
|
||||
result = cutout_main.SelectCutoutSelection.invoke(
|
||||
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
|
||||
)
|
||||
|
||||
assert issubclass(cutout_main.SelectCutoutRegion, ImageRegionGesture)
|
||||
assert issubclass(cutout_main.SelectCutoutSelection, ImageSelectionGesture)
|
||||
assert result == {"FINISHED"}
|
||||
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))
|
||||
selection_path = SelectionPath.from_json(window_manager.pie.calls[0][2].selection_path_json)
|
||||
assert selection_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
|
||||
assert window_manager.pie.calls[0][2].mesh_resolution == 10
|
||||
assert not hasattr(cutout_main, "pending_selection")
|
||||
|
||||
|
||||
@@ -10,21 +10,26 @@ from mathutils import Vector
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common import image_data
|
||||
from anyimage.common import depth_data
|
||||
from anyimage.common import depth as depth_data
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import material as material_data
|
||||
from anyimage.common import node as node_asset
|
||||
from anyimage.common import object as object_data
|
||||
from anyimage.common.image_region import (
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
from anyimage.common.selection import (
|
||||
SelectionMask,
|
||||
SelectionPath,
|
||||
rasterize_selection_path,
|
||||
)
|
||||
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 cutout_shape
|
||||
from anyimage.server.geometry.depth_texture import write_vector_depth_texture
|
||||
from anyimage.server.geometry.depth_texture import (
|
||||
write_vector_depth_texture,
|
||||
write_z_depth_texture,
|
||||
)
|
||||
from anyimage.server.models.geometry import GeometryFrame
|
||||
|
||||
|
||||
def _inputs(group):
|
||||
@@ -51,26 +56,26 @@ def _create_cutout_shape(
|
||||
bounds=None,
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
region_values=None,
|
||||
selection_values=None,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
left, top, right, bottom = bounds
|
||||
if region_values is None:
|
||||
region_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
region_mask = RegionMask(region_values, bounds)
|
||||
if selection_values is None:
|
||||
selection_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
selection_mask = SelectionMask(selection_values, bounds)
|
||||
base_shape = cutout_geometry.build_base_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
detail,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
)
|
||||
return cutout_object.create_shape_object(
|
||||
bpy.context,
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
base_shape,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
@@ -82,7 +87,7 @@ def _depth_image(points, valid, name="Depth"):
|
||||
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] = points
|
||||
pixels[..., 3] = valid
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
@@ -112,9 +117,15 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.data = source_image
|
||||
points = np.arange(12, dtype=np.float32).reshape((2, 2, 3))
|
||||
valid = np.asarray(((True, False), (True, True)))
|
||||
validity = np.asarray(((1.0, 0.0), (0.5, 1.0)), dtype=np.float32)
|
||||
frame = GeometryFrame(
|
||||
depth=points[..., 2].copy(),
|
||||
validity=validity,
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
points=points,
|
||||
)
|
||||
path = tmp_path / "vector-depth.exr"
|
||||
write_vector_depth_texture(points, valid, path)
|
||||
write_vector_depth_texture(frame, path)
|
||||
metadata = _depth_metadata(np.eye(3), (2, 2), 1.0)
|
||||
|
||||
image = depth_data.load_depth_result_image(path, source, metadata)
|
||||
@@ -122,12 +133,38 @@ def test_depth_result_image_loads_server_vector_exr_and_keeps_data_name(tmp_path
|
||||
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[valid, :3], points[valid])
|
||||
assert np.array_equal(packed[..., 3] > 0.5, valid)
|
||||
assert np.array_equal(packed[..., :3], points)
|
||||
assert np.array_equal(packed[..., 3], validity)
|
||||
assert image.alpha_mode == "CHANNEL_PACKED"
|
||||
assert image.name == "Source_depth.exr"
|
||||
|
||||
|
||||
def test_z_depth_texture_keeps_depth_outside_positive_validity(tmp_path):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Source.png", width=2, height=1)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.data = source_image
|
||||
frame = GeometryFrame(
|
||||
depth=np.asarray(((2.0, 7.0),), dtype=np.float32),
|
||||
validity=np.asarray(((1.0, 0.0),), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
)
|
||||
path = tmp_path / "z-depth.exr"
|
||||
write_z_depth_texture(frame, path)
|
||||
|
||||
image = depth_data.load_depth_result_image(
|
||||
path,
|
||||
source,
|
||||
_depth_metadata(np.eye(3), (2, 1), 2.0),
|
||||
)
|
||||
|
||||
packed = np.empty(8, dtype=np.float32)
|
||||
image.pixels.foreach_get(packed)
|
||||
packed = packed.reshape((1, 2, 4))
|
||||
assert np.array_equal(packed[..., :3], np.repeat(frame.depth[..., None], 3, axis=2))
|
||||
assert np.array_equal(packed[..., 3], frame.validity)
|
||||
|
||||
|
||||
def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
@@ -370,8 +407,8 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
assert object_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
|
||||
group = object_data.material_node_group()
|
||||
assert material_data.IMAGE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
|
||||
group = material_data.material_node_group()
|
||||
normal = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
@@ -416,7 +453,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
|
||||
color = bpy.data.images.new("Color", width=2, height=2)
|
||||
normal_image = bpy.data.images.new("Normal", width=2, height=2)
|
||||
object_material = object_data.create_image_material(
|
||||
object_material = material_data.create_image_material(
|
||||
SimpleNamespace(data=SimpleNamespace(name="Object Normal")),
|
||||
color,
|
||||
1.0,
|
||||
@@ -426,7 +463,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
layer = _group_node(group, object_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is True
|
||||
|
||||
tangent_material = object_data.create_image_material(
|
||||
tangent_material = material_data.create_image_material(
|
||||
SimpleNamespace(data=SimpleNamespace(name="Tangent Normal")),
|
||||
color,
|
||||
1.0,
|
||||
@@ -434,7 +471,7 @@ def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
)
|
||||
layer = _group_node(group, tangent_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
reused = object_data.material_node_group()
|
||||
reused = material_data.material_node_group()
|
||||
assert {
|
||||
node.space for node in reused.nodes if node.bl_idname == "ShaderNodeNormalMap"
|
||||
} == {"TANGENT", "OBJECT"}
|
||||
@@ -503,7 +540,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
|
||||
)
|
||||
assert normal_scale_attribute.attribute_type == "GEOMETRY"
|
||||
assert normal_scale_attribute.attribute_name == cutout_shape.BALLOON_ATTRIBUTE_NAME
|
||||
image_layer = _group_node(object_data.material_node_group(), material_tree)
|
||||
image_layer = _group_node(material_data.material_node_group(), material_tree)
|
||||
normal_scale_link = next(
|
||||
link
|
||||
for link in material_tree.links
|
||||
@@ -572,7 +609,7 @@ def test_cutout_material_uses_flat_tangent_normal():
|
||||
color = bpy.data.images.new("Color", width=2, height=2)
|
||||
normal = bpy.data.images.new("Normal", width=1, height=1)
|
||||
|
||||
result = object_data.create_image_material(
|
||||
result = material_data.create_image_material(
|
||||
source,
|
||||
color,
|
||||
0.0,
|
||||
@@ -580,7 +617,7 @@ def test_cutout_material_uses_flat_tangent_normal():
|
||||
scene=bpy.context.scene,
|
||||
)
|
||||
|
||||
layer = _group_node(object_data.material_node_group(), result.node_tree)
|
||||
layer = _group_node(material_data.material_node_group(), result.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
|
||||
|
||||
@@ -624,7 +661,7 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
== cutout_shape.CUTOUT_SHAPE_INDICES["DEPTH_SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 8),
|
||||
cutout_geometry.cutout_sampling_spacing(source, 8),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
@@ -703,6 +740,44 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
assert np.isfinite(internal_positions).all()
|
||||
|
||||
|
||||
def test_depth_surface_keeps_continuous_points_across_zero_validity_edge():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Surface", width=64, height=64)
|
||||
source = bpy.data.objects.new("Surface", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
size = 8
|
||||
depth = np.ones((size, size), dtype=np.float32)
|
||||
x, y = np.meshgrid(np.arange(size), np.arange(size))
|
||||
intrinsics = np.asarray(
|
||||
((8.0, 0.0, 3.5), (0.0, 8.0, 3.5), (0.0, 0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
points = np.empty((size, size, 3), dtype=np.float32)
|
||||
points[..., 0] = (x - intrinsics[0, 2]) / intrinsics[0, 0] * depth
|
||||
points[..., 1] = (y - intrinsics[1, 2]) / intrinsics[1, 1] * depth
|
||||
points[..., 2] = depth
|
||||
validity = np.ones((size, size), dtype=bool)
|
||||
validity[[0, -1], :] = False
|
||||
validity[:, [0, -1]] = False
|
||||
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_SURFACE",
|
||||
8,
|
||||
depth_image=_depth_image(points, validity),
|
||||
depth_metadata=_depth_metadata(intrinsics, (size, size), 1.0),
|
||||
)
|
||||
positions = _evaluated_positions(result)
|
||||
|
||||
assert np.isfinite(positions).all()
|
||||
assert np.ptp(positions[:, 0]) < 1e-4
|
||||
assert np.ptp(positions[:, 1]) > 1.5
|
||||
assert np.ptp(positions[:, 2]) > 1.5
|
||||
|
||||
|
||||
def _evaluated_positions(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
@@ -951,8 +1026,8 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
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
|
||||
selection_values = np.ones((60, 40), dtype=np.float32)
|
||||
selection_values[:, :2] = 0.0
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_BALLOON",
|
||||
@@ -960,7 +1035,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
bounds=crop_bounds,
|
||||
region_values=region_values,
|
||||
selection_values=selection_values,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
@@ -1135,16 +1210,16 @@ def test_local_cutout_selection_matches_uint8_tenth_alpha_boundary():
|
||||
top_down = np.ones((4, 4, 4), dtype=np.float32)
|
||||
top_down[1, 1, 3] = 25.0 / 255.0
|
||||
top_down[1, 2, 3] = 26.0 / 255.0
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
(4, 4),
|
||||
RegionPath(
|
||||
SelectionPath(
|
||||
points=((0, 0), (4, 0), (4, 4), (0, 4)),
|
||||
fill_rule="NONZERO",
|
||||
),
|
||||
source_alpha=top_down[:, :, 3],
|
||||
alpha_threshold=0.1,
|
||||
)
|
||||
selection = region_mask.full_values((4, 4))
|
||||
selection = selection_mask.full_values((4, 4))
|
||||
|
||||
assert selection[1, 1] == 0.0
|
||||
assert selection[1, 2] > 0.1
|
||||
@@ -1162,7 +1237,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
bpy.context.collection.objects.link(source)
|
||||
source.select_set(True)
|
||||
bpy.context.view_layer.objects.active = source
|
||||
region_path = RegionPath(
|
||||
selection_path = SelectionPath(
|
||||
points=((-8, 8), (72, 8), (72, 40), (-8, 40)),
|
||||
fill_rule="NONZERO",
|
||||
)
|
||||
@@ -1174,9 +1249,9 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
wraps=image_data.image_rgba,
|
||||
) as read_rgba:
|
||||
rgba = image_data.image_rgba(image)
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
tuple(image.size),
|
||||
region_path,
|
||||
selection_path,
|
||||
source_alpha=rgba[:, :, 3],
|
||||
alpha_threshold=cutout_geometry.CUTOUT_ALPHA_THRESHOLD,
|
||||
antialias=True,
|
||||
@@ -1186,7 +1261,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
source,
|
||||
"SURFACE",
|
||||
16,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
source_rgba=rgba,
|
||||
)
|
||||
|
||||
@@ -1203,7 +1278,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
== cutout_shape.CUTOUT_SHAPE_INDICES["SURFACE"]
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_point_spacing(source, 16),
|
||||
cutout_geometry.cutout_sampling_spacing(source, 16),
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
@@ -1245,7 +1320,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
color_texture.image.pixels.foreach_get(copied)
|
||||
copied = copied.reshape((34, 64, 4))
|
||||
assert np.allclose(copied[:, :, :3], 1.0)
|
||||
left, top, right, bottom = region_mask.bounds
|
||||
left, top, right, bottom = selection_mask.bounds
|
||||
assert np.allclose(
|
||||
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
|
||||
)
|
||||
@@ -1257,7 +1332,7 @@ def test_balloon_normal_scale_uses_existing_image_layer_input():
|
||||
{"BALLOON", "DEPTH_BALLOON"}
|
||||
)
|
||||
group = bpy.data.node_groups.new(
|
||||
object_data.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
material_data.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
"ShaderNodeTree",
|
||||
)
|
||||
group.interface.new_socket(
|
||||
|
||||
@@ -59,12 +59,14 @@ class ExtensionPackagingTest(unittest.TestCase):
|
||||
self.assertNotIn("assets/O_AnyImage.blend1", names)
|
||||
for path in (
|
||||
"common/__init__.py",
|
||||
"common/ai_job.py",
|
||||
"common/image_data.py",
|
||||
"common/image_interaction.py",
|
||||
"common/image_region.py",
|
||||
"common/ai.py",
|
||||
"common/depth.py",
|
||||
"common/image.py",
|
||||
"common/material.py",
|
||||
"common/node.py",
|
||||
"common/object.py",
|
||||
"common/selection.py",
|
||||
"common/viewport.py",
|
||||
"operators/cutout_tool/__init__.py",
|
||||
"operators/cutout_tool/main.py",
|
||||
"operators/cutout_tool/interaction.py",
|
||||
@@ -98,12 +100,12 @@ class ExtensionPackagingTest(unittest.TestCase):
|
||||
names,
|
||||
)
|
||||
self.assertIn("server/media/input.py", names)
|
||||
self.assertIn("common/depth_data.py", names)
|
||||
self.assertIn("common/depth.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_region.py", names)
|
||||
self.assertIn("server/region/refine_region.py", names)
|
||||
self.assertIn("server/jobs/refine_image_selection.py", names)
|
||||
self.assertIn("server/selection/refine_selection.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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import ast
|
||||
import importlib
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -7,6 +9,47 @@ from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
COMMON_ROOT = SOURCE_ROOT / "anyimage" / "common"
|
||||
COMMON_MODULES = {
|
||||
"ai",
|
||||
"depth",
|
||||
"image",
|
||||
"material",
|
||||
"node",
|
||||
"object",
|
||||
"selection",
|
||||
"viewport",
|
||||
}
|
||||
|
||||
|
||||
def test_common_modules_follow_business_boundaries():
|
||||
module_names = {
|
||||
path.stem
|
||||
for path in COMMON_ROOT.glob("*.py")
|
||||
if path.name != "__init__.py"
|
||||
}
|
||||
assert module_names == COMMON_MODULES
|
||||
|
||||
for module_name in COMMON_MODULES:
|
||||
importlib.import_module(f"anyimage.common.{module_name}")
|
||||
|
||||
ai_ui_definitions = {
|
||||
path.name
|
||||
for path in COMMON_ROOT.glob("*.py")
|
||||
if "def draw_ai_setup(" in path.read_text(encoding="utf-8")
|
||||
or "def draw_ai_property(" in path.read_text(encoding="utf-8")
|
||||
}
|
||||
assert ai_ui_definitions == {"ai.py"}
|
||||
|
||||
init_tree = ast.parse(
|
||||
(COMMON_ROOT / "__init__.py").read_text(encoding="utf-8")
|
||||
)
|
||||
assert not any(
|
||||
isinstance(node, (ast.Import, ast.ImportFrom))
|
||||
for node in init_tree.body
|
||||
)
|
||||
|
||||
|
||||
def load_image_modules():
|
||||
@@ -24,12 +67,12 @@ def load_image_modules():
|
||||
|
||||
module_paths = (
|
||||
(
|
||||
"anyimage.common.image_data",
|
||||
"anyimage.common.image",
|
||||
PROJECT_ROOT
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "common"
|
||||
/ "image_data.py",
|
||||
/ "image.py",
|
||||
),
|
||||
(
|
||||
"anyimage.common.node",
|
||||
@@ -39,6 +82,14 @@ def load_image_modules():
|
||||
/ "common"
|
||||
/ "node.py",
|
||||
),
|
||||
(
|
||||
"anyimage.common.material",
|
||||
PROJECT_ROOT
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "common"
|
||||
/ "material.py",
|
||||
),
|
||||
(
|
||||
"anyimage.common.object",
|
||||
PROJECT_ROOT
|
||||
|
||||
@@ -15,8 +15,8 @@ class NodeAssetsTest(unittest.TestCase):
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
def test_runtime_node_group_entries_load_assets(self):
|
||||
object_source = (
|
||||
SOURCE_ROOT / "common" / "object.py"
|
||||
material_source = (
|
||||
SOURCE_ROOT / "common" / "material.py"
|
||||
).read_text(encoding="utf-8")
|
||||
mesh_plane_source = (
|
||||
SOURCE_ROOT / "operators" / "convert_to_plane" / "mesh_plane.py"
|
||||
@@ -24,7 +24,7 @@ class NodeAssetsTest(unittest.TestCase):
|
||||
depth_plane_source = (
|
||||
SOURCE_ROOT / "operators" / "convert_to_plane" / "object.py"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("return load_node_group(group_name)", object_source)
|
||||
self.assertIn("return load_node_group(group_name)", material_source)
|
||||
self.assertIn(
|
||||
"load_node_group(MESH_PLANE_NODE_GROUP_NAME)",
|
||||
mesh_plane_source,
|
||||
|
||||
@@ -259,7 +259,7 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
{
|
||||
"download-model",
|
||||
"download-required-models",
|
||||
"refine-image-region",
|
||||
"refine-image-selection",
|
||||
"remove-background",
|
||||
"upscale-image",
|
||||
"generate-depth-plane-geometry",
|
||||
@@ -391,19 +391,27 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
import numpy as np
|
||||
|
||||
depth = importlib.import_module("server.geometry.depth_texture")
|
||||
GeometryFrame = importlib.import_module(
|
||||
"server.models.geometry"
|
||||
).GeometryFrame
|
||||
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(
|
||||
depth=np.asarray(((1.0, 2.0, 3.0), (4.0, 5.0, 6.0))),
|
||||
validity = np.asarray(
|
||||
((1.0, 1.0, 0.0), (1.0, 1.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
calibration = np.asarray(((False, True, True), (False, False, True)))
|
||||
frame = GeometryFrame(
|
||||
depth=np.asarray(
|
||||
((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)),
|
||||
dtype=np.float32,
|
||||
),
|
||||
validity=validity,
|
||||
points=np.ones((2, 3, 3), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
valid_mask=valid,
|
||||
model_mask=None,
|
||||
)
|
||||
|
||||
depth.write_z_depth_texture(frame.depth, frame.valid_mask, texture_path)
|
||||
depth.write_z_depth_texture(frame, texture_path)
|
||||
depth.write_depth_metadata(frame, calibration, metadata_path)
|
||||
|
||||
self.assertTrue(texture_path.is_file())
|
||||
@@ -418,6 +426,9 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
from PIL import Image
|
||||
|
||||
normal_texture = importlib.import_module("server.geometry.normal_texture")
|
||||
GeometryFrame = importlib.import_module(
|
||||
"server.models.geometry"
|
||||
).GeometryFrame
|
||||
output_path = self.storage_root / "object-normal.png"
|
||||
camera_normals = np.asarray(
|
||||
[[
|
||||
@@ -429,9 +440,15 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
dtype=np.float32,
|
||||
)
|
||||
valid = np.asarray([[True, True, True, False]], dtype=bool)
|
||||
frame = GeometryFrame(
|
||||
depth=np.ones((1, 4), dtype=np.float32),
|
||||
validity=np.ones((1, 4), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
normal=camera_normals,
|
||||
)
|
||||
|
||||
normal_texture.write_camera_normal_texture(
|
||||
camera_normals,
|
||||
frame,
|
||||
valid,
|
||||
(0, 0),
|
||||
(4, 1),
|
||||
@@ -455,10 +472,153 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
),
|
||||
)
|
||||
|
||||
def test_moge_model_input_composites_rgba_over_black(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
artifacts = sys.modules["server.geometry.prediction_artifacts"]
|
||||
first_path = self.storage_root / "first.png"
|
||||
second_path = self.storage_root / "second.png"
|
||||
first = np.asarray(
|
||||
[[(240, 120, 60, 0), (200, 100, 50, 128), (9, 8, 7, 255)]],
|
||||
dtype=np.uint8,
|
||||
)
|
||||
second = first.copy()
|
||||
second[0, 0, :3] = (1, 2, 3)
|
||||
Image.fromarray(first, mode="RGBA").save(first_path)
|
||||
Image.fromarray(second, mode="RGBA").save(second_path)
|
||||
|
||||
first_output = artifacts._limited_model_input(
|
||||
first_path,
|
||||
self.storage_root / "first-model.png",
|
||||
2048,
|
||||
)
|
||||
second_output = artifacts._limited_model_input(
|
||||
second_path,
|
||||
self.storage_root / "second-model.png",
|
||||
2048,
|
||||
)
|
||||
|
||||
with Image.open(first_output) as opened:
|
||||
first_rgb = np.asarray(opened, dtype=np.uint8)
|
||||
with Image.open(second_output) as opened:
|
||||
second_rgb = np.asarray(opened, dtype=np.uint8)
|
||||
np.testing.assert_array_equal(first_rgb, second_rgb)
|
||||
np.testing.assert_array_equal(
|
||||
first_rgb,
|
||||
np.asarray([[(0, 0, 0), (100, 50, 25), (9, 8, 7)]], dtype=np.uint8),
|
||||
)
|
||||
|
||||
def test_geometry_writers_reject_missing_or_non_finite_fields(self):
|
||||
import numpy as np
|
||||
|
||||
depth = importlib.import_module("server.geometry.depth_texture")
|
||||
normal = importlib.import_module("server.geometry.normal_texture")
|
||||
GeometryFrame = importlib.import_module(
|
||||
"server.models.geometry"
|
||||
).GeometryFrame
|
||||
base = {
|
||||
"depth": np.ones((1, 2), dtype=np.float32),
|
||||
"validity": np.ones((1, 2), dtype=np.float32),
|
||||
"intrinsics": np.eye(3, dtype=np.float32),
|
||||
}
|
||||
with self.assertRaisesRegex(ValueError, "point field"):
|
||||
depth.write_vector_depth_texture(
|
||||
GeometryFrame(**base),
|
||||
self.storage_root / "missing-points.exr",
|
||||
)
|
||||
point_frame = GeometryFrame(
|
||||
**base,
|
||||
points=np.asarray([[(0.0, 0.0, 1.0), (np.nan, 0.0, 1.0)]], dtype=np.float32),
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "Point field"):
|
||||
depth.write_vector_depth_texture(
|
||||
point_frame,
|
||||
self.storage_root / "nan-points.exr",
|
||||
)
|
||||
depth_frame = GeometryFrame(
|
||||
**{**base, "depth": np.asarray(((1.0, np.inf),), dtype=np.float32)}
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "Depth field"):
|
||||
depth.write_z_depth_texture(
|
||||
depth_frame,
|
||||
self.storage_root / "inf-depth.exr",
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "normal field"):
|
||||
normal.write_camera_normal_texture(
|
||||
GeometryFrame(**base),
|
||||
np.ones((1, 2), dtype=bool),
|
||||
(0, 0),
|
||||
(2, 1),
|
||||
(2, 1),
|
||||
self.storage_root / "missing-normal.png",
|
||||
)
|
||||
normal_frame = GeometryFrame(
|
||||
**base,
|
||||
normal=np.asarray(
|
||||
[[(0.0, 0.0, 1.0), (0.0, np.nan, 1.0)]],
|
||||
dtype=np.float32,
|
||||
),
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "Normal field"):
|
||||
normal.write_camera_normal_texture(
|
||||
normal_frame,
|
||||
np.ones((1, 2), dtype=bool),
|
||||
(0, 0),
|
||||
(2, 1),
|
||||
(2, 1),
|
||||
self.storage_root / "nan-normal.png",
|
||||
)
|
||||
self.assertFalse(any(self.storage_root.glob("*.exr")))
|
||||
self.assertFalse(any(self.storage_root.glob("*-normal.png")))
|
||||
|
||||
def test_moge_artifact_orchestration_uses_one_geometry_frame(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
artifacts = sys.modules["server.geometry.prediction_artifacts"]
|
||||
GeometryFrame = importlib.import_module(
|
||||
"server.models.geometry"
|
||||
).GeometryFrame
|
||||
input_path = self.storage_root / "selection.png"
|
||||
Image.new("RGBA", (2, 2), (255, 128, 64, 255)).save(input_path)
|
||||
output_directory = self.storage_root / "job"
|
||||
output_directory.mkdir()
|
||||
context = SimpleNamespace(
|
||||
directory=output_directory,
|
||||
resource=lambda _name: object(),
|
||||
progress=lambda *_args: None,
|
||||
check_cancelled=lambda: None,
|
||||
)
|
||||
frame = GeometryFrame(
|
||||
depth=np.ones((2, 2), dtype=np.float32),
|
||||
validity=np.ones((2, 2), dtype=np.float32),
|
||||
intrinsics=np.eye(3, dtype=np.float32),
|
||||
normal=np.zeros((2, 2, 3), dtype=np.float32),
|
||||
points=np.ones((2, 2, 3), dtype=np.float32),
|
||||
)
|
||||
|
||||
with patch.object(artifacts.moge2, "infer", return_value=[frame]) as infer:
|
||||
result = artifacts.generate_moge2_artifacts(
|
||||
context,
|
||||
{"max_input_size": 2048},
|
||||
input_path,
|
||||
depth_mode="VECTOR",
|
||||
normal_mode="OBJECT",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
set(result),
|
||||
{"vector_depth", "depth_metadata", "object_normal"},
|
||||
)
|
||||
self.assertFalse((output_directory / "model-input.png").exists())
|
||||
self.assertTrue(all(path.is_file() for path in result.values()))
|
||||
self.assertTrue(infer.call_args.kwargs["include_points"])
|
||||
|
||||
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")
|
||||
input_path.write_bytes(b"selection")
|
||||
context = SimpleNamespace(
|
||||
directory=self.storage_root / "job",
|
||||
progress=lambda *_args: None,
|
||||
@@ -505,10 +665,10 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
normal_mode="OBJECT",
|
||||
)
|
||||
|
||||
def test_generate_cutout_artifacts_refine_an_already_rasterized_region(self):
|
||||
def test_generate_cutout_artifacts_refine_an_already_rasterized_selection(self):
|
||||
job = sys.modules["server.jobs.cutout"]
|
||||
input_path = self.storage_root / "input.png"
|
||||
input_path.write_bytes(b"region")
|
||||
input_path.write_bytes(b"selection")
|
||||
directory = self.storage_root / "job"
|
||||
context = SimpleNamespace(
|
||||
directory=directory,
|
||||
@@ -520,12 +680,12 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
output.write_bytes(b"refined")
|
||||
return output, (2, 3, 8, 9)
|
||||
|
||||
with patch.object(job, "refine_region", side_effect=generate):
|
||||
with patch.object(job, "refine_selection", side_effect=generate):
|
||||
result = job.run(
|
||||
context,
|
||||
{
|
||||
"input": str(input_path),
|
||||
"refine_region": True,
|
||||
"refine_selection": True,
|
||||
"refine_model": "BEN2_BASE",
|
||||
"device": "cpu",
|
||||
},
|
||||
@@ -533,9 +693,12 @@ class ServerRuntimeTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
{"region_image": "region.png", "region_bounds": "region_bounds.json"},
|
||||
{
|
||||
"selection_image": "selection.png",
|
||||
"selection_bounds": "selection_bounds.json",
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
(directory / "region_bounds.json").read_text(encoding="utf-8"),
|
||||
(directory / "selection_bounds.json").read_text(encoding="utf-8"),
|
||||
"[2,3,8,9]",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user