Add tests and implement boundary smoothing and cutout functionality
- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity. - Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending. - Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised. - Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry. - Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity. - Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles. - Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
Binary file not shown.
@@ -6,7 +6,7 @@ import bpy
|
||||
|
||||
from ...common.material import create_image_material
|
||||
from ...common.color_image import material_color_image
|
||||
from ...common.image import image_pixels
|
||||
from ...common.image import cleanup_image_input, image_pixels, save_packed_image
|
||||
|
||||
CLIPBOARD_HASH_PROPERTY = "anyimage_clipboard_sha256"
|
||||
CLIPBOARD_CONTENT_PROPERTY = "anyimage_clipboard_content_sha256"
|
||||
@@ -49,6 +49,10 @@ class PasteTargetError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class BrushPreviewError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def target_for_context(context):
|
||||
area = getattr(context, "area", None)
|
||||
area_type = getattr(area, "type", None)
|
||||
@@ -367,10 +371,31 @@ def set_tool_texture(context, image):
|
||||
raise PasteTargetError(
|
||||
f"The active brush could not use the image texture: {error}"
|
||||
) from error
|
||||
_redraw_area(context)
|
||||
try:
|
||||
if getattr(context, "mode", "") == "PAINT_TEXTURE":
|
||||
_set_brush_preview(context, brush, image)
|
||||
finally:
|
||||
_redraw_area(context)
|
||||
return texture
|
||||
|
||||
|
||||
def _set_brush_preview(context, brush, image):
|
||||
preview_path = None
|
||||
try:
|
||||
preview_path = save_packed_image(image, context.scene)
|
||||
with context.temp_override(id=brush):
|
||||
result = bpy.ops.ed.lib_id_load_custom_preview(filepath=str(preview_path))
|
||||
if result != {"FINISHED"}:
|
||||
raise RuntimeError("Blender did not load the custom preview")
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
raise BrushPreviewError(
|
||||
f"Texture pasted, but the brush preview could not be updated: {error}"
|
||||
) from error
|
||||
finally:
|
||||
if preview_path is not None:
|
||||
cleanup_image_input(preview_path, True)
|
||||
|
||||
|
||||
def _set_unbiased_paint_color(context, brush):
|
||||
if getattr(context, "mode", "") not in WHITE_BRUSH_COLOR_MODES:
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import bpy
|
||||
|
||||
from .actions import (
|
||||
BrushPreviewError,
|
||||
PasteTargetError,
|
||||
acquire_packed_image,
|
||||
paste_image,
|
||||
@@ -134,6 +135,9 @@ class PasteClipboardImage(bpy.types.Operator):
|
||||
import_as=self.import_as,
|
||||
shadeless=self.shadeless,
|
||||
)
|
||||
except BrushPreviewError as error:
|
||||
self.report({"WARNING"}, str(error))
|
||||
return {"FINISHED"}
|
||||
except (OSError, RuntimeError, PasteTargetError) as error:
|
||||
if image is not None and not image_reused and image.users == 0:
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
@@ -35,7 +35,7 @@ def create_panorama_from_result(context, result, operator):
|
||||
try:
|
||||
with material_color_image(input_path=operator.color_path) as color:
|
||||
metadata = load_panorama_metadata(result.file("depth_metadata"))
|
||||
group = load_node_group("O Image Depth Panorama")
|
||||
group = load_node_group("O Image Depth Panorama", asset_revision=14)
|
||||
depth = load_depth_result_image(result.file("depth"), source, metadata)
|
||||
material = create_image_material(source.data, color, shadeless=True, scene=context.scene,
|
||||
texture_extension="REPEAT")
|
||||
|
||||
@@ -62,7 +62,7 @@ def create_depth_plane_object(
|
||||
node_group = load_node_group({
|
||||
"DEPTH": "O Image Depth Plane",
|
||||
"RELIEF": "O Image Relief Plane",
|
||||
}[plane_type])
|
||||
}[plane_type], asset_revision=14 if plane_type == "DEPTH" else None)
|
||||
source_name = source_object.name
|
||||
bounds = image_empty_bounds(source_object)
|
||||
mesh = create_image_plane_mesh(source_name, bounds)
|
||||
|
||||
@@ -99,7 +99,7 @@ def _assign_float_point_attribute(mesh, name, values):
|
||||
|
||||
|
||||
def load_cutout_node_group(shape):
|
||||
return load_node_group(CUTOUT_NODE_GROUP_NAMES[shape], asset_revision=11)
|
||||
return load_node_group(CUTOUT_NODE_GROUP_NAMES[shape], asset_revision=18 if shape == "DEPTH_SOLID" else 11)
|
||||
|
||||
|
||||
def _connect_cutout_normal_scale(material):
|
||||
|
||||
@@ -26,13 +26,13 @@ MAX_FRAME_RESOLUTION = 8192
|
||||
ADDON_PACKAGE = __package__
|
||||
DEFAULT_STORAGE_ROOT = Path.home() / ".anyimage"
|
||||
UPSCALE_MODEL_ITEMS = tuple(
|
||||
(model.key, model.label, model.description, number)
|
||||
for model, number in zip(UPSCALE_MODELS.values(), (0, 2, 4))
|
||||
(model.key, f"{tier} | {model.label}", model.description, number)
|
||||
for model, tier, number in zip(UPSCALE_MODELS.values(), ("Fast", "Base", "Pro"), (0, 2, 4))
|
||||
)
|
||||
|
||||
BACKGROUND_MODEL_ITEMS = tuple(
|
||||
(model.key, model.label, model.description, number)
|
||||
for model, number in zip(BACKGROUND_MODELS.values(), (1, 0, 2))
|
||||
(model.key, f"{tier} | {model.label}", model.description, number)
|
||||
for model, tier, number in zip(BACKGROUND_MODELS.values(), ("Fast", "Base", "Pro"), (1, 0, 2))
|
||||
)
|
||||
|
||||
BALLOON_PROFILE_ALGORITHM_ITEMS = (
|
||||
|
||||
@@ -122,11 +122,11 @@ def _model_items(models):
|
||||
return [
|
||||
(
|
||||
model["key"],
|
||||
model["label"],
|
||||
f"{tier} | {model['label']}",
|
||||
f"{model['description']}; license: {model['license']}",
|
||||
numbers[model["key"]],
|
||||
)
|
||||
for model in models
|
||||
for model, tier in zip(models, ("Fast", "Base", "Pro"))
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Extend reliable depth along each visible component's silhouette."""
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
|
||||
def extend_edge_depth(frame, alpha, alpha_threshold=0.9):
|
||||
"""Return edge-corrected geometry while preserving reliable camera points."""
|
||||
import numpy as np
|
||||
from scipy import ndimage
|
||||
|
||||
from ..models.onnx_moge2 import depth_to_points
|
||||
|
||||
depth = frame.depth
|
||||
alpha = np.asarray(alpha, dtype=np.float32)
|
||||
if alpha.shape != depth.shape:
|
||||
raise ValueError("Edge Alpha must match the depth dimensions")
|
||||
if np.all(alpha == 1.0):
|
||||
return frame
|
||||
|
||||
visible = (alpha > 0.0) & (alpha >= alpha_threshold)
|
||||
opaque = visible & (alpha >= 0.95)
|
||||
candidates = (
|
||||
opaque
|
||||
& (frame.validity > 0.5)
|
||||
& np.isfinite(depth)
|
||||
& (depth > 0.0)
|
||||
)
|
||||
if not candidates.any():
|
||||
return frame
|
||||
interior_distance = ndimage.distance_transform_edt(
|
||||
np.pad(opaque, 3, mode="edge")
|
||||
)[3:-3, 3:-3]
|
||||
interior = opaque & (interior_distance > 2.0)
|
||||
core = candidates & interior
|
||||
del interior_distance
|
||||
labels, _ = ndimage.label(visible, structure=np.ones((3, 3), dtype=bool))
|
||||
corrected = depth.copy()
|
||||
background_distance = np.full(depth.shape, np.inf, dtype=np.float32)
|
||||
|
||||
for component_id, bounds in enumerate(ndimage.find_objects(labels), start=1):
|
||||
region = tuple(
|
||||
slice(max(0, bound.start - 7), min(size, bound.stop + 7))
|
||||
for bound, size in zip(bounds, depth.shape)
|
||||
)
|
||||
component = labels[region] == component_id
|
||||
donors = core[region] & component
|
||||
if not donors.any():
|
||||
donors = candidates[region] & component
|
||||
if not donors.any():
|
||||
continue
|
||||
|
||||
indices = ndimage.distance_transform_edt(
|
||||
~donors, return_distances=False, return_indices=True
|
||||
)
|
||||
extended = depth[region][tuple(indices)]
|
||||
smoothed = ndimage.gaussian_filter(extended, sigma=0.8, radius=3)
|
||||
distance = ndimage.distance_transform_edt(~component)
|
||||
background = (
|
||||
~visible[region]
|
||||
& (distance <= 4.0)
|
||||
& (distance < background_distance[region])
|
||||
)
|
||||
target = (component & ~interior[region] & ~donors) | background
|
||||
corrected[region][target] = smoothed[target]
|
||||
background_distance[region][background] = distance[background]
|
||||
|
||||
changed = corrected != depth
|
||||
if not changed.any():
|
||||
return frame
|
||||
points = frame.points
|
||||
if points is not None:
|
||||
height, width = depth.shape
|
||||
intrinsics = frame.intrinsics.copy()
|
||||
intrinsics[0] /= width
|
||||
intrinsics[1] /= height
|
||||
projected = depth_to_points(corrected, intrinsics)
|
||||
points = points.copy()
|
||||
points[changed] = projected[changed]
|
||||
return replace(frame, depth=corrected, points=points)
|
||||
@@ -8,7 +8,6 @@ from .depth_texture import (
|
||||
write_depth_texture,
|
||||
)
|
||||
from .normal_texture import write_camera_normal_texture
|
||||
from .edge_depth import extend_edge_depth
|
||||
|
||||
|
||||
def _limited_model_input(source_path, output_path, max_input_size):
|
||||
@@ -79,12 +78,11 @@ def generate_moge_artifacts(
|
||||
if generate_depth:
|
||||
context.progress(0.45, "Generating depth texture")
|
||||
alpha = _image_alpha(input_path, frame.depth.shape)
|
||||
depth_frame = extend_edge_depth(frame, alpha, alpha_threshold)
|
||||
result["depth"] = write_depth_texture(
|
||||
depth_frame, context.directory / "depth.exr", alpha=alpha,
|
||||
frame, context.directory / "depth.exr", alpha=alpha,
|
||||
)
|
||||
result["depth_metadata"] = write_depth_metadata(
|
||||
depth_frame,
|
||||
frame,
|
||||
(alpha > 0.0) & (alpha >= alpha_threshold),
|
||||
context.directory / "depth.json",
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ class ModelSpec:
|
||||
|
||||
BIREFNET_LITE = ModelSpec(
|
||||
key="BIREFNET_LITE",
|
||||
label="Fast | BiRefNet Lite",
|
||||
label="BiRefNet Lite",
|
||||
directory_name="birefnet-lite",
|
||||
repository="ZhengPeng7/BiRefNet_lite",
|
||||
license_name="MIT",
|
||||
@@ -35,7 +35,7 @@ BIREFNET_LITE = ModelSpec(
|
||||
|
||||
BEN2_MODEL = ModelSpec(
|
||||
key="BEN2_BASE",
|
||||
label="General | BEN2 Base",
|
||||
label="BEN2 Base",
|
||||
directory_name="BEN2-ONNX",
|
||||
repository="onnx-community/BEN2-ONNX",
|
||||
license_name="Apache 2.0",
|
||||
@@ -54,7 +54,7 @@ BEN2_MODEL = ModelSpec(
|
||||
|
||||
REALESRGAN_GENERAL_WDN_X4V3 = ModelSpec(
|
||||
key="REALESRGAN_GENERAL_WDN_X4V3",
|
||||
label="Fast | Real-ESRGAN General WDN x4v3",
|
||||
label="Real-ESRGAN General WDN x4v3",
|
||||
directory_name="realesr-general-wdn-x4v3",
|
||||
repository="xinntao/Real-ESRGAN",
|
||||
license_name="BSD 3-Clause",
|
||||
@@ -72,7 +72,7 @@ REALESRGAN_GENERAL_WDN_X4V3 = ModelSpec(
|
||||
|
||||
REALESRGAN_X4PLUS = ModelSpec(
|
||||
key="REALESRGAN_X4PLUS",
|
||||
label="General | Real-ESRGAN x4plus",
|
||||
label="Real-ESRGAN x4plus",
|
||||
directory_name="realesrgan-x4plus",
|
||||
repository="jonathanst29/tinier-upscale-models",
|
||||
license_name="BSD 3-Clause",
|
||||
@@ -93,7 +93,7 @@ REALESRGAN_X4PLUS = ModelSpec(
|
||||
|
||||
HAT_GAN_X4_SHARPER = ModelSpec(
|
||||
key="HAT_GAN_X4_SHARPER",
|
||||
label="Detailed | HAT Sharper",
|
||||
label="HAT Sharper",
|
||||
directory_name="hat-gan-x4-sharper",
|
||||
repository="XPixelGroup/HAT",
|
||||
license_name="Apache 2.0",
|
||||
@@ -124,7 +124,7 @@ DEFAULT_UPSCALE_MODEL_KEY = REALESRGAN_GENERAL_WDN_X4V3.key
|
||||
MOGE2_MODELS = {
|
||||
"MOGE2_VITS_NORMAL": ModelSpec(
|
||||
key="MOGE2_VITS_NORMAL",
|
||||
label="Fast | MoGe-2 ViT-S Normal",
|
||||
label="MoGe-2 ViT-S Normal",
|
||||
directory_name="moge-2-vits-normal-onnx",
|
||||
repository="Ruicheng/moge-2-vits-normal-onnx",
|
||||
license_name="MIT",
|
||||
@@ -141,7 +141,7 @@ MOGE2_MODELS = {
|
||||
),
|
||||
"MOGE2_VITB_NORMAL": ModelSpec(
|
||||
key="MOGE2_VITB_NORMAL",
|
||||
label="Balanced | MoGe-2 ViT-B Normal",
|
||||
label="MoGe-2 ViT-B Normal",
|
||||
directory_name="moge-2-vitb-normal-onnx",
|
||||
repository="Ruicheng/moge-2-vitb-normal-onnx",
|
||||
license_name="MIT",
|
||||
@@ -159,7 +159,7 @@ MOGE2_MODELS = {
|
||||
}
|
||||
MOGE3_VITL = ModelSpec(
|
||||
key="MOGE3_VITL",
|
||||
label="Detailed | MoGe-3 ViT-L",
|
||||
label="MoGe-3 ViT-L",
|
||||
directory_name="moge-3-vitl-onnx",
|
||||
repository="Ruicheng/moge-3-vitl",
|
||||
license_name="MIT",
|
||||
@@ -176,7 +176,7 @@ MOGE3_VITL = ModelSpec(
|
||||
MOGE_MODELS = {**MOGE2_MODELS, MOGE3_VITL.key: MOGE3_VITL}
|
||||
BIREFNET_HR_MATTING = ModelSpec(
|
||||
key="BIREFNET_HR_MATTING",
|
||||
label="Detailed | BiRefNet HR-matting (CPU)",
|
||||
label="BiRefNet HR-matting (CPU)",
|
||||
directory_name="birefnet-hr-matting",
|
||||
repository="ZhengPeng7/BiRefNet_HR-matting",
|
||||
license_name="MIT",
|
||||
|
||||
Reference in New Issue
Block a user