Refactor Cutout and Image Editing Logic
- Renamed "Base Plane Depth" to "Reference Depth" across validate_cutout.py for consistency. - Introduced a new specification for Cutout mesh sampling to ensure source pixel spacing is preserved and limits are applied correctly. - Unified naming conventions for image editing results to maintain consistency across Remove Background, Upscale, and Crop operations. - Updated the handling of Refine Selection to ensure it retains the original bounds and does not apply unnecessary cropping. - Enhanced material generation logic to connect image color directly to IOR Level, improving the rendering of dark areas. - Established clear requirements for geometry depth controls and image result materials to ensure consistent behavior across different components. - Removed outdated logic and ensured all changes are documented and tested for regression.
This commit is contained in:
Binary file not shown.
@@ -405,7 +405,7 @@ def load_normal_result_image(path, source_object):
|
||||
return image
|
||||
|
||||
|
||||
def create_pixel_result_image(source_image, pixels, output_size, suffix=""):
|
||||
def create_image_edit_result(source_image, pixels, output_size):
|
||||
"""Create a packed Image datablock from edited RGBA pixels."""
|
||||
output_width, output_height = (int(value) for value in output_size)
|
||||
if output_width <= 0 or output_height <= 0:
|
||||
@@ -413,7 +413,7 @@ def create_pixel_result_image(source_image, pixels, output_size, suffix=""):
|
||||
if len(pixels) != output_width * output_height * 4:
|
||||
raise ValueError("The edited pixels do not match the image dimensions")
|
||||
image = bpy.data.images.new(
|
||||
f"{image_base_name(source_image)}{suffix}",
|
||||
source_image.name,
|
||||
width=output_width,
|
||||
height=output_height,
|
||||
alpha=True,
|
||||
@@ -525,11 +525,12 @@ def place_empty_image_result(
|
||||
|
||||
|
||||
|
||||
def load_color_result_image(
|
||||
def _load_packed_result_image(
|
||||
source_object,
|
||||
output_directory,
|
||||
*,
|
||||
frame_prefix="image.",
|
||||
frame_prefix,
|
||||
result_name,
|
||||
):
|
||||
output_directory = Path(output_directory)
|
||||
color_paths = sorted_frame_paths(output_directory, frame_prefix)
|
||||
@@ -555,7 +556,6 @@ def load_color_result_image(
|
||||
raise RuntimeError("Unable to load the color result") from error
|
||||
|
||||
try:
|
||||
result_name = color_data_name(source_object)
|
||||
if animated:
|
||||
pack_sequence_if_possible(result_image, len(color_paths))
|
||||
else:
|
||||
@@ -567,6 +567,36 @@ def load_color_result_image(
|
||||
return result_image
|
||||
|
||||
|
||||
def load_image_edit_result(
|
||||
source_object,
|
||||
output_directory,
|
||||
*,
|
||||
frame_prefix="image.",
|
||||
):
|
||||
"""Load one packed Job result that directly edits the source Image."""
|
||||
return _load_packed_result_image(
|
||||
source_object,
|
||||
output_directory,
|
||||
frame_prefix=frame_prefix,
|
||||
result_name=source_object.data.name,
|
||||
)
|
||||
|
||||
|
||||
def load_color_result_image(
|
||||
source_object,
|
||||
output_directory,
|
||||
*,
|
||||
frame_prefix="image.",
|
||||
):
|
||||
"""Load one packed Color texture result for a generated material."""
|
||||
return _load_packed_result_image(
|
||||
source_object,
|
||||
output_directory,
|
||||
frame_prefix=frame_prefix,
|
||||
result_name=color_data_name(source_object),
|
||||
)
|
||||
|
||||
|
||||
def image_empty_bounds(source_object):
|
||||
image_width, image_height = source_object.data.size
|
||||
if image_width <= 0 or image_height <= 0:
|
||||
|
||||
@@ -208,6 +208,10 @@ def create_image_material(
|
||||
material_inputs.outputs["Roughness"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Specular IOR Level"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ImageEditWarning(ValueError):
|
||||
@@ -86,6 +87,29 @@ class SelectionMask:
|
||||
return result
|
||||
|
||||
|
||||
def save_selection_mask(path, selection_mask):
|
||||
"""Save one local Selection Mask beside a prepared image input."""
|
||||
import numpy as np
|
||||
|
||||
path = Path(path).parent / "selection-mask.npy"
|
||||
np.save(path, selection_mask.values, allow_pickle=False)
|
||||
return path
|
||||
|
||||
|
||||
def load_selection_mask(path, bounds):
|
||||
"""Load a local Selection Mask sidecar without allowing object data."""
|
||||
import numpy as np
|
||||
|
||||
values = np.load(Path(path), allow_pickle=False)
|
||||
return SelectionMask(values, bounds)
|
||||
|
||||
|
||||
def cleanup_selection_mask(path):
|
||||
"""Remove a local Selection Mask sidecar."""
|
||||
if path:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def selection_path_bounds(points):
|
||||
import numpy as np
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ def create_depth_plane_object(
|
||||
("Subdivide", max(0, min(10, int(subdivisions)))),
|
||||
("Thickness", 0.1),
|
||||
("Depth Scale", 1.0),
|
||||
("Base Plane Depth", depth_metadata["reference_depth"] * uniform_scale),
|
||||
("Reference Depth", depth_metadata["reference_depth"] * uniform_scale),
|
||||
("Uniform Scale", uniform_scale),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
|
||||
@@ -18,7 +18,7 @@ from ...common.viewport import (
|
||||
)
|
||||
from ...common.selection import crop_pixels_to_alpha
|
||||
from ...common.image import (
|
||||
create_pixel_result_image,
|
||||
create_image_edit_result,
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
image_empty_bounds,
|
||||
@@ -829,7 +829,7 @@ class CropImagePerspective(bpy.types.Operator):
|
||||
quad,
|
||||
self.aspect_ratio,
|
||||
)
|
||||
result_image = create_pixel_result_image(
|
||||
result_image = create_image_edit_result(
|
||||
source_object.data,
|
||||
pixels,
|
||||
output_size,
|
||||
|
||||
@@ -11,8 +11,7 @@ from ...common.ai import (
|
||||
)
|
||||
from ...common.image import (
|
||||
cleanup_image_input,
|
||||
create_pixel_result_image,
|
||||
image_base_name,
|
||||
create_image_edit_result,
|
||||
image_rgba,
|
||||
load_packed_rgba_image,
|
||||
place_empty_image_result,
|
||||
@@ -28,7 +27,10 @@ from ...common.viewport import (
|
||||
)
|
||||
from ...common.selection import (
|
||||
ImageEditWarning,
|
||||
cleanup_selection_mask,
|
||||
load_selection_mask,
|
||||
rasterize_selection_path,
|
||||
save_selection_mask,
|
||||
)
|
||||
|
||||
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
@@ -64,6 +66,21 @@ def _edit_image_mask_pixels(
|
||||
)
|
||||
|
||||
|
||||
def _apply_local_selection_mask(source_rgba, selection_mask):
|
||||
"""Apply a same-sized Selection Mask without changing the image canvas."""
|
||||
import numpy as np
|
||||
|
||||
rgba = np.asarray(source_rgba, dtype=np.float32).copy()
|
||||
if rgba.ndim != 3 or rgba.shape[2] != 4:
|
||||
raise ValueError("The Refine result must contain RGBA pixels")
|
||||
if rgba.shape[:2] != selection_mask.values.shape:
|
||||
raise ValueError("The Refine result does not match the Selection")
|
||||
rgba[:, :, 3] *= selection_mask.values
|
||||
if not np.any(rgba[:, :, 3] > 0.0):
|
||||
raise ImageEditWarning("The edit contains no visible image pixels")
|
||||
return np.flipud(rgba).ravel()
|
||||
|
||||
|
||||
def _edit_image_object(
|
||||
context,
|
||||
source_object,
|
||||
@@ -78,7 +95,7 @@ def _edit_image_object(
|
||||
selection_mask,
|
||||
)
|
||||
left, top, right, bottom = crop_bounds
|
||||
result_image = create_pixel_result_image(
|
||||
result_image = create_image_edit_result(
|
||||
image,
|
||||
pixels,
|
||||
(right - left, bottom - top),
|
||||
@@ -145,13 +162,23 @@ class CropImageSelection(CropSelectionGesture, bpy.types.Operator):
|
||||
source_rgba,
|
||||
selection_mask.bounds,
|
||||
)
|
||||
try:
|
||||
selection_mask_path = save_selection_mask(
|
||||
source_path,
|
||||
selection_mask,
|
||||
)
|
||||
except Exception:
|
||||
cleanup_image_input(source_path, True)
|
||||
raise
|
||||
result = bpy.ops.anyimage.refine_crop_selection(
|
||||
input_path=str(source_path),
|
||||
selection_mask_path=str(selection_mask_path),
|
||||
source_object_name=self.source_object_name,
|
||||
selection_bounds_json=json.dumps(selection_mask.bounds),
|
||||
keep_original=keep_original,
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
cleanup_selection_mask(selection_mask_path)
|
||||
cleanup_image_input(source_path, True)
|
||||
return {"CANCELLED"}
|
||||
return {"FINISHED"}
|
||||
@@ -175,6 +202,7 @@ class RefineCropSelection(JobOperatorBase, bpy.types.Operator):
|
||||
starting_message = "Refining image selection"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
selection_mask_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
selection_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
keep_original: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
@@ -191,23 +219,23 @@ class RefineCropSelection(JobOperatorBase, bpy.types.Operator):
|
||||
def response(self, context, result):
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
|
||||
source_bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
local_bounds = tuple(
|
||||
json.loads(result.file("selection_bounds").read_text(encoding="utf-8"))
|
||||
)
|
||||
bounds = (
|
||||
source_bounds[0] + local_bounds[0],
|
||||
source_bounds[1] + local_bounds[1],
|
||||
source_bounds[0] + local_bounds[2],
|
||||
source_bounds[1] + local_bounds[3],
|
||||
)
|
||||
bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
selection_mask = load_selection_mask(self.selection_mask_path, bounds)
|
||||
source_image = source_object.data
|
||||
refined_image = load_packed_rgba_image(
|
||||
encoded_image = load_packed_rgba_image(
|
||||
result.file("selection_image"),
|
||||
source_image,
|
||||
image_base_name(source_image),
|
||||
"AnyImage Refine Result",
|
||||
)
|
||||
refined_image = None
|
||||
try:
|
||||
rgba = image_rgba(encoded_image)
|
||||
pixels = _apply_local_selection_mask(rgba, selection_mask)
|
||||
refined_image = create_image_edit_result(
|
||||
source_image,
|
||||
pixels,
|
||||
selection_mask.values.shape[::-1],
|
||||
)
|
||||
place_empty_image_result(
|
||||
context,
|
||||
source_object,
|
||||
@@ -216,14 +244,18 @@ class RefineCropSelection(JobOperatorBase, bpy.types.Operator):
|
||||
keep_original=bool(getattr(self, "keep_original", False)),
|
||||
)
|
||||
except Exception:
|
||||
if refined_image.users == 0:
|
||||
if refined_image is not None and refined_image.users == 0:
|
||||
bpy.data.images.remove(refined_image, do_unlink=True)
|
||||
raise
|
||||
finally:
|
||||
if encoded_image.users == 0:
|
||||
bpy.data.images.remove(encoded_image, do_unlink=True)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Crop Image Selection"):
|
||||
raise RuntimeError("Unable to create the Crop Image Selection Undo step")
|
||||
return "Crop Selection refined"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_selection_mask(getattr(self, "selection_mask_path", ""))
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ...preferences import addon_preferences
|
||||
from ...server.media.resolution import limited_image_size
|
||||
from ...common.image import image_empty_bounds
|
||||
|
||||
|
||||
@@ -1342,10 +1341,12 @@ def crop_plane_bounds(source_object, crop_bounds, source_size):
|
||||
|
||||
def extract_cutout_contours(
|
||||
content_values,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
):
|
||||
contour_resolution = max(512, min(int(mesh_resolution) * 8, 1024))
|
||||
height, width = content_values.shape
|
||||
sampling_resolution = max(width, height) / float(point_spacing)
|
||||
contour_resolution = max(512, min(round(sampling_resolution * 8), 1024))
|
||||
return extract_selection_contours(
|
||||
content_values,
|
||||
max_resolution=contour_resolution,
|
||||
@@ -1353,24 +1354,28 @@ def extract_cutout_contours(
|
||||
)
|
||||
|
||||
|
||||
def limit_cutout_mesh_resolution(mesh_resolution, crop_size, max_input_size):
|
||||
def limit_cutout_point_spacing(point_spacing, crop_size, max_mesh_resolution):
|
||||
crop_width, crop_height = (int(value) for value in crop_size)
|
||||
limited_width, limited_height = limited_image_size(
|
||||
(crop_width, crop_height),
|
||||
max_input_size,
|
||||
)
|
||||
scale = max(limited_width, limited_height) / max(crop_width, crop_height)
|
||||
return max(4, min(1024, round(int(mesh_resolution) * scale)))
|
||||
spacing = float(point_spacing)
|
||||
if crop_width <= 0 or crop_height <= 0:
|
||||
raise ValueError("The Cutout bounds must be positive")
|
||||
if spacing <= 0.0:
|
||||
raise ValueError("Cutout point spacing must be greater than zero")
|
||||
maximum = int(max_mesh_resolution)
|
||||
if maximum <= 0:
|
||||
raise ValueError("Maximum Cutout mesh resolution must be positive")
|
||||
return max(spacing, max(crop_width, crop_height) / maximum)
|
||||
|
||||
|
||||
def cutout_sampling_spacing(source_object, mesh_resolution):
|
||||
def cutout_sampling_spacing(source_object, point_spacing):
|
||||
import math
|
||||
|
||||
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
|
||||
metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
width = (x_max - x_min) * math.sqrt(max(metric[0][0], 0.0))
|
||||
height = (y_max - y_min) * math.sqrt(max(metric[1][1], 0.0))
|
||||
return max(width, height) / max(int(mesh_resolution), 8)
|
||||
image_width, image_height = (int(value) for value in source_object.data.size)
|
||||
return max(width / image_width, height / image_height) * float(point_spacing)
|
||||
|
||||
|
||||
def selection_calibration(content_values, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
|
||||
@@ -1397,7 +1402,7 @@ def selection_calibration(content_values, target_shape, threshold=CUTOUT_ALPHA_T
|
||||
def build_base_shape(
|
||||
context,
|
||||
source_object,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
):
|
||||
@@ -1410,7 +1415,7 @@ def build_base_shape(
|
||||
)
|
||||
selection_contours = extract_cutout_contours(
|
||||
content_values,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
)
|
||||
world_metric, world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
@@ -1418,7 +1423,7 @@ def build_base_shape(
|
||||
raise ValueError("The Image Empty has a degenerate world transform")
|
||||
preferences = addon_preferences(context)
|
||||
balloon_algorithm = getattr(preferences, "balloon_profile_algorithm", "POISSON")
|
||||
point_spacing = cutout_sampling_spacing(source_object, mesh_resolution)
|
||||
point_spacing = cutout_sampling_spacing(source_object, point_spacing)
|
||||
domain = cutout_domain(
|
||||
selection_contours,
|
||||
bounds,
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"""Cutout Selection validation and Shape-menu interaction helpers."""
|
||||
|
||||
from ...preferences import (
|
||||
MAX_CUTOUT_MESH_RESOLUTION,
|
||||
MIN_CUTOUT_MESH_RESOLUTION,
|
||||
)
|
||||
from ...properties import ai_ready
|
||||
from .shape import CUTOUT_SHAPE_LABELS
|
||||
|
||||
@@ -22,15 +18,6 @@ CUTOUT_MESH_DETAIL_SPACING = {
|
||||
}
|
||||
|
||||
|
||||
def cutout_mesh_resolution(image_size, point_spacing):
|
||||
longest_edge = max(int(value) for value in image_size)
|
||||
detail = round(longest_edge / max(int(point_spacing), 1))
|
||||
return max(
|
||||
MIN_CUTOUT_MESH_RESOLUTION,
|
||||
min(MAX_CUTOUT_MESH_RESOLUTION, detail),
|
||||
)
|
||||
|
||||
|
||||
def cutout_mesh_detail_spacing(mesh_detail):
|
||||
return CUTOUT_MESH_DETAIL_SPACING[str(mesh_detail)]
|
||||
|
||||
@@ -44,7 +31,7 @@ def _pie_button(
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
inward_axis,
|
||||
):
|
||||
operator = pie.operator(
|
||||
@@ -57,7 +44,7 @@ def _pie_button(
|
||||
operator.shape = shape
|
||||
operator.refine_selection = refine_selection
|
||||
operator.generate_normal = generate_normal
|
||||
operator.mesh_resolution = mesh_resolution
|
||||
operator.point_spacing = point_spacing
|
||||
operator.inward_axis = inward_axis
|
||||
|
||||
|
||||
@@ -77,7 +64,6 @@ def open_shape_pie(
|
||||
generate_normal = ready and bool(
|
||||
getattr(settings, "cutout_generate_normal", False)
|
||||
)
|
||||
mesh_resolution = cutout_mesh_resolution(source_object.data.size, point_spacing)
|
||||
inward_axis = str(getattr(settings, "cutout_inward_axis", "NEGATIVE_Z"))
|
||||
|
||||
def draw(menu, _context):
|
||||
@@ -93,7 +79,7 @@ def open_shape_pie(
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
inward_axis,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ import json
|
||||
import bpy
|
||||
|
||||
from ...preferences import (
|
||||
MAX_CUTOUT_MESH_RESOLUTION,
|
||||
MIN_CUTOUT_MESH_RESOLUTION,
|
||||
addon_preferences,
|
||||
configured_max_ai_input_size,
|
||||
configured_max_cutout_mesh_resolution,
|
||||
@@ -60,7 +58,7 @@ from ...common.selection import (
|
||||
from .geometry import (
|
||||
CUTOUT_ALPHA_THRESHOLD,
|
||||
build_base_shape,
|
||||
limit_cutout_mesh_resolution,
|
||||
limit_cutout_point_spacing,
|
||||
)
|
||||
from .interaction import open_shape_pie
|
||||
from .object import (
|
||||
@@ -95,7 +93,7 @@ def create_cutout_shape(
|
||||
context,
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
*,
|
||||
@@ -110,7 +108,7 @@ def create_cutout_shape(
|
||||
base_shape = build_base_shape(
|
||||
context,
|
||||
source_object,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
)
|
||||
@@ -118,7 +116,7 @@ def create_cutout_shape(
|
||||
context,
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
base_shape,
|
||||
@@ -164,21 +162,8 @@ def _generated_color_result(result, input_path, source_object, source_bounds):
|
||||
value = result.value if isinstance(result.value, dict) else {}
|
||||
if "selection_image" not in value:
|
||||
path = input_path
|
||||
bounds = source_bounds
|
||||
else:
|
||||
path = result.file("selection_image")
|
||||
local_bounds = tuple(
|
||||
int(value)
|
||||
for value in json.loads(
|
||||
result.file("selection_bounds").read_text(encoding="utf-8")
|
||||
)
|
||||
)
|
||||
bounds = (
|
||||
source_bounds[0] + local_bounds[0],
|
||||
source_bounds[1] + local_bounds[1],
|
||||
source_bounds[0] + local_bounds[2],
|
||||
source_bounds[1] + local_bounds[3],
|
||||
)
|
||||
image = load_packed_rgba_image(
|
||||
path,
|
||||
source_object.data,
|
||||
@@ -190,7 +175,7 @@ def _generated_color_result(result, input_path, source_object, source_bounds):
|
||||
if image.users == 0:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise
|
||||
return image, alpha, bounds
|
||||
return image, alpha, source_bounds
|
||||
|
||||
|
||||
def cutout_geometry_settings(context):
|
||||
@@ -215,10 +200,9 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
selection_path_json: bpy.props.StringProperty(options=_HIDDEN)
|
||||
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
|
||||
shape: bpy.props.StringProperty(options=_HIDDEN)
|
||||
mesh_resolution: bpy.props.IntProperty(
|
||||
default=128,
|
||||
min=MIN_CUTOUT_MESH_RESOLUTION,
|
||||
max=MAX_CUTOUT_MESH_RESOLUTION,
|
||||
point_spacing: bpy.props.FloatProperty(
|
||||
default=32.0,
|
||||
min=0.01,
|
||||
options=_HIDDEN,
|
||||
)
|
||||
model: bpy.props.StringProperty(
|
||||
@@ -274,8 +258,8 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
source_object.select_set(True)
|
||||
crop_width = selection_mask.bounds[2] - selection_mask.bounds[0]
|
||||
crop_height = selection_mask.bounds[3] - selection_mask.bounds[1]
|
||||
self.mesh_resolution = limit_cutout_mesh_resolution(
|
||||
self.mesh_resolution,
|
||||
self.point_spacing = limit_cutout_point_spacing(
|
||||
self.point_spacing,
|
||||
(crop_width, crop_height),
|
||||
configured_max_cutout_mesh_resolution(context),
|
||||
)
|
||||
@@ -295,7 +279,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
context,
|
||||
source_object,
|
||||
self.shape,
|
||||
self.mesh_resolution,
|
||||
self.point_spacing,
|
||||
content_values,
|
||||
selection_mask.bounds,
|
||||
inward_axis=self.inward_axis,
|
||||
@@ -387,20 +371,14 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
depth_image = None
|
||||
shape_started = False
|
||||
try:
|
||||
if "selection_image" in value:
|
||||
content_values = _cutout_content_values(
|
||||
source_alpha,
|
||||
)
|
||||
content_bounds = color_bounds
|
||||
else:
|
||||
selection_mask = getattr(self, "_selection_mask", None)
|
||||
if selection_mask is None:
|
||||
raise RuntimeError("The Cutout Selection is no longer available")
|
||||
content_values = _cutout_content_values(
|
||||
source_alpha,
|
||||
selection_mask.values,
|
||||
)
|
||||
content_bounds = selection_mask.bounds
|
||||
selection_mask = getattr(self, "_selection_mask", None)
|
||||
if selection_mask is None:
|
||||
raise RuntimeError("The Cutout Selection is no longer available")
|
||||
content_values = _cutout_content_values(
|
||||
source_alpha,
|
||||
selection_mask.values,
|
||||
)
|
||||
content_bounds = color_bounds
|
||||
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
|
||||
normal_key = f"{normal_mode.value.lower()}_normal"
|
||||
if normal_key in value:
|
||||
@@ -424,7 +402,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
context,
|
||||
source_object,
|
||||
self.shape,
|
||||
self.mesh_resolution,
|
||||
self.point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
depth_image=depth_image,
|
||||
|
||||
@@ -156,7 +156,7 @@ def create_shape_object(
|
||||
context,
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
point_spacing,
|
||||
content_values,
|
||||
content_bounds,
|
||||
base_shape,
|
||||
@@ -183,7 +183,7 @@ def create_shape_object(
|
||||
)
|
||||
world_metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
cleanup_value = cutout_cleanup_value(
|
||||
cutout_sampling_spacing(source_object, mesh_resolution),
|
||||
cutout_sampling_spacing(source_object, point_spacing),
|
||||
world_metric,
|
||||
)
|
||||
material = None
|
||||
@@ -293,7 +293,7 @@ def create_shape_object(
|
||||
for name, value in (
|
||||
("Depth Scale", 1.0),
|
||||
("Uniform Scale", uniform_scale),
|
||||
("Base Plane Depth", depth_reference),
|
||||
("Reference Depth", depth_reference),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
set_modifier_input(
|
||||
|
||||
@@ -71,13 +71,13 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
|
||||
def apply_background_response(result, source_object_name):
|
||||
from ..common.image import (
|
||||
load_color_result_image,
|
||||
load_image_edit_result,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
)
|
||||
|
||||
source_object = require_image_empty(source_object_name)
|
||||
result_image = load_color_result_image(source_object, result.directory)
|
||||
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")
|
||||
|
||||
@@ -19,7 +19,7 @@ from ..common.image import (
|
||||
cleanup_image_input,
|
||||
default_extract_frames,
|
||||
is_image_empty,
|
||||
load_color_result_image,
|
||||
load_image_edit_result,
|
||||
prepare_image_input,
|
||||
source_start_frame,
|
||||
replace_empty_image,
|
||||
@@ -148,7 +148,7 @@ class UpscaleImage(JobOperatorBase, bpy.types.Operator):
|
||||
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_color_result_image(
|
||||
result_image = load_image_edit_result(
|
||||
source_object,
|
||||
result.directory,
|
||||
frame_prefix="upscale.",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Generate optional AI files from one rectangular Cutout image input."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ..geometry.prediction_artifacts import generate_moge2_artifacts
|
||||
@@ -15,22 +14,14 @@ def run(context, parameters):
|
||||
result = {}
|
||||
if parameters.get("refine_selection"):
|
||||
context.progress(0.2, "Refining Selection")
|
||||
selection_image_path, bounds = refine_selection(
|
||||
selection_image_path = refine_selection(
|
||||
context,
|
||||
input_path,
|
||||
parameters["refine_model"],
|
||||
parameters["device"],
|
||||
context.directory / "selection.png",
|
||||
)
|
||||
bounds_path = context.directory / "selection_bounds.json"
|
||||
bounds_path.write_text(
|
||||
json.dumps(bounds, separators=(",", ":")),
|
||||
encoding="utf-8",
|
||||
)
|
||||
result.update(
|
||||
selection_image=selection_image_path.name,
|
||||
selection_bounds=bounds_path.name,
|
||||
)
|
||||
result["selection_image"] = selection_image_path.name
|
||||
|
||||
depth_mode = str(parameters.get("depth_mode", "NONE"))
|
||||
normal_mode = str(parameters.get("normal_mode", "NONE"))
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from ..selection import refine_selection
|
||||
|
||||
|
||||
def run(context, parameters):
|
||||
"""Refine a gesture mask with BEN2 and return its source-image bounds."""
|
||||
"""Refine foreground Alpha while preserving the submitted bounds canvas."""
|
||||
started = time.perf_counter()
|
||||
context.progress(0.05, "Preparing image Selection")
|
||||
context.check_cancelled()
|
||||
output_directory = context.directory
|
||||
output_directory.mkdir(parents=True, exist_ok=True)
|
||||
context.progress(0.2, f"Loading BEN2 for {parameters['device'].upper()}")
|
||||
output_path, bounds = refine_selection(
|
||||
output_path = refine_selection(
|
||||
context,
|
||||
parameters["input"],
|
||||
parameters["model"],
|
||||
parameters["device"],
|
||||
output_directory / "image.png",
|
||||
)
|
||||
bounds_path = output_directory / "bounds.json"
|
||||
bounds_path.write_text(json.dumps(bounds, separators=(",", ":")), encoding="utf-8")
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||
message = f"Image Selection refined in {elapsed_ms:.0f} ms"
|
||||
context.progress(1.0, message)
|
||||
return {"selection_image": output_path.name, "selection_bounds": bounds_path.name}
|
||||
return {"selection_image": output_path.name}
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
from ..models.ben2 import infer_foreground
|
||||
|
||||
|
||||
def _crop_to_alpha(image):
|
||||
import numpy as np
|
||||
|
||||
pixels = np.asarray(image.convert("RGBA"), dtype=np.uint8)
|
||||
visible = pixels[:, :, 3] > 0
|
||||
visible_y = np.flatnonzero(np.any(visible, axis=1))
|
||||
visible_x = np.flatnonzero(np.any(visible, axis=0))
|
||||
if not len(visible_x):
|
||||
raise ValueError("The generated Selection contains no visible pixels")
|
||||
bounds = (
|
||||
int(visible_x[0]),
|
||||
int(visible_y[0]),
|
||||
int(visible_x[-1]) + 1,
|
||||
int(visible_y[-1]) + 1,
|
||||
)
|
||||
return image.crop(bounds), bounds
|
||||
|
||||
|
||||
def refine_selection(
|
||||
context,
|
||||
input_path,
|
||||
@@ -37,6 +19,5 @@ def refine_selection(
|
||||
preserve_input_alpha=True,
|
||||
)
|
||||
context.check_cancelled()
|
||||
output, bounds = _crop_to_alpha(foreground)
|
||||
output.save(output_path, format="PNG", compress_level=1)
|
||||
return output_path, bounds
|
||||
foreground.convert("RGBA").save(output_path, format="PNG", compress_level=1)
|
||||
return output_path
|
||||
|
||||
Reference in New Issue
Block a user