Files
AnyImage/src/anyimage/preferences.py
nan 7e55af6722 Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation
- 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.
2026-09-06 21:43:47 +08:00

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"]