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