feat: Add BEN2 background removal functionality and integrate into SnapForm

- Introduced BEN2 model for background removal in the panel and properties.
- Updated runtime to handle background removal commands and model downloads.
- Implemented background removal logic in worker, including image processing and saving results.
- Enhanced tests to cover new background removal features and ensure proper integration.
- Updated version to 0.9.28 to reflect new features and improvements.
This commit is contained in:
2026-08-03 20:18:00 +08:00
parent 69eaefa016
commit d694980da7
19 changed files with 1523 additions and 84 deletions

View File

@@ -47,6 +47,52 @@ class WorkerTest(unittest.TestCase):
source.index("import torch"),
)
def test_optional_background_removal_runs_before_da3_model_load(self):
source = inspect.getsource(self.worker.reconstruct)
self.assertLess(
source.index("infer_foreground("),
source.index("DepthAnything3.from_pretrained"),
)
args = self.worker.parser().parse_args(
[
"reconstruct",
"--input",
"input.png",
"--model-dir",
"da3",
"--output-dir",
"result",
"--export-format",
"mini_npz",
"--status",
"status.json",
"--remove-background-model-dir",
"ben2",
"--process-res",
"1024",
"--process-res-method",
"upper_bound_resize",
"--conf-thresh-percentile",
"40",
"--num-max-points",
"1000",
"--show-cameras",
"0",
"--gs-views-interval",
"1",
"--video-frames",
"16",
"--device",
"auto",
"--use-ray-pose",
"1",
"--ref-view-strategy",
"first",
]
)
self.assertEqual(args.remove_background_model_dir, "ben2")
def test_prepares_a_single_image_file(self):
with tempfile.TemporaryDirectory() as directory:
image_path = Path(directory) / "input.png"
@@ -69,6 +115,117 @@ class WorkerTest(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "Unsupported input type"):
self.worker.prepare_input(input_path, 16, directory)
def test_download_passes_optional_file_patterns(self):
calls = []
fake_hugging_face = ModuleType("huggingface_hub")
fake_hugging_face.snapshot_download = lambda **options: calls.append(
options
)
args = SimpleNamespace(
repository="PramaLLC/BEN2",
model_dir="model",
status="status.json",
allow_pattern=["config.json", "model.safetensors"],
)
with tempfile.TemporaryDirectory() as directory, patch.dict(
sys.modules,
{"huggingface_hub": fake_hugging_face},
), patch.object(
self.worker,
"create_download_progress_class",
return_value=object,
):
args.model_dir = str(Path(directory) / "model")
args.status = str(Path(directory) / "status.json")
self.worker.download_model(args)
self.assertEqual(
calls[0]["allow_patterns"],
["config.json", "model.safetensors"],
)
def test_ben2_background_removal_writes_fast_transparent_png(self):
inference_options = []
save_options = []
class FakeImage:
def __enter__(self):
return self
def __exit__(self, *_args):
return None
def convert(self, mode):
self.mode = mode
return self
def save(self, path, **options):
save_options.append(options)
Path(path).touch()
class FakeModel:
@classmethod
def from_pretrained(cls, model_dir):
self.assertEqual(model_dir, "model")
return cls()
def to(model_self, device):
self.assertEqual(device.type, "cpu")
return model_self
def eval(self):
return None
def inference(self, image, **options):
inference_options.append((image, options))
return FakeImage()
fake_torch = ModuleType("torch")
fake_torch.cuda = SimpleNamespace(
is_available=lambda: False,
synchronize=lambda: None,
)
fake_torch.device = lambda device_type: SimpleNamespace(type=device_type)
fake_torch.inference_mode = nullcontext
fake_ben2 = ModuleType("ben2")
fake_ben2.BEN_Base = FakeModel
fake_pil = ModuleType("PIL")
fake_pil.Image = SimpleNamespace(open=lambda _path: FakeImage())
with tempfile.TemporaryDirectory() as directory, patch.dict(
sys.modules,
{
"torch": fake_torch,
"ben2": fake_ben2,
"PIL": fake_pil,
},
):
root = Path(directory)
input_path = root / "input.png"
input_path.touch()
args = SimpleNamespace(
input=str(input_path),
model_dir="model",
output_dir=str(root / "result"),
status=str(root / "status.json"),
device="cpu",
)
self.worker.remove_background(args)
status = json.loads(Path(args.status).read_text(encoding="utf-8"))
self.assertTrue((root / "result" / "foreground.png").is_file())
self.assertEqual(
inference_options[0][1],
{"refine_foreground": False},
)
self.assertEqual(
save_options[0],
{"format": "PNG", "compress_level": 1},
)
self.assertIn("Background removed in", status["message"])
def test_uses_official_da3_export_parameters_for_all_four_formats(self):
inference_options = []
export_calls = []