feat: Update documentation and improve depth processing logic; bump version to 0.11.1
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "snapform"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
name = "SnapForm"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
|
||||
@@ -15,6 +15,10 @@ except ImportError:
|
||||
|
||||
ONNX_PATH = "model.onnx"
|
||||
PATCH_SIZE = 14
|
||||
# DA3 cross-view attention is quadratic in (views x patches); the peak
|
||||
# attention tensor alone can exhaust memory long before ONNX Runtime reports
|
||||
# a generic allocation failure.
|
||||
ATTENTION_BUDGET_BYTES = 4 * 1024 * 1024 * 1024
|
||||
STRATEGY_CODES = {
|
||||
"first": 0,
|
||||
"middle": 1,
|
||||
@@ -177,6 +181,14 @@ def camera_from_pose(pose_encoding, height, width):
|
||||
return extrinsics, intrinsics
|
||||
|
||||
|
||||
def estimate_attention_bytes(views, height, width):
|
||||
"""Estimate the peak cross-view attention tensor size in bytes."""
|
||||
patches = (height // PATCH_SIZE) * (width // PATCH_SIZE)
|
||||
tokens = views * patches
|
||||
# 12 attention heads in DA3 Base; float32 activations.
|
||||
return tokens * tokens * 12 * 4
|
||||
|
||||
|
||||
def infer(
|
||||
session,
|
||||
image_paths,
|
||||
@@ -192,6 +204,15 @@ def infer(
|
||||
process_res,
|
||||
process_res_method,
|
||||
)
|
||||
_, views, _, height, width = model_input.shape
|
||||
attention_bytes = estimate_attention_bytes(views, height, width)
|
||||
if attention_bytes > ATTENTION_BUDGET_BYTES:
|
||||
raise RuntimeError(
|
||||
f"DA3 cannot process {views} frame(s) at {width}x{height}: "
|
||||
"the cross-view attention would need about "
|
||||
f"{attention_bytes // 2**30} GB. Reduce the frame count or "
|
||||
"the Resolution Scale."
|
||||
)
|
||||
strategy_code = np.asarray(
|
||||
[STRATEGY_CODES.get(ref_view_strategy, 2)],
|
||||
dtype=np.int64,
|
||||
|
||||
@@ -1538,6 +1538,53 @@ def modifier_input_identifier(node_group, name):
|
||||
raise RuntimeError(f"Geometry Nodes input is missing: {name}")
|
||||
|
||||
|
||||
def modifier_input_slots(modifier):
|
||||
"""Return the runtime input collection, or None for the legacy API."""
|
||||
properties = getattr(modifier, "properties", None)
|
||||
if properties is None:
|
||||
return None
|
||||
return getattr(properties, "inputs", None)
|
||||
|
||||
|
||||
def set_modifier_input(modifier, identifier, value):
|
||||
"""Assign a value to a Geometry Nodes modifier input socket."""
|
||||
inputs = modifier_input_slots(modifier)
|
||||
if inputs is not None:
|
||||
# Blender 5.2+ exposes inputs as runtime RNA keyed by socket identifier.
|
||||
getattr(inputs, identifier).value = value
|
||||
else:
|
||||
# Blender 4.x exposes inputs as modifier ID properties.
|
||||
modifier[identifier] = value
|
||||
|
||||
|
||||
def get_modifier_input(modifier, identifier):
|
||||
"""Read a Geometry Nodes modifier input socket value."""
|
||||
inputs = modifier_input_slots(modifier)
|
||||
if inputs is not None:
|
||||
return getattr(inputs, identifier).value
|
||||
return modifier[identifier]
|
||||
|
||||
|
||||
def modifier_input_driver_path(modifier, identifier):
|
||||
"""Return the driver data path for a modifier input socket."""
|
||||
if modifier_input_slots(modifier) is not None:
|
||||
return (
|
||||
f'modifiers["{modifier.name}"]'
|
||||
f".properties.inputs.{identifier}.value"
|
||||
)
|
||||
return f'modifiers["{modifier.name}"]["{identifier}"]'
|
||||
|
||||
|
||||
def drive_modifier_input(owner_object, modifier, identifier, expression="frame"):
|
||||
"""Add a scripted driver to a modifier input so it follows the scene frame."""
|
||||
driver = owner_object.driver_add(
|
||||
modifier_input_driver_path(modifier, identifier)
|
||||
)
|
||||
driver.driver.type = "SCRIPTED"
|
||||
driver.driver.expression = expression
|
||||
return driver
|
||||
|
||||
|
||||
def add_depth_plane_processing(
|
||||
plane_object,
|
||||
depth_image,
|
||||
@@ -1565,12 +1612,10 @@ def add_depth_plane_processing(
|
||||
("Cyclic", use_cyclic),
|
||||
):
|
||||
identifier = modifier_input_identifier(node_group, name)
|
||||
modifier[identifier] = value
|
||||
set_modifier_input(modifier, identifier, value)
|
||||
if animate_frame:
|
||||
frame_identifier = modifier_input_identifier(node_group, "Frame")
|
||||
driver = modifier.driver_add(f'["{frame_identifier}"]')
|
||||
driver.driver.type = "SCRIPTED"
|
||||
driver.driver.expression = "frame"
|
||||
drive_modifier_input(plane_object, modifier, frame_identifier)
|
||||
plane_object.update_tag(refresh={"DATA"})
|
||||
return modifier
|
||||
|
||||
@@ -1601,12 +1646,11 @@ def add_moge_depth_plane_processing(
|
||||
("Frames", max(int(frame_duration or 1), 1)),
|
||||
("Cyclic", use_cyclic),
|
||||
):
|
||||
modifier[modifier_input_identifier(node_group, name)] = value
|
||||
identifier = modifier_input_identifier(node_group, name)
|
||||
set_modifier_input(modifier, identifier, value)
|
||||
if animate_frame:
|
||||
frame_identifier = modifier_input_identifier(node_group, "Frame")
|
||||
driver = modifier.driver_add(f'["{frame_identifier}"]')
|
||||
driver.driver.type = "SCRIPTED"
|
||||
driver.driver.expression = "frame"
|
||||
drive_modifier_input(plane_object, modifier, frame_identifier)
|
||||
plane_object.update_tag(refresh={"DATA"})
|
||||
return modifier
|
||||
|
||||
@@ -1882,7 +1926,9 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
assign_to_source=False,
|
||||
)
|
||||
color_image = None
|
||||
if self.snapform_color_frames_dir:
|
||||
# The foreground result already carries the color, so the original
|
||||
# frame sequence is unused when background removal is available.
|
||||
if self.snapform_color_frames_dir and foreground_image is None:
|
||||
color_image = create_sequence_image(
|
||||
f"{image_base_name(source_object.data)}.frame",
|
||||
self.snapform_color_frames_dir,
|
||||
@@ -2029,7 +2075,9 @@ class BuildMoge2DepthPlane(bpy.types.Operator):
|
||||
assign_to_source=False,
|
||||
)
|
||||
color_image = None
|
||||
if self.snapform_color_frames_dir:
|
||||
# The foreground result already carries the color, so the original
|
||||
# frame sequence is unused when background removal is available.
|
||||
if self.snapform_color_frames_dir and foreground_image is None:
|
||||
color_image = create_sequence_image(
|
||||
f"{image_base_name(source_object.data)}.frame",
|
||||
self.snapform_color_frames_dir,
|
||||
@@ -2098,7 +2146,9 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
|
||||
assign_to_source=False,
|
||||
)
|
||||
color_image = None
|
||||
if self.snapform_color_frames_dir:
|
||||
# The foreground result already carries the color, so the original
|
||||
# frame sequence is unused when background removal is available.
|
||||
if self.snapform_color_frames_dir and foreground_image is None:
|
||||
color_image = create_sequence_image(
|
||||
f"{image_base_name(source_object.data)}.frame",
|
||||
self.snapform_color_frames_dir,
|
||||
|
||||
@@ -13,6 +13,7 @@ from .image_depth import (
|
||||
process_resolution_scale_property,
|
||||
remove_temporary_input,
|
||||
scaled_process_resolution,
|
||||
set_modifier_input,
|
||||
video_total_frames,
|
||||
)
|
||||
|
||||
@@ -417,7 +418,11 @@ def add_geometry_nodes_processing(
|
||||
modifier.node_group = node_group
|
||||
modifier.show_group_selector = False
|
||||
identifier = modifier_input_identifier(node_group, "Resolution")
|
||||
modifier[identifier] = default_remesh_resolution(vertices, faces)
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
identifier,
|
||||
default_remesh_resolution(vertices, faces),
|
||||
)
|
||||
|
||||
|
||||
def create_matching_camera(
|
||||
|
||||
@@ -2016,14 +2016,25 @@ def reconstruct(args, model_cache=None, cancel_check=None):
|
||||
0.25,
|
||||
f"Estimating depth for {len(images)} frame(s)",
|
||||
)
|
||||
prediction = infer(
|
||||
model,
|
||||
[str(path) for path in images],
|
||||
process_res=args.process_res,
|
||||
process_res_method=args.process_res_method,
|
||||
use_ray_pose=bool(args.use_ray_pose),
|
||||
ref_view_strategy=args.ref_view_strategy,
|
||||
)
|
||||
try:
|
||||
prediction = infer(
|
||||
model,
|
||||
[str(path) for path in images],
|
||||
process_res=args.process_res,
|
||||
process_res_method=args.process_res_method,
|
||||
use_ray_pose=bool(args.use_ray_pose),
|
||||
ref_view_strategy=args.ref_view_strategy,
|
||||
)
|
||||
except UnicodeDecodeError as error:
|
||||
# DirectML reports failures with a locale-specific message that
|
||||
# ONNX Runtime cannot always decode, masking the real error.
|
||||
if provider_label(model) == "DirectML":
|
||||
raise RuntimeError(
|
||||
"DA3 inference failed on DirectML. Switch the device to "
|
||||
"CPU in SnapForm Preferences and retry. "
|
||||
f"Original error: {error}"
|
||||
) from error
|
||||
raise
|
||||
check_cancelled(cancel_check)
|
||||
write_status(args.status, 0.88, f"Exporting {args.export_format}")
|
||||
if args.export_format == "mesh":
|
||||
|
||||
Reference in New Issue
Block a user