- 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.
241 lines
7.3 KiB
Python
241 lines
7.3 KiB
Python
import bpy
|
|
|
|
from ...properties import (
|
|
refine_selection_property,
|
|
)
|
|
from ..common.image_data import image_rgba, require_image_empty, video_total_frames
|
|
from ..common.image_interaction import (
|
|
apply_region_settings,
|
|
draw_region_settings,
|
|
report_image_edit_exception,
|
|
resolve_image_edit_click,
|
|
)
|
|
from ..common.image_interaction import serialize_matrix
|
|
from ..common.image_interaction import ImageRegionGesture, gesture_property
|
|
from ..common.image_region import (
|
|
ImageEditWarning,
|
|
RegionPath,
|
|
rasterize_region_path,
|
|
)
|
|
|
|
CUTOUT_DETAIL_MIN = 4
|
|
CUTOUT_DETAIL_MAX = 1024
|
|
|
|
|
|
def _validate_cutout_region(
|
|
source_object,
|
|
region_path,
|
|
):
|
|
image = source_object.data
|
|
rgba = image_rgba(image)
|
|
rasterize_region_path(
|
|
tuple(image.size),
|
|
region_path,
|
|
source_alpha=rgba[:, :, 3],
|
|
alpha_threshold=0.1,
|
|
antialias=True,
|
|
)
|
|
|
|
|
|
def cutout_mesh_detail(image_size, point_spacing):
|
|
longest_edge = max(int(value) for value in image_size)
|
|
detail = round(longest_edge / max(int(point_spacing), 1))
|
|
return max(CUTOUT_DETAIL_MIN, min(CUTOUT_DETAIL_MAX, detail))
|
|
|
|
|
|
def _pie_button(
|
|
pie,
|
|
shape,
|
|
label,
|
|
icon,
|
|
source_name,
|
|
region_path_json,
|
|
refine_selection,
|
|
normal_mode,
|
|
mesh_detail,
|
|
):
|
|
operator = pie.operator(
|
|
"anyimage.cutout_region_to_shape",
|
|
text=label,
|
|
icon=icon,
|
|
)
|
|
operator.source_object_name = source_name
|
|
operator.region_path_json = region_path_json
|
|
operator.shape = shape
|
|
operator.refine_selection = refine_selection
|
|
operator.normal_mode = normal_mode
|
|
operator.mesh_detail = mesh_detail
|
|
|
|
|
|
def _open_shape_pie(
|
|
context,
|
|
event,
|
|
source_object,
|
|
region_path,
|
|
refine_selection=False,
|
|
):
|
|
source_name = source_object.name
|
|
region_path_json = region_path.to_json()
|
|
settings = context.scene.anyimage_settings
|
|
point_spacing = settings.cutout_point_spacing
|
|
normal_mode = str(getattr(settings, "cutout_normal_mode", "NONE"))
|
|
mesh_detail = cutout_mesh_detail(source_object.data.size, point_spacing)
|
|
|
|
def draw(menu, _context):
|
|
pie = menu.layout.menu_pie()
|
|
_pie_button(
|
|
pie,
|
|
"SURFACE",
|
|
"Surface",
|
|
"MESH_GRID",
|
|
source_name,
|
|
region_path_json,
|
|
refine_selection,
|
|
normal_mode,
|
|
mesh_detail,
|
|
)
|
|
_pie_button(
|
|
pie,
|
|
"BALLOON",
|
|
"Balloon",
|
|
"MESH_UVSPHERE",
|
|
source_name,
|
|
region_path_json,
|
|
refine_selection,
|
|
normal_mode,
|
|
mesh_detail,
|
|
)
|
|
_pie_button(
|
|
pie,
|
|
"DEPTH_BALLOON",
|
|
"Depth Balloon",
|
|
"META_BALL",
|
|
source_name,
|
|
region_path_json,
|
|
refine_selection,
|
|
normal_mode,
|
|
mesh_detail,
|
|
)
|
|
_pie_button(
|
|
pie,
|
|
"DEPTH_SURFACE",
|
|
"Depth Surface",
|
|
"SURFACE_NCURVE",
|
|
source_name,
|
|
region_path_json,
|
|
refine_selection,
|
|
normal_mode,
|
|
mesh_detail,
|
|
)
|
|
|
|
context.window_manager.popup_menu_pie(
|
|
event,
|
|
draw,
|
|
title="Cutout Shape",
|
|
icon="MESH_DATA",
|
|
)
|
|
|
|
|
|
class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
|
|
bl_idname = "anyimage.select_cutout_region"
|
|
bl_label = "Select Cutout Region"
|
|
bl_description = "Lasso an Image Empty region, then choose its mesh shape"
|
|
bl_options = {"INTERNAL"}
|
|
active_tool_id = "anyimage.cutout_lasso"
|
|
|
|
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
|
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
|
region_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
|
refine_selection: refine_selection_property()
|
|
invert: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
|
gesture: gesture_property()
|
|
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
|
|
|
preview_fill_rule = 'NONZERO'
|
|
_pie_event = None
|
|
|
|
def _set_status(self, context):
|
|
invert = "On" if self.invert else "Off"
|
|
context.workspace.status_text_set(
|
|
f"Cutout Lasso: drag and release for shapes; "
|
|
f"F toggles Invert ({invert}); RMB or Esc cancels"
|
|
)
|
|
|
|
def invoke(self, context, event):
|
|
self._pie_event = event
|
|
if not self.full_image and event.value != "DOUBLE_CLICK":
|
|
return super().invoke(context, event)
|
|
source_object, should_edit = resolve_image_edit_click(
|
|
context, event, self.report
|
|
)
|
|
if not should_edit:
|
|
return {"FINISHED"} if source_object is not None else {"CANCELLED"}
|
|
if source_object is None:
|
|
return {"CANCELLED"}
|
|
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)
|
|
self.invert = False
|
|
self.source_object_name = source_object.name
|
|
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
|
width, height = (int(value) for value in source_object.data.size)
|
|
self.region_path_json = RegionPath(
|
|
points=((0, 0), (width, 0), (width, height), (0, height)),
|
|
fill_rule="NONZERO",
|
|
).to_json()
|
|
return self.execute(context)
|
|
|
|
def modal(self, context, event):
|
|
self._pie_event = event
|
|
return super().modal(context, event)
|
|
|
|
def execute(self, context):
|
|
try:
|
|
source_object = require_image_empty(self.source_object_name)
|
|
except RuntimeError as error:
|
|
self.report({"ERROR"}, str(error))
|
|
return {"CANCELLED"}
|
|
try:
|
|
region_path = RegionPath.from_json(self.region_path_json)
|
|
_validate_cutout_region(source_object, region_path)
|
|
except (ImageEditWarning, TypeError, ValueError) as error:
|
|
report_image_edit_exception(self, error)
|
|
return {"CANCELLED"}
|
|
_open_shape_pie(
|
|
context,
|
|
self._pie_event,
|
|
source_object,
|
|
region_path,
|
|
self.refine_selection,
|
|
)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class CutoutTool(bpy.types.WorkSpaceTool):
|
|
bl_space_type = "VIEW_3D"
|
|
bl_context_mode = "OBJECT"
|
|
bl_idname = "anyimage.cutout_lasso"
|
|
bl_label = "Cutout"
|
|
bl_description = "Lasso an Image Empty and choose a mesh shape"
|
|
bl_icon = "ops.sculpt.lasso_mask"
|
|
bl_operator = SelectCutoutRegion.bl_idname
|
|
bl_keymap = (
|
|
(
|
|
SelectCutoutRegion.bl_idname,
|
|
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
|
|
{"properties": [("gesture", "LASSO"), ("full_image", True)]},
|
|
),
|
|
(
|
|
SelectCutoutRegion.bl_idname,
|
|
{"type": "LEFTMOUSE", "value": "PRESS"},
|
|
{"properties": [("gesture", "LASSO")]},
|
|
),
|
|
)
|
|
|
|
def draw_settings(context, layout, _tool):
|
|
settings = context.scene.anyimage_settings
|
|
layout.prop(settings, "cutout_point_spacing")
|
|
layout.prop(settings, "cutout_normal_mode")
|
|
draw_region_settings(context, layout, "cutout_refine_selection")
|