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:
@@ -5,11 +5,19 @@ from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from . import runtime
|
||||
from .models import get_model, selected_model_key
|
||||
from .models import (
|
||||
BEN2_MODEL,
|
||||
get_downloadable_model,
|
||||
get_model,
|
||||
selected_model_key,
|
||||
)
|
||||
|
||||
|
||||
RECONSTRUCTION_JOB = "RECONSTRUCT"
|
||||
IMAGE_DEPTH_JOB = "IMAGE_DEPTH"
|
||||
BACKGROUND_REMOVAL_JOB = "REMOVE_BACKGROUND"
|
||||
IMAGE_BACKGROUND_JOB = "IMAGE_BACKGROUND"
|
||||
ORIGINAL_BACKGROUND_JOB = "ORIGINAL_BACKGROUND"
|
||||
IMAGE_DEPTH_DEFAULT_PROCESS_RES = 1024
|
||||
IMAGE_DEPTH_PROCESS_RES_METHOD = "upper_bound_resize"
|
||||
|
||||
@@ -33,10 +41,13 @@ class ActiveJob:
|
||||
result_directory: Path
|
||||
export_format: str = ""
|
||||
source_object_name: str = ""
|
||||
depth_data_name: str = ""
|
||||
process_res: int = IMAGE_DEPTH_DEFAULT_PROCESS_RES
|
||||
subdivision_level: int = 5
|
||||
delete_source: bool = True
|
||||
displacement_via: str = "MODIFIER"
|
||||
unshaded: bool = False
|
||||
remove_background: bool = False
|
||||
temporary_input_path: Path | None = None
|
||||
cancelled: bool = False
|
||||
|
||||
@@ -83,6 +94,9 @@ class RunJob(bpy.types.Operator):
|
||||
("DOWNLOAD_MODEL", "Download Model", ""),
|
||||
(RECONSTRUCTION_JOB, "Export DA3 Result", ""),
|
||||
(IMAGE_DEPTH_JOB, "Convert Image to Depth Plane", ""),
|
||||
(BACKGROUND_REMOVAL_JOB, "Test BEN2 Background Removal", ""),
|
||||
(IMAGE_BACKGROUND_JOB, "Remove Image Empty Background", ""),
|
||||
(ORIGINAL_BACKGROUND_JOB, "Build Original Background Plane", ""),
|
||||
),
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
@@ -95,6 +109,9 @@ class RunJob(bpy.types.Operator):
|
||||
snapform_source_object_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_depth_data_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_model_key: bpy.props.StringProperty(
|
||||
default="BASE",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
@@ -131,6 +148,14 @@ class RunJob(bpy.types.Operator):
|
||||
default="MODIFIER",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_unshaded: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_remove_background: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
global _active_job
|
||||
@@ -173,13 +198,20 @@ class RunJob(bpy.types.Operator):
|
||||
else ""
|
||||
),
|
||||
source_object_name=self.snapform_source_object_name,
|
||||
depth_data_name=self.snapform_depth_data_name,
|
||||
process_res=self.snapform_process_res,
|
||||
subdivision_level=self.snapform_subdivision_level,
|
||||
delete_source=self.snapform_delete_source,
|
||||
displacement_via=self.snapform_displacement_via,
|
||||
unshaded=self.snapform_unshaded,
|
||||
remove_background=self.snapform_remove_background,
|
||||
temporary_input_path=(
|
||||
Path(self.snapform_input_path)
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
if self.job_type in {
|
||||
IMAGE_DEPTH_JOB,
|
||||
IMAGE_BACKGROUND_JOB,
|
||||
ORIGINAL_BACKGROUND_JOB,
|
||||
}
|
||||
and self.snapform_delete_input
|
||||
else None
|
||||
),
|
||||
@@ -196,6 +228,9 @@ class RunJob(bpy.types.Operator):
|
||||
|
||||
def _prepare_command(self, _context, settings):
|
||||
paths = runtime.new_job_paths(self.job_type)
|
||||
remove_background = bool(
|
||||
getattr(self, "snapform_remove_background", False)
|
||||
)
|
||||
if self.job_type == "INSTALL_RUNTIME":
|
||||
self._require_online_access()
|
||||
return runtime.install_runtime_command(paths["status"]), paths
|
||||
@@ -205,20 +240,49 @@ class RunJob(bpy.types.Operator):
|
||||
|
||||
if self.job_type == "DOWNLOAD_MODEL":
|
||||
self._require_online_access()
|
||||
model = get_model(self.model_key)
|
||||
model = get_downloadable_model(self.model_key)
|
||||
return (
|
||||
runtime.download_model_command(model.key, paths["status"]),
|
||||
paths,
|
||||
)
|
||||
|
||||
configured_input = (
|
||||
self.snapform_input_path
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
else settings.input_path
|
||||
)
|
||||
if self.job_type in {
|
||||
IMAGE_DEPTH_JOB,
|
||||
IMAGE_BACKGROUND_JOB,
|
||||
ORIGINAL_BACKGROUND_JOB,
|
||||
}:
|
||||
configured_input = self.snapform_input_path
|
||||
elif self.job_type == BACKGROUND_REMOVAL_JOB:
|
||||
configured_input = settings.background_input_path
|
||||
else:
|
||||
configured_input = settings.input_path
|
||||
input_path = Path(bpy.path.abspath(configured_input))
|
||||
if not input_path.is_file():
|
||||
raise ValueError("Select a valid image or video file")
|
||||
if self.job_type in {
|
||||
BACKGROUND_REMOVAL_JOB,
|
||||
IMAGE_BACKGROUND_JOB,
|
||||
ORIGINAL_BACKGROUND_JOB,
|
||||
}:
|
||||
if input_path.suffix.lower() not in {
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".webp",
|
||||
}:
|
||||
raise ValueError("BEN2 requires a JPG, JPEG, PNG, or WEBP image")
|
||||
if not runtime.model_ready(BEN2_MODEL.key):
|
||||
raise RuntimeError(f"Download {BEN2_MODEL.label} first")
|
||||
return (
|
||||
runtime.remove_background_command(
|
||||
settings,
|
||||
input_path,
|
||||
paths["status"],
|
||||
paths["result"],
|
||||
BEN2_MODEL.key,
|
||||
),
|
||||
paths,
|
||||
)
|
||||
if self.job_type not in {RECONSTRUCTION_JOB, IMAGE_DEPTH_JOB}:
|
||||
raise RuntimeError(f"Unsupported task type: {self.job_type}")
|
||||
export_format = (
|
||||
@@ -236,6 +300,12 @@ class RunJob(bpy.types.Operator):
|
||||
model = get_model(model_key)
|
||||
if not runtime.model_ready(model_key):
|
||||
raise RuntimeError(f"Download {model.label} first")
|
||||
if (
|
||||
self.job_type == IMAGE_DEPTH_JOB
|
||||
and remove_background
|
||||
and not runtime.model_ready(BEN2_MODEL.key)
|
||||
):
|
||||
raise RuntimeError(f"Download {BEN2_MODEL.label} first")
|
||||
if export_format == "gs_ply" and not model.supports_gs:
|
||||
raise RuntimeError(
|
||||
"PLY export requires a DA3 Giant or Nested Giant model"
|
||||
@@ -255,6 +325,10 @@ class RunJob(bpy.types.Operator):
|
||||
else None
|
||||
),
|
||||
use_ray_pose=use_ray_pose,
|
||||
remove_background=(
|
||||
self.job_type == IMAGE_DEPTH_JOB
|
||||
and remove_background
|
||||
),
|
||||
),
|
||||
paths,
|
||||
)
|
||||
@@ -274,8 +348,15 @@ class RunJob(bpy.types.Operator):
|
||||
return f"Starting {settings.export_format} export"
|
||||
if self.job_type == IMAGE_DEPTH_JOB:
|
||||
return "Starting depth estimation"
|
||||
if self.job_type == BACKGROUND_REMOVAL_JOB:
|
||||
return "Starting BEN2 background removal"
|
||||
if self.job_type == IMAGE_BACKGROUND_JOB:
|
||||
return "Starting Image Empty background removal"
|
||||
if self.job_type == ORIGINAL_BACKGROUND_JOB:
|
||||
return "Starting background removal"
|
||||
if self.job_type == "DOWNLOAD_MODEL":
|
||||
return f"Starting {get_model(self.model_key).label} download"
|
||||
model = get_downloadable_model(self.model_key)
|
||||
return f"Starting {model.label} download"
|
||||
return labels[self.job_type]
|
||||
|
||||
def modal(self, context, event):
|
||||
@@ -330,15 +411,18 @@ class RunJob(bpy.types.Operator):
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"CANCELLED"}
|
||||
|
||||
if job.kind == RECONSTRUCTION_JOB:
|
||||
if job.kind in {RECONSTRUCTION_JOB, BACKGROUND_REMOVAL_JOB}:
|
||||
try:
|
||||
open_job_directory(job.result_directory)
|
||||
except Exception as open_error:
|
||||
self.report({"WARNING"}, str(open_error))
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Exported {job.export_format} to {job.result_directory}",
|
||||
)
|
||||
if job.kind == RECONSTRUCTION_JOB:
|
||||
message = (
|
||||
f"Exported {job.export_format} to {job.result_directory}"
|
||||
)
|
||||
else:
|
||||
message = f"Removed background to {job.result_directory}"
|
||||
self.report({"INFO"}, message)
|
||||
if job.kind == IMAGE_DEPTH_JOB:
|
||||
try:
|
||||
settings.status = "Building depth plane"
|
||||
@@ -353,6 +437,34 @@ class RunJob(bpy.types.Operator):
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"CANCELLED"}
|
||||
status["message"] = "Depth plane is ready"
|
||||
if job.kind == ORIGINAL_BACKGROUND_JOB:
|
||||
try:
|
||||
settings.status = "Building depth plane"
|
||||
settings.progress = 0.96
|
||||
self._redraw_ui(context, force=True)
|
||||
self._build_original_depth_plane(job)
|
||||
except (OSError, RuntimeError, ValueError) as import_error:
|
||||
settings.status = "Depth import failed"
|
||||
settings.progress = 0.0
|
||||
self.report({"ERROR"}, str(import_error))
|
||||
close_active_job()
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"CANCELLED"}
|
||||
status["message"] = "Depth plane is ready"
|
||||
if job.kind == IMAGE_BACKGROUND_JOB:
|
||||
try:
|
||||
settings.status = "Updating Image Empty"
|
||||
settings.progress = 0.96
|
||||
self._redraw_ui(context, force=True)
|
||||
self._apply_removed_background(job)
|
||||
except (OSError, RuntimeError, ValueError) as import_error:
|
||||
settings.status = "Image update failed"
|
||||
settings.progress = 0.0
|
||||
self.report({"ERROR"}, str(import_error))
|
||||
close_active_job()
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"CANCELLED"}
|
||||
status["message"] = "Image background is removed"
|
||||
|
||||
settings.status = status.get("message", "Task complete")
|
||||
settings.progress = 1.0
|
||||
@@ -370,9 +482,16 @@ class RunJob(bpy.types.Operator):
|
||||
result = bpy.ops.snapform.build_depth_plane(
|
||||
snapform_source_object_name=job.source_object_name,
|
||||
snapform_npz_path=str(npz_path),
|
||||
snapform_depth_data_name=job.depth_data_name,
|
||||
snapform_background_path=(
|
||||
str(job.result_directory / "foreground.png")
|
||||
if job.remove_background
|
||||
else ""
|
||||
),
|
||||
snapform_subdivision_level=job.subdivision_level,
|
||||
snapform_delete_source=job.delete_source,
|
||||
snapform_displacement_via=job.displacement_via,
|
||||
snapform_unshaded=job.unshaded,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Unable to build the depth plane")
|
||||
@@ -380,6 +499,37 @@ class RunJob(bpy.types.Operator):
|
||||
if "FINISHED" not in undo_result:
|
||||
raise RuntimeError("Unable to create the depth plane Undo step")
|
||||
|
||||
def _build_original_depth_plane(self, job):
|
||||
result = bpy.ops.snapform.build_original_depth_plane(
|
||||
snapform_source_object_name=job.source_object_name,
|
||||
snapform_background_path=str(
|
||||
job.result_directory / "foreground.png"
|
||||
),
|
||||
snapform_subdivision_level=job.subdivision_level,
|
||||
snapform_delete_source=job.delete_source,
|
||||
snapform_displacement_via=job.displacement_via,
|
||||
snapform_unshaded=job.unshaded,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Unable to build the depth plane")
|
||||
undo_result = bpy.ops.ed.undo_push(message="Convert to Depth Plane")
|
||||
if "FINISHED" not in undo_result:
|
||||
raise RuntimeError("Unable to create the depth plane Undo step")
|
||||
|
||||
def _apply_removed_background(self, job, push_undo=True):
|
||||
output_path = job.result_directory / "foreground.png"
|
||||
result = bpy.ops.snapform.apply_removed_background(
|
||||
snapform_source_object_name=job.source_object_name,
|
||||
snapform_output_path=str(output_path),
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Unable to update the Image Empty")
|
||||
if not push_undo:
|
||||
return
|
||||
undo_result = bpy.ops.ed.undo_push(message="Remove Background")
|
||||
if "FINISHED" not in undo_result:
|
||||
raise RuntimeError("Unable to create the background removal Undo step")
|
||||
|
||||
def _remove_timer(self, context):
|
||||
timer = getattr(self, "_timer", None)
|
||||
if timer is None:
|
||||
|
||||
Reference in New Issue
Block a user