feat: Implement cutout tool and depth generation features

- Added a new cutout tool for selecting regions in images, allowing users to choose mesh shapes based on selected areas.
- Introduced depth generation capabilities for rasterized images, including normal data extraction.
- Created jobs for generating depth planes and region data, enhancing the AI data generation workflow.
- Developed a selection refinement process for cutout regions, improving accuracy in generated outputs.
- Established a new region module for handling rasterized image regions and their associated AI generation tasks.
- Enhanced node graph builders for asset generation, facilitating smoother integration of new features.
This commit is contained in:
2026-08-30 23:09:13 +08:00
parent 32bcd45d7b
commit 08309f719d
88 changed files with 4145 additions and 5639 deletions

View File

@@ -8,25 +8,43 @@ from ..server.media.resolution import (
MIN_MAX_AI_INPUT_SIZE,
)
from ..runtime import JobOperatorBase
from .common.operator import (
HIDDEN,
cleanup_temporary_input,
input_path,
from .common.ai_job import (
require_input_path,
invoke_ai_setup_if_needed,
production_device,
require_environment,
require_model,
source_start_frame,
)
from .common.image import (
create_color_image,
from .common.image_data import (
cleanup_image_input,
default_extract_frames,
image_base_name,
is_image_empty,
load_color_result_image,
prepare_image_input,
source_start_frame,
replace_empty_image,
require_image_empty,
video_total_frames,
)
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
UPSCALE_FACTOR = 4
def upscale_output_size(image):
width, height = (int(value) for value in getattr(image, "size", (0, 0)))
if width <= 0 or height <= 0:
return None
return width * UPSCALE_FACTOR, height * UPSCALE_FACTOR
def upscale_input_size_allowed(image, max_input_size):
width, height = (int(value) for value in getattr(image, "size", (0, 0)))
if width <= 0 or height <= 0:
return True
return max(width, height) < int(max_input_size)
class UpscaleImage(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.upscale_image"
@@ -38,7 +56,7 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
model: bpy.props.StringProperty(
default="REALESRGAN_GENERAL_WDN_X4V3",
options=HIDDEN,
options=_HIDDEN,
)
max_input_size: bpy.props.IntProperty(
name="Max AI Input Size",
@@ -48,18 +66,26 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
max=MAX_MAX_AI_INPUT_SIZE,
step=256,
subtype="PIXEL",
options=HIDDEN,
options=_HIDDEN,
)
input_path: bpy.props.StringProperty(options=HIDDEN)
source_object_name: bpy.props.StringProperty(options=HIDDEN)
source_image_name: bpy.props.StringProperty(options=HIDDEN)
delete_input: bpy.props.BoolProperty(default=False, options=HIDDEN)
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
source_image_name: bpy.props.StringProperty(options=_HIDDEN)
delete_input: bpy.props.BoolProperty(default=False, options=_HIDDEN)
@classmethod
def poll(cls, context):
from ..runtime import runtime
return is_image_empty(context.object) and not runtime.server_busy()
source_object = getattr(context, "object", None)
return (
is_image_empty(source_object)
and upscale_input_size_allowed(
source_object.data,
configured_max_ai_input_size(context),
)
and not runtime.server_busy()
)
def invoke(self, context, _event):
setup_result = invoke_ai_setup_if_needed()
@@ -74,23 +100,15 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
if not is_image_empty(source_object):
self.report({"ERROR"}, "Select an Image Empty to upscale")
return {"CANCELLED"}
current_image = source_object.data
try:
original_image = getattr(
current_image,
"anyimage_upscale_source",
None,
)
except ReferenceError:
original_image = None
source_image = original_image or current_image
self.source_image_name = source_image.name
self._rebuild_from_original = source_image is not current_image
if self._rebuild_from_original:
source_image = source_object.data
if not upscale_input_size_allowed(source_image, self.max_input_size):
self.report(
{"WARNING"},
"This image was already upscaled; rebuilding from the original",
{"ERROR"},
"Image long edge must be smaller than Maximum AI Input Size "
f"({self.max_input_size} px)",
)
return {"CANCELLED"}
self.source_image_name = source_image.name
if video_total_frames(source_image) > 1:
context.scene.anyimage_settings.video_frames = (
default_extract_frames(source_object)
@@ -109,7 +127,7 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
self.delete_input = delete_input
result = super().execute(context)
if "CANCELLED" in result:
cleanup_temporary_input(self.input_path, self.delete_input)
cleanup_image_input(self.input_path, self.delete_input)
return result
def request(self, context):
@@ -117,7 +135,7 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
require_model(self.model)
settings = context.scene.anyimage_settings
return {
"input": str(input_path(self.input_path)),
"input": str(require_input_path(self.input_path)),
"model": self.model,
"device": production_device().lower(),
"video_frames": settings.video_frames,
@@ -126,28 +144,18 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
}
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")
source_object = require_image_empty(self.source_object_name)
source_image = bpy.data.images.get(self.source_image_name)
if source_image is None:
raise RuntimeError("The original source image is no longer available")
replaced_image = source_object.data
result_image = create_color_image(
raise RuntimeError("The source image is no longer available")
result_image = load_color_result_image(
source_object,
result.directory,
frame_prefix="upscale.",
)
if replaced_image is not source_image:
replaced_image.use_fake_user = False
if int(replaced_image.users) == 0:
bpy.data.images.remove(replaced_image, do_unlink=True)
result_image.name = f"{image_base_name(source_image)}_upscale.png"
result_image.anyimage_upscale_source = source_image
replace_empty_image(source_object, result_image)
model_label = upscale_model_label(self.model)
if getattr(self, "_rebuild_from_original", False):
return f"Image is rebuilt from the original with {model_label}"
return f"Image is upscaled with {model_label}"
def cleanup(self):
cleanup_temporary_input(self.input_path, self.delete_input)
cleanup_image_input(self.input_path, self.delete_input)