feat: Implement background depth distance feature for depth processing

- Added `background_depth_distance` property to `ActiveJob` and `RunJob` classes to manage background depth settings.
- Updated job execution to handle background depth distance, allowing for fixed negative displacement in depth calculations.
- Introduced new functions in the worker module to apply background depth distance to depth frames and manage original background depth.
- Enhanced tests to cover new functionality, ensuring correct behavior of background depth processing and reference depth extraction.
- Refactored job submission to run in a separate thread, improving responsiveness during execution.
This commit is contained in:
2026-08-09 16:34:41 +08:00
parent 2e8af26cdf
commit 8aac4822c6
9 changed files with 725 additions and 114 deletions

View File

@@ -7,8 +7,9 @@ import bpy
DEPTH_KEY_CANDIDATES = ("depth", "depths")
INTRINSICS_KEY_CANDIDATES = ("intrinsics", "ixts")
DISPLACEMENT_STRENGTH = -1.0
DISPLACEMENT_MIDLEVEL = 1.0
DISPLACEMENT_STRENGTH = 1.0
DISPLACEMENT_MIDLEVEL = 0.0
REFERENCE_DEPTH_KEY = "snapform_reference_depth"
ORIGINAL_DISPLACEMENT_STRENGTH = 1.0
ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
BUMP_BASE_DISTANCE = 0.001
@@ -548,12 +549,28 @@ def apply_plane_transform(plane_object, source_object, camera_bounds):
(
(scale_x, 0.0, 0.0, offset_x),
(0.0, scale_y, 0.0, offset_y),
(0.0, 0.0, scale_z, 0.0),
(0.0, 0.0, 1.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
)
plane_object.matrix_world = source_object.matrix_world @ fit_matrix
return abs(float(plane_object.scale.z))
return abs(float(plane_object.scale.z)) * abs(scale_z)
def apply_object_scale_to_mesh(plane_object):
"""Bake object scale into the plane while keeping location and rotation."""
from mathutils import Matrix
scale = plane_object.scale.copy()
plane_object.data.transform(
Matrix.Diagonal((scale.x, scale.y, scale.z, 1.0))
)
plane_object.data.update()
plane_object.scale = (1.0, 1.0, 1.0)
def scale_depth_values(depth, depth_scale):
return prepare_depth_values(depth) * depth_scale
def extract_depth_arrays(npz_path):
@@ -634,12 +651,31 @@ def extract_intrinsics_array(npz_path):
return intrinsics
def extract_reference_depth(npz_path):
try:
import numpy as np
except ImportError as error:
raise RuntimeError("Blender Python does not provide NumPy") from error
try:
with np.load(npz_path, allow_pickle=False) as archive:
if REFERENCE_DEPTH_KEY not in archive.files:
return None
reference_depth = float(archive[REFERENCE_DEPTH_KEY])
except OSError as error:
raise ValueError(f"Unable to read Mini NPZ: {npz_path}") from error
if not np.isfinite(reference_depth) or reference_depth <= 0.0:
raise ValueError("Mini NPZ has an invalid reference depth")
return reference_depth
def prepare_depth_values(depth):
import numpy as np
valid = np.isfinite(depth) & (depth > 0.0)
valid = np.isfinite(depth)
if not valid.any():
raise ValueError("Depth array contains no positive finite values")
raise ValueError("Depth array contains no finite values")
values = depth.astype(np.float32, copy=True)
values[~valid] = 0.0
return values
@@ -650,6 +686,7 @@ def camera_plane_bounds(
depth,
intrinsics,
displacement_strength,
reference_depth=None,
):
import numpy as np
@@ -657,10 +694,11 @@ def camera_plane_bounds(
if scale <= 1e-8:
raise ValueError("Displacement Strength must not be zero")
valid = np.isfinite(depth) & (depth > 0.0)
if not valid.any():
raise ValueError("Depth array contains no positive finite values")
reference_depth = float(np.median(depth[valid]))
if reference_depth is None:
valid = np.isfinite(depth) & (depth > 0.0)
if not valid.any():
raise ValueError("Depth array contains no positive finite values")
reference_depth = float(np.median(depth[valid]))
reference_distance = reference_depth * scale
height, width = depth.shape
@@ -978,6 +1016,7 @@ def build_image_depth_plane(
unshaded=False,
color_image=None,
depth_frames_directory=None,
depth_image=None,
):
source_name = source_object.name
frame_count = 1
@@ -1001,11 +1040,12 @@ def build_image_depth_plane(
frame_count if uses_extracted else source_frame_duration
)
texture_frame_offset = 0 if uses_extracted else source_frame_offset
depth_source_image = source_object.data
if frames_image is not None:
depth_source_image = frames_image
elif not is_animated_image(source_object.data):
depth_source_image = copy_depth_image(source_object)
depth_source_image = depth_image or source_object.data
if depth_image is None:
if frames_image is not None:
depth_source_image = frames_image
elif not is_animated_image(source_object.data):
depth_source_image = copy_depth_image(source_object)
mesh = create_plane_mesh(
source_object,
subdivision_level=subdivision_level,
@@ -1085,12 +1125,14 @@ def build_depth_plane(
depth_arrays = extract_depth_arrays(npz_path)
depth = depth_arrays[0]
intrinsics = extract_intrinsics_array(npz_path)
stored_reference_depth = extract_reference_depth(npz_path)
depth_values = prepare_depth_values(depth)
bounds, reference_depth = camera_plane_bounds(
source_object,
depth_values,
intrinsics,
DISPLACEMENT_STRENGTH,
reference_depth=stored_reference_depth,
)
depth_name = depth_name or depth_data_name(source_object)
frame_count = len(depth_arrays)
@@ -1100,18 +1142,6 @@ def build_depth_plane(
source_frame_duration,
source_use_cyclic,
) = image_user_settings(source_object)
depth_image = create_depth_image(
depth_name,
depth_values,
)
if frame_count > 1 and depth_frames_directory is not None:
bpy.data.images.remove(depth_image, do_unlink=True)
depth_image = create_depth_sequence_image(
depth_name,
[prepare_depth_values(frame) for frame in depth_arrays],
context.scene,
depth_frames_directory,
)
mesh = create_plane_mesh(source_object, bounds, subdivision_level)
plane_object = bpy.data.objects.new(
source_name,
@@ -1123,6 +1153,22 @@ def build_depth_plane(
source_object,
bounds,
)
apply_object_scale_to_mesh(plane_object)
scaled_depth_arrays = [
scale_depth_values(frame, depth_scale) for frame in depth_arrays
]
depth_image = create_depth_image(
depth_name,
scaled_depth_arrays[0],
)
if frame_count > 1 and depth_frames_directory is not None:
bpy.data.images.remove(depth_image, do_unlink=True)
depth_image = create_depth_sequence_image(
depth_name,
scaled_depth_arrays,
context.scene,
depth_frames_directory,
)
subdivision = plane_object.modifiers.new("Subdivision", "SUBSURF")
subdivision.subdivision_type = "SIMPLE"
@@ -1298,6 +1344,9 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
snapform_depth_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_depth_npz_path: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
@@ -1342,6 +1391,23 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
f"{image_base_name(source_object.data)}.frame",
self.snapform_color_frames_dir,
)
depth_image = None
if self.snapform_depth_npz_path:
depth_arrays = extract_depth_arrays(
self.snapform_depth_npz_path
)
if len(depth_arrays) == 1:
depth_image = create_depth_image(
depth_data_name(source_object),
depth_arrays[0],
)
else:
depth_image = create_depth_sequence_image(
depth_data_name(source_object),
depth_arrays,
context.scene,
self.snapform_depth_frames_dir,
)
build_image_depth_plane(
context,
source_object,
@@ -1353,6 +1419,7 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
depth_frames_directory=(
self.snapform_depth_frames_dir or None
),
depth_image=depth_image,
)
except (OSError, RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
@@ -1474,10 +1541,12 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
description="Display the source color through an Emission shader",
default=False,
)
snapform_remove_background: bpy.props.BoolProperty(
name="Remove Background",
description="Remove the color image background with BEN2",
default=False,
snapform_background_depth_distance: bpy.props.FloatProperty(
name="Background Depth Distance",
description="Set excluded background to this fixed depth distance; zero disables background exclusion",
default=0.0,
min=0.0,
soft_max=10.0,
)
snapform_model: bpy.props.EnumProperty(
name="Model",
@@ -1572,7 +1641,10 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
settings_body.prop(self, "snapform_subdivision_level")
remove_background_row = settings_body.row()
remove_background_row.enabled = model_ready(BEN2_MODEL.key)
remove_background_row.prop(self, "snapform_remove_background")
remove_background_row.prop(
self,
"snapform_background_depth_distance",
)
settings_body.prop(self, "snapform_unshaded")
layout.prop(self, "snapform_delete_original")
@@ -1590,7 +1662,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
)
if (
self.snapform_depth_source == "ORIGINAL"
and not self.snapform_remove_background
and self.snapform_background_depth_distance <= 0.0
):
if video_total_frames(source_object.data) > 1:
try:
@@ -1657,6 +1729,9 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
snapform_background_depth_distance=(
self.snapform_background_depth_distance
),
)
if "CANCELLED" not in result:
return {"FINISHED"}
@@ -1695,7 +1770,9 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
snapform_remove_background=self.snapform_remove_background,
snapform_background_depth_distance=(
self.snapform_background_depth_distance
),
)
if "CANCELLED" in result:
if delete_input: