Refactor image mesh processing and background removal functionality

- Updated NODE_GROUP_NAME from "O Refine DA3 Surface" to "O DA3 Surface".
- Introduced `remove_surface_background` function to filter out background faces and compact geometry.
- Enhanced `build_image_mesh` to conditionally remove background faces based on a new property.
- Added `snapform_remove_background` and `snapform_keep_original` properties to operators for better control over background handling.
- Updated UI to reflect new properties and improved resolution scaling logic.
- Refactored tests to cover new background removal functionality and ensure proper geometry handling.
- Adjusted geometry nodes and validation scripts to remove references to background handling.
This commit is contained in:
2026-08-12 16:15:14 +08:00
parent 7cd5681a8b
commit c3a151ad2c
13 changed files with 477 additions and 271 deletions

Binary file not shown.

View File

@@ -15,12 +15,14 @@ ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
BUMP_BASE_DISTANCE = 0.001
DEPTH_PLANE_NODE_GROUP_NAME = "O DA3 Depth Plane"
NODE_ASSET_NAME = "O_SnapForm.blend"
REMOVE_BACKGROUND_ATTRIBUTE = "o_remove_background"
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
DEPTH_SCALE_ATTRIBUTE = "o_depth_scale"
SURFACE_MATERIAL_NODE_GROUP_NAME = "O DA3 Surface"
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME = "O DA3 Depth Plane Material"
SURFACE_MATERIAL_NODE_GROUP_NAME = "O DA3 Surface Layer"
DEPTH_PLANE_MATERIAL_NODE_GROUP_NAME = "O DA3 Depth Plane Layer"
DA3_PATCH_SIZE = 14
DA3_MIN_PROCESS_RES = 252
DA3_MAX_PROCESS_RES = 1008
IMAGE_NAME_SUFFIXES = (
".jpeg",
".jpg",
@@ -180,13 +182,45 @@ def removed_background_data_name(source_object):
return f"{image_base_name(source_object.data)}.foreground"
def scaled_process_resolution(image, scale):
def scaled_process_resolution(image, scale_percent):
"""Return the scaled long edge clamped and aligned for DA3 inference."""
width, height = image.size
if width <= 0 or height <= 0:
raise ValueError("The source image has no readable dimensions")
if scale < 0.1 or scale > 1.0:
raise ValueError("Resolution Scale must be between 0.1 and 1.0")
return int(max(width, height) * scale + 0.5)
if scale_percent < 1.0 or scale_percent > 100.0:
raise ValueError("Resolution Scale must be between 1% and 100%")
process_res = max(width, height) * scale_percent / 100.0
process_res = max(DA3_MIN_PROCESS_RES, process_res)
process_res = min(DA3_MAX_PROCESS_RES, process_res)
return int(process_res / DA3_PATCH_SIZE + 0.5) * DA3_PATCH_SIZE
def process_resolution_scale_property():
return bpy.props.FloatProperty(
name="Resolution Scale",
description=(
"Scale the source image long edge for DA3 processing; the result "
"is limited to 252-1008 px and aligned to a multiple of 14"
),
default=100.0,
min=1.0,
max=100.0,
step=100,
precision=0,
subtype="PERCENTAGE",
)
def draw_process_resolution(layout, owner, image):
process_res = scaled_process_resolution(
image,
owner.snapform_process_res_factor,
)
layout.prop(
owner,
"snapform_process_res_factor",
text=f"Resolution Scale ({process_res} px)",
)
def save_packed_image(image, scene):
@@ -880,13 +914,15 @@ def material_node_group(depth_plane=False):
socket.default_value = 1.0
socket.min_value = -10000.0
socket.max_value = 10000.0
for name, socket_type in (
output_sockets = [
("Color", "NodeSocketColor"),
("Roughness", "NodeSocketFloat"),
("Alpha", "NodeSocketFloat"),
("Normal", "NodeSocketVector"),
("Displacement", "NodeSocketVector"),
):
]
if depth_plane:
output_sockets.append(("Displacement", "NodeSocketVector"))
for name, socket_type in output_sockets:
interface.new_socket(
name=name,
in_out="OUTPUT",
@@ -908,23 +944,20 @@ def material_node_group(depth_plane=False):
roughness_curve.location = (-250.0, 350.0)
bump = nodes.new("ShaderNodeBump")
bump.location = (-250.0, 0.0)
displacement = nodes.new("ShaderNodeDisplacement")
displacement.inputs["Midlevel"].default_value = 0.0
displacement.inputs["Scale"].default_value = 1.0
displacement.location = (-250.0, -300.0)
displacement = None
processed_alpha = group_input.outputs["Alpha"]
processed_depth = group_input.outputs["Depth"]
bump_depth = group_input.outputs["Color"]
if depth_plane:
displacement = nodes.new("ShaderNodeDisplacement")
displacement.inputs["Midlevel"].default_value = 0.0
displacement.inputs["Scale"].default_value = 1.0
group_input.location = (-1200.0, 100.0)
group_output.location = (1050.0, 100.0)
roughness_curve.location = (-300.0, 450.0)
bump.location = (750.0, 50.0)
displacement.location = (750.0, -300.0)
remove_attribute = nodes.new("ShaderNodeAttribute")
remove_attribute.attribute_type = "GEOMETRY"
remove_attribute.attribute_name = REMOVE_BACKGROUND_ATTRIBUTE
remove_attribute.location = (-950.0, 0.0)
background_attribute = nodes.new("ShaderNodeAttribute")
background_attribute.attribute_type = "GEOMETRY"
background_attribute.attribute_name = BACKGROUND_DEPTH_ATTRIBUTE
@@ -937,10 +970,6 @@ def material_node_group(depth_plane=False):
strength_attribute.attribute_type = "GEOMETRY"
strength_attribute.attribute_name = DEPTH_SCALE_ATTRIBUTE
strength_attribute.location = (200.0, -650.0)
alpha_mix = nodes.new("ShaderNodeMix")
alpha_mix.data_type = "FLOAT"
alpha_mix.inputs[2].default_value = 1.0
alpha_mix.location = (-650.0, 50.0)
foreground_mask = nodes.new("ShaderNodeMath")
foreground_mask.operation = "GREATER_THAN"
foreground_mask.inputs[1].default_value = 0.5
@@ -951,31 +980,23 @@ def material_node_group(depth_plane=False):
masked_depth = nodes.new("ShaderNodeMix")
masked_depth.data_type = "FLOAT"
masked_depth.location = (-350.0, -350.0)
background_mix = nodes.new("ShaderNodeMix")
background_mix.data_type = "FLOAT"
background_mix.location = (-50.0, -350.0)
enabled_depth = nodes.new("ShaderNodeMath")
enabled_depth.operation = "MULTIPLY"
enabled_depth.location = (250.0, -350.0)
scaled_depth = nodes.new("ShaderNodeMath")
scaled_depth.operation = "MULTIPLY"
scaled_depth.location = (450.0, -350.0)
links.new(remove_attribute.outputs["Fac"], alpha_mix.inputs[0])
links.new(group_input.outputs["Alpha"], alpha_mix.inputs[3])
links.new(group_input.outputs["Alpha"], foreground_mask.inputs[0])
links.new(group_input.outputs["Depth"], clamp_foreground.inputs[0])
links.new(background_attribute.outputs["Fac"], clamp_foreground.inputs[1])
links.new(foreground_mask.outputs[0], masked_depth.inputs[0])
links.new(background_attribute.outputs["Fac"], masked_depth.inputs[2])
links.new(clamp_foreground.outputs[0], masked_depth.inputs[3])
links.new(remove_attribute.outputs["Fac"], background_mix.inputs[0])
links.new(group_input.outputs["Depth"], background_mix.inputs[2])
links.new(masked_depth.outputs["Result"], background_mix.inputs[3])
links.new(background_mix.outputs["Result"], enabled_depth.inputs[0])
links.new(masked_depth.outputs["Result"], enabled_depth.inputs[0])
links.new(material_attribute.outputs["Fac"], enabled_depth.inputs[1])
links.new(enabled_depth.outputs[0], scaled_depth.inputs[0])
links.new(strength_attribute.outputs["Fac"], scaled_depth.inputs[1])
processed_alpha = alpha_mix.outputs["Result"]
processed_alpha = foreground_mask.outputs[0]
processed_depth = scaled_depth.outputs[0]
links.new(group_input.outputs["Color"], group_output.inputs["Color"])
@@ -985,7 +1006,7 @@ def material_node_group(depth_plane=False):
group_output.inputs["Roughness"],
)
links.new(processed_alpha, group_output.inputs["Alpha"])
links.new(processed_depth, bump.inputs["Height"])
links.new(bump_depth, bump.inputs["Height"])
bump_distance = nodes.new("ShaderNodeMath")
bump_distance.operation = "MULTIPLY"
bump_distance.inputs[1].default_value = BUMP_BASE_DISTANCE
@@ -993,11 +1014,12 @@ def material_node_group(depth_plane=False):
links.new(group_input.outputs["Bump Scale"], bump_distance.inputs[0])
links.new(bump_distance.outputs[0], bump.inputs["Distance"])
links.new(bump.outputs["Normal"], group_output.inputs["Normal"])
links.new(processed_depth, displacement.inputs["Height"])
links.new(
displacement.outputs["Displacement"],
group_output.inputs["Displacement"],
)
if displacement is not None:
links.new(processed_depth, displacement.inputs["Height"])
links.new(
displacement.outputs["Displacement"],
group_output.inputs["Displacement"],
)
group.color_tag = "SHADER"
group.description = (
"Prepare DA3 depth-plane material outputs."
@@ -1081,9 +1103,7 @@ def create_material(
material_inputs = nodes.new("ShaderNodeGroup")
is_depth_plane = displacement_image is not None
material_inputs.node_tree = material_node_group(is_depth_plane)
material_inputs.name = (
"O DA3 Depth Plane" if is_depth_plane else "O DA3 Surface"
)
material_inputs.name = material_inputs.node_tree.name
material_inputs.label = material_inputs.name
material_inputs.location = (-300.0, 350.0)
material_inputs.inputs["Bump Scale"].default_value = depth_scale
@@ -1243,21 +1263,32 @@ def add_depth_plane_processing(
frame_start=1,
frame_duration=1,
use_cyclic=False,
animate_frame=False,
):
node_group = load_depth_plane_node_group()
modifier = plane_object.modifiers.new(name=node_group.name, type="NODES")
modifier = plane_object.modifiers.new(
name=DEPTH_PLANE_NODE_GROUP_NAME,
type="NODES",
)
modifier.node_group = node_group
modifier.show_group_selector = False
for name, value in (
("Depth Image", depth_image),
("Foreground Image", foreground_image),
("Material", material),
("Frame", 1),
("Frame Start", frame_start),
("Frames", max(int(frame_duration or 1), 1)),
("Cyclic", use_cyclic),
):
identifier = modifier_input_identifier(node_group, name)
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"
plane_object.update_tag(refresh={"DATA"})
return modifier
@@ -1353,6 +1384,7 @@ def build_image_depth_plane(
frame_start=source_frame_start,
frame_duration=frame_count,
use_cyclic=source_use_cyclic,
animate_frame=is_animated_image(source_object.data),
)
plane_object["snapform_frame_count"] = frame_count
return finish_depth_plane(
@@ -1456,6 +1488,7 @@ def build_depth_plane(
frame_start=source_frame_start,
frame_duration=frame_count,
use_cyclic=source_use_cyclic,
animate_frame=is_animated_image(source_object.data),
)
plane_object["snapform_reference_depth"] = reference_depth
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
@@ -1744,6 +1777,11 @@ 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 background during conversion with BEN2",
default=False,
)
snapform_model: bpy.props.EnumProperty(
name="Model",
description="Model used to estimate image depth",
@@ -1754,33 +1792,24 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
description="Use ray-based camera pose estimation",
default=True,
)
snapform_process_res_factor: bpy.props.FloatProperty(
name="Resolution Scale",
description="Scale applied to the source image long-edge resolution",
default=1.0,
min=0.1,
max=1.0,
step=10,
precision=2,
)
snapform_delete_original: bpy.props.BoolProperty(
name="Delete Original",
description="Delete the source Image Empty after conversion",
default=True,
snapform_process_res_factor: process_resolution_scale_property()
snapform_keep_original: bpy.props.BoolProperty(
name="Keep Original",
description="Keep the source Image Empty after conversion",
default=False,
)
@classmethod
def poll(cls, context):
from .operators import active_job
from .runtime import host_busy, model_ready
from .models import BEN2_MODEL, MODELS
from .models import MODELS
return (
is_image_empty(context.object)
and active_job() is None
and not host_busy()
and any(model_ready(model_key) for model_key in MODELS)
and model_ready(BEN2_MODEL.key)
)
def invoke(self, context, _event):
@@ -1810,28 +1839,17 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
else:
estimate_box.prop(self, "snapform_model")
estimate_box.prop(self, "snapform_use_ray_pose")
process_res = scaled_process_resolution(
context.object.data,
self.snapform_process_res_factor,
)
estimate_box.prop(
self,
"snapform_process_res_factor",
text=f"Resolution Scale ({process_res} px)",
)
settings_header, settings_body = layout.panel(
"snapform_depth_plane_options",
default_closed=True,
)
settings_header.label(text="Options")
if settings_body is not None:
settings_body.prop(self, "snapform_unshaded")
layout.prop(self, "snapform_delete_original")
draw_process_resolution(estimate_box, self, context.object.data)
layout.prop(self, "snapform_remove_background")
layout.prop(self, "snapform_unshaded")
layout.separator(type="LINE")
layout.prop(self, "snapform_keep_original")
def execute(self, context):
from .operators import (
IMAGE_DEPTH_JOB,
ORIGINAL_BACKGROUND_JOB,
ORIGINAL_FRAMES_JOB,
)
source_object = context.object
@@ -1849,10 +1867,14 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
result = bpy.ops.snapform.run_job(
job_type=ORIGINAL_BACKGROUND_JOB,
job_type=(
ORIGINAL_BACKGROUND_JOB
if self.snapform_remove_background
else ORIGINAL_FRAMES_JOB
),
snapform_input_path=str(source_path),
snapform_source_object_name=source_object.name,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_source=not self.snapform_keep_original,
snapform_delete_input=delete_input,
snapform_unshaded=self.snapform_unshaded,
)
@@ -1888,10 +1910,10 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
snapform_model_key=self.snapform_model,
snapform_use_ray_pose=self.snapform_use_ray_pose,
snapform_process_res=process_res,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_source=not self.snapform_keep_original,
snapform_delete_input=delete_input,
snapform_unshaded=self.snapform_unshaded,
snapform_remove_background=True,
snapform_remove_background=self.snapform_remove_background,
)
if "CANCELLED" in result:
if delete_input:

View File

@@ -4,10 +4,12 @@ import bpy
from .image_depth import (
create_material,
draw_process_resolution,
image_depth_model_items,
is_image_empty,
plane_bounds,
prepare_image_input,
process_resolution_scale_property,
remove_temporary_input,
scaled_process_resolution,
video_total_frames,
@@ -33,7 +35,7 @@ FACE_ATTRIBUTES = {
"o_edge_stretch": ("FLOAT", "face_edge_stretch"),
"o_background_face": ("BOOLEAN", "face_background"),
}
NODE_GROUP_NAME = "O Refine DA3 Surface"
NODE_GROUP_NAME = "O DA3 Surface"
NODE_ASSET_NAME = "O_SnapForm.blend"
@@ -202,6 +204,30 @@ def assign_snapform_attributes(mesh, attributes):
)
def remove_surface_background(vertices, faces, uv, attributes):
"""Remove background faces and compact matching point/face data."""
import numpy as np
background_faces = np.asarray(attributes["face_background"], dtype=bool)
kept_faces = faces[~background_faces]
if not len(kept_faces):
raise ValueError("Background removal deleted every surface face")
used_vertices = np.unique(kept_faces)
index_map = np.full(len(vertices), -1, dtype=np.int32)
index_map[used_vertices] = np.arange(len(used_vertices), dtype=np.int32)
filtered_attributes = {}
for _name, (_data_type, key) in POINT_ATTRIBUTES.items():
filtered_attributes[key] = attributes[key][used_vertices]
for _name, (_data_type, key) in FACE_ATTRIBUTES.items():
filtered_attributes[key] = attributes[key][~background_faces]
return (
vertices[used_vertices],
index_map[kept_faces],
uv[used_vertices],
filtered_attributes,
)
def node_asset_path():
return Path(__file__).with_name("assets") / NODE_ASSET_NAME
@@ -333,6 +359,7 @@ def build_image_mesh(
delete_source,
create_camera,
unshaded,
remove_background=False,
):
(
camera_vertices,
@@ -343,6 +370,13 @@ def build_image_mesh(
reference_depth,
attributes,
) = load_image_mesh_data(npz_path)
if remove_background:
camera_vertices, faces, uv, attributes = remove_surface_background(
camera_vertices,
faces,
uv,
attributes,
)
vertices, camera_parameters = empty_anchored_vertices(
source_object,
camera_vertices,
@@ -415,6 +449,10 @@ class BuildImageMesh(bpy.types.Operator):
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_remove_background: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
source_object = bpy.data.objects.get(self.snapform_source_object_name)
@@ -429,6 +467,7 @@ class BuildImageMesh(bpy.types.Operator):
self.snapform_delete_source,
self.snapform_create_camera,
self.snapform_unshaded,
self.snapform_remove_background,
)
except (OSError, RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
@@ -452,15 +491,7 @@ class ConvertImageToMesh(bpy.types.Operator):
description="Use ray-based camera pose estimation",
default=True,
)
snapform_process_res_factor: bpy.props.FloatProperty(
name="Resolution Scale",
description="Scale applied to the source image long-edge resolution",
default=1.0,
min=0.1,
max=1.0,
step=10,
precision=2,
)
snapform_process_res_factor: process_resolution_scale_property()
snapform_create_camera: bpy.props.BoolProperty(
name="Create Matching Camera",
description="Create a perspective camera matching the source projection",
@@ -471,15 +502,20 @@ class ConvertImageToMesh(bpy.types.Operator):
description="Display the source color through an Emission shader",
default=False,
)
snapform_delete_original: bpy.props.BoolProperty(
name="Delete Original",
description="Delete the source Image Empty after conversion",
default=True,
snapform_remove_background: bpy.props.BoolProperty(
name="Remove Background",
description="Remove the background during conversion with BEN2",
default=False,
)
snapform_keep_original: bpy.props.BoolProperty(
name="Keep Original",
description="Keep the source Image Empty after conversion",
default=False,
)
@classmethod
def poll(cls, context):
from .models import BEN2_MODEL, MODELS
from .models import MODELS
from .operators import active_job
from .runtime import host_busy, model_ready
@@ -489,7 +525,6 @@ class ConvertImageToMesh(bpy.types.Operator):
and active_job() is None
and not host_busy()
and any(model_ready(model_key) for model_key in MODELS)
and model_ready(BEN2_MODEL.key)
)
def invoke(self, context, _event):
@@ -511,18 +546,12 @@ class ConvertImageToMesh(bpy.types.Operator):
layout = self.layout
layout.prop(self, "snapform_model")
layout.prop(self, "snapform_use_ray_pose")
process_res = scaled_process_resolution(
context.object.data,
self.snapform_process_res_factor,
)
layout.prop(
self,
"snapform_process_res_factor",
text=f"Resolution Scale ({process_res} px)",
)
draw_process_resolution(layout, self, context.object.data)
layout.prop(self, "snapform_create_camera")
layout.prop(self, "snapform_remove_background")
layout.prop(self, "snapform_unshaded")
layout.prop(self, "snapform_delete_original")
layout.separator(type="LINE")
layout.prop(self, "snapform_keep_original")
def execute(self, context):
from .operators import IMAGE_MESH_JOB
@@ -550,10 +579,11 @@ class ConvertImageToMesh(bpy.types.Operator):
snapform_model_key=self.snapform_model,
snapform_use_ray_pose=self.snapform_use_ray_pose,
snapform_process_res=process_res,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_source=not self.snapform_keep_original,
snapform_delete_input=delete_input,
snapform_mesh_create_camera=self.snapform_create_camera,
snapform_unshaded=self.snapform_unshaded,
snapform_remove_background=self.snapform_remove_background,
)
if "CANCELLED" in result:
if delete_input:

View File

@@ -328,8 +328,6 @@ class RunJob(bpy.types.Operator):
remove_background = bool(
getattr(self, "snapform_remove_background", False)
)
if self.job_type == IMAGE_MESH_JOB:
remove_background = True
if self.job_type == "INSTALL_RUNTIME":
self._require_online_access()
return runtime.install_runtime_command(paths["status"]), paths
@@ -723,6 +721,7 @@ class RunJob(bpy.types.Operator):
snapform_delete_source=job.delete_source,
snapform_create_camera=job.mesh_create_camera,
snapform_unshaded=job.unshaded,
snapform_remove_background=job.remove_background,
)
if "FINISHED" not in result:
raise RuntimeError("Unable to build the perspective surface")

View File

@@ -473,6 +473,12 @@ def export_prediction_attributes(prediction, output_directory):
return output_path
def remove_background_faces(faces, foreground_mask):
"""Return only triangles whose vertices all belong to the foreground."""
foreground_points = foreground_mask.reshape(-1)
return faces[foreground_points[faces].all(axis=1)]
def export_image_mesh_data(
prediction,
image_path,
@@ -505,9 +511,9 @@ def export_image_mesh_data(
raw_depth = depth_frames[0]
confidence = confidence_frames[0]
intrinsics = np.asarray(prediction.intrinsics[0], dtype=np.float32)
progress(2, "Unprojecting camera geometry")
progress(2, "Unprojecting the depth grid")
points = unproject_camera_depth(raw_depth, intrinsics)
progress(3, "Preparing DA3 and BEN2 attributes")
progress(3, "Preparing confidence and background data")
foreground_mask = np.ones(raw_depth.shape, dtype=bool)
if foreground_path is not None:
foreground_mask = foreground_mesh_mask(
@@ -518,7 +524,7 @@ def export_image_mesh_data(
raw_depth,
confidence,
)
progress(4, "Building editable source topology")
progress(4, "Building source triangles")
faces = mesh_triangle_faces(
points,
raw_depth,
@@ -527,11 +533,13 @@ def export_image_mesh_data(
False,
0.0,
)
if foreground_path is not None:
faces = remove_background_faces(faces, foreground_mask)
if not len(faces):
raise ValueError(
"DA3 output does not contain any valid surface faces"
)
progress(5, "Projecting UV and structured attributes")
progress(5, "Preparing source UV and attributes")
vertices, faces = compact_mesh(
points.reshape(-1, 3),
faces,
@@ -551,7 +559,7 @@ def export_image_mesh_data(
reference_depth = float(np.median(valid_depth))
output_directory = Path(output_directory)
output_directory.mkdir(parents=True, exist_ok=True)
progress(6, "Writing surface data")
progress(6, "Writing source surface data")
result_path = output_directory / "mesh.npz"
np.savez_compressed(
result_path,
@@ -564,7 +572,8 @@ def export_image_mesh_data(
**attributes,
)
texture_path = output_directory / "texture.png"
with Image.open(image_path) as image:
texture_source = foreground_path or image_path
with Image.open(texture_source) as image:
image.convert("RGBA").save(texture_path, format="PNG", compress_level=1)
return result_path, texture_path
@@ -1650,7 +1659,7 @@ def reconstruct(args, model_cache=None, cancel_check=None):
write_status(
args.status,
0.88 + 0.09 * step / total,
f"Post-processing {step}/{total}: {label}",
f"Preparing surface data {step}/{total}: {label}",
)
export_image_mesh_data(