Add image selection refinement functionality with BEN2 integration
- Implemented viewport.py for handling 3D view projections, gestures, and overlay drawing. - Created refine_image_selection.py to manage the refinement of gesture masks and output image bounds. - Added selection module with refine_selection.py to process and crop images based on alpha channel visibility. - Introduced __init__.py for selection module to expose refine_selection function.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from .remove_background import RemoveImageBackground, RunBackgroundRemoval
|
||||
from .cutout_tool import (
|
||||
CutoutRegionToShape,
|
||||
SelectCutoutRegion,
|
||||
CutoutSelectionToShape,
|
||||
SelectCutoutSelection,
|
||||
)
|
||||
from .debug import DebugBen2, DebugDa3, DebugMoge2, DebugUpscale
|
||||
from .image_tool import CLASSES as IMAGE_TOOL_CLASSES
|
||||
@@ -31,8 +31,8 @@ CLASSES = (
|
||||
DebugBen2,
|
||||
DebugUpscale,
|
||||
GenerateDepthPlane,
|
||||
CutoutRegionToShape,
|
||||
SelectCutoutRegion,
|
||||
CutoutSelectionToShape,
|
||||
SelectCutoutSelection,
|
||||
RunBackgroundRemoval,
|
||||
ClearModels,
|
||||
*IMAGE_TOOL_CLASSES,
|
||||
|
||||
@@ -8,7 +8,7 @@ from ..properties import (
|
||||
model_download_size,
|
||||
model_record,
|
||||
)
|
||||
from ..common.ai_job import require_environment
|
||||
from ..common.ai import require_environment
|
||||
|
||||
|
||||
def format_download_size(size):
|
||||
|
||||
@@ -8,6 +8,7 @@ import bpy
|
||||
CLIPBOARD_HASH_PROPERTY = "anyimage_clipboard_sha256"
|
||||
CLIPBOARD_TEXTURE_PROPERTY = "anyimage_clipboard_texture"
|
||||
DEFAULT_STENCIL_SIZE = 256.0
|
||||
REFERENCE_IMAGE_SIZE = 5.0
|
||||
NODE_HEADER_HEIGHT = 20.0
|
||||
NODE_TYPES = {
|
||||
"CompositorNodeTree": "CompositorNodeImage",
|
||||
@@ -173,7 +174,7 @@ def add_reference_image(context, image, location=None):
|
||||
reference = bpy.data.objects.new(image.name, None)
|
||||
reference.empty_display_type = "IMAGE"
|
||||
reference.data = image
|
||||
reference.empty_display_size = 2.0
|
||||
reference.empty_display_size = REFERENCE_IMAGE_SIZE
|
||||
context.collection.objects.link(reference)
|
||||
_place_in_view(context, reference, location)
|
||||
_select_only(context, reference)
|
||||
|
||||
@@ -8,7 +8,7 @@ from ...server.media.resolution import (
|
||||
MAX_MAX_AI_INPUT_SIZE,
|
||||
MIN_MAX_AI_INPUT_SIZE,
|
||||
)
|
||||
from ...common.ai_job import (
|
||||
from ...common.ai import (
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
production_device,
|
||||
@@ -16,7 +16,7 @@ from ...common.ai_job import (
|
||||
require_input_path,
|
||||
require_model,
|
||||
)
|
||||
from ...common.image_data import (
|
||||
from ...common.image import (
|
||||
cleanup_image_input,
|
||||
image_user_settings,
|
||||
is_image_empty,
|
||||
@@ -24,7 +24,7 @@ from ...common.image_data import (
|
||||
source_start_frame,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.object import create_image_material
|
||||
from ...common.material import create_image_material
|
||||
from .object import (
|
||||
create_color_image,
|
||||
create_depth_plane_from_result,
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||
import bpy
|
||||
|
||||
from ...common.image_data import (
|
||||
from ...common.image import (
|
||||
color_data_name,
|
||||
image_empty_bounds,
|
||||
image_pixels,
|
||||
load_color_result_image,
|
||||
require_image_empty,
|
||||
)
|
||||
from ...common.depth_data import (
|
||||
from ...common.depth import (
|
||||
depth_uniform_scale,
|
||||
load_depth_metadata,
|
||||
load_depth_result_image,
|
||||
)
|
||||
from ...common.material import create_image_material
|
||||
from ...common.object import (
|
||||
create_image_material,
|
||||
finalize_object_result,
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .main import CutoutRegionToShape, CutoutTool, SelectCutoutRegion
|
||||
from .main import CutoutSelectionToShape, CutoutTool, SelectCutoutSelection
|
||||
|
||||
__all__ = (
|
||||
"CutoutRegionToShape",
|
||||
"CutoutSelectionToShape",
|
||||
"CutoutTool",
|
||||
"SelectCutoutRegion",
|
||||
"SelectCutoutSelection",
|
||||
)
|
||||
|
||||
@@ -5,11 +5,11 @@ from dataclasses import dataclass
|
||||
|
||||
from ...preferences import addon_preferences
|
||||
from ...server.media.resolution import limited_image_size
|
||||
from ...common.image_data import image_empty_bounds
|
||||
from ...common.image import image_empty_bounds
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegionContours:
|
||||
class SelectionContours:
|
||||
components: tuple
|
||||
image_size: tuple[int, int]
|
||||
|
||||
@@ -22,11 +22,11 @@ class RegionContours:
|
||||
)
|
||||
image_size = tuple(int(value) for value in self.image_size)
|
||||
if not components or any(not component for component in components):
|
||||
raise ValueError("The Region has no usable contour components")
|
||||
raise ValueError("The Selection has no usable contour components")
|
||||
if any(len(contour) < 3 for component in components for contour in component):
|
||||
raise ValueError("A Region contour has fewer than three points")
|
||||
raise ValueError("A Selection contour has fewer than three points")
|
||||
if len(image_size) != 2 or min(image_size) <= 0:
|
||||
raise ValueError("The Region contour image size is invalid")
|
||||
raise ValueError("The Selection contour image size is invalid")
|
||||
object.__setattr__(self, "components", components)
|
||||
object.__setattr__(self, "image_size", image_size)
|
||||
|
||||
@@ -57,7 +57,7 @@ def _simplify_axis_aligned_contour(points):
|
||||
return simplified
|
||||
|
||||
|
||||
def _trace_region_contours(mask):
|
||||
def _trace_selection_contours(mask):
|
||||
import numpy as np
|
||||
|
||||
mask = np.asarray(mask, dtype=bool)
|
||||
@@ -89,7 +89,7 @@ def _trace_region_contours(mask):
|
||||
for y, x in np.argwhere(left):
|
||||
add_edge((int(x), int(y) + 1), (int(x), int(y)))
|
||||
if not edges:
|
||||
raise ValueError("The Cutout region has no usable boundary")
|
||||
raise ValueError("The Cutout selection has no usable boundary")
|
||||
|
||||
contours = []
|
||||
edge_count = sum(len(values) for values in edges.values())
|
||||
@@ -117,7 +117,7 @@ def _trace_region_contours(mask):
|
||||
if len(contour) >= 3:
|
||||
contours.append(contour)
|
||||
if not contours:
|
||||
raise ValueError("The Cutout region is too small")
|
||||
raise ValueError("The Cutout selection is too small")
|
||||
return contours
|
||||
|
||||
|
||||
@@ -143,13 +143,13 @@ def _point_inside_contour(point, contour):
|
||||
return inside
|
||||
|
||||
|
||||
def _group_region_contours(mask):
|
||||
contours = _trace_region_contours(mask)
|
||||
def _group_selection_contours(mask):
|
||||
contours = _trace_selection_contours(mask)
|
||||
outer = [contour for contour in contours if _signed_contour_area(contour) > 0.0]
|
||||
holes = [contour for contour in contours if _signed_contour_area(contour) < 0.0]
|
||||
if not outer:
|
||||
raise ValueError("The Cutout region has no outer boundary")
|
||||
regions = [[contour] for contour in outer]
|
||||
raise ValueError("The Cutout selection has no outer boundary")
|
||||
components = [[contour] for contour in outer]
|
||||
outer_areas = [abs(_signed_contour_area(contour)) for contour in outer]
|
||||
for hole in holes:
|
||||
start = hole[0]
|
||||
@@ -167,10 +167,10 @@ def _group_region_contours(mask):
|
||||
if _point_inside_contour(sample, contour)
|
||||
]
|
||||
if not owners:
|
||||
raise ValueError("The Cutout hole has no enclosing region")
|
||||
raise ValueError("The Cutout hole has no enclosing selection component")
|
||||
owner = min(owners, key=outer_areas.__getitem__)
|
||||
regions[owner].append(hole)
|
||||
return regions
|
||||
components[owner].append(hole)
|
||||
return components
|
||||
|
||||
|
||||
def _resize_values(values, size):
|
||||
@@ -202,10 +202,10 @@ def _resize_values(values, size):
|
||||
)
|
||||
|
||||
|
||||
def extract_region_contours(region_mask, max_resolution=512, threshold=0.5):
|
||||
def extract_selection_contours(selection_mask, max_resolution=512, threshold=0.5):
|
||||
import numpy as np
|
||||
|
||||
values = np.asarray(region_mask.values)
|
||||
values = np.asarray(selection_mask.values)
|
||||
if values.ndim != 2:
|
||||
raise ValueError("Cutout Alpha must be two-dimensional")
|
||||
if np.issubdtype(values.dtype, np.floating):
|
||||
@@ -218,10 +218,10 @@ def extract_region_contours(region_mask, max_resolution=512, threshold=0.5):
|
||||
if sampled_size != source_size:
|
||||
values = _resize_values(values, sampled_size)
|
||||
mask = values >= 255.0 * threshold
|
||||
components = _group_region_contours(mask)
|
||||
components = _group_selection_contours(mask)
|
||||
scale_x = source_size[0] / sampled_size[0]
|
||||
scale_y = source_size[1] / sampled_size[1]
|
||||
return RegionContours(
|
||||
return SelectionContours(
|
||||
components=tuple(
|
||||
tuple(
|
||||
tuple(
|
||||
@@ -257,17 +257,17 @@ def resample_closed_contour(contour, spacing):
|
||||
|
||||
points = np.asarray(contour, dtype=np.float64)
|
||||
if len(points) < 3:
|
||||
raise ValueError("The lasso does not define a valid image region")
|
||||
raise ValueError("The lasso does not define a valid image selection")
|
||||
segments = np.roll(points, -1, axis=0) - points
|
||||
lengths = np.linalg.norm(segments, axis=1)
|
||||
points = points[lengths > 1e-8]
|
||||
if len(points) < 3:
|
||||
raise ValueError("The lasso region is too small to create a Cutout")
|
||||
raise ValueError("The selection is too small to create a Cutout")
|
||||
segments = np.roll(points, -1, axis=0) - points
|
||||
lengths = np.linalg.norm(segments, axis=1)
|
||||
perimeter = float(lengths.sum())
|
||||
if perimeter <= spacing * 3.0:
|
||||
raise ValueError("The lasso region is too small to create a Cutout")
|
||||
raise ValueError("The selection is too small to create a Cutout")
|
||||
count = max(3, int(round(perimeter / spacing)))
|
||||
distances = np.arange(count, dtype=np.float64) * perimeter / count
|
||||
cumulative = np.concatenate(([0.0], np.cumsum(lengths)))
|
||||
@@ -276,7 +276,7 @@ def resample_closed_contour(contour, spacing):
|
||||
return points[indices] + segments[indices] * factors[:, None]
|
||||
|
||||
|
||||
def smooth_region_contour(contour, spacing, iterations=16):
|
||||
def smooth_selection_contour(contour, spacing, iterations=16):
|
||||
contour = resample_closed_contour(contour, spacing * 0.25)
|
||||
original_area = abs(contour_area(contour))
|
||||
for _index in range(iterations):
|
||||
@@ -509,30 +509,30 @@ def prepare_planar_contours(
|
||||
if perimeter <= 1e-12:
|
||||
continue
|
||||
contour_spacing = min(spacing, perimeter / 8.0)
|
||||
smoothed = smooth_region_contour(planar, contour_spacing)
|
||||
smoothed = smooth_selection_contour(planar, contour_spacing)
|
||||
smoothed_local = smoothed @ np.linalg.inv(transform)
|
||||
smoothed_local[:, 0] = np.clip(smoothed_local[:, 0], x_min, x_max)
|
||||
smoothed_local[:, 1] = np.clip(smoothed_local[:, 1], y_min, y_max)
|
||||
prepared.append(smoothed_local @ transform)
|
||||
if not prepared:
|
||||
raise ValueError("The Cutout has no region large enough for this resolution")
|
||||
raise ValueError("The Cutout has no selection large enough for this resolution")
|
||||
interior = uniform_interior_points_for_contours(prepared, spacing)
|
||||
return prepared, interior, spacing
|
||||
|
||||
|
||||
def _prepare_region_contours(
|
||||
region_contours,
|
||||
def _prepare_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=POINT_SPACING,
|
||||
world_metric=None,
|
||||
):
|
||||
prepared = []
|
||||
errors = []
|
||||
for component in region_contours.components:
|
||||
for component in selection_contours.components:
|
||||
try:
|
||||
contours, interior, _spacing = prepare_planar_contours(
|
||||
component,
|
||||
region_contours.image_size,
|
||||
selection_contours.image_size,
|
||||
local_bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
@@ -544,7 +544,7 @@ def _prepare_region_contours(
|
||||
if not prepared:
|
||||
if errors:
|
||||
raise errors[0]
|
||||
raise ValueError("The Cutout has no usable region")
|
||||
raise ValueError("The Cutout has no usable selection")
|
||||
return prepared, spacing
|
||||
|
||||
|
||||
@@ -582,7 +582,7 @@ def _triangulate_contour_component(contours, interior, spacing):
|
||||
centers = planar_points[faces].mean(axis=1)
|
||||
faces = faces[points_inside_contours(centers, contours)]
|
||||
if not len(faces):
|
||||
raise ValueError("Unable to triangulate the Cutout regions")
|
||||
raise ValueError("Unable to triangulate the Cutout selection components")
|
||||
used = np.unique(faces)
|
||||
if len(used) != len(planar_points):
|
||||
remap = np.full(len(planar_points), -1, dtype=np.int32)
|
||||
@@ -614,8 +614,8 @@ def boundary_vertices_from_faces(vertex_count, faces):
|
||||
return boundary
|
||||
|
||||
|
||||
def triangulate_region_contours(
|
||||
region_contours,
|
||||
def triangulate_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=POINT_SPACING,
|
||||
world_metric=None,
|
||||
@@ -623,8 +623,8 @@ def triangulate_region_contours(
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
prepared, spacing = _prepare_region_contours(
|
||||
region_contours,
|
||||
prepared, spacing = _prepare_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
@@ -635,7 +635,7 @@ def triangulate_region_contours(
|
||||
components = []
|
||||
offset = 0
|
||||
for contours, interior in prepared:
|
||||
region_points, region_faces, region_boundary = _triangulate_contour_component(
|
||||
component_points, component_faces, component_boundary = _triangulate_contour_component(
|
||||
contours,
|
||||
interior,
|
||||
spacing,
|
||||
@@ -643,17 +643,17 @@ def triangulate_region_contours(
|
||||
components.append(
|
||||
(
|
||||
offset,
|
||||
offset + len(region_points),
|
||||
region_faces,
|
||||
offset + len(component_points),
|
||||
component_faces,
|
||||
contours[0],
|
||||
)
|
||||
)
|
||||
points.append(region_points)
|
||||
faces.extend(tuple(offset + value for value in face) for face in region_faces)
|
||||
boundaries.append(region_boundary)
|
||||
offset += len(region_points)
|
||||
points.append(component_points)
|
||||
faces.extend(tuple(offset + value for value in face) for face in component_faces)
|
||||
boundaries.append(component_boundary)
|
||||
offset += len(component_points)
|
||||
if not points:
|
||||
raise ValueError("Unable to triangulate the Cutout regions")
|
||||
raise ValueError("Unable to triangulate the Cutout selection components")
|
||||
result = (
|
||||
np.concatenate(points, axis=0),
|
||||
faces,
|
||||
@@ -1065,9 +1065,9 @@ DEFAULT_DEPTH_BASELINE = 8.0
|
||||
DEFAULT_DEPTH_DIRECTION = (0.0, 0.0, 1.0)
|
||||
|
||||
|
||||
def use_adaptive_depth_calibration(region_transparent_fraction):
|
||||
"""Return whether the Region input has enough transparent context."""
|
||||
return float(region_transparent_fraction) >= ADAPTIVE_DEPTH_TRANSPARENCY_RATIO
|
||||
def use_adaptive_depth_calibration(selection_transparent_fraction):
|
||||
"""Return whether the Selection input has enough transparent context."""
|
||||
return float(selection_transparent_fraction) >= ADAPTIVE_DEPTH_TRANSPARENCY_RATIO
|
||||
|
||||
|
||||
def metric_depth_reference(camera_depth, valid, uniform_scale):
|
||||
@@ -1253,7 +1253,7 @@ def orient_cutout_vertices(vertices):
|
||||
|
||||
|
||||
def cutout_domain(
|
||||
region_contours,
|
||||
selection_contours,
|
||||
bounds,
|
||||
spacing,
|
||||
world_metric,
|
||||
@@ -1263,8 +1263,8 @@ def cutout_domain(
|
||||
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
|
||||
planar, planar_faces, boundary, components = triangulate_region_contours(
|
||||
region_contours,
|
||||
planar, planar_faces, boundary, components = triangulate_selection_contours(
|
||||
selection_contours,
|
||||
bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
@@ -1341,13 +1341,13 @@ def crop_plane_bounds(source_object, crop_bounds, source_size):
|
||||
|
||||
|
||||
def extract_cutout_contours(
|
||||
region_mask,
|
||||
selection_mask,
|
||||
mesh_resolution,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
):
|
||||
contour_resolution = max(512, min(int(mesh_resolution) * 8, 1024))
|
||||
return extract_region_contours(
|
||||
region_mask,
|
||||
return extract_selection_contours(
|
||||
selection_mask,
|
||||
max_resolution=contour_resolution,
|
||||
threshold=threshold,
|
||||
)
|
||||
@@ -1363,7 +1363,7 @@ def limit_cutout_mesh_resolution(mesh_resolution, crop_size, max_input_size):
|
||||
return max(4, min(1024, round(int(mesh_resolution) * scale)))
|
||||
|
||||
|
||||
def cutout_point_spacing(source_object, mesh_resolution):
|
||||
def cutout_sampling_spacing(source_object, mesh_resolution):
|
||||
import math
|
||||
|
||||
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
|
||||
@@ -1373,11 +1373,11 @@ def cutout_point_spacing(source_object, mesh_resolution):
|
||||
return max(width, height) / max(int(mesh_resolution), 8)
|
||||
|
||||
|
||||
def region_calibration(region_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
|
||||
"""Return the final Region mask and transparency ratio at geometry resolution."""
|
||||
def selection_calibration(selection_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
|
||||
"""Return the final Selection mask and transparency ratio at geometry resolution."""
|
||||
import numpy as np
|
||||
|
||||
values = np.asarray(region_mask.values, dtype=np.float32)
|
||||
values = np.asarray(selection_mask.values, dtype=np.float32)
|
||||
target_height, target_width = (int(value) for value in target_shape)
|
||||
source_height, source_width = values.shape
|
||||
y = np.minimum(
|
||||
@@ -1394,16 +1394,16 @@ def region_calibration(region_mask, target_shape, threshold=CUTOUT_ALPHA_THRESHO
|
||||
return valid, transparent_fraction
|
||||
|
||||
|
||||
def build_base_shape(context, source_object, mesh_resolution, region_mask):
|
||||
def build_base_shape(context, source_object, mesh_resolution, selection_mask):
|
||||
import numpy as np
|
||||
|
||||
bounds = crop_plane_bounds(
|
||||
source_object,
|
||||
region_mask.bounds,
|
||||
selection_mask.bounds,
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
region_contours = extract_cutout_contours(
|
||||
region_mask,
|
||||
selection_contours = extract_cutout_contours(
|
||||
selection_mask,
|
||||
mesh_resolution,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
)
|
||||
@@ -1412,9 +1412,9 @@ def build_base_shape(context, source_object, mesh_resolution, region_mask):
|
||||
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_point_spacing(source_object, mesh_resolution)
|
||||
point_spacing = cutout_sampling_spacing(source_object, mesh_resolution)
|
||||
domain = cutout_domain(
|
||||
region_contours,
|
||||
selection_contours,
|
||||
bounds,
|
||||
point_spacing,
|
||||
world_metric,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Cutout Region validation and Shape-menu interaction helpers."""
|
||||
"""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 ...common.image_data import image_rgba
|
||||
from ...common.image_region import rasterize_region_path
|
||||
from ...common.image import image_rgba
|
||||
from ...common.selection import rasterize_selection_path
|
||||
from .geometry import CUTOUT_ALPHA_THRESHOLD
|
||||
from .shape import CUTOUT_SHAPE_LABELS
|
||||
|
||||
@@ -17,16 +17,23 @@ CUTOUT_SHAPE_BUTTONS = (
|
||||
("DEPTH_SURFACE", "SURFACE_NCURVE"),
|
||||
)
|
||||
|
||||
CUTOUT_MESH_DETAIL_SPACING = {
|
||||
"LOW": 32,
|
||||
"MEDIUM": 16,
|
||||
"HIGH": 8,
|
||||
"ULTRA": 4,
|
||||
}
|
||||
|
||||
def validate_cutout_region(
|
||||
|
||||
def validate_cutout_selection(
|
||||
source_object,
|
||||
region_path,
|
||||
selection_path,
|
||||
):
|
||||
image = source_object.data
|
||||
rgba = image_rgba(image)
|
||||
rasterize_region_path(
|
||||
rasterize_selection_path(
|
||||
tuple(image.size),
|
||||
region_path,
|
||||
selection_path,
|
||||
source_alpha=rgba[:, :, 3],
|
||||
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
antialias=True,
|
||||
@@ -42,26 +49,30 @@ def cutout_mesh_resolution(image_size, point_spacing):
|
||||
)
|
||||
|
||||
|
||||
def cutout_mesh_detail_spacing(mesh_detail):
|
||||
return CUTOUT_MESH_DETAIL_SPACING[str(mesh_detail)]
|
||||
|
||||
|
||||
def _pie_button(
|
||||
pie,
|
||||
shape,
|
||||
label,
|
||||
icon,
|
||||
source_name,
|
||||
region_path_json,
|
||||
refine_region,
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
):
|
||||
operator = pie.operator(
|
||||
"anyimage.cutout_region_to_shape",
|
||||
"anyimage.cutout_selection_to_shape",
|
||||
text=label,
|
||||
icon=icon,
|
||||
)
|
||||
operator.source_object_name = source_name
|
||||
operator.region_path_json = region_path_json
|
||||
operator.selection_path_json = selection_path_json
|
||||
operator.shape = shape
|
||||
operator.refine_region = refine_region
|
||||
operator.refine_selection = refine_selection
|
||||
operator.generate_normal = generate_normal
|
||||
operator.mesh_resolution = mesh_resolution
|
||||
|
||||
@@ -70,15 +81,15 @@ def open_shape_pie(
|
||||
context,
|
||||
event,
|
||||
source_object,
|
||||
region_path,
|
||||
refine_region=False,
|
||||
selection_path,
|
||||
refine_selection=False,
|
||||
):
|
||||
source_name = source_object.name
|
||||
region_path_json = region_path.to_json()
|
||||
selection_path_json = selection_path.to_json()
|
||||
settings = context.scene.anyimage_settings
|
||||
point_spacing = settings.cutout_point_spacing
|
||||
point_spacing = cutout_mesh_detail_spacing(settings.cutout_mesh_detail)
|
||||
ready = ai_ready()
|
||||
refine_region = ready and refine_region
|
||||
refine_selection = ready and refine_selection
|
||||
generate_normal = ready and bool(
|
||||
getattr(settings, "cutout_generate_normal", False)
|
||||
)
|
||||
@@ -94,8 +105,8 @@ def open_shape_pie(
|
||||
CUTOUT_SHAPE_LABELS[shape],
|
||||
icon,
|
||||
source_name,
|
||||
region_path_json,
|
||||
refine_region,
|
||||
selection_path_json,
|
||||
refine_selection,
|
||||
generate_normal,
|
||||
mesh_resolution,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Convert one image Region into a Cutout Shape."""
|
||||
"""Convert one image Selection into a Cutout Shape."""
|
||||
|
||||
import json
|
||||
|
||||
@@ -14,7 +14,7 @@ from ...preferences import (
|
||||
from ...properties import (
|
||||
BEN2_MODEL_KEY,
|
||||
DEFAULT_MOGE2_MODEL_KEY,
|
||||
refine_region_property,
|
||||
refine_selection_property,
|
||||
)
|
||||
from ...runtime import JobOperatorBase
|
||||
from ...server.media.resolution import (
|
||||
@@ -22,7 +22,9 @@ from ...server.media.resolution import (
|
||||
MAX_MAX_AI_INPUT_SIZE,
|
||||
MIN_MAX_AI_INPUT_SIZE,
|
||||
)
|
||||
from ...common.ai_job import (
|
||||
from ...common.ai import (
|
||||
draw_ai_property,
|
||||
draw_ai_setup,
|
||||
invoke_ai_setup_if_needed,
|
||||
moge2_parameters,
|
||||
production_device,
|
||||
@@ -30,38 +32,36 @@ from ...common.ai_job import (
|
||||
require_input_path,
|
||||
require_model,
|
||||
)
|
||||
from ...common.image_data import (
|
||||
from ...common.image import (
|
||||
cleanup_image_input,
|
||||
image_rgba,
|
||||
load_normal_result_image,
|
||||
prepare_region_input,
|
||||
prepare_selection_input,
|
||||
require_image_empty,
|
||||
source_start_frame,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.depth_data import load_depth_metadata, load_depth_result_image
|
||||
from ...common.image_interaction import (
|
||||
ImageRegionGesture,
|
||||
apply_region_settings,
|
||||
draw_ai_property,
|
||||
draw_ai_setup,
|
||||
from ...common.depth import load_depth_metadata, load_depth_result_image
|
||||
from ...common.viewport import (
|
||||
ImageSelectionGesture,
|
||||
apply_selection_settings,
|
||||
gesture_property,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
serialize_matrix,
|
||||
)
|
||||
from ...common.image_region import (
|
||||
from ...common.selection import (
|
||||
ImageEditWarning,
|
||||
RegionMask,
|
||||
RegionPath,
|
||||
rasterize_region_path,
|
||||
SelectionMask,
|
||||
SelectionPath,
|
||||
rasterize_selection_path,
|
||||
)
|
||||
from .geometry import (
|
||||
CUTOUT_ALPHA_THRESHOLD,
|
||||
build_base_shape,
|
||||
limit_cutout_mesh_resolution,
|
||||
)
|
||||
from .interaction import open_shape_pie, validate_cutout_region
|
||||
from .interaction import open_shape_pie, validate_cutout_selection
|
||||
from .object import NormalMode, create_shape_object
|
||||
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
|
||||
|
||||
@@ -89,7 +89,7 @@ def create_cutout_shape(
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
*,
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
@@ -101,14 +101,14 @@ def create_cutout_shape(
|
||||
context,
|
||||
source_object,
|
||||
mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
)
|
||||
result = create_shape_object(
|
||||
context,
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
base_shape,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
@@ -122,10 +122,10 @@ def create_cutout_shape(
|
||||
return result
|
||||
|
||||
|
||||
def _region_mask_from_image(path, bounds):
|
||||
def _selection_mask_from_image(path, bounds):
|
||||
image = bpy.data.images.load(str(path), check_existing=False)
|
||||
try:
|
||||
return RegionMask(
|
||||
return SelectionMask(
|
||||
image_rgba(image)[:, :, 3],
|
||||
tuple(int(value) for value in bounds),
|
||||
)
|
||||
@@ -133,13 +133,13 @@ def _region_mask_from_image(path, bounds):
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
|
||||
|
||||
def _generated_region_mask(result, input_path, source_bounds):
|
||||
if not isinstance(result.value, dict) or "region_image" not in result.value:
|
||||
return _region_mask_from_image(input_path, source_bounds)
|
||||
def _generated_selection_mask(result, input_path, source_bounds):
|
||||
if not isinstance(result.value, dict) or "selection_image" not in result.value:
|
||||
return _selection_mask_from_image(input_path, source_bounds)
|
||||
local_bounds = tuple(
|
||||
int(value)
|
||||
for value in json.loads(
|
||||
result.file("region_bounds").read_text(encoding="utf-8")
|
||||
result.file("selection_bounds").read_text(encoding="utf-8")
|
||||
)
|
||||
)
|
||||
bounds = (
|
||||
@@ -148,7 +148,7 @@ def _generated_region_mask(result, input_path, source_bounds):
|
||||
source_bounds[0] + local_bounds[2],
|
||||
source_bounds[1] + local_bounds[3],
|
||||
)
|
||||
return _region_mask_from_image(result.file("region_image"), bounds)
|
||||
return _selection_mask_from_image(result.file("selection_image"), bounds)
|
||||
|
||||
|
||||
def cutout_geometry_settings(context):
|
||||
@@ -160,18 +160,18 @@ def cutout_geometry_settings(context):
|
||||
)
|
||||
|
||||
|
||||
class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.cutout_region_to_shape"
|
||||
bl_label = "Cutout Region to Shape"
|
||||
bl_description = "Build the selected image Region as a Cutout shape"
|
||||
class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.cutout_selection_to_shape"
|
||||
bl_label = "Cutout Selection to Shape"
|
||||
bl_description = "Build the image selection as a Cutout shape"
|
||||
bl_options = {"INTERNAL"}
|
||||
job_type = "generate-cutout-artifacts"
|
||||
starting_message = "Generating Cutout artifacts"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
|
||||
region_path_json: bpy.props.StringProperty(options=_HIDDEN)
|
||||
region_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
|
||||
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,
|
||||
@@ -196,11 +196,11 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
options=_HIDDEN,
|
||||
)
|
||||
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
refine_region: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
refine_selection: bpy.props.BoolProperty(default=False, options=_HIDDEN)
|
||||
|
||||
def _needs_generation(self):
|
||||
return (
|
||||
self.refine_region
|
||||
self.refine_selection
|
||||
or self.shape in DEPTH_CUTOUT_SHAPES
|
||||
or self.generate_normal
|
||||
)
|
||||
@@ -212,19 +212,19 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
raise ValueError("Cutout currently supports still images")
|
||||
if self.shape not in CUTOUT_SHAPES:
|
||||
raise ValueError(f"Unsupported Cutout shape: {self.shape}")
|
||||
region_path = RegionPath.from_json(self.region_path_json)
|
||||
selection_path = SelectionPath.from_json(self.selection_path_json)
|
||||
source_rgba = image_rgba(source_object.data)
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
tuple(source_object.data.size),
|
||||
region_path,
|
||||
selection_path,
|
||||
source_alpha=source_rgba[:, :, 3],
|
||||
alpha_threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
antialias=True,
|
||||
)
|
||||
context.view_layer.objects.active = source_object
|
||||
source_object.select_set(True)
|
||||
crop_width = region_mask.bounds[2] - region_mask.bounds[0]
|
||||
crop_height = region_mask.bounds[3] - region_mask.bounds[1]
|
||||
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,
|
||||
(crop_width, crop_height),
|
||||
@@ -237,7 +237,7 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
source_object,
|
||||
self.shape,
|
||||
self.mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
source_rgba=source_rgba,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
@@ -245,11 +245,11 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
if setup_result is not None:
|
||||
return setup_result
|
||||
self.model, self.resolution_level = cutout_geometry_settings(context)
|
||||
self.region_bounds_json = json.dumps(
|
||||
region_mask.bounds,
|
||||
self.selection_bounds_json = json.dumps(
|
||||
selection_mask.bounds,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
self.input_path = str(prepare_region_input(source_object.data, region_mask))
|
||||
self.input_path = str(prepare_selection_input(source_object.data, selection_mask))
|
||||
except (
|
||||
ImageEditWarning,
|
||||
OSError,
|
||||
@@ -269,19 +269,19 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
source_path = require_input_path(self.input_path)
|
||||
depth_mode = depth_mode_for_shape(self.shape)
|
||||
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
|
||||
if self.refine_region:
|
||||
if self.refine_selection:
|
||||
require_model(BEN2_MODEL_KEY)
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
require_model(self.model)
|
||||
parameters = {
|
||||
"input": str(source_path),
|
||||
"device": production_device().lower(),
|
||||
"refine_region": self.refine_region,
|
||||
"refine_selection": self.refine_selection,
|
||||
"depth_mode": depth_mode,
|
||||
"normal_mode": normal_mode.value,
|
||||
"max_input_size": int(self.max_ai_input_size),
|
||||
}
|
||||
if self.refine_region:
|
||||
if self.refine_selection:
|
||||
parameters["refine_model"] = BEN2_MODEL_KEY
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
start_frame = source_start_frame(self.source_object_name)
|
||||
@@ -302,8 +302,8 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
|
||||
def response(self, context, result):
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
source_bounds = tuple(json.loads(self.region_bounds_json))
|
||||
region_mask = _generated_region_mask(
|
||||
source_bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
selection_mask = _generated_selection_mask(
|
||||
result,
|
||||
self.input_path,
|
||||
source_bounds,
|
||||
@@ -333,7 +333,7 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
source_object,
|
||||
self.shape,
|
||||
self.mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
normal_image=normal_image,
|
||||
@@ -351,18 +351,18 @@ class CutoutRegionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
|
||||
class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
|
||||
bl_idname = "anyimage.select_cutout_region"
|
||||
bl_label = "Select Cutout Region"
|
||||
bl_description = "Lasso an Image Empty region, then choose its mesh shape"
|
||||
class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
|
||||
bl_idname = "anyimage.select_cutout_selection"
|
||||
bl_label = "Start Cutout Selection"
|
||||
bl_description = "Lasso an Image Empty selection, then choose its mesh shape"
|
||||
bl_options = {"INTERNAL"}
|
||||
active_tool_id = "anyimage.cutout_lasso"
|
||||
refine_region_settings = {"LASSO": "cutout_refine_region"}
|
||||
refine_selection_settings = {"LASSO": "cutout_refine_selection"}
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
region_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
refine_region: refine_region_property()
|
||||
selection_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
refine_selection: refine_selection_property()
|
||||
invert: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
gesture: gesture_property()
|
||||
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
@@ -391,12 +391,12 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
self.report({"WARNING"}, "Image tools only support a still image")
|
||||
return {"CANCELLED"}
|
||||
apply_region_settings(self, context)
|
||||
apply_selection_settings(self, context)
|
||||
self.invert = False
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
width, height = (int(value) for value in source_object.data.size)
|
||||
self.region_path_json = RegionPath(
|
||||
self.selection_path_json = SelectionPath(
|
||||
points=((0, 0), (width, 0), (width, height), (0, height)),
|
||||
fill_rule="NONZERO",
|
||||
).to_json()
|
||||
@@ -413,8 +413,8 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
region_path = RegionPath.from_json(self.region_path_json)
|
||||
validate_cutout_region(source_object, region_path)
|
||||
selection_path = SelectionPath.from_json(self.selection_path_json)
|
||||
validate_cutout_selection(source_object, selection_path)
|
||||
except (ImageEditWarning, TypeError, ValueError) as error:
|
||||
report_image_edit_exception(self, error)
|
||||
return {"CANCELLED"}
|
||||
@@ -422,8 +422,8 @@ class SelectCutoutRegion(ImageRegionGesture, bpy.types.Operator):
|
||||
context,
|
||||
self._pie_event,
|
||||
source_object,
|
||||
region_path,
|
||||
self.refine_region,
|
||||
selection_path,
|
||||
self.refine_selection,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -435,15 +435,15 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
bl_label = "Cutout"
|
||||
bl_description = "Lasso an Image Empty and choose a mesh shape"
|
||||
bl_icon = "ops.sculpt.lasso_mask"
|
||||
bl_operator = SelectCutoutRegion.bl_idname
|
||||
bl_operator = SelectCutoutSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
SelectCutoutRegion.bl_idname,
|
||||
SelectCutoutSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
|
||||
{"properties": [("gesture", "LASSO"), ("full_image", True)]},
|
||||
),
|
||||
(
|
||||
SelectCutoutRegion.bl_idname,
|
||||
SelectCutoutSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "PRESS"},
|
||||
{"properties": [("gesture", "LASSO")]},
|
||||
),
|
||||
@@ -452,6 +452,6 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
def draw_settings(context, layout, _tool):
|
||||
settings = context.scene.anyimage_settings
|
||||
status = draw_ai_setup(layout)
|
||||
draw_ai_property(layout, settings, "cutout_refine_region", status)
|
||||
layout.prop(settings, "cutout_point_spacing")
|
||||
draw_ai_property(layout, settings, "cutout_refine_selection", status)
|
||||
draw_ai_property(layout, settings, "cutout_generate_normal", status)
|
||||
layout.prop(settings, "cutout_mesh_detail")
|
||||
|
||||
@@ -4,14 +4,16 @@ from enum import StrEnum
|
||||
|
||||
import bpy
|
||||
|
||||
from ...common.image_data import (
|
||||
from ...common.image import (
|
||||
color_data_name,
|
||||
image_rgba,
|
||||
)
|
||||
from ...common.depth_data import camera_depth_values, depth_uniform_scale
|
||||
from ...common.object import (
|
||||
from ...common.depth import camera_depth_values, depth_uniform_scale
|
||||
from ...common.material import (
|
||||
IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
create_image_material,
|
||||
)
|
||||
from ...common.object import (
|
||||
finalize_object_result,
|
||||
modifier_input_identifier,
|
||||
set_modifier_input,
|
||||
@@ -22,10 +24,10 @@ from .geometry import (
|
||||
DEFAULT_DEPTH_DIRECTION,
|
||||
crop_plane_bounds,
|
||||
cutout_cleanup_value,
|
||||
cutout_point_spacing,
|
||||
cutout_sampling_spacing,
|
||||
depth_balloon_projection_parameters,
|
||||
metric_depth_reference,
|
||||
region_calibration,
|
||||
selection_calibration,
|
||||
use_adaptive_depth_calibration,
|
||||
world_plane_metric,
|
||||
)
|
||||
@@ -82,8 +84,8 @@ def _assign_float_point_attribute(mesh, name, values):
|
||||
attribute.data.foreach_set("value", np.asarray(values, dtype=np.float32))
|
||||
|
||||
|
||||
def _cropped_color_image(source_object, region_mask, source_rgba):
|
||||
crop_bounds = region_mask.bounds
|
||||
def _cropped_color_image(source_object, selection_mask, source_rgba):
|
||||
crop_bounds = selection_mask.bounds
|
||||
import numpy as np
|
||||
|
||||
source = source_object.data
|
||||
@@ -149,7 +151,7 @@ def create_shape_object(
|
||||
source_object,
|
||||
shape,
|
||||
mesh_resolution,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
base_shape,
|
||||
*,
|
||||
depth_image=None,
|
||||
@@ -163,7 +165,7 @@ def create_shape_object(
|
||||
normal_mode = NormalMode(normal_mode)
|
||||
bounds = crop_plane_bounds(
|
||||
source_object,
|
||||
region_mask.bounds,
|
||||
selection_mask.bounds,
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
crop_center = (
|
||||
@@ -172,12 +174,12 @@ def create_shape_object(
|
||||
)
|
||||
world_metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
cleanup_value = cutout_cleanup_value(
|
||||
cutout_point_spacing(source_object, mesh_resolution),
|
||||
cutout_sampling_spacing(source_object, mesh_resolution),
|
||||
world_metric,
|
||||
)
|
||||
color_image = _cropped_color_image(
|
||||
source_object,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
image_rgba(source_object.data) if source_rgba is None else source_rgba,
|
||||
)
|
||||
material = None
|
||||
@@ -206,8 +208,8 @@ def create_shape_object(
|
||||
if depth_image is None or depth_metadata is None:
|
||||
raise ValueError("Depth Shape requires a Depth texture and metadata")
|
||||
camera_depth, valid = camera_depth_values(depth_image)
|
||||
calibration_valid, transparent_fraction = region_calibration(
|
||||
region_mask, valid.shape
|
||||
calibration_valid, transparent_fraction = selection_calibration(
|
||||
selection_mask, valid.shape
|
||||
)
|
||||
calibration_valid &= valid
|
||||
uniform_scale = depth_uniform_scale(
|
||||
|
||||
@@ -4,7 +4,7 @@ import bpy
|
||||
|
||||
from ..runtime import JobOperatorBase
|
||||
from ..properties import BEN2_MODEL_KEY, REALESRGAN_GENERAL_WDN_MODEL_KEY
|
||||
from ..common.ai_job import (
|
||||
from ..common.ai import (
|
||||
require_background_input_path,
|
||||
da3_parameters,
|
||||
require_input_path,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from .box import ImageBoxTool
|
||||
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageRegion
|
||||
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageSelection
|
||||
from .lasso import ImageLassoTool
|
||||
from .line import ImageLineTool
|
||||
from .polyline import ImagePolylineTool
|
||||
|
||||
CLASSES = (
|
||||
EditImageMask,
|
||||
RefineImageRegion,
|
||||
RefineImageSelection,
|
||||
ActivateImageTool,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
|
||||
from ...common.image_interaction import draw_region_settings
|
||||
from ...common.viewport import draw_selection_settings
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ class ImageBoxTool(bpy.types.WorkSpaceTool):
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_region_settings(context, layout, "image_box_refine_region")
|
||||
draw_selection_settings(context, layout, "image_box_refine_selection")
|
||||
|
||||
@@ -2,34 +2,34 @@ import json
|
||||
|
||||
import bpy
|
||||
|
||||
from ...properties import BEN2_MODEL_KEY, refine_region_property
|
||||
from ...properties import BEN2_MODEL_KEY, refine_selection_property
|
||||
from ...runtime import JobOperatorBase
|
||||
from ...common.ai_job import (
|
||||
from ...common.ai import (
|
||||
production_device,
|
||||
require_environment,
|
||||
require_model,
|
||||
)
|
||||
from ...common.image_data import (
|
||||
from ...common.image import (
|
||||
cleanup_image_input,
|
||||
create_pixel_result_image,
|
||||
image_pixels,
|
||||
image_rgba,
|
||||
prepare_region_input,
|
||||
prepare_selection_input,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.image_interaction import (
|
||||
ImageRegionGesture,
|
||||
from ...common.viewport import (
|
||||
ImageSelectionGesture,
|
||||
gesture_property,
|
||||
image_edit_poll,
|
||||
report_image_edit_exception,
|
||||
)
|
||||
from ...common.image_region import (
|
||||
from ...common.selection import (
|
||||
ImageEditWarning,
|
||||
RegionPath,
|
||||
SelectionPath,
|
||||
crop_pixels_to_alpha,
|
||||
rasterize_region_path,
|
||||
rasterize_selection_path,
|
||||
)
|
||||
|
||||
_HIDDEN = {"HIDDEN", "SKIP_SAVE"}
|
||||
@@ -61,7 +61,7 @@ def _refined_mask_from_pixels(mask_pixels, mask_size, bounds, source_size):
|
||||
def _edit_image_mask_pixels(
|
||||
source_pixels,
|
||||
source_size,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
invert=False,
|
||||
):
|
||||
"""Apply a mask while preserving source RGB, then tightly crop the canvas."""
|
||||
@@ -71,7 +71,7 @@ def _edit_image_mask_pixels(
|
||||
rgba = np.flipud(
|
||||
np.asarray(source_pixels, dtype=np.float32).reshape((height, width, 4))
|
||||
)
|
||||
mask = np.asarray(region_mask, dtype=np.float32)
|
||||
mask = np.asarray(selection_mask, dtype=np.float32)
|
||||
if invert:
|
||||
mask = 1.0 - mask
|
||||
mask = np.where(rgba[:, :, 3] > 0.0, mask, 0.0)
|
||||
@@ -82,13 +82,13 @@ def _edit_image_mask_pixels(
|
||||
)
|
||||
|
||||
|
||||
def _edit_image_object(source_object, region_mask, invert=False):
|
||||
def _edit_image_object(source_object, selection_mask, invert=False):
|
||||
image = source_object.data
|
||||
source_size = tuple(image.size)
|
||||
pixels, crop_bounds = _edit_image_mask_pixels(
|
||||
image_pixels(image),
|
||||
source_size,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
invert,
|
||||
)
|
||||
left, top, right, bottom = crop_bounds
|
||||
@@ -104,10 +104,10 @@ def _edit_image_object(source_object, region_mask, invert=False):
|
||||
)
|
||||
|
||||
|
||||
class EditImageMask(ImageRegionGesture, bpy.types.Operator):
|
||||
refine_region_settings = {
|
||||
"BOX": "image_box_refine_region",
|
||||
"LASSO": "image_lasso_refine_region",
|
||||
class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
|
||||
refine_selection_settings = {
|
||||
"BOX": "image_box_refine_selection",
|
||||
"LASSO": "image_lasso_refine_selection",
|
||||
}
|
||||
bl_idname = "anyimage.edit_image_mask"
|
||||
bl_label = "Edit Image Mask"
|
||||
@@ -116,8 +116,8 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
region_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
refine_region: refine_region_property()
|
||||
selection_path_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
refine_selection: refine_selection_property()
|
||||
invert: bpy.props.BoolProperty(
|
||||
name="Invert",
|
||||
description="Keep the opposite side of the refined gesture mask",
|
||||
@@ -133,23 +133,23 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
region_path = RegionPath.from_json(self.region_path_json)
|
||||
selection_path = SelectionPath.from_json(self.selection_path_json)
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
raise RuntimeError("Image tools only support a still image")
|
||||
region_mask = rasterize_region_path(
|
||||
selection_mask = rasterize_selection_path(
|
||||
tuple(source_object.data.size),
|
||||
region_path,
|
||||
selection_path,
|
||||
source_alpha=image_rgba(source_object.data)[:, :, 3],
|
||||
)
|
||||
if self.refine_region:
|
||||
source_path = prepare_region_input(
|
||||
if self.refine_selection:
|
||||
source_path = prepare_selection_input(
|
||||
source_object.data,
|
||||
region_mask,
|
||||
selection_mask,
|
||||
)
|
||||
result = bpy.ops.anyimage.refine_image_region(
|
||||
result = bpy.ops.anyimage.refine_image_selection(
|
||||
input_path=str(source_path),
|
||||
source_object_name=self.source_object_name,
|
||||
region_bounds_json=json.dumps(region_mask.bounds),
|
||||
selection_bounds_json=json.dumps(selection_mask.bounds),
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
cleanup_image_input(source_path, True)
|
||||
@@ -157,7 +157,7 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
_edit_image_object(
|
||||
source_object,
|
||||
region_mask.full_values(tuple(source_object.data.size)),
|
||||
selection_mask.full_values(tuple(source_object.data.size)),
|
||||
)
|
||||
except (json.JSONDecodeError, RuntimeError, TypeError, ValueError) as error:
|
||||
report_image_edit_exception(self, error)
|
||||
@@ -165,15 +165,15 @@ class EditImageMask(ImageRegionGesture, bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.refine_image_region"
|
||||
bl_label = "Refine Image Region"
|
||||
job_type = "refine-image-region"
|
||||
class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.refine_image_selection"
|
||||
bl_label = "Refine Image Selection"
|
||||
job_type = "refine-image-selection"
|
||||
starting_message = "Refining image selection"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
region_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
selection_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
def request(self, _context):
|
||||
require_environment()
|
||||
@@ -187,9 +187,9 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
|
||||
def response(self, _context, result):
|
||||
source_object = require_image_empty(self.source_object_name)
|
||||
|
||||
source_bounds = tuple(json.loads(self.region_bounds_json))
|
||||
source_bounds = tuple(json.loads(self.selection_bounds_json))
|
||||
local_bounds = tuple(
|
||||
json.loads(result.file("region_bounds").read_text(encoding="utf-8"))
|
||||
json.loads(result.file("selection_bounds").read_text(encoding="utf-8"))
|
||||
)
|
||||
bounds = (
|
||||
source_bounds[0] + local_bounds[0],
|
||||
@@ -198,7 +198,7 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
|
||||
source_bounds[1] + local_bounds[3],
|
||||
)
|
||||
refined_image = bpy.data.images.load(
|
||||
str(result.file("region_image")), check_existing=False
|
||||
str(result.file("selection_image")), check_existing=False
|
||||
)
|
||||
try:
|
||||
refined_pixels = image_pixels(refined_image)
|
||||
@@ -217,7 +217,7 @@ class RefineImageRegion(JobOperatorBase, bpy.types.Operator):
|
||||
return str(error)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Edit Image Mask"):
|
||||
raise RuntimeError("Unable to create the Edit Image Mask Undo step")
|
||||
return "Image Region refined"
|
||||
return "Image Selection refined"
|
||||
|
||||
def cleanup(self):
|
||||
cleanup_image_input(self.input_path, True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
|
||||
from ...common.image_interaction import draw_region_settings
|
||||
from ...common.viewport import draw_selection_settings
|
||||
from .edit_mask import EditImageMask
|
||||
|
||||
|
||||
@@ -21,4 +21,4 @@ class ImageLassoTool(bpy.types.WorkSpaceTool):
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
draw_region_settings(context, layout, "image_lasso_refine_region")
|
||||
draw_selection_settings(context, layout, "image_lasso_refine_selection")
|
||||
|
||||
@@ -3,14 +3,14 @@ import math
|
||||
|
||||
import bpy
|
||||
|
||||
from ...common.image_interaction import (
|
||||
from ...common.viewport import (
|
||||
deserialize_matrix,
|
||||
drawing_in_region,
|
||||
screen_path_to_image_pixels,
|
||||
serialize_matrix,
|
||||
)
|
||||
from ...common.image_region import crop_pixels_to_alpha
|
||||
from ...common.image_data import (
|
||||
from ...common.selection import crop_pixels_to_alpha
|
||||
from ...common.image import (
|
||||
create_pixel_result_image,
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
@@ -19,7 +19,7 @@ from ...common.image_data import (
|
||||
require_image_empty,
|
||||
video_total_frames,
|
||||
)
|
||||
from ...common.image_interaction import (
|
||||
from ...common.viewport import (
|
||||
active_view3d_tool_id,
|
||||
draw_dashed_line,
|
||||
draw_polygon_fill,
|
||||
|
||||
@@ -2,7 +2,7 @@ import bpy
|
||||
|
||||
from ..properties import BEN2_MODEL_KEY
|
||||
from ..runtime import JobOperatorBase
|
||||
from ..common.ai_job import (
|
||||
from ..common.ai import (
|
||||
require_background_input_path,
|
||||
invoke_ai_setup_if_needed,
|
||||
production_device,
|
||||
@@ -10,7 +10,7 @@ from ..common.ai_job import (
|
||||
require_model,
|
||||
)
|
||||
|
||||
from ..common.image_data import (
|
||||
from ..common.image import (
|
||||
cleanup_image_input,
|
||||
default_extract_frames,
|
||||
is_image_empty,
|
||||
@@ -70,7 +70,7 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
|
||||
|
||||
def apply_background_response(result, source_object_name):
|
||||
from ..common.image_data import (
|
||||
from ..common.image import (
|
||||
load_color_result_image,
|
||||
replace_empty_image,
|
||||
require_image_empty,
|
||||
|
||||
@@ -8,14 +8,14 @@ from ..server.media.resolution import (
|
||||
MIN_MAX_AI_INPUT_SIZE,
|
||||
)
|
||||
from ..runtime import JobOperatorBase
|
||||
from ..common.ai_job import (
|
||||
from ..common.ai import (
|
||||
require_input_path,
|
||||
invoke_ai_setup_if_needed,
|
||||
production_device,
|
||||
require_environment,
|
||||
require_model,
|
||||
)
|
||||
from ..common.image_data import (
|
||||
from ..common.image import (
|
||||
cleanup_image_input,
|
||||
default_extract_frames,
|
||||
is_image_empty,
|
||||
|
||||
Reference in New Issue
Block a user