Spaces:
Running on Zero
Running on Zero
Download src/utils/intersection_handler.py from hugging-apps/rigidformer: direct link, hf CLI and curl.
- Browser
- Download file 7.94 kB
-
https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/utils/intersection_handler.py
- Command line
-
hf download hf://spaces/hugging-apps/rigidformer/src/utils/intersection_handler.py
-
curl -L -o intersection_handler.py https://huggingface.co/spaces/hugging-apps/rigidformer/resolve/main/src/utils/intersection_handler.py
7.94 kB
| """ | |
| Differentiable intersection handling. | |
| Resolves object-ground and object-object penetrations by translating whole | |
| objects, with a straight-through estimator (STE) keeping the correction | |
| differentiable. The correction formulation follows UNIC (arXiv:2603.25580, | |
| Sec. 3.3), Eq. 8: | |
| v_corrected = v + detach(target_position - v) | |
| Forward: moves v to target_position | |
| Backward: gradients pass through to v unchanged | |
| Whereas UNIC drags individual garment vertices, rigid objects are translated | |
| as a whole: | |
| - Ground plane: shift the object upward when its lowest vertex falls | |
| below ground_z + buffer | |
| - Object-object: push objects apart when their bounding spheres overlap | |
| """ | |
| import torch | |
| import torch.nn.functional as F | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| def resolve_ground_penetration( | |
| positions: torch.Tensor, | |
| obj_mask: torch.Tensor, | |
| vtx_mask: torch.Tensor, | |
| ground_z: float = 0.0, | |
| buffer: float = 0.005, | |
| ) -> torch.Tensor: | |
| """ | |
| Resolve object-ground penetrations by translating objects upward. | |
| For each valid object, find the LOWEST valid vertex. If min_z < ground_z + buffer, | |
| shift ALL vertices of that object upward by the deficit. | |
| This guarantees no valid vertex penetrates the ground, while preserving rigidity. | |
| Uses STE trick: correction is detached so gradients flow through original positions. | |
| Args: | |
| positions: (B, Nobj, Nv, 3) predicted vertex positions | |
| obj_mask: (B, Nobj) boolean mask for valid objects | |
| vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices | |
| ground_z: ground plane height (z-up coordinate system) | |
| buffer: relaxation buffer above ground plane | |
| Returns: | |
| corrected_positions: (B, Nobj, Nv, 3) with ground penetrations resolved | |
| """ | |
| B, Nobj, Nv, _ = positions.shape | |
| target_z = ground_z + buffer | |
| # Find per-object minimum z among valid vertices | |
| # Set invalid vertices to +inf so they don't affect min | |
| z_vals = positions[..., 2] # (B, Nobj, Nv) | |
| z_vals_masked = z_vals.masked_fill(~vtx_mask, float('inf')) # invalid vertices -> +inf | |
| min_z, _ = z_vals_masked.min(dim=2) # (B, Nobj) lowest valid vertex z per object | |
| # Find objects that penetrate the ground | |
| penetration_depth = target_z - min_z # positive means below ground | |
| # Padding objects have min_z=+inf, giving depth=-inf; clamp to >= 0 before | |
| # the expand below so 0*(-inf) can never produce NaN | |
| penetration_depth = penetration_depth.clamp(min=0.0) | |
| is_penetrating = (penetration_depth > 0) & obj_mask # (B, Nobj) | |
| # No early exit on "nothing penetrates": the correction below is identically | |
| # zero in that case, and the shortcut would cost a host sync per call. | |
| # Compute correction: shift entire object upward by penetration_depth | |
| correction = torch.zeros_like(positions) # (B, Nobj, Nv, 3) | |
| # Only z-component needs correction | |
| correction[..., 2] = penetration_depth.unsqueeze(-1).expand(-1, -1, Nv) # (B, Nobj, Nv) | |
| # Zero out correction for non-penetrating objects | |
| correction = correction * is_penetrating.float().unsqueeze(-1).unsqueeze(-1) # (B, Nobj, Nv, 3) | |
| # STE trick (UNIC Eq. 8): forward moves position, backward passes gradient through | |
| corrected = positions + correction.detach() | |
| return corrected | |
| def resolve_object_penetration( | |
| positions: torch.Tensor, | |
| obj_mask: torch.Tensor, | |
| vtx_mask: torch.Tensor, | |
| buffer: float = 0.005, | |
| ) -> torch.Tensor: | |
| """ | |
| Resolve object-object penetrations using bounding sphere overlap detection. | |
| For each pair of valid objects, compute COM and bounding sphere radius. | |
| If spheres overlap, push both objects apart along the COM-COM direction. | |
| Uses STE trick for differentiability. | |
| Args: | |
| positions: (B, Nobj, Nv, 3) predicted vertex positions | |
| obj_mask: (B, Nobj) boolean mask for valid objects | |
| vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices | |
| buffer: relaxation buffer between objects | |
| Returns: | |
| corrected_positions: (B, Nobj, Nv, 3) with object penetrations resolved | |
| """ | |
| B, Nobj, Nv, _ = positions.shape | |
| if Nobj < 2: | |
| return positions | |
| # Compute per-object COM and bounding sphere radius | |
| vtx_weight = vtx_mask.float().unsqueeze(-1) # (B, Nobj, Nv, 1) | |
| num_valid = vtx_weight.sum(dim=2).clamp(min=1) # (B, Nobj, 1) | |
| com = (positions * vtx_weight).sum(dim=2) / num_valid # (B, Nobj, 3) | |
| # Bounding sphere radius: max distance from COM to any valid vertex | |
| diff_to_com = positions - com.unsqueeze(2) # (B, Nobj, Nv, 3) | |
| dist_to_com = diff_to_com.norm(dim=-1) # (B, Nobj, Nv) | |
| # Mask invalid vertices with 0 distance | |
| dist_to_com = dist_to_com * vtx_mask.float() # (B, Nobj, Nv) | |
| radius = dist_to_com.max(dim=2).values # (B, Nobj) | |
| # Accumulate corrections per object | |
| correction = torch.zeros(B, Nobj, 3, device=positions.device, dtype=positions.dtype) | |
| # Check all pairs (i, j) where i < j | |
| for i in range(Nobj): | |
| for j in range(i + 1, Nobj): | |
| # Both objects must be valid | |
| pair_valid = obj_mask[:, i] & obj_mask[:, j] # (B,) | |
| if not pair_valid.any(): | |
| continue | |
| # COM distance | |
| com_diff = com[:, j] - com[:, i] # (B, 3) | |
| com_dist = com_diff.norm(dim=-1).clamp(min=1e-8) # (B,) | |
| # Minimum allowed distance | |
| min_dist = radius[:, i] + radius[:, j] + buffer # (B,) | |
| # Overlap amount | |
| overlap = min_dist - com_dist # (B,) positive means penetrating | |
| is_overlapping = (overlap > 0) & pair_valid # (B,) | |
| if not is_overlapping.any(): | |
| continue | |
| # Push direction: from i to j (normalized) | |
| push_dir = com_diff / com_dist.unsqueeze(-1) # (B, 3) | |
| # Each object gets pushed by half the overlap | |
| push_amount = (overlap / 2).clamp(min=0) * is_overlapping.float() # (B,) | |
| correction[:, i] -= push_amount.unsqueeze(-1) * push_dir # push i away from j | |
| correction[:, j] += push_amount.unsqueeze(-1) * push_dir # push j away from i | |
| if (correction.abs() < 1e-10).all(): | |
| return positions | |
| # Expand correction to per-vertex: (B, Nobj, 3) -> (B, Nobj, Nv, 3) | |
| correction_expanded = correction.unsqueeze(2).expand(-1, -1, Nv, -1) | |
| # STE trick | |
| corrected = positions + correction_expanded.detach() | |
| return corrected | |
| def handle_intersections( | |
| positions: torch.Tensor, | |
| obj_mask: torch.Tensor, | |
| vtx_mask: torch.Tensor, | |
| config: dict, | |
| ) -> torch.Tensor: | |
| """ | |
| Main entry point: apply all intersection resolution steps. | |
| Args: | |
| positions: (B, Nobj, Nv, 3) predicted vertex positions | |
| obj_mask: (B, Nobj) boolean mask | |
| vtx_mask: (B, Nobj, Nv) boolean mask for valid vertices | |
| config: intersection_handling config dict | |
| Returns: | |
| corrected positions: (B, Nobj, Nv, 3) | |
| """ | |
| if not config.get('enable', False): | |
| return positions | |
| ground_cfg = config.get('ground', {}) | |
| object_cfg = config.get('object', {}) | |
| # Step 1: Resolve ground penetrations | |
| if ground_cfg.get('enable', True): | |
| positions = resolve_ground_penetration( | |
| positions, obj_mask, vtx_mask, | |
| ground_z=ground_cfg.get('z', 0.0), | |
| buffer=ground_cfg.get('buffer', 0.005), | |
| ) | |
| # Step 2: Resolve object-object penetrations | |
| if object_cfg.get('enable', False): | |
| positions = resolve_object_penetration( | |
| positions, obj_mask, vtx_mask, | |
| buffer=object_cfg.get('buffer', 0.005), | |
| ) | |
| return positions | |