Add image extraction functionality with background removal
- Implemented ExtractImageRegion operator for extracting image regions using box or lasso selection. - Added RemoveImageRegionBackground job for background removal from selected image regions. - Created utility functions for polygon bounds, selection canvas creation, and applying selection masks. - Integrated image processing using PIL and numpy for efficient pixel manipulation. - Enhanced user experience with modal drawing and status updates during extraction.
This commit is contained in:
@@ -95,7 +95,21 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
cls.image_background = importlib.import_module("anyimage.operators.background")
|
||||
cls.image_mesh = importlib.import_module("anyimage.operators.surface")
|
||||
cls.image_lasso = importlib.import_module("anyimage.operators.image_lasso")
|
||||
cls.image_extract_common = importlib.import_module(
|
||||
"anyimage.operators.image_extract.common"
|
||||
)
|
||||
cls.image_region = importlib.import_module(
|
||||
"anyimage.operators.image_extract.region"
|
||||
)
|
||||
cls.image_box = importlib.import_module(
|
||||
"anyimage.operators.image_extract.box"
|
||||
)
|
||||
cls.image_lasso = importlib.import_module(
|
||||
"anyimage.operators.image_extract.lasso"
|
||||
)
|
||||
cls.image_perspective = importlib.import_module(
|
||||
"anyimage.operators.image_extract.perspective"
|
||||
)
|
||||
cls.balloon = importlib.import_module("anyimage.operators.balloon.operator")
|
||||
cls.balloon_geometry = importlib.import_module(
|
||||
"anyimage.operators.balloon.geometry"
|
||||
@@ -779,12 +793,12 @@ 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_lasso_clears_rgba_outside_the_selection(self):
|
||||
def test_image_region_clears_rgba_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_lasso.extract_lasso_pixels(
|
||||
pixels, bounds = self.image_region.extract_region_pixels(
|
||||
source,
|
||||
(3, 3),
|
||||
((0, 0), (3, 0), (0, 3)),
|
||||
@@ -795,12 +809,12 @@ class AddonTest(unittest.TestCase):
|
||||
np.testing.assert_allclose(result[2, 2], (0.0, 0.0, 0.0, 0.0))
|
||||
self.assertEqual(float(result[0, 0, 3]), 1.0)
|
||||
|
||||
def test_image_lasso_clears_hidden_rgb_from_source_transparency(self):
|
||||
def test_image_region_clears_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_lasso.extract_lasso_pixels(
|
||||
pixels, _bounds = self.image_region.extract_region_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(2, 2),
|
||||
((0, 0), (2, 0), (2, 2), (0, 2)),
|
||||
@@ -809,11 +823,11 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
np.testing.assert_allclose(result[0, 0], (0.0, 0.0, 0.0, 0.0))
|
||||
|
||||
def test_image_lasso_keeps_out_of_image_canvas_transparent(self):
|
||||
def test_image_region_keeps_out_of_image_canvas_transparent(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.ones((2, 2, 4), dtype=np.float32)
|
||||
pixels, bounds = self.image_lasso.extract_lasso_pixels(
|
||||
pixels, bounds = self.image_region.extract_region_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(2, 2),
|
||||
((-1, -1), (2, -1), (2, 2), (-1, 2)),
|
||||
@@ -824,22 +838,22 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(float(result[0, 0, 3]), 0.0)
|
||||
self.assertEqual(float(result[1, 1, 3]), 1.0)
|
||||
|
||||
def test_image_lasso_rejects_a_fully_out_of_image_region(self):
|
||||
def test_image_region_rejects_a_fully_out_of_image_region(self):
|
||||
import numpy as np
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "does not overlap"):
|
||||
self.image_lasso.extract_lasso_pixels(
|
||||
self.image_region.extract_region_pixels(
|
||||
np.ones(2 * 2 * 4, dtype=np.float32),
|
||||
(2, 2),
|
||||
((3, 3), (4, 3), (4, 4), (3, 4)),
|
||||
)
|
||||
|
||||
def test_image_lasso_crops_to_visible_alpha_with_pixel_padding(self):
|
||||
def test_image_region_crops_to_visible_alpha_with_pixel_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_lasso.crop_pixels_to_alpha(
|
||||
pixels, bounds = self.image_extract_common.crop_pixels_to_alpha(
|
||||
np.flipud(rgba).ravel(),
|
||||
(10, 20, 15, 24),
|
||||
padding=1,
|
||||
@@ -852,61 +866,380 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertTrue(np.all(result[:, [0, -1], 3] == 0.0))
|
||||
self.assertTrue(np.all(result[1:3, 1:3, 3] == 1.0))
|
||||
|
||||
def test_image_lasso_tool_and_operators_are_registered(self):
|
||||
self.assertIn(self.image_lasso.ExtractImageLasso, self.addon.CLASSES)
|
||||
self.assertIn(self.image_lasso.AnalyzeImageLasso, self.addon.CLASSES)
|
||||
self.assertIn(self.image_lasso.ActivateImageLassoTool, self.addon.CLASSES)
|
||||
def test_image_extract_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
|
||||
)
|
||||
self.assertIn(self.image_region.ActivateImageExtractTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.image_lasso.ImageLassoTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.image_lasso.ImageBoxTool, self.addon.CLASSES)
|
||||
self.assertNotIn(self.image_box.ImageBoxTool, self.addon.CLASSES)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageLassoTool.bl_operator,
|
||||
self.image_lasso.ExtractImageLasso.bl_idname,
|
||||
self.image_region.ExtractImageRegion.bl_idname,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageLassoTool.bl_icon,
|
||||
"ops.sculpt.lasso_mask",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageBoxTool.bl_icon,
|
||||
self.image_box.ImageBoxTool.bl_icon,
|
||||
"ops.sculpt.border_mask",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ImageBoxTool.bl_keymap[0][2]["properties"],
|
||||
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_lasso.ImageBoxTool.__dict__)
|
||||
self.assertNotIn("bl_cursor", self.image_box.ImageBoxTool.__dict__)
|
||||
self.assertNotIn("bl_cursor", self.image_lasso.ImageLassoTool.__dict__)
|
||||
self.assertEqual(
|
||||
self.image_lasso.ExtractImageLasso.__annotations__["selection_shape"][
|
||||
self.image_region.ExtractImageRegion.__annotations__["selection_shape"][
|
||||
"default"
|
||||
],
|
||||
"BOX",
|
||||
)
|
||||
operator = self.image_lasso.ExtractImageLasso
|
||||
self.assertIn("keep_original", operator.__annotations__)
|
||||
self.assertFalse(operator.__annotations__["keep_original"]["default"])
|
||||
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)
|
||||
settings = self.addon.properties.AnyImageSettings.__annotations__
|
||||
self.assertFalse(settings["image_lasso_keep_original"]["default"])
|
||||
self.assertEqual(settings["image_lasso_padding"]["default"], 0)
|
||||
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.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)
|
||||
|
||||
def test_image_extract_first_click_selects_another_image_without_extracting(self):
|
||||
selected = []
|
||||
|
||||
def image(name):
|
||||
return SimpleNamespace(
|
||||
name=name,
|
||||
type="EMPTY",
|
||||
empty_display_type="IMAGE",
|
||||
data=object(),
|
||||
select_set=lambda value: selected.append((name, value)),
|
||||
)
|
||||
|
||||
active = image("Active")
|
||||
clicked = image("Clicked")
|
||||
context = SimpleNamespace(
|
||||
active_object=active,
|
||||
selected_objects=(active,),
|
||||
view_layer=SimpleNamespace(objects=SimpleNamespace(active=active)),
|
||||
)
|
||||
event = SimpleNamespace(mouse_region_x=10, mouse_region_y=20)
|
||||
previous_ops = getattr(self.fake_bpy, "ops", None)
|
||||
previous_objects = self.fake_bpy.data.objects
|
||||
|
||||
def select(**_kwargs):
|
||||
context.active_object = clicked
|
||||
|
||||
self.fake_bpy.ops = SimpleNamespace(
|
||||
view3d=SimpleNamespace(select=select),
|
||||
object=SimpleNamespace(select_all=lambda **_kwargs: None),
|
||||
)
|
||||
self.fake_bpy.data.objects = {
|
||||
active.name: active,
|
||||
clicked.name: clicked,
|
||||
}
|
||||
try:
|
||||
source, should_extract = (
|
||||
self.image_extract_common.resolve_image_extract_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
)
|
||||
finally:
|
||||
self.fake_bpy.data.objects = previous_objects
|
||||
if previous_ops is None:
|
||||
del self.fake_bpy.ops
|
||||
else:
|
||||
self.fake_bpy.ops = previous_ops
|
||||
|
||||
self.assertIs(source, clicked)
|
||||
self.assertFalse(should_extract)
|
||||
self.assertEqual(selected, [])
|
||||
|
||||
def test_image_extract_first_click_on_non_image_keeps_active_image(self):
|
||||
selection_calls = []
|
||||
active = SimpleNamespace(
|
||||
name="Active",
|
||||
type="EMPTY",
|
||||
empty_display_type="IMAGE",
|
||||
data=object(),
|
||||
select_set=lambda value: selection_calls.append(value),
|
||||
)
|
||||
clicked = SimpleNamespace(name="Mesh", type="MESH", data=object())
|
||||
view_objects = SimpleNamespace(active=active)
|
||||
context = SimpleNamespace(
|
||||
active_object=active,
|
||||
selected_objects=(active,),
|
||||
view_layer=SimpleNamespace(objects=view_objects),
|
||||
)
|
||||
event = SimpleNamespace(mouse_region_x=10, mouse_region_y=20)
|
||||
previous_ops = getattr(self.fake_bpy, "ops", None)
|
||||
previous_objects = self.fake_bpy.data.objects
|
||||
|
||||
def select(**_kwargs):
|
||||
context.active_object = clicked
|
||||
|
||||
self.fake_bpy.ops = SimpleNamespace(
|
||||
view3d=SimpleNamespace(select=select),
|
||||
object=SimpleNamespace(
|
||||
select_all=lambda **_kwargs: selection_calls.append(False)
|
||||
),
|
||||
)
|
||||
self.fake_bpy.data.objects = {active.name: active, clicked.name: clicked}
|
||||
try:
|
||||
source, should_extract = (
|
||||
self.image_extract_common.resolve_image_extract_click(
|
||||
context, event, lambda *_args: None
|
||||
)
|
||||
)
|
||||
finally:
|
||||
self.fake_bpy.data.objects = previous_objects
|
||||
if previous_ops is None:
|
||||
del self.fake_bpy.ops
|
||||
else:
|
||||
self.fake_bpy.ops = previous_ops
|
||||
|
||||
self.assertIs(source, active)
|
||||
self.assertTrue(should_extract)
|
||||
self.assertIs(view_objects.active, active)
|
||||
self.assertEqual(selection_calls, [False, True])
|
||||
|
||||
def test_image_perspective_rectifies_identity_pixels(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.arange(3 * 4 * 4, dtype=np.float32).reshape((3, 4, 4))
|
||||
top_down /= float(top_down.max())
|
||||
top_down[:, :, 3] = 1.0
|
||||
result = self.image_perspective.warp_perspective_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(4, 3),
|
||||
((0, 0), (4, 0), (4, 3), (0, 3)),
|
||||
(4, 3),
|
||||
)
|
||||
|
||||
np.testing.assert_allclose(
|
||||
np.flipud(np.asarray(result).reshape((3, 4, 4))),
|
||||
top_down,
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
def test_image_perspective_crops_alpha_and_applies_padding(self):
|
||||
import numpy as np
|
||||
|
||||
top_down = np.zeros((4, 4, 4), dtype=np.float32)
|
||||
top_down[1:3, 1:3] = (0.2, 0.4, 0.6, 1.0)
|
||||
pixels, output_size, placement_bounds = (
|
||||
self.image_perspective.extract_perspective_pixels(
|
||||
np.flipud(top_down).ravel(),
|
||||
(4, 4),
|
||||
((0, 0), (4, 0), (4, 4), (0, 4)),
|
||||
1.0,
|
||||
1,
|
||||
)
|
||||
)
|
||||
result = np.flipud(np.asarray(pixels).reshape((4, 4, 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))
|
||||
|
||||
def test_image_perspective_rejects_non_convex_corners(self):
|
||||
with self.assertRaisesRegex(ValueError, "convex"):
|
||||
self.image_perspective.validate_perspective_quad(
|
||||
((0, 0), (10, 0), (4, 4), (0, 10))
|
||||
)
|
||||
|
||||
def test_image_perspective_corner_order_is_click_order_independent(self):
|
||||
import itertools
|
||||
import numpy as np
|
||||
|
||||
corners = ((10, 20), (80, 15), (90, 70), (5, 75))
|
||||
ordered = self.image_perspective.canonical_perspective_quad(corners)
|
||||
for permutation in itertools.permutations(corners):
|
||||
np.testing.assert_allclose(
|
||||
self.image_perspective.canonical_perspective_quad(permutation),
|
||||
ordered,
|
||||
)
|
||||
|
||||
def test_image_perspective_detects_source_image_overlap(self):
|
||||
overlaps = self.image_perspective.perspective_quad_overlaps_image
|
||||
|
||||
self.assertFalse(
|
||||
overlaps(((20, 20), (30, 20), (30, 30), (20, 30)), (10, 10))
|
||||
)
|
||||
self.assertTrue(
|
||||
overlaps(((5, 5), (15, 5), (15, 15), (5, 15)), (10, 10))
|
||||
)
|
||||
self.assertTrue(
|
||||
overlaps(((-5, -5), (15, -5), (15, 15), (-5, 15)), (10, 10))
|
||||
)
|
||||
|
||||
def test_image_perspective_builds_polyline_mask_fill_triangles(self):
|
||||
self.assertEqual(
|
||||
self.image_perspective.triangle_fan_vertices(
|
||||
((0, 0), (2, 0), (2, 2), (0, 2))
|
||||
),
|
||||
(
|
||||
(0.0, 0.0),
|
||||
(2.0, 0.0),
|
||||
(2.0, 2.0),
|
||||
(0.0, 0.0),
|
||||
(2.0, 2.0),
|
||||
(0.0, 2.0),
|
||||
),
|
||||
)
|
||||
|
||||
def test_image_perspective_limits_output_pixel_count(self):
|
||||
width, height = self.image_perspective.perspective_output_size(
|
||||
((0, 0), (10000, 0), (10000, 10000), (0, 10000)),
|
||||
1.0,
|
||||
)
|
||||
|
||||
self.assertLessEqual(
|
||||
width * height, self.image_perspective.MAX_OUTPUT_PIXELS
|
||||
)
|
||||
|
||||
def test_image_perspective_mouse_adjusts_preview_aspect_exponentially(self):
|
||||
adjust = self.image_perspective.aspect_ratio_from_mouse
|
||||
|
||||
self.assertAlmostEqual(adjust(1.5, 240.0), 3.0)
|
||||
self.assertAlmostEqual(adjust(1.5, -240.0), 0.75)
|
||||
self.assertEqual(
|
||||
adjust(1.0, 100000.0),
|
||||
self.image_perspective.MAX_INTERACTIVE_ASPECT,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_perspective.snapped_aspect_ratio(1.7),
|
||||
(16.0 / 9.0, "16 : 9"),
|
||||
)
|
||||
|
||||
def test_image_perspective_preview_is_centered_at_requested_aspect(self):
|
||||
left, bottom, width, height = self.image_perspective.preview_draw_bounds(
|
||||
(1000, 800), 2.0
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(width / height, 2.0)
|
||||
self.assertAlmostEqual(left + width * 0.5, 500.0)
|
||||
self.assertAlmostEqual(bottom + height * 0.5, 400.0)
|
||||
|
||||
def test_image_perspective_uses_quad_edges_for_initial_aspect(self):
|
||||
ratio = self.image_perspective.perspective_quad_aspect(
|
||||
((0, 0), (8, 0), (8, 4), (0, 4))
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(ratio, 2.0)
|
||||
|
||||
def test_image_perspective_maps_padding_to_source_plane_bounds(self):
|
||||
bounds = self.image_perspective.perspective_placement_bounds(
|
||||
((10, 20), (30, 20), (30, 40), (10, 40)),
|
||||
(100, 50),
|
||||
(-10, -5, 110, 55),
|
||||
)
|
||||
|
||||
self.assertEqual(bounds, (8.0, 18.0, 32.0, 42.0))
|
||||
|
||||
def test_image_perspective_tool_uses_four_clicks_and_shared_style(self):
|
||||
import inspect
|
||||
|
||||
operator = self.image_perspective.ExtractImagePerspective
|
||||
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.assertNotIn("bl_cursor", tool.__dict__)
|
||||
self.assertIn("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__)
|
||||
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)
|
||||
self.assertIn("if len(self._points) < 4", modal_source)
|
||||
self.assertIn("return self.execute(context)", modal_source)
|
||||
self.assertIn('self._phase = "ASPECT"', modal_source)
|
||||
self.assertIn("create_perspective_preview_image(", modal_source)
|
||||
self.assertIn("self._update_aspect(context, event)", modal_source)
|
||||
self.assertIn("context.window.cursor_warp(", modal_source)
|
||||
self.assertIn("perspective_quad_aspect(polygon)", modal_source)
|
||||
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("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)
|
||||
self.assertIn("draw_preview_frame(", preview_source)
|
||||
self.assertIn("draw_aspect_hud(", preview_source)
|
||||
self.assertIn("event_timer_remove", finish_source)
|
||||
self.assertIn("self._remove_preview()", finish_source)
|
||||
self.assertNotIn('"RET"', modal_source)
|
||||
|
||||
def test_image_box_builds_a_rectangular_screen_path(self):
|
||||
self.assertEqual(
|
||||
self.image_lasso.box_screen_path(((2, 3), (8, 9))),
|
||||
self.image_region.box_screen_path(((2, 3), (8, 9))),
|
||||
((2, 3), (8, 3), (8, 9), (2, 9)),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.image_lasso.box_screen_path(((2, 3), (3, 9))),
|
||||
self.image_region.box_screen_path(((2, 3), (3, 9))),
|
||||
(),
|
||||
)
|
||||
|
||||
def test_image_extract_selection_uses_white_dashed_segments(self):
|
||||
segments = self.image_lasso.dashed_line_segments(
|
||||
segments = self.image_extract_common.dashed_line_segments(
|
||||
((0, 0), (10, 0)), dash_length=3.0, gap_length=2.0
|
||||
)
|
||||
|
||||
@@ -918,15 +1251,17 @@ class AddonTest(unittest.TestCase):
|
||||
def test_image_extract_activation_defaults_to_box(self):
|
||||
import inspect
|
||||
|
||||
source = inspect.getsource(self.image_lasso.ActivateImageLassoTool.execute)
|
||||
self.assertIn("name=ImageBoxTool.bl_idname", source)
|
||||
source = inspect.getsource(self.image_region.ActivateImageExtractTool.execute)
|
||||
self.assertIn('name="anyimage.image_box"', source)
|
||||
|
||||
def test_keep_original_restores_the_source_empty_selection(self):
|
||||
def test_continuous_keeps_the_source_empty_selected(self):
|
||||
import inspect
|
||||
|
||||
execute_source = inspect.getsource(self.image_lasso.ExtractImageLasso.execute)
|
||||
response_source = inspect.getsource(self.image_lasso.AnalyzeImageLasso.response)
|
||||
expected = "source_object if self.keep_original else target"
|
||||
execute_source = inspect.getsource(self.image_region.ExtractImageRegion.execute)
|
||||
response_source = inspect.getsource(
|
||||
self.image_region.RemoveImageRegionBackground.response
|
||||
)
|
||||
expected = "select_image_extract_result("
|
||||
self.assertIn(expected, execute_source)
|
||||
self.assertIn(expected, response_source)
|
||||
|
||||
@@ -1811,7 +2146,7 @@ class AddonTest(unittest.TestCase):
|
||||
self.registered_tools,
|
||||
[
|
||||
(
|
||||
self.image_lasso.ImageBoxTool,
|
||||
self.image_box.ImageBoxTool,
|
||||
{
|
||||
"group": True,
|
||||
"separator": True,
|
||||
@@ -1820,7 +2155,14 @@ class AddonTest(unittest.TestCase):
|
||||
(
|
||||
self.image_lasso.ImageLassoTool,
|
||||
{
|
||||
"after": {self.image_lasso.ImageBoxTool.bl_idname},
|
||||
"after": {self.image_box.ImageBoxTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
(
|
||||
self.image_perspective.ImagePerspectiveTool,
|
||||
{
|
||||
"after": {self.image_lasso.ImageLassoTool.bl_idname},
|
||||
"separator": False,
|
||||
},
|
||||
),
|
||||
|
||||
@@ -68,7 +68,14 @@ class PackTest(unittest.TestCase):
|
||||
self.assertIn("operators/mesh_plane.py", names)
|
||||
self.assertIn("operators/image.py", names)
|
||||
self.assertIn("operators/balloon/operator.py", names)
|
||||
self.assertIn("operators/image_lasso.py", names)
|
||||
self.assertIn("operators/image_extract/__init__.py", names)
|
||||
self.assertIn("operators/image_extract/common.py", names)
|
||||
self.assertIn("operators/image_extract/region.py", names)
|
||||
self.assertIn("operators/image_extract/box.py", names)
|
||||
self.assertIn("operators/image_extract/lasso.py", names)
|
||||
self.assertIn("operators/image_extract/perspective.py", names)
|
||||
self.assertNotIn("operators/image_lasso.py", names)
|
||||
self.assertNotIn("operators/image_perspective.py", names)
|
||||
self.assertIn("operators/surface.py", names)
|
||||
self.assertIn(
|
||||
"assets/O_AnyImage.blend",
|
||||
@@ -79,7 +86,8 @@ class PackTest(unittest.TestCase):
|
||||
self.assertIn("server/geometry/grid_mesh.py", names)
|
||||
self.assertIn("server/geometry/surface.py", names)
|
||||
self.assertIn("server/jobs/background.py", names)
|
||||
self.assertIn("server/jobs/image_lasso.py", names)
|
||||
self.assertIn("server/jobs/image_extract.py", names)
|
||||
self.assertNotIn("server/jobs/image_lasso.py", names)
|
||||
self.assertIn("server/models/ben2.py", names)
|
||||
self.assertIn("server/models/da3.py", names)
|
||||
self.assertIn("server/models/moge2.py", names)
|
||||
|
||||
@@ -220,7 +220,7 @@ class ServerTest(unittest.TestCase):
|
||||
"download-model",
|
||||
"download-required-models",
|
||||
"analyze-balloon",
|
||||
"analyze-image-lasso",
|
||||
"extract-image-background",
|
||||
"remove-background",
|
||||
"extract-frames",
|
||||
"generate-depth",
|
||||
@@ -401,16 +401,16 @@ class ServerTest(unittest.TestCase):
|
||||
self.assertEqual(tuple(result[0, 0]), (20, 40, 60, 64))
|
||||
self.assertEqual(tuple(result[3, 3]), (0, 0, 0, 0))
|
||||
|
||||
def test_image_lasso_canvas_clears_rgba_outside_the_selection(self):
|
||||
def test_image_extract_canvas_clears_rgba_outside_the_selection(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
image_lasso = sys.modules["server.jobs.image_lasso"]
|
||||
image_extract = sys.modules["server.jobs.image_extract"]
|
||||
source_path = self.storage_root / "source.png"
|
||||
pixels = np.full((2, 2, 4), (20, 40, 60, 255), dtype=np.uint8)
|
||||
Image.fromarray(pixels, mode="RGBA").save(source_path)
|
||||
|
||||
canvas, _selection, bounds = image_lasso.create_lasso_canvas(
|
||||
canvas, _selection, bounds = image_extract.create_selection_canvas(
|
||||
source_path,
|
||||
((-1, -1), (2, -1), (-1, 2)),
|
||||
)
|
||||
@@ -421,14 +421,14 @@ class ServerTest(unittest.TestCase):
|
||||
self.assertEqual(tuple(result[2, 2]), (0, 0, 0, 0))
|
||||
self.assertEqual(tuple(result[1, 1]), (20, 40, 60, 255))
|
||||
|
||||
def test_image_lasso_crops_to_alpha_with_pixel_padding(self):
|
||||
def test_image_extract_crops_to_alpha_with_pixel_padding(self):
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
image_lasso = sys.modules["server.jobs.image_lasso"]
|
||||
image_extract = sys.modules["server.jobs.image_extract"]
|
||||
pixels = np.zeros((4, 5, 4), dtype=np.uint8)
|
||||
pixels[1:3, 2:4] = (20, 40, 60, 255)
|
||||
result, bounds = image_lasso.crop_to_alpha(
|
||||
result, bounds = image_extract.crop_to_alpha(
|
||||
Image.fromarray(pixels, mode="RGBA"),
|
||||
(10, 20, 15, 24),
|
||||
padding=1,
|
||||
|
||||
Reference in New Issue
Block a user