Refactor and enhance SnapForm functionality
- Introduced MODEL_NUMBERS for better model indexing. - Improved enum_items function to cache results and handle download status. - Updated ActiveJob class to manage temporary input paths and added displacement_via property. - Changed IMAGE_DEPTH_PROCESS_RES_METHOD from "lower_bound_resize" to "upper_bound_resize". - Enhanced RunJob operator to include new properties and improved error messages. - Modified panel layout for better user experience and added runtime installation checks. - Updated SnapFormSettings to reflect new model properties and defaults. - Added temporary input path management in runtime. - Refined worker functions to improve error handling and messaging. - Expanded tests to cover new functionality and ensure backward compatibility. - Fixed packaging script to align with versioning conventions.
This commit is contained in:
@@ -5,36 +5,29 @@ 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
|
||||
ORIGINAL_DISPLACEMENT_STRENGTH = 1.0
|
||||
ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
|
||||
BUMP_BASE_DISTANCE = 0.001
|
||||
IMAGE_NAME_SUFFIXES = (
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".png",
|
||||
".webp",
|
||||
".tiff",
|
||||
".tif",
|
||||
".exr",
|
||||
".hdr",
|
||||
".bmp",
|
||||
".tga",
|
||||
)
|
||||
|
||||
|
||||
def image_depth_model_items(owner, context):
|
||||
from .models import model_enum_items
|
||||
|
||||
return model_enum_items(owner, context)
|
||||
|
||||
|
||||
def is_image_empty(candidate):
|
||||
@@ -58,6 +51,60 @@ def image_path(image):
|
||||
return Path(bpy.path.abspath(filepath)).expanduser()
|
||||
|
||||
|
||||
def image_is_packed(image):
|
||||
if getattr(image, "packed_file", None) is not None:
|
||||
return True
|
||||
return bool(getattr(image, "packed_files", ()))
|
||||
|
||||
|
||||
def image_base_name(image):
|
||||
name = image.name
|
||||
lower_name = name.lower()
|
||||
for suffix in IMAGE_NAME_SUFFIXES:
|
||||
suffix_index = lower_name.rfind(suffix)
|
||||
if suffix_index <= 0:
|
||||
continue
|
||||
trailing = name[suffix_index + len(suffix) :]
|
||||
if trailing and not (
|
||||
trailing.startswith(".") and trailing[1:].isdigit()
|
||||
):
|
||||
continue
|
||||
return f"{name[:suffix_index]}{trailing}"
|
||||
return name
|
||||
|
||||
|
||||
def depth_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.depth"
|
||||
|
||||
|
||||
def scaled_process_resolution(image, scale):
|
||||
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)
|
||||
|
||||
|
||||
def save_packed_image(image, scene):
|
||||
from .runtime import temporary_input_path
|
||||
|
||||
output_path = temporary_input_path()
|
||||
image_settings = scene.render.image_settings
|
||||
original_file_format = image_settings.file_format
|
||||
try:
|
||||
image_settings.file_format = "PNG"
|
||||
image.save_render(str(output_path), scene=scene)
|
||||
except (OSError, RuntimeError) as error:
|
||||
output_path.unlink(missing_ok=True)
|
||||
raise RuntimeError("Unable to export the packed image") from error
|
||||
finally:
|
||||
image_settings.file_format = original_file_format
|
||||
if not output_path.is_file():
|
||||
raise RuntimeError("Blender did not export the packed image")
|
||||
return output_path
|
||||
|
||||
|
||||
def plane_bounds(source_object):
|
||||
image_width, image_height = source_object.data.size
|
||||
if image_width <= 0 or image_height <= 0:
|
||||
@@ -352,7 +399,7 @@ def create_plane_mesh(source_object, bounds=None, subdivision_level=5):
|
||||
)
|
||||
)
|
||||
|
||||
mesh = bpy.data.meshes.new(f"{source_object.name}.DA3Depth")
|
||||
mesh = bpy.data.meshes.new(source_object.name)
|
||||
mesh.from_pydata(vertices, (), faces)
|
||||
mesh.update()
|
||||
for polygon in mesh.polygons:
|
||||
@@ -369,6 +416,9 @@ def create_material(
|
||||
name,
|
||||
color_image,
|
||||
depth_scale,
|
||||
displacement_image=None,
|
||||
displacement_midlevel=0.0,
|
||||
displacement_scale=1.0,
|
||||
):
|
||||
material = bpy.data.materials.new(name)
|
||||
material.use_nodes = True
|
||||
@@ -387,8 +437,6 @@ def create_material(
|
||||
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)
|
||||
@@ -415,15 +463,118 @@ def create_material(
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
depth_texture = nodes.new("ShaderNodeTexImage")
|
||||
displacement = nodes.new("ShaderNodeDisplacement")
|
||||
depth_texture.image = displacement_image
|
||||
depth_texture.interpolation = "Linear"
|
||||
depth_texture.extension = "EXTEND"
|
||||
displacement.inputs["Midlevel"].default_value = (
|
||||
displacement_midlevel
|
||||
)
|
||||
displacement.inputs["Scale"].default_value = displacement_scale
|
||||
depth_texture.location = (-300.0, -360.0)
|
||||
displacement.location = (80.0, -260.0)
|
||||
links.new(
|
||||
depth_texture.outputs["Color"],
|
||||
displacement.inputs["Height"],
|
||||
)
|
||||
links.new(
|
||||
displacement.outputs["Displacement"],
|
||||
output.inputs["Displacement"],
|
||||
)
|
||||
set_displacement_only(material)
|
||||
return material
|
||||
|
||||
|
||||
def set_displacement_only(material):
|
||||
for owner, property_name in (
|
||||
(material, "surface_displacement_method"),
|
||||
(material, "displacement_method"),
|
||||
(getattr(material, "cycles", None), "displacement_method"),
|
||||
):
|
||||
if owner is not None and hasattr(owner, property_name):
|
||||
setattr(owner, property_name, "DISPLACEMENT")
|
||||
return
|
||||
|
||||
|
||||
def finish_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
plane_object,
|
||||
delete_source,
|
||||
):
|
||||
source_name = source_object.name
|
||||
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
|
||||
|
||||
|
||||
def build_image_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
subdivision_level,
|
||||
delete_source,
|
||||
displacement_via="MODIFIER",
|
||||
):
|
||||
source_name = source_object.name
|
||||
mesh = create_plane_mesh(
|
||||
source_object,
|
||||
subdivision_level=subdivision_level,
|
||||
)
|
||||
plane_object = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(plane_object)
|
||||
plane_object.matrix_world = source_object.matrix_world.copy()
|
||||
|
||||
subdivision = plane_object.modifiers.new("Subdivision", "SUBSURF")
|
||||
subdivision.subdivision_type = "SIMPLE"
|
||||
subdivision.levels = 1
|
||||
subdivision.render_levels = 2
|
||||
|
||||
if displacement_via == "MODIFIER":
|
||||
texture = bpy.data.textures.new(
|
||||
depth_data_name(source_object),
|
||||
type="IMAGE",
|
||||
)
|
||||
texture.image = source_object.data
|
||||
displace = plane_object.modifiers.new("Displace", "DISPLACE")
|
||||
displace.texture = texture
|
||||
displace.texture_coords = "UV"
|
||||
displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL
|
||||
|
||||
depth_scale = abs(float(plane_object.scale.z))
|
||||
material = create_material(
|
||||
source_name,
|
||||
source_object.data,
|
||||
depth_scale,
|
||||
displacement_image=(
|
||||
source_object.data if displacement_via == "MATERIAL" else None
|
||||
),
|
||||
displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL,
|
||||
displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
return finish_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
plane_object,
|
||||
delete_source,
|
||||
)
|
||||
|
||||
|
||||
def build_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
npz_path,
|
||||
subdivision_level,
|
||||
delete_source,
|
||||
displacement_via="MODIFIER",
|
||||
):
|
||||
source_name = source_object.name
|
||||
depth = extract_depth_array(npz_path)
|
||||
@@ -435,7 +586,7 @@ def build_depth_plane(
|
||||
intrinsics,
|
||||
DISPLACEMENT_STRENGTH,
|
||||
)
|
||||
depth_name = f"{source_name}.depth"
|
||||
depth_name = depth_data_name(source_object)
|
||||
depth_image = create_depth_image(
|
||||
depth_name,
|
||||
depth_values,
|
||||
@@ -457,23 +608,29 @@ def build_depth_plane(
|
||||
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
|
||||
if displacement_via == "MODIFIER":
|
||||
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_name,
|
||||
source_object.data,
|
||||
depth_scale,
|
||||
displacement_image=(
|
||||
depth_image if displacement_via == "MATERIAL" else None
|
||||
),
|
||||
displacement_midlevel=DISPLACEMENT_MIDLEVEL,
|
||||
displacement_scale=DISPLACEMENT_STRENGTH,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
plane_object["snapform_reference_depth"] = reference_depth
|
||||
@@ -481,14 +638,12 @@ def build_depth_plane(
|
||||
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
|
||||
return finish_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
plane_object,
|
||||
delete_source,
|
||||
)
|
||||
|
||||
|
||||
class BuildDepthPlane(bpy.types.Operator):
|
||||
@@ -512,6 +667,14 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
default=True,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_displacement_via: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("MODIFIER", "Modifier", "Use a Displace modifier"),
|
||||
("MATERIAL", "Material", "Use material displacement"),
|
||||
),
|
||||
default="MODIFIER",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
source_object = bpy.data.objects.get(self.snapform_source_object_name)
|
||||
@@ -526,7 +689,7 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
if not npz_path.is_file():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"DA3 Mini NPZ does not exist: {npz_path}",
|
||||
f"Depth result does not exist: {npz_path}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -537,6 +700,7 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
npz_path,
|
||||
self.snapform_subdivision_level,
|
||||
self.snapform_delete_source,
|
||||
self.snapform_displacement_via,
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
@@ -547,27 +711,49 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
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_description = "Convert an image to a displaced depth plane"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
snapform_depth_source: bpy.props.EnumProperty(
|
||||
name="Depth from",
|
||||
description="Source used to displace the plane",
|
||||
items=(
|
||||
(
|
||||
"ESTIMATE",
|
||||
"AI Generated",
|
||||
"Generate depth with an AI model",
|
||||
),
|
||||
("ORIGINAL", "Original", "Use the original image as depth"),
|
||||
),
|
||||
default="ORIGINAL",
|
||||
)
|
||||
snapform_displacement_via: bpy.props.EnumProperty(
|
||||
name="Displace via",
|
||||
description="How the depth displacement is presented",
|
||||
items=(
|
||||
("MODIFIER", "Modifier", "Use a Displace modifier"),
|
||||
("MATERIAL", "Material", "Use material displacement"),
|
||||
),
|
||||
default="MODIFIER",
|
||||
)
|
||||
snapform_model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
description="DA3 model used to estimate image depth",
|
||||
items=IMAGE_DEPTH_MODEL_ITEMS,
|
||||
default="BASE",
|
||||
description="Model used to estimate image depth",
|
||||
items=image_depth_model_items,
|
||||
)
|
||||
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_process_res_factor: bpy.props.FloatProperty(
|
||||
name="Resolution Scale",
|
||||
description="Scale applied to the source image long-edge resolution",
|
||||
default=0.5,
|
||||
min=0.1,
|
||||
max=1.0,
|
||||
step=10,
|
||||
precision=2,
|
||||
)
|
||||
snapform_delete_original: bpy.props.BoolProperty(
|
||||
name="Delete Original",
|
||||
@@ -589,13 +775,27 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
return is_image_empty(context.object) and active_job() is None
|
||||
|
||||
def invoke(self, context, _event):
|
||||
if not self.properties.is_property_set("snapform_model"):
|
||||
self.snapform_model = "BASE"
|
||||
return context.window_manager.invoke_props_dialog(self, width=340)
|
||||
|
||||
def draw(self, _context):
|
||||
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_depth_source")
|
||||
if self.snapform_depth_source == "ESTIMATE":
|
||||
estimate_box = layout.box()
|
||||
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)",
|
||||
)
|
||||
layout.prop(self, "snapform_displacement_via")
|
||||
layout.prop(self, "snapform_subdivision_level")
|
||||
layout.prop(self, "snapform_delete_original")
|
||||
|
||||
@@ -603,7 +803,47 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
from .operators import IMAGE_DEPTH_JOB
|
||||
|
||||
source_object = context.object
|
||||
source_path = image_path(source_object.data)
|
||||
if self.snapform_depth_source == "ORIGINAL":
|
||||
try:
|
||||
build_image_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
self.snapform_subdivision_level,
|
||||
self.snapform_delete_original,
|
||||
self.snapform_displacement_via,
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
undo_result = bpy.ops.ed.undo_push(
|
||||
message="Convert to Depth Plane",
|
||||
)
|
||||
if "FINISHED" not in undo_result:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
"Unable to create the depth plane Undo step",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
image = source_object.data
|
||||
try:
|
||||
process_res = scaled_process_resolution(
|
||||
image,
|
||||
self.snapform_process_res_factor,
|
||||
)
|
||||
except ValueError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
source_path = image_path(image)
|
||||
delete_input = False
|
||||
if image_is_packed(image):
|
||||
try:
|
||||
source_path = save_packed_image(image, context.scene)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
delete_input = True
|
||||
if not source_path.is_file():
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
@@ -617,11 +857,15 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
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_process_res=process_res,
|
||||
snapform_subdivision_level=self.snapform_subdivision_level,
|
||||
snapform_delete_source=self.snapform_delete_original,
|
||||
snapform_delete_input=delete_input,
|
||||
snapform_displacement_via=self.snapform_displacement_via,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
if delete_input:
|
||||
source_path.unlink(missing_ok=True)
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -53,18 +53,62 @@ MODELS = {
|
||||
supports_gs=True,
|
||||
),
|
||||
}
|
||||
MODEL_NUMBERS = {
|
||||
model_key: number for number, model_key in enumerate(MODELS)
|
||||
}
|
||||
|
||||
|
||||
def enum_items(supports_gs=None):
|
||||
return [
|
||||
(
|
||||
model.key,
|
||||
model.label,
|
||||
f"{model.description}; license: {model.license_name}",
|
||||
)
|
||||
_ENUM_ITEMS_CACHE = {}
|
||||
|
||||
|
||||
def enum_items(supports_gs=None, show_download_status=False):
|
||||
model_ready = None
|
||||
if show_download_status:
|
||||
from .runtime import model_ready
|
||||
|
||||
models = [
|
||||
model
|
||||
for model in MODELS.values()
|
||||
if supports_gs is None or model.supports_gs == supports_gs
|
||||
]
|
||||
download_status = (
|
||||
tuple(model_ready(model.key) for model in models)
|
||||
if model_ready is not None
|
||||
else ()
|
||||
)
|
||||
cache_key = (
|
||||
supports_gs,
|
||||
show_download_status,
|
||||
download_status,
|
||||
)
|
||||
cached_items = _ENUM_ITEMS_CACHE.get(cache_key)
|
||||
if cached_items is not None:
|
||||
return cached_items
|
||||
|
||||
items = []
|
||||
for index, model in enumerate(models):
|
||||
label = model.label
|
||||
if download_status and not download_status[index]:
|
||||
label = f"{label} (Missing)"
|
||||
items.append(
|
||||
(
|
||||
model.key,
|
||||
label,
|
||||
f"{model.description}; license: {model.license_name}",
|
||||
MODEL_NUMBERS[model.key],
|
||||
)
|
||||
)
|
||||
|
||||
_ENUM_ITEMS_CACHE[cache_key] = items
|
||||
return items
|
||||
|
||||
|
||||
def model_enum_items(_owner, _context):
|
||||
return enum_items(show_download_status=True)
|
||||
|
||||
|
||||
def gs_model_enum_items(_owner, _context):
|
||||
return enum_items(supports_gs=True, show_download_status=True)
|
||||
|
||||
|
||||
def selected_model_key(settings, export_format=None):
|
||||
|
||||
@@ -11,7 +11,7 @@ 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"
|
||||
IMAGE_DEPTH_PROCESS_RES_METHOD = "upper_bound_resize"
|
||||
|
||||
|
||||
def open_job_directory(result_directory):
|
||||
@@ -36,6 +36,8 @@ class ActiveJob:
|
||||
process_res: int = IMAGE_DEPTH_DEFAULT_PROCESS_RES
|
||||
subdivision_level: int = 5
|
||||
delete_source: bool = True
|
||||
displacement_via: str = "MODIFIER"
|
||||
temporary_input_path: Path | None = None
|
||||
cancelled: bool = False
|
||||
|
||||
|
||||
@@ -62,6 +64,11 @@ def close_active_job():
|
||||
if _active_job is None:
|
||||
return
|
||||
_active_job.log_file.close()
|
||||
if _active_job.temporary_input_path is not None:
|
||||
try:
|
||||
_active_job.temporary_input_path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
_active_job = None
|
||||
|
||||
|
||||
@@ -98,20 +105,32 @@ class RunJob(bpy.types.Operator):
|
||||
)
|
||||
snapform_process_res: bpy.props.IntProperty(
|
||||
default=IMAGE_DEPTH_DEFAULT_PROCESS_RES,
|
||||
min=14,
|
||||
max=2016,
|
||||
min=1,
|
||||
max=65_536,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_delete_source: bpy.props.BoolProperty(
|
||||
default=True,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_delete_input: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
default=5,
|
||||
min=0,
|
||||
max=10,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_displacement_via: bpy.props.EnumProperty(
|
||||
items=(
|
||||
("MODIFIER", "Modifier", ""),
|
||||
("MATERIAL", "Material", ""),
|
||||
),
|
||||
default="MODIFIER",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
global _active_job
|
||||
@@ -138,7 +157,7 @@ class RunJob(bpy.types.Operator):
|
||||
)
|
||||
except OSError as error:
|
||||
log_file.close()
|
||||
self.report({"ERROR"}, f"Unable to start DA3 task: {error}")
|
||||
self.report({"ERROR"}, f"Unable to start task: {error}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
_active_job = ActiveJob(
|
||||
@@ -157,6 +176,13 @@ class RunJob(bpy.types.Operator):
|
||||
process_res=self.snapform_process_res,
|
||||
subdivision_level=self.snapform_subdivision_level,
|
||||
delete_source=self.snapform_delete_source,
|
||||
displacement_via=self.snapform_displacement_via,
|
||||
temporary_input_path=(
|
||||
Path(self.snapform_input_path)
|
||||
if self.job_type == IMAGE_DEPTH_JOB
|
||||
and self.snapform_delete_input
|
||||
else None
|
||||
),
|
||||
)
|
||||
settings.status = self._starting_message(settings)
|
||||
settings.progress = 0.0
|
||||
@@ -245,9 +271,9 @@ class RunJob(bpy.types.Operator):
|
||||
"DOWNLOAD_MODEL": "Starting model download",
|
||||
}
|
||||
if self.job_type == RECONSTRUCTION_JOB:
|
||||
return f"Starting DA3 {settings.export_format} export"
|
||||
return f"Starting {settings.export_format} export"
|
||||
if self.job_type == IMAGE_DEPTH_JOB:
|
||||
return "Starting DA3 image depth conversion"
|
||||
return "Starting depth estimation"
|
||||
if self.job_type == "DOWNLOAD_MODEL":
|
||||
return f"Starting {get_model(self.model_key).label} download"
|
||||
return labels[self.job_type]
|
||||
@@ -298,7 +324,7 @@ class RunJob(bpy.types.Operator):
|
||||
settings.progress = 0.0
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"{error or 'DA3 subprocess failed'}; log: {job.log_path}",
|
||||
f"{error or 'Task subprocess failed'}; log: {job.log_path}",
|
||||
)
|
||||
close_active_job()
|
||||
self._redraw_ui(context, force=True)
|
||||
@@ -326,7 +352,7 @@ class RunJob(bpy.types.Operator):
|
||||
close_active_job()
|
||||
self._redraw_ui(context, force=True)
|
||||
return {"CANCELLED"}
|
||||
status["message"] = "DA3 depth plane is ready"
|
||||
status["message"] = "Depth plane is ready"
|
||||
|
||||
settings.status = status.get("message", "Task complete")
|
||||
settings.progress = 1.0
|
||||
@@ -346,6 +372,7 @@ class RunJob(bpy.types.Operator):
|
||||
snapform_npz_path=str(npz_path),
|
||||
snapform_subdivision_level=job.subdivision_level,
|
||||
snapform_delete_source=job.delete_source,
|
||||
snapform_displacement_via=job.displacement_via,
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Unable to build the depth plane")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
|
||||
from .models import MODELS, selected_model_key
|
||||
from .models import MODELS
|
||||
from .operators import active_job
|
||||
from .runtime import model_ready, runtime_ready
|
||||
|
||||
@@ -18,13 +18,14 @@ class RuntimeModelsPanel(bpy.types.Panel):
|
||||
|
||||
if not runtime_ready():
|
||||
layout.label(text="Runtime is not installed", icon="ERROR")
|
||||
if not running:
|
||||
operator = layout.operator(
|
||||
"snapform.run_job",
|
||||
text="Install Runtime",
|
||||
icon="IMPORT",
|
||||
)
|
||||
operator.job_type = "INSTALL_RUNTIME"
|
||||
install_row = layout.row()
|
||||
install_row.enabled = not running
|
||||
operator = install_row.operator(
|
||||
"snapform.run_job",
|
||||
text="Install Runtime",
|
||||
icon="IMPORT",
|
||||
)
|
||||
operator.job_type = "INSTALL_RUNTIME"
|
||||
|
||||
for model in MODELS.values():
|
||||
self._draw_model(layout, model, running)
|
||||
@@ -34,16 +35,16 @@ class RuntimeModelsPanel(bpy.types.Panel):
|
||||
row.label(text=model.label)
|
||||
if model_ready(model.key):
|
||||
row.label(text="Ready", icon="CHECKMARK")
|
||||
elif runtime_ready() and not running:
|
||||
operator = row.operator(
|
||||
else:
|
||||
download_row = row.row(align=True)
|
||||
download_row.enabled = runtime_ready() and not running
|
||||
operator = download_row.operator(
|
||||
"snapform.run_job",
|
||||
text="Download",
|
||||
icon="IMPORT",
|
||||
)
|
||||
operator.job_type = "DOWNLOAD_MODEL"
|
||||
operator.model_key = model.key
|
||||
else:
|
||||
row.label(text="Not downloaded", icon="ERROR")
|
||||
|
||||
|
||||
class DebugPanel(bpy.types.Panel):
|
||||
@@ -59,16 +60,10 @@ class DebugPanel(bpy.types.Panel):
|
||||
settings = context.scene.snapform_settings
|
||||
|
||||
layout.prop(settings, "export_format")
|
||||
model_key = selected_model_key(settings)
|
||||
model_property = (
|
||||
"gs_model" if settings.export_format == "gs_ply" else "model"
|
||||
)
|
||||
model_row = layout.row(align=True)
|
||||
model_row.prop(settings, model_property)
|
||||
if model_ready(model_key):
|
||||
model_row.label(text="", icon="CHECKMARK")
|
||||
else:
|
||||
model_row.label(text="", icon="ERROR")
|
||||
layout.prop(settings, model_property)
|
||||
|
||||
layout.prop(settings, "input_path")
|
||||
input_path = bpy.path.abspath(settings.input_path)
|
||||
@@ -103,10 +98,11 @@ class DebugPanel(bpy.types.Panel):
|
||||
ply_box.label(text="PLY (3DGS) Settings")
|
||||
ply_box.prop(settings, "gs_views_interval")
|
||||
|
||||
if active_job() is None:
|
||||
operator = layout.operator(
|
||||
"snapform.run_job",
|
||||
text="Export",
|
||||
icon="EXPORT",
|
||||
)
|
||||
operator.job_type = "RECONSTRUCT"
|
||||
export_row = layout.row()
|
||||
export_row.enabled = active_job() is None
|
||||
operator = export_row.operator(
|
||||
"snapform.run_job",
|
||||
text="Export",
|
||||
icon="EXPORT",
|
||||
)
|
||||
operator.job_type = "RECONSTRUCT"
|
||||
|
||||
@@ -33,6 +33,7 @@ class SnapFormPreferences(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
def draw(self, _context):
|
||||
from .operators import active_job
|
||||
from .runtime import runtime_directory, runtime_ready
|
||||
|
||||
layout = self.layout
|
||||
@@ -44,7 +45,9 @@ class SnapFormPreferences(bpy.types.AddonPreferences):
|
||||
if runtime_ready():
|
||||
layout.label(text="Runtime is ready", icon="CHECKMARK")
|
||||
else:
|
||||
operator = layout.operator(
|
||||
install_row = layout.row()
|
||||
install_row.enabled = active_job() is None
|
||||
operator = install_row.operator(
|
||||
"snapform.run_job",
|
||||
text="Install Runtime",
|
||||
icon="IMPORT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
|
||||
from .models import enum_items
|
||||
from .models import gs_model_enum_items, model_enum_items
|
||||
|
||||
|
||||
class SnapFormSettings(bpy.types.PropertyGroup):
|
||||
@@ -19,14 +19,12 @@ class SnapFormSettings(bpy.types.PropertyGroup):
|
||||
model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
description="Depth Anything 3 model",
|
||||
items=enum_items(),
|
||||
default="BASE",
|
||||
items=model_enum_items,
|
||||
)
|
||||
gs_model: bpy.props.EnumProperty(
|
||||
name="Model",
|
||||
description="Depth Anything 3 model with a Gaussian head",
|
||||
items=enum_items(supports_gs=True),
|
||||
default="GIANT_1_1",
|
||||
items=gs_model_enum_items,
|
||||
)
|
||||
device: bpy.props.EnumProperty(
|
||||
name="Device",
|
||||
@@ -60,7 +58,7 @@ class SnapFormSettings(bpy.types.PropertyGroup):
|
||||
"Make Process Res the shorter side",
|
||||
),
|
||||
),
|
||||
default="lower_bound_resize",
|
||||
default="upper_bound_resize",
|
||||
)
|
||||
conf_thresh_percentile: bpy.props.FloatProperty(
|
||||
name="Conf Thresh Percentile",
|
||||
|
||||
@@ -78,6 +78,12 @@ def jobs_directory():
|
||||
return path
|
||||
|
||||
|
||||
def temporary_input_path():
|
||||
path = jobs_directory() / "inputs"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path / f"{uuid.uuid4().hex}.png"
|
||||
|
||||
|
||||
def new_job_paths(job_type):
|
||||
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
token = uuid.uuid4().hex[:8]
|
||||
|
||||
@@ -229,7 +229,7 @@ def wait_for_exported_paths(output_directory, export_format, timeout=30.0):
|
||||
|
||||
|
||||
def reconstruct(args):
|
||||
write_status(args.status, 0.02, "Starting DA3 runtime")
|
||||
write_status(args.status, 0.02, "Starting AI runtime")
|
||||
|
||||
import torch
|
||||
from depth_anything_3.api import DepthAnything3
|
||||
@@ -291,7 +291,7 @@ def reconstruct(args):
|
||||
)
|
||||
if not result_paths:
|
||||
raise RuntimeError(
|
||||
f"DA3 did not export any {args.export_format} files"
|
||||
f"No {args.export_format} files were exported"
|
||||
)
|
||||
write_status(
|
||||
args.status,
|
||||
@@ -373,7 +373,7 @@ def main():
|
||||
else:
|
||||
reconstruct(args)
|
||||
except Exception as error:
|
||||
write_status(args.status, 1.0, "DA3 task failed", str(error))
|
||||
write_status(args.status, 1.0, "Task failed", str(error))
|
||||
raise
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user