Add tests for clipboard image functionality and mesh plane asset creation
- Implemented a comprehensive test suite for the clipboard image module, covering various functionalities including operator registration, context handling, and image processing. - Created a new mesh plane asset with customizable parameters and integrated it into the geometry node system. - Developed a validation script to ensure the correct behavior of the mesh plane asset, including input handling and output verification.
This commit is contained in:
@@ -5,6 +5,11 @@ from .balloon import (
|
||||
AnalyzeBalloon,
|
||||
)
|
||||
from .debug import DebugBen2, DebugDa3, DebugMoge2
|
||||
from .image_lasso import (
|
||||
ActivateImageLassoTool,
|
||||
AnalyzeImageLasso,
|
||||
ExtractImageLasso,
|
||||
)
|
||||
from .depth_plane import (
|
||||
ConvertToDepthPlane,
|
||||
ExtractOriginalFrames,
|
||||
@@ -12,11 +17,20 @@ from .depth_plane import (
|
||||
GenerateMoge2Depth,
|
||||
GenerateOriginalBackground,
|
||||
)
|
||||
from .models import ClearModels, DownloadModel, DownloadRequiredModels
|
||||
from .models import (
|
||||
ClearModels,
|
||||
DownloadModel,
|
||||
DownloadRequiredModels,
|
||||
OpenAIEnvironmentSettings,
|
||||
SetupAIEnvironment,
|
||||
)
|
||||
from .mesh_plane import ConvertToMeshPlane
|
||||
from .surface import ConvertToSurface, GenerateDa3Surface, GenerateMoge2Surface
|
||||
|
||||
|
||||
CLASSES = (
|
||||
SetupAIEnvironment,
|
||||
OpenAIEnvironmentSettings,
|
||||
DownloadModel,
|
||||
DownloadRequiredModels,
|
||||
DebugDa3,
|
||||
@@ -30,10 +44,14 @@ CLASSES = (
|
||||
GenerateOriginalBackground,
|
||||
ExtractOriginalFrames,
|
||||
ClearModels,
|
||||
ExtractImageLasso,
|
||||
AnalyzeImageLasso,
|
||||
ActivateImageLassoTool,
|
||||
ExtractBalloon,
|
||||
AnalyzeBalloon,
|
||||
ActivateBalloonTool,
|
||||
RemoveImageBackground,
|
||||
ConvertToMeshPlane,
|
||||
ConvertToDepthPlane,
|
||||
ConvertToSurface,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from .common import (
|
||||
HIDDEN,
|
||||
background_input_path,
|
||||
cleanup_temporary_input,
|
||||
invoke_ai_setup_if_needed,
|
||||
production_device,
|
||||
push_undo,
|
||||
require_environment,
|
||||
@@ -31,14 +32,14 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
from ..runtime import runtime
|
||||
from ..properties import required_models_ready
|
||||
|
||||
return (
|
||||
is_image_empty(context.object)
|
||||
and runtime.environment_ready()
|
||||
and not runtime.server_busy()
|
||||
and required_models_ready()
|
||||
)
|
||||
return is_image_empty(context.object) and not runtime.server_busy()
|
||||
|
||||
def invoke(self, _context, _event):
|
||||
setup_result = invoke_ai_setup_if_needed()
|
||||
if setup_result is not None:
|
||||
return setup_result
|
||||
return self.execute(_context)
|
||||
|
||||
def execute(self, context):
|
||||
source_object = context.object
|
||||
|
||||
@@ -13,6 +13,9 @@ from .depth import (
|
||||
from .geometry import INFLATION, POINT_SPACING, triangulate_balloon
|
||||
from ...properties import (
|
||||
BEN2_MODEL_KEY,
|
||||
ai_ready,
|
||||
ai_setup_label,
|
||||
ai_status,
|
||||
balloon_contour_analysis_property as contour_analysis_property,
|
||||
balloon_depth_analysis_property as depth_analysis_property,
|
||||
balloon_depth_model_property as depth_model_property,
|
||||
@@ -500,10 +503,24 @@ class ExtractBalloon(bpy.types.Operator):
|
||||
layout.prop(self, "thickness")
|
||||
layout.prop(self, "resolution", text="Resolution (m)")
|
||||
layout.prop(self, "reuse_material")
|
||||
layout.prop(self, "contour_analysis")
|
||||
layout.prop(self, "depth_analysis")
|
||||
status = ai_status()
|
||||
ready = status["ready"]
|
||||
contour_row = layout.row()
|
||||
contour_row.enabled = ready
|
||||
contour_row.prop(self, "contour_analysis")
|
||||
depth_row = layout.row()
|
||||
depth_row.enabled = ready
|
||||
depth_row.prop(self, "depth_analysis")
|
||||
if self.depth_analysis:
|
||||
layout.prop(self, "depth_model")
|
||||
model_row = layout.row()
|
||||
model_row.enabled = ready
|
||||
model_row.prop(self, "depth_model")
|
||||
if not ready:
|
||||
layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
@@ -541,8 +558,9 @@ class ExtractBalloon(bpy.types.Operator):
|
||||
self.thickness = settings.balloon_thickness
|
||||
self.resolution = settings.balloon_resolution
|
||||
self.reuse_material = settings.balloon_reuse_material
|
||||
self.contour_analysis = settings.balloon_contour_analysis
|
||||
self.depth_analysis = settings.balloon_depth_analysis
|
||||
ready = ai_ready()
|
||||
self.contour_analysis = ready and settings.balloon_contour_analysis
|
||||
self.depth_analysis = ready and settings.balloon_depth_analysis
|
||||
self.depth_model = settings.balloon_depth_model
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
@@ -835,7 +853,21 @@ class BalloonTool(bpy.types.WorkSpaceTool):
|
||||
layout.prop(settings, "balloon_thickness")
|
||||
layout.prop(settings, "balloon_resolution", text="Resolution (m)")
|
||||
layout.prop(settings, "balloon_reuse_material")
|
||||
layout.prop(settings, "balloon_contour_analysis")
|
||||
layout.prop(settings, "balloon_depth_analysis")
|
||||
status = ai_status()
|
||||
ready = status["ready"]
|
||||
contour_row = layout.row()
|
||||
contour_row.enabled = ready
|
||||
contour_row.prop(settings, "balloon_contour_analysis")
|
||||
depth_row = layout.row()
|
||||
depth_row.enabled = ready
|
||||
depth_row.prop(settings, "balloon_depth_analysis")
|
||||
if settings.balloon_depth_analysis:
|
||||
layout.prop(settings, "balloon_depth_model", text="Depth Model")
|
||||
model_row = layout.row()
|
||||
model_row.enabled = ready
|
||||
model_row.prop(settings, "balloon_depth_model", text="Depth Model")
|
||||
if not ready:
|
||||
layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import bpy
|
||||
|
||||
from .image import extract_frame_range
|
||||
from ..runtime import runtime
|
||||
from ..properties import model_ready, model_record
|
||||
from ..properties import ai_ready, model_ready, model_record
|
||||
|
||||
|
||||
BEN2_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
@@ -49,6 +49,12 @@ def require_environment():
|
||||
raise RuntimeError("Install the AnyImage Environment first")
|
||||
|
||||
|
||||
def invoke_ai_setup_if_needed():
|
||||
if ai_ready():
|
||||
return None
|
||||
return bpy.ops.anyimage.setup_ai_environment("INVOKE_DEFAULT")
|
||||
|
||||
|
||||
def require_model(model_key):
|
||||
model = model_record(model_key)
|
||||
if model is None:
|
||||
|
||||
@@ -8,6 +8,7 @@ from ..common import (
|
||||
cleanup_temporary_input,
|
||||
da3_parameters,
|
||||
input_path,
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
push_undo,
|
||||
require_environment,
|
||||
@@ -74,16 +75,13 @@ class ConvertToDepthPlane(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
from ...runtime import runtime
|
||||
from ...properties import required_models_ready
|
||||
|
||||
return (
|
||||
is_image_empty(context.object)
|
||||
and runtime.environment_ready()
|
||||
and not runtime.server_busy()
|
||||
and required_models_ready()
|
||||
)
|
||||
return is_image_empty(context.object) and not runtime.server_busy()
|
||||
|
||||
def invoke(self, context, _event):
|
||||
setup_result = invoke_ai_setup_if_needed()
|
||||
if setup_result is not None:
|
||||
return setup_result
|
||||
return context.window_manager.invoke_props_dialog(self, width=340)
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
635
src/anyimage/operators/image_lasso.py
Normal file
635
src/anyimage/operators/image_lasso.py
Normal file
@@ -0,0 +1,635 @@
|
||||
import json
|
||||
import math
|
||||
|
||||
import bpy
|
||||
|
||||
from ..properties import (
|
||||
BEN2_MODEL_KEY,
|
||||
ai_ready,
|
||||
ai_setup_label,
|
||||
ai_status,
|
||||
image_lasso_contour_analysis_property as contour_analysis_property,
|
||||
image_lasso_keep_original_property as keep_original_property,
|
||||
image_lasso_padding_property as padding_property,
|
||||
)
|
||||
from ..runtime import JobOperatorBase
|
||||
from .balloon.operator import (
|
||||
deserialize_matrix,
|
||||
drawing_in_region,
|
||||
hidden_string_property,
|
||||
screen_path_to_image_pixels,
|
||||
serialize_matrix,
|
||||
simplify_screen_path,
|
||||
)
|
||||
from .common import (
|
||||
HIDDEN,
|
||||
cleanup_temporary_input,
|
||||
production_device,
|
||||
push_undo,
|
||||
require_environment,
|
||||
require_model,
|
||||
)
|
||||
from .image import (
|
||||
image_base_name,
|
||||
is_image_empty,
|
||||
plane_bounds,
|
||||
prepare_image_input,
|
||||
remove_temporary_input,
|
||||
video_total_frames,
|
||||
)
|
||||
|
||||
|
||||
def lasso_bounds(polygon):
|
||||
import numpy as np
|
||||
|
||||
points = np.asarray(polygon, dtype=np.float64)
|
||||
if points.ndim != 2 or points.shape[0] < 3 or points.shape[1] != 2:
|
||||
raise ValueError("The lasso does not define a valid image region")
|
||||
if not np.isfinite(points).all():
|
||||
raise ValueError("The lasso 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 lasso does not define a valid image region")
|
||||
return left, top, right, bottom
|
||||
|
||||
|
||||
def points_inside_polygon(points, polygon):
|
||||
import numpy as np
|
||||
|
||||
points = np.asarray(points, dtype=np.float64)
|
||||
polygon = np.asarray(polygon, dtype=np.float64)
|
||||
inside = np.zeros(len(points), dtype=bool)
|
||||
previous = polygon[-1]
|
||||
for current in polygon:
|
||||
crosses = (current[1] > points[:, 1]) != (previous[1] > points[:, 1])
|
||||
edge_x = (
|
||||
(previous[0] - current[0])
|
||||
* (points[:, 1] - current[1])
|
||||
/ (previous[1] - current[1] + 1e-300)
|
||||
+ current[0]
|
||||
)
|
||||
inside ^= crosses & (points[:, 0] < edge_x)
|
||||
previous = current
|
||||
return inside
|
||||
|
||||
|
||||
def extract_lasso_pixels(source_pixels, source_size, polygon):
|
||||
"""Return Blender-order RGBA pixels and the unbounded lasso rectangle."""
|
||||
import numpy as np
|
||||
|
||||
source_width, source_height = (int(value) for value in source_size)
|
||||
if source_width <= 0 or source_height <= 0:
|
||||
raise ValueError("The source image has no readable dimensions")
|
||||
source = np.asarray(source_pixels, dtype=np.float32)
|
||||
if source.size != source_width * source_height * 4:
|
||||
raise ValueError("The source image pixels do not match its dimensions")
|
||||
source = np.flipud(source.reshape((source_height, source_width, 4)))
|
||||
left, top, right, bottom = lasso_bounds(polygon)
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
result = np.zeros((height, width, 4), dtype=np.float32)
|
||||
|
||||
source_left = max(left, 0)
|
||||
source_top = max(top, 0)
|
||||
source_right = min(right, source_width)
|
||||
source_bottom = min(bottom, source_height)
|
||||
if source_right <= source_left or source_bottom <= source_top:
|
||||
raise ValueError("The lasso does not overlap the source image")
|
||||
result[
|
||||
source_top - top : source_bottom - top,
|
||||
source_left - left : source_right - left,
|
||||
] = source[source_top:source_bottom, source_left:source_right]
|
||||
|
||||
y, x = np.indices((height, width), dtype=np.float64)
|
||||
centers = np.column_stack(
|
||||
((x.ravel() + left + 0.5), (y.ravel() + top + 0.5))
|
||||
)
|
||||
selected = points_inside_polygon(centers, polygon).reshape((height, width))
|
||||
result[~selected] = 0.0
|
||||
result[result[:, :, 3] <= 0.0] = 0.0
|
||||
return np.flipud(result).ravel(), (left, top, right, bottom)
|
||||
|
||||
|
||||
def crop_pixels_to_alpha(pixels, bounds, padding=0):
|
||||
"""Crop Blender-order RGBA pixels to nonzero Alpha plus pixel padding."""
|
||||
import numpy as np
|
||||
|
||||
if padding < 0:
|
||||
raise ValueError("Padding must not be negative")
|
||||
left, top, right, bottom = bounds
|
||||
width = right - left
|
||||
height = bottom - top
|
||||
rgba = np.flipud(
|
||||
np.asarray(pixels, dtype=np.float32).reshape((height, width, 4))
|
||||
)
|
||||
visible_y, visible_x = np.nonzero(rgba[:, :, 3] > 0.0)
|
||||
if not len(visible_x):
|
||||
raise ValueError("The lasso contains no visible image pixels")
|
||||
content_left = int(visible_x.min())
|
||||
content_top = int(visible_y.min())
|
||||
content_right = int(visible_x.max()) + 1
|
||||
content_bottom = int(visible_y.max()) + 1
|
||||
output_width = content_right - content_left + padding * 2
|
||||
output_height = content_bottom - content_top + padding * 2
|
||||
output = np.zeros((output_height, output_width, 4), dtype=np.float32)
|
||||
output[
|
||||
padding : padding + content_bottom - content_top,
|
||||
padding : padding + content_right - content_left,
|
||||
] = rgba[content_top:content_bottom, content_left:content_right]
|
||||
output[output[:, :, 3] <= 0.0] = 0.0
|
||||
return np.flipud(output).ravel(), (
|
||||
left + content_left - padding,
|
||||
top + content_top - padding,
|
||||
left + content_right + padding,
|
||||
top + content_bottom + padding,
|
||||
)
|
||||
|
||||
|
||||
def align_empty_to_crop(source_object, matrix_world, source_size, crop_bounds):
|
||||
from mathutils import Matrix
|
||||
|
||||
source_width, source_height = source_size
|
||||
left, top, right, bottom = crop_bounds
|
||||
x_min, x_max, y_min, y_max = plane_bounds(source_object)
|
||||
local_width = x_max - x_min
|
||||
local_height = y_max - y_min
|
||||
crop_x_min = x_min + left * local_width / source_width
|
||||
crop_x_max = x_min + right * local_width / source_width
|
||||
crop_y_max = y_max - top * local_height / source_height
|
||||
crop_y_min = y_max - bottom * local_height / source_height
|
||||
center = (
|
||||
(crop_x_min + crop_x_max) * 0.5,
|
||||
(crop_y_min + crop_y_max) * 0.5,
|
||||
0.0,
|
||||
)
|
||||
source_object.matrix_world = matrix_world @ Matrix.Translation(center)
|
||||
source_object.empty_display_size = max(
|
||||
crop_x_max - crop_x_min,
|
||||
crop_y_max - crop_y_min,
|
||||
)
|
||||
source_object.empty_image_offset = (-0.5, -0.5)
|
||||
|
||||
|
||||
def replace_empty_image(source_object, matrix_world, pixels, crop_bounds):
|
||||
source_image = source_object.data
|
||||
source_size = tuple(source_image.size)
|
||||
left, top, right, bottom = crop_bounds
|
||||
image = bpy.data.images.new(
|
||||
f"{image_base_name(source_image)}.extract",
|
||||
width=right - left,
|
||||
height=bottom - top,
|
||||
alpha=True,
|
||||
)
|
||||
try:
|
||||
image.colorspace_settings.name = source_image.colorspace_settings.name
|
||||
image.alpha_mode = "STRAIGHT"
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.pack()
|
||||
image.update()
|
||||
align_empty_to_crop(source_object, matrix_world, source_size, crop_bounds)
|
||||
source_object.data = image
|
||||
except Exception:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise
|
||||
return image
|
||||
|
||||
|
||||
def replace_empty_image_from_path(source_object, matrix_world, path, crop_bounds):
|
||||
source_size = tuple(source_object.data.size)
|
||||
colorspace = source_object.data.colorspace_settings.name
|
||||
try:
|
||||
image = bpy.data.images.load(str(path), check_existing=False)
|
||||
except (OSError, RuntimeError) as error:
|
||||
raise RuntimeError("Unable to load the extracted image") from error
|
||||
try:
|
||||
image.name = f"{image_base_name(source_object.data)}.extract"
|
||||
image.colorspace_settings.name = colorspace
|
||||
image.alpha_mode = "STRAIGHT"
|
||||
image.pack()
|
||||
align_empty_to_crop(source_object, matrix_world, source_size, crop_bounds)
|
||||
source_object.data = image
|
||||
except Exception:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise
|
||||
return image
|
||||
|
||||
|
||||
def image_lasso_target(context, source_object, keep_original):
|
||||
if not keep_original:
|
||||
return source_object
|
||||
target = source_object.copy()
|
||||
collection = (
|
||||
source_object.users_collection[0]
|
||||
if source_object.users_collection
|
||||
else context.collection
|
||||
)
|
||||
collection.objects.link(target)
|
||||
return target
|
||||
|
||||
|
||||
def select_image_lasso_result(context, target):
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
target.select_set(True)
|
||||
context.view_layer.objects.active = target
|
||||
|
||||
|
||||
def selection_shape_property():
|
||||
return bpy.props.EnumProperty(
|
||||
items=(
|
||||
("LASSO", "Lasso", "Draw a freehand image extraction region"),
|
||||
("BOX", "Box", "Draw a rectangular image extraction region"),
|
||||
),
|
||||
default="BOX",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
|
||||
def box_screen_path(path):
|
||||
if len(path) < 2:
|
||||
return ()
|
||||
x0, y0 = path[0]
|
||||
x1, y1 = path[-1]
|
||||
if abs(x1 - x0) < 2.0 or abs(y1 - y0) < 2.0:
|
||||
return ()
|
||||
return ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
|
||||
|
||||
|
||||
def dashed_line_segments(vertices, dash_length=4.0, gap_length=4.0):
|
||||
if dash_length <= 0.0 or gap_length < 0.0:
|
||||
raise ValueError("Dash length must be positive and gap length cannot be negative")
|
||||
pattern_length = dash_length + gap_length
|
||||
segments = []
|
||||
distance_offset = 0.0
|
||||
for start, end in zip(vertices, vertices[1:]):
|
||||
start_x, start_y = (float(value) for value in start)
|
||||
end_x, end_y = (float(value) for value in end)
|
||||
segment_length = math.hypot(end_x - start_x, end_y - start_y)
|
||||
if segment_length <= 0.0:
|
||||
continue
|
||||
direction_x = (end_x - start_x) / segment_length
|
||||
direction_y = (end_y - start_y) / segment_length
|
||||
position = 0.0
|
||||
while position < segment_length:
|
||||
phase = (distance_offset + position) % pattern_length
|
||||
if phase < dash_length:
|
||||
run_length = min(dash_length - phase, segment_length - position)
|
||||
segments.extend(
|
||||
(
|
||||
(
|
||||
start_x + direction_x * position,
|
||||
start_y + direction_y * position,
|
||||
),
|
||||
(
|
||||
start_x + direction_x * (position + run_length),
|
||||
start_y + direction_y * (position + run_length),
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
run_length = min(pattern_length - phase, segment_length - position)
|
||||
position += run_length
|
||||
distance_offset += segment_length
|
||||
return tuple(segments)
|
||||
|
||||
|
||||
def draw_dashed_line(vertices):
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
segments = dashed_line_segments(vertices)
|
||||
if not segments:
|
||||
return
|
||||
shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||
batch = batch_for_shader(shader, "LINES", {"pos": segments})
|
||||
gpu.state.blend_set("ALPHA")
|
||||
gpu.state.line_width_set(1.0)
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (1.0, 1.0, 1.0, 0.9))
|
||||
batch.draw(shader)
|
||||
gpu.state.blend_set("NONE")
|
||||
|
||||
|
||||
class ExtractImageLasso(bpy.types.Operator):
|
||||
bl_idname = "anyimage.extract_image_lasso"
|
||||
bl_label = "Extract Image"
|
||||
bl_description = "Replace an Image Empty with its lassoed image region"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
source_object_name: hidden_string_property()
|
||||
source_matrix_data: hidden_string_property()
|
||||
selection_data: hidden_string_property()
|
||||
contour_analysis: contour_analysis_property()
|
||||
keep_original: keep_original_property()
|
||||
padding: padding_property()
|
||||
selection_shape: selection_shape_property()
|
||||
|
||||
_path = None
|
||||
_handle = None
|
||||
_area_pointer = 0
|
||||
_region_pointer = 0
|
||||
|
||||
def draw(self, _context):
|
||||
status = ai_status()
|
||||
row = self.layout.row()
|
||||
row.enabled = status["ready"]
|
||||
row.prop(self, "contour_analysis")
|
||||
self.layout.prop(self, "keep_original")
|
||||
self.layout.prop(self, "padding")
|
||||
if not status["ready"]:
|
||||
self.layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.area is not None
|
||||
and context.area.type == "VIEW_3D"
|
||||
and is_image_empty(context.active_object)
|
||||
)
|
||||
|
||||
def _draw_selection(self):
|
||||
if not self._path or not drawing_in_region(
|
||||
self._area_pointer, self._region_pointer
|
||||
):
|
||||
return
|
||||
vertices = (
|
||||
list(box_screen_path(self._path))
|
||||
if self.selection_shape == "BOX"
|
||||
else list(self._path)
|
||||
)
|
||||
if len(vertices) > 2:
|
||||
vertices.append(vertices[0])
|
||||
draw_dashed_line(vertices)
|
||||
|
||||
def _finish(self, context):
|
||||
if self._handle is not None:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, "WINDOW")
|
||||
self._handle = None
|
||||
context.workspace.status_text_set(None)
|
||||
if context.area is not None:
|
||||
context.area.tag_redraw()
|
||||
|
||||
def cancel(self, context):
|
||||
self._finish(context)
|
||||
|
||||
def invoke(self, context, event):
|
||||
source_object = context.active_object
|
||||
self.contour_analysis = (
|
||||
ai_ready() and context.scene.anyimage_settings.image_lasso_contour_analysis
|
||||
)
|
||||
self.keep_original = context.scene.anyimage_settings.image_lasso_keep_original
|
||||
self.padding = context.scene.anyimage_settings.image_lasso_padding
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
self.selection_data = ""
|
||||
self._path = [(event.mouse_region_x, event.mouse_region_y)]
|
||||
self._area_pointer = context.area.as_pointer()
|
||||
self._region_pointer = context.region.as_pointer()
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(
|
||||
self._draw_selection, (), "WINDOW", "POST_PIXEL"
|
||||
)
|
||||
context.workspace.status_text_set(
|
||||
f"Image {self.selection_shape.title()}: release LMB to extract, "
|
||||
"RMB or Esc to cancel"
|
||||
)
|
||||
context.window_manager.modal_handler_add(self)
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
if event.type in {"ESC", "RIGHTMOUSE"} and event.value == "PRESS":
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
if event.type == "MOUSEMOVE":
|
||||
self._path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
context.area.tag_redraw()
|
||||
return {"RUNNING_MODAL"}
|
||||
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
|
||||
self._path.append((event.mouse_region_x, event.mouse_region_y))
|
||||
path = (
|
||||
list(box_screen_path(self._path))
|
||||
if self.selection_shape == "BOX"
|
||||
else simplify_screen_path(self._path)
|
||||
)
|
||||
if len(path) < 3:
|
||||
self.report({"WARNING"}, "Draw a larger image region")
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "The source Image Empty is no longer available")
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
matrix_world = deserialize_matrix(self.source_matrix_data)
|
||||
polygon = screen_path_to_image_pixels(
|
||||
path,
|
||||
context.region,
|
||||
context.region_data,
|
||||
matrix_world,
|
||||
plane_bounds(source_object),
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
self.selection_data = json.dumps(polygon, separators=(",", ":"))
|
||||
except (RuntimeError, TypeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
self._finish(context)
|
||||
return {"CANCELLED"}
|
||||
self._finish(context)
|
||||
return self.execute(context)
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
def execute(self, context):
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "The source Image Empty is no longer available")
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
polygon = json.loads(self.selection_data)
|
||||
crop_bounds = lasso_bounds(polygon)
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
raise RuntimeError("Image Lasso only supports a still image")
|
||||
if self.contour_analysis:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_object.data, context.scene
|
||||
)
|
||||
result = bpy.ops.anyimage.analyze_image_lasso(
|
||||
input_path=str(source_path),
|
||||
delete_input=delete_input,
|
||||
source_object_name=self.source_object_name,
|
||||
source_matrix_data=self.source_matrix_data,
|
||||
selection_data=self.selection_data,
|
||||
keep_original=self.keep_original,
|
||||
padding=self.padding,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
if delete_input:
|
||||
remove_temporary_input(source_path)
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
import numpy as np
|
||||
|
||||
image = source_object.data
|
||||
source_pixels = np.empty(len(image.pixels), dtype=np.float32)
|
||||
image.pixels.foreach_get(source_pixels)
|
||||
pixels, crop_bounds = extract_lasso_pixels(
|
||||
source_pixels, tuple(image.size), polygon
|
||||
)
|
||||
pixels, crop_bounds = crop_pixels_to_alpha(
|
||||
pixels, crop_bounds, self.padding
|
||||
)
|
||||
target = image_lasso_target(context, source_object, self.keep_original)
|
||||
try:
|
||||
replace_empty_image(
|
||||
target,
|
||||
deserialize_matrix(self.source_matrix_data),
|
||||
pixels,
|
||||
crop_bounds,
|
||||
)
|
||||
except Exception:
|
||||
if target is not source_object:
|
||||
bpy.data.objects.remove(target, do_unlink=True)
|
||||
raise
|
||||
select_image_lasso_result(
|
||||
context,
|
||||
source_object if self.keep_original else target,
|
||||
)
|
||||
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AnalyzeImageLasso(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.analyze_image_lasso"
|
||||
bl_label = "Analyze Image Lasso"
|
||||
job_type = "analyze-image-lasso"
|
||||
starting_message = "Analyzing image contour"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=HIDDEN)
|
||||
delete_input: bpy.props.BoolProperty(default=False, options=HIDDEN)
|
||||
source_object_name: hidden_string_property()
|
||||
source_matrix_data: hidden_string_property()
|
||||
selection_data: hidden_string_property()
|
||||
keep_original: keep_original_property()
|
||||
padding: padding_property()
|
||||
|
||||
def request(self, _context):
|
||||
require_environment()
|
||||
require_model(BEN2_MODEL_KEY)
|
||||
polygon = json.loads(self.selection_data)
|
||||
lasso_bounds(polygon)
|
||||
return {
|
||||
"input": str(self.input_path),
|
||||
"polygon": polygon,
|
||||
"padding": self.padding,
|
||||
"model": BEN2_MODEL_KEY,
|
||||
"device": production_device().lower(),
|
||||
}
|
||||
|
||||
def response(self, context, result):
|
||||
source_object = bpy.data.objects.get(self.source_object_name)
|
||||
if not is_image_empty(source_object):
|
||||
raise RuntimeError("The source Image Empty is no longer available")
|
||||
crop_bounds = tuple(
|
||||
json.loads(result.file("bounds").read_text(encoding="utf-8"))
|
||||
)
|
||||
target = image_lasso_target(context, source_object, self.keep_original)
|
||||
try:
|
||||
replace_empty_image_from_path(
|
||||
target,
|
||||
deserialize_matrix(self.source_matrix_data),
|
||||
result.file("image"),
|
||||
crop_bounds,
|
||||
)
|
||||
except Exception:
|
||||
if target is not source_object:
|
||||
bpy.data.objects.remove(target, do_unlink=True)
|
||||
raise
|
||||
select_image_lasso_result(
|
||||
context,
|
||||
source_object if self.keep_original else target,
|
||||
)
|
||||
push_undo("Extract Image")
|
||||
return "Contoured image extraction is ready"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_temporary_input(self.input_path, self.delete_input)
|
||||
|
||||
|
||||
class ActivateImageLassoTool(bpy.types.Operator):
|
||||
bl_idname = "anyimage.activate_image_lasso_tool"
|
||||
bl_label = "Image Extract Tool"
|
||||
bl_description = "Activate the Image Box tool in the 3D Viewport"
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return ExtractImageLasso.poll(context)
|
||||
|
||||
def execute(self, _context):
|
||||
return bpy.ops.wm.tool_set_by_id(
|
||||
name=ImageBoxTool.bl_idname, space_type="VIEW_3D"
|
||||
)
|
||||
|
||||
|
||||
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 = "Replace an Image Empty with one boxed image region"
|
||||
bl_icon = "ops.sculpt.border_mask"
|
||||
bl_operator = ExtractImageLasso.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
ExtractImageLasso.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("selection_shape", "BOX")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_image_extract_settings(context, layout)
|
||||
|
||||
|
||||
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 = "Replace an Image Empty with one lassoed image region"
|
||||
bl_icon = "ops.sculpt.lasso_mask"
|
||||
bl_operator = ExtractImageLasso.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
ExtractImageLasso.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("selection_shape", "LASSO")]},
|
||||
),
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_image_extract_settings(context, layout)
|
||||
|
||||
|
||||
def draw_image_extract_settings(context, layout):
|
||||
settings = context.scene.anyimage_settings
|
||||
status = ai_status()
|
||||
row = layout.row()
|
||||
row.enabled = status["ready"]
|
||||
row.prop(settings, "image_lasso_contour_analysis")
|
||||
layout.prop(settings, "image_lasso_keep_original")
|
||||
layout.prop(settings, "image_lasso_padding")
|
||||
if not status["ready"]:
|
||||
layout.operator(
|
||||
"anyimage.setup_ai_environment",
|
||||
text=ai_setup_label(status),
|
||||
icon="IMPORT",
|
||||
)
|
||||
@@ -10,6 +10,7 @@ ORIGINAL_DISPLACEMENT_MIDLEVEL = 0.0
|
||||
BUMP_BASE_DISTANCE = 0.001
|
||||
DEPTH_PLANE_NODE_GROUP_NAME = "O DA3 Depth Plane"
|
||||
MOGE_DEPTH_PLANE_NODE_GROUP_NAME = "O MoGe2 Depth Plane"
|
||||
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
|
||||
NODE_ASSET_NAME = "O_AnyImage.blend"
|
||||
MATERIAL_DISPLACEMENT_ATTRIBUTE = "o_material_displacement"
|
||||
BACKGROUND_DEPTH_ATTRIBUTE = "o_background_depth"
|
||||
@@ -647,6 +648,9 @@ def load_shadeless_node_group():
|
||||
def load_depth_plane_node_group(group_name=DEPTH_PLANE_NODE_GROUP_NAME):
|
||||
return load_asset_node_group(group_name)
|
||||
|
||||
def load_mesh_plane_node_group():
|
||||
return load_asset_node_group(MESH_PLANE_NODE_GROUP_NAME)
|
||||
|
||||
|
||||
def modifier_input_identifier(node_group, name):
|
||||
for item in node_group.interface.items_tree:
|
||||
|
||||
228
src/anyimage/operators/mesh_plane.py
Normal file
228
src/anyimage/operators/mesh_plane.py
Normal file
@@ -0,0 +1,228 @@
|
||||
import bpy
|
||||
|
||||
from .image import (
|
||||
image_user_settings,
|
||||
is_image_empty,
|
||||
material_data_name,
|
||||
plane_bounds,
|
||||
)
|
||||
from .material import (
|
||||
create_material,
|
||||
load_mesh_plane_node_group,
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
)
|
||||
|
||||
|
||||
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
|
||||
|
||||
|
||||
def is_mesh_plane_object(candidate):
|
||||
if candidate is None or getattr(candidate, "type", None) != "MESH":
|
||||
return False
|
||||
return any(
|
||||
getattr(getattr(modifier, "node_group", None), "name", None)
|
||||
== MESH_PLANE_NODE_GROUP_NAME
|
||||
for modifier in getattr(candidate, "modifiers", ())
|
||||
)
|
||||
|
||||
|
||||
def flatten_matrix(matrix):
|
||||
return tuple(
|
||||
float(matrix[row][column]) for row in range(4) for column in range(4)
|
||||
)
|
||||
|
||||
|
||||
def restore_matrix(values):
|
||||
from mathutils import Matrix
|
||||
|
||||
return Matrix(
|
||||
tuple(
|
||||
tuple(values[row * 4 + column] for column in range(4))
|
||||
for row in range(4)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_mesh_plane_mesh(name, bounds):
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
if x_max <= x_min or y_max <= y_min:
|
||||
raise ValueError("The plane has invalid dimensions")
|
||||
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
mesh.from_pydata(
|
||||
(
|
||||
(x_min, y_min, 0.0),
|
||||
(x_max, y_min, 0.0),
|
||||
(x_max, y_max, 0.0),
|
||||
(x_min, y_max, 0.0),
|
||||
),
|
||||
(),
|
||||
((0, 1, 2, 3),),
|
||||
)
|
||||
mesh.update()
|
||||
uv_layer = mesh.uv_layers.new(name="UVMap")
|
||||
for loop, uv in zip(
|
||||
mesh.loops,
|
||||
((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)),
|
||||
):
|
||||
uv_layer.data[loop.index].uv = uv
|
||||
return mesh
|
||||
|
||||
|
||||
def add_mesh_plane_processing(
|
||||
plane_object,
|
||||
material,
|
||||
subdivisions=0,
|
||||
thickness=0.0,
|
||||
):
|
||||
node_group = load_mesh_plane_node_group()
|
||||
modifier = plane_object.modifiers.new(
|
||||
name=MESH_PLANE_NODE_GROUP_NAME,
|
||||
type="NODES",
|
||||
)
|
||||
modifier.node_group = node_group
|
||||
if hasattr(modifier, "show_group_selector"):
|
||||
modifier.show_group_selector = False
|
||||
for name, value in (
|
||||
("Subdivide", max(0, int(subdivisions))),
|
||||
("Thickness", max(0.0, float(thickness))),
|
||||
("Material", material),
|
||||
):
|
||||
identifier = modifier_input_identifier(node_group, name)
|
||||
set_modifier_input(modifier, identifier, value)
|
||||
return modifier
|
||||
|
||||
|
||||
def create_mesh_plane_object(
|
||||
context,
|
||||
name,
|
||||
bounds,
|
||||
material,
|
||||
matrix_world=None,
|
||||
subdivisions=0,
|
||||
thickness=0.0,
|
||||
):
|
||||
mesh = create_mesh_plane_mesh(name, bounds)
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
if matrix_world is not None:
|
||||
plane.matrix_world = matrix_world.copy()
|
||||
add_mesh_plane_processing(
|
||||
plane,
|
||||
material,
|
||||
subdivisions=subdivisions,
|
||||
thickness=thickness,
|
||||
)
|
||||
return plane
|
||||
|
||||
|
||||
def finish_mesh_plane(context, source_object, plane_object, keep_original):
|
||||
source_name = source_object.name
|
||||
for selected in context.selected_objects:
|
||||
selected.select_set(False)
|
||||
plane_object.select_set(True)
|
||||
context.view_layer.objects.active = plane_object
|
||||
if not keep_original:
|
||||
bpy.data.objects.remove(source_object, do_unlink=True)
|
||||
plane_object.name = source_name
|
||||
return plane_object
|
||||
|
||||
|
||||
def build_mesh_plane(
|
||||
context,
|
||||
source_object,
|
||||
keep_original,
|
||||
shadeless=False,
|
||||
matrix_world=None,
|
||||
):
|
||||
frame_start, frame_offset, frame_duration, use_cyclic = image_user_settings(
|
||||
source_object
|
||||
)
|
||||
material = create_material(
|
||||
material_data_name(source_object),
|
||||
source_object.data,
|
||||
abs(float(source_object.scale.z)),
|
||||
shadeless=shadeless,
|
||||
frame_start=frame_start,
|
||||
color_frame_offset=frame_offset,
|
||||
color_frame_duration=frame_duration,
|
||||
use_cyclic=use_cyclic,
|
||||
scene=context.scene,
|
||||
)
|
||||
plane = create_mesh_plane_object(
|
||||
context,
|
||||
source_object.name,
|
||||
plane_bounds(source_object),
|
||||
material,
|
||||
matrix_world=(
|
||||
source_object.matrix_world if matrix_world is None else matrix_world
|
||||
),
|
||||
)
|
||||
return finish_mesh_plane(context, source_object, plane, keep_original)
|
||||
|
||||
|
||||
class ConvertToMeshPlane(bpy.types.Operator):
|
||||
bl_idname = "anyimage.convert_to_mesh_plane"
|
||||
bl_label = "Convert to Mesh Plane"
|
||||
bl_description = "Convert an image to an editable mesh plane without AI depth"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
shadeless: bpy.props.BoolProperty(
|
||||
name="Shadeless",
|
||||
description="Display the source color without scene lighting",
|
||||
default=False,
|
||||
)
|
||||
keep_original: bpy.props.BoolProperty(
|
||||
name="Keep Original",
|
||||
description="Keep the source Image Empty after conversion",
|
||||
default=False,
|
||||
)
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN"})
|
||||
source_matrix_world: bpy.props.FloatVectorProperty(
|
||||
size=16,
|
||||
default=(
|
||||
1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
),
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return is_image_empty(context.object) or is_mesh_plane_object(context.object)
|
||||
|
||||
def capture_source_state(self, source_object):
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_world = flatten_matrix(source_object.matrix_world)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
self.capture_source_state(context.object)
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
source_object = context.object
|
||||
if not self.source_object_name:
|
||||
if not is_image_empty(source_object):
|
||||
raise ValueError("Select an Image Empty to convert")
|
||||
self.capture_source_state(source_object)
|
||||
stored_source = bpy.data.objects.get(self.source_object_name)
|
||||
if is_image_empty(stored_source):
|
||||
source_object = stored_source
|
||||
elif not is_image_empty(source_object):
|
||||
raise ValueError("The source Image Empty is no longer available")
|
||||
build_mesh_plane(
|
||||
context,
|
||||
source_object,
|
||||
keep_original=self.keep_original,
|
||||
shadeless=self.shadeless,
|
||||
matrix_world=restore_matrix(self.source_matrix_world),
|
||||
)
|
||||
except (RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
@@ -1,8 +1,124 @@
|
||||
import bpy
|
||||
|
||||
from ..runtime import JobOperatorBase, runtime
|
||||
from ..properties import REQUIRED_MODEL_KEYS, model_record
|
||||
from .common import HIDDEN, require_environment
|
||||
from ..properties import (
|
||||
REQUIRED_MODEL_KEYS,
|
||||
ai_status,
|
||||
model_download_size,
|
||||
model_record,
|
||||
)
|
||||
from .common import ADDON_PACKAGE, HIDDEN, require_environment
|
||||
|
||||
|
||||
def format_download_size(size):
|
||||
return f"{size / 1_000_000:.0f} MB"
|
||||
|
||||
|
||||
class SetupAIEnvironment(bpy.types.Operator):
|
||||
bl_idname = "anyimage.setup_ai_environment"
|
||||
bl_label = "Set Up AI Environment"
|
||||
bl_description = "Install the environment and required AI models"
|
||||
|
||||
use_mirror: bpy.props.BoolProperty(
|
||||
name="Use Mirror",
|
||||
description=(
|
||||
"Use the China mirrors for uv, Python and Python packages; "
|
||||
"model download sources are unchanged"
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, _context):
|
||||
return not runtime.server_busy()
|
||||
|
||||
def invoke(self, context, _event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=440)
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
status = ai_status()
|
||||
if not status["environment_ready"]:
|
||||
required_size = sum(
|
||||
model_download_size(model_key)
|
||||
for model_key in REQUIRED_MODEL_KEYS
|
||||
)
|
||||
layout.label(
|
||||
text="AnyImage AI features run in an isolated Python environment."
|
||||
)
|
||||
layout.label(
|
||||
text="This installs Python, ONNX Runtime and supporting packages."
|
||||
)
|
||||
layout.label(
|
||||
text=(
|
||||
"It also downloads MoGe-2 ViT-S Normal and BEN2 Base "
|
||||
f"({format_download_size(required_size)})."
|
||||
)
|
||||
)
|
||||
layout.label(
|
||||
text="Allow about 600 MB of downloads and 1 GB of disk space."
|
||||
)
|
||||
layout.label(
|
||||
text=(
|
||||
"Installation usually takes 5–20 minutes, "
|
||||
"depending on network speed."
|
||||
)
|
||||
)
|
||||
layout.separator(type="LINE")
|
||||
layout.prop(self, "use_mirror")
|
||||
return
|
||||
|
||||
if status["ready"]:
|
||||
layout.label(text="The AI Environment is ready.", icon="CHECKMARK")
|
||||
return
|
||||
|
||||
labels = []
|
||||
for model_key in status["missing_models"]:
|
||||
model = model_record(model_key)
|
||||
labels.append(model["label"] if model else model_key)
|
||||
missing_size = sum(
|
||||
model_download_size(model_key)
|
||||
for model_key in status["missing_models"]
|
||||
)
|
||||
layout.label(
|
||||
text="The AI Environment is already installed.",
|
||||
icon="CHECKMARK",
|
||||
)
|
||||
layout.label(text=f"Required models to download: {', '.join(labels)}")
|
||||
layout.label(text=f"Download size: {format_download_size(missing_size)}")
|
||||
layout.label(text="The download usually takes 2–10 minutes.")
|
||||
|
||||
def execute(self, _context):
|
||||
status = ai_status()
|
||||
if status["ready"]:
|
||||
return {"FINISHED"}
|
||||
if status["environment_ready"]:
|
||||
return bpy.ops.anyimage.download_required_models("INVOKE_DEFAULT")
|
||||
installer = (
|
||||
bpy.ops.anyimage.install_environment_mirror
|
||||
if self.use_mirror
|
||||
else bpy.ops.anyimage.install_environment
|
||||
)
|
||||
return installer("INVOKE_DEFAULT")
|
||||
|
||||
|
||||
class OpenAIEnvironmentSettings(bpy.types.Operator):
|
||||
bl_idname = "anyimage.open_ai_environment_settings"
|
||||
bl_label = "AI Environment Settings"
|
||||
bl_description = "Open the AnyImage add-on preferences"
|
||||
|
||||
def execute(self, context):
|
||||
result = bpy.ops.screen.userpref_show("INVOKE_DEFAULT", section="ADDONS")
|
||||
if "FINISHED" not in result:
|
||||
return {"CANCELLED"}
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
if area.type != "PREFERENCES":
|
||||
continue
|
||||
with context.temp_override(window=window, area=area):
|
||||
bpy.ops.preferences.addon_show(module=ADDON_PACKAGE)
|
||||
return {"FINISHED"}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DownloadModel(JobOperatorBase, bpy.types.Operator):
|
||||
|
||||
@@ -9,6 +9,7 @@ from .common import (
|
||||
cleanup_temporary_input,
|
||||
da3_parameters,
|
||||
input_path,
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
push_undo,
|
||||
require_environment,
|
||||
@@ -548,17 +549,17 @@ class ConvertToSurface(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
from ..runtime import runtime
|
||||
from ..properties import required_models_ready
|
||||
|
||||
return (
|
||||
is_image_empty(context.object)
|
||||
and video_total_frames(context.object.data) == 1
|
||||
and runtime.environment_ready()
|
||||
and not runtime.server_busy()
|
||||
and required_models_ready()
|
||||
)
|
||||
|
||||
def invoke(self, context, _event):
|
||||
setup_result = invoke_ai_setup_if_needed()
|
||||
if setup_result is not None:
|
||||
return setup_result
|
||||
return context.window_manager.invoke_props_dialog(self, width=360)
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
Reference in New Issue
Block a user