Add SnapForm panels, preferences, properties, and runtime management
- Implemented RuntimeModelsPanel and DebugPanel for UI integration in Blender. - Created SnapFormPreferences to manage user settings for storage and runtime configuration. - Defined SnapFormSettings to hold various properties related to input, model selection, and export options. - Developed runtime management functions for installation, model downloading, and reconstruction commands. - Added status bar functionality to display job progress and status in the Blender UI. - Implemented worker script for handling model downloads and depth reconstruction tasks. - Introduced unit tests for download progress reporting and image depth extraction functionalities.
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from .operators import (
|
||||
OMOO_DA3_OT_cancel_job,
|
||||
OMOO_DA3_OT_run_job,
|
||||
cancel_active_job,
|
||||
close_active_job,
|
||||
)
|
||||
from .panel import OMOO_DA3_PT_debug, OMOO_DA3_PT_runtime_models
|
||||
from .preferences import OMOO_DA3_preferences
|
||||
from .properties import OMOO_DA3_PG_settings
|
||||
from .status_bar import draw_status_bar
|
||||
|
||||
|
||||
CLASSES = (
|
||||
OMOO_DA3_preferences,
|
||||
OMOO_DA3_PG_settings,
|
||||
OMOO_DA3_OT_run_job,
|
||||
OMOO_DA3_OT_cancel_job,
|
||||
OMOO_DA3_PT_runtime_models,
|
||||
OMOO_DA3_PT_debug,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
for class_type in CLASSES:
|
||||
bpy.utils.register_class(class_type)
|
||||
bpy.types.Scene.omoo_da3_settings = bpy.props.PointerProperty(
|
||||
type=OMOO_DA3_PG_settings,
|
||||
)
|
||||
bpy.types.STATUSBAR_HT_header.prepend(draw_status_bar)
|
||||
|
||||
|
||||
def unregister():
|
||||
cancel_active_job()
|
||||
close_active_job()
|
||||
bpy.types.STATUSBAR_HT_header.remove(draw_status_bar)
|
||||
if hasattr(bpy.types.Scene, "omoo_da3_settings"):
|
||||
del bpy.types.Scene.omoo_da3_settings
|
||||
for class_type in reversed(CLASSES):
|
||||
bpy.utils.unregister_class(class_type)
|
||||
50
src/snapform/__init__.py
Normal file
50
src/snapform/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import bpy
|
||||
|
||||
from .image_depth import (
|
||||
BuildDepthPlane,
|
||||
ConvertImageToDepthPlane,
|
||||
draw_image_context_menu,
|
||||
)
|
||||
from .operators import (
|
||||
CancelJob,
|
||||
RunJob,
|
||||
cancel_active_job,
|
||||
close_active_job,
|
||||
)
|
||||
from .panel import DebugPanel, RuntimeModelsPanel
|
||||
from .preferences import SnapFormPreferences
|
||||
from .properties import SnapFormSettings
|
||||
from .status_bar import draw_status_bar
|
||||
|
||||
|
||||
CLASSES = (
|
||||
SnapFormPreferences,
|
||||
SnapFormSettings,
|
||||
RunJob,
|
||||
CancelJob,
|
||||
BuildDepthPlane,
|
||||
ConvertImageToDepthPlane,
|
||||
RuntimeModelsPanel,
|
||||
DebugPanel,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
for class_type in CLASSES:
|
||||
bpy.utils.register_class(class_type)
|
||||
bpy.types.Scene.snapform_settings = bpy.props.PointerProperty(
|
||||
type=SnapFormSettings,
|
||||
)
|
||||
bpy.types.STATUSBAR_HT_header.append(draw_status_bar)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(draw_image_context_menu)
|
||||
|
||||
|
||||
def unregister():
|
||||
cancel_active_job()
|
||||
close_active_job()
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(draw_image_context_menu)
|
||||
bpy.types.STATUSBAR_HT_header.remove(draw_status_bar)
|
||||
if hasattr(bpy.types.Scene, "snapform_settings"):
|
||||
del bpy.types.Scene.snapform_settings
|
||||
for class_type in reversed(CLASSES):
|
||||
bpy.utils.unregister_class(class_type)
|
||||
@@ -1,9 +1,9 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "omoo_da3"
|
||||
version = "0.8.1"
|
||||
name = "OmooLab DA3"
|
||||
tagline = "Export official DA3 GLB, PLY, depth, and mini NPZ"
|
||||
id = "snapform"
|
||||
version = "0.9.27"
|
||||
name = "SnapForm"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
type = "add-on"
|
||||
|
||||
@@ -160,7 +160,7 @@ def install(args):
|
||||
]
|
||||
)
|
||||
|
||||
ready_path = runtime_dir / "omoo_da3_ready.json"
|
||||
ready_path = runtime_dir / "snapform_ready.json"
|
||||
ready_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
640
src/snapform/image_depth.py
Normal file
640
src/snapform/image_depth.py
Normal file
@@ -0,0 +1,640 @@
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
DEPTH_KEY_CANDIDATES = ("depth", "depths")
|
||||
INTRINSICS_KEY_CANDIDATES = ("intrinsics", "ixts")
|
||||
IMAGE_DEPTH_MODEL_ITEMS = (
|
||||
(
|
||||
"SMALL",
|
||||
"DA3 Small",
|
||||
"Fastest model with Apache 2.0 weights",
|
||||
),
|
||||
(
|
||||
"BASE",
|
||||
"DA3 Base",
|
||||
"Balanced model with Apache 2.0 weights",
|
||||
),
|
||||
(
|
||||
"LARGE_1_1",
|
||||
"DA3 Large 1.1",
|
||||
"Higher quality model with CC BY-NC 4.0 weights",
|
||||
),
|
||||
(
|
||||
"GIANT_1_1",
|
||||
"DA3 Giant 1.1",
|
||||
"Giant model with Gaussian head and CC BY-NC 4.0 weights",
|
||||
),
|
||||
(
|
||||
"NESTED_GIANT_LARGE_1_1",
|
||||
"DA3 Nested Giant Large 1.1",
|
||||
"Nested Giant model with metric depth and CC BY-NC 4.0 weights",
|
||||
),
|
||||
)
|
||||
DISPLACEMENT_STRENGTH = -1.0
|
||||
DISPLACEMENT_MIDLEVEL = 1.0
|
||||
BUMP_BASE_DISTANCE = 0.001
|
||||
|
||||
|
||||
def is_image_empty(candidate):
|
||||
return bool(
|
||||
candidate
|
||||
and getattr(candidate, "type", None) == "EMPTY"
|
||||
and getattr(candidate, "empty_display_type", None) == "IMAGE"
|
||||
and getattr(candidate, "data", None) is not None
|
||||
)
|
||||
|
||||
|
||||
def image_path(image):
|
||||
filepath_from_user = getattr(image, "filepath_from_user", None)
|
||||
filepath = (
|
||||
filepath_from_user()
|
||||
if callable(filepath_from_user)
|
||||
else getattr(image, "filepath", "")
|
||||
)
|
||||
if not filepath:
|
||||
return Path()
|
||||
return Path(bpy.path.abspath(filepath)).expanduser()
|
||||
|
||||
|
||||
def plane_bounds(source_object):
|
||||
image_width, image_height = source_object.data.size
|
||||
if image_width <= 0 or image_height <= 0:
|
||||
raise ValueError("The source image has no readable dimensions")
|
||||
|
||||
aspect = image_width / image_height
|
||||
display_size = float(source_object.empty_display_size)
|
||||
if aspect >= 1.0:
|
||||
width = display_size
|
||||
height = display_size / aspect
|
||||
else:
|
||||
width = display_size * aspect
|
||||
height = display_size
|
||||
|
||||
offset = source_object.empty_image_offset
|
||||
x_min = float(offset[0]) * width
|
||||
y_min = float(offset[1]) * height
|
||||
return (
|
||||
x_min,
|
||||
x_min + width,
|
||||
y_min,
|
||||
y_min + height,
|
||||
)
|
||||
|
||||
|
||||
def plane_fit_transform(source_object, camera_bounds):
|
||||
target_bounds = plane_bounds(source_object)
|
||||
camera_width = camera_bounds[1] - camera_bounds[0]
|
||||
camera_height = camera_bounds[3] - camera_bounds[2]
|
||||
if camera_width <= 0.0 or camera_height <= 0.0:
|
||||
raise ValueError("The camera plane has invalid dimensions")
|
||||
|
||||
target_width = target_bounds[1] - target_bounds[0]
|
||||
target_height = target_bounds[3] - target_bounds[2]
|
||||
scale_x = target_width / camera_width
|
||||
scale_y = target_height / camera_height
|
||||
scale_z = (scale_x * scale_y) ** 0.5
|
||||
camera_center_x = (camera_bounds[0] + camera_bounds[1]) * 0.5
|
||||
camera_center_y = (camera_bounds[2] + camera_bounds[3]) * 0.5
|
||||
target_center_x = (target_bounds[0] + target_bounds[1]) * 0.5
|
||||
target_center_y = (target_bounds[2] + target_bounds[3]) * 0.5
|
||||
return (
|
||||
scale_x,
|
||||
scale_y,
|
||||
scale_z,
|
||||
target_center_x - camera_center_x * scale_x,
|
||||
target_center_y - camera_center_y * scale_y,
|
||||
)
|
||||
|
||||
|
||||
def apply_plane_transform(plane_object, source_object, camera_bounds):
|
||||
from mathutils import Matrix
|
||||
|
||||
scale_x, scale_y, scale_z, offset_x, offset_y = plane_fit_transform(
|
||||
source_object,
|
||||
camera_bounds,
|
||||
)
|
||||
fit_matrix = Matrix(
|
||||
(
|
||||
(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, 0.0, 1.0),
|
||||
)
|
||||
)
|
||||
plane_object.matrix_world = source_object.matrix_world @ fit_matrix
|
||||
return abs(float(plane_object.scale.z))
|
||||
|
||||
|
||||
def extract_depth_array(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:
|
||||
depth_key = next(
|
||||
(
|
||||
key
|
||||
for key in DEPTH_KEY_CANDIDATES
|
||||
if key in archive.files
|
||||
),
|
||||
None,
|
||||
)
|
||||
if depth_key is None:
|
||||
keys = ", ".join(archive.files) or "none"
|
||||
raise ValueError(
|
||||
f"Mini NPZ has no depth array; available keys: {keys}"
|
||||
)
|
||||
depth = np.asarray(archive[depth_key], dtype=np.float32)
|
||||
except OSError as error:
|
||||
raise ValueError(f"Unable to read Mini NPZ: {npz_path}") from error
|
||||
|
||||
if depth.ndim >= 3 and depth.shape[-1] == 1:
|
||||
depth = depth[..., 0]
|
||||
if depth.ndim == 3:
|
||||
depth = depth[0]
|
||||
if depth.ndim != 2:
|
||||
raise ValueError(
|
||||
f"Unsupported depth array shape: {tuple(depth.shape)}"
|
||||
)
|
||||
return depth
|
||||
|
||||
|
||||
def extract_intrinsics_array(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:
|
||||
intrinsics_key = next(
|
||||
(
|
||||
key
|
||||
for key in INTRINSICS_KEY_CANDIDATES
|
||||
if key in archive.files
|
||||
),
|
||||
None,
|
||||
)
|
||||
if intrinsics_key is None:
|
||||
keys = ", ".join(archive.files) or "none"
|
||||
raise ValueError(
|
||||
"Mini NPZ has no camera intrinsics; "
|
||||
f"available keys: {keys}"
|
||||
)
|
||||
intrinsics = np.asarray(
|
||||
archive[intrinsics_key],
|
||||
dtype=np.float32,
|
||||
)
|
||||
except OSError as error:
|
||||
raise ValueError(f"Unable to read Mini NPZ: {npz_path}") from error
|
||||
|
||||
if intrinsics.ndim == 3:
|
||||
intrinsics = intrinsics[0]
|
||||
if intrinsics.shape != (3, 3):
|
||||
raise ValueError(
|
||||
f"Unsupported intrinsics shape: {tuple(intrinsics.shape)}"
|
||||
)
|
||||
return intrinsics
|
||||
|
||||
|
||||
def prepare_depth_values(depth):
|
||||
import numpy as np
|
||||
|
||||
valid = np.isfinite(depth) & (depth > 0.0)
|
||||
if not valid.any():
|
||||
raise ValueError("Depth array contains no positive finite values")
|
||||
values = depth.astype(np.float32, copy=True)
|
||||
values[~valid] = 0.0
|
||||
return values
|
||||
|
||||
|
||||
def camera_plane_bounds(
|
||||
source_object,
|
||||
depth,
|
||||
intrinsics,
|
||||
displacement_strength,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
scale = abs(float(displacement_strength))
|
||||
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]))
|
||||
reference_distance = reference_depth * scale
|
||||
|
||||
height, width = depth.shape
|
||||
fx = float(intrinsics[0, 0])
|
||||
fy = float(intrinsics[1, 1])
|
||||
cx = float(intrinsics[0, 2])
|
||||
cy = float(intrinsics[1, 2])
|
||||
if fx <= 0.0 or fy <= 0.0:
|
||||
raise ValueError("Camera intrinsics contain an invalid focal length")
|
||||
|
||||
source_width, source_height = source_object.data.size
|
||||
if source_width <= 0 or source_height <= 0:
|
||||
raise ValueError("The source image has no readable dimensions")
|
||||
source_aspect = source_width / source_height
|
||||
|
||||
# Predicted fx/fy may differ even for square pixels. Preserve their projected
|
||||
# area while enforcing the source image aspect ratio to avoid image stretch.
|
||||
raw_width = width * reference_distance / fx
|
||||
raw_height = height * reference_distance / fy
|
||||
plane_area = raw_width * raw_height
|
||||
plane_width = float(np.sqrt(plane_area * source_aspect))
|
||||
plane_height = float(np.sqrt(plane_area / source_aspect))
|
||||
|
||||
x_min = -(cx / width) * plane_width
|
||||
x_max = (1.0 - cx / width) * plane_width
|
||||
y_min = -(1.0 - cy / height) * plane_height
|
||||
y_max = (cy / height) * plane_height
|
||||
|
||||
# Preserve an intentional Image Empty origin offset around the camera frame.
|
||||
offset = source_object.empty_image_offset
|
||||
x_offset = (float(offset[0]) + 0.5) * plane_width
|
||||
y_offset = (float(offset[1]) + 0.5) * plane_height
|
||||
return (
|
||||
(
|
||||
x_min + x_offset,
|
||||
x_max + x_offset,
|
||||
y_min + y_offset,
|
||||
y_max + y_offset,
|
||||
),
|
||||
reference_depth,
|
||||
)
|
||||
|
||||
|
||||
def create_depth_image(name, depth_values):
|
||||
import numpy as np
|
||||
|
||||
height, width = depth_values.shape
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=False,
|
||||
float_buffer=True,
|
||||
)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.file_format = "OPEN_EXR"
|
||||
|
||||
# NPZ arrays use a top-left origin, while Blender image pixels start below.
|
||||
depth_pixels = np.flipud(depth_values)
|
||||
rgba = np.empty((height, width, 4), dtype=np.float32)
|
||||
rgba[..., 0] = depth_pixels
|
||||
rgba[..., 1] = depth_pixels
|
||||
rgba[..., 2] = depth_pixels
|
||||
rgba[..., 3] = 1.0
|
||||
image.pixels.foreach_set(rgba.ravel())
|
||||
# Pack as EXR so values outside 0..1 survive saving and reopening the blend.
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def grid_segments(bounds, subdivision_level):
|
||||
if subdivision_level < 0:
|
||||
raise ValueError("Subdivision must not be negative")
|
||||
|
||||
width = bounds[1] - bounds[0]
|
||||
height = bounds[3] - bounds[2]
|
||||
if width <= 0.0 or height <= 0.0:
|
||||
raise ValueError("The plane has invalid dimensions")
|
||||
|
||||
short_segments = 2**subdivision_level
|
||||
if width >= height:
|
||||
return (
|
||||
max(short_segments, int(short_segments * width / height + 0.5)),
|
||||
short_segments,
|
||||
)
|
||||
return (
|
||||
short_segments,
|
||||
max(short_segments, int(short_segments * height / width + 0.5)),
|
||||
)
|
||||
|
||||
|
||||
def create_plane_mesh(source_object, bounds=None, subdivision_level=5):
|
||||
x_min, x_max, y_min, y_max = (
|
||||
bounds if bounds is not None else plane_bounds(source_object)
|
||||
)
|
||||
x_segments, y_segments = grid_segments(
|
||||
(x_min, x_max, y_min, y_max),
|
||||
subdivision_level,
|
||||
)
|
||||
vertices = []
|
||||
uv_coordinates = []
|
||||
for y_index in range(y_segments + 1):
|
||||
v = y_index / y_segments
|
||||
y = y_min + (y_max - y_min) * v
|
||||
for x_index in range(x_segments + 1):
|
||||
u = x_index / x_segments
|
||||
x = x_min + (x_max - x_min) * u
|
||||
vertices.append((x, y, 0.0))
|
||||
uv_coordinates.append((u, v))
|
||||
|
||||
faces = []
|
||||
row_width = x_segments + 1
|
||||
for y_index in range(y_segments):
|
||||
for x_index in range(x_segments):
|
||||
bottom_left = y_index * row_width + x_index
|
||||
faces.append(
|
||||
(
|
||||
bottom_left,
|
||||
bottom_left + 1,
|
||||
bottom_left + row_width + 1,
|
||||
bottom_left + row_width,
|
||||
)
|
||||
)
|
||||
|
||||
mesh = bpy.data.meshes.new(f"{source_object.name}.DA3Depth")
|
||||
mesh.from_pydata(vertices, (), faces)
|
||||
mesh.update()
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
uv_layer = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
uv_layer.data[loop.index].uv = uv_coordinates[
|
||||
loop.vertex_index
|
||||
]
|
||||
return mesh
|
||||
|
||||
|
||||
def create_material(
|
||||
name,
|
||||
color_image,
|
||||
depth_scale,
|
||||
):
|
||||
material = bpy.data.materials.new(name)
|
||||
material.use_nodes = True
|
||||
if hasattr(material, "surface_render_method"):
|
||||
material.surface_render_method = "DITHERED"
|
||||
nodes = material.node_tree.nodes
|
||||
nodes.clear()
|
||||
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
color_texture = nodes.new("ShaderNodeTexImage")
|
||||
bump = nodes.new("ShaderNodeBump")
|
||||
roughness_curve = nodes.new("ShaderNodeFloatCurve")
|
||||
|
||||
color_texture.image = color_image
|
||||
color_texture.interpolation = "Linear"
|
||||
color_texture.extension = "EXTEND"
|
||||
bump.inputs["Distance"].default_value = BUMP_BASE_DISTANCE * depth_scale
|
||||
shader.inputs["IOR"].default_value = 1.2
|
||||
|
||||
roughness_curve.mapping.initialize()
|
||||
curve_points = roughness_curve.mapping.curves[0].points
|
||||
curve_points[0].location = (0.0, 0.5)
|
||||
curve_points[-1].location = (1.0, 0.1)
|
||||
roughness_curve.mapping.update()
|
||||
|
||||
color_texture.location = (-640.0, 180.0)
|
||||
roughness_curve.location = (-300.0, 320.0)
|
||||
bump.location = (-300.0, -80.0)
|
||||
shader.location = (80.0, 180.0)
|
||||
output.location = (460.0, 180.0)
|
||||
|
||||
links = material.node_tree.links
|
||||
links.new(color_texture.outputs["Color"], shader.inputs["Base Color"])
|
||||
links.new(color_texture.outputs["Alpha"], shader.inputs["Alpha"])
|
||||
links.new(color_texture.outputs["Color"], bump.inputs["Height"])
|
||||
links.new(bump.outputs["Normal"], shader.inputs["Normal"])
|
||||
links.new(
|
||||
color_texture.outputs["Color"],
|
||||
roughness_curve.inputs["Value"],
|
||||
)
|
||||
links.new(
|
||||
roughness_curve.outputs["Value"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
return material
|
||||
|
||||
|
||||
def build_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
npz_path,
|
||||
subdivision_level,
|
||||
delete_source,
|
||||
):
|
||||
source_name = source_object.name
|
||||
depth = extract_depth_array(npz_path)
|
||||
intrinsics = extract_intrinsics_array(npz_path)
|
||||
depth_values = prepare_depth_values(depth)
|
||||
bounds, reference_depth = camera_plane_bounds(
|
||||
source_object,
|
||||
depth_values,
|
||||
intrinsics,
|
||||
DISPLACEMENT_STRENGTH,
|
||||
)
|
||||
depth_name = f"{source_name}.depth"
|
||||
depth_image = create_depth_image(
|
||||
depth_name,
|
||||
depth_values,
|
||||
)
|
||||
mesh = create_plane_mesh(source_object, bounds, subdivision_level)
|
||||
plane_object = bpy.data.objects.new(
|
||||
source_name,
|
||||
mesh,
|
||||
)
|
||||
context.collection.objects.link(plane_object)
|
||||
depth_scale = apply_plane_transform(
|
||||
plane_object,
|
||||
source_object,
|
||||
bounds,
|
||||
)
|
||||
|
||||
subdivision = plane_object.modifiers.new("Subdivision", "SUBSURF")
|
||||
subdivision.subdivision_type = "SIMPLE"
|
||||
subdivision.levels = 1
|
||||
subdivision.render_levels = 2
|
||||
|
||||
texture = bpy.data.textures.new(
|
||||
depth_name,
|
||||
type="IMAGE",
|
||||
)
|
||||
texture.image = depth_image
|
||||
texture.extension = "EXTEND"
|
||||
displace = plane_object.modifiers.new("Displace", "DISPLACE")
|
||||
displace.texture = texture
|
||||
displace.texture_coords = "UV"
|
||||
displace.direction = "NORMAL"
|
||||
displace.strength = DISPLACEMENT_STRENGTH
|
||||
displace.mid_level = DISPLACEMENT_MIDLEVEL
|
||||
|
||||
material = create_material(
|
||||
f"{source_name}.DA3DepthMaterial",
|
||||
source_object.data,
|
||||
depth_scale,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
plane_object["snapform_reference_depth"] = reference_depth
|
||||
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
|
||||
plane_object["snapform_camera_plane_height"] = bounds[3] - bounds[2]
|
||||
plane_object["snapform_depth_scale"] = depth_scale
|
||||
|
||||
for selected in context.selected_objects:
|
||||
selected.select_set(False)
|
||||
plane_object.select_set(True)
|
||||
context.view_layer.objects.active = plane_object
|
||||
if delete_source:
|
||||
bpy.data.objects.remove(source_object, do_unlink=True)
|
||||
plane_object.name = source_name
|
||||
return plane_object
|
||||
|
||||
|
||||
class BuildDepthPlane(bpy.types.Operator):
|
||||
bl_idname = "snapform.build_depth_plane"
|
||||
bl_label = "Build Depth Plane"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
snapform_source_object_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_npz_path: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
default=5,
|
||||
min=0,
|
||||
max=10,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_delete_source: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
source_object = bpy.data.objects.get(self.snapform_source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"The source image empty is no longer available",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
npz_path = Path(self.snapform_npz_path)
|
||||
if not npz_path.is_file():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"DA3 Mini NPZ does not exist: {npz_path}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
try:
|
||||
build_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
npz_path,
|
||||
self.snapform_subdivision_level,
|
||||
self.snapform_delete_source,
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
bl_idname = "snapform.convert_image_to_depth_plane"
|
||||
bl_label = "Convert to Depth Plane"
|
||||
bl_description = "Run DA3 and convert the image to a displaced depth plane"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
snapform_model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
description="DA3 model used to estimate image depth",
|
||||
items=IMAGE_DEPTH_MODEL_ITEMS,
|
||||
default="BASE",
|
||||
)
|
||||
snapform_use_ray_pose: bpy.props.BoolProperty(
|
||||
name="Use Ray Pose",
|
||||
description="Use ray-based camera pose estimation",
|
||||
default=True,
|
||||
)
|
||||
snapform_process_res: bpy.props.IntProperty(
|
||||
name="Process Resolution",
|
||||
description="DA3 processing resolution",
|
||||
default=1024,
|
||||
min=14,
|
||||
max=2016,
|
||||
step=14,
|
||||
)
|
||||
snapform_delete_original: bpy.props.BoolProperty(
|
||||
name="Delete Original",
|
||||
description="Delete the source Image Empty after conversion",
|
||||
default=True,
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
name="Subdivision",
|
||||
description="Power of two for the short-edge segment count; the long edge follows the image aspect ratio",
|
||||
default=5,
|
||||
min=0,
|
||||
max=10,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
from .operators import active_job
|
||||
|
||||
return is_image_empty(context.object) and active_job() is None
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=340)
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "snapform_model")
|
||||
layout.prop(self, "snapform_use_ray_pose")
|
||||
layout.prop(self, "snapform_process_res")
|
||||
layout.prop(self, "snapform_subdivision_level")
|
||||
layout.prop(self, "snapform_delete_original")
|
||||
|
||||
def execute(self, context):
|
||||
from .operators import IMAGE_DEPTH_JOB
|
||||
|
||||
source_object = context.object
|
||||
source_path = image_path(source_object.data)
|
||||
if not source_path.is_file():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"The image empty must reference an existing image file",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
result = bpy.ops.snapform.run_job(
|
||||
job_type=IMAGE_DEPTH_JOB,
|
||||
snapform_input_path=str(source_path),
|
||||
snapform_source_object_name=source_object.name,
|
||||
snapform_model_key=self.snapform_model,
|
||||
snapform_use_ray_pose=self.snapform_use_ray_pose,
|
||||
snapform_process_res=self.snapform_process_res,
|
||||
snapform_subdivision_level=self.snapform_subdivision_level,
|
||||
snapform_delete_source=self.snapform_delete_original,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def draw_image_context_menu(self, context):
|
||||
active_object = context.object
|
||||
if not is_image_empty(active_object):
|
||||
return
|
||||
self.layout.separator()
|
||||
row = self.layout.row()
|
||||
row.operator_context = "INVOKE_DEFAULT"
|
||||
row.operator(
|
||||
"snapform.convert_image_to_depth_plane",
|
||||
text="Convert to Depth Plane",
|
||||
icon="MOD_DISPLACE",
|
||||
)
|
||||
@@ -9,6 +9,9 @@ from .models import get_model, selected_model_key
|
||||
|
||||
|
||||
RECONSTRUCTION_JOB = "RECONSTRUCT"
|
||||
IMAGE_DEPTH_JOB = "IMAGE_DEPTH"
|
||||
IMAGE_DEPTH_DEFAULT_PROCESS_RES = 1024
|
||||
IMAGE_DEPTH_PROCESS_RES_METHOD = "lower_bound_resize"
|
||||
|
||||
|
||||
def open_job_directory(result_directory):
|
||||
@@ -29,6 +32,10 @@ class ActiveJob:
|
||||
status_path: Path
|
||||
result_directory: Path
|
||||
export_format: str = ""
|
||||
source_object_name: str = ""
|
||||
process_res: int = IMAGE_DEPTH_DEFAULT_PROCESS_RES
|
||||
subdivision_level: int = 5
|
||||
delete_source: bool = True
|
||||
cancelled: bool = False
|
||||
|
||||
|
||||
@@ -58,9 +65,9 @@ def close_active_job():
|
||||
_active_job = None
|
||||
|
||||
|
||||
class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
bl_idname = "omoo_da3.run_job"
|
||||
bl_label = "Run Omoo DA3 Job"
|
||||
class RunJob(bpy.types.Operator):
|
||||
bl_idname = "snapform.run_job"
|
||||
bl_label = "Run SnapForm Job"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
job_type: bpy.props.EnumProperty(
|
||||
@@ -68,21 +75,52 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
("INSTALL_RUNTIME", "Install Runtime", ""),
|
||||
("DOWNLOAD_MODEL", "Download Model", ""),
|
||||
(RECONSTRUCTION_JOB, "Export DA3 Result", ""),
|
||||
(IMAGE_DEPTH_JOB, "Convert Image to Depth Plane", ""),
|
||||
),
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
model_key: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_input_path: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_source_object_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_model_key: bpy.props.StringProperty(
|
||||
default="BASE",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_use_ray_pose: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_process_res: bpy.props.IntProperty(
|
||||
default=IMAGE_DEPTH_DEFAULT_PROCESS_RES,
|
||||
min=14,
|
||||
max=2016,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_delete_source: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
default=5,
|
||||
min=0,
|
||||
max=10,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
global _active_job
|
||||
|
||||
if active_job() is not None:
|
||||
self.report({"WARNING"}, "Another Omoo DA3 task is running")
|
||||
self.report({"WARNING"}, "Another SnapForm task is running")
|
||||
return {"CANCELLED"}
|
||||
|
||||
settings = context.scene.omoo_da3_settings
|
||||
settings = context.scene.snapform_settings
|
||||
try:
|
||||
command, paths = self._prepare_command(context, settings)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
@@ -111,10 +149,14 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
status_path=paths["status"],
|
||||
result_directory=paths["result"],
|
||||
export_format=(
|
||||
settings.export_format
|
||||
if self.job_type == RECONSTRUCTION_JOB
|
||||
settings.export_format if self.job_type == RECONSTRUCTION_JOB
|
||||
else "mini_npz" if self.job_type == IMAGE_DEPTH_JOB
|
||||
else ""
|
||||
),
|
||||
source_object_name=self.snapform_source_object_name,
|
||||
process_res=self.snapform_process_res,
|
||||
subdivision_level=self.snapform_subdivision_level,
|
||||
delete_source=self.snapform_delete_source,
|
||||
)
|
||||
settings.status = self._starting_message(settings)
|
||||
settings.progress = 0.0
|
||||
@@ -133,7 +175,7 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
return runtime.install_runtime_command(paths["status"]), paths
|
||||
|
||||
if not runtime.runtime_ready():
|
||||
raise RuntimeError("Install the Omoo DA3 Runtime first")
|
||||
raise RuntimeError("Install the SnapForm Runtime first")
|
||||
|
||||
if self.job_type == "DOWNLOAD_MODEL":
|
||||
self._require_online_access()
|
||||
@@ -143,16 +185,31 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
paths,
|
||||
)
|
||||
|
||||
input_path = Path(bpy.path.abspath(settings.input_path))
|
||||
configured_input = (
|
||||
self.snapform_input_path
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
else 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")
|
||||
model_key = selected_model_key(settings)
|
||||
if self.job_type not in {RECONSTRUCTION_JOB, IMAGE_DEPTH_JOB}:
|
||||
raise RuntimeError(f"Unsupported task type: {self.job_type}")
|
||||
export_format = (
|
||||
"mini_npz"
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
else settings.export_format
|
||||
)
|
||||
use_ray_pose = None
|
||||
process_res = None
|
||||
model_key = selected_model_key(settings, export_format)
|
||||
if self.job_type == IMAGE_DEPTH_JOB:
|
||||
model_key = self.snapform_model_key
|
||||
use_ray_pose = self.snapform_use_ray_pose
|
||||
process_res = self.snapform_process_res
|
||||
model = get_model(model_key)
|
||||
if not runtime.model_ready(model_key):
|
||||
raise RuntimeError(f"Download {model.label} first")
|
||||
if self.job_type != RECONSTRUCTION_JOB:
|
||||
raise RuntimeError(f"Unsupported task type: {self.job_type}")
|
||||
export_format = settings.export_format
|
||||
if export_format == "gs_ply" and not model.supports_gs:
|
||||
raise RuntimeError(
|
||||
"PLY export requires a DA3 Giant or Nested Giant model"
|
||||
@@ -163,6 +220,15 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
export_format,
|
||||
paths["status"],
|
||||
paths["result"],
|
||||
input_path=input_path,
|
||||
model_key=model_key,
|
||||
process_res=process_res,
|
||||
process_res_method=(
|
||||
IMAGE_DEPTH_PROCESS_RES_METHOD
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
else None
|
||||
),
|
||||
use_ray_pose=use_ray_pose,
|
||||
),
|
||||
paths,
|
||||
)
|
||||
@@ -180,6 +246,8 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
}
|
||||
if self.job_type == RECONSTRUCTION_JOB:
|
||||
return f"Starting DA3 {settings.export_format} export"
|
||||
if self.job_type == IMAGE_DEPTH_JOB:
|
||||
return "Starting DA3 image depth conversion"
|
||||
if self.job_type == "DOWNLOAD_MODEL":
|
||||
return f"Starting {get_model(self.model_key).label} download"
|
||||
return labels[self.job_type]
|
||||
@@ -206,13 +274,13 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
status = runtime.read_status(job.status_path)
|
||||
if status is None:
|
||||
return
|
||||
settings = context.scene.omoo_da3_settings
|
||||
settings = context.scene.snapform_settings
|
||||
settings.progress = float(status.get("progress", settings.progress))
|
||||
settings.status = status.get("message", settings.status)
|
||||
self._redraw_ui(context)
|
||||
|
||||
def _finish(self, context, job, return_code):
|
||||
settings = context.scene.omoo_da3_settings
|
||||
settings = context.scene.snapform_settings
|
||||
self._remove_timer(context)
|
||||
job.log_file.close()
|
||||
|
||||
@@ -245,6 +313,20 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
{"INFO"},
|
||||
f"Exported {job.export_format} to {job.result_directory}",
|
||||
)
|
||||
if job.kind == IMAGE_DEPTH_JOB:
|
||||
try:
|
||||
settings.status = "Building depth plane"
|
||||
settings.progress = 0.96
|
||||
self._redraw_ui(context, force=True)
|
||||
self._build_depth_plane(context, 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"] = "DA3 depth plane is ready"
|
||||
|
||||
settings.status = status.get("message", "Task complete")
|
||||
settings.progress = 1.0
|
||||
@@ -252,6 +334,25 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"FINISHED"}
|
||||
|
||||
def _build_depth_plane(self, context, job):
|
||||
npz_path = (
|
||||
job.result_directory
|
||||
/ "exports"
|
||||
/ "mini_npz"
|
||||
/ "results.npz"
|
||||
)
|
||||
result = bpy.ops.snapform.build_depth_plane(
|
||||
snapform_source_object_name=job.source_object_name,
|
||||
snapform_npz_path=str(npz_path),
|
||||
snapform_subdivision_level=job.subdivision_level,
|
||||
snapform_delete_source=job.delete_source,
|
||||
)
|
||||
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 _remove_timer(self, context):
|
||||
timer = getattr(self, "_timer", None)
|
||||
if timer is None:
|
||||
@@ -270,9 +371,9 @@ class OMOO_DA3_OT_run_job(bpy.types.Operator):
|
||||
self._redraw_ui(context, force=True)
|
||||
|
||||
|
||||
class OMOO_DA3_OT_cancel_job(bpy.types.Operator):
|
||||
bl_idname = "omoo_da3.cancel_job"
|
||||
bl_label = "Cancel Omoo DA3 Task"
|
||||
class CancelJob(bpy.types.Operator):
|
||||
bl_idname = "snapform.cancel_job"
|
||||
bl_label = "Cancel SnapForm Task"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, _context):
|
||||
@@ -5,12 +5,12 @@ from .operators import active_job
|
||||
from .runtime import model_ready, runtime_ready
|
||||
|
||||
|
||||
class OMOO_DA3_PT_runtime_models(bpy.types.Panel):
|
||||
class RuntimeModelsPanel(bpy.types.Panel):
|
||||
bl_label = "Setup"
|
||||
bl_idname = "OMOO_DA3_PT_runtime_models"
|
||||
bl_idname = "SNAPFORM_PT_runtime_models"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Omoo DA3"
|
||||
bl_category = "SnapForm"
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
@@ -20,7 +20,7 @@ class OMOO_DA3_PT_runtime_models(bpy.types.Panel):
|
||||
layout.label(text="Runtime is not installed", icon="ERROR")
|
||||
if not running:
|
||||
operator = layout.operator(
|
||||
"omoo_da3.run_job",
|
||||
"snapform.run_job",
|
||||
text="Install Runtime",
|
||||
icon="IMPORT",
|
||||
)
|
||||
@@ -36,7 +36,7 @@ class OMOO_DA3_PT_runtime_models(bpy.types.Panel):
|
||||
row.label(text="Ready", icon="CHECKMARK")
|
||||
elif runtime_ready() and not running:
|
||||
operator = row.operator(
|
||||
"omoo_da3.run_job",
|
||||
"snapform.run_job",
|
||||
text="Download",
|
||||
icon="IMPORT",
|
||||
)
|
||||
@@ -46,17 +46,17 @@ class OMOO_DA3_PT_runtime_models(bpy.types.Panel):
|
||||
row.label(text="Not downloaded", icon="ERROR")
|
||||
|
||||
|
||||
class OMOO_DA3_PT_debug(bpy.types.Panel):
|
||||
class DebugPanel(bpy.types.Panel):
|
||||
bl_label = "Debug"
|
||||
bl_idname = "OMOO_DA3_PT_debug"
|
||||
bl_idname = "SNAPFORM_PT_debug"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Omoo DA3"
|
||||
bl_category = "SnapForm"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
settings = context.scene.omoo_da3_settings
|
||||
settings = context.scene.snapform_settings
|
||||
|
||||
layout.prop(settings, "export_format")
|
||||
model_key = selected_model_key(settings)
|
||||
@@ -105,7 +105,7 @@ class OMOO_DA3_PT_debug(bpy.types.Panel):
|
||||
|
||||
if active_job() is None:
|
||||
operator = layout.operator(
|
||||
"omoo_da3.run_job",
|
||||
"snapform.run_job",
|
||||
text="Export",
|
||||
icon="EXPORT",
|
||||
)
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
import bpy
|
||||
|
||||
|
||||
class OMOO_DA3_preferences(bpy.types.AddonPreferences):
|
||||
class SnapFormPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
storage_root: bpy.props.StringProperty(
|
||||
@@ -14,7 +14,7 @@ class OMOO_DA3_preferences(bpy.types.AddonPreferences):
|
||||
"all Blender versions can use the same directory"
|
||||
),
|
||||
subtype="DIR_PATH",
|
||||
default=str(Path.home() / ".omoo_da3"),
|
||||
default=str(Path.home() / ".snapform"),
|
||||
)
|
||||
uv_executable: bpy.props.StringProperty(
|
||||
name="uv Executable",
|
||||
@@ -45,7 +45,7 @@ class OMOO_DA3_preferences(bpy.types.AddonPreferences):
|
||||
layout.label(text="Runtime is ready", icon="CHECKMARK")
|
||||
else:
|
||||
operator = layout.operator(
|
||||
"omoo_da3.run_job",
|
||||
"snapform.run_job",
|
||||
text="Install Runtime",
|
||||
icon="IMPORT",
|
||||
)
|
||||
@@ -3,7 +3,7 @@ import bpy
|
||||
from .models import enum_items
|
||||
|
||||
|
||||
class OMOO_DA3_PG_settings(bpy.types.PropertyGroup):
|
||||
class SnapFormSettings(bpy.types.PropertyGroup):
|
||||
input_path: bpy.props.StringProperty(
|
||||
name="Image or Video",
|
||||
description="A single image or video file used for reconstruction",
|
||||
@@ -41,9 +41,9 @@ class OMOO_DA3_PG_settings(bpy.types.PropertyGroup):
|
||||
process_res: bpy.props.IntProperty(
|
||||
name="Process Res",
|
||||
description="DA3 base processing resolution",
|
||||
default=504,
|
||||
default=1024,
|
||||
min=1,
|
||||
soft_max=1008,
|
||||
soft_max=2048,
|
||||
)
|
||||
process_res_method: bpy.props.EnumProperty(
|
||||
name="Process Res Method",
|
||||
@@ -60,7 +60,7 @@ class OMOO_DA3_PG_settings(bpy.types.PropertyGroup):
|
||||
"Make Process Res the shorter side",
|
||||
),
|
||||
),
|
||||
default="upper_bound_resize",
|
||||
default="lower_bound_resize",
|
||||
)
|
||||
conf_thresh_percentile: bpy.props.FloatProperty(
|
||||
name="Conf Thresh Percentile",
|
||||
@@ -13,7 +13,7 @@ from .models import get_model, selected_model_key
|
||||
|
||||
DA3_COMMIT = "3d835ec1a5802d64a8b8b15f817a1ab54809bfe4"
|
||||
RUNTIME_SCHEMA = 2
|
||||
DEFAULT_STORAGE_ROOT = Path.home() / ".omoo_da3"
|
||||
DEFAULT_STORAGE_ROOT = Path.home() / ".snapform"
|
||||
|
||||
|
||||
def preferences():
|
||||
@@ -45,7 +45,7 @@ def runtime_python():
|
||||
|
||||
|
||||
def runtime_ready():
|
||||
ready_path = runtime_directory() / "omoo_da3_ready.json"
|
||||
ready_path = runtime_directory() / "snapform_ready.json"
|
||||
if not runtime_python().is_file() or not ready_path.is_file():
|
||||
return False
|
||||
try:
|
||||
@@ -146,16 +146,36 @@ def reconstruct_command(
|
||||
export_format,
|
||||
status_path,
|
||||
result_directory,
|
||||
input_path=None,
|
||||
model_key=None,
|
||||
process_res=None,
|
||||
process_res_method=None,
|
||||
use_ray_pose=None,
|
||||
):
|
||||
model_key = selected_model_key(settings, export_format)
|
||||
snapform_selected_key = model_key or selected_model_key(settings, export_format)
|
||||
snapform_selected_process_res = (
|
||||
settings.process_res if process_res is None else process_res
|
||||
)
|
||||
snapform_selected_process_res_method = (
|
||||
settings.process_res_method
|
||||
if process_res_method is None
|
||||
else process_res_method
|
||||
)
|
||||
snapform_selected_ray_pose = (
|
||||
settings.use_ray_pose if use_ray_pose is None else use_ray_pose
|
||||
)
|
||||
return [
|
||||
str(runtime_python()),
|
||||
str(Path(__file__).with_name("worker.py")),
|
||||
"reconstruct",
|
||||
"--input",
|
||||
str(Path(bpy.path.abspath(settings.input_path))),
|
||||
str(
|
||||
Path(input_path)
|
||||
if input_path is not None
|
||||
else Path(bpy.path.abspath(settings.input_path))
|
||||
),
|
||||
"--model-dir",
|
||||
str(model_directory(model_key)),
|
||||
str(model_directory(snapform_selected_key)),
|
||||
"--output-dir",
|
||||
str(result_directory),
|
||||
"--export-format",
|
||||
@@ -163,9 +183,9 @@ def reconstruct_command(
|
||||
"--status",
|
||||
str(status_path),
|
||||
"--process-res",
|
||||
str(settings.process_res),
|
||||
str(snapform_selected_process_res),
|
||||
"--process-res-method",
|
||||
settings.process_res_method,
|
||||
snapform_selected_process_res_method,
|
||||
"--conf-thresh-percentile",
|
||||
str(settings.conf_thresh_percentile),
|
||||
"--num-max-points",
|
||||
@@ -179,7 +199,7 @@ def reconstruct_command(
|
||||
"--device",
|
||||
settings.device.lower(),
|
||||
"--use-ray-pose",
|
||||
"1" if settings.use_ray_pose else "0",
|
||||
"1" if snapform_selected_ray_pose else "0",
|
||||
"--ref-view-strategy",
|
||||
settings.ref_view_strategy,
|
||||
]
|
||||
@@ -1,36 +1,33 @@
|
||||
import bpy
|
||||
|
||||
from .operators import active_job
|
||||
|
||||
|
||||
def draw_status_bar(self, context):
|
||||
scene = getattr(context, "scene", None)
|
||||
if active_job() is None or scene is None:
|
||||
job = active_job()
|
||||
if job is None or scene is None:
|
||||
return
|
||||
|
||||
settings = getattr(scene, "omoo_da3_settings", None)
|
||||
settings = getattr(scene, "snapform_settings", None)
|
||||
if settings is None:
|
||||
return
|
||||
|
||||
progress = min(max(settings.progress, 0.0), 1.0)
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=f"Omoo DA3: {settings.status}", icon="TIME")
|
||||
|
||||
progress_row = row.row(align=True)
|
||||
progress_row.ui_units_x = 9.0
|
||||
progress_row.ui_units_x = 12.0
|
||||
progress_row.progress(
|
||||
factor=progress,
|
||||
type="BAR",
|
||||
text=f"{progress:.0%}",
|
||||
text=settings.status,
|
||||
)
|
||||
row.operator(
|
||||
"omoo_da3.cancel_job",
|
||||
"snapform.cancel_job",
|
||||
text="",
|
||||
icon="X",
|
||||
)
|
||||
|
||||
|
||||
def redraw_ui(context, force=False):
|
||||
def tag_ui_regions(context):
|
||||
window_manager = getattr(context, "window_manager", None)
|
||||
if window_manager is None:
|
||||
return
|
||||
@@ -42,10 +39,9 @@ def redraw_ui(context, force=False):
|
||||
for area in screen.areas:
|
||||
if area.type in {"STATUSBAR", "VIEW_3D"}:
|
||||
area.tag_redraw()
|
||||
for region in getattr(area, "regions", ()):
|
||||
region.tag_redraw()
|
||||
|
||||
if not force or getattr(context, "window", None) is None:
|
||||
return
|
||||
try:
|
||||
bpy.ops.wm.redraw_timer(type="DRAW_WIN_SWAP", iterations=1)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def redraw_ui(context, force=False):
|
||||
tag_ui_regions(context)
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
@@ -93,7 +94,7 @@ def prepare_input(input_path, video_frames, temporary_root):
|
||||
import cv2
|
||||
|
||||
temporary_directory = Path(
|
||||
tempfile.mkdtemp(prefix="omoo-da3-video-", dir=temporary_root)
|
||||
tempfile.mkdtemp(prefix="snapform-video-", dir=temporary_root)
|
||||
)
|
||||
try:
|
||||
images = extract_video_frames(
|
||||
@@ -108,15 +109,85 @@ def prepare_input(input_path, video_frames, temporary_root):
|
||||
return images, temporary_directory
|
||||
|
||||
|
||||
class DownloadProgressReporter:
|
||||
def __init__(self, status_path, repository, clock=time.monotonic):
|
||||
self.status_path = status_path
|
||||
self.repository = repository
|
||||
self.clock = clock
|
||||
self.lock = threading.Lock()
|
||||
self.progress = 0.05
|
||||
self.last_write_time = 0.0
|
||||
|
||||
def report(self, current, total):
|
||||
if not total or current <= 0:
|
||||
return
|
||||
|
||||
factor = min(max(current / total, 0.0), 1.0)
|
||||
progress = 0.05 + factor * 0.90
|
||||
now = self.clock()
|
||||
with self.lock:
|
||||
if progress <= self.progress:
|
||||
return
|
||||
if now - self.last_write_time < 0.20 and progress < 0.95:
|
||||
return
|
||||
self.progress = progress
|
||||
self.last_write_time = now
|
||||
write_status(
|
||||
self.status_path,
|
||||
progress,
|
||||
f"Downloading {self.repository}",
|
||||
)
|
||||
|
||||
|
||||
def create_download_progress_class(
|
||||
status_path,
|
||||
repository,
|
||||
tqdm_type=None,
|
||||
clock=time.monotonic,
|
||||
):
|
||||
if tqdm_type is None:
|
||||
from tqdm.auto import tqdm as tqdm_type
|
||||
|
||||
reporter = DownloadProgressReporter(status_path, repository, clock)
|
||||
|
||||
class DownloadProgress(tqdm_type):
|
||||
def __init__(self, *args, **kwargs):
|
||||
description = str(kwargs.get("desc", ""))
|
||||
self.snapform_reports_progress = (
|
||||
description == "Downloading bytes"
|
||||
or description.startswith("Fetching ")
|
||||
)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def update(self, amount=1):
|
||||
result = super().update(amount)
|
||||
if self.snapform_reports_progress:
|
||||
reporter.report(self.n, self.total)
|
||||
return result
|
||||
|
||||
def refresh(self, *args, **kwargs):
|
||||
result = super().refresh(*args, **kwargs)
|
||||
if self.snapform_reports_progress:
|
||||
reporter.report(self.n, self.total)
|
||||
return result
|
||||
|
||||
return DownloadProgress
|
||||
|
||||
|
||||
def download_model(args):
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
model_dir = Path(args.model_dir)
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
write_status(args.status, 0.05, f"Downloading {args.repository}")
|
||||
progress_class = create_download_progress_class(
|
||||
args.status,
|
||||
args.repository,
|
||||
)
|
||||
snapshot_download(
|
||||
repo_id=args.repository,
|
||||
local_dir=model_dir,
|
||||
tqdm_class=progress_class,
|
||||
)
|
||||
write_status(args.status, 1.0, "Model is ready")
|
||||
|
||||
@@ -158,13 +229,15 @@ def wait_for_exported_paths(output_directory, export_format, timeout=30.0):
|
||||
|
||||
|
||||
def reconstruct(args):
|
||||
write_status(args.status, 0.02, "Starting DA3 runtime")
|
||||
|
||||
import torch
|
||||
from depth_anything_3.api import DepthAnything3
|
||||
from depth_anything_3.utils.export import export
|
||||
|
||||
output_directory = Path(args.output_dir)
|
||||
output_directory.mkdir(parents=True, exist_ok=False)
|
||||
write_status(args.status, 0.03, "Preparing input")
|
||||
write_status(args.status, 0.08, "Preparing input")
|
||||
images, temporary_directory = prepare_input(
|
||||
args.input,
|
||||
args.video_frames,
|
||||
@@ -173,12 +246,16 @@ def reconstruct(args):
|
||||
|
||||
try:
|
||||
device = select_device(args.device, torch)
|
||||
write_status(args.status, 0.10, f"Loading model on {device.type}")
|
||||
write_status(args.status, 0.15, f"Loading model on {device.type}")
|
||||
model = DepthAnything3.from_pretrained(args.model_dir)
|
||||
model = model.to(device=device)
|
||||
model.eval()
|
||||
|
||||
write_status(args.status, 0.18, f"Processing {len(images)} frame(s)")
|
||||
write_status(
|
||||
args.status,
|
||||
0.25,
|
||||
f"Estimating depth for {len(images)} frame(s)",
|
||||
)
|
||||
export_kwargs = {}
|
||||
if args.export_format == "glb":
|
||||
export_kwargs["glb"] = {
|
||||
Reference in New Issue
Block a user