feat(cutout_tool): Implement cutout region to shape functionality

- Added interaction helpers for cutout region validation and shape menu interaction.
- Created main operator for converting image regions into cutout shapes, including AI artifact generation.
- Introduced object creation logic for cutout shapes, handling depth and normal textures.
- Defined cutout shape identifiers and node groups for various shape types.
- Implemented depth texture writing and prediction artifact generation for MoGe-2.
- Developed server jobs for generating cutout artifacts, depth planes, and refining image regions.
- Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
2026-08-31 11:36:27 +08:00
parent 08309f719d
commit 87d7721128
80 changed files with 4823 additions and 4297 deletions

View File

@@ -1,4 +1,5 @@
import importlib.util
import json
import sys
import tempfile
import threading
@@ -41,8 +42,8 @@ class ServerTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server_module = load_server_module()
importlib.import_module("server.geometry.generate_depth")
importlib.import_module("server.jobs.generate_region_data")
importlib.import_module("server.geometry.prediction_artifacts")
importlib.import_module("server.jobs.cutout")
from blendjob import JobServer
from server.model_manager import ModelManager, ModelSessionCache
@@ -258,11 +259,11 @@ class ServerTest(unittest.TestCase):
{
"download-model",
"download-required-models",
"refine-image-mask",
"refine-image-region",
"remove-background",
"upscale-image",
"generate-depth-plane",
"generate-region-data",
"generate-depth-plane-geometry",
"generate-cutout-artifacts",
"debug-da3",
"debug-moge2",
"debug-ben2",
@@ -386,23 +387,12 @@ class ServerTest(unittest.TestCase):
)
self.assertEqual(events, ["clear", "close"])
def test_region_transparent_fraction_uses_visible_alpha_threshold(self):
import numpy as np
from PIL import Image
depth = sys.modules["server.geometry.generate_depth"]
output_path = self.storage_root / "region.png"
pixels = np.full((10, 10, 4), 255, dtype=np.uint8)
pixels.reshape(-1, 4)[:5, 3] = 25
Image.fromarray(pixels, mode="RGBA").save(output_path)
self.assertEqual(depth.transparent_fraction(output_path), 0.05)
def test_image_geometry_keeps_model_and_region_validity_separate(self):
def test_depth_artifacts_store_texture_and_compact_metadata(self):
import numpy as np
depth = sys.modules["server.geometry.generate_depth"]
output_path = self.storage_root / "image_geometry.npz"
depth = importlib.import_module("server.geometry.depth_texture")
texture_path = self.storage_root / "z-depth.exr"
metadata_path = self.storage_root / "depth.json"
valid = np.asarray(((True, True, False), (True, True, True)))
calibration = np.asarray(((False, True, False), (False, False, True)))
frame = SimpleNamespace(
@@ -413,19 +403,60 @@ class ServerTest(unittest.TestCase):
model_mask=None,
)
depth._write_image_geometry_archive(frame, output_path, calibration, 0.05)
depth.write_z_depth_texture(frame.depth, frame.valid_mask, texture_path)
depth.write_depth_metadata(frame, calibration, metadata_path)
with np.load(output_path) as archive:
np.testing.assert_array_equal(archive["valid"], valid)
np.testing.assert_array_equal(archive["calibration_valid"], calibration)
self.assertAlmostEqual(
float(archive["region_transparent_fraction"]),
0.05,
places=6,
)
self.assertTrue(texture_path.is_file())
self.assertEqual(texture_path.read_bytes()[:4], b"v/1\x01")
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
self.assertEqual(metadata["image_size"], [3, 2])
self.assertEqual(metadata["intrinsics"], np.eye(3).tolist())
self.assertAlmostEqual(metadata["reference_depth"], 4.0)
def test_generate_region_data_only_returns_requested_outputs(self):
job = sys.modules["server.jobs.generate_region_data"]
def test_object_normal_texture_matches_depth_surface_axes(self):
import numpy as np
from PIL import Image
normal_texture = importlib.import_module("server.geometry.normal_texture")
output_path = self.storage_root / "object-normal.png"
camera_normals = np.asarray(
[[
(0.0, 0.0, -1.0),
(1.0, 0.0, 0.0),
(0.0, 1.0, 0.0),
(0.0, 0.0, -1.0),
]],
dtype=np.float32,
)
valid = np.asarray([[True, True, True, False]], dtype=bool)
normal_texture.write_camera_normal_texture(
camera_normals,
valid,
(0, 0),
(4, 1),
(4, 1),
output_path,
normal_mode="OBJECT",
)
with Image.open(output_path) as opened:
encoded = np.asarray(opened, dtype=np.uint8)
np.testing.assert_array_equal(
encoded,
np.asarray(
[[
(0, 127, 127),
(127, 0, 127),
(127, 127, 0),
(0, 128, 128),
]],
dtype=np.uint8,
),
)
def test_generate_cutout_artifacts_only_returns_requested_outputs(self):
job = sys.modules["server.jobs.cutout"]
input_path = self.storage_root / "input.png"
input_path.write_bytes(b"region")
context = SimpleNamespace(
@@ -433,40 +464,49 @@ class ServerTest(unittest.TestCase):
progress=lambda *_args: None,
check_cancelled=lambda: None,
)
archive = context.directory / "image_geometry.npz"
normal = context.directory / "normal.png"
vector_depth = context.directory / "vector-depth.exr"
metadata = context.directory / "depth.json"
normal = context.directory / "object-normal.png"
with patch.object(
job,
"generate_depth",
return_value=(archive, normal),
"generate_moge2_artifacts",
return_value={
"vector_depth": vector_depth,
"depth_metadata": metadata,
"object_normal": normal,
},
) as generate:
result = job.run(
context,
{
"input": str(input_path),
"output_depth": True,
"depth_mode": "VECTOR",
"normal_mode": "OBJECT",
},
)
self.assertEqual(
result,
{"image_geometry": "image_geometry.npz", "normal": "normal.png"},
{
"vector_depth": "vector-depth.exr",
"depth_metadata": "depth.json",
"object_normal": "object-normal.png",
},
)
generate.assert_called_once_with(
context,
{
"input": str(input_path),
"output_depth": True,
"depth_mode": "VECTOR",
"normal_mode": "OBJECT",
},
input_path,
output_depth=True,
depth_mode="VECTOR",
normal_mode="OBJECT",
)
def test_generate_region_data_refines_an_already_rasterized_region(self):
job = sys.modules["server.jobs.generate_region_data"]
def test_generate_cutout_artifacts_refine_an_already_rasterized_region(self):
job = sys.modules["server.jobs.cutout"]
input_path = self.storage_root / "input.png"
input_path.write_bytes(b"region")
directory = self.storage_root / "job"
@@ -480,7 +520,7 @@ class ServerTest(unittest.TestCase):
output.write_bytes(b"refined")
return output, (2, 3, 8, 9)
with patch.object(job, "generate_selection", side_effect=generate):
with patch.object(job, "refine_region", side_effect=generate):
result = job.run(
context,
{
@@ -493,7 +533,7 @@ class ServerTest(unittest.TestCase):
self.assertEqual(
result,
{"region": "region.png", "region_bounds": "region_bounds.json"},
{"region_image": "region.png", "region_bounds": "region_bounds.json"},
)
self.assertEqual(
(directory / "region_bounds.json").read_text(encoding="utf-8"),