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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user