Enhance image editing tests and functionality
- Added parameterized tests for image edit replacement to ensure alpha mode preservation. - Refactored existing tests to utilize new image editing methods and improve clarity. - Introduced new tests for image target functionality, ensuring proper handling of shared textures and context. - Updated various tests to validate alpha mode handling across different scenarios. - Improved undo/redo functionality in image operators to maintain state consistency. - Added support for texture node actions, enhancing integration with Blender's node system. - Ensured that image properties are preserved during operations and that invalid contexts are handled gracefully.
This commit is contained in:
@@ -10,14 +10,8 @@ from ..common.ai import (
|
||||
require_model,
|
||||
)
|
||||
|
||||
from ..common.image import (
|
||||
cleanup_image_input,
|
||||
default_extract_frames,
|
||||
is_image_empty,
|
||||
prepare_image_input,
|
||||
source_start_frame,
|
||||
video_total_frames,
|
||||
)
|
||||
from ..common.image import cleanup_image_input
|
||||
from ..common.image_target import ImageEditTarget, image_edit_owner
|
||||
|
||||
|
||||
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
@@ -26,14 +20,14 @@ _HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
class RemoveImageBackground(bpy.types.Operator):
|
||||
bl_idname = "anyimage.remove_image_background"
|
||||
bl_label = "Remove Background"
|
||||
bl_description = "Remove the background from this Image Empty with BEN2"
|
||||
bl_description = "Remove the image background with BEN2"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
from ..runtime import runtime
|
||||
|
||||
return is_image_empty(context.object) and not runtime.server_busy()
|
||||
return image_edit_owner(context) is not None and not runtime.server_busy()
|
||||
|
||||
def invoke(self, _context, _event):
|
||||
setup_result = invoke_ai_setup_if_needed()
|
||||
@@ -41,58 +35,35 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
return setup_result
|
||||
return self.execute(_context)
|
||||
|
||||
def execute(self, context):
|
||||
source_object = context.object
|
||||
total_frames = video_total_frames(source_object.data)
|
||||
if total_frames > 1:
|
||||
context.scene.anyimage_settings.video_frames = (
|
||||
default_extract_frames(source_object)
|
||||
)
|
||||
try:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_object.data,
|
||||
context.scene,
|
||||
)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
|
||||
result = bpy.ops.anyimage.run_background_removal(
|
||||
input_path=str(source_path),
|
||||
source_object_name=source_object.name,
|
||||
delete_input=delete_input,
|
||||
)
|
||||
def execute(self, _context):
|
||||
result = bpy.ops.anyimage.run_background_removal()
|
||||
if "CANCELLED" not in result:
|
||||
return {"FINISHED"}
|
||||
if delete_input:
|
||||
cleanup_image_input(source_path, True)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
def apply_background_response(result, source_object_name):
|
||||
from ..common.image import (
|
||||
load_image_edit_result,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
)
|
||||
|
||||
source_object = require_image_empty(source_object_name)
|
||||
result_image = load_image_edit_result(source_object, result.directory)
|
||||
replace_empty_image(source_object, result_image)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Remove Background"):
|
||||
raise RuntimeError("Unable to create the Remove Background Undo step")
|
||||
|
||||
|
||||
class RunBackgroundRemoval(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.run_background_removal"
|
||||
bl_label = "Run Background Removal"
|
||||
job_type = "remove-background"
|
||||
starting_message = "Starting Image Empty background removal"
|
||||
starting_message = "Starting image background removal"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
|
||||
delete_input: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
self._image_target = ImageEditTarget.capture(context)
|
||||
source_path, self.delete_input = self._image_target.prepare(context)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
self.input_path = str(source_path)
|
||||
result = super().execute(context)
|
||||
if "CANCELLED" in result:
|
||||
self.cleanup()
|
||||
return result
|
||||
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
require_model(BEN2_MODEL_KEY)
|
||||
@@ -102,12 +73,15 @@ class RunBackgroundRemoval(JobOperatorBase, bpy.types.Operator):
|
||||
"model": BEN2_MODEL_KEY,
|
||||
"device": production_device().lower(),
|
||||
"video_frames": settings.video_frames,
|
||||
"start_frame": source_start_frame(self.source_object_name),
|
||||
"start_frame": self._image_target.start_frame,
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
apply_background_response(result, self.source_object_name)
|
||||
self._image_target.apply(result.directory)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Remove Background"):
|
||||
raise RuntimeError("Unable to create the Remove Background Undo step")
|
||||
return "Image background is removed"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_image_input(self.input_path, self.delete_input)
|
||||
self._image_target = None
|
||||
|
||||
@@ -15,17 +15,8 @@ from ..common.ai import (
|
||||
require_environment,
|
||||
require_model,
|
||||
)
|
||||
from ..common.image import (
|
||||
cleanup_image_input,
|
||||
default_extract_frames,
|
||||
is_image_empty,
|
||||
load_image_edit_result,
|
||||
prepare_image_input,
|
||||
source_start_frame,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from ..common.image import cleanup_image_input
|
||||
from ..common.image_target import ImageEditTarget, image_edit_owner, owner_image
|
||||
|
||||
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
|
||||
@@ -49,10 +40,10 @@ def upscale_input_size_allowed(image, max_input_size):
|
||||
class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.upscale_image"
|
||||
bl_label = "Upscale"
|
||||
bl_description = "Upscale this Image Empty by 2x with an AI model"
|
||||
bl_description = "Upscale the image by 2x with an AI model"
|
||||
bl_options = {"UNDO"}
|
||||
job_type = "upscale-image"
|
||||
starting_message = "Starting Image Empty upscale"
|
||||
starting_message = "Starting image upscale"
|
||||
|
||||
model: bpy.props.StringProperty(
|
||||
default="REALESRGAN_GENERAL_WDN_X4V3",
|
||||
@@ -69,19 +60,17 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
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
|
||||
|
||||
source_object = getattr(context, "object", None)
|
||||
owner = image_edit_owner(context)
|
||||
return (
|
||||
is_image_empty(source_object)
|
||||
owner is not None
|
||||
and upscale_input_size_allowed(
|
||||
source_object.data,
|
||||
owner_image(owner),
|
||||
configured_max_ai_input_size(context),
|
||||
)
|
||||
and not runtime.server_busy()
|
||||
@@ -96,11 +85,12 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
self.model = configured_upscale_model(context)
|
||||
self.max_input_size = configured_max_ai_input_size(context)
|
||||
source_object = context.object
|
||||
if not is_image_empty(source_object):
|
||||
self.report({"ERROR"}, "Select an Image Empty to upscale")
|
||||
try:
|
||||
self._image_target = ImageEditTarget.capture(context)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
source_image = source_object.data
|
||||
source_image = self._image_target.image
|
||||
if not upscale_input_size_allowed(source_image, self.max_input_size):
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
@@ -108,26 +98,17 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
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)
|
||||
)
|
||||
try:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_image,
|
||||
context.scene,
|
||||
)
|
||||
source_path, delete_input = self._image_target.prepare(context)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.input_path = str(source_path)
|
||||
self.source_object_name = source_object.name
|
||||
self.delete_input = delete_input
|
||||
result = super().execute(context)
|
||||
if "CANCELLED" in result:
|
||||
cleanup_image_input(self.input_path, self.delete_input)
|
||||
self.cleanup()
|
||||
return result
|
||||
|
||||
def request(self, context):
|
||||
@@ -139,23 +120,15 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
"model": self.model,
|
||||
"device": production_device().lower(),
|
||||
"video_frames": settings.video_frames,
|
||||
"start_frame": source_start_frame(self.source_object_name),
|
||||
"start_frame": self._image_target.start_frame,
|
||||
"max_input_size": int(self.max_input_size),
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
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 source image is no longer available")
|
||||
result_image = load_image_edit_result(
|
||||
source_object,
|
||||
result.directory,
|
||||
frame_prefix="upscale.",
|
||||
)
|
||||
replace_empty_image(source_object, result_image)
|
||||
self._image_target.apply(result.directory, frame_prefix="upscale.")
|
||||
model_label = upscale_model_label(self.model)
|
||||
return f"Image is upscaled with {model_label}"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_image_input(self.input_path, self.delete_input)
|
||||
self._image_target = None
|
||||
|
||||
Reference in New Issue
Block a user