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:
2026-08-22 15:47:34 +08:00
parent 988332548e
commit f42cba1932
27 changed files with 2513 additions and 761 deletions

View File

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

View File

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

View File

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