CoolFace
Apppublic

tohid4n/PartCrafter

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
smoothing.py644 linesDownload Raw Back to utils
1# -*- coding: utf-8 -*-2 3# Copyright (c) 2012-2015, P. M. Neila4# All rights reserved.5 6# Redistribution and use in source and binary forms, with or without7# modification, are permitted provided that the following conditions are met:8 9# * Redistributions of source code must retain the above copyright notice, this10#   list of conditions and the following disclaimer.11 12# * Redistributions in binary form must reproduce the above copyright notice,13#   this list of conditions and the following disclaimer in the documentation14#   and/or other materials provided with the distribution.15 16# * Neither the name of the copyright holder nor the names of its17#   contributors may be used to endorse or promote products derived from18#   this software without specific prior written permission.19 20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"21# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE22# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE24# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL25# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR26# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER27# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,28# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE29# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.30 31"""32Utilities for smoothing the occ/sdf grids.33"""34 35import logging36from typing import Tuple37 38import numpy as np39import torch40import torch.nn.functional as F41from scipy import ndimage as ndi42from scipy import sparse43 44__all__ = [45    "smooth",46    "smooth_constrained",47    "smooth_gaussian",48    "signed_distance_function",49    "smooth_gpu",50    "smooth_constrained_gpu",51    "smooth_gaussian_gpu",52    "signed_distance_function_gpu",53]54 55 56def _build_variable_indices(band: np.ndarray) -> np.ndarray:57    num_variables = np.count_nonzero(band)58    variable_indices = np.full(band.shape, -1, dtype=np.int_)59    variable_indices[band] = np.arange(num_variables)60    return variable_indices61 62 63def _buildq3d(variable_indices: np.ndarray):64    """65    Builds the filterq matrix for the given variables.66    """67 68    num_variables = variable_indices.max() + 169    filterq = sparse.lil_matrix((3 * num_variables, num_variables))70 71    # Pad variable_indices to simplify out-of-bounds accesses72    variable_indices = np.pad(73        variable_indices, [(0, 1), (0, 1), (0, 1)], mode="constant", constant_values=-174    )75 76    coords = np.nonzero(variable_indices >= 0)77    for count, (i, j, k) in enumerate(zip(*coords)):78 79        assert variable_indices[i, j, k] == count80 81        filterq[3 * count, count] = -282        neighbor = variable_indices[i - 1, j, k]83        if neighbor >= 0:84            filterq[3 * count, neighbor] = 185        else:86            filterq[3 * count, count] += 187 88        neighbor = variable_indices[i + 1, j, k]89        if neighbor >= 0:90            filterq[3 * count, neighbor] = 191        else:92            filterq[3 * count, count] += 193 94        filterq[3 * count + 1, count] = -295        neighbor = variable_indices[i, j - 1, k]96        if neighbor >= 0:97            filterq[3 * count + 1, neighbor] = 198        else:99            filterq[3 * count + 1, count] += 1100 101        neighbor = variable_indices[i, j + 1, k]102        if neighbor >= 0:103            filterq[3 * count + 1, neighbor] = 1104        else:105            filterq[3 * count + 1, count] += 1106 107        filterq[3 * count + 2, count] = -2108        neighbor = variable_indices[i, j, k - 1]109        if neighbor >= 0:110            filterq[3 * count + 2, neighbor] = 1111        else:112            filterq[3 * count + 2, count] += 1113 114        neighbor = variable_indices[i, j, k + 1]115        if neighbor >= 0:116            filterq[3 * count + 2, neighbor] = 1117        else:118            filterq[3 * count + 2, count] += 1119 120    filterq = filterq.tocsr()121    return filterq.T.dot(filterq)122 123 124def _buildq3d_gpu(variable_indices: torch.Tensor, chunk_size=10000):125    """126    Builds the filterq matrix for the given variables on GPU, using chunking to reduce memory usage.127    """128    device = variable_indices.device129    num_variables = variable_indices.max().item() + 1130 131    # Pad variable_indices to simplify out-of-bounds accesses132    variable_indices = torch.nn.functional.pad(133        variable_indices, (0, 1, 0, 1, 0, 1), mode="constant", value=-1134    )135 136    coords = torch.nonzero(variable_indices >= 0)137    i, j, k = coords[:, 0], coords[:, 1], coords[:, 2]138 139    # Function to process a chunk of data140    def process_chunk(start, end):141        row_indices = []142        col_indices = []143        values = []144 145        for axis in range(3):146            row_indices.append(3 * torch.arange(start, end, device=device) + axis)147            col_indices.append(148                variable_indices[i[start:end], j[start:end], k[start:end]]149            )150            values.append(torch.full((end - start,), -2, device=device))151 152            for offset in [-1, 1]:153                if axis == 0:154                    neighbor = variable_indices[155                        i[start:end] + offset, j[start:end], k[start:end]156                    ]157                elif axis == 1:158                    neighbor = variable_indices[159                        i[start:end], j[start:end] + offset, k[start:end]160                    ]161                else:162                    neighbor = variable_indices[163                        i[start:end], j[start:end], k[start:end] + offset164                    ]165 166                mask = neighbor >= 0167                row_indices.append(168                    3 * torch.arange(start, end, device=device)[mask] + axis169                )170                col_indices.append(neighbor[mask])171                values.append(torch.ones(mask.sum(), device=device))172 173                # Add 1 to the diagonal for out-of-bounds neighbors174                row_indices.append(175                    3 * torch.arange(start, end, device=device)[~mask] + axis176                )177                col_indices.append(178                    variable_indices[i[start:end], j[start:end], k[start:end]][~mask]179                )180                values.append(torch.ones((~mask).sum(), device=device))181 182        return torch.cat(row_indices), torch.cat(col_indices), torch.cat(values)183 184    # Process data in chunks185    all_row_indices = []186    all_col_indices = []187    all_values = []188 189    for start in range(0, coords.shape[0], chunk_size):190        end = min(start + chunk_size, coords.shape[0])191        row_indices, col_indices, values = process_chunk(start, end)192        all_row_indices.append(row_indices)193        all_col_indices.append(col_indices)194        all_values.append(values)195 196    # Concatenate all chunks197    row_indices = torch.cat(all_row_indices)198    col_indices = torch.cat(all_col_indices)199    values = torch.cat(all_values)200 201    # Create sparse tensor202    indices = torch.stack([row_indices, col_indices])203    filterq = torch.sparse_coo_tensor(204        indices, values, (3 * num_variables, num_variables)205    )206 207    # Compute filterq.T @ filterq208    return torch.sparse.mm(filterq.t(), filterq)209 210 211# Usage example:212# variable_indices = torch.tensor(...).cuda()  # Your input tensor on GPU213# result = _buildq3d_gpu(variable_indices)214 215 216def _buildq2d(variable_indices: np.ndarray):217    """218    Builds the filterq matrix for the given variables.219 220    Version for 2 dimensions.221    """222 223    num_variables = variable_indices.max() + 1224    filterq = sparse.lil_matrix((3 * num_variables, num_variables))225 226    # Pad variable_indices to simplify out-of-bounds accesses227    variable_indices = np.pad(228        variable_indices, [(0, 1), (0, 1)], mode="constant", constant_values=-1229    )230 231    coords = np.nonzero(variable_indices >= 0)232    for count, (i, j) in enumerate(zip(*coords)):233        assert variable_indices[i, j] == count234 235        filterq[2 * count, count] = -2236        neighbor = variable_indices[i - 1, j]237        if neighbor >= 0:238            filterq[2 * count, neighbor] = 1239        else:240            filterq[2 * count, count] += 1241 242        neighbor = variable_indices[i + 1, j]243        if neighbor >= 0:244            filterq[2 * count, neighbor] = 1245        else:246            filterq[2 * count, count] += 1247 248        filterq[2 * count + 1, count] = -2249        neighbor = variable_indices[i, j - 1]250        if neighbor >= 0:251            filterq[2 * count + 1, neighbor] = 1252        else:253            filterq[2 * count + 1, count] += 1254 255        neighbor = variable_indices[i, j + 1]256        if neighbor >= 0:257            filterq[2 * count + 1, neighbor] = 1258        else:259            filterq[2 * count + 1, count] += 1260 261    filterq = filterq.tocsr()262    return filterq.T.dot(filterq)263 264 265def _jacobi(266    filterq,267    x0: np.ndarray,268    lower_bound: np.ndarray,269    upper_bound: np.ndarray,270    max_iters: int = 10,271    rel_tol: float = 1e-6,272    weight: float = 0.5,273):274    """Jacobi method with constraints."""275 276    jacobi_r = sparse.lil_matrix(filterq)277    shp = jacobi_r.shape278    jacobi_d = 1.0 / filterq.diagonal()279    jacobi_r.setdiag((0,) * shp[0])280    jacobi_r = jacobi_r.tocsr()281 282    x = x0283 284    # We check the stopping criterion each 10 iterations285    check_each = 10286    cum_rel_tol = 1 - (1 - rel_tol) ** check_each287 288    energy_now = np.dot(x, filterq.dot(x)) / 2289    logging.info("Energy at iter %d: %.6g", 0, energy_now)290    for i in range(max_iters):291 292        x_1 = -jacobi_d * jacobi_r.dot(x)293        x = weight * x_1 + (1 - weight) * x294 295        # Constraints.296        x = np.maximum(x, lower_bound)297        x = np.minimum(x, upper_bound)298 299        # Stopping criterion300        if (i + 1) % check_each == 0:301            # Update energy302            energy_before = energy_now303            energy_now = np.dot(x, filterq.dot(x)) / 2304 305            logging.info("Energy at iter %d: %.6g", i + 1, energy_now)306 307            # Check stopping criterion308            cum_rel_improvement = (energy_before - energy_now) / energy_before309            if cum_rel_improvement < cum_rel_tol:310                break311 312    return x313 314 315def signed_distance_function(316    levelset: np.ndarray, band_radius: int317) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:318    """319    Return the distance to the 0.5 levelset of a function, the mask of the320    border (i.e., the nearest cells to the 0.5 level-set) and the mask of the321    band (i.e., the cells of the function whose distance to the 0.5 level-set322    is less of equal to `band_radius`).323    """324 325    binary_array = np.where(levelset > 0, True, False)326 327    # Compute the band and the border.328    dist_func = ndi.distance_transform_edt329    distance = np.where(330        binary_array, dist_func(binary_array) - 0.5, -dist_func(~binary_array) + 0.5331    )332    border = np.abs(distance) < 1333    band = np.abs(distance) <= band_radius334 335    return distance, border, band336 337 338def signed_distance_function_iso0(339    levelset: np.ndarray, band_radius: int340) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:341    """342    Return the distance to the 0 levelset of a function, the mask of the343    border (i.e., the nearest cells to the 0 level-set) and the mask of the344    band (i.e., the cells of the function whose distance to the 0 level-set345    is less of equal to `band_radius`).346    """347 348    binary_array = levelset > 0349 350    # Compute the band and the border.351    dist_func = ndi.distance_transform_edt352    distance = np.where(353        binary_array, dist_func(binary_array), -dist_func(~binary_array)354    )355    border = np.zeros_like(levelset, dtype=bool)356    border[:-1, :, :] |= levelset[:-1, :, :] * levelset[1:, :, :] <= 0357    border[:, :-1, :] |= levelset[:, :-1, :] * levelset[:, 1:, :] <= 0358    border[:, :, :-1] |= levelset[:, :, :-1] * levelset[:, :, 1:] <= 0359    band = np.abs(distance) <= band_radius360 361    return distance, border, band362 363 364def signed_distance_function_gpu(levelset: torch.Tensor, band_radius: int):365    binary_array = (levelset > 0).float()366 367    # Compute distance transform368    dist_pos = (369        F.max_pool3d(370            -binary_array.unsqueeze(0).unsqueeze(0), kernel_size=3, stride=1, padding=1371        )372        .squeeze(0)373        .squeeze(0)374        + binary_array375    )376    dist_neg = F.max_pool3d(377        (binary_array - 1).unsqueeze(0).unsqueeze(0), kernel_size=3, stride=1, padding=1378    ).squeeze(0).squeeze(0) + (1 - binary_array)379 380    distance = torch.where(binary_array > 0, dist_pos - 0.5, -dist_neg + 0.5)381 382    # breakpoint()383 384    # Use levelset as distance directly385    # distance = levelset386    # print(distance.shape)387    # Compute border and band388    border = torch.abs(distance) < 1389    band = torch.abs(distance) <= band_radius390 391    return distance, border, band392 393 394def smooth_constrained(395    binary_array: np.ndarray,396    band_radius: int = 4,397    max_iters: int = 250,398    rel_tol: float = 1e-6,399) -> np.ndarray:400    """401    Implementation of the smoothing method from402 403    "Surface Extraction from Binary Volumes with Higher-Order Smoothness"404    Victor Lempitsky, CVPR10405    """406 407    # # Compute the distance map, the border and the band.408    logging.info("Computing distance transform...")409    # distance, _, band = signed_distance_function(binary_array, band_radius)410    binary_array_gpu = torch.from_numpy(binary_array).cuda()411    distance, _, band = signed_distance_function_gpu(binary_array_gpu, band_radius)412    distance = distance.cpu().numpy()413    band = band.cpu().numpy()414 415    variable_indices = _build_variable_indices(band)416 417    # Compute filterq.418    logging.info("Building matrix filterq...")419    if binary_array.ndim == 3:420        filterq = _buildq3d(variable_indices)421        # variable_indices_gpu = torch.from_numpy(variable_indices).cuda()422        # filterq_gpu = _buildq3d_gpu(variable_indices_gpu)423        # filterq = filterq_gpu.cpu().numpy()424    elif binary_array.ndim == 2:425        filterq = _buildq2d(variable_indices)426    else:427        raise ValueError("binary_array.ndim not in [2, 3]")428 429    # Initialize the variables.430    res = np.asarray(distance, dtype=np.double)431    x = res[band]432    upper_bound = np.where(x < 0, x, np.inf)433    lower_bound = np.where(x > 0, x, -np.inf)434 435    upper_bound[np.abs(upper_bound) < 1] = 0436    lower_bound[np.abs(lower_bound) < 1] = 0437 438    # Solve.439    logging.info("Minimizing energy...")440    x = _jacobi(441        filterq=filterq,442        x0=x,443        lower_bound=lower_bound,444        upper_bound=upper_bound,445        max_iters=max_iters,446        rel_tol=rel_tol,447    )448 449    res[band] = x450    return res451 452 453def total_variation_denoising(x, weight=0.1, num_iterations=5, eps=1e-8):454    diff_x = torch.diff(x, dim=0, prepend=x[:1])455    diff_y = torch.diff(x, dim=1, prepend=x[:, :1])456    diff_z = torch.diff(x, dim=2, prepend=x[:, :, :1])457 458    norm = torch.sqrt(diff_x**2 + diff_y**2 + diff_z**2 + eps)459 460    div_x = torch.diff(diff_x / norm, dim=0, append=diff_x[-1:] / norm[-1:])461    div_y = torch.diff(diff_y / norm, dim=1, append=diff_y[:, -1:] / norm[:, -1:])462    div_z = torch.diff(diff_z / norm, dim=2, append=diff_z[:, :, -1:] / norm[:, :, -1:])463 464    return x - weight * (div_x + div_y + div_z)465 466 467def smooth_constrained_gpu(468    binary_array: torch.Tensor,469    band_radius: int = 4,470    max_iters: int = 250,471    rel_tol: float = 1e-4,472):473    distance, _, band = signed_distance_function_gpu(binary_array, band_radius)474 475    # Initialize variables476    x = distance[band]477    upper_bound = torch.where(x < 0, x, torch.tensor(float("inf"), device=x.device))478    lower_bound = torch.where(x > 0, x, torch.tensor(float("-inf"), device=x.device))479 480    upper_bound[torch.abs(upper_bound) < 1] = 0481    lower_bound[torch.abs(lower_bound) < 1] = 0482 483    # Define the 3D Laplacian kernel484    laplacian_kernel = torch.tensor(485        [486            [487                [488                    [[0, 1, 0], [1, -6, 1], [0, 1, 0]],489                    [[1, 0, 1], [0, 0, 0], [1, 0, 1]],490                    [[0, 1, 0], [1, 0, 1], [0, 1, 0]],491                ]492            ]493        ],494        device=x.device,495    ).float()496 497    laplacian_kernel = laplacian_kernel / laplacian_kernel.abs().sum()498 499    breakpoint()500 501    # Simplified Jacobi iteration502    for i in range(max_iters):503        # Reshape x to 5D tensor (batch, channel, depth, height, width)504        x_5d = x.view(1, 1, *band.shape)505        x_3d = x.view(*band.shape)506 507        # Apply 3D convolution508        laplacian = F.conv3d(x_5d, laplacian_kernel, padding=1)509 510        # Reshape back to original dimensions511        laplacian = laplacian.view(x.shape)512 513        # Use a small relaxation factor to improve stability514        relaxation_factor = 0.1515        tv_weight = 0.1516        # x_new = x + relaxation_factor * laplacian517        x_new = total_variation_denoising(x_3d, weight=tv_weight)518        # Print laplacian min and max519        # print(f"Laplacian min: {laplacian.min().item():.4f}, max: {laplacian.max().item():.4f}")520 521        # Apply constraints522        # Reshape x_new to match the dimensions of lower_bound and upper_bound523        x_new = x_new.view(x.shape)524        x_new = torch.clamp(x_new, min=lower_bound, max=upper_bound)525 526        # Check for convergence527        diff_norm = torch.norm(x_new - x)528        print(diff_norm)529        x_norm = torch.norm(x)530 531        if x_norm > 1e-8:  # Avoid division by very small numbers532            relative_change = diff_norm / x_norm533            if relative_change < rel_tol:534                break535        elif diff_norm < rel_tol:  # If x_norm is very small, check absolute change536            break537 538        x = x_new539 540        # Check for NaN and break if found, also check for inf541        if torch.isnan(x).any() or torch.isinf(x).any():542            print(f"NaN or Inf detected at iteration {i}")543            breakpoint()544            break545 546    result = distance.clone()547    result[band] = x548    return result549 550 551def smooth_gaussian(binary_array: np.ndarray, sigma: float = 3) -> np.ndarray:552    vol = np.float_(binary_array) - 0.5553    return ndi.gaussian_filter(vol, sigma=sigma)554 555 556def smooth_gaussian_gpu(binary_array: torch.Tensor, sigma: float = 3):557    # vol = binary_array.float()558    vol = binary_array559    kernel_size = int(2 * sigma + 1)560    kernel = torch.ones(561        1,562        1,563        kernel_size,564        kernel_size,565        kernel_size,566        device=binary_array.device,567        dtype=vol.dtype,568    ) / (kernel_size**3)569    return F.conv3d(570        vol.unsqueeze(0).unsqueeze(0), kernel, padding=kernel_size // 2571    ).squeeze()572 573 574def smooth(binary_array: np.ndarray, method: str = "auto", **kwargs) -> np.ndarray:575    """576    Smooths the 0.5 level-set of a binary array. Returns a floating-point577    array with a smoothed version of the original level-set in the 0 isovalue.578 579    This function can apply two different methods:580 581    - A constrained smoothing method which preserves details and fine582      structures, but it is slow and requires a large amount of memory. This583      method is recommended when the input array is small (smaller than584      (500, 500, 500)).585    - A Gaussian filter applied over the binary array. This method is fast, but586      not very precise, as it can destroy fine details. It is only recommended587      when the input array is large and the 0.5 level-set does not contain588      thin structures.589 590    Parameters591    ----------592    binary_array : ndarray593        Input binary array with the 0.5 level-set to smooth.594    method : str, one of ['auto', 'gaussian', 'constrained']595        Smoothing method. If 'auto' is given, the method will be automatically596        chosen based on the size of `binary_array`.597 598    Parameters for 'gaussian'599    -------------------------600    sigma : float601        Size of the Gaussian filter (default 3).602 603    Parameters for 'constrained'604    ----------------------------605    max_iters : positive integer606        Number of iterations of the constrained optimization method607        (default 250).608    rel_tol: float609        Relative tolerance as a stopping criterion (default 1e-6).610 611    Output612    ------613    res : ndarray614        Floating-point array with a smoothed 0 level-set.615    """616 617    binary_array = np.asarray(binary_array)618 619    if method == "auto":620        if binary_array.size > 512**3:621            method = "gaussian"622        else:623            method = "constrained"624 625    if method == "gaussian":626        return smooth_gaussian(binary_array, **kwargs)627 628    if method == "constrained":629        return smooth_constrained(binary_array, **kwargs)630 631    raise ValueError("Unknown method '{}'".format(method))632 633 634def smooth_gpu(binary_array: torch.Tensor, method: str = "auto", **kwargs):635    if method == "auto":636        method = "gaussian" if binary_array.numel() > 512**3 else "constrained"637 638    if method == "gaussian":639        return smooth_gaussian_gpu(binary_array, **kwargs)640    elif method == "constrained":641        return smooth_constrained_gpu(binary_array, **kwargs)642    else:643        raise ValueError(f"Unknown method '{method}'")644