feat: Implement cutout tool and depth generation features
- Added a new cutout tool for selecting regions in images, allowing users to choose mesh shapes based on selected areas. - Introduced depth generation capabilities for rasterized images, including normal data extraction. - Created jobs for generating depth planes and region data, enhancing the AI data generation workflow. - Developed a selection refinement process for cutout regions, improving accuracy in generated outputs. - Established a new region module for handling rasterized image regions and their associated AI generation tasks. - Enhanced node graph builders for asset generation, facilitating smoother integration of new features.
This commit is contained in:
@@ -19,9 +19,6 @@ from build_cutout import (
|
||||
)
|
||||
from build_mesh_plane import build_mesh_plane_group
|
||||
from build_shadeless import build_shadeless_group
|
||||
from anyimage.operators.common.node_graph import expand_smooth
|
||||
|
||||
|
||||
OUTPUT_PATH = (
|
||||
PROJECT_ROOT
|
||||
/ "src"
|
||||
@@ -40,7 +37,7 @@ def main():
|
||||
shape: build_node_group(shape)
|
||||
for shape in CUTOUT_NODE_GROUP_NAMES
|
||||
}
|
||||
build_cutout_modifier_group(shape_groups, expand_smooth)
|
||||
build_cutout_modifier_group(shape_groups)
|
||||
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(OUTPUT_PATH), check_existing=False)
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import math
|
||||
|
||||
import bpy
|
||||
|
||||
from node_graph import expand_smooth
|
||||
|
||||
from anyimage.operators.cutout_tool.shape_types import (
|
||||
BALLOON_ATTRIBUTE_NAME,
|
||||
CUTOUT_NODE_GROUP_NAME,
|
||||
@@ -727,7 +729,7 @@ def build_node_group(shape):
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
reference_depth = _float_input(
|
||||
group,
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
1.0,
|
||||
0.0,
|
||||
None,
|
||||
@@ -751,7 +753,7 @@ def build_node_group(shape):
|
||||
options = group.interface.new_panel(name="Options", default_closed=True)
|
||||
reference_depth = _float_input(
|
||||
group,
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
1.0,
|
||||
0.0,
|
||||
None,
|
||||
@@ -788,7 +790,7 @@ def build_node_group(shape):
|
||||
"Depth Image",
|
||||
"Depth Scale",
|
||||
"Uniform Scale",
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
"Depth Direction",
|
||||
},
|
||||
)
|
||||
@@ -813,7 +815,7 @@ def build_node_group(shape):
|
||||
controls.outputs["Uniform Scale"],
|
||||
controls.outputs["Depth Direction"],
|
||||
controls.outputs["Depth Image"],
|
||||
controls.outputs["Reference Depth"],
|
||||
controls.outputs["Base Plane Depth"],
|
||||
)
|
||||
output_location = (5100.0, 600.0)
|
||||
else:
|
||||
@@ -823,7 +825,7 @@ def build_node_group(shape):
|
||||
controls.outputs["Thickness"],
|
||||
controls.outputs["Depth Scale"],
|
||||
controls.outputs["Uniform Scale"],
|
||||
controls.outputs["Reference Depth"],
|
||||
controls.outputs["Base Plane Depth"],
|
||||
controls.outputs["Depth Image"],
|
||||
)
|
||||
output_location = (8200.0, 400.0)
|
||||
@@ -844,7 +846,7 @@ def build_node_group(shape):
|
||||
return group
|
||||
|
||||
|
||||
def build_cutout_modifier_group(shape_groups, expand_smooth):
|
||||
def build_cutout_modifier_group(shape_groups):
|
||||
group = bpy.data.node_groups.get(CUTOUT_NODE_GROUP_NAME)
|
||||
if group is not None:
|
||||
return group
|
||||
@@ -896,16 +898,14 @@ def build_cutout_modifier_group(shape_groups, expand_smooth):
|
||||
|
||||
reference_depth = _float_input(
|
||||
group,
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
1.0,
|
||||
0.0,
|
||||
None,
|
||||
subtype="DISTANCE",
|
||||
parent=options,
|
||||
)
|
||||
reference_depth.description = (
|
||||
"Metric reference depth shared by both Depth shapes"
|
||||
)
|
||||
reference_depth.description = "Depth of the flat base plane for both Depth shapes"
|
||||
depth_direction = _vector_input(
|
||||
group,
|
||||
"Depth Direction",
|
||||
@@ -936,10 +936,13 @@ def build_cutout_modifier_group(shape_groups, expand_smooth):
|
||||
(-2700.0, 500.0),
|
||||
{"Geometry", "Cleanup"},
|
||||
)
|
||||
cleanup_area = _math(group.nodes, "MULTIPLY", (-2500.0, 750.0))
|
||||
cleanup_area.inputs[1].default_value = 0.01
|
||||
group.links.new(base_controls.outputs["Cleanup"], cleanup_area.inputs[0])
|
||||
geometry = _island_cleanup(
|
||||
group,
|
||||
base_controls.outputs["Geometry"],
|
||||
base_controls.outputs["Cleanup"],
|
||||
cleanup_area.outputs[0],
|
||||
(-2350.0, 250.0),
|
||||
)
|
||||
|
||||
|
||||
181
tools/node_assets/node_graph.py
Normal file
181
tools/node_assets/node_graph.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Node graph builders used only while generating bundled assets."""
|
||||
|
||||
|
||||
def group_input(nodes, location, visible_names):
|
||||
node = nodes.new("NodeGroupInput")
|
||||
node.location = location
|
||||
visible = set(visible_names)
|
||||
for output in node.outputs:
|
||||
output.hide = output.name not in visible
|
||||
return node
|
||||
|
||||
|
||||
def edge_boundary_field(nodes, links, location):
|
||||
x, y = location
|
||||
vertex_neighbors = nodes.new("GeometryNodeInputMeshVertexNeighbors")
|
||||
vertex_neighbors.location = (x, y + 150.0)
|
||||
loose_vertex = nodes.new("FunctionNodeCompare")
|
||||
loose_vertex.data_type = "INT"
|
||||
loose_vertex.operation = "EQUAL"
|
||||
loose_vertex_a = next(
|
||||
socket for socket in loose_vertex.inputs if socket.identifier == "A_INT"
|
||||
)
|
||||
loose_vertex_b = next(
|
||||
socket for socket in loose_vertex.inputs if socket.identifier == "B_INT"
|
||||
)
|
||||
loose_vertex_b.default_value = 1
|
||||
loose_vertex.location = (x + 200.0, y + 150.0)
|
||||
links.new(vertex_neighbors.outputs["Vertex Count"], loose_vertex_a)
|
||||
|
||||
edge_neighbors = nodes.new("GeometryNodeInputMeshEdgeNeighbors")
|
||||
edge_neighbors.location = (x, y - 150.0)
|
||||
boundary_edge = nodes.new("FunctionNodeCompare")
|
||||
boundary_edge.data_type = "INT"
|
||||
boundary_edge.operation = "EQUAL"
|
||||
boundary_edge_a = next(
|
||||
socket for socket in boundary_edge.inputs if socket.identifier == "A_INT"
|
||||
)
|
||||
boundary_edge_b = next(
|
||||
socket for socket in boundary_edge.inputs if socket.identifier == "B_INT"
|
||||
)
|
||||
boundary_edge_b.default_value = 1
|
||||
boundary_edge.location = (x + 200.0, y - 150.0)
|
||||
links.new(edge_neighbors.outputs["Face Count"], boundary_edge_a)
|
||||
edge_domain = nodes.new("GeometryNodeFieldOnDomain")
|
||||
edge_domain.data_type = "BOOLEAN"
|
||||
edge_domain.domain = "EDGE"
|
||||
edge_domain.location = (x + 400.0, y - 150.0)
|
||||
links.new(boundary_edge.outputs["Result"], edge_domain.inputs["Value"])
|
||||
|
||||
is_boundary = nodes.new("FunctionNodeBooleanMath")
|
||||
is_boundary.operation = "OR"
|
||||
is_boundary.location = (x + 600.0, y)
|
||||
links.new(edge_domain.outputs["Value"], is_boundary.inputs[0])
|
||||
links.new(loose_vertex.outputs["Result"], is_boundary.inputs[1])
|
||||
return is_boundary.outputs["Boolean"]
|
||||
|
||||
|
||||
def expand_smooth(
|
||||
group, geometry, x_offset=0.0, iterations_name="Iterations"
|
||||
):
|
||||
nodes = group.nodes
|
||||
links = group.links
|
||||
existing_nodes = set(nodes)
|
||||
iteration_controls = group_input(nodes, (550.0, 1150.0), {iterations_name})
|
||||
weight_controls = group_input(nodes, (2300.0, 1450.0), {"Smooth"})
|
||||
repeat_output = nodes.new("GeometryNodeRepeatOutput")
|
||||
repeat_output.location = (3050.0, 550.0)
|
||||
repeat_input = nodes.new("GeometryNodeRepeatInput")
|
||||
repeat_input.pair_with_output(repeat_output)
|
||||
repeat_output.repeat_items.new("GEOMETRY", "Geometry")
|
||||
repeat_input.location = (550.0, 550.0)
|
||||
links.new(
|
||||
iteration_controls.outputs[iterations_name],
|
||||
repeat_input.inputs["Iterations"],
|
||||
)
|
||||
links.new(geometry, repeat_input.inputs["Geometry"])
|
||||
|
||||
boundary_field = edge_boundary_field(nodes, links, (750.0, -350.0))
|
||||
boundary_points = nodes.new("GeometryNodeFieldOnDomain")
|
||||
boundary_points.data_type = "BOOLEAN"
|
||||
boundary_points.domain = "POINT"
|
||||
boundary_points.location = (1550.0, -350.0)
|
||||
links.new(boundary_field, boundary_points.inputs["Value"])
|
||||
|
||||
boundary_geometry = nodes.new("GeometryNodeSeparateGeometry")
|
||||
boundary_geometry.domain = "POINT"
|
||||
boundary_geometry.location = (1750.0, -150.0)
|
||||
links.new(repeat_input.outputs["Geometry"], boundary_geometry.inputs["Geometry"])
|
||||
links.new(boundary_points.outputs["Value"], boundary_geometry.inputs["Selection"])
|
||||
|
||||
position = nodes.new("GeometryNodeInputPosition")
|
||||
position.location = (750.0, 850.0)
|
||||
smooth_all = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_all.data_type = "FLOAT_VECTOR"
|
||||
smooth_all.inputs["Iterations"].default_value = 1
|
||||
smooth_all.inputs["Weight"].default_value = 1.0
|
||||
smooth_all.location = (950.0, 850.0)
|
||||
links.new(position.outputs["Position"], smooth_all.inputs["Value"])
|
||||
|
||||
boundary_position = nodes.new("GeometryNodeInputPosition")
|
||||
boundary_position.location = (1350.0, 250.0)
|
||||
smooth_boundary = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_boundary.data_type = "FLOAT_VECTOR"
|
||||
smooth_boundary.inputs["Iterations"].default_value = 1
|
||||
smooth_boundary.inputs["Weight"].default_value = 0.5
|
||||
smooth_boundary.location = (1550.0, 250.0)
|
||||
links.new(boundary_position.outputs["Position"], smooth_boundary.inputs["Value"])
|
||||
nearest_boundary = nodes.new("GeometryNodeSampleNearest")
|
||||
nearest_boundary.domain = "POINT"
|
||||
nearest_boundary.location = (1800.0, 150.0)
|
||||
links.new(boundary_geometry.outputs["Selection"], nearest_boundary.inputs["Geometry"])
|
||||
sample_boundary = nodes.new("GeometryNodeSampleIndex")
|
||||
sample_boundary.data_type = "FLOAT_VECTOR"
|
||||
sample_boundary.domain = "POINT"
|
||||
sample_boundary.clamp = False
|
||||
sample_boundary.location = (2050.0, 250.0)
|
||||
links.new(boundary_geometry.outputs["Selection"], sample_boundary.inputs["Geometry"])
|
||||
links.new(smooth_boundary.outputs["Value"], sample_boundary.inputs["Value"])
|
||||
links.new(nearest_boundary.outputs["Index"], sample_boundary.inputs["Index"])
|
||||
|
||||
choose_boundary = nodes.new("GeometryNodeSwitch")
|
||||
choose_boundary.input_type = "VECTOR"
|
||||
choose_boundary.location = (2300.0, 350.0)
|
||||
direct_boundary = edge_boundary_field(nodes, links, (750.0, 450.0))
|
||||
links.new(direct_boundary, choose_boundary.inputs["Switch"])
|
||||
links.new(smooth_all.outputs["Value"], choose_boundary.inputs["False"])
|
||||
links.new(sample_boundary.outputs["Value"], choose_boundary.inputs["True"])
|
||||
boundary_mix = nodes.new("ShaderNodeMix")
|
||||
boundary_mix.data_type = "VECTOR"
|
||||
boundary_mix.inputs[0].default_value = 1.0
|
||||
boundary_mix.location = (2550.0, 400.0)
|
||||
links.new(smooth_all.outputs["Value"], boundary_mix.inputs[4])
|
||||
links.new(choose_boundary.outputs["Output"], boundary_mix.inputs[5])
|
||||
|
||||
normal = nodes.new("GeometryNodeInputNormal")
|
||||
normal.location = (1350.0, 1050.0)
|
||||
smooth_normal = nodes.new("GeometryNodeBlurAttribute")
|
||||
smooth_normal.data_type = "FLOAT_VECTOR"
|
||||
smooth_normal.inputs["Iterations"].default_value = 10
|
||||
smooth_normal.inputs["Weight"].default_value = 1.0
|
||||
smooth_normal.location = (1550.0, 1050.0)
|
||||
links.new(normal.outputs["Normal"], smooth_normal.inputs["Value"])
|
||||
normal_coherence = nodes.new("ShaderNodeVectorMath")
|
||||
normal_coherence.operation = "LENGTH"
|
||||
normal_coherence.location = (1800.0, 1050.0)
|
||||
links.new(smooth_normal.outputs["Value"], normal_coherence.inputs[0])
|
||||
sharp_switch = nodes.new("GeometryNodeSwitch")
|
||||
sharp_switch.input_type = "FLOAT"
|
||||
sharp_switch.inputs["Switch"].default_value = True
|
||||
sharp_switch.inputs["False"].default_value = 1.0
|
||||
sharp_switch.location = (2050.0, 1050.0)
|
||||
links.new(normal_coherence.outputs["Value"], sharp_switch.inputs["True"])
|
||||
smooth_weight = nodes.new("ShaderNodeMath")
|
||||
smooth_weight.operation = "MULTIPLY"
|
||||
smooth_weight.location = (2300.0, 1050.0)
|
||||
links.new(weight_controls.outputs["Smooth"], smooth_weight.inputs[0])
|
||||
links.new(sharp_switch.outputs["Output"], smooth_weight.inputs[1])
|
||||
|
||||
target_position = nodes.new("GeometryNodeSwitch")
|
||||
target_position.input_type = "VECTOR"
|
||||
target_position.inputs["Switch"].default_value = True
|
||||
target_position.location = (2550.0, 700.0)
|
||||
links.new(smooth_all.outputs["Value"], target_position.inputs["False"])
|
||||
links.new(boundary_mix.outputs["Result"], target_position.inputs["True"])
|
||||
current_position = nodes.new("GeometryNodeInputPosition")
|
||||
current_position.location = (2300.0, 850.0)
|
||||
mix_position = nodes.new("ShaderNodeMix")
|
||||
mix_position.data_type = "VECTOR"
|
||||
mix_position.location = (2800.0, 850.0)
|
||||
links.new(smooth_weight.outputs[0], mix_position.inputs[0])
|
||||
links.new(current_position.outputs["Position"], mix_position.inputs[4])
|
||||
links.new(target_position.outputs["Output"], mix_position.inputs[5])
|
||||
set_position = nodes.new("GeometryNodeSetPosition")
|
||||
set_position.location = (2800.0, 550.0)
|
||||
links.new(repeat_input.outputs["Geometry"], set_position.inputs["Geometry"])
|
||||
links.new(mix_position.outputs["Result"], set_position.inputs["Position"])
|
||||
links.new(set_position.outputs["Geometry"], repeat_output.inputs["Geometry"])
|
||||
for node in set(nodes) - existing_nodes:
|
||||
node.location.x += x_offset
|
||||
return repeat_output.outputs["Geometry"]
|
||||
|
||||
@@ -11,10 +11,8 @@ sys.path.insert(
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from anyimage.operators.common.node_asset import (
|
||||
from anyimage.operators.common.mesh_result import (
|
||||
load_asset_node_group,
|
||||
load_mesh_plane_node_group,
|
||||
load_shadeless_node_group,
|
||||
)
|
||||
from anyimage.operators.cutout_tool.shape_types import (
|
||||
CUTOUT_NODE_GROUP_NAME,
|
||||
@@ -32,8 +30,8 @@ def input_names(group):
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
mesh_plane = load_mesh_plane_node_group()
|
||||
shadeless = load_shadeless_node_group()
|
||||
mesh_plane = load_asset_node_group("O Mesh Plane")
|
||||
shadeless = load_asset_node_group("O Shadeless")
|
||||
cutout = load_asset_node_group(CUTOUT_NODE_GROUP_NAME)
|
||||
cutout_groups = {
|
||||
name: bpy.data.node_groups[name]
|
||||
|
||||
@@ -13,7 +13,7 @@ sys.path.insert(
|
||||
|
||||
from anyimage.operators.cutout_tool import node_assets as cutout_nodes
|
||||
from anyimage.operators.cutout_tool import shape_types as shapes
|
||||
from anyimage.operators.common.node_asset import (
|
||||
from anyimage.operators.common.mesh_result import (
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
)
|
||||
@@ -125,7 +125,7 @@ def main():
|
||||
"Smooth Iterations",
|
||||
"Cleanup",
|
||||
"Smooth",
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
"Depth Direction",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
@@ -138,7 +138,7 @@ def main():
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Double Sided",
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
"Depth Direction",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
@@ -147,7 +147,7 @@ def main():
|
||||
"Geometry",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Reference Depth",
|
||||
"Base Plane Depth",
|
||||
"Uniform Scale",
|
||||
"Depth Image",
|
||||
],
|
||||
@@ -171,7 +171,7 @@ def main():
|
||||
"Thickness": set(shapes.CUTOUT_SHAPES),
|
||||
"Depth Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Double Sided": {"DEPTH_BALLOON"},
|
||||
"Reference Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Base Plane Depth": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Depth Direction": {"DEPTH_BALLOON"},
|
||||
"Uniform Scale": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
"Depth Image": {"DEPTH_BALLOON", "DEPTH_SURFACE"},
|
||||
@@ -230,6 +230,15 @@ def main():
|
||||
)
|
||||
assert cleanup.default_value == 0.0
|
||||
assert cleanup.subtype == "NONE"
|
||||
cleanup_scale = next(
|
||||
link.to_node
|
||||
for link in wrapper.links
|
||||
if link.from_socket.name == "Cleanup"
|
||||
and link.from_node.bl_idname == "NodeGroupInput"
|
||||
)
|
||||
assert cleanup_scale.bl_idname == "ShaderNodeMath"
|
||||
assert cleanup_scale.operation == "MULTIPLY"
|
||||
assert abs(cleanup_scale.inputs[1].default_value - 0.01) < 1e-7
|
||||
assert smooth.default_value == 1.0
|
||||
assert iterations.default_value == 0
|
||||
assert iterations.min_value == 0
|
||||
@@ -341,7 +350,7 @@ def main():
|
||||
("Depth Image", point_image),
|
||||
("Depth Scale", 1.0),
|
||||
("Uniform Scale", 1.0),
|
||||
("Reference Depth", 2.0),
|
||||
("Base Plane Depth", 2.0),
|
||||
("Depth Direction", (0.0, 0.0, 1.0)),
|
||||
):
|
||||
set_modifier_input(
|
||||
@@ -392,7 +401,7 @@ def main():
|
||||
("Depth Image", point_image),
|
||||
("Depth Scale", 1.0),
|
||||
("Uniform Scale", 1.0),
|
||||
("Reference Depth", 2.0),
|
||||
("Base Plane Depth", 2.0),
|
||||
("Smooth Iterations", 0),
|
||||
):
|
||||
set_modifier_input(
|
||||
|
||||
@@ -11,8 +11,7 @@ sys.path.insert(
|
||||
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
|
||||
)
|
||||
|
||||
from anyimage.operators.common.node_asset import (
|
||||
get_modifier_input,
|
||||
from anyimage.operators.common.mesh_result import (
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
)
|
||||
@@ -28,6 +27,13 @@ def evaluated_mesh(obj):
|
||||
return evaluated, evaluated.to_mesh()
|
||||
|
||||
|
||||
def modifier_input_value(modifier, identifier):
|
||||
inputs = getattr(getattr(modifier, "properties", None), "inputs", None)
|
||||
if inputs is not None:
|
||||
return getattr(inputs, identifier).value
|
||||
return modifier[identifier]
|
||||
|
||||
|
||||
def main():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
mesh = create_mesh_plane_mesh("Mesh Plane", (-1.0, 1.0, -0.5, 0.5))
|
||||
@@ -49,7 +55,7 @@ def main():
|
||||
assert inputs["Subdivide"].max_value == 10
|
||||
assert inputs["Thickness"].default_value == 0.0
|
||||
assert inputs["Material"].hide_in_modifier
|
||||
assert get_modifier_input(modifier, inputs["Material"].identifier) == material
|
||||
assert modifier_input_value(modifier, inputs["Material"].identifier) == material
|
||||
|
||||
evaluated, result = evaluated_mesh(obj)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user