Add image selection refinement functionality with BEN2 integration
- Implemented viewport.py for handling 3D view projections, gestures, and overlay drawing. - Created refine_image_selection.py to manage the refinement of gesture masks and output image bounds. - Added selection module with refine_selection.py to process and crop images based on alpha channel visibility. - Introduced __init__.py for selection module to expose refine_selection function.
This commit is contained in:
@@ -1 +1 @@
|
||||
"""Shared Blender-side image, node, object, interaction, and AI Job helpers."""
|
||||
"""Shared Blender-side AI, image, selection, viewport, and result helpers."""
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
"""Shared Blender-side AI setup, validation, UI, and Job parameters."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..preferences import addon_preferences
|
||||
from ..properties import ai_ready, model_ready, model_record
|
||||
from ..properties import (
|
||||
ai_ready,
|
||||
ai_setup_label,
|
||||
ai_status,
|
||||
model_ready,
|
||||
model_record,
|
||||
)
|
||||
from ..runtime import runtime
|
||||
|
||||
BEN2_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
@@ -12,6 +20,23 @@ BEN2_VIDEO_SUFFIXES = {
|
||||
}
|
||||
|
||||
|
||||
def draw_ai_setup(layout):
|
||||
status = ai_status()
|
||||
if not status["ready"]:
|
||||
layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
return status
|
||||
|
||||
|
||||
def draw_ai_property(layout, owner, name, status):
|
||||
row = layout.row()
|
||||
row.enabled = status["ready"]
|
||||
row.prop(owner, name)
|
||||
|
||||
|
||||
def require_input_path(value):
|
||||
path = Path(bpy.path.abspath(value))
|
||||
if path.is_file() or path.is_dir():
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .image_data import depth_data_name, image_empty_bounds
|
||||
from .image import depth_data_name, image_empty_bounds
|
||||
|
||||
|
||||
def load_depth_metadata(path):
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Shared Blender Image and Image Empty lifecycle helpers."""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -260,21 +262,21 @@ def prepare_image_input(image, scene):
|
||||
return source_path, False
|
||||
|
||||
|
||||
def prepare_region_input(image, region_mask):
|
||||
"""Write one cropped RGBA Region input owned by its temporary directory."""
|
||||
def prepare_selection_input(image, selection_mask):
|
||||
"""Write one cropped RGBA Selection input owned by its temporary directory."""
|
||||
import numpy as np
|
||||
|
||||
left, top, right, bottom = region_mask.bounds
|
||||
left, top, right, bottom = selection_mask.bounds
|
||||
rgba = image_rgba(image)
|
||||
cropped = rgba[top:bottom, left:right].copy()
|
||||
if cropped.shape[:2] != region_mask.values.shape:
|
||||
raise ValueError("The Region does not match the source image")
|
||||
cropped[:, :, 3] *= region_mask.values
|
||||
if cropped.shape[:2] != selection_mask.values.shape:
|
||||
raise ValueError("The Selection does not match the source image")
|
||||
cropped[:, :, 3] *= selection_mask.values
|
||||
cropped[cropped[:, :, 3] <= 0.0, :3] = 0.0
|
||||
output_directory = Path(tempfile.mkdtemp(prefix="anyimage-region-"))
|
||||
output_directory = Path(tempfile.mkdtemp(prefix="anyimage-selection-"))
|
||||
output_path = output_directory / "input.png"
|
||||
temporary_image = bpy.data.images.new(
|
||||
"AnyImage Region Input",
|
||||
"AnyImage Selection Input",
|
||||
width=right - left,
|
||||
height=bottom - top,
|
||||
alpha=True,
|
||||
@@ -294,7 +296,7 @@ def prepare_region_input(image, region_mask):
|
||||
bpy.data.images.remove(temporary_image, do_unlink=True)
|
||||
if not output_path.is_file():
|
||||
cleanup_image_input(output_path, True)
|
||||
raise RuntimeError("Blender did not export the Region input")
|
||||
raise RuntimeError("Blender did not export the Selection input")
|
||||
return output_path
|
||||
|
||||
|
||||
261
src/anyimage/common/material.py
Normal file
261
src/anyimage/common/material.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""Build and configure Blender materials for Image results."""
|
||||
|
||||
import bpy
|
||||
|
||||
from .image import (
|
||||
configure_image_user,
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
is_animated_image,
|
||||
)
|
||||
from .node import load_node_group
|
||||
|
||||
|
||||
DISPLACEMENT_STRENGTH = 1.0
|
||||
DISPLACEMENT_MIDLEVEL = 0.0
|
||||
IMAGE_MATERIAL_NODE_GROUP_NAME = "O Image Layer"
|
||||
DEPTH_MATERIAL_NODE_GROUP_NAME = "O Image Depth Layer"
|
||||
SHADELESS_NODE_GROUP_NAME = "O Shadeless"
|
||||
VIEW_TRANSFORM_COLOR_SPACES = (
|
||||
("ACES 1.3", "ACES 1.3 sRGB"),
|
||||
("ACES 2.0", "ACES 2.0 sRGB"),
|
||||
("AgX", "AgX Base sRGB"),
|
||||
("Filmic", "Filmic sRGB"),
|
||||
("Khronos PBR Neutral", "Khronos PBR Neutral sRGB"),
|
||||
)
|
||||
|
||||
|
||||
def material_color_space_name(scene):
|
||||
view_settings = getattr(scene, "view_settings", None)
|
||||
view_transform = getattr(view_settings, "view_transform", "")
|
||||
normalized = str(view_transform).strip().casefold()
|
||||
for view_name, color_space in VIEW_TRANSFORM_COLOR_SPACES:
|
||||
if normalized.startswith(view_name.casefold()):
|
||||
return color_space
|
||||
return "sRGB"
|
||||
|
||||
|
||||
def set_material_color_space(image, scene=None):
|
||||
if scene is None:
|
||||
scene = getattr(bpy.context, "scene", None)
|
||||
settings = image.colorspace_settings
|
||||
preferred = material_color_space_name(scene)
|
||||
original = settings.name
|
||||
pixels = None
|
||||
if preferred != original and (
|
||||
getattr(image, "source", None) == "GENERATED"
|
||||
or getattr(image, "is_dirty", False)
|
||||
):
|
||||
pixels = image_pixels(image)
|
||||
for color_space in dict.fromkeys((preferred, "sRGB", original)):
|
||||
if not color_space:
|
||||
continue
|
||||
try:
|
||||
settings.name = color_space
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if settings.name == color_space:
|
||||
if pixels is not None:
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
return color_space
|
||||
return settings.name
|
||||
|
||||
|
||||
def material_node_group(depth_plane=False):
|
||||
group_name = (
|
||||
DEPTH_MATERIAL_NODE_GROUP_NAME
|
||||
if depth_plane
|
||||
else IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
)
|
||||
return load_node_group(group_name)
|
||||
|
||||
|
||||
def create_image_material(
|
||||
source_object,
|
||||
color_image,
|
||||
depth_scale,
|
||||
displacement_image=None,
|
||||
displacement_midlevel=0.0,
|
||||
displacement_scale=1.0,
|
||||
shadeless=False,
|
||||
frame_count=1,
|
||||
frame_start=1,
|
||||
color_frame_offset=0,
|
||||
color_frame_duration=None,
|
||||
depth_frame_offset=0,
|
||||
depth_frame_duration=None,
|
||||
use_cyclic=None,
|
||||
normal_image=None,
|
||||
normal_space="TANGENT",
|
||||
ior=None,
|
||||
scene=None,
|
||||
):
|
||||
material = bpy.data.materials.new(image_base_name(source_object.data))
|
||||
material.use_nodes = True
|
||||
if hasattr(material, "surface_render_method"):
|
||||
material.surface_render_method = "DITHERED"
|
||||
nodes = material.node_tree.nodes
|
||||
nodes.clear()
|
||||
|
||||
color_texture = nodes.new("ShaderNodeTexImage")
|
||||
uv_map = nodes.new("ShaderNodeUVMap")
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
|
||||
uv_map.uv_map = "UVMap"
|
||||
uv_map.location = (-1250.0, 350.0)
|
||||
material_image = color_image
|
||||
set_material_color_space(material_image, scene)
|
||||
color_texture.image = material_image
|
||||
color_texture.interpolation = "Linear"
|
||||
color_texture.extension = "EXTEND"
|
||||
color_texture.location = (-1000.0, 350.0)
|
||||
output.location = (850.0, 350.0)
|
||||
extracted_frames = frame_count if frame_count > 1 else None
|
||||
color_frames = (
|
||||
color_frame_duration if color_frame_duration is not None else extracted_frames
|
||||
)
|
||||
depth_frames = (
|
||||
depth_frame_duration if depth_frame_duration is not None else extracted_frames
|
||||
)
|
||||
material_frames = color_frames
|
||||
material_frame_offset = color_frame_offset
|
||||
if frame_count > 1 or is_animated_image(material_image):
|
||||
configure_image_user(
|
||||
color_texture.image_user,
|
||||
material_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=material_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
|
||||
links = material.node_tree.links
|
||||
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
|
||||
alpha = color_texture.outputs["Alpha"]
|
||||
material_inputs = nodes.new("ShaderNodeGroup")
|
||||
is_depth_plane = displacement_image is not None
|
||||
material_inputs.node_tree = material_node_group(is_depth_plane)
|
||||
material_inputs.location = (-300.0, 350.0)
|
||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||
if "Object Space" in material_inputs.inputs:
|
||||
if normal_space not in {"TANGENT", "OBJECT"}:
|
||||
raise ValueError(f"Unsupported normal space: {normal_space}")
|
||||
material_inputs.inputs["Object Space"].default_value = normal_space == "OBJECT"
|
||||
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
||||
links.new(alpha, material_inputs.inputs["Alpha"])
|
||||
if normal_image is not None and "Normal" in material_inputs.inputs:
|
||||
normal_texture = nodes.new("ShaderNodeTexImage")
|
||||
normal_texture.image = normal_image
|
||||
normal_texture.interpolation = "Linear"
|
||||
normal_texture.extension = "EXTEND"
|
||||
normal_texture.location = (-1000.0, -50.0)
|
||||
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
||||
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
||||
if shadeless:
|
||||
shader = nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
|
||||
shader.name = SHADELESS_NODE_GROUP_NAME
|
||||
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix_shader = nodes.new("ShaderNodeMixShader")
|
||||
shader.location = (0.0, 550.0)
|
||||
transparent.location = (0.0, 150.0)
|
||||
mix_shader.location = (300.0, 350.0)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Alpha"],
|
||||
mix_shader.inputs["Fac"],
|
||||
)
|
||||
links.new(
|
||||
transparent.outputs["BSDF"],
|
||||
mix_shader.inputs[1],
|
||||
)
|
||||
links.new(
|
||||
shader.outputs["Shader"],
|
||||
mix_shader.inputs[2],
|
||||
)
|
||||
links.new(
|
||||
mix_shader.outputs["Shader"],
|
||||
output.inputs["Surface"],
|
||||
)
|
||||
else:
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.location = (250.0, 400.0)
|
||||
if ior is not None:
|
||||
shader.inputs["IOR"].default_value = float(ior)
|
||||
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Base Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Subsurface Radius"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Emission Color"],
|
||||
)
|
||||
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
|
||||
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
|
||||
links.new(
|
||||
material_inputs.outputs["Normal"],
|
||||
shader.inputs["Coat Normal"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Roughness"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
depth_texture = nodes.new("ShaderNodeTexImage")
|
||||
depth_texture.image = displacement_image
|
||||
depth_texture.interpolation = "Linear"
|
||||
depth_texture.extension = "EXTEND"
|
||||
if frame_count > 1 or is_animated_image(displacement_image):
|
||||
configure_image_user(
|
||||
depth_texture.image_user,
|
||||
depth_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=depth_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
depth_texture.location = (-1000.0, -400.0)
|
||||
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
|
||||
displacement_height = depth_texture.outputs["Color"]
|
||||
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
|
||||
depth_adjust = nodes.new("ShaderNodeMath")
|
||||
depth_adjust.operation = "SUBTRACT"
|
||||
depth_adjust.inputs[1].default_value = displacement_midlevel
|
||||
depth_adjust.location = (500.0, -550.0)
|
||||
depth_scale_node = nodes.new("ShaderNodeMath")
|
||||
depth_scale_node.operation = "MULTIPLY"
|
||||
depth_scale_node.inputs[1].default_value = displacement_scale
|
||||
depth_scale_node.location = (700.0, -550.0)
|
||||
links.new(displacement_height, depth_adjust.inputs[0])
|
||||
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
|
||||
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
|
||||
else:
|
||||
links.new(displacement_height, material_inputs.inputs["Depth"])
|
||||
links.new(
|
||||
material_inputs.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
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import bpy
|
||||
"""Shared Blender Object and Geometry Nodes Modifier helpers."""
|
||||
|
||||
from .image_data import (
|
||||
configure_image_user,
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
is_animated_image,
|
||||
)
|
||||
from .node import load_node_group
|
||||
import bpy
|
||||
|
||||
|
||||
def modifier_input_identifier(node_group, name):
|
||||
@@ -30,265 +24,15 @@ def set_modifier_input(modifier, identifier, value):
|
||||
"""Assign a value to a Geometry Nodes modifier input socket."""
|
||||
inputs = _modifier_input_slots(modifier)
|
||||
if inputs is not None:
|
||||
# Blender 5.2+ exposes inputs as runtime RNA keyed by socket identifier.
|
||||
input_slot = getattr(inputs, identifier)
|
||||
value_property = input_slot.bl_rna.properties["value"]
|
||||
if value_property.type == "ENUM" and type(value) is int:
|
||||
value = value_property.enum_items[value].identifier
|
||||
input_slot.value = value
|
||||
else:
|
||||
# Blender 4.x exposes inputs as modifier ID properties.
|
||||
modifier[identifier] = value
|
||||
|
||||
|
||||
DISPLACEMENT_STRENGTH = 1.0
|
||||
DISPLACEMENT_MIDLEVEL = 0.0
|
||||
IMAGE_MATERIAL_NODE_GROUP_NAME = "O Image Layer"
|
||||
DEPTH_MATERIAL_NODE_GROUP_NAME = "O Image Depth Layer"
|
||||
SHADELESS_NODE_GROUP_NAME = "O Shadeless"
|
||||
VIEW_TRANSFORM_COLOR_SPACES = (
|
||||
("ACES 1.3", "ACES 1.3 sRGB"),
|
||||
("ACES 2.0", "ACES 2.0 sRGB"),
|
||||
("AgX", "AgX Base sRGB"),
|
||||
("Filmic", "Filmic sRGB"),
|
||||
("Khronos PBR Neutral", "Khronos PBR Neutral sRGB"),
|
||||
)
|
||||
|
||||
|
||||
def material_color_space_name(scene):
|
||||
view_settings = getattr(scene, "view_settings", None)
|
||||
view_transform = getattr(view_settings, "view_transform", "")
|
||||
normalized = str(view_transform).strip().casefold()
|
||||
for view_name, color_space in VIEW_TRANSFORM_COLOR_SPACES:
|
||||
if normalized.startswith(view_name.casefold()):
|
||||
return color_space
|
||||
return "sRGB"
|
||||
|
||||
|
||||
def set_material_color_space(image, scene=None):
|
||||
if scene is None:
|
||||
scene = getattr(bpy.context, "scene", None)
|
||||
settings = image.colorspace_settings
|
||||
preferred = material_color_space_name(scene)
|
||||
original = settings.name
|
||||
pixels = None
|
||||
if preferred != original and (
|
||||
getattr(image, "source", None) == "GENERATED"
|
||||
or getattr(image, "is_dirty", False)
|
||||
):
|
||||
pixels = image_pixels(image)
|
||||
for color_space in dict.fromkeys((preferred, "sRGB", original)):
|
||||
if not color_space:
|
||||
continue
|
||||
try:
|
||||
settings.name = color_space
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if settings.name == color_space:
|
||||
if pixels is not None:
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
return color_space
|
||||
return settings.name
|
||||
|
||||
|
||||
def material_node_group(depth_plane=False):
|
||||
group_name = (
|
||||
DEPTH_MATERIAL_NODE_GROUP_NAME
|
||||
if depth_plane
|
||||
else IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
)
|
||||
return load_node_group(group_name)
|
||||
|
||||
|
||||
def create_image_material(
|
||||
source_object,
|
||||
color_image,
|
||||
depth_scale,
|
||||
displacement_image=None,
|
||||
displacement_midlevel=0.0,
|
||||
displacement_scale=1.0,
|
||||
shadeless=False,
|
||||
frame_count=1,
|
||||
frame_start=1,
|
||||
color_frame_offset=0,
|
||||
color_frame_duration=None,
|
||||
depth_frame_offset=0,
|
||||
depth_frame_duration=None,
|
||||
use_cyclic=None,
|
||||
normal_image=None,
|
||||
normal_space="TANGENT",
|
||||
ior=None,
|
||||
scene=None,
|
||||
):
|
||||
material = bpy.data.materials.new(image_base_name(source_object.data))
|
||||
material.use_nodes = True
|
||||
if hasattr(material, "surface_render_method"):
|
||||
material.surface_render_method = "DITHERED"
|
||||
nodes = material.node_tree.nodes
|
||||
nodes.clear()
|
||||
|
||||
color_texture = nodes.new("ShaderNodeTexImage")
|
||||
uv_map = nodes.new("ShaderNodeUVMap")
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
|
||||
uv_map.uv_map = "UVMap"
|
||||
uv_map.location = (-1250.0, 350.0)
|
||||
material_image = color_image
|
||||
set_material_color_space(material_image, scene)
|
||||
color_texture.image = material_image
|
||||
color_texture.interpolation = "Linear"
|
||||
color_texture.extension = "EXTEND"
|
||||
color_texture.location = (-1000.0, 350.0)
|
||||
output.location = (850.0, 350.0)
|
||||
extracted_frames = frame_count if frame_count > 1 else None
|
||||
color_frames = (
|
||||
color_frame_duration if color_frame_duration is not None else extracted_frames
|
||||
)
|
||||
depth_frames = (
|
||||
depth_frame_duration if depth_frame_duration is not None else extracted_frames
|
||||
)
|
||||
material_frames = color_frames
|
||||
material_frame_offset = color_frame_offset
|
||||
if frame_count > 1 or is_animated_image(material_image):
|
||||
configure_image_user(
|
||||
color_texture.image_user,
|
||||
material_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=material_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
|
||||
links = material.node_tree.links
|
||||
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
|
||||
alpha = color_texture.outputs["Alpha"]
|
||||
material_inputs = nodes.new("ShaderNodeGroup")
|
||||
is_depth_plane = displacement_image is not None
|
||||
material_inputs.node_tree = material_node_group(is_depth_plane)
|
||||
material_inputs.location = (-300.0, 350.0)
|
||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||
if "Object Space" in material_inputs.inputs:
|
||||
if normal_space not in {"TANGENT", "OBJECT"}:
|
||||
raise ValueError(f"Unsupported normal space: {normal_space}")
|
||||
material_inputs.inputs["Object Space"].default_value = normal_space == "OBJECT"
|
||||
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
||||
links.new(alpha, material_inputs.inputs["Alpha"])
|
||||
if normal_image is not None and "Normal" in material_inputs.inputs:
|
||||
normal_texture = nodes.new("ShaderNodeTexImage")
|
||||
normal_texture.image = normal_image
|
||||
normal_texture.interpolation = "Linear"
|
||||
normal_texture.extension = "EXTEND"
|
||||
normal_texture.location = (-1000.0, -50.0)
|
||||
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
||||
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
||||
if shadeless:
|
||||
shader = nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
|
||||
shader.name = SHADELESS_NODE_GROUP_NAME
|
||||
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix_shader = nodes.new("ShaderNodeMixShader")
|
||||
shader.location = (0.0, 550.0)
|
||||
transparent.location = (0.0, 150.0)
|
||||
mix_shader.location = (300.0, 350.0)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Alpha"],
|
||||
mix_shader.inputs["Fac"],
|
||||
)
|
||||
links.new(
|
||||
transparent.outputs["BSDF"],
|
||||
mix_shader.inputs[1],
|
||||
)
|
||||
links.new(
|
||||
shader.outputs["Shader"],
|
||||
mix_shader.inputs[2],
|
||||
)
|
||||
links.new(
|
||||
mix_shader.outputs["Shader"],
|
||||
output.inputs["Surface"],
|
||||
)
|
||||
else:
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.location = (250.0, 400.0)
|
||||
if ior is not None:
|
||||
shader.inputs["IOR"].default_value = float(ior)
|
||||
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Base Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Subsurface Radius"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Emission Color"],
|
||||
)
|
||||
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
|
||||
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
|
||||
links.new(
|
||||
material_inputs.outputs["Normal"],
|
||||
shader.inputs["Coat Normal"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Roughness"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
depth_texture = nodes.new("ShaderNodeTexImage")
|
||||
depth_texture.image = displacement_image
|
||||
depth_texture.interpolation = "Linear"
|
||||
depth_texture.extension = "EXTEND"
|
||||
if frame_count > 1 or is_animated_image(displacement_image):
|
||||
configure_image_user(
|
||||
depth_texture.image_user,
|
||||
depth_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=depth_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
depth_texture.location = (-1000.0, -400.0)
|
||||
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
|
||||
displacement_height = depth_texture.outputs["Color"]
|
||||
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
|
||||
depth_adjust = nodes.new("ShaderNodeMath")
|
||||
depth_adjust.operation = "SUBTRACT"
|
||||
depth_adjust.inputs[1].default_value = displacement_midlevel
|
||||
depth_adjust.location = (500.0, -550.0)
|
||||
depth_scale_node = nodes.new("ShaderNodeMath")
|
||||
depth_scale_node.operation = "MULTIPLY"
|
||||
depth_scale_node.inputs[1].default_value = displacement_scale
|
||||
depth_scale_node.location = (700.0, -550.0)
|
||||
links.new(displacement_height, depth_adjust.inputs[0])
|
||||
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
|
||||
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
|
||||
else:
|
||||
links.new(displacement_height, material_inputs.inputs["Depth"])
|
||||
links.new(
|
||||
material_inputs.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 finalize_object_result(
|
||||
context,
|
||||
source_object,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Selection values, serialization, rasterization, and Alpha cropping."""
|
||||
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
@@ -8,7 +10,7 @@ class ImageEditWarning(ValueError):
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegionPath:
|
||||
class SelectionPath:
|
||||
points: tuple[tuple[float, float], ...]
|
||||
invert: bool = False
|
||||
fill_rule: str = "EVEN_ODD"
|
||||
@@ -18,9 +20,9 @@ class RegionPath:
|
||||
(float(point[0]), float(point[1]))
|
||||
for point in self.points
|
||||
)
|
||||
region_path_bounds(points)
|
||||
selection_path_bounds(points)
|
||||
if self.fill_rule not in {"EVEN_ODD", "NONZERO"}:
|
||||
raise ValueError(f"Unknown Region Path fill rule: {self.fill_rule}")
|
||||
raise ValueError(f"Unknown Selection Path fill rule: {self.fill_rule}")
|
||||
object.__setattr__(self, "points", points)
|
||||
object.__setattr__(self, "invert", bool(self.invert))
|
||||
|
||||
@@ -38,7 +40,7 @@ class RegionPath:
|
||||
def from_json(cls, value):
|
||||
data = json.loads(value)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("The Region Path data is invalid")
|
||||
raise ValueError("The Selection Path data is invalid")
|
||||
return cls(
|
||||
points=tuple(data.get("points", ())),
|
||||
invert=bool(data.get("invert", False)),
|
||||
@@ -47,7 +49,7 @@ class RegionPath:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegionMask:
|
||||
class SelectionMask:
|
||||
values: object
|
||||
bounds: tuple[int, int, int, int]
|
||||
|
||||
@@ -57,16 +59,16 @@ class RegionMask:
|
||||
values = np.asarray(self.values, dtype=np.float32)
|
||||
bounds = tuple(int(value) for value in self.bounds)
|
||||
if values.ndim != 2:
|
||||
raise ValueError("The region mask must be two-dimensional")
|
||||
raise ValueError("The selection mask must be two-dimensional")
|
||||
if len(bounds) != 4:
|
||||
raise ValueError("The region bounds are invalid")
|
||||
raise ValueError("The selection bounds are invalid")
|
||||
left, top, right, bottom = bounds
|
||||
if right <= left or bottom <= top:
|
||||
raise ValueError("The region bounds are empty")
|
||||
raise ValueError("The selection bounds are empty")
|
||||
if values.shape != (bottom - top, right - left):
|
||||
raise ValueError("The region mask does not match its bounds")
|
||||
raise ValueError("The selection mask does not match its bounds")
|
||||
if not np.isfinite(values).all():
|
||||
raise ValueError("The region mask contains invalid values")
|
||||
raise ValueError("The selection mask contains invalid values")
|
||||
object.__setattr__(self, "values", np.clip(values, 0.0, 1.0))
|
||||
object.__setattr__(self, "bounds", bounds)
|
||||
|
||||
@@ -89,20 +91,20 @@ class RegionMask:
|
||||
return result
|
||||
|
||||
|
||||
def region_path_bounds(points):
|
||||
def selection_path_bounds(points):
|
||||
import numpy as np
|
||||
|
||||
points = np.asarray(points, dtype=np.float64)
|
||||
if points.ndim != 2 or points.shape[0] < 3 or points.shape[1] != 2:
|
||||
raise ValueError("The region does not define a valid image path")
|
||||
raise ValueError("The selection does not define a valid image path")
|
||||
if not np.isfinite(points).all():
|
||||
raise ValueError("The region contains invalid image coordinates")
|
||||
raise ValueError("The selection contains invalid image coordinates")
|
||||
left = int(math.floor(float(points[:, 0].min())))
|
||||
top = int(math.floor(float(points[:, 1].min())))
|
||||
right = int(math.ceil(float(points[:, 0].max())))
|
||||
bottom = int(math.ceil(float(points[:, 1].max())))
|
||||
if right <= left or bottom <= top:
|
||||
raise ValueError("The region does not define a valid image area")
|
||||
raise ValueError("The selection does not define a valid image area")
|
||||
return left, top, right, bottom
|
||||
|
||||
|
||||
@@ -112,7 +114,7 @@ def _rasterize_path(image_size, points, fill_rule):
|
||||
width, height = (int(value) for value in image_size)
|
||||
if width <= 0 or height <= 0:
|
||||
raise ValueError("The source image has no readable dimensions")
|
||||
left, top, right, bottom = region_path_bounds(points)
|
||||
left, top, right, bottom = selection_path_bounds(points)
|
||||
left = max(left, 0)
|
||||
top = max(top, 0)
|
||||
right = min(right, width)
|
||||
@@ -164,9 +166,9 @@ def _antialias_mask(mask):
|
||||
return result
|
||||
|
||||
|
||||
def rasterize_region_path(
|
||||
def rasterize_selection_path(
|
||||
image_size,
|
||||
region_path,
|
||||
selection_path,
|
||||
*,
|
||||
source_alpha=None,
|
||||
alpha_threshold=0.0,
|
||||
@@ -177,23 +179,23 @@ def rasterize_region_path(
|
||||
width, height = (int(value) for value in image_size)
|
||||
mask = _rasterize_path(
|
||||
(width, height),
|
||||
region_path.points,
|
||||
region_path.fill_rule,
|
||||
selection_path.points,
|
||||
selection_path.fill_rule,
|
||||
)
|
||||
if antialias:
|
||||
mask = _antialias_mask(mask)
|
||||
else:
|
||||
mask = mask.astype(np.float32)
|
||||
if region_path.invert:
|
||||
if selection_path.invert:
|
||||
mask = 1.0 - mask
|
||||
if source_alpha is not None:
|
||||
alpha = np.asarray(source_alpha)
|
||||
if alpha.shape != (height, width):
|
||||
raise ValueError("The source Alpha does not match the image region")
|
||||
raise ValueError("The source Alpha does not match the image selection")
|
||||
mask = np.where(alpha > float(alpha_threshold), mask, 0.0)
|
||||
visible_y, visible_x = np.nonzero(mask > float(alpha_threshold))
|
||||
if not len(visible_x):
|
||||
raise ImageEditWarning("The region contains no visible image pixels")
|
||||
raise ImageEditWarning("The selection contains no visible image pixels")
|
||||
bounds = (
|
||||
int(visible_x.min()),
|
||||
int(visible_y.min()),
|
||||
@@ -201,7 +203,7 @@ def rasterize_region_path(
|
||||
int(visible_y.max()) + 1,
|
||||
)
|
||||
left, top, right, bottom = bounds
|
||||
return RegionMask(mask[top:bottom, left:right], bounds)
|
||||
return SelectionMask(mask[top:bottom, left:right], bounds)
|
||||
|
||||
|
||||
def crop_pixels_to_alpha(pixels, bounds):
|
||||
@@ -1,16 +1,19 @@
|
||||
"""View3D projection, gestures, tool settings, and overlay drawing."""
|
||||
|
||||
import json
|
||||
import math
|
||||
|
||||
import bpy
|
||||
|
||||
from ..properties import ai_ready, ai_setup_label, ai_status
|
||||
from .image_data import (
|
||||
from ..properties import ai_ready
|
||||
from .ai import draw_ai_property, draw_ai_setup
|
||||
from .image import (
|
||||
image_empty_bounds,
|
||||
is_image_empty,
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from .image_region import ImageEditWarning, RegionPath
|
||||
from .selection import ImageEditWarning, SelectionPath
|
||||
|
||||
MIN_PATH_DISTANCE = 2.0
|
||||
|
||||
@@ -148,46 +151,29 @@ def active_view3d_tool_id(context):
|
||||
return getattr(tool, "idname", None)
|
||||
|
||||
|
||||
def refine_region_setting_name(operator):
|
||||
setting_names = getattr(operator, "refine_region_settings", {})
|
||||
def refine_selection_setting_name(operator):
|
||||
setting_names = getattr(operator, "refine_selection_settings", {})
|
||||
return setting_names.get(operator.gesture)
|
||||
|
||||
|
||||
def apply_region_settings(operator, context):
|
||||
def apply_selection_settings(operator, context):
|
||||
settings = context.scene.anyimage_settings
|
||||
setting_name = refine_region_setting_name(operator)
|
||||
operator.refine_region = (
|
||||
setting_name = refine_selection_setting_name(operator)
|
||||
operator.refine_selection = (
|
||||
setting_name is not None and ai_ready() and getattr(settings, setting_name)
|
||||
)
|
||||
|
||||
|
||||
def draw_ai_setup(layout):
|
||||
status = ai_status()
|
||||
if not status["ready"]:
|
||||
layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
return status
|
||||
|
||||
|
||||
def draw_ai_property(layout, owner, name, status):
|
||||
row = layout.row()
|
||||
row.enabled = status["ready"]
|
||||
row.prop(owner, name)
|
||||
|
||||
|
||||
def draw_region_operator_settings(layout, operator):
|
||||
def draw_selection_operator_settings(layout, operator):
|
||||
if operator.gesture not in {"BOX", "LASSO"}:
|
||||
layout.prop(operator, "invert")
|
||||
return
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, operator, "refine_region", status)
|
||||
draw_ai_property(layout, operator, "refine_selection", status)
|
||||
layout.prop(operator, "invert")
|
||||
|
||||
|
||||
def draw_region_settings(context, layout, setting_name):
|
||||
def draw_selection_settings(context, layout, setting_name):
|
||||
settings = context.scene.anyimage_settings
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, settings, setting_name, status)
|
||||
@@ -621,7 +607,7 @@ def image_line_half_plane_polygon(line, image_size):
|
||||
return line_half_plane_polygon(line[1], line[0], image_size)
|
||||
|
||||
|
||||
class ImageRegionGesture:
|
||||
class ImageSelectionGesture:
|
||||
active_tool_id = None
|
||||
preview_fill_rule = "EVEN_ODD"
|
||||
_path = None
|
||||
@@ -636,7 +622,7 @@ class ImageRegionGesture:
|
||||
_region_size = (0, 0)
|
||||
|
||||
def draw(self, _context):
|
||||
draw_region_operator_settings(self.layout, self)
|
||||
draw_selection_operator_settings(self.layout, self)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -656,7 +642,7 @@ class ImageRegionGesture:
|
||||
vertices.append(self._cursor)
|
||||
return tuple(vertices)
|
||||
|
||||
def _draw_region(self):
|
||||
def _draw_selection(self):
|
||||
if not self._path or not drawing_in_region(
|
||||
self._area_pointer, self._region_pointer
|
||||
):
|
||||
@@ -750,11 +736,11 @@ class ImageRegionGesture:
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
self.report({"WARNING"}, "Image tools only support a still image")
|
||||
return {"CANCELLED"}
|
||||
apply_region_settings(self, context)
|
||||
apply_selection_settings(self, context)
|
||||
self.invert = False
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
self.region_path_json = ""
|
||||
self.selection_path_json = ""
|
||||
point = (float(event.mouse_region_x), float(event.mouse_region_y))
|
||||
self._path = [point]
|
||||
self._cursor = point
|
||||
@@ -766,7 +752,7 @@ class ImageRegionGesture:
|
||||
self._region_pointer = context.region.as_pointer()
|
||||
self._region_size = (context.region.width, context.region.height)
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self._draw_region, (), "WINDOW", "POST_PIXEL"
|
||||
self._draw_selection, (), "WINDOW", "POST_PIXEL"
|
||||
)
|
||||
self._set_status(context)
|
||||
context.window_manager.modal_handler_add(self)
|
||||
@@ -785,7 +771,7 @@ class ImageRegionGesture:
|
||||
image_empty_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
region_path = image_line_half_plane_polygon(
|
||||
selection_path = image_line_half_plane_polygon(
|
||||
line, tuple(source_object.data.size)
|
||||
)
|
||||
else:
|
||||
@@ -793,7 +779,7 @@ class ImageRegionGesture:
|
||||
if len(path) < 3:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
region_path = screen_path_to_image_pixels(
|
||||
selection_path = screen_path_to_image_pixels(
|
||||
path,
|
||||
context.region,
|
||||
context.region_data,
|
||||
@@ -801,8 +787,8 @@ class ImageRegionGesture:
|
||||
image_empty_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
self.region_path_json = RegionPath(
|
||||
points=region_path,
|
||||
self.selection_path_json = SelectionPath(
|
||||
points=selection_path,
|
||||
invert=self.invert,
|
||||
fill_rule=self.preview_fill_rule,
|
||||
).to_json()
|
||||
Reference in New Issue
Block a user