- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior. - Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations. - Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
318 lines
9.7 KiB
Python
318 lines
9.7 KiB
Python
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
from .server.media.resolution import (
|
|
DEFAULT_MAX_AI_INPUT_SIZE,
|
|
MAX_MAX_AI_INPUT_SIZE,
|
|
MIN_MAX_AI_INPUT_SIZE,
|
|
)
|
|
from .server.model_catalog import (
|
|
DEFAULT_UPSCALE_MODEL_KEY,
|
|
UPSCALE_MODELS,
|
|
)
|
|
|
|
DEFAULT_MOGE2_MODEL_KEY = "MOGE2_VITS_NORMAL"
|
|
DEFAULT_BACKGROUND_MODEL_KEY = "BEN2_BASE"
|
|
DEFAULT_MAX_CUTOUT_MESH_RESOLUTION = 1024
|
|
MIN_CUTOUT_MESH_RESOLUTION = 4
|
|
MAX_CUTOUT_MESH_RESOLUTION = 1024
|
|
DEFAULT_MAX_FRAME_RESOLUTION = 2048
|
|
MIN_FRAME_RESOLUTION = 64
|
|
MAX_FRAME_RESOLUTION = 8192
|
|
|
|
ADDON_PACKAGE = __package__
|
|
DEFAULT_STORAGE_ROOT = Path.home() / ".anyimage"
|
|
UPSCALE_MODEL_ITEMS = tuple(
|
|
(model.key, model.label, model.description, index)
|
|
for index, model in enumerate(UPSCALE_MODELS.values())
|
|
)
|
|
|
|
BACKGROUND_MODEL_ITEMS = (
|
|
(
|
|
DEFAULT_BACKGROUND_MODEL_KEY,
|
|
"BEN2 Base",
|
|
"Model used for AI Remove Background",
|
|
),
|
|
)
|
|
|
|
BALLOON_PROFILE_ALGORITHM_ITEMS = (
|
|
(
|
|
"POISSON",
|
|
"Poisson",
|
|
"Smooth cotangent-Laplacian Balloon profile",
|
|
),
|
|
("TEDDY", "Teddy", "Chordal-axis Balloon profile"),
|
|
)
|
|
|
|
|
|
def addon_preferences(context=None):
|
|
preference_context = (
|
|
context if getattr(context, "preferences", None) is not None else bpy.context
|
|
)
|
|
addon = preference_context.preferences.addons.get(ADDON_PACKAGE)
|
|
return getattr(addon, "preferences", None)
|
|
|
|
|
|
def configured_storage_root():
|
|
configured = getattr(addon_preferences(), "storage_root", "")
|
|
if configured:
|
|
return Path(bpy.path.abspath(configured))
|
|
return DEFAULT_STORAGE_ROOT
|
|
|
|
|
|
def configured_material_view_adaptation(context=None):
|
|
return bool(
|
|
getattr(addon_preferences(context), "adapt_material_to_view_transform", True)
|
|
)
|
|
|
|
|
|
def configured_max_cutout_mesh_resolution(context=None):
|
|
preferences = addon_preferences(context)
|
|
value = int(
|
|
getattr(
|
|
preferences,
|
|
"max_cutout_mesh_resolution",
|
|
DEFAULT_MAX_CUTOUT_MESH_RESOLUTION,
|
|
)
|
|
)
|
|
return min(
|
|
max(value, MIN_CUTOUT_MESH_RESOLUTION),
|
|
MAX_CUTOUT_MESH_RESOLUTION,
|
|
)
|
|
|
|
|
|
def configured_max_frame_resolution(context=None):
|
|
preferences = addon_preferences(context)
|
|
value = int(
|
|
getattr(
|
|
preferences,
|
|
"max_frame_resolution",
|
|
DEFAULT_MAX_FRAME_RESOLUTION,
|
|
)
|
|
)
|
|
return min(max(value, MIN_FRAME_RESOLUTION), MAX_FRAME_RESOLUTION)
|
|
|
|
|
|
def configured_max_ai_input_size(context=None):
|
|
preferences = addon_preferences(context)
|
|
value = int(getattr(preferences, "max_ai_input_size", DEFAULT_MAX_AI_INPUT_SIZE))
|
|
return min(max(value, MIN_MAX_AI_INPUT_SIZE), MAX_MAX_AI_INPUT_SIZE)
|
|
|
|
|
|
def device_items(owner, context):
|
|
from .properties import device_enum_items
|
|
|
|
return device_enum_items(owner, context)
|
|
|
|
|
|
def geometry_model_items(owner, context):
|
|
from .properties import geometry_model_enum_items
|
|
|
|
return geometry_model_enum_items(owner, context)
|
|
|
|
|
|
class AnyImagePreferences(bpy.types.AddonPreferences):
|
|
bl_idname = ADDON_PACKAGE
|
|
|
|
adapt_material_to_view_transform: bpy.props.BoolProperty(
|
|
name="Adapt to Scene View Transform",
|
|
description=(
|
|
"Use the scene view transform's inverse color space with Straight alpha "
|
|
"for new material color textures; otherwise use sRGB with Premultiplied alpha"
|
|
),
|
|
default=True,
|
|
)
|
|
storage_root: bpy.props.StringProperty(
|
|
name="Storage Root",
|
|
description=(
|
|
"Shared user-writable storage for the environment, models and jobs; "
|
|
"all Blender versions can use the same directory"
|
|
),
|
|
subtype="DIR_PATH",
|
|
default=str(DEFAULT_STORAGE_ROOT),
|
|
)
|
|
device: bpy.props.EnumProperty(
|
|
name="Device",
|
|
description=(
|
|
"Inference device used by production tasks; only devices supported "
|
|
"on this machine are listed"
|
|
),
|
|
items=device_items,
|
|
default=0,
|
|
)
|
|
upscale_model: bpy.props.EnumProperty(
|
|
name="Upscale Image Model",
|
|
description="Model used to upscale images",
|
|
items=UPSCALE_MODEL_ITEMS,
|
|
default=DEFAULT_UPSCALE_MODEL_KEY,
|
|
)
|
|
max_ai_input_size: bpy.props.IntProperty(
|
|
name="Maximum AI Input Size",
|
|
description=(
|
|
"Maximum source long edge eligible for Upscale and maximum long edge "
|
|
"used by Depth, Normal and Selection refinement"
|
|
),
|
|
default=DEFAULT_MAX_AI_INPUT_SIZE,
|
|
min=MIN_MAX_AI_INPUT_SIZE,
|
|
max=MAX_MAX_AI_INPUT_SIZE,
|
|
step=256,
|
|
subtype="PIXEL",
|
|
)
|
|
max_cutout_mesh_resolution: bpy.props.IntProperty(
|
|
name="Maximum Cutout Mesh Resolution",
|
|
description="Maximum sampling resolution along the longest Cutout edge",
|
|
default=DEFAULT_MAX_CUTOUT_MESH_RESOLUTION,
|
|
min=MIN_CUTOUT_MESH_RESOLUTION,
|
|
max=MAX_CUTOUT_MESH_RESOLUTION,
|
|
step=64,
|
|
subtype="PIXEL",
|
|
)
|
|
max_frame_resolution: bpy.props.IntProperty(
|
|
name="Maximum Frame Resolution",
|
|
description="Maximum output resolution along the longest Frame edge",
|
|
default=DEFAULT_MAX_FRAME_RESOLUTION,
|
|
min=MIN_FRAME_RESOLUTION,
|
|
max=MAX_FRAME_RESOLUTION,
|
|
step=128,
|
|
subtype="PIXEL",
|
|
)
|
|
balloon_profile_algorithm: bpy.props.EnumProperty(
|
|
name="Balloon Profile",
|
|
description="Algorithm used to generate o_balloon for every Cutout shape",
|
|
items=BALLOON_PROFILE_ALGORITHM_ITEMS,
|
|
default="POISSON",
|
|
)
|
|
background_model: bpy.props.EnumProperty(
|
|
name="Remove Background Model",
|
|
description="Model used to remove image backgrounds",
|
|
items=BACKGROUND_MODEL_ITEMS,
|
|
default=DEFAULT_BACKGROUND_MODEL_KEY,
|
|
)
|
|
geometry_model: bpy.props.EnumProperty(
|
|
name="Generate Depth Map Model",
|
|
description="Model used to generate depth maps",
|
|
items=geometry_model_items,
|
|
default=0,
|
|
)
|
|
geometry_resolution_level: bpy.props.IntProperty(
|
|
name="MoGe Resolution Level",
|
|
description="MoGe-2 inference resolution used for geometry and normal generation",
|
|
default=5,
|
|
min=0,
|
|
max=9,
|
|
)
|
|
|
|
def draw(self, _context):
|
|
from .properties import ai_setup_label, ai_status, model_catalog
|
|
from .runtime import runtime
|
|
|
|
layout = self.layout
|
|
status = ai_status()
|
|
models = {model["key"]: model for model in model_catalog()}
|
|
|
|
server = self._draw_box(layout, "Server")
|
|
server.prop(self, "storage_root")
|
|
server_row = server.row(align=True)
|
|
if not status["environment_ready"]:
|
|
setup_row = server_row.row(align=True)
|
|
setup_row.enabled = not runtime.server_busy()
|
|
setup_row.operator(
|
|
"anyimage.setup_ai_environment",
|
|
text=ai_setup_label(status),
|
|
icon="IMPORT",
|
|
)
|
|
else:
|
|
server_row.label(text="Installed", icon="CHECKMARK")
|
|
server_row.operator(
|
|
"anyimage.open_server_log",
|
|
text="Open Server Log",
|
|
icon="TEXT",
|
|
)
|
|
|
|
if status["environment_ready"]:
|
|
ai = self._draw_box(layout, "AI")
|
|
ai.prop(self, "device")
|
|
ai.prop(self, "max_ai_input_size")
|
|
ai.separator(type="LINE")
|
|
self._draw_model_property(
|
|
ai,
|
|
"geometry_model",
|
|
models,
|
|
DEFAULT_MOGE2_MODEL_KEY,
|
|
"AI Generate Depth Map",
|
|
)
|
|
self._draw_model_property(
|
|
ai,
|
|
"background_model",
|
|
models,
|
|
DEFAULT_BACKGROUND_MODEL_KEY,
|
|
"AI Remove Background",
|
|
)
|
|
self._draw_model_property(
|
|
ai,
|
|
"upscale_model",
|
|
models,
|
|
DEFAULT_UPSCALE_MODEL_KEY,
|
|
"AI Upscale Image",
|
|
)
|
|
|
|
shading = self._draw_box(layout, "Shading")
|
|
shading.prop(self, "adapt_material_to_view_transform")
|
|
|
|
tools = self._draw_box(layout, "Tools")
|
|
tools.label(text="Frame Tool")
|
|
tools.prop(self, "max_frame_resolution")
|
|
tools.separator(type="LINE")
|
|
tools.label(text="Depth & Normal")
|
|
tools.prop(self, "geometry_resolution_level")
|
|
tools.separator(type="LINE")
|
|
tools.label(text="Cutout Tool")
|
|
tools.prop(self, "max_cutout_mesh_resolution")
|
|
tools.prop(self, "balloon_profile_algorithm")
|
|
|
|
@staticmethod
|
|
def _draw_box(layout, label):
|
|
box = layout.box()
|
|
box.label(text=label)
|
|
return box
|
|
|
|
def _draw_model_property(
|
|
self,
|
|
layout,
|
|
property_name,
|
|
models,
|
|
default_model_key,
|
|
ready_label,
|
|
):
|
|
model_key = getattr(self, property_name, default_model_key)
|
|
if model_key not in models:
|
|
model_key = default_model_key
|
|
split = layout.split(factor=0.58, align=True)
|
|
split.column(align=True).prop(self, property_name, text="")
|
|
self._draw_model_status(
|
|
split.column(align=True),
|
|
models.get(model_key),
|
|
ready_label,
|
|
)
|
|
|
|
@staticmethod
|
|
def _draw_model_status(layout, model, ready_label):
|
|
from .runtime import runtime
|
|
|
|
row = layout.row(align=True)
|
|
if model is None:
|
|
row.label(text="Unavailable", icon="ERROR")
|
|
return
|
|
if model.get("ready"):
|
|
row.label(text=ready_label, icon="CHECKMARK")
|
|
return
|
|
download_row = row.row(align=True)
|
|
download_row.enabled = not runtime.server_busy()
|
|
operator = download_row.operator(
|
|
"anyimage.download_model",
|
|
text="Download",
|
|
icon="IMPORT",
|
|
)
|
|
operator.model = model["key"]
|