feat(cutout_tool): Implement cutout region to shape functionality
- Added interaction helpers for cutout region validation and shape menu interaction. - Created main operator for converting image regions into cutout shapes, including AI artifact generation. - Introduced object creation logic for cutout shapes, handling depth and normal textures. - Defined cutout shape identifiers and node groups for various shape types. - Implemented depth texture writing and prediction artifact generation for MoGe-2. - Developed server jobs for generating cutout artifacts, depth planes, and refining image regions. - Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from ..common.mesh_result import load_image_geometry
|
||||
from ..common.image_data import (
|
||||
color_data_name,
|
||||
depth_data_name,
|
||||
image_pixels,
|
||||
load_color_result_image,
|
||||
load_normal_result_image,
|
||||
image_empty_bounds,
|
||||
require_image_empty,
|
||||
)
|
||||
from ..common.mesh_result import create_image_material
|
||||
from ..common.mesh_result import modifier_input_identifier, set_modifier_input
|
||||
from ..common.mesh_result import finalize_object_result
|
||||
from .mesh_plane import create_mesh_plane_mesh, create_mesh_plane_object
|
||||
from .normal_map import bake_tangent_normal_image
|
||||
from .nodes import depth_plane_node_group
|
||||
|
||||
|
||||
def copy_color_image(source_object):
|
||||
source = source_object.data
|
||||
image = source.copy()
|
||||
if getattr(source, "source", None) == "GENERATED" or getattr(
|
||||
source, "is_dirty", False
|
||||
):
|
||||
pixels = image_pixels(source)
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
image.name = color_data_name(source_object)
|
||||
return image
|
||||
|
||||
|
||||
def build_mesh_plane(context, source_object, subdivisions, material):
|
||||
source_name = source_object.name
|
||||
plane = create_mesh_plane_object(
|
||||
context,
|
||||
source_name,
|
||||
image_empty_bounds(source_object),
|
||||
material,
|
||||
matrix_world=source_object.matrix_world,
|
||||
subdivisions=subdivisions,
|
||||
)
|
||||
for polygon in plane.data.polygons:
|
||||
polygon.use_smooth = True
|
||||
plane["anyimage_mesh_shape"] = "PLANE"
|
||||
return finalize_object_result(context, source_object, plane)
|
||||
|
||||
|
||||
def anchored_point_field(source_object, field):
|
||||
import numpy as np
|
||||
|
||||
points = field.points
|
||||
intrinsics = field.intrinsics
|
||||
valid = field.valid
|
||||
width, height = field.image_size
|
||||
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
|
||||
plane_width = x_max - x_min
|
||||
plane_height = y_max - y_min
|
||||
fx, fy = float(intrinsics[0, 0]), float(intrinsics[1, 1])
|
||||
cx, cy = float(intrinsics[0, 2]), float(intrinsics[1, 2])
|
||||
if fx <= 0.0 or fy <= 0.0:
|
||||
raise ValueError("Camera intrinsics contain an invalid focal length")
|
||||
scale_x = plane_width * fx / (max(width - 1, 1) * field.reference_depth)
|
||||
scale_y = plane_height * fy / (max(height - 1, 1) * field.reference_depth)
|
||||
scale_z = float(np.sqrt(scale_x * scale_y))
|
||||
camera_x = x_min + plane_width * cx / max(width - 1, 1)
|
||||
camera_y = y_max - plane_height * cy / max(height - 1, 1)
|
||||
anchored = np.empty_like(points, dtype=np.float32)
|
||||
anchored[..., 0] = camera_x + points[..., 0] * scale_x
|
||||
anchored[..., 1] = camera_y - points[..., 1] * scale_y
|
||||
anchored[..., 2] = (field.reference_depth - points[..., 2]) * scale_z
|
||||
return anchored, valid, points[..., 2]
|
||||
|
||||
|
||||
def create_depth_image(source_object, anchored_points):
|
||||
import numpy as np
|
||||
|
||||
values = np.asarray(anchored_points, dtype=np.float32)[..., 2]
|
||||
height, width = values.shape
|
||||
pixels = np.empty((height, width, 4), dtype=np.float32)
|
||||
pixels[:, :, :3] = values[:, :, None]
|
||||
pixels[:, :, 3] = 1.0
|
||||
image = bpy.data.images.new(
|
||||
depth_data_name(source_object),
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=True,
|
||||
float_buffer=True,
|
||||
)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.pixels.foreach_set(np.flipud(pixels).ravel())
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def build_depth_plane(context, source_object, subdivisions, material, field):
|
||||
source_name = source_object.name
|
||||
mesh = create_mesh_plane_mesh(source_name, image_empty_bounds(source_object))
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
plane.matrix_world = source_object.matrix_world.copy()
|
||||
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
|
||||
|
||||
node_group = depth_plane_node_group()
|
||||
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
|
||||
modifier.node_group = node_group
|
||||
modifier.show_group_selector = False
|
||||
depth_image = create_depth_image(
|
||||
source_object, anchored_point_field(source_object, field)[0]
|
||||
)
|
||||
for name, value in (
|
||||
("Subdivide", max(0, min(10, int(subdivisions)))),
|
||||
("Material", material),
|
||||
("Thickness", 0.1),
|
||||
("Depth Amount", 1.0),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(node_group, name),
|
||||
value,
|
||||
)
|
||||
return finalize_object_result(context, source_object, plane)
|
||||
|
||||
|
||||
def build_depth_plane_response(context, result, operator):
|
||||
source_object = require_image_empty(operator.source_object_name)
|
||||
color_image = load_color_result_image(source_object, result.directory)
|
||||
color_image.name = color_data_name(source_object)
|
||||
normal_path = result.directory / "normal.png"
|
||||
normal_image = (
|
||||
load_normal_result_image(normal_path, source_object)
|
||||
if normal_path.is_file()
|
||||
else None
|
||||
)
|
||||
material = create_image_material(
|
||||
source_object,
|
||||
color_image,
|
||||
0.0,
|
||||
normal_image=normal_image,
|
||||
scene=context.scene,
|
||||
)
|
||||
plane = build_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
operator.mesh_detail,
|
||||
material,
|
||||
load_image_geometry(result.file("image_geometry")),
|
||||
)
|
||||
if normal_image is not None:
|
||||
bake_tangent_normal_image(
|
||||
context,
|
||||
source_object,
|
||||
plane,
|
||||
normal_image,
|
||||
)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Convert to Depth Plane"):
|
||||
raise RuntimeError("Unable to create the Convert to Depth Plane Undo step")
|
||||
@@ -8,6 +8,14 @@ from ...server.media.resolution import (
|
||||
MAX_MAX_AI_INPUT_SIZE,
|
||||
MIN_MAX_AI_INPUT_SIZE,
|
||||
)
|
||||
from ..common.ai_job import (
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
production_device,
|
||||
require_environment,
|
||||
require_input_path,
|
||||
require_model,
|
||||
)
|
||||
from ..common.image_data import (
|
||||
cleanup_image_input,
|
||||
image_user_settings,
|
||||
@@ -17,18 +25,11 @@ from ..common.image_data import (
|
||||
video_total_frames,
|
||||
)
|
||||
from ..common.mesh_result import create_image_material
|
||||
from ..common.ai_job import (
|
||||
da3_parameters,
|
||||
require_input_path,
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
production_device,
|
||||
require_environment,
|
||||
require_model,
|
||||
from .object import (
|
||||
create_color_image,
|
||||
create_depth_plane_from_result,
|
||||
create_plane_object,
|
||||
)
|
||||
from .build import build_depth_plane_response, build_mesh_plane, copy_color_image
|
||||
from .resolution import scaled_process_resolution
|
||||
|
||||
|
||||
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
|
||||
@@ -36,23 +37,21 @@ _HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
def depth_plane_settings(context):
|
||||
preferences = addon_preferences(context)
|
||||
return (
|
||||
getattr(preferences, "mesh_depth_model", DEFAULT_MOGE2_MODEL_KEY),
|
||||
float(getattr(preferences, "mesh_process_res_factor", 100.0)),
|
||||
int(getattr(preferences, "mesh_resolution_level", 5)),
|
||||
getattr(preferences, "geometry_model", DEFAULT_MOGE2_MODEL_KEY),
|
||||
int(getattr(preferences, "geometry_resolution_level", 5)),
|
||||
)
|
||||
|
||||
|
||||
class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.generate_depth_plane"
|
||||
bl_label = "Generate Depth Plane"
|
||||
job_type = "generate-depth-plane"
|
||||
job_type = "generate-depth-plane-geometry"
|
||||
starting_message = "Preparing Depth Plane"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
|
||||
mesh_detail: bpy.props.IntProperty(default=0, min=0, max=10, options=_HIDDEN)
|
||||
model: bpy.props.StringProperty(default=DEFAULT_MOGE2_MODEL_KEY, options=_HIDDEN)
|
||||
process_res: bpy.props.IntProperty(default=1008, min=1, options=_HIDDEN)
|
||||
resolution_level: bpy.props.IntProperty(default=5, min=0, max=9, options=_HIDDEN)
|
||||
max_input_size: bpy.props.IntProperty(
|
||||
default=DEFAULT_MAX_AI_INPUT_SIZE,
|
||||
@@ -65,38 +64,27 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
require_model(self.model)
|
||||
input_path = require_input_path(self.input_path)
|
||||
parameters = {
|
||||
"input": str(require_input_path(self.input_path)),
|
||||
"input": str(input_path),
|
||||
"device": production_device().lower(),
|
||||
"video_frames": 1,
|
||||
"start_frame": source_start_frame(self.source_object_name),
|
||||
"max_input_size": int(self.max_input_size),
|
||||
}
|
||||
settings = context.scene.anyimage_settings
|
||||
if self.model.startswith("MOGE2_"):
|
||||
parameters.update(
|
||||
moge2_parameters(
|
||||
settings,
|
||||
require_input_path(self.input_path),
|
||||
self.model,
|
||||
self.resolution_level,
|
||||
source_start_frame(self.source_object_name),
|
||||
)
|
||||
)
|
||||
else:
|
||||
parameters.update(
|
||||
da3_parameters(
|
||||
settings,
|
||||
require_input_path(self.input_path),
|
||||
self.model,
|
||||
self.process_res,
|
||||
source_start_frame(self.source_object_name),
|
||||
)
|
||||
parameters.update(
|
||||
moge2_parameters(
|
||||
context.scene.anyimage_settings,
|
||||
input_path,
|
||||
self.model,
|
||||
self.resolution_level,
|
||||
source_start_frame(self.source_object_name),
|
||||
)
|
||||
)
|
||||
return parameters
|
||||
|
||||
def response(self, context, result):
|
||||
build_depth_plane_response(context, result, self)
|
||||
create_depth_plane_from_result(context, result, self)
|
||||
return "Depth Plane is ready"
|
||||
|
||||
def cleanup(self):
|
||||
@@ -116,12 +104,7 @@ def start_depth_plane_job(operator, context):
|
||||
source_object.data,
|
||||
context.scene,
|
||||
)
|
||||
model, process_factor, resolution_level = depth_plane_settings(context)
|
||||
process_res = (
|
||||
scaled_process_resolution(source_object.data, process_factor)
|
||||
if not model.startswith("MOGE2_")
|
||||
else 1
|
||||
)
|
||||
model, resolution_level = depth_plane_settings(context)
|
||||
except (RuntimeError, ValueError) as error:
|
||||
operator.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
@@ -130,14 +113,12 @@ def start_depth_plane_job(operator, context):
|
||||
source_object_name=source_object.name,
|
||||
mesh_detail=operator.mesh_detail,
|
||||
model=model,
|
||||
process_res=process_res,
|
||||
resolution_level=resolution_level,
|
||||
max_input_size=configured_max_ai_input_size(context),
|
||||
delete_input=delete_input,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
if delete_input:
|
||||
cleanup_image_input(source_path, True)
|
||||
if "CANCELLED" in result and delete_input:
|
||||
cleanup_image_input(source_path, True)
|
||||
return {"CANCELLED"} if "CANCELLED" in result else {"FINISHED"}
|
||||
|
||||
|
||||
@@ -155,7 +136,7 @@ class ConvertToPlane(bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
source_object = context.object
|
||||
color_image = copy_color_image(source_object)
|
||||
color_image = create_color_image(source_object)
|
||||
frame_start, frame_offset, frame_duration, use_cyclic = image_user_settings(
|
||||
source_object
|
||||
)
|
||||
@@ -169,7 +150,7 @@ class ConvertToPlane(bpy.types.Operator):
|
||||
use_cyclic=use_cyclic,
|
||||
scene=context.scene,
|
||||
)
|
||||
build_mesh_plane(context, source_object, self.mesh_detail, material)
|
||||
create_plane_object(context, source_object, self.mesh_detail, material)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,14 @@ def depth_plane_node_group():
|
||||
depth_amount = _interface_socket(group, "Depth Amount", "INPUT", "NodeSocketFloat")
|
||||
depth_amount.default_value = 1.0
|
||||
depth_amount.min_value = 0.0
|
||||
reference_depth = _interface_socket(
|
||||
group, "Reference Depth", "INPUT", "NodeSocketFloat"
|
||||
)
|
||||
reference_depth.hide_in_modifier = True
|
||||
uniform_scale = _interface_socket(
|
||||
group, "Uniform Scale", "INPUT", "NodeSocketFloat"
|
||||
)
|
||||
uniform_scale.hide_in_modifier = True
|
||||
depth_image = _interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage")
|
||||
depth_image.hide_in_modifier = True
|
||||
material = _interface_socket(group, "Material", "INPUT", "NodeSocketMaterial")
|
||||
@@ -71,10 +79,14 @@ def depth_plane_node_group():
|
||||
(-1250.0, 850.0),
|
||||
{"Thickness", "Depth Amount"},
|
||||
)
|
||||
image_input = _group_input(nodes, (-1050.0, 50.0), {"Depth Image"})
|
||||
image_input = _group_input(
|
||||
nodes,
|
||||
(-1050.0, 50.0),
|
||||
{"Depth Image", "Reference Depth", "Uniform Scale"},
|
||||
)
|
||||
cleanup_input = _group_input(nodes, (650.0, -200.0), {"Thickness"})
|
||||
group_output = nodes.new("NodeGroupOutput")
|
||||
group_output.location = (1600.0, 450.0)
|
||||
group_output.location = (1850.0, 450.0)
|
||||
mesh_plane = nodes.new("GeometryNodeGroup")
|
||||
mesh_plane.node_tree = mesh_plane_node_group()
|
||||
mesh_plane.inputs["Thickness"].default_value = 0.0
|
||||
@@ -103,41 +115,51 @@ def depth_plane_node_group():
|
||||
separate = nodes.new("ShaderNodeSeparateXYZ")
|
||||
separate.location = (-600.0, 50.0)
|
||||
links.new(texture.outputs["Color"], separate.inputs["Vector"])
|
||||
relative_depth = nodes.new("ShaderNodeMath")
|
||||
relative_depth.operation = "SUBTRACT"
|
||||
relative_depth.location = (-600.0, -150.0)
|
||||
links.new(image_input.outputs["Reference Depth"], relative_depth.inputs[0])
|
||||
links.new(separate.outputs["X"], relative_depth.inputs[1])
|
||||
local_depth = nodes.new("ShaderNodeMath")
|
||||
local_depth.operation = "MULTIPLY"
|
||||
local_depth.location = (-350.0, -150.0)
|
||||
links.new(relative_depth.outputs[0], local_depth.inputs[0])
|
||||
links.new(image_input.outputs["Uniform Scale"], local_depth.inputs[1])
|
||||
raw_depth = nodes.new("ShaderNodeMath")
|
||||
raw_depth.operation = "MULTIPLY"
|
||||
raw_depth.location = (-350.0, 150.0)
|
||||
links.new(separate.outputs["X"], raw_depth.inputs[0])
|
||||
raw_depth.location = (-100.0, 150.0)
|
||||
links.new(local_depth.outputs[0], raw_depth.inputs[0])
|
||||
links.new(shape_input.outputs["Depth Amount"], raw_depth.inputs[1])
|
||||
inward_limit = nodes.new("ShaderNodeMath")
|
||||
inward_limit.operation = "MULTIPLY"
|
||||
inward_limit.inputs[1].default_value = -0.98
|
||||
inward_limit.location = (-350.0, -50.0)
|
||||
inward_limit.location = (-100.0, -50.0)
|
||||
links.new(shape_input.outputs["Thickness"], inward_limit.inputs[0])
|
||||
clamp = nodes.new("ShaderNodeMath")
|
||||
clamp.operation = "MAXIMUM"
|
||||
clamp.location = (-100.0, 50.0)
|
||||
clamp.location = (150.0, 50.0)
|
||||
links.new(raw_depth.outputs[0], clamp.inputs[0])
|
||||
links.new(inward_limit.outputs[0], clamp.inputs[1])
|
||||
has_thickness = nodes.new("ShaderNodeMath")
|
||||
has_thickness.operation = "GREATER_THAN"
|
||||
has_thickness.inputs[1].default_value = 1e-6
|
||||
has_thickness.location = (-350.0, -250.0)
|
||||
has_thickness.location = (-100.0, -350.0)
|
||||
links.new(shape_input.outputs["Thickness"], has_thickness.inputs[0])
|
||||
choose_depth = nodes.new("GeometryNodeSwitch")
|
||||
choose_depth.input_type = "FLOAT"
|
||||
choose_depth.location = (150.0, 50.0)
|
||||
choose_depth.location = (400.0, 50.0)
|
||||
links.new(has_thickness.outputs[0], choose_depth.inputs["Switch"])
|
||||
links.new(raw_depth.outputs[0], choose_depth.inputs["False"])
|
||||
links.new(clamp.outputs[0], choose_depth.inputs["True"])
|
||||
combine = nodes.new("ShaderNodeCombineXYZ")
|
||||
combine.location = (350.0, 50.0)
|
||||
combine.location = (600.0, 50.0)
|
||||
links.new(choose_depth.outputs["Output"], combine.inputs["Z"])
|
||||
front = nodes.new("FunctionNodeBooleanMath")
|
||||
front.operation = "NOT"
|
||||
front.location = (150.0, 300.0)
|
||||
front.location = (400.0, 300.0)
|
||||
links.new(extrude.outputs["Top"], front.inputs[0])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
set_position.location = (550.0, 450.0)
|
||||
set_position.location = (800.0, 450.0)
|
||||
links.new(geometry, set_position.inputs["Geometry"])
|
||||
links.new(front.outputs[0], set_position.inputs["Selection"])
|
||||
links.new(combine.outputs["Vector"], set_position.inputs["Offset"])
|
||||
@@ -146,27 +168,27 @@ def depth_plane_node_group():
|
||||
zero_thickness = nodes.new("ShaderNodeMath")
|
||||
zero_thickness.operation = "LESS_THAN"
|
||||
zero_thickness.inputs[1].default_value = 1e-6
|
||||
zero_thickness.location = (750.0, -150.0)
|
||||
zero_thickness.location = (1000.0, -150.0)
|
||||
links.new(cleanup_input.outputs["Thickness"], zero_thickness.inputs[0])
|
||||
back_or_side = nodes.new("FunctionNodeBooleanMath")
|
||||
back_or_side.operation = "OR"
|
||||
back_or_side.location = (750.0, 150.0)
|
||||
back_or_side.location = (1000.0, 150.0)
|
||||
links.new(extrude.outputs["Top"], back_or_side.inputs[0])
|
||||
links.new(extrude.outputs["Side"], back_or_side.inputs[1])
|
||||
delete_selection = nodes.new("FunctionNodeBooleanMath")
|
||||
delete_selection.operation = "AND"
|
||||
delete_selection.location = (950.0, 50.0)
|
||||
delete_selection.location = (1200.0, 50.0)
|
||||
links.new(zero_thickness.outputs[0], delete_selection.inputs[0])
|
||||
links.new(back_or_side.outputs[0], delete_selection.inputs[1])
|
||||
delete_geometry = nodes.new("GeometryNodeDeleteGeometry")
|
||||
delete_geometry.domain = "FACE"
|
||||
delete_geometry.location = (1100.0, 450.0)
|
||||
delete_geometry.location = (1350.0, 450.0)
|
||||
links.new(geometry, delete_geometry.inputs["Geometry"])
|
||||
links.new(delete_selection.outputs[0], delete_geometry.inputs["Selection"])
|
||||
shade_smooth = nodes.new("GeometryNodeSetShadeSmooth")
|
||||
shade_smooth.domain = "FACE"
|
||||
shade_smooth.inputs["Shade Smooth"].default_value = True
|
||||
shade_smooth.location = (1350.0, 450.0)
|
||||
shade_smooth.location = (1600.0, 450.0)
|
||||
links.new(delete_geometry.outputs["Geometry"], shade_smooth.inputs["Geometry"])
|
||||
links.new(shade_smooth.outputs["Geometry"], group_output.inputs["Geometry"])
|
||||
group.use_fake_user = True
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
from ..common.image_data import image_pixels
|
||||
|
||||
|
||||
def _normalized(values, fallback):
|
||||
import numpy as np
|
||||
|
||||
values = np.asarray(values, dtype=np.float64)
|
||||
lengths = np.linalg.norm(values, axis=-1)
|
||||
result = np.empty_like(values)
|
||||
usable = lengths > 1e-10
|
||||
result[usable] = values[usable] / lengths[usable, None]
|
||||
result[~usable] = fallback
|
||||
return result
|
||||
|
||||
|
||||
def rasterize_tangent_normal(
|
||||
object_normal,
|
||||
triangle_uv,
|
||||
triangle_normal,
|
||||
triangle_tangent,
|
||||
triangle_sign,
|
||||
return_coverage=False,
|
||||
):
|
||||
"""Convert object-space pixels through a smooth per-corner UV tangent frame."""
|
||||
import numpy as np
|
||||
|
||||
object_normal = _normalized(object_normal, (0.0, 0.0, 1.0))
|
||||
height, width = object_normal.shape[:2]
|
||||
output = np.zeros((height, width, 3), dtype=np.float32)
|
||||
output[..., 2] = 1.0
|
||||
coverage = np.zeros((height, width), dtype=bool)
|
||||
scale = np.asarray((max(width - 1, 1), max(height - 1, 1)))
|
||||
|
||||
for uv, corner_normal, corner_tangent, corner_sign in zip(
|
||||
triangle_uv,
|
||||
triangle_normal,
|
||||
triangle_tangent,
|
||||
triangle_sign,
|
||||
strict=True,
|
||||
):
|
||||
pixel = np.asarray(uv, dtype=np.float64) * scale
|
||||
x_min = max(int(np.ceil(np.min(pixel[:, 0]))), 0)
|
||||
x_max = min(int(np.floor(np.max(pixel[:, 0]))), width - 1)
|
||||
y_min = max(int(np.ceil(np.min(pixel[:, 1]))), 0)
|
||||
y_max = min(int(np.floor(np.max(pixel[:, 1]))), height - 1)
|
||||
if x_min > x_max or y_min > y_max:
|
||||
continue
|
||||
|
||||
a, b, c = pixel
|
||||
denominator = (b[1] - c[1]) * (a[0] - c[0]) + (
|
||||
c[0] - b[0]
|
||||
) * (a[1] - c[1])
|
||||
if abs(denominator) <= 1e-12:
|
||||
continue
|
||||
grid_x, grid_y = np.meshgrid(
|
||||
np.arange(x_min, x_max + 1, dtype=np.float64),
|
||||
np.arange(y_min, y_max + 1, dtype=np.float64),
|
||||
)
|
||||
first = (
|
||||
(b[1] - c[1]) * (grid_x - c[0])
|
||||
+ (c[0] - b[0]) * (grid_y - c[1])
|
||||
) / denominator
|
||||
second = (
|
||||
(c[1] - a[1]) * (grid_x - c[0])
|
||||
+ (a[0] - c[0]) * (grid_y - c[1])
|
||||
) / denominator
|
||||
third = 1.0 - first - second
|
||||
inside = (first >= -1e-6) & (second >= -1e-6) & (third >= -1e-6)
|
||||
if not inside.any():
|
||||
continue
|
||||
|
||||
weights = np.stack((first[inside], second[inside], third[inside]), axis=-1)
|
||||
normal = _normalized(weights @ corner_normal, (0.0, 0.0, 1.0))
|
||||
tangent = weights @ corner_tangent
|
||||
tangent -= normal * np.sum(tangent * normal, axis=-1, keepdims=True)
|
||||
tangent = _normalized(tangent, (1.0, 0.0, 0.0))
|
||||
sign = np.where(weights @ corner_sign >= 0.0, 1.0, -1.0)
|
||||
bitangent = np.cross(normal, tangent) * sign[:, None]
|
||||
|
||||
rows = grid_y[inside].astype(np.int32)
|
||||
columns = grid_x[inside].astype(np.int32)
|
||||
source = object_normal[rows, columns]
|
||||
output[rows, columns, 0] = np.sum(source * tangent, axis=-1)
|
||||
output[rows, columns, 1] = np.sum(source * bitangent, axis=-1)
|
||||
output[rows, columns, 2] = np.sum(source * normal, axis=-1)
|
||||
coverage[rows, columns] = True
|
||||
|
||||
output[coverage] = _normalized(output[coverage], (0.0, 0.0, 1.0))
|
||||
return (output, coverage) if return_coverage else output
|
||||
|
||||
|
||||
def bake_tangent_normal_image(context, source_object, target_object, image):
|
||||
"""Bake a MoGe object-space image against the evaluated target mesh TBN."""
|
||||
import numpy as np
|
||||
|
||||
width, height = (int(value) for value in image.size)
|
||||
rgba = image_pixels(image).reshape((height, width, 4))
|
||||
source_normal = rgba[..., :3].astype(np.float64) * 2.0 - 1.0
|
||||
|
||||
source_to_target = (
|
||||
target_object.matrix_world.inverted().to_3x3()
|
||||
@ source_object.matrix_world.to_3x3()
|
||||
)
|
||||
normal_matrix = source_to_target.inverted().transposed()
|
||||
transform = np.asarray(normal_matrix, dtype=np.float64)
|
||||
target_normal = source_normal @ transform.T
|
||||
target_normal = _normalized(target_normal, (0.0, 0.0, 1.0))
|
||||
front = _normalized(
|
||||
np.asarray((0.0, 0.0, 1.0), dtype=np.float64)[None, :] @ transform.T,
|
||||
(0.0, 0.0, 1.0),
|
||||
)[0]
|
||||
|
||||
context.view_layer.update()
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
evaluated = target_object.evaluated_get(depsgraph)
|
||||
mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
|
||||
try:
|
||||
uv_layer = mesh.uv_layers.active
|
||||
if uv_layer is None:
|
||||
raise ValueError("The target mesh has no active UV map")
|
||||
mesh.calc_loop_triangles()
|
||||
mesh.calc_tangents(uvmap=uv_layer.name)
|
||||
loop_count = len(mesh.loops)
|
||||
uv = np.empty((loop_count, 2), dtype=np.float64)
|
||||
uv_layer.data.foreach_get("uv", uv.ravel())
|
||||
tangent = np.empty((loop_count, 3), dtype=np.float64)
|
||||
sign = np.empty(loop_count, dtype=np.float64)
|
||||
mesh.loops.foreach_get("tangent", tangent.ravel())
|
||||
mesh.loops.foreach_get("bitangent_sign", sign)
|
||||
normal = np.asarray(
|
||||
[tuple(loop.normal) for loop in mesh.loops],
|
||||
dtype=np.float64,
|
||||
)
|
||||
triangles = np.asarray(
|
||||
[tuple(triangle.loops) for triangle in mesh.loop_triangles],
|
||||
dtype=np.int32,
|
||||
)
|
||||
triangle_normal = np.asarray(
|
||||
[tuple(triangle.normal) for triangle in mesh.loop_triangles],
|
||||
dtype=np.float64,
|
||||
)
|
||||
front_facing = triangle_normal @ front > 1e-6
|
||||
triangles = triangles[front_facing]
|
||||
tangent_normal, coverage = rasterize_tangent_normal(
|
||||
target_normal,
|
||||
uv[triangles],
|
||||
normal[triangles],
|
||||
tangent[triangles],
|
||||
sign[triangles],
|
||||
return_coverage=True,
|
||||
)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
rgba[coverage, :3] = tangent_normal[coverage] * 0.5 + 0.5
|
||||
image.pixels.foreach_set(rgba.ravel())
|
||||
image.update()
|
||||
image.pack()
|
||||
return image
|
||||
125
src/anyimage/operators/convert_to_plane/object.py
Normal file
125
src/anyimage/operators/convert_to_plane/object.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Create Blender Image, Material, Mesh, and Object data for Plane conversion."""
|
||||
|
||||
import bpy
|
||||
|
||||
from ..common.image_data import (
|
||||
color_data_name,
|
||||
image_empty_bounds,
|
||||
image_pixels,
|
||||
load_color_result_image,
|
||||
require_image_empty,
|
||||
)
|
||||
from ..common.depth_data import (
|
||||
depth_uniform_scale,
|
||||
load_depth_metadata,
|
||||
load_depth_result_image,
|
||||
)
|
||||
from ..common.mesh_result import (
|
||||
create_image_material,
|
||||
finalize_object_result,
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
)
|
||||
from .mesh_plane import create_mesh_plane_mesh, create_mesh_plane_object
|
||||
from .nodes import depth_plane_node_group
|
||||
|
||||
|
||||
def create_color_image(source_object):
|
||||
source = source_object.data
|
||||
image = source.copy()
|
||||
if getattr(source, "source", None) == "GENERATED" or getattr(
|
||||
source, "is_dirty", False
|
||||
):
|
||||
pixels = image_pixels(source)
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
image.name = color_data_name(source_object)
|
||||
return image
|
||||
|
||||
|
||||
def create_plane_object(context, source_object, subdivisions, material):
|
||||
source_name = source_object.name
|
||||
plane = create_mesh_plane_object(
|
||||
context,
|
||||
source_name,
|
||||
image_empty_bounds(source_object),
|
||||
material,
|
||||
matrix_world=source_object.matrix_world,
|
||||
subdivisions=subdivisions,
|
||||
)
|
||||
for polygon in plane.data.polygons:
|
||||
polygon.use_smooth = True
|
||||
plane["anyimage_mesh_shape"] = "PLANE"
|
||||
return finalize_object_result(context, source_object, plane)
|
||||
|
||||
|
||||
def create_depth_plane_object(
|
||||
context,
|
||||
source_object,
|
||||
subdivisions,
|
||||
material,
|
||||
depth_image,
|
||||
depth_metadata,
|
||||
):
|
||||
source_name = source_object.name
|
||||
mesh = create_mesh_plane_mesh(source_name, image_empty_bounds(source_object))
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
plane.matrix_world = source_object.matrix_world.copy()
|
||||
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
|
||||
|
||||
node_group = depth_plane_node_group()
|
||||
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
|
||||
modifier.node_group = node_group
|
||||
modifier.show_group_selector = False
|
||||
for name, value in (
|
||||
("Subdivide", max(0, min(10, int(subdivisions)))),
|
||||
("Material", material),
|
||||
("Thickness", 0.1),
|
||||
("Depth Amount", 1.0),
|
||||
("Reference Depth", depth_metadata["reference_depth"]),
|
||||
("Uniform Scale", depth_uniform_scale(source_object, depth_metadata)),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(node_group, name),
|
||||
value,
|
||||
)
|
||||
return finalize_object_result(context, source_object, plane)
|
||||
|
||||
|
||||
def create_depth_plane_from_result(context, result, operator):
|
||||
source_object = require_image_empty(operator.source_object_name)
|
||||
color_image = load_color_result_image(source_object, result.directory)
|
||||
color_image.name = color_data_name(source_object)
|
||||
material = create_image_material(
|
||||
source_object,
|
||||
color_image,
|
||||
0.0,
|
||||
scene=context.scene,
|
||||
)
|
||||
depth_metadata = load_depth_metadata(result.file("depth_metadata"))
|
||||
depth_image = load_depth_result_image(
|
||||
result.file("z_depth"),
|
||||
source_object,
|
||||
depth_metadata,
|
||||
)
|
||||
try:
|
||||
create_depth_plane_object(
|
||||
context,
|
||||
source_object,
|
||||
operator.mesh_detail,
|
||||
material,
|
||||
depth_image,
|
||||
depth_metadata,
|
||||
)
|
||||
except Exception:
|
||||
if depth_image.users == 0:
|
||||
bpy.data.images.remove(depth_image, do_unlink=True)
|
||||
raise
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Convert to Depth Plane"):
|
||||
raise RuntimeError("Unable to create the Convert to Depth Plane Undo step")
|
||||
@@ -1,16 +0,0 @@
|
||||
DA3_PATCH_SIZE = 14
|
||||
DA3_MIN_PROCESS_RES = 252
|
||||
DA3_MAX_PROCESS_RES = 1008
|
||||
|
||||
|
||||
def scaled_process_resolution(image, scale_percent):
|
||||
"""Return the scaled long edge clamped and aligned for DA3 inference."""
|
||||
width, height = image.size
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("The source image has no readable dimensions")
|
||||
if scale_percent < 1.0 or scale_percent > 100.0:
|
||||
raise ValueError("Resolution Scale must be between 1% and 100%")
|
||||
process_res = max(width, height) * scale_percent / 100.0
|
||||
process_res = max(DA3_MIN_PROCESS_RES, process_res)
|
||||
process_res = min(DA3_MAX_PROCESS_RES, process_res)
|
||||
return int(process_res / DA3_PATCH_SIZE + 0.5) * DA3_PATCH_SIZE
|
||||
Reference in New Issue
Block a user