Add image selection refinement functionality with BEN2 integration

- Implemented viewport.py for handling 3D view projections, gestures, and overlay drawing.
- Created refine_image_selection.py to manage the refinement of gesture masks and output image bounds.
- Added selection module with refine_selection.py to process and crop images based on alpha channel visibility.
- Introduced __init__.py for selection module to expose refine_selection function.
This commit is contained in:
2026-08-31 22:10:05 +08:00
parent 14d63001d2
commit c97b4a3e41
76 changed files with 2127 additions and 1137 deletions

View File

@@ -259,7 +259,7 @@ class ServerRuntimeTest(unittest.TestCase):
{
"download-model",
"download-required-models",
"refine-image-region",
"refine-image-selection",
"remove-background",
"upscale-image",
"generate-depth-plane-geometry",
@@ -391,19 +391,27 @@ class ServerRuntimeTest(unittest.TestCase):
import numpy as np
depth = importlib.import_module("server.geometry.depth_texture")
GeometryFrame = importlib.import_module(
"server.models.geometry"
).GeometryFrame
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(
depth=np.asarray(((1.0, 2.0, 3.0), (4.0, 5.0, 6.0))),
validity = np.asarray(
((1.0, 1.0, 0.0), (1.0, 1.0, 1.0)),
dtype=np.float32,
)
calibration = np.asarray(((False, True, True), (False, False, True)))
frame = GeometryFrame(
depth=np.asarray(
((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)),
dtype=np.float32,
),
validity=validity,
points=np.ones((2, 3, 3), dtype=np.float32),
intrinsics=np.eye(3, dtype=np.float32),
valid_mask=valid,
model_mask=None,
)
depth.write_z_depth_texture(frame.depth, frame.valid_mask, texture_path)
depth.write_z_depth_texture(frame, texture_path)
depth.write_depth_metadata(frame, calibration, metadata_path)
self.assertTrue(texture_path.is_file())
@@ -418,6 +426,9 @@ class ServerRuntimeTest(unittest.TestCase):
from PIL import Image
normal_texture = importlib.import_module("server.geometry.normal_texture")
GeometryFrame = importlib.import_module(
"server.models.geometry"
).GeometryFrame
output_path = self.storage_root / "object-normal.png"
camera_normals = np.asarray(
[[
@@ -429,9 +440,15 @@ class ServerRuntimeTest(unittest.TestCase):
dtype=np.float32,
)
valid = np.asarray([[True, True, True, False]], dtype=bool)
frame = GeometryFrame(
depth=np.ones((1, 4), dtype=np.float32),
validity=np.ones((1, 4), dtype=np.float32),
intrinsics=np.eye(3, dtype=np.float32),
normal=camera_normals,
)
normal_texture.write_camera_normal_texture(
camera_normals,
frame,
valid,
(0, 0),
(4, 1),
@@ -455,10 +472,153 @@ class ServerRuntimeTest(unittest.TestCase):
),
)
def test_moge_model_input_composites_rgba_over_black(self):
import numpy as np
from PIL import Image
artifacts = sys.modules["server.geometry.prediction_artifacts"]
first_path = self.storage_root / "first.png"
second_path = self.storage_root / "second.png"
first = np.asarray(
[[(240, 120, 60, 0), (200, 100, 50, 128), (9, 8, 7, 255)]],
dtype=np.uint8,
)
second = first.copy()
second[0, 0, :3] = (1, 2, 3)
Image.fromarray(first, mode="RGBA").save(first_path)
Image.fromarray(second, mode="RGBA").save(second_path)
first_output = artifacts._limited_model_input(
first_path,
self.storage_root / "first-model.png",
2048,
)
second_output = artifacts._limited_model_input(
second_path,
self.storage_root / "second-model.png",
2048,
)
with Image.open(first_output) as opened:
first_rgb = np.asarray(opened, dtype=np.uint8)
with Image.open(second_output) as opened:
second_rgb = np.asarray(opened, dtype=np.uint8)
np.testing.assert_array_equal(first_rgb, second_rgb)
np.testing.assert_array_equal(
first_rgb,
np.asarray([[(0, 0, 0), (100, 50, 25), (9, 8, 7)]], dtype=np.uint8),
)
def test_geometry_writers_reject_missing_or_non_finite_fields(self):
import numpy as np
depth = importlib.import_module("server.geometry.depth_texture")
normal = importlib.import_module("server.geometry.normal_texture")
GeometryFrame = importlib.import_module(
"server.models.geometry"
).GeometryFrame
base = {
"depth": np.ones((1, 2), dtype=np.float32),
"validity": np.ones((1, 2), dtype=np.float32),
"intrinsics": np.eye(3, dtype=np.float32),
}
with self.assertRaisesRegex(ValueError, "point field"):
depth.write_vector_depth_texture(
GeometryFrame(**base),
self.storage_root / "missing-points.exr",
)
point_frame = GeometryFrame(
**base,
points=np.asarray([[(0.0, 0.0, 1.0), (np.nan, 0.0, 1.0)]], dtype=np.float32),
)
with self.assertRaisesRegex(ValueError, "Point field"):
depth.write_vector_depth_texture(
point_frame,
self.storage_root / "nan-points.exr",
)
depth_frame = GeometryFrame(
**{**base, "depth": np.asarray(((1.0, np.inf),), dtype=np.float32)}
)
with self.assertRaisesRegex(ValueError, "Depth field"):
depth.write_z_depth_texture(
depth_frame,
self.storage_root / "inf-depth.exr",
)
with self.assertRaisesRegex(ValueError, "normal field"):
normal.write_camera_normal_texture(
GeometryFrame(**base),
np.ones((1, 2), dtype=bool),
(0, 0),
(2, 1),
(2, 1),
self.storage_root / "missing-normal.png",
)
normal_frame = GeometryFrame(
**base,
normal=np.asarray(
[[(0.0, 0.0, 1.0), (0.0, np.nan, 1.0)]],
dtype=np.float32,
),
)
with self.assertRaisesRegex(ValueError, "Normal field"):
normal.write_camera_normal_texture(
normal_frame,
np.ones((1, 2), dtype=bool),
(0, 0),
(2, 1),
(2, 1),
self.storage_root / "nan-normal.png",
)
self.assertFalse(any(self.storage_root.glob("*.exr")))
self.assertFalse(any(self.storage_root.glob("*-normal.png")))
def test_moge_artifact_orchestration_uses_one_geometry_frame(self):
import numpy as np
from PIL import Image
artifacts = sys.modules["server.geometry.prediction_artifacts"]
GeometryFrame = importlib.import_module(
"server.models.geometry"
).GeometryFrame
input_path = self.storage_root / "selection.png"
Image.new("RGBA", (2, 2), (255, 128, 64, 255)).save(input_path)
output_directory = self.storage_root / "job"
output_directory.mkdir()
context = SimpleNamespace(
directory=output_directory,
resource=lambda _name: object(),
progress=lambda *_args: None,
check_cancelled=lambda: None,
)
frame = GeometryFrame(
depth=np.ones((2, 2), dtype=np.float32),
validity=np.ones((2, 2), dtype=np.float32),
intrinsics=np.eye(3, dtype=np.float32),
normal=np.zeros((2, 2, 3), dtype=np.float32),
points=np.ones((2, 2, 3), dtype=np.float32),
)
with patch.object(artifacts.moge2, "infer", return_value=[frame]) as infer:
result = artifacts.generate_moge2_artifacts(
context,
{"max_input_size": 2048},
input_path,
depth_mode="VECTOR",
normal_mode="OBJECT",
)
self.assertEqual(
set(result),
{"vector_depth", "depth_metadata", "object_normal"},
)
self.assertFalse((output_directory / "model-input.png").exists())
self.assertTrue(all(path.is_file() for path in result.values()))
self.assertTrue(infer.call_args.kwargs["include_points"])
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")
input_path.write_bytes(b"selection")
context = SimpleNamespace(
directory=self.storage_root / "job",
progress=lambda *_args: None,
@@ -505,10 +665,10 @@ class ServerRuntimeTest(unittest.TestCase):
normal_mode="OBJECT",
)
def test_generate_cutout_artifacts_refine_an_already_rasterized_region(self):
def test_generate_cutout_artifacts_refine_an_already_rasterized_selection(self):
job = sys.modules["server.jobs.cutout"]
input_path = self.storage_root / "input.png"
input_path.write_bytes(b"region")
input_path.write_bytes(b"selection")
directory = self.storage_root / "job"
context = SimpleNamespace(
directory=directory,
@@ -520,12 +680,12 @@ class ServerRuntimeTest(unittest.TestCase):
output.write_bytes(b"refined")
return output, (2, 3, 8, 9)
with patch.object(job, "refine_region", side_effect=generate):
with patch.object(job, "refine_selection", side_effect=generate):
result = job.run(
context,
{
"input": str(input_path),
"refine_region": True,
"refine_selection": True,
"refine_model": "BEN2_BASE",
"device": "cpu",
},
@@ -533,9 +693,12 @@ class ServerRuntimeTest(unittest.TestCase):
self.assertEqual(
result,
{"region_image": "region.png", "region_bounds": "region_bounds.json"},
{
"selection_image": "selection.png",
"selection_bounds": "selection_bounds.json",
},
)
self.assertEqual(
(directory / "region_bounds.json").read_text(encoding="utf-8"),
(directory / "selection_bounds.json").read_text(encoding="utf-8"),
"[2,3,8,9]",
)