Add image perspective editing and related tools
- Implemented the Image Perspective tool for editing image corners in Blender. - Created a new operator `EditImagePerspective` to handle the perspective transformation. - Added a preview feature for the perspective adjustment. - Introduced the `ImagePolylineTool` for editing image masks with polylines. - Developed backend support for Real-ESRGAN model inference, including ONNX integration. - Added functionality for image masking and background removal. - Created unit tests for the Real-ESRGAN ONNX model to ensure correct preprocessing and inference.
This commit is contained in:
@@ -87,6 +87,7 @@ class AddonTest(unittest.TestCase):
|
||||
cls.depth_operator = importlib.import_module(
|
||||
"anyimage.operators.depth_plane.operator"
|
||||
)
|
||||
cls.debug = importlib.import_module("anyimage.operators.debug")
|
||||
cls.depth_build = importlib.import_module(
|
||||
"anyimage.operators.depth_plane.build"
|
||||
)
|
||||
@@ -95,20 +96,26 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
cls.image_background = importlib.import_module("anyimage.operators.background")
|
||||
cls.image_mesh = importlib.import_module("anyimage.operators.surface")
|
||||
cls.image_extract_common = importlib.import_module(
|
||||
"anyimage.operators.image_extract.common"
|
||||
cls.image_edit_common = importlib.import_module(
|
||||
"anyimage.operators.image_edit.common"
|
||||
)
|
||||
cls.image_region = importlib.import_module(
|
||||
"anyimage.operators.image_extract.region"
|
||||
cls.image_mask = importlib.import_module(
|
||||
"anyimage.operators.image_edit.mask"
|
||||
)
|
||||
cls.image_box = importlib.import_module(
|
||||
"anyimage.operators.image_extract.box"
|
||||
"anyimage.operators.image_edit.box"
|
||||
)
|
||||
cls.image_lasso = importlib.import_module(
|
||||
"anyimage.operators.image_extract.lasso"
|
||||
"anyimage.operators.image_edit.lasso"
|
||||
)
|
||||
cls.image_line = importlib.import_module(
|
||||
"anyimage.operators.image_edit.line"
|
||||
)
|
||||
cls.image_polyline = importlib.import_module(
|
||||
"anyimage.operators.image_edit.polyline"
|
||||
)
|
||||
cls.image_perspective = importlib.import_module(
|
||||
"anyimage.operators.image_extract.perspective"
|
||||
"anyimage.operators.image_edit.perspective"
|
||||
)
|
||||
cls.balloon = importlib.import_module("anyimage.operators.balloon.operator")
|
||||
cls.balloon_geometry = importlib.import_module(
|
||||
@@ -253,6 +260,57 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn(self.job_runtime.JobOperatorBase, operator.__mro__)
|
||||
self.assertIn(self.fake_bpy.types.Operator, operator.__mro__)
|
||||
|
||||
def test_realesrgan_debug_operator_is_a_direct_model_request(self):
|
||||
operator_type = self.debug.DebugRealesrganGeneralX4v3
|
||||
self.assertIn(operator_type, self.addon.CLASSES)
|
||||
self.assertEqual(
|
||||
operator_type.bl_idname,
|
||||
"anyimage.debug_realesrgan_general_x4v3",
|
||||
)
|
||||
self.assertEqual(
|
||||
operator_type.job_type,
|
||||
"debug-realesrgan-general-x4v3",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
image_path = Path(directory) / "input.png"
|
||||
image_path.touch()
|
||||
settings = SimpleNamespace(
|
||||
input_path=str(image_path),
|
||||
device="CPU",
|
||||
video_frames=3,
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=settings)
|
||||
)
|
||||
with (
|
||||
patch.object(self.debug, "require_environment"),
|
||||
patch.object(self.debug, "require_model") as require_model,
|
||||
):
|
||||
request = operator_type().request(context)
|
||||
|
||||
require_model.assert_called_once_with(
|
||||
self.addon.properties.REALESRGAN_MODEL_KEY
|
||||
)
|
||||
self.assertEqual(
|
||||
request,
|
||||
{
|
||||
"input": str(image_path),
|
||||
"model": "REALESRGAN_GENERAL_X4V3",
|
||||
"device": "cpu",
|
||||
"video_frames": 3,
|
||||
"start_frame": 1,
|
||||
},
|
||||
)
|
||||
self.assertNotIn("tile", request)
|
||||
self.assertNotIn("strength", request)
|
||||
|
||||
panel = importlib.import_module("anyimage.panel")
|
||||
self.assertIn(
|
||||
panel.RealesrganGeneralX4v3DebugPanel,
|
||||
self.addon.CLASSES,
|
||||
)
|
||||
|
||||
def test_default_request_sends_every_operator_property(self):
|
||||
class Example(self.job_runtime.JobOperatorBase):
|
||||
value: object()
|
||||
@@ -614,6 +672,62 @@ class AddonTest(unittest.TestCase):
|
||||
panel = importlib.import_module("anyimage.panel")
|
||||
self.assertFalse(hasattr(panel, "ModelsPanel"))
|
||||
|
||||
def test_preferences_draws_upscale_model_download(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
downloads = []
|
||||
|
||||
class Layout:
|
||||
enabled = True
|
||||
|
||||
def prop(self, *_args, **_options):
|
||||
return None
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
def label(self, **_options):
|
||||
return None
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operator = SimpleNamespace(model="")
|
||||
if identifier == "anyimage.download_model":
|
||||
downloads.append(operator)
|
||||
return operator
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
catalog = ({
|
||||
"key": "REALESRGAN_GENERAL_X4V3",
|
||||
"label": "Real-ESRGAN General x4v3",
|
||||
"family": "upscale",
|
||||
"enabled": True,
|
||||
"ready": False,
|
||||
},)
|
||||
with (
|
||||
patch.object(
|
||||
preference_module,
|
||||
"ai_status",
|
||||
return_value={
|
||||
"environment_ready": True,
|
||||
"missing_models": (),
|
||||
"ready": True,
|
||||
},
|
||||
),
|
||||
patch.object(
|
||||
preference_module,
|
||||
"model_catalog",
|
||||
return_value=catalog,
|
||||
),
|
||||
patch.object(preference_module.runtime, "server_busy", return_value=False),
|
||||
):
|
||||
preferences.draw(None)
|
||||
|
||||
self.assertEqual(len(downloads), 1)
|
||||
self.assertEqual(downloads[0].model, "REALESRGAN_GENERAL_X4V3")
|
||||
|
||||
def test_mesh_plane_operator_is_registered_with_material_options(self):
|
||||
operator = self.mesh_plane.ConvertToMeshPlane
|
||||
self.assertIn(operator, self.addon.CLASSES)
|
||||
@@ -793,141 +907,213 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertGreater(self.balloon_geometry.polygon_area(contour), 0.0)
|
||||
self.assertLess(float(lengths.std() / lengths.mean()), 0.15)
|
||||
|
||||
def test_image_region_clears_rgba_outside_the_selection(self):
|
||||
def test_image_mask_preserves_source_rgb_outside_the_selection(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.full((3, 3, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
|
||||
source = np.flipud(top_down).ravel()
|
||||
pixels, bounds = self.image_region.extract_region_pixels(
|
||||
selection = self.image_mask.polygon_mask(
|
||||
(3, 3), ((0, 0), (3, 0), (0, 3))
|
||||
)
|
||||
pixels, bounds = self.image_mask.edit_image_mask_pixels(
|
||||
source,
|
||||
(3, 3),
|
||||
((0, 0), (3, 0), (0, 3)),
|
||||
selection,
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((3, 3, 4)))
|
||||
width = bounds[2] - bounds[0]
|
||||
height = bounds[3] - bounds[1]
|
||||
result = np.flipud(np.asarray(pixels).reshape((height, width, 4)))
|
||||
|
||||
self.assertEqual(bounds, (0, 0, 3, 3))
|
||||
np.testing.assert_allclose(result[2, 2], (0.0, 0.0, 0.0, 0.0))
|
||||
self.assertEqual(bounds, (0, 0, 2, 2))
|
||||
np.testing.assert_allclose(result[1, 1], (0.2, 0.4, 0.6, 0.0))
|
||||
self.assertEqual(float(result[0, 0, 3]), 1.0)
|
||||
|
||||
def test_image_region_clears_hidden_rgb_from_source_transparency(self):
|
||||
def test_image_mask_preserves_hidden_rgb_from_source_transparency(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.full((2, 2, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
|
||||
top_down[0, 0, 3] = 0.0
|
||||
pixels, _bounds = self.image_region.extract_region_pixels(
|
||||
pixels, _bounds = self.image_mask.edit_image_mask_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(2, 2),
|
||||
((0, 0), (2, 0), (2, 2), (0, 2)),
|
||||
np.ones((2, 2), dtype=np.float32),
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
|
||||
|
||||
np.testing.assert_allclose(result[0, 0], (0.0, 0.0, 0.0, 0.0))
|
||||
np.testing.assert_allclose(result[0, 0], (0.2, 0.4, 0.6, 0.0))
|
||||
|
||||
def test_image_region_keeps_out_of_image_canvas_transparent(self):
|
||||
def test_image_mask_invert_runs_after_refinement(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.ones((2, 2, 4), dtype=np.float32)
|
||||
pixels, bounds = self.image_region.extract_region_pixels(
|
||||
top_down = np.full((2, 2, 4), (0.2, 0.4, 0.6, 1.0), dtype=np.float32)
|
||||
refined = np.asarray(((1.0, 0.25), (0.0, 1.0)), dtype=np.float32)
|
||||
pixels, bounds = self.image_mask.edit_image_mask_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(2, 2),
|
||||
((-1, -1), (2, -1), (2, 2), (-1, 2)),
|
||||
refined,
|
||||
invert=True,
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((3, 3, 4)))
|
||||
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
|
||||
|
||||
self.assertEqual(bounds, (-1, -1, 2, 2))
|
||||
self.assertEqual(bounds, (0, 0, 2, 2))
|
||||
self.assertEqual(float(result[0, 0, 3]), 0.0)
|
||||
self.assertEqual(float(result[1, 1, 3]), 1.0)
|
||||
self.assertAlmostEqual(float(result[0, 1, 3]), 0.75)
|
||||
np.testing.assert_allclose(result[0, 0], (0.2, 0.4, 0.6, 0.0))
|
||||
|
||||
def test_image_region_rejects_a_fully_out_of_image_region(self):
|
||||
def test_image_mask_rejects_a_fully_transparent_result(self):
|
||||
import numpy as np
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "does not overlap"):
|
||||
self.image_region.extract_region_pixels(
|
||||
with self.assertRaisesRegex(ValueError, "no visible"):
|
||||
self.image_mask.edit_image_mask_pixels(
|
||||
np.ones(2 * 2 * 4, dtype=np.float32),
|
||||
(2, 2),
|
||||
((3, 3), (4, 3), (4, 4), (3, 4)),
|
||||
np.zeros((2, 2), dtype=np.float32),
|
||||
)
|
||||
|
||||
def test_image_region_crops_to_visible_alpha_with_pixel_padding(self):
|
||||
def test_image_mask_crops_to_visible_alpha_without_padding(self):
|
||||
import numpy as np
|
||||
|
||||
rgba = np.zeros((4, 5, 4), dtype=np.float32)
|
||||
rgba[1:3, 2:4] = (0.2, 0.4, 0.6, 1.0)
|
||||
pixels, bounds = self.image_extract_common.crop_pixels_to_alpha(
|
||||
rgba = np.full((4, 5, 4), (0.8, 0.7, 0.6, 0.0), dtype=np.float32)
|
||||
rgba[1, 2] = (0.2, 0.4, 0.6, 1.0)
|
||||
rgba[2, 3] = (0.2, 0.4, 0.6, 1.0)
|
||||
pixels, bounds = self.image_edit_common.crop_pixels_to_alpha(
|
||||
np.flipud(rgba).ravel(),
|
||||
(10, 20, 15, 24),
|
||||
padding=1,
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((4, 4, 4)))
|
||||
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
|
||||
|
||||
self.assertEqual(bounds, (11, 20, 15, 24))
|
||||
self.assertEqual(result.shape, (4, 4, 4))
|
||||
self.assertTrue(np.all(result[[0, -1], :, 3] == 0.0))
|
||||
self.assertTrue(np.all(result[:, [0, -1], 3] == 0.0))
|
||||
self.assertTrue(np.all(result[1:3, 1:3, 3] == 1.0))
|
||||
self.assertEqual(bounds, (12, 21, 14, 23))
|
||||
self.assertEqual(result.shape, (2, 2, 4))
|
||||
np.testing.assert_allclose(result[0, 1], (0.8, 0.7, 0.6, 0.0))
|
||||
|
||||
def test_image_extract_tools_and_operators_are_registered(self):
|
||||
def test_image_mask_restores_refined_alpha_to_source_coordinates(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.zeros((2, 2, 4), dtype=np.float32)
|
||||
top_down[:, :, 3] = ((1.0, 0.5), (0.0, 1.0))
|
||||
mask = self.image_mask.refined_mask_from_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(2, 2),
|
||||
(1, 1, 3, 3),
|
||||
(4, 4),
|
||||
)
|
||||
|
||||
expected = np.zeros((4, 4), dtype=np.float32)
|
||||
expected[1:3, 1:3] = top_down[:, :, 3]
|
||||
np.testing.assert_allclose(mask, expected)
|
||||
|
||||
def test_image_edit_aligns_canvas_without_changing_object_transform(self):
|
||||
matrix = object()
|
||||
image_object = SimpleNamespace(
|
||||
matrix_world=matrix,
|
||||
empty_display_size=4.0,
|
||||
empty_image_offset=(0.0, 0.0),
|
||||
)
|
||||
with patch.object(
|
||||
self.image_edit_common,
|
||||
"plane_bounds",
|
||||
return_value=(-2.0, 2.0, -1.0, 1.0),
|
||||
):
|
||||
self.image_edit_common.align_empty_display_to_bounds(
|
||||
image_object,
|
||||
(100, 50),
|
||||
(50, 25),
|
||||
(50, 0, 100, 25),
|
||||
)
|
||||
|
||||
self.assertIs(image_object.matrix_world, matrix)
|
||||
self.assertEqual(image_object.empty_display_size, 2.0)
|
||||
self.assertEqual(image_object.empty_image_offset, (0.0, 0.0))
|
||||
|
||||
def test_image_edit_tools_and_operators_are_registered(self):
|
||||
import inspect
|
||||
|
||||
self.assertIn(self.image_region.ExtractImageRegion, self.addon.CLASSES)
|
||||
self.assertIn(
|
||||
self.image_region.RemoveImageRegionBackground, self.addon.CLASSES
|
||||
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.ActivateImageEditTool, self.addon.CLASSES)
|
||||
tool_cases = (
|
||||
(self.image_box.ImageBoxTool, "BOX", "ops.sculpt.border_mask"),
|
||||
(self.image_lasso.ImageLassoTool, "LASSO", "ops.sculpt.lasso_mask"),
|
||||
(self.image_line.ImageLineTool, "LINE", "ops.sculpt.line_mask"),
|
||||
(
|
||||
self.image_polyline.ImagePolylineTool,
|
||||
"POLYLINE",
|
||||
"ops.sculpt.polyline_mask",
|
||||
),
|
||||
)
|
||||
self.assertIn(self.image_region.ActivateImageExtractTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.image_lasso.ImageLassoTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.image_box.ImageBoxTool, self.addon.CLASSES)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageLassoTool.bl_operator,
|
||||
self.image_region.ExtractImageRegion.bl_idname,
|
||||
for tool, gesture, icon in tool_cases:
|
||||
self.assertNotIn(tool, self.addon.CLASSES)
|
||||
self.assertEqual(tool.bl_operator, operator.bl_idname)
|
||||
self.assertEqual(tool.bl_icon, icon)
|
||||
self.assertEqual(
|
||||
tool.bl_keymap[0][2]["properties"], [("gesture", gesture)]
|
||||
)
|
||||
self.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertIn("draw_settings", self.image_lasso.ImageLassoTool.__dict__)
|
||||
self.assertNotIn("draw_settings", self.image_line.ImageLineTool.__dict__)
|
||||
self.assertNotIn(
|
||||
"draw_settings", self.image_polyline.ImagePolylineTool.__dict__
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageLassoTool.bl_icon,
|
||||
"ops.sculpt.lasso_mask",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_box.ImageBoxTool.bl_icon,
|
||||
"ops.sculpt.border_mask",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_box.ImageBoxTool.bl_keymap[0][2]["properties"],
|
||||
[("selection_shape", "BOX")],
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageLassoTool.bl_keymap[0][2]["properties"],
|
||||
[("selection_shape", "LASSO")],
|
||||
)
|
||||
self.assertNotIn("bl_cursor", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertNotIn("bl_cursor", self.image_lasso.ImageLassoTool.__dict__)
|
||||
self.assertEqual(
|
||||
self.image_region.ExtractImageRegion.__annotations__["selection_shape"][
|
||||
"default"
|
||||
],
|
||||
"BOX",
|
||||
)
|
||||
operator = self.image_region.ExtractImageRegion
|
||||
self.assertIn("continuous", operator.__annotations__)
|
||||
self.assertFalse(operator.__annotations__["continuous"]["default"])
|
||||
self.assertIn("padding", operator.__annotations__)
|
||||
self.assertEqual(operator.__annotations__["padding"]["default"], 0)
|
||||
self.assertEqual(operator.__annotations__["gesture"]["default"], "BOX")
|
||||
self.assertIn("invert", operator.__annotations__)
|
||||
self.assertNotIn("continuous", operator.__annotations__)
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
settings = self.addon.properties.AnyImageSettings.__annotations__
|
||||
self.assertFalse(settings["image_extract_continuous"]["default"])
|
||||
self.assertEqual(settings["image_extract_padding"]["default"], 0)
|
||||
self.assertEqual(
|
||||
settings["image_extract_remove_background"]["name"],
|
||||
"Remove Background",
|
||||
)
|
||||
source_picker = inspect.getsource(
|
||||
self.image_extract_common.resolve_image_extract_click
|
||||
)
|
||||
continuous_offset = inspect.getsource(
|
||||
self.image_extract_common.offset_continuous_result
|
||||
)
|
||||
self.assertFalse(settings["image_edit_refine_selection"]["default"])
|
||||
self.assertNotIn("image_extract_continuous", settings)
|
||||
self.assertNotIn("image_extract_padding", settings)
|
||||
self.assertNotIn("image_extract_remove_background", settings)
|
||||
source_picker = inspect.getsource(self.image_edit_common.resolve_image_edit_click)
|
||||
self.assertIn("bpy.ops.view3d.select(", source_picker)
|
||||
self.assertIn("Click an Image Empty", source_picker)
|
||||
self.assertIn("_restore_selection(", source_picker)
|
||||
self.assertIn("EXTRACT_LAYER_STEP * layer", continuous_offset)
|
||||
modal_source = inspect.getsource(operator.modal)
|
||||
self.assertIn('event.type == "F"', modal_source)
|
||||
self.assertIn("self.invert = not self.invert", modal_source)
|
||||
|
||||
def test_image_extract_first_click_selects_another_image_without_extracting(self):
|
||||
def test_image_line_and_polyline_disable_refine_selection(self):
|
||||
settings = SimpleNamespace(image_edit_refine_selection=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,
|
||||
)
|
||||
self.image_edit_common.apply_image_mask_settings(operator, context)
|
||||
self.assertFalse(operator.refine_selection)
|
||||
|
||||
operator = SimpleNamespace(gesture="BOX", refine_selection=False)
|
||||
self.image_edit_common.apply_image_mask_settings(operator, context)
|
||||
self.assertTrue(operator.refine_selection)
|
||||
|
||||
def test_image_line_click_is_cancelled_before_projection(self):
|
||||
import inspect
|
||||
|
||||
modal_source = inspect.getsource(self.image_mask.EditImageMask.modal)
|
||||
projection_source = inspect.getsource(
|
||||
self.image_mask.image_line_half_plane_polygon
|
||||
)
|
||||
self.assertIn("math.dist(self._path[0], self._cursor) < 2.0", modal_source)
|
||||
self.assertIn("if len(line) != 2", projection_source)
|
||||
|
||||
def test_image_polyline_closes_from_start_or_enter(self):
|
||||
import inspect
|
||||
|
||||
modal_source = inspect.getsource(
|
||||
self.image_mask.EditImageMask._modal_polyline
|
||||
)
|
||||
draw_source = inspect.getsource(
|
||||
self.image_mask.EditImageMask._draw_selection
|
||||
)
|
||||
self.assertIn("return self._confirm_polyline(context)", modal_source)
|
||||
self.assertNotIn("DOUBLE_CLICK", modal_source)
|
||||
self.assertIn('event.type in {"RET", "NUMPAD_ENTER"}', modal_source)
|
||||
self.assertIn("_draw_polyline_close_indicator()", draw_source)
|
||||
|
||||
def test_image_edit_first_click_selects_another_image_without_editing(self):
|
||||
selected = []
|
||||
|
||||
def image(name):
|
||||
@@ -962,8 +1148,8 @@ class AddonTest(unittest.TestCase):
|
||||
clicked.name: clicked,
|
||||
}
|
||||
try:
|
||||
source, should_extract = (
|
||||
self.image_extract_common.resolve_image_extract_click(
|
||||
source, should_edit = (
|
||||
self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
)
|
||||
@@ -975,10 +1161,10 @@ class AddonTest(unittest.TestCase):
|
||||
self.fake_bpy.ops = previous_ops
|
||||
|
||||
self.assertIs(source, clicked)
|
||||
self.assertFalse(should_extract)
|
||||
self.assertFalse(should_edit)
|
||||
self.assertEqual(selected, [])
|
||||
|
||||
def test_image_extract_first_click_on_non_image_keeps_active_image(self):
|
||||
def test_image_edit_first_click_on_non_image_keeps_active_image(self):
|
||||
selection_calls = []
|
||||
active = SimpleNamespace(
|
||||
name="Active",
|
||||
@@ -1009,8 +1195,8 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
self.fake_bpy.data.objects = {active.name: active, clicked.name: clicked}
|
||||
try:
|
||||
source, should_extract = (
|
||||
self.image_extract_common.resolve_image_extract_click(
|
||||
source, should_edit = (
|
||||
self.image_edit_common.resolve_image_edit_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
)
|
||||
@@ -1022,7 +1208,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.fake_bpy.ops = previous_ops
|
||||
|
||||
self.assertIs(source, active)
|
||||
self.assertTrue(should_extract)
|
||||
self.assertTrue(should_edit)
|
||||
self.assertIs(view_objects.active, active)
|
||||
self.assertEqual(selection_calls, [False, True])
|
||||
|
||||
@@ -1045,7 +1231,7 @@ class AddonTest(unittest.TestCase):
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
def test_image_perspective_crops_alpha_and_applies_padding(self):
|
||||
def test_image_perspective_tightly_crops_alpha(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.zeros((4, 4, 4), dtype=np.float32)
|
||||
@@ -1056,16 +1242,13 @@ class AddonTest(unittest.TestCase):
|
||||
(4, 4),
|
||||
((0, 0), (4, 0), (4, 4), (0, 4)),
|
||||
1.0,
|
||||
1,
|
||||
)
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((4, 4, 4)))
|
||||
result = np.flipud(np.asarray(pixels).reshape((2, 2, 4)))
|
||||
|
||||
self.assertEqual(output_size, (4, 4))
|
||||
self.assertEqual(placement_bounds, (0.0, 0.0, 4.0, 4.0))
|
||||
self.assertTrue(np.all(result[[0, -1], :, 3] == 0.0))
|
||||
self.assertTrue(np.all(result[:, [0, -1], 3] == 0.0))
|
||||
self.assertTrue(np.all(result[1:3, 1:3, 3] == 1.0))
|
||||
self.assertEqual(output_size, (2, 2))
|
||||
self.assertEqual(placement_bounds, (1.0, 1.0, 3.0, 3.0))
|
||||
self.assertTrue(np.all(result[:, :, 3] == 1.0))
|
||||
|
||||
def test_image_perspective_rejects_non_convex_corners(self):
|
||||
with self.assertRaisesRegex(ValueError, "convex"):
|
||||
@@ -1100,7 +1283,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
def test_image_perspective_builds_polyline_mask_fill_triangles(self):
|
||||
self.assertEqual(
|
||||
self.image_perspective.triangle_fan_vertices(
|
||||
self.image_edit_common.triangle_fan_vertices(
|
||||
((0, 0), (2, 0), (2, 2), (0, 2))
|
||||
),
|
||||
(
|
||||
@@ -1153,7 +1336,7 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
self.assertAlmostEqual(ratio, 2.0)
|
||||
|
||||
def test_image_perspective_maps_padding_to_source_plane_bounds(self):
|
||||
def test_image_perspective_maps_crop_to_source_plane_bounds(self):
|
||||
bounds = self.image_perspective.perspective_placement_bounds(
|
||||
((10, 20), (30, 20), (30, 40), (10, 40)),
|
||||
(100, 50),
|
||||
@@ -1162,39 +1345,29 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(bounds, (8.0, 18.0, 32.0, 42.0))
|
||||
|
||||
def test_image_perspective_tool_uses_four_clicks_and_shared_style(self):
|
||||
def test_image_perspective_tool_uses_four_clicks_and_deform_behavior(self):
|
||||
import inspect
|
||||
|
||||
operator = self.image_perspective.ExtractImagePerspective
|
||||
operator = self.image_perspective.EditImagePerspective
|
||||
tool = self.image_perspective.ImagePerspectiveTool
|
||||
settings = self.addon.properties.AnyImageSettings.__annotations__
|
||||
|
||||
self.assertIn(operator, self.addon.CLASSES)
|
||||
self.assertIn(
|
||||
self.image_perspective.RemoveImagePerspectiveBackground,
|
||||
self.addon.CLASSES,
|
||||
)
|
||||
self.assertNotIn(tool, self.addon.CLASSES)
|
||||
self.assertEqual(tool.bl_operator, operator.bl_idname)
|
||||
self.assertEqual(tool.bl_icon, "ops.sculpt.polyline_mask")
|
||||
self.assertEqual(tool.bl_icon, "ops.sculpt.line_project")
|
||||
self.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertIn("draw_settings", tool.__dict__)
|
||||
self.assertNotIn("draw_settings", tool.__dict__)
|
||||
self.assertNotIn("image_perspective_aspect_mode", settings)
|
||||
self.assertNotIn("image_perspective_aspect_ratio", settings)
|
||||
self.assertNotIn("aspect_mode", operator.__annotations__)
|
||||
self.assertIn("aspect_ratio", operator.__annotations__)
|
||||
self.assertIn("remove_background", operator.__annotations__)
|
||||
self.assertIn("continuous", operator.__annotations__)
|
||||
self.assertIn("padding", operator.__annotations__)
|
||||
self.assertNotIn("refine_selection", operator.__annotations__)
|
||||
self.assertNotIn("continuous", operator.__annotations__)
|
||||
self.assertNotIn("padding", operator.__annotations__)
|
||||
modal_source = inspect.getsource(operator.modal)
|
||||
invoke_source = inspect.getsource(operator.invoke)
|
||||
execute_source = inspect.getsource(operator.execute)
|
||||
background_request_source = inspect.getsource(
|
||||
self.image_perspective.RemoveImagePerspectiveBackground.request
|
||||
)
|
||||
background_response_source = inspect.getsource(
|
||||
self.image_perspective.RemoveImagePerspectiveBackground.response
|
||||
)
|
||||
draw_source = inspect.getsource(operator._draw_overlay)
|
||||
preview_source = inspect.getsource(operator._draw_preview)
|
||||
finish_source = inspect.getsource(operator._finish)
|
||||
@@ -1208,17 +1381,9 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertIn("active_view3d_tool_id(context)", modal_source)
|
||||
self.assertIn("drawing_in_region(", modal_source)
|
||||
self.assertNotIn("context.region_data.is_perspective", invoke_source)
|
||||
self.assertIn("resolve_image_extract_click(", invoke_source)
|
||||
self.assertIn("apply_image_extract_settings(self, context)", invoke_source)
|
||||
self.assertIn(
|
||||
"bpy.ops.anyimage.remove_image_perspective_background(",
|
||||
execute_source,
|
||||
)
|
||||
self.assertIn("resolve_image_edit_click(", invoke_source)
|
||||
self.assertIn("extract_perspective_pixels(", execute_source)
|
||||
self.assertIn("create_perspective_result(", execute_source)
|
||||
self.assertIn("image_extract_background_request(", background_request_source)
|
||||
self.assertIn("placement_polygon=polygon", background_response_source)
|
||||
self.assertIn("self.padding", background_response_source)
|
||||
self.assertIn("draw_polygon_fill(vertices)", draw_source)
|
||||
self.assertIn("self._draw_preview()", draw_source)
|
||||
self.assertIn("draw_texture_2d(", preview_source)
|
||||
@@ -1230,16 +1395,16 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
def test_image_box_builds_a_rectangular_screen_path(self):
|
||||
self.assertEqual(
|
||||
self.image_region.box_screen_path(((2, 3), (8, 9))),
|
||||
self.image_mask.box_screen_path((2, 3), (8, 9)),
|
||||
((2, 3), (8, 3), (8, 9), (2, 9)),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_region.box_screen_path(((2, 3), (3, 9))),
|
||||
self.image_mask.box_screen_path((2, 3), (3, 9)),
|
||||
(),
|
||||
)
|
||||
|
||||
def test_image_extract_selection_uses_white_dashed_segments(self):
|
||||
segments = self.image_extract_common.dashed_line_segments(
|
||||
def test_image_edit_selection_uses_white_dashed_segments(self):
|
||||
segments = self.image_edit_common.dashed_line_segments(
|
||||
((0, 0), (10, 0)), dash_length=3.0, gap_length=2.0
|
||||
)
|
||||
|
||||
@@ -1248,22 +1413,29 @@ class AddonTest(unittest.TestCase):
|
||||
((0.0, 0.0), (3.0, 0.0), (5.0, 0.0), (8.0, 0.0)),
|
||||
)
|
||||
|
||||
def test_image_extract_activation_defaults_to_box(self):
|
||||
def test_image_edit_activation_defaults_to_box(self):
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(self.image_region.ActivateImageExtractTool.execute)
|
||||
source = inspect.getsource(self.image_mask.ActivateImageEditTool.execute)
|
||||
self.assertIn('name="anyimage.image_box"', source)
|
||||
|
||||
def test_continuous_keeps_the_source_empty_selected(self):
|
||||
import inspect
|
||||
def test_image_line_snaps_and_keeps_the_previewed_half_plane(self):
|
||||
import math
|
||||
|
||||
execute_source = inspect.getsource(self.image_region.ExtractImageRegion.execute)
|
||||
response_source = inspect.getsource(
|
||||
self.image_region.RemoveImageRegionBackground.response
|
||||
endpoint = self.image_mask.snapped_line_endpoint((0, 0), (10, 1), True)
|
||||
self.assertAlmostEqual(math.degrees(math.atan2(endpoint[1], endpoint[0])), 5.0)
|
||||
preview = self.image_mask.line_half_plane_polygon(
|
||||
(0, 5), (10, 5), (10, 10)
|
||||
)
|
||||
image = self.image_mask.image_line_half_plane_polygon(
|
||||
((0, 5), (10, 5)), (10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_edit_common.polygon_bounds(preview), (0, 5, 10, 10)
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_edit_common.polygon_bounds(image), (0, 0, 10, 5)
|
||||
)
|
||||
expected = "select_image_extract_result("
|
||||
self.assertIn(expected, execute_source)
|
||||
self.assertIn(expected, response_source)
|
||||
|
||||
def test_balloon_interior_points_have_uniform_spacing(self):
|
||||
import numpy as np
|
||||
@@ -2160,12 +2332,25 @@ class AddonTest(unittest.TestCase):
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_perspective.ImagePerspectiveTool,
|
||||
self.image_line.ImageLineTool,
|
||||
{
|
||||
"after": {self.image_lasso.ImageLassoTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_polyline.ImagePolylineTool,
|
||||
{
|
||||
"after": {self.image_line.ImageLineTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_perspective.ImagePerspectiveTool,
|
||||
{
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.balloon.BalloonTool,
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user