WireFrameDETR / s23dr_2026 /postprocess.py
StarAtNyte1's picture
Upload s23dr_2026/postprocess.py with huggingface_hub
95bc182 verified
Raw
History Blame Contribute Delete
26.1 kB
import numpy as np
def _empty_edges_like(edges: np.ndarray) -> np.ndarray:
return np.zeros((0, 2), dtype=edges.dtype)
def _compact_vertices_for_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts[:0], _empty_edges_like(edges)
used_vertex_indices, inverse = np.unique(edges.reshape(-1), return_inverse=True)
compact_verts = verts[used_vertex_indices]
compact_edges = inverse.reshape(-1, 2).astype(edges.dtype, copy=False)
return compact_verts, compact_edges
def _compact_vertices_and_features_for_edges(
verts: np.ndarray, edges: np.ndarray, vertex_feats: np.ndarray
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts[:0], _empty_edges_like(edges), vertex_feats[:0]
used_vertex_indices, inverse = np.unique(edges.reshape(-1), return_inverse=True)
compact_verts = verts[used_vertex_indices]
compact_edges = inverse.reshape(-1, 2).astype(edges.dtype, copy=False)
compact_feats = vertex_feats[used_vertex_indices]
return compact_verts, compact_edges, compact_feats
def _merged_vertex_from_incident_lines(
verts: np.ndarray,
edges: np.ndarray,
component_members: np.ndarray,
incident_edge_indices_by_vertex: list[list[int]],
average_position: np.ndarray,
intersection_angle_threshold_deg: float | None,
) -> np.ndarray:
if len(component_members) < 2:
return average_position
if intersection_angle_threshold_deg is None or intersection_angle_threshold_deg <= 0:
return average_position
component_member_set = set(int(vertex) for vertex in component_members)
component_edge_indices: set[int] = set()
for vertex in component_members:
component_edge_indices.update(incident_edge_indices_by_vertex[int(vertex)])
line_points: list[np.ndarray] = []
line_dirs: list[np.ndarray] = []
for edge_idx in component_edge_indices:
a, b = edges[edge_idx]
a = int(a)
b = int(b)
if (a in component_member_set) == (b in component_member_set):
continue
line_vec = verts[b] - verts[a]
line_length = np.linalg.norm(line_vec)
if line_length == 0:
continue
line_points.append(verts[a].astype(np.float64, copy=False))
line_dirs.append(line_vec.astype(np.float64, copy=False) / line_length)
if len(line_dirs) < 2:
return average_position
directions = np.array(line_dirs, dtype=np.float64)
dot_products = np.abs(directions @ directions.T)
upper_triangle = np.triu_indices(len(directions), k=1)
angles_deg = np.degrees(np.arccos(np.clip(dot_products[upper_triangle], -1.0, 1.0)))
if not np.any(angles_deg >= intersection_angle_threshold_deg):
return average_position
system = np.zeros((3, 3), dtype=np.float64)
rhs = np.zeros(3, dtype=np.float64)
identity = np.eye(3, dtype=np.float64)
for point, direction in zip(line_points, line_dirs):
projection = identity - np.outer(direction, direction)
system += projection
rhs += projection @ point
try:
intersection = np.linalg.lstsq(system, rhs, rcond=None)[0]
except np.linalg.LinAlgError:
return average_position
if not np.all(np.isfinite(intersection)):
return average_position
return intersection.astype(average_position.dtype, copy=False)
def merge_close_vertices(
verts: np.ndarray,
edges: np.ndarray,
distance_threshold: float,
intersection_angle_threshold_deg: float | None = 30.0,
) -> tuple[np.ndarray, np.ndarray]:
if len(verts) == 0 or distance_threshold <= 0:
return verts, edges
cell_size = float(distance_threshold)
cell_coords = np.floor(verts / cell_size).astype(np.int64)
buckets: dict[tuple[int, int, int], list[int]] = {}
parent = np.arange(len(verts), dtype=np.int64)
rank = np.zeros(len(verts), dtype=np.int8)
members = [set([i]) for i in range(len(verts))]
edge_neighbors = [set() for _ in range(len(verts))]
incident_edge_indices_by_vertex: list[list[int]] = [[] for _ in range(len(verts))]
for edge_idx, (a, b) in enumerate(edges):
a = int(a)
b = int(b)
incident_edge_indices_by_vertex[a].append(edge_idx)
if a != b:
incident_edge_indices_by_vertex[b].append(edge_idx)
if a == b:
continue
edge_neighbors[a].add(b)
edge_neighbors[b].add(a)
def find(i: int) -> int:
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
def components_share_edge(a_members: set[int], b_members: set[int]) -> bool:
if len(a_members) > len(b_members):
a_members, b_members = b_members, a_members
return any(not edge_neighbors[vertex].isdisjoint(b_members) for vertex in a_members)
def union(a: int, b: int) -> None:
ra, rb = find(a), find(b)
if ra == rb or components_share_edge(members[ra], members[rb]):
return
if rank[ra] < rank[rb]:
ra, rb = rb, ra
parent[rb] = ra
if rank[ra] == rank[rb]:
rank[ra] += 1
members[ra].update(members[rb])
members[rb].clear()
for i, cell in enumerate(cell_coords):
key = tuple(cell.tolist())
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
for dz in (-1, 0, 1):
neighbor_key = (key[0] + dx, key[1] + dy, key[2] + dz)
for j in buckets.get(neighbor_key, ()):
if np.linalg.norm(verts[i] - verts[j]) <= distance_threshold:
union(i, j)
buckets.setdefault(key, []).append(i)
roots = np.array([find(i) for i in range(len(verts))], dtype=np.int64)
unique_roots, inverse = np.unique(roots, return_inverse=True)
merged_verts = np.zeros((len(unique_roots), 3), dtype=verts.dtype)
for new_vertex_idx in range(len(unique_roots)):
component_members = np.flatnonzero(inverse == new_vertex_idx)
average_position = np.mean(verts[component_members], axis=0)
merged_verts[new_vertex_idx] = _merged_vertex_from_incident_lines(
verts,
edges,
component_members,
incident_edge_indices_by_vertex,
average_position,
intersection_angle_threshold_deg,
)
old_to_new = inverse
merged_edges = old_to_new[edges]
keep = merged_edges[:, 0] != merged_edges[:, 1]
merged_edges = merged_edges[keep]
if len(merged_edges) == 0:
return merged_verts, np.zeros((0, 2), dtype=np.int64)
merged_edges = np.sort(merged_edges, axis=1)
merged_edges = np.unique(merged_edges, axis=0)
return merged_verts, merged_edges
def merge_vertices_with_features(
verts: np.ndarray,
edges: np.ndarray,
vertex_feats: np.ndarray,
alpha: float = 0.95,
distance_threshold: float | None = None,
vertex_scores: np.ndarray | None = None,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
if len(verts) == 0 or len(vertex_feats) == 0:
return verts, edges, vertex_feats
if len(verts) != len(vertex_feats):
raise ValueError("verts and vertex_feats must have the same length")
feats = vertex_feats.astype(np.float32, copy=False)
sim = feats @ feats.T
parent = np.arange(len(verts), dtype=np.int64)
rank = np.zeros(len(verts), dtype=np.int8)
members = [set([i]) for i in range(len(verts))]
edge_neighbors = [set() for _ in range(len(verts))]
for a, b in edges:
if a == b:
continue
edge_neighbors[int(a)].add(int(b))
edge_neighbors[int(b)].add(int(a))
def find(i: int) -> int:
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
def components_share_edge(a_members: set[int], b_members: set[int]) -> bool:
if len(a_members) > len(b_members):
a_members, b_members = b_members, a_members
return any(not edge_neighbors[vertex].isdisjoint(b_members) for vertex in a_members)
def union(a: int, b: int) -> None:
ra, rb = find(a), find(b)
if ra == rb or components_share_edge(members[ra], members[rb]):
return
if rank[ra] < rank[rb]:
ra, rb = rb, ra
parent[rb] = ra
if rank[ra] == rank[rb]:
rank[ra] += 1
members[ra].update(members[rb])
members[rb].clear()
rows, cols = np.where(np.triu(sim >= alpha, k=1))
if distance_threshold is not None:
if distance_threshold <= 0:
rows, cols = np.array([], dtype=np.int64), np.array([], dtype=np.int64)
else:
distances = np.linalg.norm(verts[rows] - verts[cols], axis=1)
keep = distances <= distance_threshold
rows, cols = rows[keep], cols[keep]
for a, b in zip(rows.tolist(), cols.tolist()):
union(a, b)
roots = np.array([find(i) for i in range(len(verts))], dtype=np.int64)
unique_roots, inverse = np.unique(roots, return_inverse=True)
merged_verts = np.zeros((len(unique_roots), 3), dtype=verts.dtype)
counts = np.bincount(inverse)
np.add.at(merged_verts, inverse, verts)
merged_verts /= counts[:, None]
if vertex_scores is not None:
scores = vertex_scores.astype(np.float32, copy=False)
best_score = np.full(len(unique_roots), -np.inf, dtype=np.float32)
best_index = np.zeros(len(unique_roots), dtype=np.int64)
for idx, group_idx in enumerate(inverse):
score = scores[idx]
if score > best_score[group_idx]:
best_score[group_idx] = score
best_index[group_idx] = idx
merged_feats = vertex_feats[best_index]
else:
merged_feats = np.zeros((len(unique_roots), vertex_feats.shape[1]), dtype=vertex_feats.dtype)
np.add.at(merged_feats, inverse, vertex_feats)
merged_feats /= counts[:, None]
merged_edges = inverse[edges]
keep = merged_edges[:, 0] != merged_edges[:, 1]
merged_edges = merged_edges[keep]
if len(merged_edges) == 0:
return merged_verts, np.zeros((0, 2), dtype=np.int64), merged_feats
merged_edges = np.sort(merged_edges, axis=1)
merged_edges = np.unique(merged_edges, axis=0)
return merged_verts, merged_edges, merged_feats
def filter_edges_by_length(
verts: np.ndarray,
edges: np.ndarray,
min_length: float | None = 0.05,
max_length: float | None = 100.0,
) -> tuple[np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts[:0], _empty_edges_like(edges)
edge_segments = verts[edges]
edge_lengths = np.linalg.norm(edge_segments[:, 1] - edge_segments[:, 0], axis=1)
keep = np.ones(len(edges), dtype=bool)
if min_length is not None:
keep &= edge_lengths >= min_length
if max_length is not None:
keep &= edge_lengths <= max_length
return _compact_vertices_for_edges(verts, edges[keep])
def add_gap_fill_edges_from_candidates(
verts: np.ndarray,
edges: np.ndarray,
candidate_edge_points: np.ndarray,
merge_distance_threshold: float,
) -> tuple[np.ndarray, np.ndarray]:
if len(verts) == 0 or len(candidate_edge_points) == 0 or merge_distance_threshold < 0:
return verts, edges
candidate_edge_points = np.asarray(candidate_edge_points).reshape(-1, 2, verts.shape[1])
endpoint_distances = np.linalg.norm(
candidate_edge_points.reshape(-1, verts.shape[1])[:, None, :] - verts[None, :, :],
axis=2,
)
nearest_vertices = np.argmin(endpoint_distances, axis=1).reshape(-1, 2)
nearest_distances = np.min(endpoint_distances, axis=1).reshape(-1, 2)
known_edges = {tuple(sorted((int(a), int(b)))) for a, b in edges if int(a) != int(b)}
added_edges: list[tuple[int, int]] = []
for edge_vertices, edge_distances in zip(nearest_vertices, nearest_distances):
if np.any(edge_distances > merge_distance_threshold):
continue
a, b = int(edge_vertices[0]), int(edge_vertices[1])
if a == b:
continue
edge_key = tuple(sorted((a, b)))
if edge_key in known_edges:
continue
known_edges.add(edge_key)
added_edges.append(edge_key)
if len(added_edges) == 0:
return verts, edges
added_edges_array = np.array(added_edges, dtype=edges.dtype)
return verts, np.concatenate([edges, added_edges_array], axis=0)
def add_edges_by_translated_edge_symmetry(
verts: np.ndarray,
edges: np.ndarray,
distance_threshold: float = 0.2,
) -> tuple[np.ndarray, np.ndarray]:
if len(verts) == 0 or len(edges) == 0 or distance_threshold < 0:
return verts, edges
template_edges = edges.copy()
known_edges = {tuple(sorted((int(a), int(b)))) for a, b in edges if int(a) != int(b)}
added_edges: list[tuple[int, int]] = []
for a, b in template_edges:
a = int(a)
b = int(b)
edge_vector = verts[b] - verts[a]
if not np.any(edge_vector):
continue
for template_vector in (edge_vector, -edge_vector):
translated_endpoints = verts + template_vector
distances = np.linalg.norm(translated_endpoints[:, None, :] - verts[None, :, :], axis=2)
nearest_vertices = np.argmin(distances, axis=1)
nearest_distances = np.min(distances, axis=1)
for start_vertex, (end_vertex, end_distance) in enumerate(zip(nearest_vertices, nearest_distances)):
if end_distance > distance_threshold:
continue
end_vertex = int(end_vertex)
if start_vertex == end_vertex:
continue
edge_key = tuple(sorted((start_vertex, end_vertex)))
if edge_key in known_edges:
continue
known_edges.add(edge_key)
added_edges.append(edge_key)
if len(added_edges) == 0:
return verts, edges
added_edges_array = np.array(added_edges, dtype=edges.dtype)
return verts, np.concatenate([edges, added_edges_array], axis=0)
def _edge_angle_to_xz_plane_deg(edge_vecs: np.ndarray) -> np.ndarray:
xz_norms = np.linalg.norm(edge_vecs[:, [0, 2]], axis=1)
return np.degrees(np.arctan2(np.abs(edge_vecs[:, 1]), xz_norms))
def _infer_principal_xz_directions(
edge_vecs: np.ndarray,
edge_lengths: np.ndarray,
horizontal_angle_threshold_deg: float,
) -> tuple[np.ndarray, np.ndarray] | None:
xz_vecs = edge_vecs[:, [0, 2]]
xz_norms = np.linalg.norm(xz_vecs, axis=1)
angles = _edge_angle_to_xz_plane_deg(edge_vecs)
keep = (edge_lengths > 0) & (xz_norms > 0) & (angles <= horizontal_angle_threshold_deg)
if not np.any(keep):
return None
dirs = xz_vecs[keep] / xz_norms[keep, None]
weights = edge_lengths[keep]
covariance = np.einsum("i,ij,ik->jk", weights, dirs, dirs)
if not np.any(covariance):
return None
eigvals, eigvecs = np.linalg.eigh(covariance)
primary = eigvecs[:, int(np.argmax(eigvals))]
primary_norm = np.linalg.norm(primary)
if primary_norm == 0:
return None
primary = primary / primary_norm
secondary = np.array([-primary[1], primary[0]], dtype=primary.dtype)
return primary, secondary
def project_near_horizontal_edges_to_xz_plane(
verts: np.ndarray,
edges: np.ndarray,
horizontal_angle_threshold_deg: float = 5.0,
principal_direction_angle_threshold_deg: float = 5.0,
) -> tuple[np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts, edges
edge_vecs = verts[edges[:, 1]] - verts[edges[:, 0]]
edge_lengths = np.linalg.norm(edge_vecs, axis=1)
principal_dirs = _infer_principal_xz_directions(edge_vecs, edge_lengths, horizontal_angle_threshold_deg)
if principal_dirs is None:
return verts, edges
xz_vecs = edge_vecs[:, [0, 2]]
xz_norms = np.linalg.norm(xz_vecs, axis=1)
valid = (edge_lengths > 0) & (xz_norms > 0)
if not np.any(valid):
return verts, edges
angles_to_xz = _edge_angle_to_xz_plane_deg(edge_vecs)
dirs_xz = np.zeros_like(xz_vecs, dtype=np.result_type(verts.dtype, np.float64))
dirs_xz[valid] = xz_vecs[valid] / xz_norms[valid, None]
dots = np.stack([np.abs(dirs_xz @ direction) for direction in principal_dirs], axis=1)
angles_to_principal = np.degrees(np.arccos(np.clip(np.max(dots, axis=1), -1.0, 1.0)))
eligible = (
valid
& (angles_to_xz <= horizontal_angle_threshold_deg)
& (angles_to_principal <= principal_direction_angle_threshold_deg)
)
eligible_edges = edges[eligible]
if len(eligible_edges) == 0:
return verts, edges
parent = np.arange(len(verts), dtype=np.int64)
def find(i: int) -> int:
while parent[i] != i:
parent[i] = parent[parent[i]]
i = parent[i]
return i
def union(a: int, b: int) -> None:
ra, rb = find(a), find(b)
if ra != rb:
parent[rb] = ra
for a, b in eligible_edges:
union(int(a), int(b))
new_verts = verts.copy()
eligible_vertices = np.unique(eligible_edges.reshape(-1))
roots = np.array([find(int(vertex)) for vertex in eligible_vertices], dtype=np.int64)
for root in np.unique(roots):
component_vertices = eligible_vertices[roots == root]
new_verts[component_vertices, 1] = np.mean(verts[component_vertices, 1])
return new_verts, edges
def snap_edges_to_principal_xz_directions(
verts: np.ndarray,
edges: np.ndarray,
horizontal_angle_threshold_deg: float = 5.0,
principal_direction_angle_threshold_deg: float = 5.0,
) -> tuple[np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts, edges
edge_vecs = verts[edges[:, 1]] - verts[edges[:, 0]]
edge_lengths = np.linalg.norm(edge_vecs, axis=1)
principal_dirs = _infer_principal_xz_directions(edge_vecs, edge_lengths, horizontal_angle_threshold_deg)
if principal_dirs is None:
return verts, edges
xz_vecs = edge_vecs[:, [0, 2]]
xz_norms = np.linalg.norm(xz_vecs, axis=1)
valid = xz_norms > 0
if not np.any(valid):
return verts, edges
dirs_xz = np.zeros_like(xz_vecs, dtype=np.result_type(verts.dtype, np.float64))
dirs_xz[valid] = xz_vecs[valid] / xz_norms[valid, None]
dots = np.stack([np.abs(dirs_xz @ direction) for direction in principal_dirs], axis=1)
best_direction_indices = np.argmax(dots, axis=1)
angles_to_principal = np.degrees(np.arccos(np.clip(np.max(dots, axis=1), -1.0, 1.0)))
eligible = valid & (angles_to_principal <= principal_direction_angle_threshold_deg)
eligible_edges = edges[eligible]
if len(eligible_edges) == 0:
return verts, edges
eligible_vertices = np.unique(eligible_edges.reshape(-1))
vertex_to_local = {int(vertex): idx for idx, vertex in enumerate(eligible_vertices)}
constraints = np.zeros((len(eligible_edges), 2 * len(eligible_vertices)), dtype=np.float64)
eligible_direction_indices = best_direction_indices[eligible]
for row, ((a, b), direction_idx) in enumerate(zip(eligible_edges, eligible_direction_indices)):
direction = principal_dirs[int(direction_idx)]
normal = np.array([-direction[1], direction[0]], dtype=np.float64)
a_col = 2 * vertex_to_local[int(a)]
b_col = 2 * vertex_to_local[int(b)]
constraints[row, a_col : a_col + 2] = -normal
constraints[row, b_col : b_col + 2] = normal
original_xz = verts[eligible_vertices][:, [0, 2]].astype(np.float64, copy=False).reshape(-1)
residual = constraints @ original_xz
if not np.any(residual):
return verts, edges
system = constraints @ constraints.T
correction_weights = np.linalg.lstsq(system, residual, rcond=None)[0]
snapped_xz = original_xz - constraints.T @ correction_weights
new_verts = verts.copy()
snapped_xz = snapped_xz.reshape(-1, 2)
new_verts[eligible_vertices, 0] = snapped_xz[:, 0]
new_verts[eligible_vertices, 2] = snapped_xz[:, 1]
return new_verts, edges
project_near_horizontal_edges_to_xy_plane = project_near_horizontal_edges_to_xz_plane
snap_edges_to_principal_xy_directions = snap_edges_to_principal_xz_directions
def _remove_solitary_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
if len(edges) == 0:
return verts[:0], _empty_edges_like(edges)
degrees = np.bincount(edges.reshape(-1), minlength=len(verts))
edge_degrees = degrees[edges]
keep = np.any(edge_degrees > 1, axis=1)
return _compact_vertices_for_edges(verts, edges[keep])
def remove_solitary_edges(verts: np.ndarray, edges: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
return _remove_solitary_edges(verts, edges)
def filter_edges_by_connectivity(
verts: np.ndarray,
edges: np.ndarray,
min_endpoint_degree: int = 2,
) -> tuple[np.ndarray, np.ndarray]:
"""Keep edges where at least one endpoint connects to >= min_endpoint_degree other edges.
Boosts structurally connected edges; penalizes isolated ones. min_endpoint_degree=2
keeps any edge touching a vertex with 2+ incident edges.
"""
if len(edges) == 0 or min_endpoint_degree <= 1:
return verts, edges
degrees = np.bincount(edges.reshape(-1), minlength=len(verts))
endpoint_degrees = degrees[edges] # [E, 2]
keep = np.any(endpoint_degrees >= min_endpoint_degree, axis=1)
return _compact_vertices_for_edges(verts, edges[keep])
def _point_to_segments_distance(points: np.ndarray, seg_starts: np.ndarray, seg_ends: np.ndarray) -> np.ndarray:
dtype = np.result_type(points.dtype, seg_starts.dtype, seg_ends.dtype, np.float64)
points = points.astype(dtype, copy=False)
seg_starts = seg_starts.astype(dtype, copy=False)
seg_ends = seg_ends.astype(dtype, copy=False)
seg_vecs = seg_ends - seg_starts
seg_lens_sq = np.einsum("ij,ij->i", seg_vecs, seg_vecs)
offsets = points[:, None, :] - seg_starts[None, :, :]
t = np.zeros((len(points), len(seg_starts)), dtype=dtype)
valid = seg_lens_sq > 0
if np.any(valid):
t[:, valid] = np.einsum("pkj,kj->pk", offsets[:, valid], seg_vecs[valid]) / seg_lens_sq[valid]
t = np.clip(t, 0.0, 1.0)
closest = seg_starts[None, :, :] + t[:, :, None] * seg_vecs[None, :, :]
return np.linalg.norm(points[:, None, :] - closest, axis=2)
def remove_edges_close_to_other_edges(
verts: np.ndarray, edges: np.ndarray, distance_threshold: float
) -> tuple[np.ndarray, np.ndarray]:
if distance_threshold <= 0:
return verts, edges
if len(edges) == 0:
return verts[:0], _empty_edges_like(edges)
edge_segments = verts[edges]
edge_vecs = edge_segments[:, 1] - edge_segments[:, 0]
edge_lengths = np.linalg.norm(edge_vecs, axis=1)
edge_order = np.lexsort((np.arange(len(edges)), -edge_lengths))
kept_edge_indices: list[int] = []
for edge_idx in edge_order:
if len(kept_edge_indices) == 0:
kept_edge_indices.append(edge_idx)
continue
endpoints = edge_segments[edge_idx]
kept_segments = edge_segments[kept_edge_indices]
distances = _point_to_segments_distance(endpoints, kept_segments[:, 0], kept_segments[:, 1])
if np.any(distances.mean(axis=0) <= distance_threshold):
continue
kept_edge_indices.append(edge_idx)
kept_edges = edges[sorted(kept_edge_indices)]
return _compact_vertices_for_edges(verts, kept_edges)
def post_process_wireframe(
verts: np.ndarray,
edges: np.ndarray,
merge_distance_threshold: float,
remove_solitary_edges: bool = False,
filter_by_length: bool = False,
project_near_horizontal_edges: bool = False,
snap_to_principal_directions: bool = False,
min_edge_length: float | None = 0.05,
max_edge_length: float | None = 100.0,
horizontal_angle_threshold_deg: float = 5.0,
principal_direction_angle_threshold_deg: float = 5.0,
intersection_angle_threshold_deg: float | None = 30.0,
gap_fill_candidate_edge_points: np.ndarray | None = None,
complete_symmetric_edges: bool = False,
symmetric_edge_distance_threshold: float = 0.2,
connectivity_filter_min_degree: int = 0,
) -> tuple[np.ndarray, np.ndarray]:
verts, edges = merge_close_vertices(
verts,
edges,
merge_distance_threshold,
intersection_angle_threshold_deg=intersection_angle_threshold_deg,
)
if complete_symmetric_edges:
verts, edges = add_edges_by_translated_edge_symmetry(
verts,
edges,
distance_threshold=symmetric_edge_distance_threshold,
)
if filter_by_length:
verts, edges = filter_edges_by_length(verts, edges, min_edge_length, max_edge_length)
if project_near_horizontal_edges:
verts, edges = project_near_horizontal_edges_to_xz_plane(
verts,
edges,
horizontal_angle_threshold_deg=horizontal_angle_threshold_deg,
principal_direction_angle_threshold_deg=principal_direction_angle_threshold_deg,
)
if snap_to_principal_directions:
verts, edges = snap_edges_to_principal_xz_directions(
verts,
edges,
horizontal_angle_threshold_deg=horizontal_angle_threshold_deg,
principal_direction_angle_threshold_deg=principal_direction_angle_threshold_deg,
)
if remove_solitary_edges:
verts, edges = _remove_solitary_edges(verts, edges)
if gap_fill_candidate_edge_points is not None:
verts, edges = add_gap_fill_edges_from_candidates(
verts,
edges,
gap_fill_candidate_edge_points,
merge_distance_threshold,
)
if connectivity_filter_min_degree > 1:
verts, edges = filter_edges_by_connectivity(verts, edges, min_endpoint_degree=connectivity_filter_min_degree)
return verts, edges