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:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user