Add comprehensive documentation for SnapForm extension
- Created development.md for development setup and licensing information. - Added features.md detailing functionalities and usage instructions for developers and users. - Introduced index.md to serve as a navigation hub for the documentation. - Developed outputs.md to explain output formats and their specifications. - Established service.md to describe the AI Service architecture and environment setup. - Created surface.md to outline the process of converting Image Empty to perspective meshes.
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "snapform"
|
||||
version = "0.10.11"
|
||||
version = "0.10.12"
|
||||
name = "SnapForm"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
|
||||
@@ -167,6 +167,14 @@ def depth_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.depth"
|
||||
|
||||
|
||||
def normal_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.normal"
|
||||
|
||||
|
||||
def mask_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.mask"
|
||||
|
||||
|
||||
def copy_depth_image(source_object):
|
||||
"""Return a renamed copy of the source image for use as the depth map."""
|
||||
depth_image = source_object.data.copy()
|
||||
@@ -818,11 +826,12 @@ def create_moge_mask_image(name, mask_values):
|
||||
return image
|
||||
|
||||
|
||||
def create_moge_normal_image(name, normal_values):
|
||||
def create_moge_normal_image(name, normal_values, convert_coordinates=True):
|
||||
import numpy as np
|
||||
|
||||
converted = normal_values.astype(np.float32, copy=True)
|
||||
converted *= np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
||||
if convert_coordinates:
|
||||
converted *= np.array([1.0, -1.0, -1.0], dtype=np.float32)
|
||||
converted = np.clip(converted * 0.5 + 0.5, 0.0, 1.0)
|
||||
height, width = converted.shape[:2]
|
||||
image = bpy.data.images.new(
|
||||
@@ -1144,6 +1153,9 @@ def material_node_group(depth_plane=False, surface_group_name=None):
|
||||
("Alpha", "NodeSocketFloat"),
|
||||
("Bump Scale", "NodeSocketFloat"),
|
||||
]
|
||||
if group_name == MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME:
|
||||
input_sockets.insert(2, ("Normal", "NodeSocketColor"))
|
||||
input_sockets.insert(3, ("Normal Strength", "NodeSocketFloat"))
|
||||
if depth_plane:
|
||||
input_sockets.insert(2, ("Depth", "NodeSocketFloat"))
|
||||
for name, socket_type in input_sockets:
|
||||
@@ -1161,6 +1173,10 @@ def material_node_group(depth_plane=False, surface_group_name=None):
|
||||
socket.default_value = 1.0
|
||||
socket.min_value = -10000.0
|
||||
socket.max_value = 10000.0
|
||||
elif name == "Normal Strength":
|
||||
socket.default_value = 1.0
|
||||
socket.min_value = 0.0
|
||||
socket.max_value = 10.0
|
||||
output_sockets = [
|
||||
("Color", "NodeSocketColor"),
|
||||
("Roughness", "NodeSocketFloat"),
|
||||
@@ -1191,6 +1207,16 @@ def material_node_group(depth_plane=False, surface_group_name=None):
|
||||
roughness_curve.location = (-250.0, 350.0)
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
bump.location = (-250.0, 0.0)
|
||||
if group_name == MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME:
|
||||
normal_map = nodes.new("ShaderNodeNormalMap")
|
||||
normal_map.space = "OBJECT"
|
||||
normal_map.location = (-500.0, -150.0)
|
||||
links.new(group_input.outputs["Normal"], normal_map.inputs["Color"])
|
||||
links.new(
|
||||
group_input.outputs["Normal Strength"],
|
||||
normal_map.inputs["Strength"],
|
||||
)
|
||||
links.new(normal_map.outputs["Normal"], bump.inputs["Normal"])
|
||||
displacement = None
|
||||
|
||||
processed_alpha = group_input.outputs["Alpha"]
|
||||
@@ -1295,6 +1321,7 @@ def create_material(
|
||||
depth_frame_duration=None,
|
||||
use_cyclic=None,
|
||||
surface_group_name=None,
|
||||
normal_image=None,
|
||||
):
|
||||
material = bpy.data.materials.new(name)
|
||||
material.use_nodes = True
|
||||
@@ -1359,6 +1386,14 @@ def create_material(
|
||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
||||
links.new(alpha, material_inputs.inputs["Alpha"])
|
||||
if normal_image is not None and "Normal" in material_inputs.inputs:
|
||||
normal_texture = nodes.new("ShaderNodeTexImage")
|
||||
normal_texture.image = normal_image
|
||||
normal_texture.interpolation = "Linear"
|
||||
normal_texture.extension = "EXTEND"
|
||||
normal_texture.location = (-1000.0, -50.0)
|
||||
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
||||
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
||||
if unshaded:
|
||||
emission = nodes.new("ShaderNodeEmission")
|
||||
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
||||
@@ -1923,13 +1958,13 @@ def build_moge2_depth_plane(
|
||||
depth_scale = apply_plane_transform(plane_object, source_object, bounds)
|
||||
apply_object_scale_to_mesh(plane_object)
|
||||
depth_image = create_depth_image(
|
||||
f"{depth_data_name(source_object)}.moge2",
|
||||
depth_data_name(source_object),
|
||||
displacement[0] * depth_scale,
|
||||
)
|
||||
if len(depth) > 1 and data_frames_directory is not None:
|
||||
bpy.data.images.remove(depth_image, do_unlink=True)
|
||||
depth_image = create_moge_sequence_image(
|
||||
f"{depth_data_name(source_object)}.moge2",
|
||||
depth_data_name(source_object),
|
||||
[frame * depth_scale for frame in displacement],
|
||||
context.scene,
|
||||
data_frames_directory,
|
||||
@@ -2201,7 +2236,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
default=False,
|
||||
)
|
||||
snapform_model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
name="Depth From",
|
||||
description="Model or original image used to build depth",
|
||||
items=image_depth_model_items,
|
||||
default=1,
|
||||
|
||||
@@ -7,6 +7,7 @@ from .image_depth import (
|
||||
create_material,
|
||||
draw_process_resolution,
|
||||
is_image_empty,
|
||||
normal_data_name,
|
||||
plane_bounds,
|
||||
prepare_image_input,
|
||||
process_resolution_scale_property,
|
||||
@@ -18,22 +19,16 @@ from .image_depth import (
|
||||
|
||||
POINT_ATTRIBUTES = {
|
||||
"o_depth": ("FLOAT", "point_depth"),
|
||||
"o_confidence": ("FLOAT", "point_confidence"),
|
||||
"o_confidence_percentile": (
|
||||
"FLOAT",
|
||||
"point_confidence_percentile",
|
||||
),
|
||||
"o_background": ("BOOLEAN", "point_background"),
|
||||
}
|
||||
FACE_ATTRIBUTES = {
|
||||
"o_face_confidence": (
|
||||
"FLOAT",
|
||||
"face_confidence_percentile",
|
||||
),
|
||||
"o_background_face": ("BOOLEAN", "face_background"),
|
||||
}
|
||||
OPTIONAL_FACE_ATTRIBUTES = {
|
||||
"o_mask": ("BOOLEAN", "face_mask"),
|
||||
"o_depth_discontinuity": ("FLOAT", "face_depth_discontinuity"),
|
||||
}
|
||||
NODE_GROUP_NAME = "O DA3 Surface"
|
||||
MOGE_NODE_GROUP_NAME = "O MoGe2 Surface"
|
||||
@@ -61,10 +56,10 @@ def load_image_mesh_data(npz_path, moge2=False):
|
||||
"point_depth",
|
||||
]
|
||||
if moge2:
|
||||
required.append("face_mask")
|
||||
required.extend(("face_mask", "face_depth_discontinuity", "normal_map"))
|
||||
else:
|
||||
required.extend(
|
||||
("point_confidence", "point_confidence_percentile", "point_background", "face_confidence_percentile", "face_background")
|
||||
("face_confidence_percentile", "face_depth_discontinuity")
|
||||
)
|
||||
missing = [key for key in required if key not in archive.files]
|
||||
if missing:
|
||||
@@ -93,9 +88,9 @@ def load_image_mesh_data(npz_path, moge2=False):
|
||||
for _name, (_data_type, key) in OPTIONAL_FACE_ATTRIBUTES.items():
|
||||
if key in archive.files:
|
||||
attributes[key] = np.asarray(archive[key])
|
||||
if "vertex_normal" in archive.files:
|
||||
attributes["vertex_normal"] = np.asarray(
|
||||
archive["vertex_normal"],
|
||||
if "normal_map" in archive.files:
|
||||
attributes["normal_map"] = np.asarray(
|
||||
archive["normal_map"],
|
||||
dtype=np.float32,
|
||||
)
|
||||
except OSError as error:
|
||||
@@ -112,6 +107,14 @@ def load_image_mesh_data(npz_path, moge2=False):
|
||||
raise ValueError(f"Unsupported image size: {image_size}")
|
||||
if not reference_depth > 0.0:
|
||||
raise ValueError("Surface reference depth must be positive")
|
||||
if moge2 and attributes["normal_map"].shape != (
|
||||
image_size[1],
|
||||
image_size[0],
|
||||
3,
|
||||
):
|
||||
raise ValueError("Surface normal map and image sizes do not match")
|
||||
if moge2 and not np.isfinite(attributes["normal_map"]).all():
|
||||
raise ValueError("Surface normal map contains invalid values")
|
||||
for _name, (_data_type, key) in POINT_ATTRIBUTES.items():
|
||||
if key in attributes and len(attributes[key]) != len(vertices):
|
||||
raise ValueError(f"Point attribute has the wrong length: {key}")
|
||||
@@ -208,6 +211,13 @@ def assign_snapform_attributes(mesh, attributes, moge2=False):
|
||||
if moge2:
|
||||
assign_mesh_attribute(mesh, "o_depth", "FLOAT", "POINT", attributes["point_depth"])
|
||||
assign_mesh_attribute(mesh, "o_mask", "BOOLEAN", "FACE", attributes["face_mask"])
|
||||
assign_mesh_attribute(
|
||||
mesh,
|
||||
"o_depth_discontinuity",
|
||||
"FLOAT",
|
||||
"FACE",
|
||||
attributes["face_depth_discontinuity"],
|
||||
)
|
||||
return
|
||||
for name, (data_type, key) in POINT_ATTRIBUTES.items():
|
||||
assign_mesh_attribute(
|
||||
@@ -234,50 +244,12 @@ def assign_snapform_attributes(mesh, attributes, moge2=False):
|
||||
"FACE",
|
||||
attributes[key],
|
||||
)
|
||||
assign_edge_depth_difference(mesh, attributes["point_depth"])
|
||||
|
||||
|
||||
def assign_edge_depth_difference(mesh, point_depth):
|
||||
import numpy as np
|
||||
|
||||
edge_vertices = np.empty(len(mesh.edges) * 2, dtype=np.int32)
|
||||
mesh.edges.foreach_get("vertices", edge_vertices)
|
||||
edge_vertices = edge_vertices.reshape(-1, 2)
|
||||
depth = np.asarray(point_depth, dtype=np.float32)
|
||||
first = depth[edge_vertices[:, 0]]
|
||||
second = depth[edge_vertices[:, 1]]
|
||||
denominator = np.maximum(np.minimum(np.abs(first), np.abs(second)), 1e-6)
|
||||
difference = np.abs(first - second) / denominator
|
||||
assign_mesh_attribute(
|
||||
mesh,
|
||||
"o_edge_stretch",
|
||||
"o_depth_discontinuity",
|
||||
"FLOAT",
|
||||
"EDGE",
|
||||
difference,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
"FACE",
|
||||
attributes["face_depth_discontinuity"],
|
||||
)
|
||||
|
||||
|
||||
@@ -285,7 +257,7 @@ def prepare_moge_surface_archive(results_path, output_path):
|
||||
import numpy as np
|
||||
|
||||
with np.load(results_path, allow_pickle=False) as archive:
|
||||
required = {"depth", "mask", "points", "intrinsics"}
|
||||
required = {"depth", "mask", "normal", "points", "intrinsics"}
|
||||
missing = required.difference(archive.files)
|
||||
if missing:
|
||||
raise ValueError(f"MoGe-2 surface data is missing: {', '.join(sorted(missing))}")
|
||||
@@ -293,11 +265,7 @@ def prepare_moge_surface_archive(results_path, output_path):
|
||||
mask = np.asarray(archive["mask"], dtype=bool)
|
||||
points = np.asarray(archive["points"], dtype=np.float32)
|
||||
intrinsics = np.asarray(archive["intrinsics"], dtype=np.float32)
|
||||
normal = (
|
||||
np.asarray(archive["normal"], dtype=np.float32)
|
||||
if "normal" in archive.files
|
||||
else None
|
||||
)
|
||||
normal = np.asarray(archive["normal"], dtype=np.float32)
|
||||
if depth.shape[0] != 1 or points.shape[:3] != (*depth.shape,):
|
||||
raise ValueError("Convert to Surface requires one matching MoGe-2 point grid")
|
||||
raw_depth = depth[0]
|
||||
@@ -314,8 +282,9 @@ def prepare_moge_surface_archive(results_path, output_path):
|
||||
if not len(faces):
|
||||
raise ValueError("MoGe-2 output does not contain a valid surface")
|
||||
flat_points = points[0].reshape(-1, 3)
|
||||
clean_mask = mask[0] & ~depth_discontinuity_mask(raw_depth, 0.04)
|
||||
face_mask = clean_mask.reshape(-1)[faces].all(axis=1)
|
||||
face_mask = mask[0].reshape(-1)[faces].all(axis=1)
|
||||
depth_edge_ratio = depth_discontinuity_ratio(raw_depth)
|
||||
face_depth_discontinuity = depth_edge_ratio.reshape(-1)[faces].max(axis=1)
|
||||
used = np.unique(faces)
|
||||
index_map = np.full(height * width, -1, dtype=np.int32)
|
||||
index_map[used] = np.arange(len(used), dtype=np.int32)
|
||||
@@ -341,32 +310,21 @@ def prepare_moge_surface_archive(results_path, output_path):
|
||||
image_size=np.array((width, height), dtype=np.int32),
|
||||
reference_depth=np.float32(reference_depth),
|
||||
point_depth=point_depth,
|
||||
vertex_normal=(
|
||||
normal[0].reshape(-1, 3)[used]
|
||||
if normal is not None
|
||||
else np.empty((0, 3), dtype=np.float32)
|
||||
),
|
||||
normal_map=normal[0],
|
||||
face_mask=face_mask,
|
||||
face_depth_discontinuity=face_depth_discontinuity,
|
||||
)
|
||||
return output_path
|
||||
|
||||
|
||||
def depth_discontinuity_mask(depth, threshold):
|
||||
def depth_discontinuity_ratio(depth):
|
||||
import numpy as np
|
||||
|
||||
edges = np.zeros(depth.shape, dtype=bool)
|
||||
for axis in (0, 1):
|
||||
first = np.take(depth, range(depth.shape[axis] - 1), axis=axis)
|
||||
second = np.take(depth, range(1, depth.shape[axis]), axis=axis)
|
||||
scale = np.maximum(np.minimum(np.abs(first), np.abs(second)), 1e-6)
|
||||
discontinuity = np.abs(first - second) / scale > threshold
|
||||
lower = [slice(None), slice(None)]
|
||||
upper = [slice(None), slice(None)]
|
||||
lower[axis] = slice(0, -1)
|
||||
upper[axis] = slice(1, None)
|
||||
edges[tuple(lower)] |= discontinuity
|
||||
edges[tuple(upper)] |= discontinuity
|
||||
return edges
|
||||
padded = np.pad(depth, 1, mode="edge")
|
||||
windows = np.lib.stride_tricks.sliding_window_view(padded, (3, 3))
|
||||
difference = np.nanmax(windows, axis=(-2, -1))
|
||||
difference -= np.nanmin(windows, axis=(-2, -1))
|
||||
return difference / np.maximum(depth, 1e-6)
|
||||
|
||||
|
||||
def node_asset_path():
|
||||
@@ -505,7 +463,6 @@ def build_image_mesh(
|
||||
delete_source,
|
||||
create_camera,
|
||||
unshaded,
|
||||
remove_background=False,
|
||||
moge2=False,
|
||||
):
|
||||
import numpy as np
|
||||
@@ -519,13 +476,6 @@ def build_image_mesh(
|
||||
reference_depth,
|
||||
attributes,
|
||||
) = load_image_mesh_data(npz_path, moge2=moge2)
|
||||
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,
|
||||
@@ -541,24 +491,25 @@ def build_image_mesh(
|
||||
mesh.update()
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
if moge2 and len(attributes.get("vertex_normal", ())) == len(vertices):
|
||||
normals = attributes["vertex_normal"].copy()
|
||||
normals[:, 0] /= camera_parameters["scale_x"]
|
||||
normals[:, 1] /= -camera_parameters["scale_y"]
|
||||
normals[:, 2] /= -camera_parameters["scale_z"]
|
||||
lengths = np.linalg.norm(normals, axis=1, keepdims=True)
|
||||
normals /= np.maximum(lengths, 1e-6)
|
||||
set_custom_normals = getattr(
|
||||
mesh,
|
||||
"normals_split_custom_set_from_vertices",
|
||||
None,
|
||||
)
|
||||
if callable(set_custom_normals):
|
||||
set_custom_normals(normals.tolist())
|
||||
mesh_object = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(mesh_object)
|
||||
mesh_object.matrix_world = source_object.matrix_world.copy()
|
||||
depth_scale = material_depth_scale(mesh_object, camera_parameters)
|
||||
normal_image = None
|
||||
if moge2 and attributes.get("normal_map", np.empty(0)).size:
|
||||
from .image_depth import create_moge_normal_image
|
||||
|
||||
normal_map = attributes["normal_map"].copy()
|
||||
normal_map[..., 0] /= camera_parameters["scale_x"]
|
||||
normal_map[..., 1] /= -camera_parameters["scale_y"]
|
||||
normal_map[..., 2] /= -camera_parameters["scale_z"]
|
||||
lengths = np.linalg.norm(normal_map, axis=-1, keepdims=True)
|
||||
normal_map /= np.maximum(lengths, 1e-6)
|
||||
normal_image = create_moge_normal_image(
|
||||
normal_data_name(source_object),
|
||||
normal_map,
|
||||
convert_coordinates=False,
|
||||
)
|
||||
material = create_material(
|
||||
source_name,
|
||||
source_object.data,
|
||||
@@ -567,6 +518,7 @@ def build_image_mesh(
|
||||
surface_group_name=(
|
||||
MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME if moge2 else None
|
||||
),
|
||||
normal_image=normal_image,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
mesh_object["snapform_reference_depth"] = reference_depth
|
||||
@@ -620,10 +572,6 @@ class BuildImageMesh(bpy.types.Operator):
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_remove_background: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_moge2: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
@@ -642,7 +590,6 @@ class BuildImageMesh(bpy.types.Operator):
|
||||
self.snapform_delete_source,
|
||||
self.snapform_create_camera,
|
||||
self.snapform_unshaded,
|
||||
self.snapform_remove_background,
|
||||
self.snapform_moge2,
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
@@ -658,7 +605,7 @@ class ConvertImageToMesh(bpy.types.Operator):
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
snapform_model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
name="Depth From",
|
||||
description="Model used to estimate image depth",
|
||||
items=image_surface_model_items,
|
||||
default=1,
|
||||
|
||||
@@ -822,7 +822,6 @@ 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")
|
||||
@@ -841,7 +840,6 @@ 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=False,
|
||||
snapform_moge2=True,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
|
||||
@@ -125,11 +125,12 @@ class ModelsPanel(bpy.types.Panel):
|
||||
layout = self.layout
|
||||
running = active_job() is not None or client.service_busy()
|
||||
|
||||
for model in MOGE2_MODELS.values():
|
||||
self._draw_model(layout, model, running)
|
||||
for model in MODELS.values():
|
||||
if model.enabled:
|
||||
self._draw_model(layout, model, running)
|
||||
for model in MOGE2_MODELS.values():
|
||||
self._draw_model(layout, model, running)
|
||||
layout.separator(type="LINE")
|
||||
self._draw_model(layout, BEN2_MODEL, running)
|
||||
|
||||
def _draw_model(self, layout, model, running):
|
||||
@@ -190,7 +191,7 @@ def draw_da3_input_settings(layout, settings, model_property):
|
||||
|
||||
|
||||
class Da3OutputDebugPanel(bpy.types.Panel):
|
||||
bl_label = "DA3 Output Test"
|
||||
bl_label = "DA3"
|
||||
bl_idname = "SNAPFORM_PT_debug_da3_output"
|
||||
bl_parent_id = DebugPanel.bl_idname
|
||||
bl_space_type = "VIEW_3D"
|
||||
@@ -240,7 +241,7 @@ class Da3OutputDebugPanel(bpy.types.Panel):
|
||||
|
||||
|
||||
class Moge2ApiDebugPanel(bpy.types.Panel):
|
||||
bl_label = "MoGe-2 API Test"
|
||||
bl_label = "MoGe-2"
|
||||
bl_idname = "SNAPFORM_PT_debug_moge2_api"
|
||||
bl_parent_id = DebugPanel.bl_idname
|
||||
bl_space_type = "VIEW_3D"
|
||||
@@ -271,7 +272,7 @@ class Moge2ApiDebugPanel(bpy.types.Panel):
|
||||
|
||||
|
||||
class Ben2RemovalDebugPanel(bpy.types.Panel):
|
||||
bl_label = "BEN2 Removal Test"
|
||||
bl_label = "BEN2"
|
||||
bl_idname = "SNAPFORM_PT_debug_ben2_removal"
|
||||
bl_parent_id = DebugPanel.bl_idname
|
||||
bl_space_type = "VIEW_3D"
|
||||
|
||||
@@ -110,6 +110,8 @@ class ModelCache:
|
||||
|
||||
def get_da3(self, model_dir, device):
|
||||
key = (str(Path(model_dir).resolve()), device.type)
|
||||
if self.moge_model is not None:
|
||||
self.release_moge()
|
||||
if self.da3_model is not None and self.da3_key == key:
|
||||
return self.da3_model
|
||||
self.release_da3()
|
||||
@@ -132,6 +134,8 @@ class ModelCache:
|
||||
|
||||
def get_moge(self, model_dir, device):
|
||||
key = (str(Path(model_dir).resolve()), device.type)
|
||||
if self.da3_model is not None:
|
||||
self.release_da3()
|
||||
if self.moge_model is not None and self.moge_key == key:
|
||||
return self.moge_model
|
||||
self.release_moge()
|
||||
|
||||
@@ -347,6 +347,16 @@ def confidence_percentile_map(depth, confidence):
|
||||
return result
|
||||
|
||||
|
||||
def depth_discontinuity_ratio(depth):
|
||||
import numpy as np
|
||||
|
||||
padded = np.pad(depth, 1, mode="edge")
|
||||
windows = np.lib.stride_tricks.sliding_window_view(padded, (3, 3))
|
||||
difference = np.nanmax(windows, axis=(-2, -1))
|
||||
difference -= np.nanmin(windows, axis=(-2, -1))
|
||||
return difference / np.maximum(depth, 1e-6)
|
||||
|
||||
|
||||
def triangle_edge_stretch_values(points, triangles):
|
||||
import numpy as np
|
||||
|
||||
@@ -381,39 +391,23 @@ def image_mesh_attributes(
|
||||
vertices,
|
||||
faces,
|
||||
intrinsics,
|
||||
depth,
|
||||
confidence,
|
||||
confidence_percentiles,
|
||||
foreground_mask,
|
||||
face_depth_discontinuity,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
pixels = project_camera_pixels(vertices, intrinsics)
|
||||
point_confidence = sample_image_nearest(confidence, pixels).astype(
|
||||
np.float32
|
||||
)
|
||||
point_confidence = np.nan_to_num(point_confidence, nan=0.0)
|
||||
point_percentile = sample_image_nearest(
|
||||
confidence_percentiles,
|
||||
pixels,
|
||||
project_camera_pixels(vertices, intrinsics),
|
||||
).astype(np.float32)
|
||||
point_background = ~sample_image_nearest(foreground_mask, pixels).astype(
|
||||
bool
|
||||
)
|
||||
face_confidence = point_percentile[faces].min(axis=1)
|
||||
face_edge_stretch = triangle_edge_stretch_values(
|
||||
vertices,
|
||||
faces,
|
||||
).max(axis=1)
|
||||
face_background = point_background[faces].any(axis=1)
|
||||
return {
|
||||
"point_depth": vertices[:, 2].astype(np.float32),
|
||||
"point_confidence": point_confidence,
|
||||
"point_confidence_percentile": point_percentile,
|
||||
"point_background": point_background,
|
||||
"face_confidence_percentile": face_confidence.astype(np.float32),
|
||||
"face_edge_stretch": face_edge_stretch.astype(np.float32),
|
||||
"face_background": face_background,
|
||||
"face_depth_discontinuity": np.asarray(
|
||||
face_depth_discontinuity,
|
||||
dtype=np.float32,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -507,6 +501,8 @@ def export_image_mesh_data(
|
||||
"DA3 output does not contain any valid surface faces"
|
||||
)
|
||||
progress(5, "Preparing source UV and attributes")
|
||||
depth_discontinuity = depth_discontinuity_ratio(raw_depth)
|
||||
face_depth_discontinuity = depth_discontinuity.reshape(-1)[faces].max(axis=1)
|
||||
vertices, faces = compact_mesh(
|
||||
points.reshape(-1, 3),
|
||||
faces,
|
||||
@@ -517,13 +513,10 @@ def export_image_mesh_data(
|
||||
vertices,
|
||||
faces,
|
||||
intrinsics,
|
||||
raw_depth,
|
||||
confidence,
|
||||
confidence_percentiles,
|
||||
foreground_mask,
|
||||
face_depth_discontinuity,
|
||||
)
|
||||
valid_depth = raw_depth[np.isfinite(raw_depth) & (raw_depth > 0.0)]
|
||||
reference_depth = float(np.median(valid_depth))
|
||||
reference_depth = float(np.median(vertices[:, 2]))
|
||||
output_directory = Path(output_directory)
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
progress(6, "Writing source surface data")
|
||||
@@ -1568,26 +1561,46 @@ def export_moge_surface_archive(
|
||||
mask,
|
||||
normal,
|
||||
intrinsics,
|
||||
foreground_mask=None,
|
||||
):
|
||||
import numpy as np
|
||||
import utils3d
|
||||
|
||||
height, width = depth.shape
|
||||
clean_mask = mask & ~utils3d.np.depth_map_edge(depth, rtol=0.04)
|
||||
finite_mask = np.isfinite(depth) & (depth > 0.0)
|
||||
finite_mask &= np.isfinite(points).all(axis=-1)
|
||||
if foreground_mask is not None:
|
||||
finite_mask &= foreground_mask
|
||||
depth_discontinuity = depth_discontinuity_ratio(depth)
|
||||
result = utils3d.np.build_mesh_from_map(
|
||||
points,
|
||||
utils3d.np.uv_map(height, width),
|
||||
normal,
|
||||
mask=clean_mask,
|
||||
mask.astype(np.float32),
|
||||
depth_discontinuity.astype(np.float32),
|
||||
depth,
|
||||
mask=finite_mask,
|
||||
tri=True,
|
||||
)
|
||||
faces, vertices, vertex_uvs, vertex_normals = result
|
||||
(
|
||||
faces,
|
||||
vertices,
|
||||
vertex_uvs,
|
||||
_vertex_normals,
|
||||
vertex_mask,
|
||||
vertex_depth_discontinuity,
|
||||
point_depth,
|
||||
) = result
|
||||
vertex_uvs = vertex_uvs * np.array([1.0, -1.0]) + np.array([0.0, 1.0])
|
||||
pixel_intrinsics = intrinsics.copy()
|
||||
pixel_intrinsics[0] *= width
|
||||
pixel_intrinsics[1] *= height
|
||||
pixel_intrinsics[2] = (0.0, 0.0, 1.0)
|
||||
point_depth = vertices[:, 2].astype(np.float32)
|
||||
point_depth = point_depth.astype(np.float32)
|
||||
face_mask = vertex_mask[faces].all(axis=1)
|
||||
face_depth_discontinuity = vertex_depth_discontinuity[faces].max(axis=1)
|
||||
masked_depth = point_depth[vertex_mask.astype(bool)]
|
||||
reference_depth = np.median(masked_depth if len(masked_depth) else point_depth)
|
||||
np.savez(
|
||||
output_path,
|
||||
vertices=vertices.astype(np.float32),
|
||||
@@ -1595,10 +1608,11 @@ def export_moge_surface_archive(
|
||||
uv=vertex_uvs.astype(np.float32),
|
||||
intrinsics=pixel_intrinsics.astype(np.float32),
|
||||
image_size=np.array((width, height), dtype=np.int32),
|
||||
reference_depth=np.float32(np.median(point_depth)),
|
||||
reference_depth=np.float32(reference_depth),
|
||||
point_depth=point_depth,
|
||||
vertex_normal=vertex_normals.astype(np.float32),
|
||||
face_mask=np.ones(len(faces), dtype=bool),
|
||||
normal_map=normal.astype(np.float32),
|
||||
face_mask=face_mask,
|
||||
face_depth_discontinuity=face_depth_discontinuity,
|
||||
)
|
||||
|
||||
|
||||
@@ -1703,6 +1717,7 @@ def run_moge(args, model_cache=None, cancel_check=None):
|
||||
debug_format,
|
||||
)
|
||||
|
||||
foreground_mask = None
|
||||
remove_background_model_dir = getattr(
|
||||
args, "remove_background_model_dir", ""
|
||||
)
|
||||
@@ -1724,7 +1739,6 @@ def run_moge(args, model_cache=None, cancel_check=None):
|
||||
foreground_path,
|
||||
result["depth"][0].shape,
|
||||
)
|
||||
result["mask"][0] &= foreground_mask
|
||||
else:
|
||||
infer_foreground_frames(
|
||||
images,
|
||||
@@ -1749,6 +1763,11 @@ def run_moge(args, model_cache=None, cancel_check=None):
|
||||
result["mask"][0],
|
||||
result["normal"][0],
|
||||
result["intrinsics"][0],
|
||||
foreground_mask=(
|
||||
foreground_mask
|
||||
if remove_background_model_dir
|
||||
else None
|
||||
),
|
||||
)
|
||||
np.savez(output_directory / "results.npz", **result)
|
||||
copy_frames(images, output_directory)
|
||||
|
||||
Reference in New Issue
Block a user