feat: Update version to 0.13.0 in project files and adjust dependencies
This commit is contained in:
@@ -5,13 +5,12 @@ from .operators.clipboard_image import CLASSES as CLIPBOARD_CLASSES
|
||||
from .menu import AnyImageImageMenu, draw_image_context_menu
|
||||
from .operators import CLASSES as OPERATOR_CLASSES
|
||||
from .operators.cutout_tool import CutoutTool
|
||||
from .operators.image_tool import (
|
||||
ImageBoxTool,
|
||||
ImageLassoTool,
|
||||
ImageLineTool,
|
||||
ImagePolylineTool,
|
||||
from .operators.crop_tool import (
|
||||
CropBoxTool,
|
||||
CropLassoTool,
|
||||
CropPerspectiveTool,
|
||||
CropPolylineTool,
|
||||
)
|
||||
from .operators.perspective_tool import ImagePerspectiveTool
|
||||
from .panel import (
|
||||
Ben2RemovalDebugPanel,
|
||||
Da3OutputDebugPanel,
|
||||
@@ -43,28 +42,23 @@ CLASSES = (
|
||||
def register():
|
||||
for class_type in CLASSES:
|
||||
bpy.utils.register_class(class_type)
|
||||
bpy.utils.register_tool(ImageBoxTool, separator=True, group=True)
|
||||
bpy.utils.register_tool(CropBoxTool, separator=True, group=True)
|
||||
bpy.utils.register_tool(
|
||||
ImageLassoTool,
|
||||
after={ImageBoxTool.bl_idname},
|
||||
CropLassoTool,
|
||||
after={CropBoxTool.bl_idname},
|
||||
separator=False,
|
||||
)
|
||||
bpy.utils.register_tool(
|
||||
ImageLineTool,
|
||||
after={ImageLassoTool.bl_idname},
|
||||
CropPolylineTool,
|
||||
after={CropLassoTool.bl_idname},
|
||||
separator=False,
|
||||
)
|
||||
bpy.utils.register_tool(
|
||||
ImagePolylineTool,
|
||||
after={ImageLineTool.bl_idname},
|
||||
CropPerspectiveTool,
|
||||
after={CropPolylineTool.bl_idname},
|
||||
separator=False,
|
||||
)
|
||||
bpy.utils.register_tool(ImagePerspectiveTool, separator=False)
|
||||
bpy.utils.register_tool(
|
||||
CutoutTool,
|
||||
after={ImagePerspectiveTool.bl_idname},
|
||||
separator=True,
|
||||
)
|
||||
bpy.utils.register_tool(CutoutTool)
|
||||
bpy.types.Scene.anyimage_settings = bpy.props.PointerProperty(type=AnyImageSettings)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.prepend(draw_image_context_menu)
|
||||
bpy.types.OUTLINER_MT_object.prepend(draw_image_context_menu)
|
||||
@@ -76,11 +70,10 @@ def unregister():
|
||||
runtime.unregister()
|
||||
clipboard_image.unregister_keymaps()
|
||||
bpy.utils.unregister_tool(CutoutTool)
|
||||
bpy.utils.unregister_tool(ImagePerspectiveTool)
|
||||
bpy.utils.unregister_tool(ImagePolylineTool)
|
||||
bpy.utils.unregister_tool(ImageLineTool)
|
||||
bpy.utils.unregister_tool(ImageLassoTool)
|
||||
bpy.utils.unregister_tool(ImageBoxTool)
|
||||
bpy.utils.unregister_tool(CropPerspectiveTool)
|
||||
bpy.utils.unregister_tool(CropPolylineTool)
|
||||
bpy.utils.unregister_tool(CropLassoTool)
|
||||
bpy.utils.unregister_tool(CropBoxTool)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(draw_image_context_menu)
|
||||
bpy.types.OUTLINER_MT_object.remove(draw_image_context_menu)
|
||||
if hasattr(bpy.types.Scene, "anyimage_settings"):
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "anyimage"
|
||||
version = "0.12.15"
|
||||
version = "0.13.0"
|
||||
name = "AnyImage"
|
||||
tagline = "Build spatial structures from images, fast"
|
||||
maintainer = "OmooLab"
|
||||
|
||||
@@ -484,6 +484,46 @@ def replace_empty_image(source_object, result_image, *, placement_bounds=None):
|
||||
return result_image
|
||||
|
||||
|
||||
def place_empty_image_result(
|
||||
context,
|
||||
source_object,
|
||||
result_image,
|
||||
*,
|
||||
placement_bounds=None,
|
||||
keep_original=False,
|
||||
):
|
||||
"""Place one crop result and optionally preserve its source."""
|
||||
if not keep_original:
|
||||
replace_empty_image(
|
||||
source_object,
|
||||
result_image,
|
||||
placement_bounds=placement_bounds,
|
||||
)
|
||||
return source_object
|
||||
|
||||
result_object = source_object.copy()
|
||||
collections = tuple(source_object.users_collection)
|
||||
if not collections:
|
||||
collections = (context.collection,)
|
||||
try:
|
||||
for collection in collections:
|
||||
collection.objects.link(result_object)
|
||||
replace_empty_image(
|
||||
result_object,
|
||||
result_image,
|
||||
placement_bounds=placement_bounds,
|
||||
)
|
||||
except Exception:
|
||||
bpy.data.objects.remove(result_object, do_unlink=True)
|
||||
raise
|
||||
|
||||
for selected in context.selected_objects:
|
||||
selected.select_set(False)
|
||||
result_object.select_set(True)
|
||||
context.view_layer.objects.active = result_object
|
||||
return result_object
|
||||
|
||||
|
||||
|
||||
def load_color_result_image(
|
||||
source_object,
|
||||
|
||||
@@ -135,7 +135,7 @@ def resolve_image_edit_click(context, event, report):
|
||||
if is_image_empty(active_object):
|
||||
_restore_selection(context, active_object, selected_objects)
|
||||
return active_object, True
|
||||
report({"WARNING"}, "Click an Image Empty to use an Image tool")
|
||||
report({"WARNING"}, "Click an Image Empty to use a Crop tool")
|
||||
return None, False
|
||||
|
||||
|
||||
@@ -151,32 +151,50 @@ def active_view3d_tool_id(context):
|
||||
return getattr(tool, "idname", None)
|
||||
|
||||
|
||||
def refine_selection_setting_name(operator):
|
||||
setting_names = getattr(operator, "refine_selection_settings", {})
|
||||
def _crop_setting_name(operator, mapping_name):
|
||||
setting_names = getattr(operator, mapping_name, {})
|
||||
return setting_names.get(operator.gesture)
|
||||
|
||||
|
||||
def apply_selection_settings(operator, context):
|
||||
def apply_crop_settings(operator, context):
|
||||
settings = context.scene.anyimage_settings
|
||||
setting_name = refine_selection_setting_name(operator)
|
||||
setting_name = _crop_setting_name(operator, "refine_selection_settings")
|
||||
operator.refine_selection = (
|
||||
setting_name is not None and ai_ready() and getattr(settings, setting_name)
|
||||
)
|
||||
setting_name = _crop_setting_name(operator, "invert_settings")
|
||||
if setting_name is not None:
|
||||
operator.invert = bool(getattr(settings, setting_name))
|
||||
setting_name = _crop_setting_name(operator, "keep_original_settings")
|
||||
if setting_name is not None:
|
||||
operator.keep_original = bool(getattr(settings, setting_name))
|
||||
|
||||
|
||||
def draw_selection_operator_settings(layout, operator):
|
||||
if operator.gesture not in {"BOX", "LASSO"}:
|
||||
def draw_crop_operator_settings(layout, operator):
|
||||
if _crop_setting_name(operator, "refine_selection_settings") is not None:
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, operator, "refine_selection", status)
|
||||
if _crop_setting_name(operator, "invert_settings") is not None:
|
||||
layout.prop(operator, "invert")
|
||||
return
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, operator, "refine_selection", status)
|
||||
layout.prop(operator, "invert")
|
||||
if _crop_setting_name(operator, "keep_original_settings") is not None:
|
||||
layout.prop(operator, "keep_original")
|
||||
|
||||
|
||||
def draw_selection_settings(context, layout, setting_name):
|
||||
def draw_crop_settings(
|
||||
context,
|
||||
layout,
|
||||
*,
|
||||
keep_original,
|
||||
invert=None,
|
||||
refine_selection=None,
|
||||
):
|
||||
settings = context.scene.anyimage_settings
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, settings, setting_name, status)
|
||||
if refine_selection is not None:
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, settings, refine_selection, status)
|
||||
if invert is not None:
|
||||
layout.prop(settings, invert)
|
||||
layout.prop(settings, keep_original)
|
||||
|
||||
|
||||
def dashed_line_segments(vertices, dash_length=4.0, gap_length=4.0):
|
||||
@@ -475,13 +493,11 @@ def draw_polygon_fill(
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
GESTURE_TOOL_IDS = {
|
||||
"BOX": "anyimage.image_box",
|
||||
"LASSO": "anyimage.image_lasso",
|
||||
"LINE": "anyimage.image_line",
|
||||
"POLYLINE": "anyimage.image_polyline",
|
||||
CROP_TOOL_IDS = {
|
||||
"BOX": "anyimage.crop_box",
|
||||
"LASSO": "anyimage.crop_lasso",
|
||||
"POLYLINE": "anyimage.crop_polyline",
|
||||
}
|
||||
LINE_SNAP_RADIANS = math.radians(5.0)
|
||||
POLYLINE_CLOSE_RADIUS = 8.0
|
||||
LASSO_POINT_DISTANCE = 2.0
|
||||
LASSO_SIMPLIFY_TOLERANCE = 0.75
|
||||
@@ -528,7 +544,6 @@ def gesture_property():
|
||||
items=(
|
||||
("BOX", "Box", "Edit with a rectangular gesture"),
|
||||
("LASSO", "Lasso", "Edit with a freehand gesture"),
|
||||
("LINE", "Line", "Edit one side of a line"),
|
||||
("POLYLINE", "Polyline", "Edit with a clicked polygon"),
|
||||
),
|
||||
default="BOX",
|
||||
@@ -544,62 +559,7 @@ def box_screen_path(start, end):
|
||||
return ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
|
||||
|
||||
|
||||
def snapped_line_endpoint(start, end, snap):
|
||||
if not snap:
|
||||
return tuple(float(value) for value in end)
|
||||
delta_x = float(end[0]) - float(start[0])
|
||||
delta_y = float(end[1]) - float(start[1])
|
||||
length = math.hypot(delta_x, delta_y)
|
||||
if length <= 0.0:
|
||||
return tuple(float(value) for value in end)
|
||||
angle = round(math.atan2(delta_y, delta_x) / LINE_SNAP_RADIANS)
|
||||
angle *= LINE_SNAP_RADIANS
|
||||
return (
|
||||
float(start[0]) + math.cos(angle) * length,
|
||||
float(start[1]) + math.sin(angle) * length,
|
||||
)
|
||||
|
||||
|
||||
def line_half_plane_polygon(start, end, rectangle_size):
|
||||
"""Clip a rectangle to the left side of one directed line."""
|
||||
width, height = (float(value) for value in rectangle_size)
|
||||
if math.dist(start, end) < 2.0:
|
||||
return ()
|
||||
vertices = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)]
|
||||
|
||||
def side(point):
|
||||
return (end[0] - start[0]) * (point[1] - start[1]) - (end[1] - start[1]) * (
|
||||
point[0] - start[0]
|
||||
)
|
||||
|
||||
result = []
|
||||
previous = vertices[-1]
|
||||
previous_side = side(previous)
|
||||
for current in vertices:
|
||||
current_side = side(current)
|
||||
if (current_side >= 0.0) != (previous_side >= 0.0):
|
||||
factor = previous_side / (previous_side - current_side)
|
||||
result.append(
|
||||
(
|
||||
previous[0] + (current[0] - previous[0]) * factor,
|
||||
previous[1] + (current[1] - previous[1]) * factor,
|
||||
)
|
||||
)
|
||||
if current_side >= 0.0:
|
||||
result.append(current)
|
||||
previous = current
|
||||
previous_side = current_side
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def image_line_half_plane_polygon(line, image_size):
|
||||
"""Keep the image-space half-plane shown by the screen-space preview."""
|
||||
if len(line) != 2:
|
||||
raise ValueError("Drag a longer line across the image plane")
|
||||
return line_half_plane_polygon(line[1], line[0], image_size)
|
||||
|
||||
|
||||
class ImageSelectionGesture:
|
||||
class CropSelectionGesture:
|
||||
active_tool_id = None
|
||||
_path = None
|
||||
_cursor = None
|
||||
@@ -613,7 +573,7 @@ class ImageSelectionGesture:
|
||||
_region_size = (0, 0)
|
||||
|
||||
def draw(self, _context):
|
||||
draw_selection_operator_settings(self.layout, self)
|
||||
draw_crop_operator_settings(self.layout, self)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -624,10 +584,6 @@ class ImageSelectionGesture:
|
||||
return box_screen_path(self._path[0], self._cursor)
|
||||
if self.gesture == "LASSO":
|
||||
return tuple(self._path)
|
||||
if self.gesture == "LINE":
|
||||
return line_half_plane_polygon(
|
||||
self._path[0], self._cursor, self._region_size
|
||||
)
|
||||
vertices = list(self._path)
|
||||
if self._cursor is not None:
|
||||
vertices.append(self._cursor)
|
||||
@@ -641,10 +597,7 @@ class ImageSelectionGesture:
|
||||
polygon = self._screen_path()
|
||||
if polygon != self._outline_polygon:
|
||||
self._outline_polygon = polygon
|
||||
if self.gesture == "LINE":
|
||||
outline = (self._path[0], self._cursor)
|
||||
else:
|
||||
outline = (*polygon, polygon[0]) if len(polygon) > 1 else ()
|
||||
outline = (*polygon, polygon[0]) if len(polygon) > 1 else ()
|
||||
self._outline_segments = dashed_line_segments(outline)
|
||||
if len(polygon) >= 3:
|
||||
if polygon != self._fill_polygon:
|
||||
@@ -655,9 +608,6 @@ class ImageSelectionGesture:
|
||||
self._fill_triangles,
|
||||
(1.0, 0.25, 0.2, 0.16) if self.invert else (1.0, 1.0, 1.0, 0.16),
|
||||
)
|
||||
if self.gesture == "LINE":
|
||||
draw_dashed_line((), segments=self._outline_segments)
|
||||
return
|
||||
if len(polygon) > 1:
|
||||
draw_dashed_line((), segments=self._outline_segments)
|
||||
if (
|
||||
@@ -694,12 +644,10 @@ class ImageSelectionGesture:
|
||||
invert = "On" if self.invert else "Off"
|
||||
if self.gesture == "POLYLINE":
|
||||
action = "click points; click start or Enter to finish; Backspace removes"
|
||||
elif self.gesture == "LINE":
|
||||
action = "drag line; Ctrl snaps 5°; release to edit"
|
||||
else:
|
||||
action = "drag and release to edit"
|
||||
context.workspace.status_text_set(
|
||||
f"Image {self.gesture.title()}: {action}; F toggles Invert ({invert}); "
|
||||
f"Crop {self.gesture.title()}: {action}; Invert {invert}; "
|
||||
"RMB or Esc cancels"
|
||||
)
|
||||
|
||||
@@ -726,10 +674,9 @@ class ImageSelectionGesture:
|
||||
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")
|
||||
self.report({"WARNING"}, "Crop tools only support a still image")
|
||||
return {"CANCELLED"}
|
||||
apply_selection_settings(self, context)
|
||||
self.invert = False
|
||||
apply_crop_settings(self, context)
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
point = (float(event.mouse_region_x), float(event.mouse_region_y))
|
||||
@@ -753,31 +700,18 @@ class ImageSelectionGesture:
|
||||
def _complete(self, context):
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
matrix = deserialize_matrix(self.source_matrix_data)
|
||||
if self.gesture == "LINE":
|
||||
line = screen_path_to_image_pixels(
|
||||
(self._path[0], self._cursor),
|
||||
context.region,
|
||||
context.region_data,
|
||||
matrix,
|
||||
image_empty_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
selection_path = image_line_half_plane_polygon(
|
||||
line, tuple(source_object.data.size)
|
||||
)
|
||||
else:
|
||||
path = self._screen_path()
|
||||
if len(path) < 3:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
selection_path = screen_path_to_image_pixels(
|
||||
path,
|
||||
context.region,
|
||||
context.region_data,
|
||||
matrix,
|
||||
image_empty_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
path = self._screen_path()
|
||||
if len(path) < 3:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
selection_path = screen_path_to_image_pixels(
|
||||
path,
|
||||
context.region,
|
||||
context.region_data,
|
||||
matrix,
|
||||
image_empty_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
selection_path = SelectionPath(
|
||||
points=selection_path,
|
||||
invert=self.invert,
|
||||
@@ -787,18 +721,13 @@ class ImageSelectionGesture:
|
||||
|
||||
def modal(self, context, event):
|
||||
active_tool = active_view3d_tool_id(context)
|
||||
expected_tool = self.active_tool_id or GESTURE_TOOL_IDS[self.gesture]
|
||||
expected_tool = self.active_tool_id or CROP_TOOL_IDS[self.gesture]
|
||||
if active_tool is not None and active_tool != expected_tool:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
if event.type in {"ESC", "RIGHTMOUSE"} and event.value == "PRESS":
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
if event.type == "F" and event.value == "PRESS":
|
||||
self.invert = not self.invert
|
||||
self._set_status(context)
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
if self.gesture == "POLYLINE":
|
||||
return self._modal_polyline(context, event)
|
||||
if event.type == "MOUSEMOVE":
|
||||
@@ -806,8 +735,6 @@ class ImageSelectionGesture:
|
||||
if self.gesture == "LASSO":
|
||||
append_lasso_point(self._path, point)
|
||||
self._cursor = point
|
||||
elif self.gesture == "LINE":
|
||||
self._cursor = snapped_line_endpoint(self._path[0], point, event.ctrl)
|
||||
else:
|
||||
self._cursor = point
|
||||
context.area.tag_redraw()
|
||||
@@ -817,11 +744,6 @@ class ImageSelectionGesture:
|
||||
if self.gesture == "LASSO":
|
||||
append_lasso_point(self._path, point)
|
||||
self._cursor = point
|
||||
elif self.gesture == "LINE":
|
||||
self._cursor = snapped_line_endpoint(self._path[0], point, event.ctrl)
|
||||
if math.dist(self._path[0], self._cursor) < 2.0:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
self._cursor = point
|
||||
try:
|
||||
|
||||
@@ -4,13 +4,12 @@ from .cutout_tool import (
|
||||
SelectCutoutSelection,
|
||||
)
|
||||
from .debug import DebugBen2, DebugDa3, DebugMoge2, DebugUpscale
|
||||
from .image_tool import CLASSES as IMAGE_TOOL_CLASSES
|
||||
from .crop_tool import CLASSES as CROP_TOOL_CLASSES
|
||||
from .convert_to_plane import (
|
||||
ConvertToDepthPlane,
|
||||
ConvertToPlane,
|
||||
GenerateDepthPlane,
|
||||
)
|
||||
from .perspective_tool import CLASSES as PERSPECTIVE_CLASSES
|
||||
from .ai_setup import (
|
||||
ClearModels,
|
||||
DownloadModel,
|
||||
@@ -35,8 +34,7 @@ CLASSES = (
|
||||
SelectCutoutSelection,
|
||||
RunBackgroundRemoval,
|
||||
ClearModels,
|
||||
*IMAGE_TOOL_CLASSES,
|
||||
*PERSPECTIVE_CLASSES,
|
||||
*CROP_TOOL_CLASSES,
|
||||
RemoveImageBackground,
|
||||
UpscaleImage,
|
||||
ConvertToPlane,
|
||||
|
||||
21
src/anyimage/operators/crop_tool/__init__.py
Normal file
21
src/anyimage/operators/crop_tool/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from .box import CropBoxTool
|
||||
from .lasso import CropLassoTool
|
||||
from .perspective import CropImagePerspective, CropPerspectiveTool
|
||||
from .polyline import CropPolylineTool
|
||||
from .selection import ActivateCropTool, CropImageSelection, RefineCropSelection
|
||||
|
||||
|
||||
CLASSES = (
|
||||
CropImageSelection,
|
||||
RefineCropSelection,
|
||||
ActivateCropTool,
|
||||
CropImagePerspective,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"CLASSES",
|
||||
"CropBoxTool",
|
||||
"CropLassoTool",
|
||||
"CropPerspectiveTool",
|
||||
"CropPolylineTool",
|
||||
)
|
||||
30
src/anyimage/operators/crop_tool/box.py
Normal file
30
src/anyimage/operators/crop_tool/box.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import draw_crop_settings
|
||||
from .selection import CropImageSelection
|
||||
|
||||
|
||||
class CropBoxTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.crop_box"
|
||||
bl_label = "Crop Box"
|
||||
bl_description = "Crop an Image Empty with a rectangular selection"
|
||||
bl_icon = "ops.sculpt.border_mask"
|
||||
bl_operator = CropImageSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
CropImageSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "BOX")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_crop_settings(
|
||||
context,
|
||||
layout,
|
||||
refine_selection="crop_box_refine_selection",
|
||||
invert="crop_box_invert",
|
||||
keep_original="crop_box_keep_original",
|
||||
)
|
||||
30
src/anyimage/operators/crop_tool/lasso.py
Normal file
30
src/anyimage/operators/crop_tool/lasso.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import draw_crop_settings
|
||||
from .selection import CropImageSelection
|
||||
|
||||
|
||||
class CropLassoTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.crop_lasso"
|
||||
bl_label = "Crop Lasso"
|
||||
bl_description = "Crop an Image Empty with a freehand selection"
|
||||
bl_icon = "ops.sculpt.lasso_mask"
|
||||
bl_operator = CropImageSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
CropImageSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "LASSO")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_crop_settings(
|
||||
context,
|
||||
layout,
|
||||
refine_selection="crop_lasso_refine_selection",
|
||||
invert="crop_lasso_invert",
|
||||
keep_original="crop_lasso_keep_original",
|
||||
)
|
||||
@@ -4,8 +4,15 @@ import math
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import (
|
||||
active_view3d_tool_id,
|
||||
deserialize_matrix,
|
||||
draw_crop_settings,
|
||||
draw_dashed_line,
|
||||
draw_polygon_fill,
|
||||
drawing_in_region,
|
||||
image_edit_poll,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
screen_path_to_image_pixels,
|
||||
serialize_matrix,
|
||||
)
|
||||
@@ -15,25 +22,15 @@ from ...common.image import (
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
image_empty_bounds,
|
||||
replace_empty_image,
|
||||
place_empty_image_result,
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.viewport import (
|
||||
active_view3d_tool_id,
|
||||
draw_dashed_line,
|
||||
draw_polygon_fill,
|
||||
image_edit_poll,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
)
|
||||
|
||||
|
||||
MIN_QUAD_AREA = 16.0
|
||||
MAX_OUTPUT_DIMENSION = 8192
|
||||
MAX_OUTPUT_PIXELS = 16_777_216
|
||||
WARP_CHUNK_ROWS = 256
|
||||
IMAGE_PERSPECTIVE_TOOL_ID = "anyimage.image_perspective"
|
||||
CROP_PERSPECTIVE_TOOL_ID = "anyimage.crop_perspective"
|
||||
PREVIEW_TEXTURE_SIZE = 512
|
||||
ASPECT_PIXELS_PER_DOUBLE = 240.0
|
||||
MIN_INTERACTIVE_ASPECT = 0.05
|
||||
@@ -497,14 +494,18 @@ def extract_perspective_pixels(
|
||||
return pixels, cropped_size, placement_bounds
|
||||
|
||||
|
||||
class EditImagePerspective(bpy.types.Operator):
|
||||
bl_idname = "anyimage.edit_image_perspective"
|
||||
bl_label = "Edit Image Perspective"
|
||||
bl_description = "Deform four image corners into a rectangular image"
|
||||
class CropImagePerspective(bpy.types.Operator):
|
||||
bl_idname = "anyimage.crop_image_perspective"
|
||||
bl_label = "Crop Image Perspective"
|
||||
bl_description = "Crop four image corners into a rectangular image"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
keep_original: bpy.props.BoolProperty(
|
||||
default=False,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
quad_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
aspect_ratio: bpy.props.FloatProperty(
|
||||
default=1.0,
|
||||
@@ -576,14 +577,14 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
def _set_point_status(self, context):
|
||||
remaining = 4 - len(self._points)
|
||||
context.workspace.status_text_set(
|
||||
f"Image Perspective: click {remaining} more corner"
|
||||
f"Crop Perspective: click {remaining} more corner"
|
||||
f"{'s' if remaining != 1 else ''}; click order does not matter; "
|
||||
"Backspace removes, RMB or Esc cancels"
|
||||
)
|
||||
|
||||
def _set_aspect_status(self, context):
|
||||
context.workspace.status_text_set(
|
||||
f"Image Perspective Aspect {self.aspect_ratio:.3f}: "
|
||||
f"Crop Perspective Aspect {self.aspect_ratio:.3f}: "
|
||||
"move mouse horizontally, LMB confirms, Backspace returns, "
|
||||
"RMB or Esc cancels"
|
||||
)
|
||||
@@ -639,11 +640,14 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Image Perspective only supports a still image",
|
||||
"Crop Perspective only supports a still image",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
self.keep_original = (
|
||||
context.scene.anyimage_settings.crop_perspective_keep_original
|
||||
)
|
||||
self.quad_json = ""
|
||||
self._phase = "POINTS"
|
||||
self._preview_image = None
|
||||
@@ -665,7 +669,7 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
|
||||
def modal(self, context, event):
|
||||
active_tool = active_view3d_tool_id(context)
|
||||
if active_tool is not None and active_tool != IMAGE_PERSPECTIVE_TOOL_ID:
|
||||
if active_tool is not None and active_tool != CROP_PERSPECTIVE_TOOL_ID:
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
if event.type in {"ESC", "RIGHTMOUSE"} and event.value == "PRESS":
|
||||
@@ -802,7 +806,7 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
raise RuntimeError("Image Perspective only supports a still image")
|
||||
raise RuntimeError("Crop Perspective only supports a still image")
|
||||
quad = validate_perspective_quad(json.loads(self.quad_json))
|
||||
if not perspective_quad_overlaps_image(
|
||||
quad, tuple(source_object.data.size)
|
||||
@@ -830,10 +834,12 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
pixels,
|
||||
output_size,
|
||||
)
|
||||
replace_empty_image(
|
||||
place_empty_image_result(
|
||||
context,
|
||||
source_object,
|
||||
result_image,
|
||||
placement_bounds=placement_bounds,
|
||||
keep_original=bool(getattr(self, "keep_original", False)),
|
||||
)
|
||||
except (
|
||||
json.JSONDecodeError,
|
||||
@@ -847,18 +853,25 @@ class EditImagePerspective(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImagePerspectiveTool(bpy.types.WorkSpaceTool):
|
||||
class CropPerspectiveTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = IMAGE_PERSPECTIVE_TOOL_ID
|
||||
bl_label = "Image Perspective"
|
||||
bl_description = "Rectify four image corners on the source Image Empty plane"
|
||||
bl_idname = CROP_PERSPECTIVE_TOOL_ID
|
||||
bl_label = "Crop Perspective"
|
||||
bl_description = "Crop four image corners to a rectangular perspective"
|
||||
bl_icon = "ops.sculpt.line_project"
|
||||
bl_operator = EditImagePerspective.bl_idname
|
||||
bl_operator = CropImagePerspective.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
EditImagePerspective.bl_idname,
|
||||
CropImagePerspective.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_crop_settings(
|
||||
context,
|
||||
layout,
|
||||
keep_original="crop_perspective_keep_original",
|
||||
)
|
||||
29
src/anyimage/operators/crop_tool/polyline.py
Normal file
29
src/anyimage/operators/crop_tool/polyline.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import draw_crop_settings
|
||||
from .selection import CropImageSelection
|
||||
|
||||
|
||||
class CropPolylineTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.crop_polyline"
|
||||
bl_label = "Crop Polyline"
|
||||
bl_description = "Crop an Image Empty with a clicked polygon selection"
|
||||
bl_icon = "ops.sculpt.polyline_mask"
|
||||
bl_operator = CropImageSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
CropImageSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "POLYLINE")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_crop_settings(
|
||||
context,
|
||||
layout,
|
||||
invert="crop_polyline_invert",
|
||||
keep_original="crop_polyline_keep_original",
|
||||
)
|
||||
@@ -15,13 +15,13 @@ from ...common.image import (
|
||||
image_base_name,
|
||||
image_rgba,
|
||||
load_packed_rgba_image,
|
||||
place_empty_image_result,
|
||||
prepare_image_bounds_input,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.viewport import (
|
||||
ImageSelectionGesture,
|
||||
CropSelectionGesture,
|
||||
gesture_property,
|
||||
image_edit_poll,
|
||||
report_image_edit_exception,
|
||||
@@ -64,7 +64,14 @@ def _edit_image_mask_pixels(
|
||||
)
|
||||
|
||||
|
||||
def _edit_image_object(source_object, source_rgba, selection_mask):
|
||||
def _edit_image_object(
|
||||
context,
|
||||
source_object,
|
||||
source_rgba,
|
||||
selection_mask,
|
||||
*,
|
||||
keep_original=False,
|
||||
):
|
||||
image = source_object.data
|
||||
pixels, crop_bounds = _edit_image_mask_pixels(
|
||||
source_rgba,
|
||||
@@ -76,21 +83,33 @@ def _edit_image_object(source_object, source_rgba, selection_mask):
|
||||
pixels,
|
||||
(right - left, bottom - top),
|
||||
)
|
||||
return replace_empty_image(
|
||||
return place_empty_image_result(
|
||||
context,
|
||||
source_object,
|
||||
result_image,
|
||||
placement_bounds=crop_bounds,
|
||||
keep_original=keep_original,
|
||||
)
|
||||
|
||||
|
||||
class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
class CropImageSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
refine_selection_settings = {
|
||||
"BOX": "image_box_refine_selection",
|
||||
"LASSO": "image_lasso_refine_selection",
|
||||
"BOX": "crop_box_refine_selection",
|
||||
"LASSO": "crop_lasso_refine_selection",
|
||||
}
|
||||
bl_idname = "anyimage.edit_image_mask"
|
||||
bl_label = "Edit Image Mask"
|
||||
bl_description = "Edit and tightly crop an Image Empty with a gesture"
|
||||
invert_settings = {
|
||||
"BOX": "crop_box_invert",
|
||||
"LASSO": "crop_lasso_invert",
|
||||
"POLYLINE": "crop_polyline_invert",
|
||||
}
|
||||
keep_original_settings = {
|
||||
"BOX": "crop_box_keep_original",
|
||||
"LASSO": "crop_lasso_keep_original",
|
||||
"POLYLINE": "crop_polyline_keep_original",
|
||||
}
|
||||
bl_idname = "anyimage.crop_image_selection"
|
||||
bl_label = "Crop Image Selection"
|
||||
bl_description = "Crop an Image Empty with a selection"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
@@ -100,11 +119,13 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
name="Invert",
|
||||
description="Keep the opposite side of the refined gesture mask",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
options=_HIDDEN,
|
||||
)
|
||||
keep_original: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
gesture: gesture_property()
|
||||
|
||||
def _submit_selection_path(self, context, selection_path):
|
||||
keep_original = bool(getattr(self, "keep_original", False))
|
||||
try:
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
except RuntimeError as error:
|
||||
@@ -112,7 +133,7 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
raise RuntimeError("Image tools only support a still image")
|
||||
raise RuntimeError("Crop tools only support a still image")
|
||||
selection_mask = rasterize_selection_path(
|
||||
tuple(source_object.data.size),
|
||||
selection_path,
|
||||
@@ -124,19 +145,22 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
source_rgba,
|
||||
selection_mask.bounds,
|
||||
)
|
||||
result = bpy.ops.anyimage.refine_image_selection(
|
||||
result = bpy.ops.anyimage.refine_crop_selection(
|
||||
input_path=str(source_path),
|
||||
source_object_name=self.source_object_name,
|
||||
selection_bounds_json=json.dumps(selection_mask.bounds),
|
||||
keep_original=keep_original,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
cleanup_image_input(source_path, True)
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
_edit_image_object(
|
||||
context,
|
||||
source_object,
|
||||
source_rgba,
|
||||
selection_mask,
|
||||
keep_original=keep_original,
|
||||
)
|
||||
except (RuntimeError, TypeError, ValueError) as error:
|
||||
report_image_edit_exception(self, error)
|
||||
@@ -144,15 +168,16 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.refine_image_selection"
|
||||
bl_label = "Refine Image Selection"
|
||||
class RefineCropSelection(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.refine_crop_selection"
|
||||
bl_label = "Refine Crop Selection"
|
||||
job_type = "refine-image-selection"
|
||||
starting_message = "Refining image selection"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
selection_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
keep_original: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
|
||||
def request(self, _context):
|
||||
require_environment()
|
||||
@@ -163,7 +188,7 @@ class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
|
||||
"device": production_device().lower(),
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
def response(self, context, result):
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
|
||||
source_bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
@@ -183,27 +208,29 @@ class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
|
||||
image_base_name(source_image),
|
||||
)
|
||||
try:
|
||||
replace_empty_image(
|
||||
place_empty_image_result(
|
||||
context,
|
||||
source_object,
|
||||
refined_image,
|
||||
placement_bounds=bounds,
|
||||
keep_original=bool(getattr(self, "keep_original", False)),
|
||||
)
|
||||
except Exception:
|
||||
if refined_image.users == 0:
|
||||
bpy.data.images.remove(refined_image, do_unlink=True)
|
||||
raise
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Edit Image Mask"):
|
||||
raise RuntimeError("Unable to create the Edit Image Mask Undo step")
|
||||
return "Image Selection refined"
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Crop Image Selection"):
|
||||
raise RuntimeError("Unable to create the Crop Image Selection Undo step")
|
||||
return "Crop Selection refined"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
|
||||
class ActivateImageTool(bpy.types.Operator):
|
||||
bl_idname = "anyimage.activate_image_tool"
|
||||
bl_label = "Image Tool"
|
||||
bl_description = "Activate the Image Box tool in the 3D Viewport"
|
||||
class ActivateCropTool(bpy.types.Operator):
|
||||
bl_idname = "anyimage.activate_crop_tool"
|
||||
bl_label = "Crop Tool"
|
||||
bl_description = "Activate Crop Box in the 3D Viewport"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
@@ -212,5 +239,5 @@ class ActivateImageTool(bpy.types.Operator):
|
||||
|
||||
def execute(self, _context):
|
||||
return bpy.ops.wm.tool_set_by_id(
|
||||
name="anyimage.image_box", space_type="VIEW_3D"
|
||||
name="anyimage.crop_box", space_type="VIEW_3D"
|
||||
)
|
||||
@@ -45,6 +45,7 @@ def _pie_button(
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
inward_axis,
|
||||
):
|
||||
operator = pie.operator(
|
||||
"anyimage.cutout_selection_to_shape",
|
||||
@@ -57,6 +58,7 @@ def _pie_button(
|
||||
operator.refine_selection = refine_selection
|
||||
operator.generate_normal = generate_normal
|
||||
operator.mesh_resolution = mesh_resolution
|
||||
operator.inward_axis = inward_axis
|
||||
|
||||
|
||||
def open_shape_pie(
|
||||
@@ -76,6 +78,7 @@ def open_shape_pie(
|
||||
getattr(settings, "cutout_generate_normal", False)
|
||||
)
|
||||
mesh_resolution = cutout_mesh_resolution(source_object.data.size, point_spacing)
|
||||
inward_axis = str(getattr(settings, "cutout_inward_axis", "NEGATIVE_Z"))
|
||||
|
||||
def draw(menu, _context):
|
||||
pie = menu.layout.menu_pie()
|
||||
@@ -91,6 +94,7 @@ def open_shape_pie(
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
inward_axis,
|
||||
)
|
||||
|
||||
context.window_manager.popup_menu_pie(
|
||||
|
||||
@@ -45,8 +45,8 @@ from ...common.image import (
|
||||
)
|
||||
from ...common.depth import load_depth_metadata, load_depth_result_image
|
||||
from ...common.viewport import (
|
||||
ImageSelectionGesture,
|
||||
apply_selection_settings,
|
||||
CropSelectionGesture,
|
||||
apply_crop_settings,
|
||||
gesture_property,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
@@ -64,9 +64,11 @@ from .geometry import (
|
||||
)
|
||||
from .interaction import open_shape_pie
|
||||
from .object import (
|
||||
CutoutInwardAxis,
|
||||
NormalMode,
|
||||
create_cropped_color_image,
|
||||
create_shape_object,
|
||||
orient_object_normal_image,
|
||||
)
|
||||
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
|
||||
|
||||
@@ -101,6 +103,7 @@ def create_cutout_shape(
|
||||
depth_metadata=None,
|
||||
normal_image=None,
|
||||
normal_mode=NormalMode.NONE,
|
||||
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
|
||||
color_image,
|
||||
):
|
||||
try:
|
||||
@@ -123,6 +126,7 @@ def create_cutout_shape(
|
||||
depth_metadata=depth_metadata,
|
||||
normal_image=normal_image,
|
||||
normal_mode=normal_mode,
|
||||
inward_axis=inward_axis,
|
||||
color_image=color_image,
|
||||
)
|
||||
except Exception:
|
||||
@@ -235,6 +239,10 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
)
|
||||
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
refine_selection: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
inward_axis: bpy.props.StringProperty(
|
||||
default=CutoutInwardAxis.NEGATIVE_Z.value,
|
||||
options=_HIDDEN,
|
||||
)
|
||||
|
||||
def _needs_generation(self):
|
||||
return (
|
||||
@@ -245,6 +253,11 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self.inward_axis = getattr(
|
||||
self,
|
||||
"inward_axis",
|
||||
CutoutInwardAxis.NEGATIVE_Z.value,
|
||||
)
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
if video_total_frames(source_object.data) != 1:
|
||||
raise ValueError("Cutout currently supports still images")
|
||||
@@ -285,6 +298,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
self.mesh_resolution,
|
||||
content_values,
|
||||
selection_mask.bounds,
|
||||
inward_axis=self.inward_axis,
|
||||
color_image=color_image,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -355,6 +369,11 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
return parameters
|
||||
|
||||
def response(self, context, result):
|
||||
inward_axis = getattr(
|
||||
self,
|
||||
"inward_axis",
|
||||
CutoutInwardAxis.NEGATIVE_Z.value,
|
||||
)
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
source_bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
color_image, source_alpha, color_bounds = _generated_color_result(
|
||||
@@ -389,6 +408,8 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
result.file(normal_key),
|
||||
source_object,
|
||||
)
|
||||
if normal_mode == NormalMode.OBJECT:
|
||||
orient_object_normal_image(normal_image, inward_axis)
|
||||
depth_metadata = None
|
||||
depth_mode = depth_mode_for_shape(self.shape)
|
||||
if depth_mode != "NONE":
|
||||
@@ -410,6 +431,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
depth_metadata=depth_metadata,
|
||||
normal_image=normal_image,
|
||||
normal_mode=normal_mode,
|
||||
inward_axis=inward_axis,
|
||||
color_image=color_image,
|
||||
)
|
||||
except Exception:
|
||||
@@ -426,7 +448,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
|
||||
class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
|
||||
class SelectCutoutSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
bl_idname = "anyimage.select_cutout_selection"
|
||||
bl_label = "Start Cutout Selection"
|
||||
bl_description = "Lasso an Image Empty selection, then choose its mesh shape"
|
||||
@@ -444,10 +466,8 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
|
||||
_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"
|
||||
"Cutout Lasso: drag and release for shapes; RMB or Esc cancels"
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
@@ -462,9 +482,9 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
|
||||
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")
|
||||
self.report({"WARNING"}, "Cutout only supports a still image")
|
||||
return {"CANCELLED"}
|
||||
apply_selection_settings(self, context)
|
||||
apply_crop_settings(self, context)
|
||||
self.invert = False
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
@@ -476,7 +496,7 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
|
||||
|
||||
def modal(self, context, event):
|
||||
self._pie_event = event
|
||||
return super().modal(context, event)
|
||||
return CropSelectionGesture.modal(self, context, event)
|
||||
|
||||
def _submit_selection_path(self, context, selection_path):
|
||||
try:
|
||||
@@ -500,7 +520,7 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
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_icon = "ops.mesh.primitive_sphere_add_gizmo"
|
||||
bl_operator = SelectCutoutSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
@@ -521,3 +541,4 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
draw_ai_property(layout, settings, "cutout_refine_selection", status)
|
||||
draw_ai_property(layout, settings, "cutout_generate_normal", status)
|
||||
layout.prop(settings, "cutout_mesh_detail")
|
||||
layout.prop(settings, "cutout_inward_axis")
|
||||
|
||||
@@ -31,6 +31,7 @@ from .geometry import (
|
||||
from .shape import (
|
||||
BALLOON_ATTRIBUTE_NAME,
|
||||
BALLOON_CUTOUT_SHAPES,
|
||||
CUTOUT_AXIS_TO_SOURCE,
|
||||
CUTOUT_NODE_GROUP_NAME,
|
||||
CUTOUT_SHAPE_INDICES,
|
||||
DEPTH_CUTOUT_SHAPES,
|
||||
@@ -43,23 +44,38 @@ class NormalMode(StrEnum):
|
||||
OBJECT = "OBJECT"
|
||||
|
||||
|
||||
CUTOUT_AXIS_TO_SOURCE = (
|
||||
(0.0, -1.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 1.0, 0.0),
|
||||
(-1.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
class CutoutInwardAxis(StrEnum):
|
||||
NEGATIVE_Z = "NEGATIVE_Z"
|
||||
POSITIVE_X = "POSITIVE_X"
|
||||
|
||||
|
||||
def _cutout_world_matrix(source_matrix, crop_center):
|
||||
def _cutout_world_matrix(source_matrix, crop_center, inward_axis):
|
||||
from mathutils import Matrix
|
||||
|
||||
center_x, center_y = (float(value) for value in crop_center)
|
||||
return (
|
||||
source_matrix
|
||||
@ Matrix.Translation((center_x, center_y, 0.0))
|
||||
@ Matrix(CUTOUT_AXIS_TO_SOURCE)
|
||||
)
|
||||
matrix = source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
|
||||
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
|
||||
matrix @= Matrix(CUTOUT_AXIS_TO_SOURCE)
|
||||
return matrix
|
||||
|
||||
|
||||
def orient_object_normal_image(image, inward_axis):
|
||||
"""Convert internal +X Cutout normals to the selected object axes."""
|
||||
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
|
||||
return image
|
||||
import numpy as np
|
||||
|
||||
pixels = np.empty(int(image.size[0]) * int(image.size[1]) * 4, dtype=np.float32)
|
||||
image.pixels.foreach_get(pixels)
|
||||
rgba = pixels.reshape((-1, 4))
|
||||
normal = rgba[:, :3] * 2.0 - 1.0
|
||||
rgba[:, :3] = np.column_stack(
|
||||
(-normal[:, 1], normal[:, 2], -normal[:, 0])
|
||||
) * 0.5 + 0.5
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def _assign_uv(mesh, uv):
|
||||
@@ -149,11 +165,13 @@ def create_shape_object(
|
||||
depth_metadata=None,
|
||||
normal_image=None,
|
||||
normal_mode=NormalMode.NONE,
|
||||
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
|
||||
color_image,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
normal_mode = NormalMode(normal_mode)
|
||||
inward_axis = CutoutInwardAxis(inward_axis)
|
||||
bounds = crop_plane_bounds(
|
||||
source_object,
|
||||
content_bounds,
|
||||
@@ -245,6 +263,7 @@ def create_shape_object(
|
||||
mesh_object.matrix_world = _cutout_world_matrix(
|
||||
source_object.matrix_world,
|
||||
crop_center,
|
||||
inward_axis,
|
||||
)
|
||||
mesh_object["anyimage_mesh_shape"] = shape
|
||||
node_group = load_cutout_node_group()
|
||||
@@ -253,6 +272,10 @@ def create_shape_object(
|
||||
modifier.show_group_selector = False
|
||||
for name, value in (
|
||||
("Shape", CUTOUT_SHAPE_INDICES[shape]),
|
||||
(
|
||||
"Inward Axis",
|
||||
0 if inward_axis == CutoutInwardAxis.NEGATIVE_Z else 1,
|
||||
),
|
||||
("Cleanup", cleanup_value),
|
||||
("Smooth Iterations", 0),
|
||||
("Smooth", 1.0),
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
BALLOON_ATTRIBUTE_NAME = "o_balloon"
|
||||
CUTOUT_NODE_GROUP_NAME = "O Image Cutout"
|
||||
CUTOUT_AXIS_TO_SOURCE = (
|
||||
(0.0, -1.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 1.0, 0.0),
|
||||
(-1.0, 0.0, 0.0, 0.0),
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
CUTOUT_NODE_GROUP_NAMES = {
|
||||
"SURFACE": "O Image Surface",
|
||||
"BALLOON": "O Image Balloon",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from .box import ImageBoxTool
|
||||
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageSelection
|
||||
from .lasso import ImageLassoTool
|
||||
from .line import ImageLineTool
|
||||
from .polyline import ImagePolylineTool
|
||||
|
||||
CLASSES = (
|
||||
EditImageMask,
|
||||
RefineImageSelection,
|
||||
ActivateImageTool,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"CLASSES",
|
||||
"ImageBoxTool",
|
||||
"ImageLassoTool",
|
||||
"ImageLineTool",
|
||||
"ImagePolylineTool",
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import draw_selection_settings
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
class ImageBoxTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.image_box"
|
||||
bl_label = "Image Box"
|
||||
bl_description = "Edit an Image Empty with a rectangular mask"
|
||||
bl_icon = "ops.sculpt.border_mask"
|
||||
bl_operator = EditImageMask.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
EditImageMask.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "BOX")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_selection_settings(context, layout, "image_box_refine_selection")
|
||||
@@ -1,24 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from ...common.viewport import draw_selection_settings
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
class ImageLassoTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.image_lasso"
|
||||
bl_label = "Image Lasso"
|
||||
bl_description = "Edit an Image Empty with a freehand mask"
|
||||
bl_icon = "ops.sculpt.lasso_mask"
|
||||
bl_operator = EditImageMask.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
EditImageMask.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "LASSO")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_selection_settings(context, layout, "image_lasso_refine_selection")
|
||||
@@ -1,20 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
class ImageLineTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.image_line"
|
||||
bl_label = "Image Line"
|
||||
bl_description = "Edit one side of an Image Empty with a line"
|
||||
bl_icon = "ops.sculpt.line_mask"
|
||||
bl_operator = EditImageMask.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
EditImageMask.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "LINE")]},
|
||||
),
|
||||
)
|
||||
@@ -1,20 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
class ImagePolylineTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_context_mode = "OBJECT"
|
||||
bl_idname = "anyimage.image_polyline"
|
||||
bl_label = "Image Polyline"
|
||||
bl_description = "Edit an Image Empty with a clicked polygon mask"
|
||||
bl_icon = "ops.sculpt.polyline_mask"
|
||||
bl_operator = EditImageMask.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
EditImageMask.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "POLYLINE")]},
|
||||
),
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
from .edit_perspective import EditImagePerspective, ImagePerspectiveTool
|
||||
|
||||
|
||||
CLASSES = (EditImagePerspective,)
|
||||
|
||||
__all__ = ("CLASSES", "ImagePerspectiveTool")
|
||||
@@ -200,6 +200,34 @@ def refine_selection_property():
|
||||
)
|
||||
|
||||
|
||||
def crop_invert_property():
|
||||
return bpy.props.BoolProperty(
|
||||
name="Invert",
|
||||
description="Keep the area outside the crop selection",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
def keep_original_property():
|
||||
return bpy.props.BoolProperty(
|
||||
name="Keep Original",
|
||||
description="Create a crop result while preserving the source Image Empty",
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
def cutout_inward_axis_property():
|
||||
return bpy.props.EnumProperty(
|
||||
name="Inward Axis",
|
||||
description="Local axis pointing inward from the image sheet",
|
||||
items=(
|
||||
("NEGATIVE_Z", "-Z", "Match the source Image Empty local axes"),
|
||||
("POSITIVE_X", "+X", "Use the original Cutout local axes"),
|
||||
),
|
||||
default="NEGATIVE_Z",
|
||||
)
|
||||
|
||||
|
||||
def cutout_generate_normal_property():
|
||||
return bpy.props.BoolProperty(
|
||||
name="Normal Map",
|
||||
@@ -229,8 +257,16 @@ class AnyImageSettings(bpy.types.PropertyGroup):
|
||||
cutout_mesh_detail: cutout_mesh_detail_property()
|
||||
cutout_generate_normal: cutout_generate_normal_property()
|
||||
cutout_refine_selection: refine_selection_property()
|
||||
image_box_refine_selection: refine_selection_property()
|
||||
image_lasso_refine_selection: refine_selection_property()
|
||||
cutout_inward_axis: cutout_inward_axis_property()
|
||||
crop_box_refine_selection: refine_selection_property()
|
||||
crop_box_invert: crop_invert_property()
|
||||
crop_box_keep_original: keep_original_property()
|
||||
crop_lasso_refine_selection: refine_selection_property()
|
||||
crop_lasso_invert: crop_invert_property()
|
||||
crop_lasso_keep_original: keep_original_property()
|
||||
crop_polyline_invert: crop_invert_property()
|
||||
crop_polyline_keep_original: keep_original_property()
|
||||
crop_perspective_keep_original: keep_original_property()
|
||||
input_path: bpy.props.StringProperty(
|
||||
name="Image or Video",
|
||||
description="A single image or video file used for reconstruction",
|
||||
|
||||
Reference in New Issue
Block a user