Add soft min and max constraints for frame resolution and cutout edge length properties
This commit is contained in:
@@ -380,88 +380,6 @@ def points_inside_contours(points, contours):
|
||||
return inside
|
||||
|
||||
|
||||
def uniform_interior_points_for_contours(contours, spacing):
|
||||
import numpy as np
|
||||
|
||||
contours = [np.asarray(contour, dtype=np.float64) for contour in contours]
|
||||
boundary_points = np.concatenate(contours, axis=0)
|
||||
minimum = boundary_points.min(axis=0)
|
||||
maximum = boundary_points.max(axis=0)
|
||||
row_spacing = spacing * math.sqrt(3.0) * 0.5
|
||||
row_count = int(math.ceil((maximum[1] - minimum[1]) / row_spacing)) + 1
|
||||
column_count = int(math.ceil((maximum[0] - minimum[0]) / spacing)) + 1
|
||||
rows = np.arange(row_count, dtype=np.float64)
|
||||
columns = np.arange(column_count, dtype=np.float64)
|
||||
y = minimum[1] + rows * row_spacing
|
||||
x = (
|
||||
minimum[0]
|
||||
+ columns[None, :] * spacing
|
||||
+ (rows.astype(np.int64) % 2)[:, None] * spacing * 0.5
|
||||
)
|
||||
x = np.broadcast_to(x, (row_count, column_count))
|
||||
points = np.column_stack((x.ravel(), np.repeat(y, column_count)))
|
||||
inside = points_inside_contours(points, contours).reshape(row_count, column_count)
|
||||
|
||||
clearance = spacing * 0.4
|
||||
near_boundary = np.zeros_like(inside)
|
||||
for contour in contours:
|
||||
for start, end in zip(contour, np.roll(contour, -1, axis=0)):
|
||||
segment = end - start
|
||||
length_squared = float(segment.dot(segment))
|
||||
if length_squared <= 1e-16:
|
||||
continue
|
||||
first_row = max(
|
||||
0,
|
||||
int(
|
||||
math.ceil(
|
||||
(min(start[1], end[1]) - clearance - minimum[1]) / row_spacing
|
||||
)
|
||||
),
|
||||
)
|
||||
last_row = min(
|
||||
row_count - 1,
|
||||
int(
|
||||
math.floor(
|
||||
(max(start[1], end[1]) + clearance - minimum[1]) / row_spacing
|
||||
)
|
||||
),
|
||||
)
|
||||
for row in range(first_row, last_row + 1):
|
||||
offset = spacing * 0.5 if row % 2 else 0.0
|
||||
first_column = max(
|
||||
0,
|
||||
int(
|
||||
math.ceil(
|
||||
(min(start[0], end[0]) - clearance - minimum[0] - offset)
|
||||
/ spacing
|
||||
)
|
||||
),
|
||||
)
|
||||
last_column = min(
|
||||
column_count - 1,
|
||||
int(
|
||||
math.floor(
|
||||
(max(start[0], end[0]) + clearance - minimum[0] - offset)
|
||||
/ spacing
|
||||
)
|
||||
),
|
||||
)
|
||||
if last_column < first_column:
|
||||
continue
|
||||
selected = np.arange(first_column, last_column + 1)
|
||||
row_points = np.column_stack(
|
||||
(x[row, selected], np.full(len(selected), y[row]))
|
||||
)
|
||||
factor = np.clip(
|
||||
(row_points - start) @ segment / length_squared, 0.0, 1.0
|
||||
)
|
||||
nearest = start + factor[:, None] * segment
|
||||
close = np.sum((row_points - nearest) ** 2, axis=1) < clearance**2
|
||||
near_boundary[row, selected[close]] = True
|
||||
usable = inside & ~near_boundary
|
||||
return np.column_stack((x[usable], np.broadcast_to(y[:, None], x.shape)[usable]))
|
||||
|
||||
|
||||
def metric_transform(world_metric=None):
|
||||
import numpy as np
|
||||
|
||||
@@ -478,82 +396,13 @@ def metric_transform(world_metric=None):
|
||||
raise ValueError("The Image Empty has a degenerate world transform") from error
|
||||
|
||||
|
||||
def prepare_planar_contours(
|
||||
contours,
|
||||
image_size,
|
||||
local_bounds,
|
||||
spacing=POINT_SPACING,
|
||||
world_metric=None,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
if spacing <= 0.0:
|
||||
raise ValueError("Cutout point spacing must be greater than zero")
|
||||
image_width, image_height = image_size
|
||||
x_min, x_max, y_min, y_max = local_bounds
|
||||
local_width = x_max - x_min
|
||||
local_height = y_max - y_min
|
||||
transform = metric_transform(world_metric)
|
||||
prepared = []
|
||||
for contour in contours:
|
||||
pixels = np.asarray(contour, dtype=np.float64)
|
||||
if pixels.ndim != 2 or pixels.shape[0] < 3 or pixels.shape[1] != 2:
|
||||
continue
|
||||
local = np.empty_like(pixels)
|
||||
local[:, 0] = x_min + local_width * pixels[:, 0] / image_width
|
||||
local[:, 1] = y_max - local_height * pixels[:, 1] / image_height
|
||||
planar = local @ transform
|
||||
perimeter = float(
|
||||
np.linalg.norm(np.roll(planar, -1, axis=0) - planar, axis=1).sum()
|
||||
)
|
||||
if perimeter <= 1e-12:
|
||||
continue
|
||||
contour_spacing = min(spacing, perimeter / 8.0)
|
||||
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 selection large enough for this resolution")
|
||||
interior = uniform_interior_points_for_contours(prepared, spacing)
|
||||
return prepared, interior, spacing
|
||||
|
||||
|
||||
def _prepare_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=POINT_SPACING,
|
||||
world_metric=None,
|
||||
):
|
||||
prepared = []
|
||||
errors = []
|
||||
for component in selection_contours.components:
|
||||
try:
|
||||
contours, interior, _spacing = prepare_planar_contours(
|
||||
component,
|
||||
selection_contours.image_size,
|
||||
local_bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
)
|
||||
except ValueError as error:
|
||||
errors.append(error)
|
||||
continue
|
||||
prepared.append((contours, interior))
|
||||
if not prepared:
|
||||
if errors:
|
||||
raise errors[0]
|
||||
raise ValueError("The Cutout has no usable selection")
|
||||
return prepared, spacing
|
||||
|
||||
|
||||
def _triangulate_contour_component(contours, interior, spacing):
|
||||
def contour_interior_point(contours, spacing):
|
||||
"""Choose an interior seed from the largest auxiliary triangle."""
|
||||
import numpy as np
|
||||
from mathutils import Vector
|
||||
from mathutils.geometry import delaunay_2d_cdt
|
||||
|
||||
input_points = np.concatenate((*contours, interior), axis=0)
|
||||
input_points = np.concatenate(contours, axis=0)
|
||||
boundary_edges = []
|
||||
offset = 0
|
||||
for contour in contours:
|
||||
@@ -583,15 +432,15 @@ def _triangulate_contour_component(contours, interior, spacing):
|
||||
faces = faces[points_inside_contours(centers, contours)]
|
||||
if not len(faces):
|
||||
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)
|
||||
remap[used] = np.arange(len(used), dtype=np.int32)
|
||||
planar_points = planar_points[used]
|
||||
faces = remap[faces]
|
||||
boundary = boundary_vertices_from_faces(len(planar_points), faces)
|
||||
planar_faces = [tuple(int(value) for value in face) for face in faces]
|
||||
return planar_points, planar_faces, boundary
|
||||
triangles = planar_points[faces]
|
||||
first = triangles[:, 1] - triangles[:, 0]
|
||||
second = triangles[:, 2] - triangles[:, 0]
|
||||
areas = abs(first[:, 0] * second[:, 1] - first[:, 1] * second[:, 0])
|
||||
seed = triangles[np.argmax(areas)].mean(axis=0)
|
||||
if not points_inside_contours(seed[None], contours)[0]:
|
||||
raise ValueError("Cutout seed is outside its contour")
|
||||
return seed
|
||||
|
||||
|
||||
|
||||
def boundary_vertices_from_faces(vertex_count, faces):
|
||||
@@ -614,54 +463,6 @@ def boundary_vertices_from_faces(vertex_count, faces):
|
||||
return boundary
|
||||
|
||||
|
||||
def triangulate_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=POINT_SPACING,
|
||||
world_metric=None,
|
||||
return_components=False,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
prepared, spacing = _prepare_selection_contours(
|
||||
selection_contours,
|
||||
local_bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
)
|
||||
points = []
|
||||
faces = []
|
||||
boundaries = []
|
||||
components = []
|
||||
offset = 0
|
||||
for contours, interior in prepared:
|
||||
component_points, component_faces, component_boundary = _triangulate_contour_component(
|
||||
contours,
|
||||
interior,
|
||||
spacing,
|
||||
)
|
||||
components.append(
|
||||
(
|
||||
offset,
|
||||
offset + len(component_points),
|
||||
component_faces,
|
||||
contours[0],
|
||||
)
|
||||
)
|
||||
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 selection components")
|
||||
result = (
|
||||
np.concatenate(points, axis=0),
|
||||
faces,
|
||||
np.concatenate(boundaries),
|
||||
)
|
||||
return (*result, components) if return_components else result
|
||||
|
||||
|
||||
CUTOUT_ALPHA_THRESHOLD = 0.9
|
||||
CUTOUT_CLEANUP_UI_SCALE = 100.0
|
||||
|
||||
@@ -674,84 +475,26 @@ class BaseShape:
|
||||
balloon: object
|
||||
|
||||
|
||||
def refine_balloon_triangles(points, faces, boundary):
|
||||
"""Add interior samples to boundary ears and interior boundary chords."""
|
||||
from collections import Counter
|
||||
import numpy as np
|
||||
|
||||
edges = Counter(tuple(sorted((face[i], face[(i + 1) % 3]))) for face in faces for i in range(3))
|
||||
values, midpoints = list(points), {}
|
||||
for edge, count in edges.items():
|
||||
if count == 2 and all(boundary[index] for index in edge):
|
||||
midpoints[edge] = len(values)
|
||||
values.append(points[list(edge)].mean(axis=0))
|
||||
refined = []
|
||||
for face in faces:
|
||||
ring = []
|
||||
for index, vertex in enumerate(face):
|
||||
ring.append(vertex)
|
||||
midpoint = midpoints.get(tuple(sorted((vertex, face[(index + 1) % 3]))))
|
||||
if midpoint is not None:
|
||||
ring.append(midpoint)
|
||||
if len(ring) == 3 and not all(boundary[vertex] for vertex in face):
|
||||
refined.append(tuple(face))
|
||||
continue
|
||||
center = len(values)
|
||||
values.append(points[list(face)].mean(axis=0))
|
||||
refined.extend((center, ring[i], ring[(i + 1) % len(ring)]) for i in range(len(ring)))
|
||||
points = np.asarray(values)
|
||||
return points, refined, boundary_vertices_from_faces(len(points), refined)
|
||||
|
||||
|
||||
def cutout_domain(
|
||||
selection_contours,
|
||||
bounds,
|
||||
spacing,
|
||||
world_metric,
|
||||
balloon_algorithm="POISSON",
|
||||
*,
|
||||
fine_mesh=None,
|
||||
):
|
||||
def cutout_domain(mesh, bounds, world_metric, balloon_algorithm="POISSON"):
|
||||
"""Map the quality mesh into local coordinates and solve its height."""
|
||||
import numpy as np
|
||||
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
pixels, planar_faces, boundary, pixel_components, image_size = mesh
|
||||
image_width, image_height = image_size
|
||||
transform = metric_transform(world_metric)
|
||||
|
||||
if fine_mesh is None:
|
||||
planar, planar_faces, boundary, components = triangulate_selection_contours(
|
||||
selection_contours,
|
||||
bounds,
|
||||
spacing=spacing,
|
||||
world_metric=world_metric,
|
||||
return_components=True,
|
||||
)
|
||||
else:
|
||||
pixels, planar_faces, boundary, pixel_components, image_size = fine_mesh
|
||||
image_width, image_height = image_size
|
||||
transform = metric_transform(world_metric)
|
||||
def pixel_to_planar(points):
|
||||
local = np.asarray(points, dtype=np.float64).copy()
|
||||
local[:, 0] = x_min + local[:, 0] * (x_max - x_min) / image_width
|
||||
local[:, 1] = y_max - local[:, 1] * (y_max - y_min) / image_height
|
||||
return local @ transform
|
||||
|
||||
def pixel_to_planar(points):
|
||||
local = np.asarray(points, dtype=np.float64).copy()
|
||||
local[:, 0] = x_min + local[:, 0] * (x_max - x_min) / image_width
|
||||
local[:, 1] = y_max - local[:, 1] * (y_max - y_min) / image_height
|
||||
return local @ transform
|
||||
|
||||
planar = pixel_to_planar(pixels)
|
||||
planar_faces = planar_faces.tolist()
|
||||
components = [
|
||||
(start, end, faces.tolist(), pixel_to_planar(contour))
|
||||
for start, end, faces, contour in pixel_components
|
||||
]
|
||||
refined_points, refined_faces, refined_components = [], [], []
|
||||
offset = 0
|
||||
for start, end, component_faces, contour in components:
|
||||
points, faces, _ = refine_balloon_triangles(planar[start:end], component_faces, boundary[start:end])
|
||||
refined_points.append(points)
|
||||
refined_faces.extend(tuple(offset + index for index in face) for face in faces)
|
||||
refined_components.append((offset, offset + len(points), faces, contour))
|
||||
offset += len(points)
|
||||
planar = np.concatenate(refined_points)
|
||||
planar_faces, components = refined_faces, refined_components
|
||||
boundary = boundary_vertices_from_faces(len(planar), planar_faces)
|
||||
planar = pixel_to_planar(pixels)
|
||||
components = [
|
||||
(start, end, faces.tolist(), pixel_to_planar(contour))
|
||||
for start, end, faces, contour in pixel_components
|
||||
]
|
||||
profile = balloon.balloon_profile(
|
||||
balloon_algorithm, planar, planar_faces, boundary, components
|
||||
)
|
||||
@@ -836,7 +579,11 @@ def extract_cutout_contours(
|
||||
|
||||
|
||||
def effective_cutout_edge_length(
|
||||
context, source_object, edge_length, content_values, content_bounds,
|
||||
context,
|
||||
source_object,
|
||||
edge_length,
|
||||
content_values,
|
||||
content_bounds,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
):
|
||||
"""Resolve target spacing in Blender world units from visible content bounds."""
|
||||
@@ -850,16 +597,25 @@ def effective_cutout_edge_length(
|
||||
columns = np.flatnonzero(visible.any(axis=0))
|
||||
if not len(rows):
|
||||
raise ValueError("The Cutout selection contains no visible image pixels")
|
||||
bounds = crop_plane_bounds(source_object, content_bounds, tuple(source_object.data.size))
|
||||
bounds = crop_plane_bounds(
|
||||
source_object, content_bounds, tuple(source_object.data.size)
|
||||
)
|
||||
metric, _ = world_plane_metric(source_object.matrix_world)
|
||||
metric_transform(metric)
|
||||
width = (bounds[1] - bounds[0]) * (columns.max() + 1 - columns.min()) / values.shape[1]
|
||||
width = (
|
||||
(bounds[1] - bounds[0]) * (columns.max() + 1 - columns.min()) / values.shape[1]
|
||||
)
|
||||
height = (bounds[3] - bounds[2]) * (rows.max() + 1 - rows.min()) / values.shape[0]
|
||||
longest = max(width * math.sqrt(metric[0][0]), height * math.sqrt(metric[1][1]))
|
||||
return max(float(edge_length), longest * configured_min_cutout_relative_edge_length(context))
|
||||
return max(
|
||||
float(edge_length),
|
||||
longest * configured_min_cutout_relative_edge_length(context),
|
||||
)
|
||||
|
||||
|
||||
def selection_calibration(content_values, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
|
||||
def selection_calibration(
|
||||
content_values, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD
|
||||
):
|
||||
"""Return Cutout validity and transparency ratio at geometry resolution."""
|
||||
import numpy as np
|
||||
|
||||
@@ -900,39 +656,27 @@ def build_base_shape(
|
||||
world_metric, world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
if world_z_scale <= 1e-10:
|
||||
raise ValueError("The Image Empty has a degenerate world transform")
|
||||
pixel_transform = np.diag((
|
||||
(bounds[1] - bounds[0]) / content_values.shape[1],
|
||||
-(bounds[3] - bounds[2]) / content_values.shape[0],
|
||||
)) @ metric_transform(world_metric)
|
||||
longest = max(
|
||||
np.linalg.norm(pixel_transform[0]) * content_values.shape[1],
|
||||
np.linalg.norm(pixel_transform[1]) * content_values.shape[0],
|
||||
)
|
||||
fine_mesh = None
|
||||
selection_contours = None
|
||||
if fine_outline:
|
||||
from .fine_mesh import build_fine_mesh
|
||||
pixel_transform = np.diag(
|
||||
(
|
||||
(bounds[1] - bounds[0]) / content_values.shape[1],
|
||||
-(bounds[3] - bounds[2]) / content_values.shape[0],
|
||||
)
|
||||
) @ metric_transform(world_metric)
|
||||
from .mesh import build_mesh
|
||||
|
||||
fine_mesh = (
|
||||
*build_fine_mesh(content_values, edge_length, alpha_threshold, pixel_transform=pixel_transform),
|
||||
(content_values.shape[1], content_values.shape[0]),
|
||||
)
|
||||
else:
|
||||
selection_contours = extract_cutout_contours(
|
||||
mesh = (
|
||||
*build_mesh(
|
||||
content_values,
|
||||
longest / edge_length,
|
||||
threshold=alpha_threshold,
|
||||
)
|
||||
edge_length,
|
||||
alpha_threshold,
|
||||
pixel_transform=pixel_transform,
|
||||
fine_outline=fine_outline,
|
||||
),
|
||||
(content_values.shape[1], content_values.shape[0]),
|
||||
)
|
||||
preferences = addon_preferences(context)
|
||||
balloon_algorithm = getattr(preferences, "balloon_profile_algorithm", "POISSON")
|
||||
domain = cutout_domain(
|
||||
selection_contours,
|
||||
bounds,
|
||||
edge_length,
|
||||
world_metric,
|
||||
balloon_algorithm,
|
||||
fine_mesh=fine_mesh,
|
||||
)
|
||||
domain = cutout_domain(mesh, bounds, world_metric, balloon_algorithm)
|
||||
source_vertices, faces, uv, _boundary, planar, balloon = domain
|
||||
return BaseShape(
|
||||
vertices=np.asarray(source_vertices, dtype=np.float64),
|
||||
|
||||
Reference in New Issue
Block a user