This commit is contained in:
kijai
2024-11-02 17:10:18 +02:00
parent 2fb84ed8e8
commit 02df2ca14c
64 changed files with 12005 additions and 0 deletions
+425
View File
@@ -0,0 +1,425 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# MoGe
/data
/download
/extract
/view_point_cloud
/view_depth_map
/blobcache
/snapshot
/reference_embeddings
/.msra_intern_s_toolkit
/debug
/workspace
/mlruns
/infer_output
/video_output
/eval_output
/.blobcache
/test_images
/test_videos
/vis
/videos
/raid
/blobmnt
/eval_dump
/pretrained
/.gradio
+3
View File
@@ -0,0 +1,3 @@
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+1
View File
@@ -0,0 +1 @@
from .moge_model import MoGeModel
+6
View File
@@ -0,0 +1,6 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
__version__ = "0.0.1"
+4
View File
@@ -0,0 +1,4 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
+156
View File
@@ -0,0 +1,156 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
from enum import Enum
from typing import Union
import torch
from .utils import _DINOV2_BASE_URL, _make_dinov2_model_name
class Weights(Enum):
LVD142M = "LVD142M"
def _make_dinov2_model(
*,
arch_name: str = "vit_large",
img_size: int = 518,
patch_size: int = 14,
init_values: float = 1.0,
ffn_layer: str = "mlp",
block_chunks: int = 0,
num_register_tokens: int = 0,
interpolate_antialias: bool = False,
interpolate_offset: float = 0.1,
pretrained: bool = True,
weights: Union[Weights, str] = Weights.LVD142M,
**kwargs,
):
from ..models import vision_transformer as vits
if isinstance(weights, str):
try:
weights = Weights[weights]
except KeyError:
raise AssertionError(f"Unsupported weights: {weights}")
model_base_name = _make_dinov2_model_name(arch_name, patch_size)
vit_kwargs = dict(
img_size=img_size,
patch_size=patch_size,
init_values=init_values,
ffn_layer=ffn_layer,
block_chunks=block_chunks,
num_register_tokens=num_register_tokens,
interpolate_antialias=interpolate_antialias,
interpolate_offset=interpolate_offset,
)
vit_kwargs.update(**kwargs)
model = vits.__dict__[arch_name](**vit_kwargs)
if pretrained:
model_full_name = _make_dinov2_model_name(arch_name, patch_size, num_register_tokens)
url = _DINOV2_BASE_URL + f"/{model_base_name}/{model_full_name}_pretrain.pth"
state_dict = torch.hub.load_state_dict_from_url(url, map_location="cpu")
model.load_state_dict(state_dict, strict=True)
return model
def dinov2_vits14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-S/14 model (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(arch_name="vit_small", pretrained=pretrained, weights=weights, **kwargs)
def dinov2_vitb14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-B/14 model (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(arch_name="vit_base", pretrained=pretrained, weights=weights, **kwargs)
def dinov2_vitl14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-L/14 model (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(arch_name="vit_large", pretrained=pretrained, weights=weights, **kwargs)
def dinov2_vitg14(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-g/14 model (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(
arch_name="vit_giant2",
ffn_layer="swiglufused",
weights=weights,
pretrained=pretrained,
**kwargs,
)
def dinov2_vits14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-S/14 model with registers (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(
arch_name="vit_small",
pretrained=pretrained,
weights=weights,
num_register_tokens=4,
interpolate_antialias=True,
interpolate_offset=0.0,
**kwargs,
)
def dinov2_vitb14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-B/14 model with registers (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(
arch_name="vit_base",
pretrained=pretrained,
weights=weights,
num_register_tokens=4,
interpolate_antialias=True,
interpolate_offset=0.0,
**kwargs,
)
def dinov2_vitl14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-L/14 model with registers (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(
arch_name="vit_large",
pretrained=pretrained,
weights=weights,
num_register_tokens=4,
interpolate_antialias=True,
interpolate_offset=0.0,
**kwargs,
)
def dinov2_vitg14_reg(*, pretrained: bool = True, weights: Union[Weights, str] = Weights.LVD142M, **kwargs):
"""
DINOv2 ViT-g/14 model with registers (optionally) pretrained on the LVD-142M dataset.
"""
return _make_dinov2_model(
arch_name="vit_giant2",
ffn_layer="swiglufused",
weights=weights,
pretrained=pretrained,
num_register_tokens=4,
interpolate_antialias=True,
interpolate_offset=0.0,
**kwargs,
)
+39
View File
@@ -0,0 +1,39 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import itertools
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
_DINOV2_BASE_URL = "https://dl.fbaipublicfiles.com/dinov2"
def _make_dinov2_model_name(arch_name: str, patch_size: int, num_register_tokens: int = 0) -> str:
compact_arch_name = arch_name.replace("_", "")[:4]
registers_suffix = f"_reg{num_register_tokens}" if num_register_tokens else ""
return f"dinov2_{compact_arch_name}{patch_size}{registers_suffix}"
class CenterPadding(nn.Module):
def __init__(self, multiple):
super().__init__()
self.multiple = multiple
def _get_pad(self, size):
new_size = math.ceil(size / self.multiple) * self.multiple
pad_size = new_size - size
pad_size_left = pad_size // 2
pad_size_right = pad_size - pad_size_left
return pad_size_left, pad_size_right
@torch.inference_mode()
def forward(self, x):
pads = list(itertools.chain.from_iterable(self._get_pad(m) for m in x.shape[:1:-1]))
output = F.pad(x, pads)
return output
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
from .dino_head import DINOHead
from .mlp import Mlp
from .patch_embed import PatchEmbed
from .swiglu_ffn import SwiGLUFFN, SwiGLUFFNFused
from .block import NestedTensorBlock
from .attention import MemEffAttention
+89
View File
@@ -0,0 +1,89 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py
import logging
import os
import warnings
from torch import Tensor
from torch import nn
logger = logging.getLogger("dinov2")
XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
try:
if XFORMERS_ENABLED:
from xformers.ops import memory_efficient_attention, unbind
XFORMERS_AVAILABLE = True
# warnings.warn("xFormers is available (Attention)")
else:
# warnings.warn("xFormers is disabled (Attention)")
raise ImportError
except ImportError:
XFORMERS_AVAILABLE = False
# warnings.warn("xFormers is not available (Attention)")
class Attention(nn.Module):
def __init__(
self,
dim: int,
num_heads: int = 8,
qkv_bias: bool = False,
proj_bias: bool = True,
attn_drop: float = 0.0,
proj_drop: float = 0.0,
) -> None:
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.scale = head_dim**-0.5
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
self.attn_drop = nn.Dropout(attn_drop)
self.proj = nn.Linear(dim, dim, bias=proj_bias)
self.proj_drop = nn.Dropout(proj_drop)
def forward(self, x: Tensor, attn_bias=None) -> Tensor:
B, N, C = x.shape
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0] * self.scale, qkv[1], qkv[2]
attn = q @ k.transpose(-2, -1)
attn = attn.softmax(dim=-1)
attn = self.attn_drop(attn)
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
x = self.proj(x)
x = self.proj_drop(x)
return x
class MemEffAttention(Attention):
def forward(self, x: Tensor, attn_bias=None) -> Tensor:
if not XFORMERS_AVAILABLE:
if attn_bias is not None:
raise AssertionError("xFormers is required for using nested tensors")
return super().forward(x)
B, N, C = x.shape
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads)
q, k, v = unbind(qkv, 2)
x = memory_efficient_attention(q, k, v, attn_bias=attn_bias)
x = x.reshape([B, N, C])
x = self.proj(x)
x = self.proj_drop(x)
return x
+259
View File
@@ -0,0 +1,259 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py
import logging
import os
from typing import Callable, List, Any, Tuple, Dict
import warnings
import torch
from torch import nn, Tensor
from .attention import Attention, MemEffAttention
from .drop_path import DropPath
from .layer_scale import LayerScale
from .mlp import Mlp
logger = logging.getLogger("dinov2")
XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
try:
if XFORMERS_ENABLED:
from xformers.ops import fmha, scaled_index_add, index_select_cat
XFORMERS_AVAILABLE = True
# warnings.warn("xFormers is available (Block)")
else:
# warnings.warn("xFormers is disabled (Block)")
raise ImportError
except ImportError:
XFORMERS_AVAILABLE = False
# warnings.warn("xFormers is not available (Block)")
class Block(nn.Module):
def __init__(
self,
dim: int,
num_heads: int,
mlp_ratio: float = 4.0,
qkv_bias: bool = False,
proj_bias: bool = True,
ffn_bias: bool = True,
drop: float = 0.0,
attn_drop: float = 0.0,
init_values=None,
drop_path: float = 0.0,
act_layer: Callable[..., nn.Module] = nn.GELU,
norm_layer: Callable[..., nn.Module] = nn.LayerNorm,
attn_class: Callable[..., nn.Module] = Attention,
ffn_layer: Callable[..., nn.Module] = Mlp,
) -> None:
super().__init__()
# print(f"biases: qkv: {qkv_bias}, proj: {proj_bias}, ffn: {ffn_bias}")
self.norm1 = norm_layer(dim)
self.attn = attn_class(
dim,
num_heads=num_heads,
qkv_bias=qkv_bias,
proj_bias=proj_bias,
attn_drop=attn_drop,
proj_drop=drop,
)
self.ls1 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
self.drop_path1 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
self.norm2 = norm_layer(dim)
mlp_hidden_dim = int(dim * mlp_ratio)
self.mlp = ffn_layer(
in_features=dim,
hidden_features=mlp_hidden_dim,
act_layer=act_layer,
drop=drop,
bias=ffn_bias,
)
self.ls2 = LayerScale(dim, init_values=init_values) if init_values else nn.Identity()
self.drop_path2 = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
self.sample_drop_ratio = drop_path
def forward(self, x: Tensor) -> Tensor:
def attn_residual_func(x: Tensor) -> Tensor:
return self.ls1(self.attn(self.norm1(x)))
def ffn_residual_func(x: Tensor) -> Tensor:
return self.ls2(self.mlp(self.norm2(x)))
if self.training and self.sample_drop_ratio > 0.1:
# the overhead is compensated only for a drop path rate larger than 0.1
x = drop_add_residual_stochastic_depth(
x,
residual_func=attn_residual_func,
sample_drop_ratio=self.sample_drop_ratio,
)
x = drop_add_residual_stochastic_depth(
x,
residual_func=ffn_residual_func,
sample_drop_ratio=self.sample_drop_ratio,
)
elif self.training and self.sample_drop_ratio > 0.0:
x = x + self.drop_path1(attn_residual_func(x))
x = x + self.drop_path1(ffn_residual_func(x)) # FIXME: drop_path2
else:
x = x + attn_residual_func(x)
x = x + ffn_residual_func(x)
return x
def drop_add_residual_stochastic_depth(
x: Tensor,
residual_func: Callable[[Tensor], Tensor],
sample_drop_ratio: float = 0.0,
) -> Tensor:
# 1) extract subset using permutation
b, n, d = x.shape
sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
x_subset = x[brange]
# 2) apply residual_func to get residual
residual = residual_func(x_subset)
x_flat = x.flatten(1)
residual = residual.flatten(1)
residual_scale_factor = b / sample_subset_size
# 3) add the residual
x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor)
return x_plus_residual.view_as(x)
def get_branges_scales(x, sample_drop_ratio=0.0):
b, n, d = x.shape
sample_subset_size = max(int(b * (1 - sample_drop_ratio)), 1)
brange = (torch.randperm(b, device=x.device))[:sample_subset_size]
residual_scale_factor = b / sample_subset_size
return brange, residual_scale_factor
def add_residual(x, brange, residual, residual_scale_factor, scaling_vector=None):
if scaling_vector is None:
x_flat = x.flatten(1)
residual = residual.flatten(1)
x_plus_residual = torch.index_add(x_flat, 0, brange, residual.to(dtype=x.dtype), alpha=residual_scale_factor)
else:
x_plus_residual = scaled_index_add(
x, brange, residual.to(dtype=x.dtype), scaling=scaling_vector, alpha=residual_scale_factor
)
return x_plus_residual
attn_bias_cache: Dict[Tuple, Any] = {}
def get_attn_bias_and_cat(x_list, branges=None):
"""
this will perform the index select, cat the tensors, and provide the attn_bias from cache
"""
batch_sizes = [b.shape[0] for b in branges] if branges is not None else [x.shape[0] for x in x_list]
all_shapes = tuple((b, x.shape[1]) for b, x in zip(batch_sizes, x_list))
if all_shapes not in attn_bias_cache.keys():
seqlens = []
for b, x in zip(batch_sizes, x_list):
for _ in range(b):
seqlens.append(x.shape[1])
attn_bias = fmha.BlockDiagonalMask.from_seqlens(seqlens)
attn_bias._batch_sizes = batch_sizes
attn_bias_cache[all_shapes] = attn_bias
if branges is not None:
cat_tensors = index_select_cat([x.flatten(1) for x in x_list], branges).view(1, -1, x_list[0].shape[-1])
else:
tensors_bs1 = tuple(x.reshape([1, -1, *x.shape[2:]]) for x in x_list)
cat_tensors = torch.cat(tensors_bs1, dim=1)
return attn_bias_cache[all_shapes], cat_tensors
def drop_add_residual_stochastic_depth_list(
x_list: List[Tensor],
residual_func: Callable[[Tensor, Any], Tensor],
sample_drop_ratio: float = 0.0,
scaling_vector=None,
) -> Tensor:
# 1) generate random set of indices for dropping samples in the batch
branges_scales = [get_branges_scales(x, sample_drop_ratio=sample_drop_ratio) for x in x_list]
branges = [s[0] for s in branges_scales]
residual_scale_factors = [s[1] for s in branges_scales]
# 2) get attention bias and index+concat the tensors
attn_bias, x_cat = get_attn_bias_and_cat(x_list, branges)
# 3) apply residual_func to get residual, and split the result
residual_list = attn_bias.split(residual_func(x_cat, attn_bias=attn_bias)) # type: ignore
outputs = []
for x, brange, residual, residual_scale_factor in zip(x_list, branges, residual_list, residual_scale_factors):
outputs.append(add_residual(x, brange, residual, residual_scale_factor, scaling_vector).view_as(x))
return outputs
class NestedTensorBlock(Block):
def forward_nested(self, x_list: List[Tensor]) -> List[Tensor]:
"""
x_list contains a list of tensors to nest together and run
"""
assert isinstance(self.attn, MemEffAttention)
if self.training and self.sample_drop_ratio > 0.0:
def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
return self.attn(self.norm1(x), attn_bias=attn_bias)
def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
return self.mlp(self.norm2(x))
x_list = drop_add_residual_stochastic_depth_list(
x_list,
residual_func=attn_residual_func,
sample_drop_ratio=self.sample_drop_ratio,
scaling_vector=self.ls1.gamma if isinstance(self.ls1, LayerScale) else None,
)
x_list = drop_add_residual_stochastic_depth_list(
x_list,
residual_func=ffn_residual_func,
sample_drop_ratio=self.sample_drop_ratio,
scaling_vector=self.ls2.gamma if isinstance(self.ls1, LayerScale) else None,
)
return x_list
else:
def attn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
return self.ls1(self.attn(self.norm1(x), attn_bias=attn_bias))
def ffn_residual_func(x: Tensor, attn_bias=None) -> Tensor:
return self.ls2(self.mlp(self.norm2(x)))
attn_bias, x = get_attn_bias_and_cat(x_list)
x = x + attn_residual_func(x, attn_bias=attn_bias)
x = x + ffn_residual_func(x)
return attn_bias.split(x)
def forward(self, x_or_x_list):
if isinstance(x_or_x_list, Tensor):
return super().forward(x_or_x_list)
elif isinstance(x_or_x_list, list):
if not XFORMERS_AVAILABLE:
raise AssertionError("xFormers is required for using nested tensors")
return self.forward_nested(x_or_x_list)
else:
raise AssertionError
+58
View File
@@ -0,0 +1,58 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import torch
import torch.nn as nn
from torch.nn.init import trunc_normal_
from torch.nn.utils import weight_norm
class DINOHead(nn.Module):
def __init__(
self,
in_dim,
out_dim,
use_bn=False,
nlayers=3,
hidden_dim=2048,
bottleneck_dim=256,
mlp_bias=True,
):
super().__init__()
nlayers = max(nlayers, 1)
self.mlp = _build_mlp(nlayers, in_dim, bottleneck_dim, hidden_dim=hidden_dim, use_bn=use_bn, bias=mlp_bias)
self.apply(self._init_weights)
self.last_layer = weight_norm(nn.Linear(bottleneck_dim, out_dim, bias=False))
self.last_layer.weight_g.data.fill_(1)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
trunc_normal_(m.weight, std=0.02)
if isinstance(m, nn.Linear) and m.bias is not None:
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.mlp(x)
eps = 1e-6 if x.dtype == torch.float16 else 1e-12
x = nn.functional.normalize(x, dim=-1, p=2, eps=eps)
x = self.last_layer(x)
return x
def _build_mlp(nlayers, in_dim, bottleneck_dim, hidden_dim=None, use_bn=False, bias=True):
if nlayers == 1:
return nn.Linear(in_dim, bottleneck_dim, bias=bias)
else:
layers = [nn.Linear(in_dim, hidden_dim, bias=bias)]
if use_bn:
layers.append(nn.BatchNorm1d(hidden_dim))
layers.append(nn.GELU())
for _ in range(nlayers - 2):
layers.append(nn.Linear(hidden_dim, hidden_dim, bias=bias))
if use_bn:
layers.append(nn.BatchNorm1d(hidden_dim))
layers.append(nn.GELU())
layers.append(nn.Linear(hidden_dim, bottleneck_dim, bias=bias))
return nn.Sequential(*layers)
+34
View File
@@ -0,0 +1,34 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/drop.py
from torch import nn
def drop_path(x, drop_prob: float = 0.0, training: bool = False):
if drop_prob == 0.0 or not training:
return x
keep_prob = 1 - drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
if keep_prob > 0.0:
random_tensor.div_(keep_prob)
output = x * random_tensor
return output
class DropPath(nn.Module):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks)."""
def __init__(self, drop_prob=None):
super(DropPath, self).__init__()
self.drop_prob = drop_prob
def forward(self, x):
return drop_path(x, self.drop_prob, self.training)
+27
View File
@@ -0,0 +1,27 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# Modified from: https://github.com/huggingface/pytorch-image-models/blob/main/timm/models/vision_transformer.py#L103-L110
from typing import Union
import torch
from torch import Tensor
from torch import nn
class LayerScale(nn.Module):
def __init__(
self,
dim: int,
init_values: Union[float, Tensor] = 1e-5,
inplace: bool = False,
) -> None:
super().__init__()
self.inplace = inplace
self.gamma = nn.Parameter(init_values * torch.ones(dim))
def forward(self, x: Tensor) -> Tensor:
return x.mul_(self.gamma) if self.inplace else x * self.gamma
+40
View File
@@ -0,0 +1,40 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/mlp.py
from typing import Callable, Optional
from torch import Tensor, nn
class Mlp(nn.Module):
def __init__(
self,
in_features: int,
hidden_features: Optional[int] = None,
out_features: Optional[int] = None,
act_layer: Callable[..., nn.Module] = nn.GELU,
drop: float = 0.0,
bias: bool = True,
) -> None:
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.fc1 = nn.Linear(in_features, hidden_features, bias=bias)
self.act = act_layer()
self.fc2 = nn.Linear(hidden_features, out_features, bias=bias)
self.drop = nn.Dropout(drop)
def forward(self, x: Tensor) -> Tensor:
x = self.fc1(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
+88
View File
@@ -0,0 +1,88 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/master/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/layers/patch_embed.py
from typing import Callable, Optional, Tuple, Union
from torch import Tensor
import torch.nn as nn
def make_2tuple(x):
if isinstance(x, tuple):
assert len(x) == 2
return x
assert isinstance(x, int)
return (x, x)
class PatchEmbed(nn.Module):
"""
2D image to patch embedding: (B,C,H,W) -> (B,N,D)
Args:
img_size: Image size.
patch_size: Patch token size.
in_chans: Number of input image channels.
embed_dim: Number of linear projection output channels.
norm_layer: Normalization layer.
"""
def __init__(
self,
img_size: Union[int, Tuple[int, int]] = 224,
patch_size: Union[int, Tuple[int, int]] = 16,
in_chans: int = 3,
embed_dim: int = 768,
norm_layer: Optional[Callable] = None,
flatten_embedding: bool = True,
) -> None:
super().__init__()
image_HW = make_2tuple(img_size)
patch_HW = make_2tuple(patch_size)
patch_grid_size = (
image_HW[0] // patch_HW[0],
image_HW[1] // patch_HW[1],
)
self.img_size = image_HW
self.patch_size = patch_HW
self.patches_resolution = patch_grid_size
self.num_patches = patch_grid_size[0] * patch_grid_size[1]
self.in_chans = in_chans
self.embed_dim = embed_dim
self.flatten_embedding = flatten_embedding
self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_HW, stride=patch_HW)
self.norm = norm_layer(embed_dim) if norm_layer else nn.Identity()
def forward(self, x: Tensor) -> Tensor:
_, _, H, W = x.shape
patch_H, patch_W = self.patch_size
assert H % patch_H == 0, f"Input image height {H} is not a multiple of patch height {patch_H}"
assert W % patch_W == 0, f"Input image width {W} is not a multiple of patch width: {patch_W}"
x = self.proj(x) # B C H W
H, W = x.size(2), x.size(3)
x = x.flatten(2).transpose(1, 2) # B HW C
x = self.norm(x)
if not self.flatten_embedding:
x = x.reshape(-1, H, W, self.embed_dim) # B H W C
return x
def flops(self) -> float:
Ho, Wo = self.patches_resolution
flops = Ho * Wo * self.embed_dim * self.in_chans * (self.patch_size[0] * self.patch_size[1])
if self.norm is not None:
flops += Ho * Wo * self.embed_dim
return flops
+72
View File
@@ -0,0 +1,72 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import os
from typing import Callable, Optional
import warnings
from torch import Tensor, nn
import torch.nn.functional as F
class SwiGLUFFN(nn.Module):
def __init__(
self,
in_features: int,
hidden_features: Optional[int] = None,
out_features: Optional[int] = None,
act_layer: Callable[..., nn.Module] = None,
drop: float = 0.0,
bias: bool = True,
) -> None:
super().__init__()
out_features = out_features or in_features
hidden_features = hidden_features or in_features
self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias)
self.w3 = nn.Linear(hidden_features, out_features, bias=bias)
def forward(self, x: Tensor) -> Tensor:
x12 = self.w12(x)
x1, x2 = x12.chunk(2, dim=-1)
hidden = F.silu(x1) * x2
return self.w3(hidden)
XFORMERS_ENABLED = os.environ.get("XFORMERS_DISABLED") is None
try:
if XFORMERS_ENABLED:
from xformers.ops import SwiGLU
XFORMERS_AVAILABLE = True
# warnings.warn("xFormers is available (SwiGLU)")
else:
# warnings.warn("xFormers is disabled (SwiGLU)")
raise ImportError
except ImportError:
SwiGLU = SwiGLUFFN
XFORMERS_AVAILABLE = False
# warnings.warn("xFormers is not available (SwiGLU)")
class SwiGLUFFNFused(SwiGLU):
def __init__(
self,
in_features: int,
hidden_features: Optional[int] = None,
out_features: Optional[int] = None,
act_layer: Callable[..., nn.Module] = None,
drop: float = 0.0,
bias: bool = True,
) -> None:
out_features = out_features or in_features
hidden_features = hidden_features or in_features
hidden_features = (int(hidden_features * 2 / 3) + 7) // 8 * 8
super().__init__(
in_features=in_features,
hidden_features=hidden_features,
out_features=out_features,
bias=bias,
)
+43
View File
@@ -0,0 +1,43 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import logging
from . import vision_transformer as vits
logger = logging.getLogger("dinov2")
def build_model(args, only_teacher=False, img_size=224):
args.arch = args.arch.removesuffix("_memeff")
if "vit" in args.arch:
vit_kwargs = dict(
img_size=img_size,
patch_size=args.patch_size,
init_values=args.layerscale,
ffn_layer=args.ffn_layer,
block_chunks=args.block_chunks,
qkv_bias=args.qkv_bias,
proj_bias=args.proj_bias,
ffn_bias=args.ffn_bias,
num_register_tokens=args.num_register_tokens,
interpolate_offset=args.interpolate_offset,
interpolate_antialias=args.interpolate_antialias,
)
teacher = vits.__dict__[args.arch](**vit_kwargs)
if only_teacher:
return teacher, teacher.embed_dim
student = vits.__dict__[args.arch](
**vit_kwargs,
drop_path_rate=args.drop_path_rate,
drop_path_uniform=args.drop_path_uniform,
)
embed_dim = student.embed_dim
return student, teacher, embed_dim
def build_model_from_cfg(cfg, only_teacher=False):
return build_model(cfg.student, only_teacher=only_teacher, img_size=cfg.crops.global_crops_size)
@@ -0,0 +1,396 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
# References:
# https://github.com/facebookresearch/dino/blob/main/vision_transformer.py
# https://github.com/rwightman/pytorch-image-models/tree/master/timm/models/vision_transformer.py
from functools import partial
import math
import logging
from typing import Sequence, Tuple, Union, Callable
import torch
import torch.nn as nn
import torch.utils.checkpoint
from torch.nn.init import trunc_normal_
from ..layers import Mlp, PatchEmbed, SwiGLUFFNFused, MemEffAttention, NestedTensorBlock as Block
logger = logging.getLogger("dinov2")
def named_apply(fn: Callable, module: nn.Module, name="", depth_first=True, include_root=False) -> nn.Module:
if not depth_first and include_root:
fn(module=module, name=name)
for child_name, child_module in module.named_children():
child_name = ".".join((name, child_name)) if name else child_name
named_apply(fn=fn, module=child_module, name=child_name, depth_first=depth_first, include_root=True)
if depth_first and include_root:
fn(module=module, name=name)
return module
class BlockChunk(nn.ModuleList):
def forward(self, x):
for b in self:
x = b(x)
return x
class DinoVisionTransformer(nn.Module):
def __init__(
self,
img_size=224,
patch_size=16,
in_chans=3,
embed_dim=768,
depth=12,
num_heads=12,
mlp_ratio=4.0,
qkv_bias=True,
ffn_bias=True,
proj_bias=True,
drop_path_rate=0.0,
drop_path_uniform=False,
init_values=None, # for layerscale: None or 0 => no layerscale
embed_layer=PatchEmbed,
act_layer=nn.GELU,
block_fn=Block,
ffn_layer="mlp",
block_chunks=1,
num_register_tokens=0,
interpolate_antialias=False,
interpolate_offset=0.1,
):
"""
Args:
img_size (int, tuple): input image size
patch_size (int, tuple): patch size
in_chans (int): number of input channels
embed_dim (int): embedding dimension
depth (int): depth of transformer
num_heads (int): number of attention heads
mlp_ratio (int): ratio of mlp hidden dim to embedding dim
qkv_bias (bool): enable bias for qkv if True
proj_bias (bool): enable bias for proj in attn if True
ffn_bias (bool): enable bias for ffn if True
drop_path_rate (float): stochastic depth rate
drop_path_uniform (bool): apply uniform drop rate across blocks
weight_init (str): weight init scheme
init_values (float): layer-scale init values
embed_layer (nn.Module): patch embedding layer
act_layer (nn.Module): MLP activation layer
block_fn (nn.Module): transformer block class
ffn_layer (str): "mlp", "swiglu", "swiglufused" or "identity"
block_chunks: (int) split block sequence into block_chunks units for FSDP wrap
num_register_tokens: (int) number of extra cls tokens (so-called "registers")
interpolate_antialias: (str) flag to apply anti-aliasing when interpolating positional embeddings
interpolate_offset: (float) work-around offset to apply when interpolating positional embeddings
"""
super().__init__()
norm_layer = partial(nn.LayerNorm, eps=1e-6)
self.num_features = self.embed_dim = embed_dim # num_features for consistency with other models
self.num_tokens = 1
self.n_blocks = depth
self.num_heads = num_heads
self.patch_size = patch_size
self.num_register_tokens = num_register_tokens
self.interpolate_antialias = interpolate_antialias
self.interpolate_offset = interpolate_offset
self.patch_embed = embed_layer(img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim)
num_patches = self.patch_embed.num_patches
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + self.num_tokens, embed_dim))
assert num_register_tokens >= 0
self.register_tokens = (
nn.Parameter(torch.zeros(1, num_register_tokens, embed_dim)) if num_register_tokens else None
)
if drop_path_uniform is True:
dpr = [drop_path_rate] * depth
else:
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)] # stochastic depth decay rule
if ffn_layer == "mlp":
logger.info("using MLP layer as FFN")
ffn_layer = Mlp
elif ffn_layer == "swiglufused" or ffn_layer == "swiglu":
logger.info("using SwiGLU layer as FFN")
ffn_layer = SwiGLUFFNFused
elif ffn_layer == "identity":
logger.info("using Identity layer as FFN")
def f(*args, **kwargs):
return nn.Identity()
ffn_layer = f
else:
raise NotImplementedError
blocks_list = [
block_fn(
dim=embed_dim,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
proj_bias=proj_bias,
ffn_bias=ffn_bias,
drop_path=dpr[i],
norm_layer=norm_layer,
act_layer=act_layer,
ffn_layer=ffn_layer,
init_values=init_values,
)
for i in range(depth)
]
if block_chunks > 0:
self.chunked_blocks = True
chunked_blocks = []
chunksize = depth // block_chunks
for i in range(0, depth, chunksize):
# this is to keep the block index consistent if we chunk the block list
chunked_blocks.append([nn.Identity()] * i + blocks_list[i : i + chunksize])
self.blocks = nn.ModuleList([BlockChunk(p) for p in chunked_blocks])
else:
self.chunked_blocks = False
self.blocks = nn.ModuleList(blocks_list)
self.norm = norm_layer(embed_dim)
self.head = nn.Identity()
self.mask_token = nn.Parameter(torch.zeros(1, embed_dim))
self.init_weights()
def init_weights(self):
trunc_normal_(self.pos_embed, std=0.02)
nn.init.normal_(self.cls_token, std=1e-6)
if self.register_tokens is not None:
nn.init.normal_(self.register_tokens, std=1e-6)
named_apply(init_weights_vit_timm, self)
def interpolate_pos_encoding(self, x, w, h):
previous_dtype = x.dtype
npatch = x.shape[1] - 1
N = self.pos_embed.shape[1] - 1
if npatch == N and w == h:
return self.pos_embed
pos_embed = self.pos_embed.float()
class_pos_embed = pos_embed[:, 0]
patch_pos_embed = pos_embed[:, 1:]
dim = x.shape[-1]
w0 = w // self.patch_size
h0 = h // self.patch_size
M = int(math.sqrt(N)) # Recover the number of patches in each dimension
assert N == M * M
kwargs = {}
if self.interpolate_offset:
# Historical kludge: add a small number to avoid floating point error in the interpolation, see https://github.com/facebookresearch/dino/issues/8
# Note: still needed for backward-compatibility, the underlying operators are using both output size and scale factors
sx = float(w0 + self.interpolate_offset) / M
sy = float(h0 + self.interpolate_offset) / M
kwargs["scale_factor"] = (sx, sy)
else:
# Simply specify an output size instead of a scale factor
kwargs["size"] = (w0, h0)
patch_pos_embed = nn.functional.interpolate(
patch_pos_embed.reshape(1, M, M, dim).permute(0, 3, 1, 2),
mode="bicubic",
antialias=self.interpolate_antialias,
**kwargs,
)
assert (w0, h0) == patch_pos_embed.shape[-2:]
patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1).to(previous_dtype)
def prepare_tokens_with_masks(self, x, masks=None):
B, nc, w, h = x.shape
x = self.patch_embed(x)
if masks is not None:
x = torch.where(masks.unsqueeze(-1), self.mask_token.to(x.dtype).unsqueeze(0), x)
x = torch.cat((self.cls_token.expand(x.shape[0], -1, -1), x), dim=1)
x = x + self.interpolate_pos_encoding(x, w, h)
if self.register_tokens is not None:
x = torch.cat(
(
x[:, :1],
self.register_tokens.expand(x.shape[0], -1, -1),
x[:, 1:],
),
dim=1,
)
return x
def forward_features_list(self, x_list, masks_list):
x = [self.prepare_tokens_with_masks(x, masks) for x, masks in zip(x_list, masks_list)]
for blk in self.blocks:
x = blk(x)
all_x = x
output = []
for x, masks in zip(all_x, masks_list):
x_norm = self.norm(x)
output.append(
{
"x_norm_clstoken": x_norm[:, 0],
"x_norm_regtokens": x_norm[:, 1 : self.num_register_tokens + 1],
"x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :],
"x_prenorm": x,
"masks": masks,
}
)
return output
def forward_features(self, x, masks=None):
if isinstance(x, list):
return self.forward_features_list(x, masks)
x = self.prepare_tokens_with_masks(x, masks)
for blk in self.blocks:
x = blk(x)
x_norm = self.norm(x)
return {
"x_norm_clstoken": x_norm[:, 0],
"x_norm_regtokens": x_norm[:, 1 : self.num_register_tokens + 1],
"x_norm_patchtokens": x_norm[:, self.num_register_tokens + 1 :],
"x_prenorm": x,
"masks": masks,
}
def _get_intermediate_layers_not_chunked(self, x, n=1):
x = self.prepare_tokens_with_masks(x)
# If n is an int, take the n last blocks. If it's a list, take them
output, total_block_len = [], len(self.blocks)
blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n
for i, blk in enumerate(self.blocks):
x = blk(x)
if i in blocks_to_take:
output.append(x)
assert len(output) == len(blocks_to_take), f"only {len(output)} / {len(blocks_to_take)} blocks found"
return output
def _get_intermediate_layers_chunked(self, x, n=1):
x = self.prepare_tokens_with_masks(x)
output, i, total_block_len = [], 0, len(self.blocks[-1])
# If n is an int, take the n last blocks. If it's a list, take them
blocks_to_take = range(total_block_len - n, total_block_len) if isinstance(n, int) else n
for block_chunk in self.blocks:
for blk in block_chunk[i:]: # Passing the nn.Identity()
x = blk(x)
if i in blocks_to_take:
output.append(x)
i += 1
assert len(output) == len(blocks_to_take), f"only {len(output)} / {len(blocks_to_take)} blocks found"
return output
def get_intermediate_layers(
self,
x: torch.Tensor,
n: Union[int, Sequence] = 1, # Layers or n last layers to take
reshape: bool = False,
return_class_token: bool = False,
norm=True,
) -> Tuple[Union[torch.Tensor, Tuple[torch.Tensor]]]:
if self.chunked_blocks:
outputs = self._get_intermediate_layers_chunked(x, n)
else:
outputs = self._get_intermediate_layers_not_chunked(x, n)
if norm:
outputs = [self.norm(out) for out in outputs]
class_tokens = [out[:, 0] for out in outputs]
outputs = [out[:, 1 + self.num_register_tokens :] for out in outputs]
if reshape:
B, _, w, h = x.shape
outputs = [
out.reshape(B, w // self.patch_size, h // self.patch_size, -1).permute(0, 3, 1, 2).contiguous()
for out in outputs
]
if return_class_token:
return tuple(zip(outputs, class_tokens))
return tuple(outputs)
def forward(self, *args, is_training=False, **kwargs):
ret = self.forward_features(*args, **kwargs)
if is_training:
return ret
else:
return self.head(ret["x_norm_clstoken"])
def init_weights_vit_timm(module: nn.Module, name: str = ""):
"""ViT weight initialization, original timm impl (for reproducibility)"""
if isinstance(module, nn.Linear):
trunc_normal_(module.weight, std=0.02)
if module.bias is not None:
nn.init.zeros_(module.bias)
def vit_small(patch_size=16, num_register_tokens=0, **kwargs):
model = DinoVisionTransformer(
patch_size=patch_size,
embed_dim=384,
depth=12,
num_heads=6,
mlp_ratio=4,
block_fn=partial(Block, attn_class=MemEffAttention),
num_register_tokens=num_register_tokens,
**kwargs,
)
return model
def vit_base(patch_size=16, num_register_tokens=0, **kwargs):
model = DinoVisionTransformer(
patch_size=patch_size,
embed_dim=768,
depth=12,
num_heads=12,
mlp_ratio=4,
block_fn=partial(Block, attn_class=MemEffAttention),
num_register_tokens=num_register_tokens,
**kwargs,
)
return model
def vit_large(patch_size=16, num_register_tokens=0, **kwargs):
model = DinoVisionTransformer(
patch_size=patch_size,
embed_dim=1024,
depth=24,
num_heads=16,
mlp_ratio=4,
block_fn=partial(Block, attn_class=MemEffAttention),
num_register_tokens=num_register_tokens,
**kwargs,
)
return model
def vit_giant2(patch_size=16, num_register_tokens=0, **kwargs):
"""
Close to ViT-giant, with embed-dim 1536 and 24 heads => embed-dim per head 64
"""
model = DinoVisionTransformer(
patch_size=patch_size,
embed_dim=1536,
depth=40,
num_heads=24,
mlp_ratio=4,
block_fn=partial(Block, attn_class=MemEffAttention),
num_register_tokens=num_register_tokens,
**kwargs,
)
return model
+4
View File
@@ -0,0 +1,4 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
+95
View File
@@ -0,0 +1,95 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
from enum import Enum
import os
from pathlib import Path
from typing import Any, Dict, Optional
class ClusterType(Enum):
AWS = "aws"
FAIR = "fair"
RSC = "rsc"
def _guess_cluster_type() -> ClusterType:
uname = os.uname()
if uname.sysname == "Linux":
if uname.release.endswith("-aws"):
# Linux kernel versions on AWS instances are of the form "5.4.0-1051-aws"
return ClusterType.AWS
elif uname.nodename.startswith("rsc"):
# Linux kernel versions on RSC instances are standard ones but hostnames start with "rsc"
return ClusterType.RSC
return ClusterType.FAIR
def get_cluster_type(cluster_type: Optional[ClusterType] = None) -> Optional[ClusterType]:
if cluster_type is None:
return _guess_cluster_type()
return cluster_type
def get_checkpoint_path(cluster_type: Optional[ClusterType] = None) -> Optional[Path]:
cluster_type = get_cluster_type(cluster_type)
if cluster_type is None:
return None
CHECKPOINT_DIRNAMES = {
ClusterType.AWS: "checkpoints",
ClusterType.FAIR: "checkpoint",
ClusterType.RSC: "checkpoint/dino",
}
return Path("/") / CHECKPOINT_DIRNAMES[cluster_type]
def get_user_checkpoint_path(cluster_type: Optional[ClusterType] = None) -> Optional[Path]:
checkpoint_path = get_checkpoint_path(cluster_type)
if checkpoint_path is None:
return None
username = os.environ.get("USER")
assert username is not None
return checkpoint_path / username
def get_slurm_partition(cluster_type: Optional[ClusterType] = None) -> Optional[str]:
cluster_type = get_cluster_type(cluster_type)
if cluster_type is None:
return None
SLURM_PARTITIONS = {
ClusterType.AWS: "learnlab",
ClusterType.FAIR: "learnlab",
ClusterType.RSC: "learn",
}
return SLURM_PARTITIONS[cluster_type]
def get_slurm_executor_parameters(
nodes: int, num_gpus_per_node: int, cluster_type: Optional[ClusterType] = None, **kwargs
) -> Dict[str, Any]:
# create default parameters
params = {
"mem_gb": 0, # Requests all memory on a node, see https://slurm.schedmd.com/sbatch.html
"gpus_per_node": num_gpus_per_node,
"tasks_per_node": num_gpus_per_node, # one task per GPU
"cpus_per_task": 10,
"nodes": nodes,
"slurm_partition": get_slurm_partition(cluster_type),
}
# apply cluster-specific adjustments
cluster_type = get_cluster_type(cluster_type)
if cluster_type == ClusterType.AWS:
params["cpus_per_task"] = 12
del params["mem_gb"]
elif cluster_type == ClusterType.RSC:
params["cpus_per_task"] = 12
# set additional parameters / apply overrides
params.update(kwargs)
return params
+72
View File
@@ -0,0 +1,72 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import math
import logging
import os
from omegaconf import OmegaConf
import dinov2.distributed as distributed
from dinov2.logging import setup_logging
from dinov2.utils import utils
from dinov2.configs import dinov2_default_config
logger = logging.getLogger("dinov2")
def apply_scaling_rules_to_cfg(cfg): # to fix
if cfg.optim.scaling_rule == "sqrt_wrt_1024":
base_lr = cfg.optim.base_lr
cfg.optim.lr = base_lr
cfg.optim.lr *= math.sqrt(cfg.train.batch_size_per_gpu * distributed.get_global_size() / 1024.0)
logger.info(f"sqrt scaling learning rate; base: {base_lr}, new: {cfg.optim.lr}")
else:
raise NotImplementedError
return cfg
def write_config(cfg, output_dir, name="config.yaml"):
logger.info(OmegaConf.to_yaml(cfg))
saved_cfg_path = os.path.join(output_dir, name)
with open(saved_cfg_path, "w") as f:
OmegaConf.save(config=cfg, f=f)
return saved_cfg_path
def get_cfg_from_args(args):
args.output_dir = os.path.abspath(args.output_dir)
args.opts += [f"train.output_dir={args.output_dir}"]
default_cfg = OmegaConf.create(dinov2_default_config)
cfg = OmegaConf.load(args.config_file)
cfg = OmegaConf.merge(default_cfg, cfg, OmegaConf.from_cli(args.opts))
return cfg
def default_setup(args):
distributed.enable(overwrite=True)
seed = getattr(args, "seed", 0)
rank = distributed.get_global_rank()
global logger
setup_logging(output=args.output_dir, level=logging.INFO)
logger = logging.getLogger("dinov2")
utils.fix_random_seeds(seed + rank)
logger.info("git:\n {}\n".format(utils.get_sha()))
logger.info("\n".join("%s: %s" % (k, str(v)) for k, v in sorted(dict(vars(args)).items())))
def setup(args):
"""
Create configs and perform basic setups.
"""
cfg = get_cfg_from_args(args)
os.makedirs(args.output_dir, exist_ok=True)
default_setup(args)
apply_scaling_rules_to_cfg(cfg)
write_config(cfg, args.output_dir)
return cfg
+37
View File
@@ -0,0 +1,37 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
from typing import Dict, Union
import numpy as np
import torch
TypeSpec = Union[str, np.dtype, torch.dtype]
_NUMPY_TO_TORCH_DTYPE: Dict[np.dtype, torch.dtype] = {
np.dtype("bool"): torch.bool,
np.dtype("uint8"): torch.uint8,
np.dtype("int8"): torch.int8,
np.dtype("int16"): torch.int16,
np.dtype("int32"): torch.int32,
np.dtype("int64"): torch.int64,
np.dtype("float16"): torch.float16,
np.dtype("float32"): torch.float32,
np.dtype("float64"): torch.float64,
np.dtype("complex64"): torch.complex64,
np.dtype("complex128"): torch.complex128,
}
def as_torch_dtype(dtype: TypeSpec) -> torch.dtype:
if isinstance(dtype, torch.dtype):
return dtype
if isinstance(dtype, str):
dtype = np.dtype(dtype)
assert isinstance(dtype, np.dtype), f"Expected an instance of nunpy dtype, got {type(dtype)}"
return _NUMPY_TO_TORCH_DTYPE[dtype]
+103
View File
@@ -0,0 +1,103 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
from collections import defaultdict
import logging
logger = logging.getLogger("dinov2")
def get_vit_lr_decay_rate(name, lr_decay_rate=1.0, num_layers=12, force_is_backbone=False, chunked_blocks=False):
"""
Calculate lr decay rate for different ViT blocks.
Args:
name (string): parameter name.
lr_decay_rate (float): base lr decay rate.
num_layers (int): number of ViT blocks.
Returns:
lr decay rate for the given parameter.
"""
layer_id = num_layers + 1
if name.startswith("backbone") or force_is_backbone:
if (
".pos_embed" in name
or ".patch_embed" in name
or ".mask_token" in name
or ".cls_token" in name
or ".register_tokens" in name
):
layer_id = 0
elif force_is_backbone and (
"pos_embed" in name
or "patch_embed" in name
or "mask_token" in name
or "cls_token" in name
or "register_tokens" in name
):
layer_id = 0
elif ".blocks." in name and ".residual." not in name:
layer_id = int(name[name.find(".blocks.") :].split(".")[2]) + 1
elif chunked_blocks and "blocks." in name and "residual." not in name:
layer_id = int(name[name.find("blocks.") :].split(".")[2]) + 1
elif "blocks." in name and "residual." not in name:
layer_id = int(name[name.find("blocks.") :].split(".")[1]) + 1
return lr_decay_rate ** (num_layers + 1 - layer_id)
def get_params_groups_with_decay(model, lr_decay_rate=1.0, patch_embed_lr_mult=1.0):
chunked_blocks = False
if hasattr(model, "n_blocks"):
logger.info("chunked fsdp")
n_blocks = model.n_blocks
chunked_blocks = model.chunked_blocks
elif hasattr(model, "blocks"):
logger.info("first code branch")
n_blocks = len(model.blocks)
elif hasattr(model, "backbone"):
logger.info("second code branch")
n_blocks = len(model.backbone.blocks)
else:
logger.info("else code branch")
n_blocks = 0
all_param_groups = []
for name, param in model.named_parameters():
name = name.replace("_fsdp_wrapped_module.", "")
if not param.requires_grad:
continue
decay_rate = get_vit_lr_decay_rate(
name, lr_decay_rate, num_layers=n_blocks, force_is_backbone=n_blocks > 0, chunked_blocks=chunked_blocks
)
d = {"params": param, "is_last_layer": False, "lr_multiplier": decay_rate, "wd_multiplier": 1.0, "name": name}
if "last_layer" in name:
d.update({"is_last_layer": True})
if name.endswith(".bias") or "norm" in name or "gamma" in name:
d.update({"wd_multiplier": 0.0})
if "patch_embed" in name:
d.update({"lr_multiplier": d["lr_multiplier"] * patch_embed_lr_mult})
all_param_groups.append(d)
logger.info(f"""{name}: lr_multiplier: {d["lr_multiplier"]}, wd_multiplier: {d["wd_multiplier"]}""")
return all_param_groups
def fuse_params_groups(all_params_groups, keys=("lr_multiplier", "wd_multiplier", "is_last_layer")):
fused_params_groups = defaultdict(lambda: {"params": []})
for d in all_params_groups:
identifier = ""
for k in keys:
identifier += k + str(d[k]) + "_"
for k in keys:
fused_params_groups[identifier][k] = d[k]
fused_params_groups[identifier]["params"].append(d["params"])
return fused_params_groups.values()
+95
View File
@@ -0,0 +1,95 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the Apache License, Version 2.0
# found in the LICENSE file in the root directory of this source tree.
import logging
import os
import random
import subprocess
from urllib.parse import urlparse
import numpy as np
import torch
from torch import nn
logger = logging.getLogger("dinov2")
def load_pretrained_weights(model, pretrained_weights, checkpoint_key):
if urlparse(pretrained_weights).scheme: # If it looks like an URL
state_dict = torch.hub.load_state_dict_from_url(pretrained_weights, map_location="cpu")
else:
state_dict = torch.load(pretrained_weights, map_location="cpu")
if checkpoint_key is not None and checkpoint_key in state_dict:
logger.info(f"Take key {checkpoint_key} in provided checkpoint dict")
state_dict = state_dict[checkpoint_key]
# remove `module.` prefix
state_dict = {k.replace("module.", ""): v for k, v in state_dict.items()}
# remove `backbone.` prefix induced by multicrop wrapper
state_dict = {k.replace("backbone.", ""): v for k, v in state_dict.items()}
msg = model.load_state_dict(state_dict, strict=False)
logger.info("Pretrained weights found at {} and loaded with msg: {}".format(pretrained_weights, msg))
def fix_random_seeds(seed=31):
"""
Fix random seeds.
"""
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
def get_sha():
cwd = os.path.dirname(os.path.abspath(__file__))
def _run(command):
return subprocess.check_output(command, cwd=cwd).decode("ascii").strip()
sha = "N/A"
diff = "clean"
branch = "N/A"
try:
sha = _run(["git", "rev-parse", "HEAD"])
subprocess.check_output(["git", "diff"], cwd=cwd)
diff = _run(["git", "diff-index", "HEAD"])
diff = "has uncommitted changes" if diff else "clean"
branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
except Exception:
pass
message = f"sha: {sha}, status: {diff}, branch: {branch}"
return message
class CosineScheduler(object):
def __init__(self, base_value, final_value, total_iters, warmup_iters=0, start_warmup_value=0, freeze_iters=0):
super().__init__()
self.final_value = final_value
self.total_iters = total_iters
freeze_schedule = np.zeros((freeze_iters))
warmup_schedule = np.linspace(start_warmup_value, base_value, warmup_iters)
iters = np.arange(total_iters - warmup_iters - freeze_iters)
schedule = final_value + 0.5 * (base_value - final_value) * (1 + np.cos(np.pi * iters / len(iters)))
self.schedule = np.concatenate((freeze_schedule, warmup_schedule, schedule))
assert len(self.schedule) == self.total_iters
def __getitem__(self, it):
if it >= self.total_iters:
return self.final_value
else:
return self.schedule[it]
def has_batchnorms(model):
bn_types = (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)
for name, module in model.named_modules():
if isinstance(module, bn_types):
return True
return False
+372
View File
@@ -0,0 +1,372 @@
from typing import *
from numbers import Number
from pathlib import Path
import importlib
import warnings
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils
import torch.utils.checkpoint
import torch.version
from ...utils3d.torch import intrinsics_from_fov_xy, unproject_cv, image_uv
from huggingface_hub import hf_hub_download
from ..utils.geometry_torch import image_plane_uv, point_map_to_depth
from .utils import wrap_dinov2_attention_with_sdpa, wrap_module_with_gradient_checkpointing
class ResidualConvBlock(nn.Module):
def __init__(self, in_channels: int, out_channels: int = None, hidden_channels: int = None, padding_mode: str = 'replicate', activation: Literal['relu', 'leaky_relu', 'silu', 'elu'] = 'relu', norm: Literal['group_norm', 'layer_norm'] = 'group_norm'):
super(ResidualConvBlock, self).__init__()
if out_channels is None:
out_channels = in_channels
if hidden_channels is None:
hidden_channels = in_channels
if activation =='relu':
activation_cls = lambda: nn.ReLU(inplace=True)
elif activation == 'leaky_relu':
activation_cls = lambda: nn.LeakyReLU(negative_slope=0.2, inplace=True)
elif activation =='silu':
activation_cls = lambda: nn.SiLU(inplace=True)
elif activation == 'elu':
activation_cls = lambda: nn.ELU(inplace=True)
else:
raise ValueError(f'Unsupported activation function: {activation}')
self.layers = nn.Sequential(
nn.GroupNorm(1, in_channels),
activation_cls(),
nn.Conv2d(in_channels, hidden_channels, kernel_size=3, padding=1, padding_mode=padding_mode),
nn.GroupNorm(hidden_channels // 32 if norm == 'group_norm' else 1, hidden_channels),
activation_cls(),
nn.Conv2d(hidden_channels, out_channels, kernel_size=3, padding=1, padding_mode=padding_mode)
)
self.skip_connection = nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0) if in_channels != out_channels else nn.Identity()
def forward(self, x):
skip = self.skip_connection(x)
x = self.layers(x)
x = x + skip
return x
class Head(nn.Module):
def __init__(
self,
num_features: int,
dim_in: int,
dim_out: List[int],
dim_proj: int = 512,
dim_upsample: List[int] = [256, 128, 128],
dim_times_res_block_hidden: int = 1,
num_res_blocks: int = 1,
res_block_norm: Literal['group_norm', 'layer_norm'] = 'group_norm',
last_res_blocks: int = 0,
last_conv_channels: int = 32,
last_conv_size: int = 1
):
super().__init__()
self.projects = nn.ModuleList([
nn.Conv2d(in_channels=dim_in, out_channels=dim_proj, kernel_size=1, stride=1, padding=0,) for _ in range(num_features)
])
self.upsample_blocks = nn.ModuleList([
nn.Sequential(
self._make_upsampler(in_ch + 2, out_ch),
*(ResidualConvBlock(out_ch, out_ch, dim_times_res_block_hidden * out_ch, activation="relu", norm=res_block_norm) for _ in range(num_res_blocks))
) for in_ch, out_ch in zip([dim_proj] + dim_upsample[:-1], dim_upsample)
])
self.output_block = nn.ModuleList([
self._make_output_block(
dim_upsample[-1] + 2, dim_out_, dim_times_res_block_hidden, last_res_blocks, last_conv_channels, last_conv_size, res_block_norm,
) for dim_out_ in dim_out
])
def _make_upsampler(self, in_channels: int, out_channels: int):
upsampler = nn.Sequential(
nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2),
nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, padding_mode='replicate')
)
upsampler[0].weight.data[:] = upsampler[0].weight.data[:, :, :1, :1]
return upsampler
def _make_output_block(self, dim_in: int, dim_out: int, dim_times_res_block_hidden: int, last_res_blocks: int, last_conv_channels: int, last_conv_size: int, res_block_norm: Literal['group_norm', 'layer_norm']):
return nn.Sequential(
nn.Conv2d(dim_in, last_conv_channels, kernel_size=3, stride=1, padding=1, padding_mode='replicate'),
*(ResidualConvBlock(last_conv_channels, last_conv_channels, dim_times_res_block_hidden * last_conv_channels, activation='relu', norm=res_block_norm) for _ in range(last_res_blocks)),
nn.ReLU(inplace=True),
nn.Conv2d(last_conv_channels, dim_out, kernel_size=last_conv_size, stride=1, padding=last_conv_size // 2, padding_mode='replicate'),
)
def forward(self, hidden_states: torch.Tensor, image: torch.Tensor):
img_h, img_w = image.shape[-2:]
patch_h, patch_w = img_h // 14, img_w // 14
# Process the hidden states
x = torch.stack([
proj(feat.permute(0, 2, 1).unflatten(2, (patch_h, patch_w)).contiguous())
for proj, (feat, clstoken) in zip(self.projects, hidden_states)
], dim=1).sum(dim=1)
# Upsample stage
# (patch_h, patch_w) -> (patch_h * 2, patch_w * 2) -> (patch_h * 4, patch_w * 4) -> (patch_h * 8, patch_w * 8)
for i, block in enumerate(self.upsample_blocks):
# UV coordinates is for awareness of image aspect ratio
uv = image_plane_uv(width=x.shape[-1], height=x.shape[-2], aspect_ratio=img_w / img_h, dtype=x.dtype, device=x.device)
uv = uv.permute(2, 0, 1).unsqueeze(0).expand(x.shape[0], -1, -1, -1)
x = torch.cat([x, uv], dim=1)
for layer in block:
x = torch.utils.checkpoint.checkpoint(layer, x, use_reentrant=False)
# (patch_h * 8, patch_w * 8) -> (img_h, img_w)
x = F.interpolate(x, (img_h, img_w), mode="bilinear", align_corners=False)
uv = image_plane_uv(width=x.shape[-1], height=x.shape[-2], aspect_ratio=img_w / img_h, dtype=x.dtype, device=x.device)
uv = uv.permute(2, 0, 1).unsqueeze(0).expand(x.shape[0], -1, -1, -1)
x = torch.cat([x, uv], dim=1)
if isinstance(self.output_block, nn.ModuleList):
output = [torch.utils.checkpoint.checkpoint(block, x, use_reentrant=False) for block in self.output_block]
else:
output = torch.utils.checkpoint.checkpoint(self.output_block, x, use_reentrant=False)
return output
class MoGeModel(nn.Module):
image_mean: torch.Tensor
image_std: torch.Tensor
def __init__(self,
encoder: str = 'dinov2_vitb14',
intermediate_layers: Union[int, List[int]] = 4,
dim_proj: int = 512,
dim_upsample: List[int] = [256, 128, 128],
dim_times_res_block_hidden: int = 1,
num_res_blocks: int = 1,
output_mask: bool = False,
split_head: bool = False,
remap_output: Literal[False, True, 'linear', 'sinh', 'exp', 'sinh_exp'] = 'linear',
res_block_norm: Literal['group_norm', 'layer_norm'] = 'group_norm',
trained_diagonal_size_range: Tuple[Number, Number] = (600, 900),
trained_area_range: Tuple[Number, Number] = (500 * 500, 700 * 700),
last_res_blocks: int = 0,
last_conv_channels: int = 32,
last_conv_size: int = 1,
**deprecated_kwargs
):
super(MoGeModel, self).__init__()
if deprecated_kwargs:
warnings.warn(f"The following deprecated/invalid arguments are ignored: {deprecated_kwargs}")
self.encoder = encoder
self.remap_output = remap_output
self.intermediate_layers = intermediate_layers
self.trained_diagonal_size_range = trained_diagonal_size_range
self.trained_area_range = trained_area_range
self.output_mask = output_mask
self.split_head = split_head
# NOTE: We have copied the DINOv2 code in torchhub to this repository.
# Minimal modifications have been made: removing irrelevant code, unnecessary warnings and fixing importing issues.
hub_loader = getattr(importlib.import_module(".dinov2.hub.backbones", __package__), encoder)
self.backbone = hub_loader(pretrained=False)
dim_feature = self.backbone.blocks[0].attn.qkv.in_features
self.head = Head(
num_features=intermediate_layers if isinstance(intermediate_layers, int) else len(intermediate_layers),
dim_in=dim_feature,
dim_out=3 if not output_mask else 4 if output_mask and not split_head else [3, 1],
dim_proj=dim_proj,
dim_upsample=dim_upsample,
dim_times_res_block_hidden=dim_times_res_block_hidden,
num_res_blocks=num_res_blocks,
res_block_norm=res_block_norm,
last_res_blocks=last_res_blocks,
last_conv_channels=last_conv_channels,
last_conv_size=last_conv_size
)
image_mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
image_std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
self.register_buffer("image_mean", image_mean)
self.register_buffer("image_std", image_std)
if torch.__version__ >= '2.0':
self.enable_pytorch_native_sdpa()
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path: Union[str, Path, IO[bytes]], model_kwargs: Optional[Dict[str, Any]] = None, **hf_kwargs) -> 'MoGeModel':
"""
Load a model from a checkpoint file.
### Parameters:
- `pretrained_model_name_or_path`: path to the checkpoint file or repo id.
- `model_kwargs`: additional keyword arguments to override the parameters in the checkpoint.
- `hf_kwargs`: additional keyword arguments to pass to the `hf_hub_download` function. Ignored if `pretrained_model_name_or_path` is a local path.
### Returns:
- A new instance of `MoGe` with the parameters loaded from the checkpoint.
"""
if Path(pretrained_model_name_or_path).exists():
checkpoint = torch.load(pretrained_model_name_or_path, map_location='cpu', weights_only=True)
else:
cached_checkpoint_path = hf_hub_download(
repo_id=pretrained_model_name_or_path,
repo_type="model",
filename="model.pt",
**hf_kwargs
)
checkpoint = torch.load(cached_checkpoint_path, map_location='cpu', weights_only=True)
model_config = checkpoint['model_config']
if model_kwargs is not None:
model_config.update(model_kwargs)
model = cls(**model_config)
model.load_state_dict(checkpoint['model'])
return model
@staticmethod
def cache_pretrained_backbone(encoder: str, pretrained: bool):
_ = torch.hub.load('facebookresearch/dinov2', encoder, pretrained=pretrained)
def load_pretrained_backbone(self):
"Load the backbone with pretrained dinov2 weights from torch hub"
state_dict = torch.hub.load('facebookresearch/dinov2', self.encoder, pretrained=True).state_dict()
self.backbone.load_state_dict(state_dict)
def enable_backbone_gradient_checkpointing(self):
for i in range(len(self.backbone.blocks)):
self.backbone.blocks[i] = wrap_module_with_gradient_checkpointing(self.backbone.blocks[i])
def enable_pytorch_native_sdpa(self):
for i in range(len(self.backbone.blocks)):
self.backbone.blocks[i].attn = wrap_dinov2_attention_with_sdpa(self.backbone.blocks[i].attn)
def forward(self, image: torch.Tensor, mixed_precision: bool = False) -> Dict[str, torch.Tensor]:
raw_img_h, raw_img_w = image.shape[-2:]
patch_h, patch_w = raw_img_h // 14, raw_img_w // 14
image = (image - self.image_mean) / self.image_std
# Apply image transformation for DINOv2
image_14 = F.interpolate(image, (patch_h * 14, patch_w * 14), mode="bilinear", align_corners=False, antialias=True)
# Get intermediate layers from the backbone
with torch.autocast(device_type='cuda', dtype=torch.float16, enabled=mixed_precision):
features = self.backbone.get_intermediate_layers(image_14, self.intermediate_layers, return_class_token=True)
# Predict points (and mask)
output = self.head(features, image)
if self.output_mask:
if self.split_head:
points, mask = output
else:
points, mask = output.split([3, 1], dim=1)
points, mask = points.permute(0, 2, 3, 1), mask.squeeze(1)
else:
points = output.permute(0, 2, 3, 1)
if self.remap_output == 'linear' or self.remap_output == False:
pass
elif self.remap_output =='sinh' or self.remap_output == True:
points = torch.sinh(points)
elif self.remap_output == 'exp':
xy, z = points.split([2, 1], dim=-1)
z = torch.exp(z)
points = torch.cat([xy * z, z], dim=-1)
elif self.remap_output =='sinh_exp':
xy, z = points.split([2, 1], dim=-1)
points = torch.cat([torch.sinh(xy), torch.exp(z)], dim=-1)
else:
raise ValueError(f"Invalid remap output type: {self.remap_output}")
return_dict = {'points': points}
if self.output_mask:
return_dict['mask'] = mask
return return_dict
@torch.inference_mode()
def infer(
self,
image: torch.Tensor,
force_projection: bool = True,
resolution_level: int = 9,
apply_mask: bool = True,
) -> Dict[str, torch.Tensor]:
"""
User-friendly inference function
### Parameters
- `image`: input image tensor of shape (B, 3, H, W) or (3, H, W)
- `resolution_level`: the resolution level to use for the output point map in 0-9. Default: 9 (highest)
- `interpolation_mode`: interpolation mode for the output points map. Default: 'bilinear'.
### Returns
A dictionary containing the following keys:
- `points`: output tensor of shape (B, H, W, 3) or (H, W, 3).
- `depth`: tensor of shape (B, H, W) or (H, W) containing the depth map.
- `intrinsics`: tensor of shape (B, 3, 3) or (3, 3) containing the camera intrinsics.
"""
if image.dim() == 3:
omit_batch_dim = True
image = image.unsqueeze(0)
else:
omit_batch_dim = False
original_height, original_width = image.shape[-2:]
area = original_height * original_width
min_area, max_area = self.trained_area_range
expected_area = min_area + (max_area - min_area) * (resolution_level / 9)
if expected_area != area:
expected_width, expected_height = int(original_width * (expected_area / area) ** 0.5), int(original_height * (expected_area / area) ** 0.5)
image = F.interpolate(image, (expected_height, expected_width), mode="bicubic", align_corners=False, antialias=True)
output = self.forward(image)
points, mask = output['points'], output.get('mask', None)
# Get camera-origin-centered point map
depth, fov_x, fov_y, z_shift = point_map_to_depth(points, None if mask is None else mask > 0.5)
intrinsics = intrinsics_from_fov_xy(fov_x, fov_y)
# If projection constraint is forces, recompute the point map using the actual depth map
if force_projection:
points = unproject_cv(image_uv(width=expected_width, height=expected_height, dtype=points.dtype, device=points.device), depth, extrinsics=None, intrinsics=intrinsics[..., None, :, :])
else:
points = points + torch.stack([torch.zeros_like(z_shift), torch.zeros_like(z_shift), z_shift], dim=-1)[..., None, None, :]
# Resize the output to the original resolution
if expected_area != area:
points = F.interpolate(points.permute(0, 3, 1, 2), (original_height, original_width), mode='bilinear', align_corners=False, antialias=False).permute(0, 2, 3, 1)
depth = F.interpolate(depth.unsqueeze(1), (original_height, original_width), mode='bilinear', align_corners=False, antialias=False).squeeze(1)
mask = None if mask is None else F.interpolate(mask.unsqueeze(1), (original_height, original_width), mode='bilinear', align_corners=False, antialias=False).squeeze(1)
# Apply mask if needed
if self.output_mask and apply_mask:
mask_binary = (depth > 0) & (mask > 0.5)
points = torch.where(mask_binary[..., None], points, torch.inf)
depth = torch.where(mask_binary, depth, torch.inf)
if omit_batch_dim:
points = points.squeeze(0)
intrinsics = intrinsics.squeeze(0)
depth = depth.squeeze(0)
if self.output_mask:
mask = mask.squeeze(0)
return_dict = {
'points': points,
'intrinsics': intrinsics,
'depth': depth,
}
if self.output_mask:
return_dict['mask'] = mask > 0.5
return return_dict
+38
View File
@@ -0,0 +1,38 @@
from typing import *
import torch
import torch.nn as nn
import torch.nn.functional as F
def wrap_module_with_gradient_checkpointing(module: nn.Module):
from torch.utils.checkpoint import checkpoint
class _CheckpointingWrapper(module.__class__):
_restore_cls = module.__class__
def forward(self, *args, **kwargs):
return checkpoint(super().forward, *args, use_reentrant=False, **kwargs)
module.__class__ = _CheckpointingWrapper
return module
def unwrap_module_with_gradient_checkpointing(module: nn.Module):
module.__class__ = module.__class__._restore_cls
def wrap_dinov2_attention_with_sdpa(module: nn.Module):
assert torch.__version__ >= '2.0', "SDPA requires PyTorch 2.0 or later"
class _AttentionWrapper(module.__class__):
def forward(self, x: torch.Tensor, attn_bias=None) -> torch.Tensor:
B, N, C = x.shape
qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) # (3, B, H, N, C // H)
q, k, v = torch.unbind(qkv, 0) # (B, H, N, C // H)
x = F.scaled_dot_product_attention(q, k, v, attn_bias)
x = x.permute(0, 2, 1, 3).reshape(B, N, C)
x = self.proj(x)
x = self.proj_drop(x)
return x
module.__class__ = _AttentionWrapper
return module
View File
+314
View File
@@ -0,0 +1,314 @@
from typing import IO, Generator, Tuple, Union, overload
from pathlib import Path, PosixPath, PurePosixPath
import io
import os
import re
import requests
import fnmatch
from azure.identity import DefaultAzureCredential
from azure.storage.blob import ContainerClient, BlobClient
import requests.adapters
import requests.packages
from urllib3.util.retry import Retry
__all__ = [
'download_blob', 'upload_blob',
'download_blob_with_cache',
'open_blob', 'open_blob_with_cache',
'blob_file_exists',
'AzureBlobPath','SmartPath'
]
DEFAULT_CREDENTIAL = DefaultAzureCredential()
BLOB_CACHE_DIR = './.blobcache'
def download_blob(blob: Union[str, BlobClient]) -> bytes:
if isinstance(blob, str):
blob_client = BlobClient.from_blob_url(blob_client)
else:
blob_client = blob
return blob_client.download_blob().read()
def upload_blob(blob: Union[str, BlobClient], data: Union[str, bytes]):
if isinstance(blob, str):
blob_client = BlobClient.from_blob_url(blob)
else:
blob_client = blob
blob_client.upload_blob(data, overwrite=True)
def download_blob_with_cache(container: Union[str, ContainerClient], blob_name: str, cache_dir: str = 'blobcache') -> bytes:
"""
Download a blob file from a container and return its content as bytes.
If the file is already present in the cache, it is read from there.
"""
cache_path = Path(cache_dir) / blob_name
if cache_path.exists():
return cache_path.read_bytes()
data = download_blob(container, blob_name)
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_bytes(data)
return data
def open_blob(container: Union[str, ContainerClient], blob_name: str) -> io.BytesIO:
"""
Open a blob file for reading from a container and return its content as a BytesIO object.
"""
return io.BytesIO(download_blob(container, blob_name))
def open_blob_with_cache(container: Union[str, ContainerClient], blob_name: str, cache_dir: str = 'blobcache') -> io.BytesIO:
"""
Open a blob file for reading from a container and return its content as a BytesIO object.
If the file is already present in the cache, it is read from there.
"""
return io.BytesIO(download_blob_with_cache(container, blob_name, cache_dir=cache_dir))
def blob_file_exists(container: Union[str, ContainerClient], blob_name: str) -> bool:
"""
Check if a blob file exists in a container.
"""
if isinstance(container, str):
container = ContainerClient.from_container_url(container)
blob_client = container.get_blob_client(blob_name)
return blob_client.exists()
def is_blob_url(url: str) -> bool:
return re.match(r'https://[^/]+blob.core.windows.net/+', url) is not None
def split_blob_url(url: str) -> Tuple[str, str, str]:
match = re.match(r'(https://[^/]+blob.core.windows.net/[^/?]+)(/([^\?]*))?(\?.+)?', url)
if match:
container, _, path, sas = match.groups()
return container, path or '', sas or ''
raise ValueError(f'Not a valid blob URL: {url}')
def join_blob_path(url: str, *others: str) -> str:
container, path, sas = split_blob_url(url)
return container + '/' + os.path.join(path, *others) + sas
class AzureBlobStringWriter(io.StringIO):
def __init__(self, blob_client: BlobClient, encoding: str = 'utf-8', **kwargs):
self._encoding = encoding
self.blob_client = blob_client
self.kwargs = kwargs
super().__init__()
def close(self):
self.blob_client.upload_blob(self.getvalue().encode(self._encoding), blob_type='BlockBlob', overwrite=True, **self.kwargs)
class AzureBlobBytesWriter(io.BytesIO):
def __init__(self, blob_client: BlobClient, **kwargs):
super().__init__()
self.blob_client = blob_client
self.kwargs = kwargs
def close(self):
self.blob_client.upload_blob(self.getvalue(), blob_type='BlockBlob', overwrite=True, **self.kwargs)
def open_azure_blob(blob: Union[str, BlobClient], mode: str = 'r', encoding: str = 'utf-8', newline: str = None, cache_blob: bool = False, **kwargs) -> IO:
if isinstance(blob, str):
blob_client = BlobClient.from_blob_url(blob)
elif isinstance(blob, BlobClient):
blob_client = blob
else:
raise ValueError(f'Must be a blob URL or a BlobClient object: {blob}')
if cache_blob:
cache_path = Path(BLOB_CACHE_DIR, blob_client.account_name, blob_client.container_name, blob_client.blob_name)
if mode == 'r' or mode == 'rb':
if cache_blob:
if cache_path.exists():
data = cache_path.read_bytes()
else:
data = blob_client.download_blob(**kwargs).read()
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_bytes(data)
else:
data = blob_client.download_blob(**kwargs).read()
if mode == 'r':
return io.StringIO(data.decode(encoding), newline=newline)
else:
return io.BytesIO(data)
elif mode == 'w':
return AzureBlobStringWriter(blob_client, **kwargs)
elif mode == 'wb':
return AzureBlobBytesWriter(blob_client, **kwargs)
else:
raise ValueError(f'Unsupported mode: {mode}')
def smart_open(path_or_url: Union[Path, str], mode: str = 'r', encoding: str = 'utf-8') -> IO:
if is_blob_url(str(path_or_url)):
return open_azure_blob(str(path_or_url), mode, encoding)
return open(path_or_url, mode, encoding)
class AzureBlobPath(PurePosixPath):
"""
Implementation of pathlib.Path like interface for Azure Blob Storage.
"""
container_client: ContainerClient
_parse_path = PurePosixPath._parse_args if hasattr(PurePosixPath, '_parse_args') else PurePosixPath._parse_path
def __new__(cls, *args, **kwargs):
"""Override the old __new__ method. Parts are parsed in __init__"""
return object.__new__(cls)
def __init__(self, root: Union[str, 'AzureBlobPath', ContainerClient], *others: Union[str, PurePosixPath], pool_maxsize: int = 256, retries: int = 3):
if isinstance(root, AzureBlobPath):
self.container_client = root.container_client
parts = root.parts + others
elif isinstance(root, str):
url = root
container, path, sas = split_blob_url(url)
session = self._get_session(pool_maxsize=pool_maxsize, retries=retries)
if sas:
self.container_client = ContainerClient.from_container_url(container + sas, session=session)
else:
self.container_client = ContainerClient.from_container_url(container, credential=DEFAULT_CREDENTIAL, session=session)
parts = (path, *others)
elif isinstance(root, ContainerClient):
self.container_client = root
parts = others
else:
raise ValueError(f'Invalid root: {root}')
if hasattr(PurePosixPath, '_parse_args'):
# For compatibility with Python 3.10
drv, root, parts = PurePosixPath._parse_args(parts)
self._drv = drv
self._root = root
self._parts = parts
else:
super().__init__(*parts)
def _get_session(self, pool_maxsize: int = 1024, retries: int = 3) -> requests.Session:
session = requests.Session()
retry_strategy = Retry(
total=retries,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "PUT", "DELETE"],
backoff_factor=1,
raise_on_status=False,
read=retries,
connect=retries,
redirect=retries,
)
adapter = requests.adapters.HTTPAdapter(pool_connections=pool_maxsize, pool_maxsize=pool_maxsize, max_retries=retry_strategy)
session.mount('http://', adapter)
session.mount('https://', adapter)
return session
def _from_parsed_parts(self, drv, root, parts):
"For compatibility with Python 3.10"
return AzureBlobPath(self.container_client, drv, root, *parts)
def with_segments(self, *pathsegments):
return AzureBlobPath(self.container_client, *pathsegments)
@property
def path(self) -> str:
return '/'.join(self.parts)
@property
def blob_client(self) -> BlobClient:
return self.container_client.get_blob_client(self.path)
@property
def url(self) -> str:
if len(self.parts) == 0:
return self.container_client.url
return self.container_client.get_blob_client(self.path).url
@property
def container_name(self) -> str:
return self.container_client.container_name
@property
def account_name(self) -> str:
return self.container_client.account_name
def __str__(self):
return self.url
def __repr__(self):
return self.url
def open(self, mode: str = 'r', encoding: str = 'utf-8', cache_blob: bool = False, **kwargs) -> IO:
return open_azure_blob(self.blob_client, mode, encoding, cache_blob=cache_blob, **kwargs)
def __truediv__(self, other: Union[str, Path]) -> 'AzureBlobPath':
return self.joinpath(other)
def mkdir(self, parents: bool = False, exist_ok: bool = False):
pass
def iterdir(self) -> Generator['AzureBlobPath', None, None]:
path = self.path
if not path.endswith('/'):
path += '/'
for item in self.container_client.walk_blobs(self.path):
yield AzureBlobPath(self.container_client, item.name)
def glob(self, pattern: str) -> Generator['AzureBlobPath', None, None]:
special_chars = ".^$+{}[]()|/"
for char in special_chars:
pattern = pattern.replace(char, "\\" + char)
pattern = pattern.replace('**', './/.')
pattern = pattern.replace('*', '[^/]*')
pattern = pattern.replace('.//.', '.*')
pattern = "^" + pattern + "$"
reg = re.compile(pattern)
for item in self.container_client.list_blobs(self.path):
if reg.match(os.path.relpath(item.name, self.path)):
yield AzureBlobPath(self.container_client, item.name)
def exists(self) -> bool:
return self.blob_client.exists()
def read_bytes(self, cache_blob: bool = False) -> bytes:
with self.open('rb', cache_blob=cache_blob) as f:
return f.read()
def read_text(self, encoding: str = 'utf-8', cache_blob: bool = False) -> str:
with self.open('r', encoding=encoding, cache_blob=cache_blob) as f:
return f.read()
def write_bytes(self, data: bytes):
self.blob_client.upload_blob(data, overwrite=True)
def write_text(self, data: str, encoding: str = 'utf-8'):
self.blob_client.upload_blob(data.encode(encoding), overwrite=True)
def unlink(self):
self.blob_client.delete_blob()
def new_client(self) -> 'AzureBlobPath':
return AzureBlobPath(self.container_client.url, self.path)
class SmartPath(Path, AzureBlobPath):
"""
Supports both local file paths and Azure Blob Storage URLs.
"""
def __new__(cls, first: Union[Path, str], *others: Union[str, PurePosixPath]) -> Union[Path, AzureBlobPath]:
if is_blob_url(str(first)):
return AzureBlobPath(str(first), *others)
return Path(first, *others)
+55
View File
@@ -0,0 +1,55 @@
from pathlib import Path
from typing import *
import requests
from tqdm import tqdm
__all__ = ["download_file", "download_bytes"]
def download_file(url: str, filepath: Union[str, Path], headers: dict = None, resume: bool = True) -> None:
# Ensure headers is a dict if not provided
headers = headers or {}
# Initialize local variables
file_path = Path(filepath)
downloaded_bytes = 0
# Check if we should resume the download
if resume and file_path.exists():
downloaded_bytes = file_path.stat().st_size
headers['Range'] = f"bytes={downloaded_bytes}-"
# Make a GET request to fetch the file
with requests.get(url, stream=True, headers=headers) as response:
response.raise_for_status() # This will raise an HTTPError if the status is 4xx/5xx
# Calculate the total size to download
total_size = downloaded_bytes + int(response.headers.get('content-length', 0))
# Display a progress bar while downloading
with (
tqdm(desc=f"Downloading {file_path.name}", total=total_size, unit='B', unit_scale=True, leave=False) as pbar,
open(file_path, 'ab') as file,
):
# Set the initial position of the progress bar
pbar.update(downloaded_bytes)
# Write the content to the file in chunks
for chunk in response.iter_content(chunk_size=4096):
file.write(chunk)
pbar.update(len(chunk))
def download_bytes(url: str, headers: dict = None) -> bytes:
# Ensure headers is a dict if not provided
headers = headers or {}
# Make a GET request to fetch the file
with requests.get(url, stream=True, headers=headers) as response:
response.raise_for_status() # This will raise an HTTPError if the status is 4xx/5xx
# Read the content of the response
return response.content
+193
View File
@@ -0,0 +1,193 @@
from typing import *
from functools import partial
import math
import numpy as np
from ...utils3d.numpy import image_pixel_center, image_uv, sliding_window_2d
def weighted_mean_numpy(x: np.ndarray, w: np.ndarray = None, axis: Union[int, Tuple[int,...]] = None, keepdims: bool = False, eps: float = 1e-7) -> np.ndarray:
if w is None:
return np.mean(x, axis=axis)
else:
w = w.astype(x.dtype)
return (x * w).mean(axis=axis) / np.clip(w.mean(axis=axis), eps, None)
def harmonic_mean_numpy(x: np.ndarray, w: np.ndarray = None, axis: Union[int, Tuple[int,...]] = None, keepdims: bool = False, eps: float = 1e-7) -> np.ndarray:
if w is None:
return 1 / (1 / np.clip(x, eps, None)).mean(axis=axis)
else:
w = w.astype(x.dtype)
return 1 / (weighted_mean_numpy(1 / (x + eps), w, axis=axis, keepdims=keepdims, eps=eps) + eps)
def image_plane_uv_numpy(width: int, height: int, aspect_ratio: float = None, dtype: np.dtype = np.float32) -> np.ndarray:
"UV with left-top corner as (-width / diagonal, -height / diagonal) and right-bottom corner as (width / diagonal, height / diagonal)"
if aspect_ratio is None:
aspect_ratio = width / height
span_x = aspect_ratio / (1 + aspect_ratio ** 2) ** 0.5
span_y = 1 / (1 + aspect_ratio ** 2) ** 0.5
u = np.linspace(-span_x * (width - 1) / width, span_x * (width - 1) / width, width, dtype=dtype)
v = np.linspace(-span_y * (height - 1) / height, span_y * (height - 1) / height, height, dtype=dtype)
u, v = np.meshgrid(u, v, indexing='xy')
uv = np.stack([u, v], axis=-1)
return uv
def focal_to_fov_numpy(focal: np.ndarray):
return 2 * np.arctan(0.5 / focal)
def fov_to_focal_numpy(fov: np.ndarray):
return 0.5 / np.tan(fov / 2)
def intrinsics_to_fov_numpy(intrinsics: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
fov_x = focal_to_fov_numpy(intrinsics[..., 0, 0])
fov_y = focal_to_fov_numpy(intrinsics[..., 1, 1])
return fov_x, fov_y
def point_map_to_depth_legacy_numpy(points: np.ndarray):
height, width = points.shape[-3:-1]
diagonal = (height ** 2 + width ** 2) ** 0.5
uv = image_plane_uv_numpy(width, height, dtype=points.dtype) # (H, W, 2)
_, uv = np.broadcast_arrays(points[..., :2], uv)
# Solve least squares problem
b = (uv * points[..., 2:]).reshape(*points.shape[:-3], -1) # (..., H * W * 2)
A = np.stack([points[..., :2], -uv], axis=-1).reshape(*points.shape[:-3], -1, 2) # (..., H * W * 2, 2)
M = A.swapaxes(-2, -1) @ A
solution = (np.linalg.inv(M + 1e-6 * np.eye(2)) @ (A.swapaxes(-2, -1) @ b[..., None])).squeeze(-1)
focal, shift = solution
depth = points[..., 2] + shift[..., None, None]
fov_x = np.arctan(width / diagonal / focal) * 2
fov_y = np.arctan(height / diagonal / focal) * 2
return depth, fov_x, fov_y, shift
def solve_optimal_shift_focal(uv: np.ndarray, xyz: np.ndarray, ransac_iters: int = None, ransac_hypothetical_size: float = 0.1, ransac_threshold: float = 0.1):
"Solve `min |focal * xy / (z + shift) - uv|` with respect to shift and focal"
from scipy.optimize import least_squares
uv, xy, z = uv.reshape(-1, 2), xyz[..., :2].reshape(-1, 2), xyz[..., 2].reshape(-1)
def fn(uv: np.ndarray, xy: np.ndarray, z: np.ndarray, shift: np.ndarray):
xy_proj = xy / (z + shift)[: , None]
f = (xy_proj * uv).sum() / np.square(xy_proj).sum()
err = (f * xy_proj - uv).ravel()
return err
initial_shift = 0 #-z.min(keepdims=True) + 1.0
if ransac_iters is None:
solution = least_squares(partial(fn, uv, xy, z), x0=initial_shift, ftol=1e-3, method='lm')
optim_shift = solution['x'].squeeze().astype(np.float32)
else:
best_err, best_shift = np.inf, None
for _ in range(ransac_iters):
maybe_inliers = np.random.choice(len(z), size=int(ransac_hypothetical_size * len(z)), replace=False)
solution = least_squares(partial(fn, uv[maybe_inliers], xy[maybe_inliers], z[maybe_inliers]), x0=initial_shift, ftol=1e-3, method='lm')
maybe_shift = solution['x'].squeeze().astype(np.float32)
confirmed_inliers = np.linalg.norm(fn(uv, xy, z, maybe_shift).reshape(-1, 2), axis=-1) < ransac_threshold
if confirmed_inliers.sum() > 10:
solution = least_squares(partial(fn, uv[confirmed_inliers], xy[confirmed_inliers], z[confirmed_inliers]), x0=maybe_shift, ftol=1e-3, method='lm')
better_shift = solution['x'].squeeze().astype(np.float32)
else:
better_shift = maybe_shift
err = np.linalg.norm(fn(uv, xy, z, better_shift).reshape(-1, 2), axis=-1).clip(max=ransac_threshold).mean()
if err < best_err:
best_err, best_shift = err, better_shift
initial_shift = best_shift
optim_shift = best_shift
xy_proj = xy / (z + optim_shift)[: , None]
optim_focal = (xy_proj * uv).sum() / (xy_proj * xy_proj).sum()
return optim_shift, optim_focal
def point_map_to_depth_numpy(points: np.ndarray, mask: np.ndarray = None, downsample_size: Tuple[int, int] = (64, 64)):
import cv2
assert points.shape[-1] == 3, "Points should (H, W, 3)"
height, width = points.shape[-3], points.shape[-2]
diagonal = (height ** 2 + width ** 2) ** 0.5
uv = image_plane_uv_numpy(width=width, height=height)
if mask is None:
points_lr = cv2.resize(points, downsample_size, interpolation=cv2.INTER_LINEAR).reshape(-1, 3)
uv_lr = cv2.resize(uv, downsample_size, interpolation=cv2.INTER_LINEAR).reshape(-1, 2)
else:
index, mask_lr = mask_aware_nearest_resize_numpy(mask, *downsample_size)
points_lr, uv_lr = points[index][mask_lr], uv[index][mask_lr]
if points_lr.size == 0:
return np.zeros((height, width)), 0, 0, 0
optim_shift, optim_focal = solve_optimal_shift_focal(uv_lr, points_lr, ransac_iters=None)
fov_x = 2 * np.arctan(width / diagonal / optim_focal)
fov_y = 2 * np.arctan(height / diagonal / optim_focal)
depth = points[:, :, 2] + optim_shift
return depth, fov_x, fov_y, optim_shift
def mask_aware_nearest_resize_numpy(mask: np.ndarray, target_width: int, target_height: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Resize 2D map by nearest interpolation. Return the nearest neighbor index and mask of the resized map.
### Parameters
- `mask`: Input 2D mask of shape (..., H, W)
- `target_width`: target width of the resized map
- `target_height`: target height of the resized map
### Returns
- `nearest_idx`: Nearest neighbor index of the resized map of shape (..., target_height, target_width). Indices are like j + i * W, where j is the row index and i is the column index.
- `target_mask`: Mask of the resized map of shape (..., target_height, target_width)
"""
height, width = mask.shape[-2:]
filter_h_f, filter_w_f = max(1, height / target_height), max(1, width / target_width)
filter_h_i, filter_w_i = math.ceil(filter_h_f), math.ceil(filter_w_f)
filter_size = filter_h_i * filter_w_i
padding_h, padding_w = round(filter_h_f / 2), round(filter_w_f / 2)
# Window the original mask and uv
uv = image_pixel_center(width=width, height=height, dtype=np.float32)
indices = np.arange(height * width, dtype=np.int32).reshape(height, width)
padded_uv = np.full((height + 2 * padding_h, width + 2 * padding_w, 2), 0, dtype=np.float32)
padded_uv[padding_h:padding_h + height, padding_w:padding_w + width] = uv
padded_mask = np.full((*mask.shape[:-2], height + 2 * padding_h, width + 2 * padding_w), False, dtype=bool)
padded_mask[..., padding_h:padding_h + height, padding_w:padding_w + width] = mask
padded_indices = np.full((height + 2 * padding_h, width + 2 * padding_w), 0, dtype=np.int32)
padded_indices[padding_h:padding_h + height, padding_w:padding_w + width] = indices
windowed_uv = sliding_window_2d(padded_uv, (filter_h_i, filter_w_i), 1, axis=(0, 1))
windowed_mask = sliding_window_2d(padded_mask, (filter_h_i, filter_w_i), 1, axis=(-2, -1))
windowed_indices = sliding_window_2d(padded_indices, (filter_h_i, filter_w_i), 1, axis=(0, 1))
# Gather the target pixels's local window
target_uv = image_uv(width=target_width, height=target_height, dtype=np.float32) * np.array([width, height], dtype=np.float32)
target_corner = target_uv - np.array((filter_w_f / 2, filter_h_f / 2), dtype=np.float32)
target_corner = np.round(target_corner - 0.5).astype(np.int32) + np.array((padding_w, padding_h), dtype=np.int32)
target_window_uv = windowed_uv[target_corner[..., 1], target_corner[..., 0], :, :, :].reshape(target_height, target_width, 2, filter_size) # (target_height, tgt_width, 2, filter_size)
target_window_mask = windowed_mask[..., target_corner[..., 1], target_corner[..., 0], :, :].reshape(*mask.shape[:-2], target_height, target_width, filter_size) # (..., target_height, tgt_width, filter_size)
target_window_indices = windowed_indices[target_corner[..., 1], target_corner[..., 0], :, :].reshape(target_height, target_width, filter_size) # (target_height, tgt_width, filter_size)
# Compute nearest neighbor in the local window for each pixel
dist = np.square(target_window_uv - target_uv[..., None])
dist = dist[..., 0, :] + dist[..., 1, :]
dist = np.where(target_window_mask, dist, np.inf) # (..., target_height, tgt_width, filter_size)
nearest_in_window = np.argmin(dist, axis=-1, keepdims=True) # (..., target_height, tgt_width, 1)
nearest_idx = np.take_along_axis(target_window_indices, nearest_in_window, axis=-1).squeeze(-1) # (..., target_height, tgt_width)
nearest_i, nearest_j = nearest_idx // width, nearest_idx % width
target_mask = np.any(target_window_mask, axis=-1)
batch_indices = [np.arange(n).reshape([1] * i + [n] + [1] * (mask.ndim - i - 1)) for i, n in enumerate(mask.shape[:-2])]
return (*batch_indices, nearest_i, nearest_j), target_mask
+209
View File
@@ -0,0 +1,209 @@
from typing import *
import math
import torch
import torch.nn.functional as F
import torch.types
from ...utils3d.torch import image_uv, image_pixel_center, sliding_window_2d
from .geometry_numpy import solve_optimal_shift_focal
def weighted_mean(x: torch.Tensor, w: torch.Tensor = None, dim: Union[int, torch.Size] = None, keepdim: bool = False, eps: float = 1e-7) -> torch.Tensor:
if w is None:
return x.mean(dim=dim, keepdim=keepdim)
else:
w = w.to(x.dtype)
return (x * w).mean(dim=dim, keepdim=keepdim) / w.mean(dim=dim, keepdim=keepdim).add(eps)
def harmonic_mean(x: torch.Tensor, w: torch.Tensor = None, dim: Union[int, torch.Size] = None, keepdim: bool = False, eps: float = 1e-7) -> torch.Tensor:
if w is None:
return x.add(eps).reciprocal().mean(dim=dim, keepdim=keepdim).reciprocal()
else:
w = w.to(x.dtype)
return weighted_mean(x.add(eps).reciprocal(), w, dim=dim, keepdim=keepdim, eps=eps).add(eps).reciprocal()
def geometric_mean(x: torch.Tensor, w: torch.Tensor = None, dim: Union[int, torch.Size] = None, keepdim: bool = False, eps: float = 1e-7) -> torch.Tensor:
if w is None:
return x.add(eps).log().mean(dim=dim).exp()
else:
w = w.to(x.dtype)
return weighted_mean(x.add(eps).log(), w, dim=dim, keepdim=keepdim, eps=eps).exp()
def image_plane_uv(width: int, height: int, aspect_ratio: float = None, dtype: torch.dtype = None, device: torch.device = None) -> torch.Tensor:
"UV with left-top corner as (-width / diagonal, -height / diagonal) and right-bottom corner as (width / diagonal, height / diagonal)"
if aspect_ratio is None:
aspect_ratio = width / height
span_x = aspect_ratio / (1 + aspect_ratio ** 2) ** 0.5
span_y = 1 / (1 + aspect_ratio ** 2) ** 0.5
u = torch.linspace(-span_x * (width - 1) / width, span_x * (width - 1) / width, width, dtype=dtype, device=device)
v = torch.linspace(-span_y * (height - 1) / height, span_y * (height - 1) / height, height, dtype=dtype, device=device)
u, v = torch.meshgrid(u, v, indexing='xy')
uv = torch.stack([u, v], dim=-1)
return uv
def gaussian_blur_2d(input: torch.Tensor, kernel_size: int, sigma: float) -> torch.Tensor:
kernel = torch.exp(-(torch.arange(-kernel_size // 2 + 1, kernel_size // 2 + 1, dtype=input.dtype, device=input.device) ** 2) / (2 * sigma ** 2))
kernel = kernel / kernel.sum()
kernel = (kernel[:, None] * kernel[None, :]).reshape(1, 1, kernel_size, kernel_size)
input = F.pad(input, (kernel_size // 2, kernel_size // 2, kernel_size // 2, kernel_size // 2), mode='replicate')
input = F.conv2d(input, kernel, groups=input.shape[1])
return input
def focal_to_fov(focal: torch.Tensor):
return 2 * torch.atan(0.5 / focal)
def fov_to_focal(fov: torch.Tensor):
return 0.5 / torch.tan(fov / 2)
def intrinsics_to_fov(intrinsics: torch.Tensor):
"""
Returns field of view in radians from normalized intrinsics matrix.
### Parameters:
- intrinsics: torch.Tensor of shape (..., 3, 3)
### Returns:
- fov_x: torch.Tensor of shape (...)
- fov_y: torch.Tensor of shape (...)
"""
focal_x = intrinsics[..., 0, 0]
focal_y = intrinsics[..., 1, 1]
return 2 * torch.atan(0.5 / focal_x), 2 * torch.atan(0.5 / focal_y)
def point_map_to_depth_legacy(points: torch.Tensor):
height, width = points.shape[-3:-1]
diagonal = (height ** 2 + width ** 2) ** 0.5
uv = image_plane_uv(width, height, dtype=points.dtype, device=points.device) # (H, W, 2)
# Solve least squares problem
b = (uv * points[..., 2:]).flatten(-3, -1) # (..., H * W * 2)
A = torch.stack([points[..., :2], -uv.expand_as(points[..., :2])], dim=-1).flatten(-4, -2) # (..., H * W * 2, 2)
M = A.transpose(-2, -1) @ A
solution = (torch.inverse(M + 1e-6 * torch.eye(2).to(A)) @ (A.transpose(-2, -1) @ b[..., None])).squeeze(-1)
focal, shift = solution.unbind(-1)
depth = points[..., 2] + shift[..., None, None]
fov_x = torch.atan(width / diagonal / focal) * 2
fov_y = torch.atan(height / diagonal / focal) * 2
return depth, fov_x, fov_y, shift
def point_map_to_depth(points: torch.Tensor, mask: torch.Tensor = None, downsample_size: Tuple[int, int] = (64, 64)):
"""
Recover the depth map and FoV from a point map with unknown z shift and focal.
Note that it assumes:
- the optical center is at the center of the map
- the map is undistorted
- the map is isometric in the x and y directions
### Parameters:
- `points: torch.Tensor` of shape (..., H, W, 3)
- `downsample_size: Tuple[int, int]` in (height, width), the size of the downsampled map. Downsampling produces approximate solution and is efficient for large maps.
### Returns:
- `depth: torch.Tensor` of shape (..., H, W)
- `fov_x: torch.Tensor` of shape (...)
- `fov_y: torch.Tensor` of shape (...)
- `shift: torch.Tensor` of shape (...), the z shift, making `depth = points[..., 2] + shift`
"""
shape = points.shape
height, width = points.shape[-3], points.shape[-2]
diagonal = (height ** 2 + width ** 2) ** 0.5
points = points.reshape(-1, *shape[-3:])
mask = None if mask is None else mask.reshape(-1, *shape[-3:-1])
uv = image_plane_uv(width, height, dtype=points.dtype, device=points.device) # (H, W, 2)
points_lr = F.interpolate(points.permute(0, 3, 1, 2), downsample_size, mode='nearest').permute(0, 2, 3, 1)
uv_lr = F.interpolate(uv.unsqueeze(0).permute(0, 3, 1, 2), downsample_size, mode='nearest').squeeze(0).permute(1, 2, 0)
mask_lr = None if mask is None else F.interpolate(mask.to(torch.float32).unsqueeze(1), downsample_size, mode='nearest').squeeze(1) > 0
uv_lr_np = uv_lr.cpu().numpy()
points_lr_np = points_lr.detach().cpu().numpy()
mask_lr_np = None if mask is None else mask_lr.cpu().numpy()
optim_shift, optim_focal = [], []
for i in range(points.shape[0]):
points_lr_i_np = points_lr_np[i] if mask is None else points_lr_np[i][mask_lr_np[i]]
uv_lr_i_np = uv_lr_np if mask is None else uv_lr_np[mask_lr_np[i]]
optim_shift_i, optim_focal_i = solve_optimal_shift_focal(uv_lr_i_np, points_lr_i_np, ransac_iters=None)
optim_shift.append(float(optim_shift_i))
optim_focal.append(float(optim_focal_i))
optim_shift = torch.tensor(optim_shift, device=points.device, dtype=points.dtype)
optim_focal = torch.tensor(optim_focal, device=points.device, dtype=points.dtype)
fov_x = 2 * torch.atan(width / diagonal / optim_focal)
fov_y = 2 * torch.atan(height / diagonal / optim_focal)
depth = (points[..., 2] + optim_shift[:, None, None]).reshape(shape[:-1])
fov_x = fov_x.reshape(shape[:-3])
fov_y = fov_y.reshape(shape[:-3])
optim_shift = optim_shift.reshape(shape[:-3])
return depth, fov_x, fov_y, optim_shift
def mask_aware_nearest_resize(mask: torch.BoolTensor, target_width: int, target_height: int) -> Tuple[torch.LongTensor, torch.LongTensor, torch.BoolTensor]:
"""
Resize 2D map by nearest interpolation. Return the nearest neighbor index and mask of the resized map.
### Parameters
- `mask`: Input 2D mask of shape (..., H, W)
- `target_width`: target width of the resized map
- `target_height`: target height of the resized map
### Returns
- `nearest_idx`: Nearest neighbor index of the resized map of shape (..., target_height, target_width) for each dimension
- `target_mask`: Mask of the resized map of shape (..., target_height, target_width)
"""
height, width = mask.shape[-2:]
device = mask.device
filter_h_f, filter_w_f = max(1, height / target_height), max(1, width / target_width)
filter_h_i, filter_w_i = math.ceil(filter_h_f), math.ceil(filter_w_f)
filter_size = filter_h_i * filter_w_i
padding_h, padding_w = round(filter_h_f / 2), round(filter_w_f / 2)
# Window the original mask and uv
uv = image_pixel_center(width=width, height=height, dtype=torch.float32, device=device)
indices = torch.arange(height * width, dtype=torch.long, device=device).reshape(height, width)
padded_uv = torch.full((height + 2 * padding_h, width + 2 * padding_w, 2), 0, dtype=torch.float32, device=device)
padded_uv[padding_h:padding_h + height, padding_w:padding_w + width] = uv
padded_mask = torch.full((*mask.shape[:-2], height + 2 * padding_h, width + 2 * padding_w), False, dtype=torch.bool, device=device)
padded_mask[..., padding_h:padding_h + height, padding_w:padding_w + width] = mask
padded_indices = torch.full((height + 2 * padding_h, width + 2 * padding_w), 0, dtype=torch.long, device=device)
padded_indices[padding_h:padding_h + height, padding_w:padding_w + width] = indices
windowed_uv = sliding_window_2d(padded_uv, (filter_h_i, filter_w_i), 1, dim=(0, 1))
windowed_mask = sliding_window_2d(padded_mask, (filter_h_i, filter_w_i), 1, dim=(-2, -1))
windowed_indices = sliding_window_2d(padded_indices, (filter_h_i, filter_w_i), 1, dim=(0, 1))
# Gather the target pixels's local window
target_uv = image_uv(width=target_width, height=target_height, dtype=torch.float32, device=device) * torch.tensor([width, height], dtype=torch.float32, device=device)
target_corner = target_uv - torch.tensor((filter_w_f / 2, filter_h_f / 2), dtype=torch.float32, device=device)
target_corner = torch.round(target_corner - 0.5).long() + torch.tensor((padding_w, padding_h), dtype=torch.long, device=device)
target_window_uv = windowed_uv[target_corner[..., 1], target_corner[..., 0], :, :, :].reshape(target_height, target_width, 2, filter_size) # (target_height, tgt_width, 2, filter_size)
target_window_mask = windowed_mask[..., target_corner[..., 1], target_corner[..., 0], :, :].reshape(*mask.shape[:-2], target_height, target_width, filter_size) # (..., target_height, tgt_width, filter_size)
target_window_indices = windowed_indices[target_corner[..., 1], target_corner[..., 0], :, :].reshape(target_height, target_width, filter_size) # (target_height, tgt_width, filter_size)
target_window_indices = target_window_indices.expand_as(target_window_mask)
# Compute nearest neighbor in the local window for each pixel
dist = torch.where(target_window_mask, torch.norm(target_window_uv - target_uv[..., None], dim=-2), torch.inf) # (..., target_height, tgt_width, filter_size)
nearest = torch.argmin(dist, dim=-1, keepdim=True) # (..., target_height, tgt_width, 1)
nearest_idx = torch.gather(target_window_indices, index=nearest, dim=-1).squeeze(-1) # (..., target_height, tgt_width)
target_mask = torch.any(target_window_mask, dim=-1)
nearest_i, nearest_j = nearest_idx // width, nearest_idx % width
batch_indices = [torch.arange(n, device=device).reshape([1] * i + [n] + [1] * (mask.dim() - i - 1)) for i, n in enumerate(mask.shape[:-2])]
return (*batch_indices, nearest_i, nearest_j), target_mask
+346
View File
@@ -0,0 +1,346 @@
import os
os.environ['OPENCV_IO_ENABLE_OPENEXR'] = '1'
from typing import IO
import zipfile
import json
import io
from typing import *
from pathlib import Path
import re
import numpy as np
import cv2
from .tools import timeit
LEGACY_SEGFORMER_CLASSES = [
'wall', 'building', 'sky', 'floor', 'tree', 'ceiling', 'road', 'bed ',
'windowpane', 'grass', 'cabinet', 'sidewalk', 'person', 'earth',
'door', 'table', 'mountain', 'plant', 'curtain', 'chair', 'car',
'water', 'painting', 'sofa', 'shelf', 'house', 'sea', 'mirror', 'rug',
'field', 'armchair', 'seat', 'fence', 'desk', 'rock', 'wardrobe',
'lamp', 'bathtub', 'railing', 'cushion', 'base', 'box', 'column',
'signboard', 'chest of drawers', 'counter', 'sand', 'sink',
'skyscraper', 'fireplace', 'refrigerator', 'grandstand', 'path',
'stairs', 'runway', 'case', 'pool table', 'pillow', 'screen door',
'stairway', 'river', 'bridge', 'bookcase', 'blind', 'coffee table',
'toilet', 'flower', 'book', 'hill', 'bench', 'countertop', 'stove',
'palm', 'kitchen island', 'computer', 'swivel chair', 'boat', 'bar',
'arcade machine', 'hovel', 'bus', 'towel', 'light', 'truck', 'tower',
'chandelier', 'awning', 'streetlight', 'booth', 'television receiver',
'airplane', 'dirt track', 'apparel', 'pole', 'land', 'bannister',
'escalator', 'ottoman', 'bottle', 'buffet', 'poster', 'stage', 'van',
'ship', 'fountain', 'conveyer belt', 'canopy', 'washer', 'plaything',
'swimming pool', 'stool', 'barrel', 'basket', 'waterfall', 'tent',
'bag', 'minibike', 'cradle', 'oven', 'ball', 'food', 'step', 'tank',
'trade name', 'microwave', 'pot', 'animal', 'bicycle', 'lake',
'dishwasher', 'screen', 'blanket', 'sculpture', 'hood', 'sconce',
'vase', 'traffic light', 'tray', 'ashcan', 'fan', 'pier', 'crt screen',
'plate', 'monitor', 'bulletin board', 'shower', 'radiator', 'glass',
'clock', 'flag'
]
LEGACY_SEGFORMER_LABELS = {k: i for i, k in enumerate(LEGACY_SEGFORMER_CLASSES)}
def write_rgbd_zip(
file: Union[IO, os.PathLike],
image: Union[np.ndarray, bytes],
depth: Union[np.ndarray, bytes], mask: Union[np.ndarray, bytes],
segmentation_mask: Union[np.ndarray, bytes] = None, segmentation_labels: Union[Dict[str, int], bytes] = None,
intrinsics: np.ndarray = None,
normal: np.ndarray = None, normal_mask: np.ndarray = None,
meta: Union[Dict[str, Any], bytes] = None,
*, image_quality: int = 95, depth_type: Literal['linear', 'log', 'disparity'] = 'linear', depth_format: Literal['png', 'exr'] = 'png', depth_max_dynamic_range: float = 1e4, png_compression: int = 7
):
"""
Write RGBD data as zip archive containing the image, depth, mask, segmentation_mask, and meta data.
In the zip file there will be:
- `meta.json`: The meta data as a JSON file.
- `image.jpg`: The RGB image as a JPEG file.
- `depth.png/exr`: The depth map as a PNG or EXR file, depending on the `depth_type`.
- `mask.png` (optional): The mask as a uint8 PNG file.
- `segmentation_mask.png` (optional): The segformer mask as a uint8/uint16 PNG file.
You can provided those data as np.ndarray or bytes. If you provide them as np.ndarray, they will be properly processed and encoded.
If you provide them as bytes, they will be written as is, assuming they are already encoded.
"""
if meta is None:
meta = {}
elif isinstance(meta, bytes):
meta = json.loads(meta.decode())
if isinstance(image, bytes):
image_bytes = image
elif isinstance(image, np.ndarray):
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
image_bytes = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, image_quality])[1].tobytes()
if isinstance(depth, bytes):
depth_bytes = depth
elif isinstance(depth, np.ndarray):
meta['depth_type'] = depth_type
if depth_type == 'linear':
if depth.dtype == np.float16:
depth_format = 'exr'
depth_bytes = cv2.imencode('.exr', depth.astype(np.float32), [cv2.IMWRITE_EXR_TYPE, cv2.IMWRITE_EXR_TYPE_HALF])[1].tobytes()
elif np.issubdtype(depth.dtype, np.floating):
depth_format = 'exr'
depth_bytes = cv2.imencode('.exr', depth.astype(np.float32), [cv2.IMWRITE_EXR_TYPE, cv2.IMWRITE_EXR_TYPE_FLOAT])[1].tobytes()
elif depth.dtype in [np.uint8, np.uint16]:
depth_format = 'png'
depth_bytes = cv2.imencode('.png', depth, [cv2.IMWRITE_PNG_COMPRESSION, png_compression])[1].tobytes()
elif depth_type == 'log':
depth_format = 'png'
depth = depth.astype(np.float32)
near = max(depth[mask].min(), 1e-3)
far = min(depth[mask].max(), near * depth_max_dynamic_range)
depth = ((np.log(depth.clip(near, far) / near) / np.log(far / near)).clip(0, 1) * 65535).astype(np.uint16)
depth_bytes = cv2.imencode('.png', depth, [cv2.IMWRITE_PNG_COMPRESSION, png_compression])[1].tobytes()
meta['depth_near'] = float(near)
meta['depth_far'] = float(far)
elif depth_type == 'disparity':
depth_format = 'png'
depth = depth.astype(np.float32)
depth = 1 / (depth + 1e-12)
depth = (depth / depth[mask].max()).clip(0, 1)
if np.unique(depth) < 200:
depth = (depth * 255).astype(np.uint8)
else:
depth = (depth * 65535).astype(np.uint16)
depth_bytes = cv2.imencode('.png', depth, [cv2.IMWRITE_PNG_COMPRESSION, png_compression])[1].tobytes()
if isinstance(mask, bytes):
mask_bytes = mask
elif isinstance(mask, np.ndarray):
mask_bytes = cv2.imencode('.png', mask.astype(np.uint8) * 255)[1].tobytes()
if segmentation_mask is not None:
if isinstance(segmentation_mask, bytes):
segmentation_mask_bytes = segmentation_mask
else:
segmentation_mask_bytes = cv2.imencode('.png', segmentation_mask)[1].tobytes()
assert segmentation_labels is not None, "You provided a segmentation mask, but not the corresponding labels."
if isinstance(segmentation_labels, bytes):
segmentation_labels = json.loads(segmentation_labels)
meta['segmentation_labels'] = segmentation_labels
if intrinsics is not None:
meta['intrinsics'] = intrinsics.tolist()
if normal is not None:
if isinstance(normal, bytes):
normal_bytes = normal
elif isinstance(normal, np.ndarray):
normal = ((normal * [0.5, -0.5, -0.5] + 0.5).clip(0, 1) * 65535).astype(np.uint16)
normal = cv2.cvtColor(normal, cv2.COLOR_RGB2BGR)
normal_bytes = cv2.imencode('.png', normal, [cv2.IMWRITE_PNG_COMPRESSION, png_compression])[1].tobytes()
if normal_mask is None:
normal_mask = np.ones(image.shape[:2], dtype=bool)
normal_mask_bytes = cv2.imencode('.png', normal_mask.astype(np.uint8) * 255)[1].tobytes()
meta_bytes = meta if isinstance(meta, bytes) else json.dumps(meta).encode()
with zipfile.ZipFile(file, 'w') as z:
z.writestr('meta.json', meta_bytes)
z.writestr('image.jpg', image_bytes)
z.writestr(f'depth.{depth_format}', depth_bytes)
z.writestr('mask.png', mask_bytes)
if segmentation_mask is not None:
z.writestr('segmentation_mask.png', segmentation_mask_bytes)
if normal is not None:
z.writestr('normal.png', normal_bytes)
z.writestr('normal_mask.png', normal_mask_bytes)
def read_rgbd_zip(file: Union[str, Path, IO], return_bytes: bool = False) -> Dict[str, Union[np.ndarray, Dict[str, Any], bytes]]:
"""
Read an RGBD zip file and return the image, depth, mask, segmentation_mask, intrinsics, and meta data.
### Parameters:
- `file: Union[str, Path, IO]`
The file path or file object to read from.
- `return_bytes: bool = False`
If True, return the image, depth, mask, and segmentation_mask as raw bytes.
### Returns:
- `Tuple[Dict[str, Union[np.ndarray, Dict[str, Any]]], Dict[str, bytes]]`
A dictionary containing: (If missing, the value will be None; if return_bytes is True, the value will be bytes)
- `image`: RGB numpy.ndarray of shape (H, W, 3).
- `depth`: float32 numpy.ndarray of shape (H, W).
- `mask`: bool numpy.ndarray of shape (H, W).
- `segformer_mask`: uint8 numpy.ndarray of shape (H, W).
- `intrinsics`: float32 numpy.ndarray of shape (3, 3).
- `meta`: Dict[str, Any].
"""
# Load & extract archive
with zipfile.ZipFile(file, 'r') as z:
meta = z.read('meta.json')
if not return_bytes:
meta = json.loads(z.read('meta.json'))
image = z.read('image.jpg')
if not return_bytes:
image = cv2.imdecode(np.frombuffer(z.read('image.jpg'), np.uint8), cv2.IMREAD_COLOR)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
depth_name = next(s for s in z.namelist() if s.startswith('depth'))
depth = z.read(depth_name)
if not return_bytes:
depth = cv2.imdecode(np.frombuffer(z.read(depth_name), np.uint8), cv2.IMREAD_UNCHANGED)
if 'mask.png' in z.namelist():
mask = z.read('mask.png')
if not return_bytes:
mask = cv2.imdecode(np.frombuffer(z.read('mask.png'), np.uint8), cv2.IMREAD_UNCHANGED) > 0
else:
mask = None
if 'segformer_mask.png' in z.namelist():
# NOTE: Legacy support for segformer_mask.png
segmentation_mask = z.read('segformer_mask.png')
segmentation_labels = None
if not return_bytes:
segmentation_mask = cv2.imdecode(np.frombuffer(segmentation_mask, np.uint8), cv2.IMREAD_UNCHANGED)
segmentation_labels = LEGACY_SEGFORMER_LABELS
elif 'segmentation_mask.png' in z.namelist():
segmentation_mask = z.read('segmentation_mask.png')
segmentation_labels = None
if not return_bytes:
segmentation_mask = cv2.imdecode(np.frombuffer(segmentation_mask, np.uint8), cv2.IMREAD_UNCHANGED)
segmentation_labels = meta['segmentation_labels']
else:
segmentation_mask = None
segmentation_labels = None
if 'normal.png' in z.namelist():
normal = z.read('normal.png')
if not return_bytes:
normal = cv2.imdecode(np.frombuffer(z.read('normal.png'), np.uint8), cv2.IMREAD_UNCHANGED)
normal = cv2.cvtColor(normal, cv2.COLOR_BGR2RGB)
normal = (normal.astype(np.float32) / 65535 - 0.5) * [2.0, -2.0, -2.0]
normal = normal / np.linalg.norm(normal, axis=-1, keepdims=True)
if 'normal_mask.png' in z.namelist():
normal_mask = z.read('normal_mask.png')
normal_mask = cv2.imdecode(np.frombuffer(normal_mask, np.uint8), cv2.IMREAD_UNCHANGED) > 0
else:
normal_mask = np.ones(image.shape[:2], dtype=bool)
else:
normal, normal_mask = None, None
# recover linear depth
if not return_bytes:
if mask is None:
mask = np.ones(image.shape[:2], dtype=bool)
if meta['depth_type'] == 'linear':
depth = depth.astype(np.float32)
mask = mask & (depth > 0)
elif meta['depth_type'] == 'log':
near, far = meta['depth_near'], meta['depth_far']
if depth.dtype == np.uint16:
depth = depth.astype(np.float32) / 65535
elif depth.dtype == np.uint8:
depth = depth.astype(np.float32) / 255
depth = near ** (1 - depth) * far ** depth
mask = mask & ~np.isnan(depth)
elif meta['depth_type'] == 'disparity':
mask = mask & (depth > 0)
if depth.dtype == np.uint16:
depth = depth.astype(np.float32) / 65535
elif depth.dtype == np.uint8:
depth = depth.astype(np.float32) / 255
depth = 1 / (depth + 1e-12)
# intrinsics
if not return_bytes and 'intrinsics' in meta:
intrinsics = np.array(meta['intrinsics'], dtype=np.float32)
else:
intrinsics = None
# depth unit
if not return_bytes and 'depth_unit' in meta:
depth_unit_str = meta['depth_unit']
if r := re.match(r'([\d.]*)(\w*)', depth_unit_str):
digits, unit = r.groups()
depth_unit = float(digits or 1) * {'m': 1, 'cm': 0.01, 'mm': 0.001}[unit]
else:
depth_unit = None
else:
depth_unit = None
return_dict = {
'image': image,
'depth': depth,
'mask': mask,
'segmentation_mask': segmentation_mask,
'segmentation_labels': segmentation_labels,
'normal': normal,
'normal_mask': normal_mask,
'intrinsics': intrinsics,
'depth_unit': depth_unit,
'meta': meta,
}
return_dict = {k: v for k, v in return_dict.items() if v is not None}
return return_dict
def write_rgbxyz(file: Union[IO, Path], image: np.ndarray, points: np.ndarray, mask: np.ndarray = None, image_quality: int = 95):
if isinstance(image, bytes):
image_bytes = image
elif isinstance(image, np.ndarray):
image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
image_bytes = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, image_quality])[1].tobytes()
if isinstance(points, bytes):
points_bytes = points
elif isinstance(points, np.ndarray):
points_bytes = cv2.imencode('.exr', points.astype(np.float32), [cv2.IMWRITE_EXR_TYPE, cv2.IMWRITE_EXR_TYPE_FLOAT])[1].tobytes()
if mask is None:
mask = np.ones(image.shape[:2], dtype=bool)
if isinstance(mask, bytes):
mask_bytes = mask
elif isinstance(mask, np.ndarray):
mask_bytes = cv2.imencode('.png', mask.astype(np.uint8) * 255)[1].tobytes()
is_archive = hasattr(file, 'write') or Path(file).suffix == '.zip'
if is_archive:
with zipfile.ZipFile(file, 'w') as z:
z.writestr('image.jpg', image_bytes)
z.writestr('points.exr', points_bytes)
if mask is not None:
z.writestr('mask.png', mask_bytes)
else:
file = Path(file)
file.mkdir(parents=True, exist_ok=True)
with open(file / 'image.jpg', 'wb') as f:
f.write(image_bytes)
with open(file / 'points.exr', 'wb') as f:
f.write(points_bytes)
if mask is not None:
with open(file / 'mask.png', 'wb') as f:
f.write(mask_bytes)
def read_rgbxyz(file: Union[IO, str, Path]) -> Tuple[np.ndarray, np.ndarray, np.ndarray, Dict[str, Any]]:
is_archive = hasattr(file, 'read') or Path(file).suffix == '.zip'
if is_archive:
with zipfile.ZipFile(file, 'r') as z:
image = cv2.cvtColor(cv2.imdecode(np.frombuffer(z.read('image.jpg'), np.uint8), cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB)
points = cv2.cvtColor(cv2.imdecode(np.frombuffer(z.read('points.exr'), np.uint8), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB)
if 'mask.png' in z.namelist():
mask = cv2.imdecode(np.frombuffer(z.read('mask.png'), np.uint8), cv2.IMREAD_GRAYSCALE) > 0
else:
mask = np.ones(image.shape[:2], dtype=bool)
else:
file = Path(file)
file.mkdir(parents=True, exist_ok=True)
image = cv2.cvtColor(cv2.imread(str(file / 'image.jpg'), cv2.IMREAD_COLOR), cv2.COLOR_BGR2RGB)
points = cv2.cvtColor(cv2.imread(str(file / 'points.exr'), cv2.IMREAD_UNCHANGED), cv2.COLOR_BGR2RGB)
if (file /'mask.png').exists():
mask = cv2.imread(str(file / 'mask.png'), cv2.IMREAD_GRAYSCALE) > 0
else:
mask = np.ones(image.shape[:2], dtype=bool)
return image, points, mask
+503
View File
@@ -0,0 +1,503 @@
from typing import *
from abc import abstractmethod
from queue import Empty, Full
from threading import Thread
from queue import Queue
from multiprocessing import Process
from threading import Thread, Event
import multiprocessing
import threading
import inspect
import time
import uuid
from copy import deepcopy
import itertools
import functools
__all__ = [
'Node',
'Link',
'ConcurrentNode',
'Worker',
'WorkerFunction',
'Provider',
'ProviderFunction',
'Sequential',
'Batch',
'Unbatch',
'Parallel',
'Graph',
'Buffer',
]
TERMINATE_CHECK_INTERVAL = 0.5
class _ItemWrapper:
def __init__(self, data: Any, id: Union[int, List[int]] = None):
self.data = data
self.id = id
class Terminate(Exception):
pass
def _get_queue_item(queue: Queue, terminate_flag: Event, timeout: float = None) -> _ItemWrapper:
while True:
try:
item: _ItemWrapper = queue.get(block=True, timeout=TERMINATE_CHECK_INTERVAL if timeout is None else min(timeout, TERMINATE_CHECK_INTERVAL))
if terminate_flag.is_set():
raise Terminate()
return item
except Empty:
if terminate_flag.is_set():
raise Terminate()
if timeout is not None:
timeout -= TERMINATE_CHECK_INTERVAL
if timeout <= 0:
raise Empty()
def _put_queue_item(queue: Queue, item: _ItemWrapper, terminate_flag: Event):
while True:
try:
queue.put(item, block=True, timeout=TERMINATE_CHECK_INTERVAL)
if terminate_flag.is_set():
raise Terminate()
return
except Full:
if terminate_flag.is_set():
raise Terminate()
class Node:
def __init__(self, in_buffer_size: int = 1, out_buffer_size: int = 1) -> None:
self.input: Queue = Queue(maxsize=in_buffer_size)
self.output: Queue = Queue(maxsize=out_buffer_size)
self.in_buffer_size = in_buffer_size
self.out_buffer_size = out_buffer_size
@abstractmethod
def start(self):
pass
@abstractmethod
def terminate(self):
pass
def stop(self):
self.terminate()
self.join()
@abstractmethod
def join(self):
pass
def put(self, data: Any, key: str = None, block: bool = True) -> None:
item = _ItemWrapper(data)
self.input.put(item, block=block)
def get(self, key: str = None, block: bool = True) -> Any:
item: _ItemWrapper = self.output.get(block=block)
return item.data
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.terminate()
self.join()
class ConcurrentNode(Node):
job: Union[Thread, Process]
def __init__(self, running_as: Literal['thread', 'process'] = 'thread', in_buffer_size: int = 1, out_buffer_size: int = 1) -> None:
super().__init__(in_buffer_size, out_buffer_size)
self.running_as = running_as
@abstractmethod
def _loop_fn(self, input: Queue, output: Queue, terminate_flag: Event):
pass
def start(self):
if self.running_as == 'thread':
terminate_flag = threading.Event()
job = Thread(target=self._loop_fn, args=(self.input, self.output, terminate_flag))
elif self.running_as == 'process':
terminate_flag = multiprocessing.Event()
job = Process(target=self._loop_fn, args=(self.input, self.output, terminate_flag))
job.start()
self.job = job
self.terminate_flag = terminate_flag
def terminate(self):
self.terminate_flag.set()
def join(self):
self.job.join()
class Worker(ConcurrentNode):
def __init__(self, running_as: Literal['thread', 'process'] = 'thread', in_buffer_size: int = 0, out_buffer_size: int = 0) -> None:
super().__init__(running_as, in_buffer_size, out_buffer_size)
def init(self) -> None:
"""
This method is called the the thread is started, to initialize any resources that is only held in the thread.
"""
pass
@abstractmethod
def work(self, *args, **kwargs) -> Union[Any, Dict[str, Any]]:
"""
This method defines the job that the node should do for each input item.
A item obtained from the input queue is passed as arguments to this method, and the result is placed in the output queue.
The method is executed concurrently with other nodes.
"""
pass
def _loop_fn(self, input: Queue, output: Queue, terminate_flag: Event):
self.init()
try:
while True:
item = _get_queue_item(input, terminate_flag)
result = self.work(item.data)
_put_queue_item(output, _ItemWrapper(result, item.id), terminate_flag)
except Terminate:
return
class Provider(ConcurrentNode):
"""
A node that provides data to successive nodes. It takes no input and provides data to the output queue.
"""
def __init__(self, running_as: Literal['thread', 'process'], out_buffer_size: int = 1) -> None:
super().__init__(running_as, 0, out_buffer_size)
def init(self) -> None:
"""
This method is called the the thread or process is started, to initialize any resources that is only held in the thread or process.
"""
pass
@abstractmethod
def provide(self) -> Generator[Any, None, None]:
pass
def _loop_fn(self, input: Queue, output: Queue, terminate_flag: Event):
self.init()
try:
for data in self.provide():
_put_queue_item(output, _ItemWrapper(data), terminate_flag)
except Terminate:
return
class WorkerFunction(Worker):
def __init__(self, fn: Callable, running_as: 'thread', in_buffer_size: int = 1, out_buffer_size: int = 1) -> None:
super().__init__(running_as, in_buffer_size, out_buffer_size)
self.fn = fn
def work(self, *args, **kwargs):
return self.fn(*args, **kwargs)
class ProviderFunction(Provider):
def __init__(self, fn: Callable, running_as: 'thread', out_buffer_size: int = 1) -> None:
super().__init__(running_as, out_buffer_size)
self.fn = fn
def provide(self):
for item in self.fn():
yield item
class Link:
def __init__(self, src: Queue, dst: Queue):
self.src = src
self.dst = dst
def _thread_fn(self):
try:
while True:
item = _get_queue_item(self.src, self.terminate_flag)
_put_queue_item(self.dst, item, self.terminate_flag)
except Terminate:
return
def start(self):
self.terminate_flag = threading.Event()
self.thread = Thread(target=self._thread_fn)
self.thread.start()
def terminate(self):
self.terminate_flag.set()
def join(self):
self.thread.join()
class Graph(Node):
"""
Graph pipeline of nodes and links
"""
nodes: List[Node]
links: List[Link]
def __init__(self, in_buffer_size: int = 1, out_buffer_size: int = 1):
super().__init__(in_buffer_size, out_buffer_size)
self.nodes = []
self.links = []
def add(self, node: Node):
self.nodes.append(node)
def link(self, src: Union[Node, Tuple[Node, str]], dst: Union[Node, Tuple[Node, str]]):
"""
Links the output of the source node to the input of the destination node.
If the source or destination node is None, the pipeline's input or output is used.
"""
src_queue = self.input if src is None else src.output
dst_queue = self.output if dst is None else dst.input
self.links.append(Link(src_queue, dst_queue))
def chain(self, nodes: Iterable[Node]):
"""
Link the output of each node to the input of the next node.
"""
nodes = list(nodes)
for i in range(len(nodes) - 1):
self.link(nodes[i], nodes[i + 1])
def start(self):
for node in self.nodes:
node.start()
for link in self.links:
link.start()
def terminate(self):
for node in self.nodes:
node.terminate()
for link in self.links:
link.terminate()
def join(self):
for node in self.nodes:
node.join()
for link in self.links:
link.join()
def __iter__(self):
providers = [node for node in self.nodes if isinstance(node, Provider)]
if len(providers) == 0:
raise ValueError("No provider node found in the pipeline. If you want to iterate over the pipeline, the pipeline must be driven by a provider node.")
with self:
# while all(provider.job.is_alive() for provider in providers):
while True:
yield self.get()
def __call__(self, data: Any) -> Any:
"""
Submit data to the pipeline's input queue, and return the output data asynchronously.
NOTE: The pipeline must be streamed (i.e., every output item is uniquely associated with an input item) for this to work.
"""
# TODO
class Sequential(Graph):
"""
Pipeline of nodes in sequential order, where each node takes the output of the previous node as input.
The order of input and output items is preserved (FIFO)
"""
def __init__(self, nodes: List[Union[Node, Callable]], function_running_as: Literal['thread', 'process'] = 'thread', in_buffer_size: int = 1, out_buffer_size: int = 1):
"""
Initialize the pipeline with a list of nodes to execute sequentially.
### Parameters:
- nodes: List of nodes or functions to execute sequentially. Generator functions are wrapped in provider nodes, and other functions are wrapped in worker nodes.
- function_running_as: Whether to wrap the function as a thread or process worker. Default is 'thread'.
- in_buffer_size: Maximum size of the input queue of the pipeline. Default is 0 (unlimited).
- out_buffer_size: Maximum size of the output queue of the pipeline. Default is 0 (unlimited).
"""
super().__init__(in_buffer_size, out_buffer_size)
for node in nodes:
if isinstance(node, Node):
pass
elif isinstance(node, Callable):
if inspect.isgeneratorfunction(node):
node = ProviderFunction(node, function_running_as)
else:
node = WorkerFunction(node, function_running_as)
else:
raise ValueError(f"Invalid node type: {type(node)}")
self.add(node)
self.chain([None, *self.nodes, None])
class Parallel(Node):
"""
A FIFO node that runs multiple nodes in parallel to process the input items. Each input item is handed to one of the nodes whoever is available.
NOTE: It is FIFO if and only if all the nested nodes are FIFO.
"""
nodes: List[Node]
def __init__(self, nodes: Iterable[Node], in_buffer_size: int = 1, out_buffer_size: int = 1, function_running_as: Literal['thread', 'process'] = 'thread'):
super().__init__(in_buffer_size, out_buffer_size)
self.nodes = []
for node in nodes:
if isinstance(node, Node):
pass
elif isinstance(node, Callable):
if inspect.isgeneratorfunction(node):
node = ProviderFunction(node, function_running_as)
else:
node = WorkerFunction(node, function_running_as)
else:
raise ValueError(f"Invalid node type: {type(node)}")
self.nodes.append(node)
self.output_order = Queue()
self.lock = threading.Lock()
def _in_thread_fn(self, node: Node):
try:
while True:
with self.lock:
# A better idea: first make sure its node is vacant, then get it a new item.
# Currently we will not be able to know which node is busy util there is at least one item already waiting in the queue of the node.
# This could lead to suboptimal scheduling.
item = _get_queue_item(self.input, self.terminate_flag)
self.output_order.put(node.output)
_put_queue_item(node.input, item, self.terminate_flag)
except Terminate:
return
def _out_thread_fn(self):
try:
while True:
queue = _get_queue_item(self.output_order, self.terminate_flag)
item = _get_queue_item(queue, self.terminate_flag)
_put_queue_item(self.output, item, self.terminate_flag)
except Terminate:
return
def start(self):
self.terminate_flag = threading.Event()
self.in_threads = []
for node in self.nodes:
thread = Thread(target=self._in_thread_fn, args=(node,))
thread.start()
self.in_threads.append(thread)
thread = Thread(target=self._out_thread_fn)
thread.start()
self.out_thread = thread
for node in self.nodes:
node.start()
def terminate(self):
self.terminate_flag.set()
for node in self.nodes:
node.terminate()
def join(self):
for thread in self.in_threads:
thread.join()
self.out_thread.join()
class UnorderedParallel(Graph):
"""
Pipeline of nodes in parallel, where each input item is handed to one of the nodes whoever is available.
NOTE: The order of the output items is NOT guaranteed to be the same as the input items, depending on how fast the nodes handle their input.
"""
def __init__(self, nodes: List[Union[Node, Callable]], function_running_as: Literal['thread', 'process'] = 'thread', in_buffer_size: int = 1, out_buffer_size: int = 1):
"""
Initialize the pipeline with a list of nodes to execute in parallel. If a function is given, it is wrapped in a worker node.
### Parameters:
- nodes: List of nodes or functions to execute in parallel. Generator functions are wrapped in provider nodes, and other functions are wrapped in worker nodes.
- function_running_as: Whether to wrap the function as a thread or process worker. Default is 'thread'.
- in_buffer_size: Maximum size of the input queue of the pipeline. Default is 0 (unlimited).
- out_buffer_size: Maximum size of the output queue of the pipeline. Default is 0 (unlimited).
"""
super().__init__(in_buffer_size, out_buffer_size)
for node in nodes:
if isinstance(node, Node):
pass
elif isinstance(node, Callable):
if inspect.isgeneratorfunction(node):
node = ProviderFunction(node, function_running_as)
else:
node = WorkerFunction(node, function_running_as)
else:
raise ValueError(f"Invalid node type: {type(node)}")
self.add(node)
for i in range(len(nodes)):
self.chain([None, self.nodes[i], None])
class Batch(ConcurrentNode):
"""
Groups every `batch_size` items into a batch (a list of items) and passes the batch to successive nodes.
The `patience` parameter specifies the maximum time to wait for a batch to be filled before sending it to the next node,
i.e., when the earliest item in the batch is out of `patience` seconds, the batch is sent regardless of its size.
"""
def __init__(self, batch_size: int, patience: float = None, in_buffer_size: int = 1, out_buffer_size: int = 1):
assert batch_size > 0, "Batch size must be greater than 0."
super().__init__('thread', in_buffer_size, out_buffer_size)
self.batch_size = batch_size
self.patience = patience
def _loop_fn(self, input: Queue, output: Queue, terminate_flag: Event):
try:
while True:
batch_id, batch_data = [], []
# Try to fill the batch
for i in range(self.batch_size):
if i == 0 or self.patience is None:
timeout = None
else:
timeout = self.patience - (time.time() - earliest_time)
if timeout < 0:
break
try:
item = _get_queue_item(input, terminate_flag, timeout)
except Empty:
break
if i == 0:
earliest_time = time.time()
batch_data.append(item.data)
batch_id.append(item.id)
batch = _ItemWrapper(batch_data, batch_id)
_put_queue_item(output, batch, terminate_flag)
except Terminate:
return
class Unbatch(ConcurrentNode):
"""
Ungroups every batch (a list of items) into individual items and passes them to successive nodes.
"""
def __init__(self, in_buffer_size: int = 1, out_buffer_size: int = 1):
super().__init__('thread', in_buffer_size, out_buffer_size)
def _loop_fn(self, input: Queue, output: Queue, terminate_flag: Event):
try:
while True:
batch = _get_queue_item(input, terminate_flag)
for id, data in zip(batch.id or itertools.repeat(None), batch.data):
item = _ItemWrapper(data, id)
_put_queue_item(output, item, terminate_flag)
except Terminate:
return
class Buffer(Node):
"A FIFO node that buffers items in a queue. Usefull achieve better temporal balance when its successor node has a variable processing time."
def __init__(self, size: int):
super().__init__(size, size)
self.size = size
self.input = self.output = Queue(maxsize=size)
+240
View File
@@ -0,0 +1,240 @@
from typing import *
import time
from pathlib import Path
from numbers import Number
def catch_exception(fn):
def wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except Exception as e:
import traceback
print(f"Exception in {fn.__name__}({', '.join(repr(arg) for arg in args)}, {', '.join(f'{k}={v!r}' for k, v in kwargs.items())})")
traceback.print_exc(chain=False)
time.sleep(0.1)
return None
return wrapper
class CallbackOnException:
def __init__(self, callback: Callable, exception: type):
self.exception = exception
self.callback = callback
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if isinstance(exc_val, self.exception):
self.callback()
return True
return False
def traverse_nested_dict_keys(d: Dict[str, Dict]) -> Generator[Tuple[str, ...], None, None]:
for k, v in d.items():
if isinstance(v, dict):
for sub_key in traverse_nested_dict_keys(v):
yield (k, ) + sub_key
else:
yield (k, )
def get_nested_dict(d: Dict[str, Dict], keys: Tuple[str, ...], default: Any = None):
for k in keys:
d = d.get(k, default)
if d is None:
break
return d
def set_nested_dict(d: Dict[str, Dict], keys: Tuple[str, ...], value: Any):
for k in keys[:-1]:
d = d.setdefault(k, {})
d[keys[-1]] = value
def key_average(list_of_dicts: list) -> Dict[str, Any]:
"""
Returns a dictionary with the average value of each key in the input list of dictionaries.
"""
_nested_dict_keys = set()
for d in list_of_dicts:
_nested_dict_keys.update(traverse_nested_dict_keys(d))
_nested_dict_keys = sorted(_nested_dict_keys)
result = {}
for k in _nested_dict_keys:
values = [
get_nested_dict(d, k) for d in list_of_dicts
if get_nested_dict(d, k) is not None
]
avg = sum(values) / len(values) if values else float('nan')
set_nested_dict(result, k, avg)
return result
def flatten_nested_dict(d: Dict[str, Any], parent_key: Tuple[str, ...] = None) -> Dict[Tuple[str, ...], Any]:
"""
Flattens a nested dictionary into a single-level dictionary, with keys as tuples.
"""
items = []
if parent_key is None:
parent_key = ()
for k, v in d.items():
new_key = parent_key + (k, )
if isinstance(v, MutableMapping):
items.extend(flatten_nested_dict(v, new_key).items())
else:
items.append((new_key, v))
return dict(items)
def unflatten_nested_dict(d: Dict[str, Any]) -> Dict[str, Any]:
"""
Unflattens a single-level dictionary into a nested dictionary, with keys as tuples.
"""
result = {}
for k, v in d.items():
sub_dict = result
for k_ in k[:-1]:
if k_ not in sub_dict:
sub_dict[k_] = {}
sub_dict = sub_dict[k_]
sub_dict[k[-1]] = v
return result
def read_jsonl(file):
import json
with open(file, 'r') as f:
data = f.readlines()
return [json.loads(line) for line in data]
def write_jsonl(data: List[dict], file):
import json
with open(file, 'w') as f:
for item in data:
f.write(json.dumps(item) + '\n')
def save_metrics(save_path: Union[str, Path], all_metrics: Dict[str, List[Dict]]):
import pandas as pd
import json
with open(save_path, 'w') as f:
json.dump(all_metrics, f, indent=4)
def to_hierachical_dataframe(data: List[Dict[Tuple[str, ...], Any]]):
import pandas as pd
data = [flatten_nested_dict(d) for d in data]
df = pd.DataFrame(data)
df = df.sort_index(axis=1)
df.columns = pd.MultiIndex.from_tuples(df.columns)
return df
def recursive_replace(d: Union[List, Dict, str], mapping: Dict[str, str]):
if isinstance(d, str):
for old, new in mapping.items():
d = d.replace(old, new)
elif isinstance(d, list):
for i, item in enumerate(d):
d[i] = recursive_replace(item, mapping)
elif isinstance(d, dict):
for k, v in d.items():
d[k] = recursive_replace(v, mapping)
return d
class timeit:
_history: Dict[str, List['timeit']] = {}
def __init__(self, name: str = None, verbose: bool = True, multiple: bool = False):
self.name = name
self.verbose = verbose
self.start = None
self.end = None
self.multiple = multiple
if multiple and name not in timeit._history:
timeit._history[name] = []
def __call__(self, func: Callable):
import inspect
if inspect.iscoroutinefunction(func):
async def wrapper(*args, **kwargs):
with timeit(self.name or func.__qualname__):
ret = await func(*args, **kwargs)
return ret
return wrapper
else:
def wrapper(*args, **kwargs):
with timeit(self.name or func.__qualname__):
ret = func(*args, **kwargs)
return ret
return wrapper
def __enter__(self):
self.start = time.time()
@property
def time(self) -> float:
assert self.start is not None, "Time not yet started."
assert self.end is not None, "Time not yet ended."
return self.end - self.start
@property
def history(self) -> List['timeit']:
return timeit._history.get(self.name, [])
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.time()
if self.multiple:
timeit._history[self.name].append(self)
if self.verbose:
if self.multiple:
avg = sum(t.time for t in timeit._history[self.name]) / len(timeit._history[self.name])
print(f"{self.name or 'It'} took {avg} seconds in average.")
else:
print(f"{self.name or 'It'} took {self.time} seconds.")
def strip_common_prefix_suffix(strings: List[str]) -> List[str]:
first = strings[0]
for start in range(len(first)):
if any(s[start] != strings[0][start] for s in strings):
break
for end in range(1, min(len(s) for s in strings)):
if any(s[-end] != first[-end] for s in strings):
break
return [s[start:len(s) - end + 1] for s in strings]
def multithead_execute(inputs: List[Any], num_workers: int, pbar = None):
from concurrent.futures import ThreadPoolExecutor
from contextlib import nullcontext
from tqdm import tqdm
if pbar is not None:
pbar.total = len(inputs) if hasattr(inputs, '__len__') else None
else:
pbar = tqdm(total=len(inputs) if hasattr(inputs, '__len__') else None)
def decorator(fn: Callable):
with (
ThreadPoolExecutor(max_workers=num_workers) as executor,
pbar
):
pbar.refresh()
@catch_exception
def _fn(input):
ret = fn(input)
pbar.update()
return ret
executor.map(_fn, inputs)
executor.shutdown(wait=True)
return decorator
+51
View File
@@ -0,0 +1,51 @@
import numpy as np
import matplotlib
def colorize_depth(depth: np.ndarray, mask: np.ndarray = None, normalize: bool = True, cmap: str = 'Spectral') -> np.ndarray:
if mask is None:
depth = np.where(depth > 0, depth, np.nan)
else:
depth = np.where((depth > 0) & mask, depth, np.nan)
disp = 1 / depth
if normalize:
min_disp, max_disp = np.nanquantile(disp, 0.001), np.nanquantile(disp, 0.999)
disp = (disp - min_disp) / (max_disp - min_disp)
colored = np.nan_to_num(matplotlib.colormaps[cmap](1.0 - disp), 0)
colored = (colored.clip(0, 1) * 255).astype(np.uint8)[:, :, :3]
return colored
def colorize_depth_affine(depth: np.ndarray, mask: np.ndarray = None, cmap: str = 'Spectral') -> np.ndarray:
if mask is not None:
depth = np.where(mask, depth, np.nan)
min_depth, max_depth = np.nanquantile(depth, 0.001), np.nanquantile(depth, 0.999)
depth = (depth - min_depth) / (max_depth - min_depth)
colored = np.nan_to_num(matplotlib.colormaps[cmap](depth), 0)
colored = (colored.clip(0, 1) * 255).astype(np.uint8)[:, :, :3]
return colored
def colorize_disparity(disparity: np.ndarray, mask: np.ndarray = None, normalize: bool = True, cmap: str = 'Spectral') -> np.ndarray:
if mask is not None:
disparity = np.where(mask, disparity, np.nan)
if normalize:
min_disp, max_disp = np.nanquantile(disparity, 0.001), np.nanquantile(disparity, 0.999)
disparity = (disparity - min_disp) / (max_disp - min_disp)
colored = np.nan_to_num(matplotlib.colormaps[cmap](1.0 - disparity), 0)
colored = (colored.clip(0, 1) * 255).astype(np.uint8)[:, :, :3]
return colored
def colorize_segmentation(segmentation: np.ndarray, cmap: str = 'Set1') -> np.ndarray:
colored = matplotlib.colormaps[cmap]((segmentation % 20) / 20)
colored = (colored.clip(0, 1) * 255).astype(np.uint8)[:, :, :3]
return colored
def colorize_normal(normal: np.ndarray) -> np.ndarray:
normal = normal * [0.5, -0.5, -0.5] + 0.5
normal = (normal.clip(0, 1) * 255).astype(np.uint8)
return normal
+73
View File
@@ -0,0 +1,73 @@
import requests
from typing import *
__all__ = ["WebFile"]
class WebFile:
def __init__(self, url: str, session: Optional[requests.Session] = None, headers: Optional[Dict[str, str]] = None, size: Optional[int] = None):
self.url = url
self.session = session or requests.Session()
self.session.headers.update(headers or {})
self._offset = 0
self.size = size if size is not None else self._fetch_size()
def _fetch_size(self):
with self.session.get(self.url, stream=True) as response:
response.raise_for_status()
content_length = response.headers.get("Content-Length")
if content_length is None:
raise ValueError("Missing Content-Length in header")
return int(content_length)
def _fetch_data(self, offset: int, n: int) -> bytes:
headers = {"Range": f"bytes={offset}-{min(offset + n - 1, self.size)}"}
response = self.session.get(self.url, headers=headers)
response.raise_for_status()
return response.content
def seekable(self) -> bool:
return True
def tell(self) -> int:
return self._offset
def available(self) -> int:
return self.size - self._offset
def seek(self, offset: int, whence: int = 0) -> None:
if whence == 0:
new_offset = offset
elif whence == 1:
new_offset = self._offset + offset
elif whence == 2:
new_offset = self.size + offset
else:
raise ValueError("Invalid value for whence")
self._offset = max(0, min(new_offset, self.size))
def read(self, n: Optional[int] = None) -> bytes:
if n is None or n < 0:
n = self.available()
else:
n = min(n, self.available())
if n == 0:
return b''
data = self._fetch_data(self._offset, n)
self._offset += len(data)
return data
def close(self) -> None:
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
pass
+128
View File
@@ -0,0 +1,128 @@
from typing import *
import io
import os
from zipfile import (
ZipInfo, BadZipFile, ZipFile, ZipExtFile,
sizeFileHeader, structFileHeader, stringFileHeader,
_FH_SIGNATURE, _FH_FILENAME_LENGTH, _FH_EXTRA_FIELD_LENGTH, _FH_GENERAL_PURPOSE_FLAG_BITS,
_MASK_COMPRESSED_PATCH, _MASK_STRONG_ENCRYPTION, _MASK_UTF_FILENAME, _MASK_ENCRYPTED
)
import struct
from requests import Session
from .webfile import WebFile
class _SharedWebFile(WebFile):
def __init__(self, webfile: WebFile, pos: int):
super().__init__(webfile.url, webfile.session, size=webfile.size)
self.seek(pos)
class WebZipFile(ZipFile):
"Lock-free version of ZipFile that reads from a WebFile, allowing for concurrent reads."
def __init__(self, url: str, session: Optional[Session] = None, headers: Optional[Dict[str, str]] = None):
"""Open the ZIP file with mode read 'r', write 'w', exclusive create 'x',
or append 'a'."""
webf = WebFile(url, session=session, headers=headers)
super().__init__(webf, mode='r')
def open(self, name, mode="r", pwd=None, *, force_zip64=False):
"""Return file-like object for 'name'.
name is a string for the file name within the ZIP file, or a ZipInfo
object.
mode should be 'r' to read a file already in the ZIP file, or 'w' to
write to a file newly added to the archive.
pwd is the password to decrypt files (only used for reading).
When writing, if the file size is not known in advance but may exceed
2 GiB, pass force_zip64 to use the ZIP64 format, which can handle large
files. If the size is known in advance, it is best to pass a ZipInfo
instance for name, with zinfo.file_size set.
"""
if mode not in {"r", "w"}:
raise ValueError('open() requires mode "r" or "w"')
if pwd and (mode == "w"):
raise ValueError("pwd is only supported for reading files")
if not self.fp:
raise ValueError(
"Attempt to use ZIP archive that was already closed")
assert mode == "r", "Only read mode is supported for now"
# Make sure we have an info object
if isinstance(name, ZipInfo):
# 'name' is already an info object
zinfo = name
elif mode == 'w':
zinfo = ZipInfo(name)
zinfo.compress_type = self.compression
zinfo._compresslevel = self.compresslevel
else:
# Get info object for name
zinfo = self.getinfo(name)
if mode == 'w':
return self._open_to_write(zinfo, force_zip64=force_zip64)
if self._writing:
raise ValueError("Can't read from the ZIP file while there "
"is an open writing handle on it. "
"Close the writing handle before trying to read.")
# Open for reading:
self._fileRefCnt += 1
zef_file = _SharedWebFile(self.fp, zinfo.header_offset)
try:
# Skip the file header:
fheader = zef_file.read(sizeFileHeader)
if len(fheader) != sizeFileHeader:
raise BadZipFile("Truncated file header")
fheader = struct.unpack(structFileHeader, fheader)
if fheader[_FH_SIGNATURE] != stringFileHeader:
raise BadZipFile("Bad magic number for file header")
fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
if fheader[_FH_EXTRA_FIELD_LENGTH]:
zef_file.seek(fheader[_FH_EXTRA_FIELD_LENGTH], whence=1)
if zinfo.flag_bits & _MASK_COMPRESSED_PATCH:
# Zip 2.7: compressed patched data
raise NotImplementedError("compressed patched data (flag bit 5)")
if zinfo.flag_bits & _MASK_STRONG_ENCRYPTION:
# strong encryption
raise NotImplementedError("strong encryption (flag bit 6)")
if fheader[_FH_GENERAL_PURPOSE_FLAG_BITS] & _MASK_UTF_FILENAME:
# UTF-8 filename
fname_str = fname.decode("utf-8")
else:
fname_str = fname.decode(self.metadata_encoding or "cp437")
if fname_str != zinfo.orig_filename:
raise BadZipFile(
'File name in directory %r and header %r differ.'
% (zinfo.orig_filename, fname))
# check for encrypted flag & handle password
is_encrypted = zinfo.flag_bits & _MASK_ENCRYPTED
if is_encrypted:
if not pwd:
pwd = self.pwd
if pwd and not isinstance(pwd, bytes):
raise TypeError("pwd: expected bytes, got %s" % type(pwd).__name__)
if not pwd:
raise RuntimeError("File %r is encrypted, password "
"required for extraction" % name)
else:
pwd = None
return ZipExtFile(zef_file, mode, zinfo, pwd, True)
except:
zef_file.close()
raise
+213
View File
@@ -0,0 +1,213 @@
import os
import torch
import folder_paths
import comfy.model_management as mm
from comfy.utils import ProgressBar, load_torch_file
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__)
from .moge.model import MoGeModel
from .utils3d.numpy import image_mesh, image_uv, depth_edge
import trimesh
import numpy as np
from pathlib import Path
import uuid
import tempfile
from PIL import Image
from contextlib import nullcontext
try:
from accelerate import init_empty_weights
from accelerate.utils import set_module_tensor_to_device
is_accelerate_available = True
except:
is_accelerate_available = False
pass
script_directory = os.path.dirname(os.path.abspath(__file__))
#region ModelLoading
class DownloadAndLoadMoGeModel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": (
[
"MoGe_ViT_L_fp16.safetensors",
"MoGe_ViT_L_fp32.safetensors",
],
{"tooltip": "Downloads from 'https://huggingface.co/Kijai/MoGe_safetensors' to 'models/MoGe'", },
),
"precision": (["fp16", "fp32", "bf16"],
{"default": "fp32", "tooltip": "The precision to use for the model weights. Has no effect with GGUF models"},),
},
}
RETURN_TYPES = ("MOGEMODEL",)
RETURN_NAMES = ("mochi_model", )
FUNCTION = "loadmodel"
CATEGORY = "MoGe"
DESCRIPTION = "Downloads and loads the selected MoGe model from Huggingface"
def loadmodel(self, model, precision):
device = mm.get_torch_device()
offload_device = mm.unet_offload_device()
mm.soft_empty_cache()
dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[precision]
model_download_path = os.path.join(folder_paths.models_dir, 'MoGe')
model_path = os.path.join(model_download_path, model)
repo_id = "kijai/MoGE_safetensors"
if not os.path.exists(model_path):
log.info(f"Downloading mochi model to: {model_path}")
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=repo_id,
allow_patterns=[f"*{model}*"],
local_dir=model_download_path,
local_dir_use_symlinks=False,
)
model_config = {
'encoder': 'dinov2_vitl14',
'remap_output':
'exp',
'output_mask': True,
'split_head': True,
'intermediate_layers': 4,
'dim_upsample': [256, 128, 64],
'dim_times_res_block_hidden': 2,
'num_res_blocks': 2,
'trained_area_range': [250000, 500000],
'last_conv_channels': 32,
'last_conv_size': 1
}
with (init_empty_weights() if is_accelerate_available else nullcontext()):
model = MoGeModel(**model_config)
model_sd = load_torch_file(model_path)
if is_accelerate_available:
for key in model_sd:
set_module_tensor_to_device(model, key, dtype=dtype, device=device, value=model_sd[key])
else:
model.load_state_dict(model_sd, strict=True)
model.to(dtype).to(device)
model.eval()
del model_sd
return (model,)
class MoGeProcess:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MOGEMODEL",),
"image": ("IMAGE",),
"resolution_level": ("INT", {"default": 9}),
"remove_edge": ("BOOLEAN", {"default": True}),
"metallic_factor": ("FLOAT", {"default": 0.5, "step": 0.01}),
"roughness_factor": ("FLOAT", {"default": 1.0, "step": 0.01}),
"output_format": (["glb", "ply"], {"default": "glb"}),
"filename_prefix": ("STRING", {"default": "3D/MoGe"}),
},
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("depth", )
FUNCTION = "process"
CATEGORY = "MoGe"
OUTPUT_NODE = True
DESCRIPTION = "Runs the MoGe model on the input image"
def process(self, model, image, resolution_level, remove_edge, metallic_factor, roughness_factor, output_format, filename_prefix):
device = mm.get_torch_device()
offload_device = mm.unet_offload_device()
mm.soft_empty_cache()
B, H, W, C = image.shape
input_tensor = image.permute(0, 3, 1, 2).to(device)
# model infer
output = model.infer(input_tensor[0], resolution_level=resolution_level, apply_mask=True)
# tensor outputs
points_tensor = output['points']
depth_tensor = output['depth']
mask_tensor = output['mask']
# convert to np
points_np = points_tensor.cpu().numpy()
depth_np = depth_tensor.cpu().numpy()
mask_np = mask_tensor.cpu().numpy()
input_np = image.cpu().numpy().astype(np.float32)
print(input_np[0].shape)
faces, vertices, vertex_colors, vertex_uvs = image_mesh(
points_np,
input_np[0],
image_uv(width=W, height=H),
mask=mask_np & ~depth_edge(depth_np, mask=mask_np, rtol=0.02) if remove_edge else mask_np,
tri=True
)
vertices, vertex_uvs = vertices * [1, -1, -1], vertex_uvs * [1, -1] + [0, 1]
run_id = str(uuid.uuid4())
tempdir = folder_paths.get_temp_directory()
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, folder_paths.get_output_directory())
if output_format == 'glb':
output_glb_path = Path(full_output_folder, f'{filename}_{counter:05}_.glb')
output_glb_path.parent.mkdir(exist_ok=True)
trimesh.Trimesh(
vertices=vertices * [-1, 1, -1], # No idea why Gradio 3D Viewer' default camera is flipped
faces=faces,
visual = trimesh.visual.texture.TextureVisuals(
uv=vertex_uvs,
material=trimesh.visual.material.PBRMaterial(
baseColorTexture=Image.fromarray((input_np[0] * 255).astype(np.uint8)),
metallicFactor=metallic_factor,
roughnessFactor=roughness_factor
)
),
process=False
).export(output_glb_path)
elif output_format == 'ply':
output_ply_path = Path(tempdir, f'{run_id}.ply')
output_ply_path.parent.mkdir(exist_ok=True)
trimesh.Trimesh(
vertices=vertices,
faces=faces,
vertex_colors=vertex_colors,
process=False
).export(output_ply_path)
counter += 1
depth_min = depth_tensor.min()
depth_max = depth_tensor.max()
depth_tensor = 1 - ((depth_tensor - depth_min) / (depth_max - depth_min))
depth_tensor = depth_tensor.unsqueeze(0).cpu().float()
return depth_tensor,
#endregion
#region NodeMappings
NODE_CLASS_MAPPINGS = {
"DownloadAndLoadMoGeModel": DownloadAndLoadMoGeModel,
"MoGeProcess": MoGeProcess,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DownloadAndLoadMoGeModel": "(Down)load MoGe Model",
"MoGeProcess": "MoGe Process",
}
+3
View File
@@ -0,0 +1,3 @@
# utils3d
This is a collection of utility functions for 3D computer vision tasks copied from https://github.com/EasternJournalist/utils3d.
+14
View File
@@ -0,0 +1,14 @@
"""
A package for common utility functions in 3D computer graphics and vision. Providing NumPy utilities in `utils3d.numpy`, PyTorch utilities in `utils3d.torch`, and IO utilities in `utils3d.io`.
"""
import importlib
__all__ = ['numpy', 'torch', 'io']
def __getattr__(module_name: str):
return importlib.import_module(f'.{module_name}', __package__)
if __name__ == '__main__':
from . import torch
from . import numpy
from . import io
+4
View File
@@ -0,0 +1,4 @@
from .wavefront_obj import *
from .colmap import *
from .ply import *
from .glb import *
+139
View File
@@ -0,0 +1,139 @@
from typing import *
from pathlib import Path
import numpy as np
from scipy.spatial.transform import Rotation
__all__ = ['read_extrinsics_from_colmap', 'read_intrinsics_from_colmap', 'write_extrinsics_as_colmap', 'write_intrinsics_as_colmap']
def write_extrinsics_as_colmap(file: Union[str, Path], extrinsics: np.ndarray, image_names: Union[str, List[str]] = 'image_{i:04d}.png', camera_ids: List[int] = None):
"""
Write extrinsics to colmap `images.txt` file.
Args:
file: Path to `images.txt` file.
extrinsics: (N, 4, 4) array of extrinsics.
image_names: str or List of str, image names. Length is N.
If str, it should be a format string with `i` as the index. (i starts from 1, in correspondence with IMAGE_ID in colmap)
camera_ids: List of int, camera ids. Length is N.
If None, it will be set to [1, 2, ..., N].
"""
assert extrinsics.shape[1:] == (4, 4) and extrinsics.ndim == 3 or extrinsics.shape == (4, 4)
if extrinsics.ndim == 2:
extrinsics = extrinsics[np.newaxis, ...]
quats = Rotation.from_matrix(extrinsics[:, :3, :3]).as_quat()
trans = extrinsics[:, :3, 3]
if camera_ids is None:
camera_ids = list(range(1, len(extrinsics) + 1))
if isinstance(image_names, str):
image_names = [image_names.format(i=i) for i in range(1, len(extrinsics) + 1)]
assert len(extrinsics) == len(image_names) == len(camera_ids), \
f'Number of extrinsics ({len(extrinsics)}), image_names ({len(image_names)}), and camera_ids ({len(camera_ids)}) must be the same'
with open(file, 'w') as fp:
print("# IMAGE_ID, QW, QX, QY, QZ, TX, TY, TZ, CAMERA_ID, NAME", file=fp)
for i, (quat, t, name, camera_id) in enumerate(zip(quats.tolist(), trans.tolist(), image_names, camera_ids)):
# Colmap has wxyz order while scipy.spatial.transform.Rotation has xyzw order. Haha, wcnm.
qx, qy, qz, qw = quat
tx, ty, tz = t
print(f'{i + 1} {qw:f} {qx:f} {qy:f} {qz:f} {tx:f} {ty:f} {tz:f} {camera_id:d} {name}', file=fp)
print()
def write_intrinsics_as_colmap(file: Union[str, Path], intrinsics: np.ndarray, width: int, height: int, normalized: bool = False):
"""
Write intrinsics to colmap `cameras.txt` file. Currently only support PINHOLE model (no distortion)
Args:
file: Path to `cameras.txt` file.
intrinsics: (N, 3, 3) array of intrinsics.
width: Image width.
height: Image height.
normalized: Whether the intrinsics are normalized. If True, the intrinsics will unnormalized for writing.
"""
assert intrinsics.shape[1:] == (3, 3) and intrinsics.ndim == 3 or intrinsics.shape == (3, 3)
if intrinsics.ndim == 2:
intrinsics = intrinsics[np.newaxis, ...]
if normalized:
intrinsics = intrinsics * np.array([width, height, 1])[:, None]
with open(file, 'w') as fp:
print("# CAMERA_ID, MODEL, WIDTH, HEIGHT, PARAMS[]", file=fp)
for i, intr in enumerate(intrinsics):
fx, fy, cx, cy = intr[0, 0], intr[1, 1], intr[0, 2], intr[1, 2]
print(f'{i + 1} PINHOLE {width:d} {height:d} {fx:f} {fy:f} {cx:f} {cy:f}', file=fp)
def read_extrinsics_from_colmap(file: Union[str, Path]) -> Union[np.ndarray, List[int], List[str]]:
"""
Read extrinsics from colmap `images.txt` file.
Args:
file: Path to `images.txt` file.
Returns:
extrinsics: (N, 4, 4) array of extrinsics.
camera_ids: List of int, camera ids. Length is N. Note that camera ids in colmap typically starts from 1.
image_names: List of str, image names. Length is N.
"""
with open(file) as fp:
lines = fp.readlines()
image_names, quats, trans, camera_ids = [], [], [], []
i_line = 0
for line in lines:
line = line.strip()
if line.startswith('#'):
continue
i_line += 1
if i_line % 2 == 0:
continue
image_id, qw, qx, qy, qz, tx, ty, tz, camera_id, name = line.split()
quats.append([float(qx), float(qy), float(qz), float(qw)])
trans.append([float(tx), float(ty), float(tz)])
camera_ids.append(int(camera_id))
image_names.append(name)
quats = np.array(quats, dtype=np.float32)
trans = np.array(trans, dtype=np.float32)
rotation = Rotation.from_quat(quats).as_matrix()
extrinsics = np.concatenate([
np.concatenate([rotation, trans[..., None]], axis=-1),
np.array([0, 0, 0, 1], dtype=np.float32)[None, None, :].repeat(len(quats), axis=0)
], axis=-2)
return extrinsics, camera_ids, image_names
def read_intrinsics_from_colmap(file: Union[str, Path], normalize: bool = False) -> Tuple[List[int], np.ndarray, np.ndarray]:
"""
Read intrinsics from colmap `cameras.txt` file.
Args:
file: Path to `cameras.txt` file.
normalize: Whether to normalize the intrinsics. If True, the intrinsics will be normalized. (mapping coordinates to [0, 1] range)
Returns:
camera_ids: List of int, camera ids. Length is N. Note that camera ids in colmap typically starts from 1.
intrinsics: (N, 3, 3) array of intrinsics.
distortions: (N, 5) array of distortions.
"""
with open(file) as fp:
lines = fp.readlines()
intrinsics, distortions, camera_ids = [], [], []
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue
camera_id, model, width, height, *params = line.split()
camera_id, width, height = int(camera_id), int(width), int(height)
if model == 'PINHOLE':
fx, fy, cx, cy = map(float, params[:4])
k1 = k2 = k3 = p1 = p2 = 0.0
elif model == 'OPENCV':
fx, fy, cx, cy, k1, k2, p1, p2, k3 = *map(float, params[:8]), 0.0
elif model == 'SIMPLE_RADIAL':
f, cx, cy, k = map(float, params[:4])
fx = fy = f
k1, k2, p1, p2, k3 = k, 0.0, 0.0, 0.0, 0.0
camera_ids.append(camera_id)
if normalize:
fx, fy, cx, cy = fx / width, fy / height, cx / width, cy / height
intrinsics.append([[fx, 0, cx], [0, fy, cy], [0, 0, 1]])
distortions.append([k1, k2, p1, p2, k3])
intrinsics = np.array(intrinsics, dtype=np.float32)
distortions = np.array(distortions, dtype=np.float32)
return camera_ids, intrinsics, distortions
+105
View File
@@ -0,0 +1,105 @@
from typing import *
from pathlib import Path
import numpy as np
def write_glb(path: Union[str, Path], vertices: np.ndarray, faces: np.ndarray, vertex_colors: np.ndarray = None, uv: np.ndarray = None):
import pygltflib
has_colors = vertex_colors is not None
has_uv = uv is not None
triangles_bytes = faces.astype(np.uint32).flatten().tobytes()
vertices_bytes = vertices.astype(np.float32).tobytes()
vertex_colors_bytes = vertex_colors.astype(np.float32).tobytes() if has_colors else None
uv_bytes = uv.astype(np.float32).tobytes() if has_uv else None
gltf = pygltflib.GLTF2(
scene=0,
scenes=[pygltflib.Scene(nodes=[0])],
nodes=[pygltflib.Node(mesh=0)],
meshes=[
pygltflib.Mesh(
primitives=[
pygltflib.Primitive(
attributes=pygltflib.Attributes(
POSITION=1,
COLOR_0=2 if has_colors else None,
TEXCOORD_0=2 + has_colors if has_uv else None
),
indices=0
)
]
)
],
accessors=list(filter(None, [
pygltflib.Accessor( # triangles accessor
bufferView=0,
componentType=pygltflib.UNSIGNED_INT,
count=faces.size,
type=pygltflib.SCALAR,
max=[int(faces.max())],
min=[int(faces.min())],
),
pygltflib.Accessor( # vertices accessor
bufferView=1,
componentType=pygltflib.FLOAT,
count=len(vertices),
type=pygltflib.VEC3,
max=vertices.max(axis=0).tolist(),
min=vertices.min(axis=0).tolist(),
),
pygltflib.Accessor( # vertex colors accessor
bufferView=2,
componentType=pygltflib.FLOAT,
count=len(vertices),
type=pygltflib.VEC3,
max=vertex_colors.max(axis=0).tolist(),
min=vertex_colors.min(axis=0).tolist(),
) if has_colors else None,
pygltflib.Accessor( # uv accessor
bufferView=3,
componentType=pygltflib.FLOAT,
count=len(uv),
type=pygltflib.VEC2,
max=uv.max(axis=0).tolist(),
min=uv.min(axis=0).tolist(),
) if has_uv else None,
])),
bufferViews=list(filter(None, [
pygltflib.BufferView( # triangles buffer view
buffer=0,
byteLength=len(triangles_bytes),
target=pygltflib.ELEMENT_ARRAY_BUFFER,
),
pygltflib.BufferView( # vertices buffer view
buffer=0,
byteOffset=len(triangles_bytes),
byteLength=len(vertices_bytes),
target=pygltflib.ARRAY_BUFFER,
),
pygltflib.BufferView( # vertex colors buffer view
buffer=0,
byteOffset=len(triangles_bytes) + len(vertices_bytes),
byteLength=len(vertex_colors_bytes),
target=pygltflib.ARRAY_BUFFER,
) if has_colors else None,
pygltflib.BufferView( # uv buffer view
buffer=0,
byteOffset=len(triangles_bytes) + len(vertices_bytes) + (len(vertex_colors_bytes) if has_colors else 0),
byteLength=len(uv_bytes),
target=pygltflib.ARRAY_BUFFER,
) if has_uv else None,
])),
buffers=[
pygltflib.Buffer(
byteLength=len(triangles_bytes) + len(vertices_bytes) + (len(vertex_colors_bytes) if has_colors else 0) + (len(uv_bytes) if has_uv else 0),
)
]
)
gltf.set_binary_blob(triangles_bytes + vertices_bytes + (vertex_colors_bytes or b'') + (uv_bytes or b''))
with open(path, 'wb') as f:
for chunk in gltf.save_to_bytes():
f.write(chunk)
+104
View File
@@ -0,0 +1,104 @@
import numpy as np
from typing import *
from pathlib import Path
def read_ply(
file: Union[str, Path],
encoding: Union[str, None] = None,
ignore_unknown: bool = False
) -> Tuple[np.ndarray, np.ndarray]:
"""
Read .ply file, without preprocessing.
Args:
file (Any): filepath
encoding (str, optional):
Returns:
Tuple[np.ndarray, np.ndarray]: vertices, faces
"""
import plyfile
plydata = plyfile.PlyData.read(file)
vertices = np.stack([plydata['vertex'][k] for k in ['x', 'y', 'z']], axis=-1)
if 'face' in plydata:
faces = np.array(plydata['face']['vertex_indices'].tolist())
else:
faces = None
return vertices, faces
def write_ply(
file: Union[str, Path],
vertices: np.ndarray,
faces: np.ndarray = None,
edges: np.ndarray = None,
vertex_colors: np.ndarray = None,
edge_colors: np.ndarray = None,
text: bool = False
):
"""
Write .ply file, without preprocessing.
Args:
file (Any): filepath
vertices (np.ndarray): [N, 3]
faces (np.ndarray): [T, E]
edges (np.ndarray): [E, 2]
vertex_colors (np.ndarray, optional): [N, 3]. Defaults to None.
edge_colors (np.ndarray, optional): [E, 3]. Defaults to None.
text (bool, optional): save data in text format. Defaults to False.
"""
import plyfile
assert vertices.ndim == 2 and vertices.shape[1] == 3
vertices = vertices.astype(np.float32)
if faces is not None:
assert faces.ndim == 2
faces = faces.astype(np.int32)
if edges is not None:
assert edges.ndim == 2 and edges.shape[1] == 2
edges = edges.astype(np.int32)
if vertex_colors is not None:
assert vertex_colors.ndim == 2 and vertex_colors.shape[1] == 3
if vertex_colors.dtype in [np.float32, np.float64]:
vertex_colors = vertex_colors * 255
vertex_colors = np.clip(vertex_colors, 0, 255).astype(np.uint8)
vertices_data = np.zeros(len(vertices), dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4'), ('red', 'u1'), ('green', 'u1'), ('blue', 'u1')])
vertices_data['x'] = vertices[:, 0]
vertices_data['y'] = vertices[:, 1]
vertices_data['z'] = vertices[:, 2]
vertices_data['red'] = vertex_colors[:, 0]
vertices_data['green'] = vertex_colors[:, 1]
vertices_data['blue'] = vertex_colors[:, 2]
else:
vertices_data = np.array([tuple(v) for v in vertices], dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')])
if faces is not None:
faces_data = np.zeros(len(faces), dtype=[('vertex_indices', 'i4', (faces.shape[1],))])
faces_data['vertex_indices'] = faces
if edges is not None:
if edge_colors is not None:
assert edge_colors.ndim == 2 and edge_colors.shape[1] == 3
if edge_colors.dtype in [np.float32, np.float64]:
edge_colors = edge_colors * 255
edge_colors = np.clip(edge_colors, 0, 255).astype(np.uint8)
edges_data = np.zeros(len(edges), dtype=[('vertex1', 'i4'), ('vertex2', 'i4'), ('red', 'u1'), ('green', 'u1'), ('blue', 'u1')])
edges_data['vertex1'] = edges[:, 0]
edges_data['vertex2'] = edges[:, 1]
edges_data['red'] = edge_colors[:, 0]
edges_data['green'] = edge_colors[:, 1]
edges_data['blue'] = edge_colors[:, 2]
else:
edges_data = np.array([tuple(e) for e in edges], dtype=[('vertex1', 'i4'), ('vertex2', 'i4')])
ply_data = [plyfile.PlyElement.describe(vertices_data, 'vertex')]
if faces is not None:
ply_data.append(plyfile.PlyElement.describe(faces_data, 'face'))
if edges is not None:
ply_data.append(plyfile.PlyElement.describe(edges_data, 'edge'))
plyfile.PlyData(ply_data, text=text).write(file)
+146
View File
@@ -0,0 +1,146 @@
from io import TextIOWrapper
from typing import Dict, Any, Union, Iterable
import numpy as np
from pathlib import Path
__all__ = [
'read_obj',
'write_obj',
'simple_write_obj'
]
def read_obj(
file : Union[str, Path, TextIOWrapper],
encoding: Union[str, None] = None,
ignore_unknown: bool = False
):
"""
Read wavefront .obj file, without preprocessing.
Why bothering having this read_obj() while we already have other libraries like `trimesh`?
This function read the raw format from .obj file and keeps the order of vertices and faces,
while trimesh which involves modification like merge/split vertices, which could break the orders of vertices and faces,
Those libraries are commonly aiming at geometry processing and rendering supporting various formats.
If you want mesh geometry processing, you may turn to `trimesh` for more features.
### Parameters
`file` (str, Path, TextIOWrapper): filepath or file object
encoding (str, optional):
### Returns
obj (dict): A dict containing .obj components
{
'mtllib': [],
'v': [[0,1, 0.2, 1.0], [1.2, 0.0, 0.0], ...],
'vt': [[0.5, 0.5], ...],
'vn': [[0., 0.7, 0.7], [0., -0.7, 0.7], ...],
'f': [[0, 1, 2], [2, 3, 4],...],
'usemtl': [{'name': 'mtl1', 'f': 7}]
}
"""
if hasattr(file,'read'):
lines = file.read().splitlines()
else:
with open(file, 'r', encoding=encoding) as fp:
lines = fp.read().splitlines()
mtllib = []
v, vt, vn, vp = [], [], [], [] # Vertex coordinates, Vertex texture coordinate, Vertex normal, Vertex parameter
f, ft, fn = [], [], [] # Face indices, Face texture indices, Face normal indices
o = []
s = []
usemtl = []
def pad(l: list, n: Any):
return l + [n] * (3 - len(l))
for i, line in enumerate(lines):
sq = line.strip().split()
if len(sq) == 0:
continue
if sq[0] == 'v':
assert 4 <= len(sq) <= 5, f'Invalid format of line {i}: {line}'
v.append([float(e) for e in sq[1:]][:3])
elif sq[0] == 'vt':
assert 3 <= len(sq) <= 4, f'Invalid format of line {i}: {line}'
vt.append([float(e) for e in sq[1:]][:2])
elif sq[0] == 'vn':
assert len(sq) == 4, f'Invalid format of line {i}: {line}'
vn.append([float(e) for e in sq[1:]])
elif sq[0] == 'vp':
assert 2 <= len(sq) <= 4, f'Invalid format of line {i}: {line}'
vp.append(pad([float(e) for e in sq[1:]], 0))
elif sq[0] == 'f':
spliting = [pad([int(j) - 1 for j in e.split('/')], -1) for e in sq[1:]]
f.append([e[0] for e in spliting])
ft.append([e[1] for e in spliting])
fn.append([e[2] for e in spliting])
elif sq[0] == 'usemtl':
assert len(sq) == 2
usemtl.append((sq[1], len(f)))
elif sq[0] == 'o':
assert len(sq) == 2
o.append((sq[1], len(f)))
elif sq[0] == 's':
s.append((sq[1], len(f)))
elif sq[0] == 'mtllib':
assert len(sq) == 2
mtllib.append(sq[1])
elif sq[0][0] == '#':
continue
else:
if not ignore_unknown:
raise Exception(f'Unknown keyword {sq[0]}')
min_poly_vertices = min(len(f) for f in f)
max_poly_vertices = max(len(f) for f in f)
return {
'mtllib': mtllib,
'v': np.array(v, dtype=np.float32),
'vt': np.array(vt, dtype=np.float32),
'vn': np.array(vn, dtype=np.float32),
'vp': np.array(vp, dtype=np.float32),
'f': np.array(f, dtype=np.int32) if min_poly_vertices == max_poly_vertices else f,
'ft': np.array(ft, dtype=np.int32) if min_poly_vertices == max_poly_vertices else ft,
'fn': np.array(fn, dtype=np.int32) if min_poly_vertices == max_poly_vertices else fn,
'o': o,
's': s,
'usemtl': usemtl,
}
def write_obj(
file: Union[str, Path],
obj: Dict[str, Any],
encoding: Union[str, None] = None
):
with open(file, 'w', encoding=encoding) as fp:
for k in ['v', 'vt', 'vn', 'vp']:
if k not in obj:
continue
for v in obj[k]:
print(k, *map(float, v), file=fp)
for f in obj['f']:
print('f', *((str('/').join(map(int, i)) if isinstance(int(i), Iterable) else i) for i in f), file=fp)
def simple_write_obj(
file: Union[str, Path],
vertices: np.ndarray,
faces: np.ndarray,
encoding: Union[str, None] = None
):
"""
Write wavefront .obj file, without preprocessing.
Args:
vertices (np.ndarray): [N, 3]
faces (np.ndarray): [T, 3]
file (Any): filepath
encoding (str, optional):
"""
with open(file, 'w', encoding=encoding) as fp:
for v in vertices:
print('v', *map(float, v), file=fp)
for f in faces:
print('f', *map(int, f + 1), file=fp)
+138
View File
@@ -0,0 +1,138 @@
"""
3D utility functions workings with NumPy.
"""
import importlib
import itertools
import numpy
__modules_all__ = {
'mesh':[
'triangulate',
'compute_face_normal',
'compute_face_angle',
'compute_vertex_normal',
'compute_vertex_normal_weighted',
'remove_corrupted_faces',
'merge_duplicate_vertices',
'remove_unreferenced_vertices',
'subdivide_mesh_simple',
'mesh_relations',
'flatten_mesh_indices'
],
'quadmesh': [
'calc_quad_candidates',
'calc_quad_distortion',
'calc_quad_direction',
'calc_quad_smoothness',
'sovle_quad',
'sovle_quad_qp',
'tri_to_quad'
],
'utils': [
'sliding_window_1d',
'sliding_window_nd',
'sliding_window_2d',
'max_pool_1d',
'max_pool_2d',
'max_pool_nd',
'depth_edge',
'depth_aliasing',
'interpolate',
'image_scrcoord',
'image_uv',
'image_pixel_center',
'image_pixel',
'image_mesh',
'image_mesh_from_depth',
'depth_to_normal',
'point_to_normal',
'chessboard',
'cube',
'square',
'camera_frustum',
],
'transforms': [
'perspective',
'perspective_from_fov',
'perspective_from_fov_xy',
'intrinsics_from_focal_center',
'intrinsics_from_fov',
'fov_to_focal',
'focal_to_fov',
'intrinsics_to_fov',
'view_look_at',
'extrinsics_look_at',
'perspective_to_intrinsics',
'perspective_to_near_far',
'intrinsics_to_perspective',
'extrinsics_to_view',
'view_to_extrinsics',
'normalize_intrinsics',
'crop_intrinsics',
'pixel_to_uv',
'pixel_to_ndc',
'uv_to_pixel',
'project_depth',
'depth_buffer_to_linear',
'unproject_cv',
'unproject_gl',
'project_cv',
'project_gl',
'quaternion_to_matrix',
'axis_angle_to_matrix',
'matrix_to_quaternion',
'extrinsics_to_essential',
'euler_axis_angle_rotation',
'euler_angles_to_matrix',
'skew_symmetric',
'rotation_matrix_from_vectors',
'ray_intersection',
'se3_matrix',
'slerp_quaternion',
'slerp_vector',
'lerp',
'lerp_se3_matrix',
'piecewise_lerp',
'piecewise_lerp_se3_matrix',
'apply_transform'
],
'spline': [
'linear_spline_interpolate',
],
'rasterization': [
'RastContext',
'rasterize_triangle_faces',
'rasterize_edges',
'texture',
'warp_image_by_depth',
],
}
__all__ = list(itertools.chain(*__modules_all__.values()))
def __getattr__(name):
try:
return globals()[name]
except KeyError:
pass
try:
module_name = next(m for m in __modules_all__ if name in __modules_all__[m])
except StopIteration:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
module = importlib.import_module(f'.{module_name}', __name__)
for key in __modules_all__[module_name]:
globals()[key] = getattr(module, key)
return globals()[name]
if __name__ == '__main__':
from .quadmesh import *
from .transforms import *
from .mesh import *
from .utils import *
from .rasterization import *
from .spline import *
+88
View File
@@ -0,0 +1,88 @@
# decorator
import numpy as np
from numbers import Number
import inspect
def get_args_order(func, args, kwargs):
"""
Get the order of the arguments of a function.
"""
names = inspect.getfullargspec(func).args
names_idx = {name: i for i, name in enumerate(names)}
args_order = []
kwargs_order = {}
for name, arg in kwargs.items():
if name in names:
kwargs_order[name] = names_idx[name]
names.remove(name)
for i, arg in enumerate(args):
if i < len(names):
args_order.append(names_idx[names[i]])
return args_order, kwargs_order
def broadcast_args(args, kwargs, args_dim, kwargs_dim):
spatial = []
for arg, arg_dim in zip(args + list(kwargs.values()), args_dim + list(kwargs_dim.values())):
if isinstance(arg, np.ndarray) and arg_dim is not None:
arg_spatial = arg.shape[:arg.ndim-arg_dim]
if len(arg_spatial) > len(spatial):
spatial = [1] * (len(arg_spatial) - len(spatial)) + spatial
for j in range(len(arg_spatial)):
if spatial[-j] < arg_spatial[-j]:
if spatial[-j] == 1:
spatial[-j] = arg_spatial[-j]
else:
raise ValueError("Cannot broadcast arguments.")
for i, arg in enumerate(args):
if isinstance(arg, np.ndarray) and args_dim[i] is not None:
args[i] = np.broadcast_to(arg, [*spatial, *arg.shape[arg.ndim-args_dim[i]:]])
for key, arg in kwargs.items():
if isinstance(arg, np.ndarray) and kwargs_dim[key] is not None:
kwargs[key] = np.broadcast_to(arg, [*spatial, *arg.shape[arg.ndim-kwargs_dim[key]:]])
return args, kwargs, spatial
def batched(*dims):
"""
Decorator that allows a function to be called with batched arguments.
"""
def decorator(func):
def wrapper(*args, **kwargs):
args = list(args)
# get arguments dimensions
args_order, kwargs_order = get_args_order(func, args, kwargs)
args_dim = [dims[i] for i in args_order]
kwargs_dim = {key: dims[i] for key, i in kwargs_order.items()}
# convert to numpy array
for i, arg in enumerate(args):
if isinstance(arg, (Number, list, tuple)) and args_dim[i] is not None:
args[i] = np.array(arg)
for key, arg in kwargs.items():
if isinstance(arg, (Number, list, tuple)) and kwargs_dim[key] is not None:
kwargs[key] = np.array(arg)
# broadcast arguments
args, kwargs, spatial = broadcast_args(args, kwargs, args_dim, kwargs_dim)
for i, (arg, arg_dim) in enumerate(zip(args, args_dim)):
if isinstance(arg, np.ndarray) and arg_dim is not None:
args[i] = arg.reshape([-1, *arg.shape[arg.ndim-arg_dim:]])
for key, arg in kwargs.items():
if isinstance(arg, np.ndarray) and kwargs_dim[key] is not None:
kwargs[key] = arg.reshape([-1, *arg.shape[arg.ndim-kwargs_dim[key]:]])
# call function
results = func(*args, **kwargs)
type_results = type(results)
results = list(results) if isinstance(results, (tuple, list)) else [results]
# restore spatial dimensions
for i, result in enumerate(results):
results[i] = result.reshape([*spatial, *result.shape[1:]])
if type_results == tuple:
results = tuple(results)
elif type_results == list:
results = list(results)
else:
results = results[0]
return results
return wrapper
return decorator
+355
View File
@@ -0,0 +1,355 @@
import numpy as np
from typing import *
from ._helpers import batched
__all__ = [
'triangulate',
'compute_face_normal',
'compute_face_angle',
'compute_vertex_normal',
'compute_vertex_normal_weighted',
'remove_corrupted_faces',
'merge_duplicate_vertices',
'remove_unreferenced_vertices',
'subdivide_mesh_simple',
'mesh_relations',
'flatten_mesh_indices'
]
def triangulate(
faces: np.ndarray,
vertices: np.ndarray = None,
backslash: np.ndarray = None
) -> np.ndarray:
"""
Triangulate a polygonal mesh.
Args:
faces (np.ndarray): [L, P] polygonal faces
vertices (np.ndarray, optional): [N, 3] 3-dimensional vertices.
If given, the triangulation is performed according to the distance
between vertices. Defaults to None.
backslash (np.ndarray, optional): [L] boolean array indicating
how to triangulate the quad faces. Defaults to None.
Returns:
(np.ndarray): [L * (P - 2), 3] triangular faces
"""
if faces.shape[-1] == 3:
return faces
P = faces.shape[-1]
if vertices is not None:
assert faces.shape[-1] == 4, "now only support quad mesh"
if backslash is None:
backslash = np.linalg.norm(vertices[faces[:, 0]] - vertices[faces[:, 2]], axis=-1) < \
np.linalg.norm(vertices[faces[:, 1]] - vertices[faces[:, 3]], axis=-1)
if backslash is None:
loop_indice = np.stack([
np.zeros(P - 2, dtype=int),
np.arange(1, P - 1, 1, dtype=int),
np.arange(2, P, 1, dtype=int)
], axis=1)
return faces[:, loop_indice].reshape((-1, 3))
else:
assert faces.shape[-1] == 4, "now only support quad mesh"
faces = np.where(
backslash[:, None],
faces[:, [0, 1, 2, 0, 2, 3]],
faces[:, [0, 1, 3, 3, 1, 2]]
).reshape((-1, 3))
return faces
@batched(2, None)
def compute_face_normal(
vertices: np.ndarray,
faces: np.ndarray
) -> np.ndarray:
"""
Compute face normals of a triangular mesh
Args:
vertices (np.ndarray): [..., N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
Returns:
normals (np.ndarray): [..., T, 3] face normals
"""
normal = np.cross(
vertices[..., faces[:, 1], :] - vertices[..., faces[:, 0], :],
vertices[..., faces[:, 2], :] - vertices[..., faces[:, 0], :]
)
normal_norm = np.linalg.norm(normal, axis=-1, keepdims=True)
normal_norm[normal_norm == 0] = 1
normal /= normal_norm
return normal
@batched(2, None)
def compute_face_angle(
vertices: np.ndarray,
faces: np.ndarray,
eps: float = 1e-12
) -> np.ndarray:
"""
Compute face angles of a triangular mesh
Args:
vertices (np.ndarray): [..., N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
Returns:
angles (np.ndarray): [..., T, 3] face angles
"""
face_angle = np.zeros_like(faces, dtype=vertices.dtype)
for i in range(3):
edge1 = vertices[..., faces[:, (i + 1) % 3], :] - vertices[..., faces[:, i], :]
edge2 = vertices[..., faces[:, (i + 2) % 3], :] - vertices[..., faces[:, i], :]
face_angle[..., i] = np.arccos(np.sum(
edge1 / np.clip(np.linalg.norm(edge1, axis=-1, keepdims=True), eps, None) *
edge2 / np.clip(np.linalg.norm(edge2, axis=-1, keepdims=True), eps, None),
axis=-1
))
return face_angle
@batched(2, None, 2)
def compute_vertex_normal(
vertices: np.ndarray,
faces: np.ndarray,
face_normal: np.ndarray = None
) -> np.ndarray:
"""
Compute vertex normals of a triangular mesh by averaging neightboring face normals
TODO: can be improved.
Args:
vertices (np.ndarray): [..., N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
face_normal (np.ndarray, optional): [..., T, 3] face normals.
None to compute face normals from vertices and faces. Defaults to None.
Returns:
normals (np.ndarray): [..., N, 3] vertex normals
"""
if face_normal is None:
face_normal = compute_face_normal(vertices, faces)
vertex_normal = np.zeros_like(vertices, dtype=vertices.dtype)
for n in range(vertices.shape[0]):
for i in range(3):
vertex_normal[n, :, 0] += np.bincount(faces[:, i], weights=face_normal[n, :, 0], minlength=vertices.shape[1])
vertex_normal[n, :, 1] += np.bincount(faces[:, i], weights=face_normal[n, :, 1], minlength=vertices.shape[1])
vertex_normal[n, :, 2] += np.bincount(faces[:, i], weights=face_normal[n, :, 2], minlength=vertices.shape[1])
vertex_normal_norm = np.linalg.norm(vertex_normal, axis=-1, keepdims=True)
vertex_normal_norm[vertex_normal_norm == 0] = 1
vertex_normal /= vertex_normal_norm
return vertex_normal
@batched(2, None, 2)
def compute_vertex_normal_weighted(
vertices: np.ndarray,
faces: np.ndarray,
face_normal: np.ndarray = None
) -> np.ndarray:
"""
Compute vertex normals of a triangular mesh by weighted sum of neightboring face normals
according to the angles
Args:
vertices (np.ndarray): [..., N, 3] 3-dimensional vertices
faces (np.ndarray): [..., T, 3] triangular face indices
face_normal (np.ndarray, optional): [..., T, 3] face normals.
None to compute face normals from vertices and faces. Defaults to None.
Returns:
normals (np.ndarray): [..., N, 3] vertex normals
"""
if face_normal is None:
face_normal = compute_face_normal(vertices, faces)
face_angle = compute_face_angle(vertices, faces)
vertex_normal = np.zeros_like(vertices)
for n in range(vertices.shape[0]):
for i in range(3):
vertex_normal[n, :, 0] += np.bincount(faces[n, :, i], weights=face_normal[n, :, 0] * face_angle[n, :, i], minlength=vertices.shape[1])
vertex_normal[n, :, 1] += np.bincount(faces[n, :, i], weights=face_normal[n, :, 1] * face_angle[n, :, i], minlength=vertices.shape[1])
vertex_normal[n, :, 2] += np.bincount(faces[n, :, i], weights=face_normal[n, :, 2] * face_angle[n, :, i], minlength=vertices.shape[1])
vertex_normal_norm = np.linalg.norm(vertex_normal, axis=-1, keepdims=True)
vertex_normal_norm[vertex_normal_norm == 0] = 1
vertex_normal /= vertex_normal_norm
return vertex_normal
def remove_corrupted_faces(
faces: np.ndarray
) -> np.ndarray:
"""
Remove corrupted faces (faces with duplicated vertices)
Args:
faces (np.ndarray): [T, 3] triangular face indices
Returns:
np.ndarray: [T_, 3] triangular face indices
"""
corrupted = (faces[:, 0] == faces[:, 1]) | (faces[:, 1] == faces[:, 2]) | (faces[:, 2] == faces[:, 0])
return faces[~corrupted]
def merge_duplicate_vertices(
vertices: np.ndarray,
faces: np.ndarray,
tol: float = 1e-6
) -> Tuple[np.ndarray, np.ndarray]:
"""
Merge duplicate vertices of a triangular mesh.
Duplicate vertices are merged by selecte one of them, and the face indices are updated accordingly.
Args:
vertices (np.ndarray): [N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
tol (float, optional): tolerance for merging. Defaults to 1e-6.
Returns:
vertices (np.ndarray): [N_, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
"""
vertices_round = np.round(vertices / tol)
_, uni_i, uni_inv = np.unique(vertices_round, return_index=True, return_inverse=True, axis=0)
vertices = vertices[uni_i]
faces = uni_inv[faces]
return vertices, faces
def remove_unreferenced_vertices(
faces: np.ndarray,
*vertice_attrs,
return_indices: bool = False
) -> Tuple[np.ndarray, ...]:
"""
Remove unreferenced vertices of a mesh.
Unreferenced vertices are removed, and the face indices are updated accordingly.
Args:
faces (np.ndarray): [T, P] face indices
*vertice_attrs: vertex attributes
Returns:
faces (np.ndarray): [T, P] face indices
*vertice_attrs: vertex attributes
indices (np.ndarray, optional): [N] indices of vertices that are kept. Defaults to None.
"""
P = faces.shape[-1]
fewer_indices, inv_map = np.unique(faces, return_inverse=True)
faces = inv_map.astype(np.int32).reshape(-1, P)
ret = [faces]
for attr in vertice_attrs:
ret.append(attr[fewer_indices])
if return_indices:
ret.append(fewer_indices)
return tuple(ret)
def subdivide_mesh_simple(
vertices: np.ndarray,
faces: np.ndarray,
n: int = 1
) -> Tuple[np.ndarray, np.ndarray]:
"""
Subdivide a triangular mesh by splitting each triangle into 4 smaller triangles.
NOTE: All original vertices are kept, and new vertices are appended to the end of the vertex list.
Args:
vertices (np.ndarray): [N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
n (int, optional): number of subdivisions. Defaults to 1.
Returns:
vertices (np.ndarray): [N_, 3] subdivided 3-dimensional vertices
faces (np.ndarray): [4 * T, 3] subdivided triangular face indices
"""
for _ in range(n):
edges = np.stack([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], axis=0)
edges = np.sort(edges, axis=2)
uni_edges, uni_inv = np.unique(edges.reshape(-1, 2), return_inverse=True, axis=0)
uni_inv = uni_inv.reshape(3, -1)
midpoints = (vertices[uni_edges[:, 0]] + vertices[uni_edges[:, 1]]) / 2
n_vertices = vertices.shape[0]
vertices = np.concatenate([vertices, midpoints], axis=0)
faces = np.concatenate([
np.stack([faces[:, 0], n_vertices + uni_inv[0], n_vertices + uni_inv[2]], axis=1),
np.stack([faces[:, 1], n_vertices + uni_inv[1], n_vertices + uni_inv[0]], axis=1),
np.stack([faces[:, 2], n_vertices + uni_inv[2], n_vertices + uni_inv[1]], axis=1),
np.stack([n_vertices + uni_inv[0], n_vertices + uni_inv[1], n_vertices + uni_inv[2]], axis=1),
], axis=0)
return vertices, faces
def mesh_relations(
faces: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
"""
Calculate the relation between vertices and faces.
NOTE: The input mesh must be a manifold triangle mesh.
Args:
faces (np.ndarray): [T, 3] triangular face indices
Returns:
edges (np.ndarray): [E, 2] edge indices
edge2face (np.ndarray): [E, 2] edge to face relation. The second column is -1 if the edge is boundary.
face2edge (np.ndarray): [T, 3] face to edge relation
face2face (np.ndarray): [T, 3] face to face relation
"""
T = faces.shape[0]
edges = np.stack([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], axis=1).reshape(-1, 2) # [3T, 2]
edges = np.sort(edges, axis=1) # [3T, 2]
edges, face2edge, occurence = np.unique(edges, axis=0, return_inverse=True, return_counts=True) # [E, 2], [3T], [E]
E = edges.shape[0]
assert np.all(occurence <= 2), "The input mesh is not a manifold mesh."
# Edge to face relation
padding = np.arange(E, dtype=np.int32)[occurence == 1]
padded_face2edge = np.concatenate([face2edge, padding], axis=0) # [2E]
edge2face = np.argsort(padded_face2edge, kind='stable').reshape(-1, 2) // 3 # [E, 2]
edge2face_valid = edge2face[:, 1] < T # [E]
edge2face[~edge2face_valid, 1] = -1
# Face to edge relation
face2edge = face2edge.reshape(-1, 3) # [T, 3]
# Face to face relation
face2face = edge2face[face2edge] # [T, 3, 2]
face2face = face2face[face2face != np.arange(T)[:, None, None]].reshape(T, 3) # [T, 3]
return edges, edge2face, face2edge, face2face
@overload
def flatten_mesh_indices(faces1: np.ndarray, attr1: np.ndarray, *other_faces_attrs_pairs: np.ndarray) -> Tuple[np.ndarray, ...]:
"""
Rearrange the indices of a mesh to a flattened version. Vertices will be no longer shared.
### Parameters:
- `faces1`: [T, P] face indices of the first attribute
- `attr1`: [N1, ...] attributes of the first mesh
- ...
### Returns:
- `faces`: [T, P] flattened face indices, contigous from 0 to T * P - 1
- `attr1`: [T * P, ...] attributes of the first mesh, where every P values correspond to a face
_ ...
"""
def flatten_mesh_indices(*args: np.ndarray) -> Tuple[np.ndarray, ...]:
assert len(args) % 2 == 0, "The number of arguments must be even."
T, P = args[0].shape
assert all(arg.shape[0] == T and arg.shape[1] == P for arg in args[::2]), "The faces must have the same shape."
attr_flat = []
for faces_, attr_ in zip(args[::2], args[1::2]):
attr_flat_ = attr_[faces_].reshape(-1, *attr_.shape[1:])
attr_flat.append(attr_flat_)
faces_flat = np.arange(T * P, dtype=np.int32).reshape(T, P)
return faces_flat, *attr_flat
+472
View File
@@ -0,0 +1,472 @@
import numpy as np
import scipy as sp
import scipy.optimize as spopt
import piqp
from typing import *
__all__ = [
'calc_quad_candidates',
'calc_quad_distortion',
'calc_quad_direction',
'calc_quad_smoothness',
'sovle_quad',
'sovle_quad_qp',
'tri_to_quad'
]
def calc_quad_candidates(
edges: np.ndarray,
face2edge: np.ndarray,
edge2face: np.ndarray,
):
"""
Calculate the candidate quad faces.
Args:
edges (np.ndarray): [E, 2] edge indices
face2edge (np.ndarray): [T, 3] face to edge relation
edge2face (np.ndarray): [E, 2] edge to face relation
Returns:
quads (np.ndarray): [Q, 4] quad candidate indices
quad2edge (np.ndarray): [Q, 4] edge to quad candidate relation
quad2adj (np.ndarray): [Q, 8] adjacent quad candidates of each quad candidate
quads_valid (np.ndarray): [E] whether the quad corresponding to the edge is valid
"""
E = edges.shape[0]
T = face2edge.shape[0]
quads_valid = edge2face[:, 1] != -1
Q = quads_valid.sum()
quad2face = edge2face[quads_valid] # [Q, 2]
quad2edge = face2edge[quad2face] # [Q, 2, 3]
flag = quad2edge == np.arange(E)[quads_valid][:, None, None] # [Q, 2, 3]
flag = flag.argmax(axis=-1) # [Q, 2]
quad2edge = np.stack([
quad2edge[np.arange(Q)[:, None], np.arange(2)[None, :], (flag + 1) % 3],
quad2edge[np.arange(Q)[:, None], np.arange(2)[None, :], (flag + 2) % 3],
], axis=-1).reshape(Q, 4) # [Q, 4]
quads = np.concatenate([
np.where(
(edges[quad2edge[:, 0:1], 1:] == edges[quad2edge[:, 1:2], :]).any(axis=-1),
edges[quad2edge[:, 0:1], [[0, 1]]],
edges[quad2edge[:, 0:1], [[1, 0]]],
),
np.where(
(edges[quad2edge[:, 2:3], 1:] == edges[quad2edge[:, 3:4], :]).any(axis=-1),
edges[quad2edge[:, 2:3], [[0, 1]]],
edges[quad2edge[:, 2:3], [[1, 0]]],
),
], axis=1) # [Q, 4]
quad2adj = edge2face[quad2edge] # [Q, 4, 2]
quad2adj = quad2adj[quad2adj != quad2face[:, [0,0,1,1], None]].reshape(Q, 4) # [Q, 4]
quad2adj_valid = quad2adj != -1
quad2adj = face2edge[quad2adj] # [Q, 4, 3]
quad2adj[~quad2adj_valid, 0] = quad2edge[~quad2adj_valid]
quad2adj[~quad2adj_valid, 1:] = -1
quad2adj = quad2adj[quad2adj != quad2edge[..., None]].reshape(Q, 8) # [Q, 8]
edge_valid = -np.ones(E, dtype=np.int32)
edge_valid[quads_valid] = np.arange(Q)
quad2adj_valid = quad2adj != -1
quad2adj[quad2adj_valid] = edge_valid[quad2adj[quad2adj_valid]] # [Q, 8]
return quads, quad2edge, quad2adj, quads_valid
def calc_quad_distortion(
vertices: np.ndarray,
quads: np.ndarray,
):
"""
Calculate the distortion of each candidate quad face.
Args:
vertices (np.ndarray): [N, 3] 3-dimensional vertices
quads (np.ndarray): [Q, 4] quad face indices
Returns:
distortion (np.ndarray): [Q] distortion of each quad face
"""
edge0 = vertices[quads[:, 1]] - vertices[quads[:, 0]] # [Q, 3]
edge1 = vertices[quads[:, 2]] - vertices[quads[:, 1]] # [Q, 3]
edge2 = vertices[quads[:, 3]] - vertices[quads[:, 2]] # [Q, 3]
edge3 = vertices[quads[:, 0]] - vertices[quads[:, 3]] # [Q, 3]
cross = vertices[quads[:, 0]] - vertices[quads[:, 2]] # [Q, 3]
len0 = np.maximum(np.linalg.norm(edge0, axis=-1), 1e-10) # [Q]
len1 = np.maximum(np.linalg.norm(edge1, axis=-1), 1e-10) # [Q]
len2 = np.maximum(np.linalg.norm(edge2, axis=-1), 1e-10) # [Q]
len3 = np.maximum(np.linalg.norm(edge3, axis=-1), 1e-10) # [Q]
len_cross = np.maximum(np.linalg.norm(cross, axis=-1), 1e-10) # [Q]
angle0 = np.arccos(np.clip(np.sum(-edge0 * edge1, axis=-1) / (len0 * len1), -1, 1)) # [Q]
angle1 = np.arccos(np.clip(np.sum(-edge1 * cross, axis=-1) / (len1 * len_cross), -1, 1)) \
+ np.arccos(np.clip(np.sum(cross * edge2, axis=-1) / (len_cross * len2), -1, 1)) # [Q]
angle2 = np.arccos(np.clip(np.sum(-edge2 * edge3, axis=-1) / (len2 * len3), -1, 1)) # [Q]
angle3 = np.arccos(np.clip(np.sum(-edge3 * -cross, axis=-1) / (len3 * len_cross), -1, 1)) \
+ np.arccos(np.clip(np.sum(-cross * edge0, axis=-1) / (len_cross * len0), -1, 1)) # [Q]
normal0 = np.cross(edge0, edge1) # [Q, 3]
normal1 = np.cross(edge2, edge3) # [Q, 3]
normal0 = normal0 / np.maximum(np.linalg.norm(normal0, axis=-1, keepdims=True), 1e-10) # [Q, 3]
normal1 = normal1 / np.maximum(np.linalg.norm(normal1, axis=-1, keepdims=True), 1e-10) # [Q, 3]
angle_normal = np.arccos(np.clip(np.sum(normal0 * normal1, axis=-1), -1, 1)) # [Q]
D90 = np.pi / 2
D180 = np.pi
D360 = np.pi * 2
ang_eng = (np.abs(angle0 - D90)**2 + np.abs(angle1 - D90)**2 + np.abs(angle2 - D90)**2 + np.abs(angle3 - D90)**2) / 4 # [Q]
dist_eng = np.abs(angle0 - angle2)**2 / np.minimum(np.maximum(np.minimum(angle0, angle2), 1e-10), np.maximum(D180 - np.maximum(angle0, angle2), 1e-10)) \
+ np.abs(angle1 - angle3)**2 / np.minimum(np.maximum(np.minimum(angle1, angle3), 1e-10), np.maximum(D180 - np.maximum(angle1, angle3), 1e-10)) # [Q]
plane_eng = np.where(angle_normal < D90/2, np.abs(angle_normal)**2, 1e10) # [Q]
eng = ang_eng + 2 * dist_eng + 2 * plane_eng # [Q]
return eng
def calc_quad_direction(
vertices: np.ndarray,
quads: np.ndarray,
):
"""
Calculate the direction of each candidate quad face.
Args:
vertices (np.ndarray): [N, 3] 3-dimensional vertices
quads (np.ndarray): [Q, 4] quad face indices
Returns:
direction (np.ndarray): [Q, 4] direction of each quad face.
Represented by the angle between the crossing and each edge.
"""
mid0 = (vertices[quads[:, 0]] + vertices[quads[:, 1]]) / 2 # [Q, 3]
mid1 = (vertices[quads[:, 1]] + vertices[quads[:, 2]]) / 2 # [Q, 3]
mid2 = (vertices[quads[:, 2]] + vertices[quads[:, 3]]) / 2 # [Q, 3]
mid3 = (vertices[quads[:, 3]] + vertices[quads[:, 0]]) / 2 # [Q, 3]
cross0 = mid2 - mid0 # [Q, 3]
cross1 = mid3 - mid1 # [Q, 3]
cross0 = cross0 / np.maximum(np.linalg.norm(cross0, axis=-1, keepdims=True), 1e-10) # [Q, 3]
cross1 = cross1 / np.maximum(np.linalg.norm(cross1, axis=-1, keepdims=True), 1e-10) # [Q, 3]
edge0 = vertices[quads[:, 1]] - vertices[quads[:, 0]] # [Q, 3]
edge1 = vertices[quads[:, 2]] - vertices[quads[:, 1]] # [Q, 3]
edge2 = vertices[quads[:, 3]] - vertices[quads[:, 2]] # [Q, 3]
edge3 = vertices[quads[:, 0]] - vertices[quads[:, 3]] # [Q, 3]
edge0 = edge0 / np.maximum(np.linalg.norm(edge0, axis=-1, keepdims=True), 1e-10) # [Q, 3]
edge1 = edge1 / np.maximum(np.linalg.norm(edge1, axis=-1, keepdims=True), 1e-10) # [Q, 3]
edge2 = edge2 / np.maximum(np.linalg.norm(edge2, axis=-1, keepdims=True), 1e-10) # [Q, 3]
edge3 = edge3 / np.maximum(np.linalg.norm(edge3, axis=-1, keepdims=True), 1e-10) # [Q, 3]
direction = np.stack([
np.arccos(np.clip(np.sum(cross0 * edge0, axis=-1), -1, 1)),
np.arccos(np.clip(np.sum(cross1 * edge1, axis=-1), -1, 1)),
np.arccos(np.clip(np.sum(-cross0 * edge2, axis=-1), -1, 1)),
np.arccos(np.clip(np.sum(-cross1 * edge3, axis=-1), -1, 1)),
], axis=-1) # [Q, 4]
return direction
def calc_quad_smoothness(
quad2edge: np.ndarray,
quad2adj: np.ndarray,
quads_direction: np.ndarray,
):
"""
Calculate the smoothness of each candidate quad face connection.
Args:
quad2adj (np.ndarray): [Q, 8] adjacent quad faces of each quad face
quads_direction (np.ndarray): [Q, 4] direction of each quad face
Returns:
smoothness (np.ndarray): [Q, 8] smoothness of each quad face connection
"""
Q = quad2adj.shape[0]
quad2adj_valid = quad2adj != -1
connections = np.stack([
np.arange(Q)[:, None].repeat(8, axis=1),
quad2adj,
], axis=-1)[quad2adj_valid] # [C, 2]
shared_edge_idx_0 = np.array([[0, 0, 1, 1, 2, 2, 3, 3]]).repeat(Q, axis=0)[quad2adj_valid] # [C]
shared_edge_idx_1 = np.argmax(quad2edge[quad2adj][quad2adj_valid] == quad2edge[connections[:, 0], shared_edge_idx_0][:, None], axis=-1) # [C]
valid_smoothness = np.abs(quads_direction[connections[:, 0], shared_edge_idx_0] - quads_direction[connections[:, 1], shared_edge_idx_1])**2 # [C]
smoothness = np.zeros([Q, 8], dtype=np.float32)
smoothness[quad2adj_valid] = valid_smoothness
return smoothness
def sovle_quad(
face2edge: np.ndarray,
edge2face: np.ndarray,
quad2adj: np.ndarray,
quads_distortion: np.ndarray,
quads_smoothness: np.ndarray,
quads_valid: np.ndarray,
):
"""
Solve the quad mesh from the candidate quad faces.
Args:
face2edge (np.ndarray): [T, 3] face to edge relation
edge2face (np.ndarray): [E, 2] edge to face relation
quad2adj (np.ndarray): [Q, 8] adjacent quad faces of each quad face
quads_distortion (np.ndarray): [Q] distortion of each quad face
quads_smoothness (np.ndarray): [Q, 8] smoothness of each quad face connection
quads_valid (np.ndarray): [E] whether the quad corresponding to the edge is valid
Returns:
weights (np.ndarray): [Q] weight of each valid quad face
"""
T = face2edge.shape[0]
E = edge2face.shape[0]
Q = quads_distortion.shape[0]
edge_valid = -np.ones(E, dtype=np.int32)
edge_valid[quads_valid] = np.arange(Q)
quads_connection = np.stack([
np.arange(Q)[:, None].repeat(8, axis=1),
quad2adj,
], axis=-1)[quad2adj != -1] # [C, 2]
quads_connection = np.sort(quads_connection, axis=-1) # [C, 2]
quads_connection, quads_connection_idx = np.unique(quads_connection, axis=0, return_index=True) # [C, 2], [C]
quads_smoothness = quads_smoothness[quad2adj != -1] # [C]
quads_smoothness = quads_smoothness[quads_connection_idx] # [C]
C = quads_connection.shape[0]
# Construct the linear programming problem
# Variables:
# quads_weight: [Q] weight of each quad face
# tri_min_weight: [T] minimum weight of each triangle face
# conn_min_weight: [C] minimum weight of each quad face connection
# conn_max_weight: [C] maximum weight of each quad face connection
# Objective:
# mimi
c = np.concatenate([
quads_distortion - 3,
quads_smoothness*4 - 2,
quads_smoothness*4,
], axis=0) # [Q+C]
A_ub_triplet = np.concatenate([
np.stack([np.arange(T), edge_valid[face2edge[:, 0]], np.ones(T)], axis=1), # [T, 3]
np.stack([np.arange(T), edge_valid[face2edge[:, 1]], np.ones(T)], axis=1), # [T, 3]
np.stack([np.arange(T), edge_valid[face2edge[:, 2]], np.ones(T)], axis=1), # [T, 3]
np.stack([np.arange(T, T+C), np.arange(Q, Q+C), np.ones(C)], axis=1), # [C, 3]
np.stack([np.arange(T, T+C), quads_connection[:, 0], -np.ones(C)], axis=1), # [C, 3]
np.stack([np.arange(T, T+C), quads_connection[:, 1], -np.ones(C)], axis=1), # [C, 3]
np.stack([np.arange(T+C, T+2*C), np.arange(Q+C, Q+2*C), -np.ones(C)], axis=1), # [C, 3]
np.stack([np.arange(T+C, T+2*C), quads_connection[:, 0], np.ones(C)], axis=1), # [C, 3]
np.stack([np.arange(T+C, T+2*C), quads_connection[:, 1], np.ones(C)], axis=1), # [C, 3]
], axis=0) # [3T+6C, 3]
A_ub_triplet = A_ub_triplet[A_ub_triplet[:, 1] != -1] # [3T', 3]
A_ub = sp.sparse.coo_matrix((A_ub_triplet[:, 2], (A_ub_triplet[:, 0], A_ub_triplet[:, 1])), shape=[T+2*C, Q+2*C]) # [T,
b_ub = np.concatenate([np.ones(T), -np.ones(C), np.ones(C)], axis=0) # [T+2C]
bound = np.stack([
np.concatenate([np.zeros(Q), -np.ones(C), np.zeros(C)], axis=0),
np.concatenate([np.ones(Q), np.ones(C), np.ones(C)], axis=0),
], axis=1) # [Q+2C, 2]
A_eq = None
b_eq = None
print('Solver statistics:')
print(f' #T = {T}')
print(f' #Q = {Q}')
print(f' #C = {C}')
# Solve the linear programming problem
last_num_valid = 0
for i in range(100):
res_ = spopt.linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bound)
if not res_.success:
print(f' Iter {i} | Failed with {res_.message}')
break
res = res_
weights = res.x[:Q]
valid = (weights > 0.5)
num_valid = valid.sum()
print(f' Iter {i} | #Q_valid = {num_valid}')
if num_valid == last_num_valid:
break
last_num_valid = num_valid
A_eq_triplet = np.stack([
np.arange(num_valid),
np.arange(Q)[valid],
np.ones(num_valid),
], axis=1) # [num_valid, 3]
A_eq = sp.sparse.coo_matrix((A_eq_triplet[:, 2], (A_eq_triplet[:, 0], A_eq_triplet[:, 1])), shape=[num_valid, Q+2*C]) # [num_valid, Q+C]
b_eq = np.where(weights[valid] > 0.5, 1, 0) # [num_valid]
# Return the result
quads_weight = res.x[:Q]
conn_min_weight = res.x[Q:Q+C]
conn_max_weight = res.x[Q+C:Q+2*C]
return quads_weight, conn_min_weight, conn_max_weight
def sovle_quad_qp(
face2edge: np.ndarray,
edge2face: np.ndarray,
quad2adj: np.ndarray,
quads_distortion: np.ndarray,
quads_smoothness: np.ndarray,
quads_valid: np.ndarray,
):
"""
Solve the quad mesh from the candidate quad faces.
Args:
face2edge (np.ndarray): [T, 3] face to edge relation
edge2face (np.ndarray): [E, 2] edge to face relation
quad2adj (np.ndarray): [Q, 8] adjacent quad faces of each quad face
quads_distortion (np.ndarray): [Q] distortion of each quad face
quads_smoothness (np.ndarray): [Q, 8] smoothness of each quad face connection
quads_valid (np.ndarray): [E] whether the quad corresponding to the edge is valid
Returns:
weights (np.ndarray): [Q] weight of each valid quad face
"""
T = face2edge.shape[0]
E = edge2face.shape[0]
Q = quads_distortion.shape[0]
edge_valid = -np.ones(E, dtype=np.int32)
edge_valid[quads_valid] = np.arange(Q)
# Construct the quadratic programming problem
C_smoothness_triplet = np.stack([
np.arange(Q)[:, None].repeat(8, axis=1)[quad2adj != -1],
quad2adj[quad2adj != -1],
5 * quads_smoothness[quad2adj != -1],
], axis=-1) # [C, 3]
# C_smoothness_triplet = np.concatenate([
# C_smoothness_triplet,
# np.stack([np.arange(Q), np.arange(Q), 20*np.ones(Q)], axis=1),
# ], axis=0) # [C+Q, 3]
C_smoothness = sp.sparse.coo_matrix((C_smoothness_triplet[:, 2], (C_smoothness_triplet[:, 0], C_smoothness_triplet[:, 1])), shape=[Q, Q]) # [Q, Q]
C_smoothness = C_smoothness.tocsc()
C_dist = quads_distortion - 20 # [Q]
A_eq = sp.sparse.coo_matrix((np.zeros(Q), (np.zeros(Q), np.arange(Q))), shape=[1, Q]) # [1, Q]\
A_eq = A_eq.tocsc()
b_eq = np.array([0])
A_ub_triplet = np.concatenate([
np.stack([np.arange(T), edge_valid[face2edge[:, 0]], np.ones(T)], axis=1), # [T, 3]
np.stack([np.arange(T), edge_valid[face2edge[:, 1]], np.ones(T)], axis=1), # [T, 3]
np.stack([np.arange(T), edge_valid[face2edge[:, 2]], np.ones(T)], axis=1), # [T, 3]
], axis=0) # [3T, 3]
A_ub_triplet = A_ub_triplet[A_ub_triplet[:, 1] != -1] # [3T', 3]
A_ub = sp.sparse.coo_matrix((A_ub_triplet[:, 2], (A_ub_triplet[:, 0], A_ub_triplet[:, 1])), shape=[T, Q]) # [T, Q]
A_ub = A_ub.tocsc()
b_ub = np.ones(T)
lb = np.zeros(Q)
ub = np.ones(Q)
solver = piqp.SparseSolver()
solver.settings.verbose = True
solver.settings.compute_timings = True
solver.setup(C_smoothness, C_dist, A_eq, b_eq, A_ub, b_ub, lb, ub)
status = solver.solve()
# x = cp.Variable(Q)
# prob = cp.Problem(
# cp.Minimize(cp.quad_form(x, C_smoothness) + C_dist.T @ x),
# [
# A_ub @ x <= b_ub,
# x >= 0, x <= 1,
# ]
# )
# # Solve the quadratic programming problem
# prob.solve(solver=cp.PIQP, verbose=True)
# Return the result
weights = solver.result.x
return weights
def tri_to_quad(
vertices: np.ndarray,
faces: np.ndarray,
) -> Tuple[np.ndarray, np.ndarray]:
"""
Convert a triangle mesh to a quad mesh.
NOTE: The input mesh must be a manifold mesh.
Args:
vertices (np.ndarray): [N, 3] 3-dimensional vertices
faces (np.ndarray): [T, 3] triangular face indices
Returns:
vertices (np.ndarray): [N_, 3] 3-dimensional vertices
faces (np.ndarray): [Q, 4] quad face indices
"""
raise NotImplementedError
if __name__ == '__main__':
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
import utils3d
import numpy as np
import cv2
from vis import vis_edge_color
file = 'miku'
vertices, faces = utils3d.io.read_ply(f'test/assets/{file}.ply')
edges, edge2face, face2edge, face2face = calc_relations(faces)
quad_cands, quad2edge, quad2adj, quad_valid = calc_quad_candidates(edges, face2edge, edge2face)
distortion = calc_quad_distortion(vertices, quad_cands)
direction = calc_quad_direction(vertices, quad_cands)
smoothness = calc_quad_smoothness(quad2edge, quad2adj, direction)
boundary_edges = edges[edge2face[:, 1] == -1]
quads_weight, conn_min_weight, conn_max_weight = sovle_quad(face2edge, edge2face, quad2adj, distortion, smoothness, quad_valid)
quads = quad_cands[quads_weight > 0.5]
print('Mesh statistics')
print(f' #V = {vertices.shape[0]}')
print(f' #F = {faces.shape[0]}')
print(f' #E = {edges.shape[0]}')
print(f' #B = {boundary_edges.shape[0]}')
print(f' #Q_cand = {quad_cands.shape[0]}')
print(f' #Q = {quads.shape[0]}')
utils3d.io.write_ply(f'test/assets/{file}_boundary_edges.ply', vertices=vertices, edges=boundary_edges)
utils3d.io.write_ply(f'test/assets/{file}_quad_candidates.ply', vertices=vertices, faces=quads)
edge_colors = np.zeros([edges.shape[0], 3], dtype=np.uint8)
distortion = (distortion - distortion.min()) / (distortion.max() - distortion.min())
distortion = (distortion * 255).astype(np.uint8)
edge_colors[quad_valid] = cv2.cvtColor(cv2.applyColorMap(distortion, cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB).reshape(-1, 3)
utils3d.io.write_ply(f'test/assets/{file}_quad_candidates_distortion.ply', **vis_edge_color(vertices, edges, edge_colors))
edge_colors = np.zeros([edges.shape[0], 3], dtype=np.uint8)
edge_colors[quad_valid] = cv2.cvtColor(cv2.applyColorMap((quads_weight * 255).astype(np.uint8), cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB).reshape(-1, 3)
utils3d.io.write_ply(f'test/assets/{file}_quad_candidates_weights.ply', **vis_edge_color(vertices, edges, edge_colors))
utils3d.io.write_ply(f'test/assets/{file}_quad.ply', vertices=vertices, faces=quads)
quad_centers = vertices[quad_cands].mean(axis=1)
conns = np.stack([
np.arange(quad_cands.shape[0])[:, None].repeat(8, axis=1),
quad2adj,
], axis=-1)[quad2adj != -1] # [C, 2]
conns, conns_idx = np.unique(np.sort(conns, axis=-1), axis=0, return_index=True) # [C, 2], [C]
smoothness = smoothness[quad2adj != -1][conns_idx] # [C]
conns_color = cv2.cvtColor(cv2.applyColorMap((smoothness * 255).astype(np.uint8), cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB).reshape(-1, 3)
utils3d.io.write_ply(f'test/assets/{file}_quad_conn_smoothness.ply', **vis_edge_color(quad_centers, conns, conns_color))
conns_color = cv2.cvtColor(cv2.applyColorMap((conn_min_weight * 255).astype(np.uint8), cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB).reshape(-1, 3)
utils3d.io.write_ply(f'test/assets/{file}_quad_conn_min.ply', **vis_edge_color(quad_centers, conns, conns_color))
conns_color = cv2.cvtColor(cv2.applyColorMap((conn_max_weight * 255).astype(np.uint8), cv2.COLORMAP_JET), cv2.COLOR_BGR2RGB).reshape(-1, 3)
utils3d.io.write_ply(f'test/assets/{file}_quad_conn_max.ply', **vis_edge_color(quad_centers, conns, conns_color))
+471
View File
@@ -0,0 +1,471 @@
import os
from typing import *
import numpy as np
import moderngl
from . import transforms, utils, mesh
__all__ = [
'RastContext',
'rasterize_triangle_faces',
'rasterize_edges',
'texture',
'warp_image_by_depth',
]
def map_np_dtype(dtype) -> str:
if dtype == int:
return 'i4'
elif dtype == np.uint8:
return 'u1'
elif dtype == np.uint32:
return 'u2'
elif dtype == np.float16:
return 'f2'
elif dtype == np.float32:
return 'f4'
def one_value(dtype):
if dtype == 'u1':
return 255
elif dtype == 'u2':
return 65535
else:
return 1
class RastContext:
def __init__(self, standalone: bool = True, backend: str = None, **kwargs):
"""
Create a moderngl context.
Args:
standalone (bool, optional): whether to create a standalone context. Defaults to True.
backend (str, optional): backend to use. Defaults to None.
Keyword Args:
See moderngl.create_context
"""
if backend is None:
self.mgl_ctx = moderngl.create_context(standalone=standalone, **kwargs)
else:
self.mgl_ctx = moderngl.create_context(standalone=standalone, backend=backend, **kwargs)
self.__prog_src = {}
self.__prog = {}
def __del__(self):
self.mgl_ctx.release()
def screen_quad(self) -> moderngl.VertexArray:
self.screen_quad_vbo = self.mgl_ctx.buffer(np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype='f4'))
self.screen_quad_ibo = self.mgl_ctx.buffer(np.array([0, 1, 2, 0, 2, 3], dtype=np.int32))
def program_vertex_attribute(self, n: int) -> moderngl.Program:
assert n in [1, 2, 3, 4], 'vertex attribute only supports channels 1, 2, 3, 4'
if 'vertex_attribute_vsh' not in self.__prog_src:
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'vertex_attribute.vsh'), 'r') as f:
self.__prog_src['vertex_attribute_vsh'] = f.read()
if 'vertex_attribute_fsh' not in self.__prog_src:
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'vertex_attribute.fsh'), 'r') as f:
self.__prog_src['vertex_attribute_fsh'] = f.read()
if f'vertex_attribute_{n}' not in self.__prog:
vsh = self.__prog_src['vertex_attribute_vsh'].replace('vecN', f'vec{n}')
fsh = self.__prog_src['vertex_attribute_fsh'].replace('vecN', f'vec{n}')
self.__prog[f'vertex_attribute_{n}'] = self.mgl_ctx.program(vertex_shader=vsh, fragment_shader=fsh)
return self.__prog[f'vertex_attribute_{n}']
def program_texture(self, n: int) -> moderngl.Program:
assert n in [1, 2, 3, 4], 'texture only supports channels 1, 2, 3, 4'
if 'texture_vsh' not in self.__prog_src:
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'texture.vsh'), 'r') as f:
self.__prog_src['texture_vsh'] = f.read()
if 'texture_fsh' not in self.__prog_src:
with open(os.path.join(os.path.dirname(__file__), 'shaders', 'texture.fsh'), 'r') as f:
self.__prog_src['texture_fsh'] = f.read()
if f'texture_{n}' not in self.__prog:
vsh = self.__prog_src['texture_vsh'].replace('vecN', f'vec{n}')
fsh = self.__prog_src['texture_fsh'].replace('vecN', f'vec{n}')
self.__prog[f'texture_{n}'] = self.mgl_ctx.program(vertex_shader=vsh, fragment_shader=fsh)
self.__prog[f'texture_{n}']['tex'] = 0
self.__prog[f'texture_{n}']['uv'] = 1
return self.__prog[f'texture_{n}']
def rasterize_triangle_faces(
ctx: RastContext,
vertices: np.ndarray,
faces: np.ndarray,
attr: np.ndarray,
width: int,
height: int,
transform: np.ndarray = None,
cull_backface: bool = True,
return_depth: bool = False,
image: np.ndarray = None,
depth: np.ndarray = None
) -> Tuple[np.ndarray, np.ndarray]:
"""
Rasterize vertex attribute.
Args:
vertices (np.ndarray): [N, 3]
faces (np.ndarray): [T, 3]
attr (np.ndarray): [N, C]
width (int): width of rendered image
height (int): height of rendered image
transform (np.ndarray): [4, 4] model-view-projection transformation matrix.
cull_backface (bool): whether to cull backface
image: (np.ndarray): [H, W, C] background image
depth: (np.ndarray): [H, W] background depth
Returns:
image (np.ndarray): [H, W, C] rendered image
depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None.
"""
assert vertices.ndim == 2 and vertices.shape[1] == 3
assert faces.ndim == 2 and faces.shape[1] == 3, f"Faces should be a 2D array with shape (T, 3), but got {faces.shape}"
assert attr.ndim == 2 and attr.shape[1] in [1, 2, 3, 4], f'Vertex attribute only supports channels 1, 2, 3, 4, but got {attr.shape}'
assert vertices.shape[0] == attr.shape[0]
assert vertices.dtype == np.float32
assert faces.dtype == np.uint32 or faces.dtype == np.int32
assert attr.dtype == np.float32, "Attribute should be float32"
C = attr.shape[1]
prog = ctx.program_vertex_attribute(C)
transform = np.eye(4, np.float32) if transform is None else transform
# Create buffers
ibo = ctx.mgl_ctx.buffer(np.ascontiguousarray(faces, dtype='i4'))
vbo_vertices = ctx.mgl_ctx.buffer(np.ascontiguousarray(vertices, dtype='f4'))
vbo_attr = ctx.mgl_ctx.buffer(np.ascontiguousarray(attr, dtype='f4'))
vao = ctx.mgl_ctx.vertex_array(
prog,
[
(vbo_vertices, '3f', 'i_position'),
(vbo_attr, f'{C}f', 'i_attr'),
],
ibo,
mode=moderngl.TRIANGLES,
)
# Create framebuffer
image_tex = ctx.mgl_ctx.texture((width, height), C, dtype='f4', data=np.ascontiguousarray(image[::-1, :, :]) if image is not None else None)
depth_tex = ctx.mgl_ctx.depth_texture((width, height), data=np.ascontiguousarray(depth[::-1, :]) if depth is not None else None)
fbo = ctx.mgl_ctx.framebuffer(
color_attachments=[image_tex],
depth_attachment=depth_tex,
)
# Render
prog['u_mvp'].write(transform.transpose().copy().astype('f4'))
fbo.use()
fbo.viewport = (0, 0, width, height)
ctx.mgl_ctx.depth_func = '<'
ctx.mgl_ctx.enable(ctx.mgl_ctx.DEPTH_TEST)
if cull_backface:
ctx.mgl_ctx.enable(ctx.mgl_ctx.CULL_FACE)
else:
ctx.mgl_ctx.disable(ctx.mgl_ctx.CULL_FACE)
vao.render()
ctx.mgl_ctx.disable(ctx.mgl_ctx.DEPTH_TEST)
# Read
image = np.zeros((height, width, C), dtype='f4')
image_tex.read_into(image)
image = image[::-1, :, :]
if return_depth:
depth = np.zeros((height, width), dtype='f4')
depth_tex.read_into(depth)
depth = depth[::-1, :]
else:
depth = None
# Release
vao.release()
ibo.release()
vbo_vertices.release()
vbo_attr.release()
fbo.release()
image_tex.release()
depth_tex.release()
return image, depth
def rasterize_edges(
ctx: RastContext,
vertices: np.ndarray,
edges: np.ndarray,
attr: np.ndarray,
width: int,
height: int,
transform: np.ndarray = None,
line_width: float = 1.0,
return_depth: bool = False,
image: np.ndarray = None,
depth: np.ndarray = None
) -> Tuple[np.ndarray, ...]:
"""
Rasterize vertex attribute.
Args:
vertices (np.ndarray): [N, 3]
faces (np.ndarray): [T, 3]
attr (np.ndarray): [N, C]
width (int): width of rendered image
height (int): height of rendered image
transform (np.ndarray): [4, 4] model-view-projection matrix
line_width (float): width of line. Defaults to 1.0. NOTE: Values other than 1.0 may not work across all platforms.
cull_backface (bool): whether to cull backface
Returns:
image (np.ndarray): [H, W, C] rendered image
depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None.
"""
assert vertices.ndim == 2 and vertices.shape[1] == 3
assert edges.ndim == 2 and edges.shape[1] == 2, f"Edges should be a 2D array with shape (T, 2), but got {edges.shape}"
assert attr.ndim == 2 and attr.shape[1] in [1, 2, 3, 4], f'Vertex attribute only supports channels 1, 2, 3, 4, but got {attr.shape}'
assert vertices.shape[0] == attr.shape[0]
assert vertices.dtype == np.float32
assert edges.dtype == np.uint32 or edges.dtype == np.int32
assert attr.dtype == np.float32, "Attribute should be float32"
C = attr.shape[1]
prog = ctx.program_vertex_attribute(C)
transform = transform if transform is not None else np.eye(4, np.float32)
# Create buffers
ibo = ctx.mgl_ctx.buffer(np.ascontiguousarray(edges, dtype='i4'))
vbo_vertices = ctx.mgl_ctx.buffer(np.ascontiguousarray(vertices, dtype='f4'))
vbo_attr = ctx.mgl_ctx.buffer(np.ascontiguousarray(attr, dtype='f4'))
vao = ctx.mgl_ctx.vertex_array(
prog,
[
(vbo_vertices, '3f', 'i_position'),
(vbo_attr, f'{C}f', 'i_attr'),
],
ibo,
mode=moderngl.LINES,
)
# Create framebuffer
image_tex = ctx.mgl_ctx.texture((width, height), C, dtype='f4', data=np.ascontiguousarray(image[::-1, :, :]) if image is not None else None)
depth_tex = ctx.mgl_ctx.depth_texture((width, height), data=np.ascontiguousarray(depth[::-1, :]) if depth is not None else None)
fbo = ctx.mgl_ctx.framebuffer(
color_attachments=[image_tex],
depth_attachment=depth_tex,
)
# Render
prog['u_mvp'].write(transform.transpose().copy().astype('f4'))
fbo.use()
fbo.viewport = (0, 0, width, height)
ctx.mgl_ctx.depth_func = '<'
ctx.mgl_ctx.enable(ctx.mgl_ctx.DEPTH_TEST)
ctx.mgl_ctx.line_width = line_width
vao.render()
ctx.mgl_ctx.disable(ctx.mgl_ctx.DEPTH_TEST)
# Read
image = np.zeros((height, width, C), dtype='f4')
image_tex.read_into(image)
image = image[::-1, :, :]
if return_depth:
depth = np.zeros((height, width), dtype='f4')
depth_tex.read_into(depth)
depth = depth[::-1, :]
else:
depth = None
# Release
vao.release()
ibo.release()
vbo_vertices.release()
vbo_attr.release()
fbo.release()
image_tex.release()
depth_tex.release()
return image, depth
def texture(
ctx: RastContext,
uv: np.ndarray,
texture: np.ndarray,
interpolation: str= 'linear',
wrap: str = 'clamp'
) -> np.ndarray:
"""
Given an UV image, texturing from the texture map
"""
assert len(texture.shape) == 3 and 1 <= texture.shape[2] <= 4
assert uv.shape[2] == 2
height, width = uv.shape[:2]
texture_dtype = map_np_dtype(texture.dtype)
# Create VAO
screen_quad_vbo = ctx.mgl_ctx.buffer(np.array([[-1, -1], [1, -1], [1, 1], [-1, 1]], dtype='f4'))
screen_quad_ibo = ctx.mgl_ctx.buffer(np.array([0, 1, 2, 0, 2, 3], dtype=np.int32))
screen_quad_vao = ctx.mgl_ctx.vertex_array(ctx.program_texture(texture.shape[2]), [(screen_quad_vbo, '2f4', 'in_vert')], index_buffer=screen_quad_ibo, index_element_size=4)
# Create texture, set filter and bind. TODO: min mag filter, mipmap
texture_tex = ctx.mgl_ctx.texture((texture.shape[1], texture.shape[0]), texture.shape[2], dtype=texture_dtype, data=np.ascontiguousarray(texture))
if interpolation == 'linear':
texture_tex.filter = (moderngl.LINEAR, moderngl.LINEAR)
elif interpolation == 'nearest':
texture_tex.filter = (moderngl.NEAREST, moderngl.NEAREST)
texture_tex.use(location=0)
texture_uv = ctx.mgl_ctx.texture((width, height), 2, dtype='f4', data=np.ascontiguousarray(uv.astype('f4', copy=False)))
texture_uv.filter = (moderngl.NEAREST, moderngl.NEAREST)
texture_uv.use(location=1)
# Create render buffer and frame buffer
rb = ctx.mgl_ctx.renderbuffer((uv.shape[1], uv.shape[0]), texture.shape[2], dtype=texture_dtype)
fbo = ctx.mgl_ctx.framebuffer(color_attachments=[rb])
# Render
fbo.use()
fbo.viewport = (0, 0, width, height)
ctx.mgl_ctx.disable(ctx.mgl_ctx.BLEND)
screen_quad_vao.render()
# Read buffer
image_buffer = np.frombuffer(fbo.read(components=texture.shape[2], attachment=0, dtype=texture_dtype), dtype=texture_dtype).reshape((height, width, texture.shape[2]))
# Release
texture_tex.release()
rb.release()
fbo.release()
return image_buffer
def warp_image_by_depth(
ctx: RastContext,
src_depth: np.ndarray,
src_image: np.ndarray = None,
width: int = None,
height: int = None,
*,
extrinsics_src: np.ndarray = None,
extrinsics_tgt: np.ndarray = None,
intrinsics_src: np.ndarray = None,
intrinsics_tgt: np.ndarray = None,
near: float = 0.1,
far: float = 100.0,
cull_backface: bool = True,
ssaa: int = 1,
return_depth: bool = False,
) -> Tuple[np.ndarray, ...]:
"""
Warp image by depth map.
Args:
ctx (RastContext): rasterizer context
src_depth (np.ndarray): [H, W]
src_image (np.ndarray, optional): [H, W, C]. The image to warp. Defaults to None (use uv coordinates).
width (int, optional): width of the output image. None to use depth map width. Defaults to None.
height (int, optional): height of the output image. None to use depth map height. Defaults to None.
extrinsics_src (np.ndarray, optional): extrinsics matrix of the source camera. Defaults to None (identity).
extrinsics_tgt (np.ndarray, optional): extrinsics matrix of the target camera. Defaults to None (identity).
intrinsics_src (np.ndarray, optional): intrinsics matrix of the source camera. Defaults to None (use the same as intrinsics_tgt).
intrinsics_tgt (np.ndarray, optional): intrinsics matrix of the target camera. Defaults to None (use the same as intrinsics_src).
cull_backface (bool, optional): whether to cull backface. Defaults to True.
ssaa (int, optional): super sampling anti-aliasing. Defaults to 1.
Returns:
tgt_image (np.ndarray): [H, W, C] warped image (or uv coordinates if image is None).
tgt_depth (np.ndarray): [H, W] screen space depth, ranging from 0 to 1. If return_depth is False, it is None.
"""
assert src_depth.ndim == 2
if width is None:
width = src_depth.shape[1]
if height is None:
height = src_depth.shape[0]
if src_image is not None:
assert src_image.shape[-2:] == src_depth.shape[-2:], f'Shape of source image {src_image.shape} does not match shape of source depth {src_depth.shape}'
# set up default camera parameters
extrinsics_src = np.eye(4) if extrinsics_src is None else extrinsics_src
extrinsics_tgt = np.eye(4) if extrinsics_tgt is None else extrinsics_tgt
intrinsics_src = intrinsics_tgt if intrinsics_src is None else intrinsics_src
intrinsics_tgt = intrinsics_src if intrinsics_tgt is None else intrinsics_tgt
assert all(x is not None for x in [extrinsics_src, extrinsics_tgt, intrinsics_src, intrinsics_tgt]), "Make sure you have provided all the necessary camera parameters."
# check shapes
assert extrinsics_src.shape == (4, 4) and extrinsics_tgt.shape == (4, 4)
assert intrinsics_src.shape == (3, 3) and intrinsics_tgt.shape == (3, 3)
# convert to view and perspective matrices
view_tgt = transforms.extrinsics_to_view(extrinsics_tgt)
perspective_tgt = transforms.intrinsics_to_perspective(intrinsics_tgt, near=near, far=far)
# unproject depth map
uv, faces = utils.image_mesh(*src_depth.shape[-2:])
pts = transforms.unproject_cv(uv, src_depth.reshape(-1), extrinsics_src, intrinsics_src)
faces = mesh.triangulate(faces, vertices=pts)
# rasterize attributes
if src_image is not None:
attr = src_image.reshape(-1, src_image.shape[-1])
else:
attr = uv
tgt_image, tgt_depth = rasterize_triangle_faces(
ctx,
pts,
faces,
attr,
width * ssaa,
height * ssaa,
transform=perspective_tgt @ view_tgt,
cull_backface=cull_backface,
return_depth=return_depth,
)
if ssaa > 1:
tgt_image = tgt_image.reshape(height, ssaa, width, ssaa, -1).mean(axis=(1, 3))
tgt_depth = tgt_depth.reshape(height, ssaa, width, ssaa, -1).mean(axis=(1, 3)) if return_depth else None
return tgt_image, tgt_depth
def test():
"""
Test if rasterization works. It will render a cube with random colors and save it as a CHECKME.png file.
"""
ctx = RastContext(backend='egl')
vertices, faces = utils.cube(tri=True)
attr = np.random.rand(len(vertices), 3).astype(np.float32)
perspective = transforms.perspective(np.deg2rad(60), 1, 0.01, 100)
view = transforms.view_look_at(np.array([2, 2, 2]), np.array([0, 0, 0]), np.array([0, 1, 0]))
image, _ = rasterize_triangle_faces(
ctx,
vertices,
faces,
attr,
512, 512,
view=view,
projection=perspective,
cull_backface=True,
ssaa=1,
return_depth=True,
)
import cv2
cv2.imwrite('CHECKME.png', cv2.cvtColor((image.clip(0, 1) * 255).astype(np.uint8), cv2.COLOR_RGB2BGR))
+11
View File
@@ -0,0 +1,11 @@
#version 330
uniform sampler2D tex;
uniform sampler2D uv;
in vec2 scr_coord;
out vecN tex_color;
void main() {
tex_color = vecN(texture(tex, texture(uv, scr_coord).xy));
}
+9
View File
@@ -0,0 +1,9 @@
#version 330 core
in vec2 in_vert;
out vec2 scr_coord;
void main() {
scr_coord = in_vert * 0.5 + 0.5;
gl_Position = vec4(in_vert, 0., 1.);
}
@@ -0,0 +1,9 @@
#version 330
in vecN v_attr;
out vecN f_attr;
void main() {
f_attr = v_attr;
}
@@ -0,0 +1,13 @@
#version 330
uniform mat4 u_mvp;
in vec3 i_position;
in vecN i_attr;
out vecN v_attr;
void main() {
gl_Position = u_mvp * vec4(i_position, 1.0);
v_attr = i_attr;
}
+82
View File
@@ -0,0 +1,82 @@
from typing import *
import numpy as np
__all__ = ['linear_spline_interpolate']
def linear_spline_interpolate(x: np.ndarray, t: np.ndarray, s: np.ndarray, extrapolation_mode: Literal['constant', 'linear'] = 'constant') -> np.ndarray:
"""
Linear spline interpolation.
### Parameters:
- `x`: np.ndarray, shape (n, d): the values of data points.
- `t`: np.ndarray, shape (n,): the times of the data points.
- `s`: np.ndarray, shape (m,): the times to be interpolated.
- `extrapolation_mode`: str, the mode of extrapolation. 'constant' means extrapolate the boundary values, 'linear' means extrapolate linearly.
### Returns:
- `y`: np.ndarray, shape (..., m, d): the interpolated values.
"""
i = np.searchsorted(t, s, side='left')
if extrapolation_mode == 'constant':
prev = np.clip(i - 1, 0, len(t) - 1)
suc = np.clip(i, 0, len(t) - 1)
elif extrapolation_mode == 'linear':
prev = np.clip(i - 1, 0, len(t) - 2)
suc = np.clip(i, 1, len(t) - 1)
else:
raise ValueError(f'Invalid extrapolation_mode: {extrapolation_mode}')
u = (s - t[prev]) / np.maximum(t[suc] - t[prev], 1e-12)
y = u * x[suc] + (1 - u) * x[prev]
return y
def _solve_tridiagonal(a: np.ndarray, b: np.ndarray, c: np.ndarray, d: np.ndarray) -> np.ndarray:
n = b.shape[-1]
cc = np.zeros_like(b)
dd = np.zeros_like(b)
cc[..., 0] = c[..., 0] / b[..., 0]
dd[..., 0] = d[..., 0] / b[..., 0]
for i in range(1, n):
cc[..., i] = c[..., i] / (b[..., i] - a[..., i - 1] * cc[..., i - 1])
dd[..., i] = (d[..., i] - a[..., i - 1] * dd[..., i - 1]) / (b[..., i] - a[..., i - 1] * cc[..., i - 1])
x = np.zeros_like(b)
x[..., -1] = dd[..., -1]
for i in range(n - 2, -1, -1):
x[..., i] = dd[..., i] - cc[..., i] * x[..., i + 1]
return x
def cubic_spline_interpolate(x: np.ndarray, t: np.ndarray, s: np.ndarray, v0: np.ndarray = None, vn: np.ndarray = None) -> np.ndarray:
"""
Cubic spline interpolation.
### Parameters:
- `x`: np.ndarray, shape (..., n,): the x-coordinates of the data points.
- `t`: np.ndarray, shape (n,): the knot vector. NOTE: t must be sorted in ascending order.
- `s`: np.ndarray, shape (..., m,): the y-coordinates of the data points.
- `v0`: np.ndarray, shape (...,): the value of the derivative at the first knot, as the boundary condition. If None, it is set to zero.
- `vn`: np.ndarray, shape (...,): the value of the derivative at the last knot, as the boundary condition. If None, it is set to zero.
### Returns:
- `y`: np.ndarray, shape (..., m): the interpolated values.
"""
h = t[..., 1:] - t[..., :-1]
mu = h[..., :-1] / (h[..., :-1] + h[..., 1:])
la = 1 - mu
d = (x[..., 1:] - x[..., :-1]) / h
d = 6 * (d[..., 1:] - d[..., :-1]) / (t[..., 2:] - t[..., :-2])
mu = np.concatenate([mu, np.ones_like(mu[..., :1])], axis=-1)
la = np.concatenate([np.ones_like(la[..., :1]), la], axis=-1)
d = np.concatenate([(((x[..., 1] - x[..., 0]) / h[0] - v0) / h[0])[..., None], d, ((vn - (x[..., -1] - x[..., -2]) / h[-1]) / h[-1])[..., None]], axis=-1)
M = _solve_tridiagonal(mu, np.full_like(d, fill_value=2), la, d)
i = np.searchsorted(t, s, side='left')
File diff suppressed because it is too large Load Diff
+562
View File
@@ -0,0 +1,562 @@
import numpy as np
from typing import *
from numbers import Number
from ._helpers import batched
from . import transforms
from . import mesh
__all__ = [
'sliding_window_1d',
'sliding_window_nd',
'sliding_window_2d',
'max_pool_1d',
'max_pool_2d',
'max_pool_nd',
'depth_edge',
'depth_aliasing',
'interpolate',
'image_scrcoord',
'image_uv',
'image_pixel_center',
'image_pixel',
'image_mesh',
'image_mesh_from_depth',
'depth_to_normal',
'point_to_normal',
'chessboard',
'cube',
'square',
'camera_frustum',
'to4x4'
]
def sliding_window_1d(x: np.ndarray, window_size: int, stride: int, axis: int = -1):
"""
Return x view of the input array with x sliding window of the given kernel size and stride.
The sliding window is performed over the given axis, and the window dimension is append to the end of the output array's shape.
Args:
x (np.ndarray): input array with shape (..., axis_size, ...)
kernel_size (int): size of the sliding window
stride (int): stride of the sliding window
axis (int): axis to perform sliding window over
Returns:
a_sliding (np.ndarray): view of the input array with shape (..., n_windows, ..., kernel_size), where n_windows = (axis_size - kernel_size + 1) // stride
"""
assert x.shape[axis] >= window_size, f"kernel_size ({window_size}) is larger than axis_size ({x.shape[axis]})"
axis = axis % x.ndim
shape = (*x.shape[:axis], (x.shape[axis] - window_size + 1) // stride, *x.shape[axis + 1:], window_size)
strides = (*x.strides[:axis], stride * x.strides[axis], *x.strides[axis + 1:], x.strides[axis])
x_sliding = np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)
return x_sliding
def sliding_window_nd(x: np.ndarray, window_size: Tuple[int,...], stride: Tuple[int,...], axis: Tuple[int,...]) -> np.ndarray:
axis = [axis[i] % x.ndim for i in range(len(axis))]
for i in range(len(axis)):
x = sliding_window_1d(x, window_size[i], stride[i], axis[i])
return x
def sliding_window_2d(x: np.ndarray, window_size: Union[int, Tuple[int, int]], stride: Union[int, Tuple[int, int]], axis: Tuple[int, int] = (-2, -1)) -> np.ndarray:
if isinstance(window_size, int):
window_size = (window_size, window_size)
if isinstance(stride, int):
stride = (stride, stride)
return sliding_window_nd(x, window_size, stride, axis)
def max_pool_1d(x: np.ndarray, kernel_size: int, stride: int, padding: int = 0, axis: int = -1):
axis = axis % x.ndim
if padding > 0:
fill_value = np.nan if x.dtype.kind == 'f' else np.iinfo(x.dtype).min
padding_arr = np.full((*x.shape[:axis], padding, *x.shape[axis + 1:]), fill_value=fill_value, dtype=x.dtype)
x = np.concatenate([padding_arr, x, padding_arr], axis=axis)
a_sliding = sliding_window_1d(x, kernel_size, stride, axis)
max_pool = np.nanmax(a_sliding, axis=-1)
return max_pool
def max_pool_nd(x: np.ndarray, kernel_size: Tuple[int,...], stride: Tuple[int,...], padding: Tuple[int,...], axis: Tuple[int,...]) -> np.ndarray:
for i in range(len(axis)):
x = max_pool_1d(x, kernel_size[i], stride[i], padding[i], axis[i])
return x
def max_pool_2d(x: np.ndarray, kernel_size: Union[int, Tuple[int, int]], stride: Union[int, Tuple[int, int]], padding: Union[int, Tuple[int, int]], axis: Tuple[int, int] = (-2, -1)):
if isinstance(kernel_size, Number):
kernel_size = (kernel_size, kernel_size)
if isinstance(stride, Number):
stride = (stride, stride)
if isinstance(padding, Number):
padding = (padding, padding)
axis = tuple(axis)
return max_pool_nd(x, kernel_size, stride, padding, axis)
def depth_edge(depth: np.ndarray, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: np.ndarray = None) -> np.ndarray:
"""
Compute the edge mask of x depth map. The edge is defined as the pixels whose neighbors have x large difference in depth.
Args:
depth (np.ndarray): shape (..., height, width), linear depth map
atol (float): absolute tolerance
rtol (float): relative tolerance
Returns:
edge (np.ndarray): shape (..., height, width) of dtype torch.bool
"""
if mask is None:
diff = (max_pool_2d(depth, kernel_size, stride=1, padding=kernel_size // 2) + max_pool_2d(-depth, kernel_size, stride=1, padding=kernel_size // 2))
else:
diff = (max_pool_2d(np.where(mask, depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2) + max_pool_2d(np.where(mask, -depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2))
edge = np.zeros_like(depth, dtype=bool)
if atol is not None:
edge |= diff > atol
if rtol is not None:
edge |= diff / depth > rtol
return edge
def depth_aliasing(depth: np.ndarray, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: np.ndarray = None) -> np.ndarray:
"""
Compute the map that indicates the aliasing of x depth map. The aliasing is defined as the pixels which neither close to the maximum nor the minimum of its neighbors.
Args:
depth (np.ndarray): shape (..., height, width), linear depth map
atol (float): absolute tolerance
rtol (float): relative tolerance
Returns:
edge (np.ndarray): shape (..., height, width) of dtype torch.bool
"""
if mask is None:
diff_max = max_pool_2d(depth, kernel_size, stride=1, padding=kernel_size // 2) - depth
diff_min = max_pool_2d(-depth, kernel_size, stride=1, padding=kernel_size // 2) + depth
else:
diff_max = max_pool_2d(np.where(mask, depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2) - depth
diff_min = max_pool_2d(np.where(mask, -depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2) + depth
diff = np.minimum(diff_max, diff_min)
edge = np.zeros_like(depth, dtype=bool)
if atol is not None:
edge |= diff > atol
if rtol is not None:
edge |= diff / depth > rtol
return edge
def point_to_normal(point: np.ndarray, mask: np.ndarray = None) -> np.ndarray:
"""
Calculate normal map from point map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
Args:
point (np.ndarray): shape (height, width, 3), point map
Returns:
normal (np.ndarray): shape (height, width, 3), normal map.
"""
height, width = point.shape[-3:-1]
has_mask = mask is not None
if mask is None:
mask = np.ones_like(point[..., 0], dtype=bool)
mask_pad = np.zeros((height + 2, width + 2), dtype=bool)
mask_pad[1:-1, 1:-1] = mask
mask = mask_pad
pts = np.zeros((height + 2, width + 2, 3), dtype=point.dtype)
pts[1:-1, 1:-1, :] = point
up = pts[:-2, 1:-1, :] - pts[1:-1, 1:-1, :]
left = pts[1:-1, :-2, :] - pts[1:-1, 1:-1, :]
down = pts[2:, 1:-1, :] - pts[1:-1, 1:-1, :]
right = pts[1:-1, 2:, :] - pts[1:-1, 1:-1, :]
normal = np.stack([
np.cross(up, left, axis=-1),
np.cross(left, down, axis=-1),
np.cross(down, right, axis=-1),
np.cross(right, up, axis=-1),
])
normal = normal / (np.linalg.norm(normal, axis=-1, keepdims=True) + 1e-12)
valid = np.stack([
mask[:-2, 1:-1] & mask[1:-1, :-2],
mask[1:-1, :-2] & mask[2:, 1:-1],
mask[2:, 1:-1] & mask[1:-1, 2:],
mask[1:-1, 2:] & mask[:-2, 1:-1],
]) & mask[None, 1:-1, 1:-1]
normal = (normal * valid[..., None]).sum(axis=0)
normal = normal / (np.linalg.norm(normal, axis=-1, keepdims=True) + 1e-12)
if has_mask:
return normal, valid.any(axis=0)
else:
return normal
def depth_to_normal(depth: np.ndarray, intrinsics: np.ndarray, mask: np.ndarray = None) -> np.ndarray:
"""
Calculate normal map from depth map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
Args:
depth (np.ndarray): shape (height, width), linear depth map
intrinsics (np.ndarray): shape (3, 3), intrinsics matrix
Returns:
normal (np.ndarray): shape (height, width, 3), normal map.
"""
has_mask = mask is not None
height, width = depth.shape[-2:]
if mask is None:
mask = np.ones_like(depth, dtype=bool)
uv = image_uv(width=width, height=height, dtype=np.float32)
pts = transforms.unproject_cv(uv, depth, intrinsics=intrinsics, extrinsics=None)
return point_to_normal(pts, mask)
def interpolate(bary: np.ndarray, tri_id: np.ndarray, attr: np.ndarray, faces: np.ndarray) -> np.ndarray:
"""Interpolate with given barycentric coordinates and triangle indices
Args:
bary (np.ndarray): shape (..., 3), barycentric coordinates
tri_id (np.ndarray): int array of shape (...), triangle indices
attr (np.ndarray): shape (N, M), vertices attributes
faces (np.ndarray): int array of shape (T, 3), face vertex indices
Returns:
np.ndarray: shape (..., M) interpolated result
"""
faces_ = np.concatenate([np.zeros((1, 3), dtype=faces.dtype), faces + 1], axis=0)
attr_ = np.concatenate([np.zeros((1, attr.shape[1]), dtype=attr.dtype), attr], axis=0)
return np.sum(bary[..., None] * attr_[faces_[tri_id + 1]], axis=-2)
def image_scrcoord(
width: int,
height: int,
) -> np.ndarray:
"""
Get OpenGL's screen space coordinates, ranging in [0, 1].
[0, 0] is the bottom-left corner of the image.
Args:
width (int): image width
height (int): image height
Returns:
(np.ndarray): shape (height, width, 2)
"""
x, y = np.meshgrid(
np.linspace(0.5 / width, 1 - 0.5 / width, width, dtype=np.float32),
np.linspace(1 - 0.5 / height, 0.5 / height, height, dtype=np.float32),
indexing='xy'
)
return np.stack([x, y], axis=2)
def image_uv(
height: int,
width: int,
left: int = None,
top: int = None,
right: int = None,
bottom: int = None,
dtype: np.dtype = np.float32
) -> np.ndarray:
"""
Get image space UV grid, ranging in [0, 1].
>>> image_uv(10, 10):
[[[0.05, 0.05], [0.15, 0.05], ..., [0.95, 0.05]],
[[0.05, 0.15], [0.15, 0.15], ..., [0.95, 0.15]],
... ... ...
[[0.05, 0.95], [0.15, 0.95], ..., [0.95, 0.95]]]
Args:
width (int): image width
height (int): image height
Returns:
np.ndarray: shape (height, width, 2)
"""
if left is None: left = 0
if top is None: top = 0
if right is None: right = width
if bottom is None: bottom = height
u = np.linspace((left + 0.5) / width, (right - 0.5) / width, right - left, dtype=dtype)
v = np.linspace((top + 0.5) / height, (bottom - 0.5) / height, bottom - top, dtype=dtype)
u, v = np.meshgrid(u, v, indexing='xy')
return np.stack([u, v], axis=2)
def image_pixel_center(
height: int,
width: int,
left: int = None,
top: int = None,
right: int = None,
bottom: int = None,
dtype: np.dtype = np.float32
) -> np.ndarray:
"""
Get image pixel center coordinates, ranging in [0, width] and [0, height].
`image[i, j]` has pixel center coordinates `(j + 0.5, i + 0.5)`.
>>> image_pixel_center(10, 10):
[[[0.5, 0.5], [1.5, 0.5], ..., [9.5, 0.5]],
[[0.5, 1.5], [1.5, 1.5], ..., [9.5, 1.5]],
... ... ...
[[0.5, 9.5], [1.5, 9.5], ..., [9.5, 9.5]]]
Args:
width (int): image width
height (int): image height
Returns:
np.ndarray: shape (height, width, 2)
"""
if left is None: left = 0
if top is None: top = 0
if right is None: right = width
if bottom is None: bottom = height
u = np.linspace(left + 0.5, right - 0.5, right - left, dtype=dtype)
v = np.linspace(top + 0.5, bottom - 0.5, bottom - top, dtype=dtype)
u, v = np.meshgrid(u, v, indexing='xy')
return np.stack([u, v], axis=2)
def image_pixel(
height: int,
width: int,
left: int = None,
top: int = None,
right: int = None,
bottom: int = None,
dtype: np.dtype = np.int32
) -> np.ndarray:
"""
Get image pixel coordinates grid, ranging in [0, width - 1] and [0, height - 1].
`image[i, j]` has pixel center coordinates `(j, i)`.
>>> image_pixel_center(10, 10):
[[[0, 0], [1, 0], ..., [9, 0]],
[[0, 1.5], [1, 1], ..., [9, 1]],
... ... ...
[[0, 9.5], [1, 9], ..., [9, 9 ]]]
Args:
width (int): image width
height (int): image height
Returns:
np.ndarray: shape (height, width, 2)
"""
if left is None: left = 0
if top is None: top = 0
if right is None: right = width
if bottom is None: bottom = height
u = np.arange(left, right, dtype=dtype)
v = np.arange(top, bottom, dtype=dtype)
u, v = np.meshgrid(u, v, indexing='xy')
return np.stack([u, v], axis=2)
def image_mesh(
*image_attrs: np.ndarray,
mask: np.ndarray = None,
tri: bool = False,
return_indices: bool = False
) -> Tuple[np.ndarray, ...]:
"""
Get x quad mesh regarding image pixel uv coordinates as vertices and image grid as faces.
Args:
*image_attrs (np.ndarray): image attributes in shape (height, width, [channels])
mask (np.ndarray, optional): binary mask of shape (height, width), dtype=bool. Defaults to None.
Returns:
faces (np.ndarray): faces connecting neighboring pixels. shape (T, 4) if tri is False, else (T, 3)
*vertex_attrs (np.ndarray): vertex attributes in corresponding order with input image_attrs
indices (np.ndarray, optional): indices of vertices in the original mesh
"""
assert (len(image_attrs) > 0) or (mask is not None), "At least one of image_attrs or mask should be provided"
height, width = next(image_attrs).shape[:2] if mask is None else mask.shape
assert all(img.shape[:2] == (height, width) for img in image_attrs), "All image_attrs should have the same shape"
row_faces = np.stack([np.arange(0, width - 1, dtype=np.int32), np.arange(width, 2 * width - 1, dtype=np.int32), np.arange(1 + width, 2 * width, dtype=np.int32), np.arange(1, width, dtype=np.int32)], axis=1)
faces = (np.arange(0, (height - 1) * width, width, dtype=np.int32)[:, None, None] + row_faces[None, :, :]).reshape((-1, 4))
if mask is None:
if tri:
faces = mesh.triangulate(faces)
ret = [faces, *(img.reshape(-1, *img.shape[2:]) for img in image_attrs)]
if return_indices:
ret.append(np.arange(height * width, dtype=np.int32))
return tuple(ret)
else:
quad_mask = (mask[:-1, :-1] & mask[1:, :-1] & mask[1:, 1:] & mask[:-1, 1:]).ravel()
faces = faces[quad_mask]
if tri:
faces = mesh.triangulate(faces)
return mesh.remove_unreferenced_vertices(
faces,
*(x.reshape(-1, *x.shape[2:]) for x in image_attrs),
return_indices=return_indices
)
def image_mesh_from_depth(
depth: np.ndarray,
extrinsics: np.ndarray = None,
intrinsics: np.ndarray = None,
*vertice_attrs: np.ndarray,
atol: float = None,
rtol: float = None,
remove_by_depth: bool = False,
return_uv: bool = False,
return_indices: bool = False
) -> Tuple[np.ndarray, ...]:
"""
Get x triangle mesh by lifting depth map to 3D.
Args:
depth (np.ndarray): [H, W] depth map
extrinsics (np.ndarray, optional): [4, 4] extrinsics matrix. Defaults to None.
intrinsics (np.ndarray, optional): [3, 3] intrinsics matrix. Defaults to None.
*vertice_attrs (np.ndarray): [H, W, C] vertex attributes. Defaults to None.
atol (float, optional): absolute tolerance. Defaults to None.
rtol (float, optional): relative tolerance. Defaults to None.
triangles with vertices having depth difference larger than atol + rtol * depth will be marked.
remove_by_depth (bool, optional): whether to remove triangles with large depth difference. Defaults to True.
return_uv (bool, optional): whether to return uv coordinates. Defaults to False.
return_indices (bool, optional): whether to return indices of vertices in the original mesh. Defaults to False.
Returns:
vertices (np.ndarray): [N, 3] vertices
faces (np.ndarray): [T, 3] faces
*vertice_attrs (np.ndarray): [N, C] vertex attributes
image_uv (np.ndarray, optional): [N, 2] uv coordinates
ref_indices (np.ndarray, optional): [N] indices of vertices in the original mesh
"""
height, width = depth.shape
image_uv, image_face = image_mesh(height, width)
depth = depth.reshape(-1)
pts = transforms.unproject_cv(image_uv, depth, extrinsics, intrinsics)
image_face = mesh.triangulate(image_face, vertices=pts)
ref_indices = None
ret = []
if atol is not None or rtol is not None:
atol = 0 if atol is None else atol
rtol = 0 if rtol is None else rtol
mean = depth[image_face].mean(axis=1)
diff = np.max(np.abs(depth[image_face] - depth[image_face[:, [1, 2, 0]]]), axis=1)
mask = (diff <= atol + rtol * mean)
image_face_ = image_face[mask]
image_face_, ref_indices = mesh.remove_unreferenced_vertices(image_face_, return_indices=True)
remove = remove_by_depth and ref_indices is not None
if remove:
pts = pts[ref_indices]
image_face = image_face_
ret += [pts, image_face]
for attr in vertice_attrs:
ret.append(attr.reshape(-1, attr.shape[-1]) if not remove else attr.reshape(-1, attr.shape[-1])[ref_indices])
if return_uv:
ret.append(image_uv if not remove else image_uv[ref_indices])
if return_indices and ref_indices is not None:
ret.append(ref_indices)
return tuple(ret)
def chessboard(width: int, height: int, grid_size: int, color_a: np.ndarray, color_b: np.ndarray) -> np.ndarray:
"""get x chessboard image
Args:
width (int): image width
height (int): image height
grid_size (int): size of chessboard grid
color_a (np.ndarray): color of the grid at the top-left corner
color_b (np.ndarray): color in complementary grid cells
Returns:
image (np.ndarray): shape (height, width, channels), chessboard image
"""
x = np.arange(width) // grid_size
y = np.arange(height) // grid_size
mask = (x[None, :] + y[:, None]) % 2
image = (1 - mask[..., None]) * color_a + mask[..., None] * color_b
return image
def square(tri: bool = False) -> Tuple[np.ndarray, np.ndarray]:
"""
Get a square mesh of area 1 centered at origin in the xy-plane.
### Returns
vertices (np.ndarray): shape (4, 3)
faces (np.ndarray): shape (1, 4)
"""
vertices = np.array([
[-0.5, 0.5, 0], [0.5, 0.5, 0], [0.5, -0.5, 0], [-0.5, -0.5, 0] # v0-v1-v2-v3
], dtype=np.float32)
if tri:
faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32)
else:
faces = np.array([[0, 1, 2, 3]], dtype=np.int32)
return vertices, faces
def cube(tri: bool = False) -> Tuple[np.ndarray, np.ndarray]:
"""
Get x cube mesh of size 1 centered at origin.
### Parameters
tri (bool, optional): return triangulated mesh. Defaults to False, which returns quad mesh.
### Returns
vertices (np.ndarray): shape (8, 3)
faces (np.ndarray): shape (12, 3)
"""
vertices = np.array([
[-0.5, 0.5, 0.5], [0.5, 0.5, 0.5], [0.5, -0.5, 0.5], [-0.5, -0.5, 0.5], # v0-v1-v2-v3
[-0.5, 0.5, -0.5], [0.5, 0.5, -0.5], [0.5, -0.5, -0.5], [-0.5, -0.5, -0.5] # v4-v5-v6-v7
], dtype=np.float32).reshape((-1, 3))
faces = np.array([
[0, 1, 2, 3], # v0-v1-v2-v3 (front)
[4, 5, 1, 0], # v4-v5-v1-v0 (top)
[3, 2, 6, 7], # v3-v2-v6-v7 (bottom)
[5, 4, 7, 6], # v5-v4-v7-v6 (back)
[1, 5, 6, 2], # v1-v5-v6-v2 (right)
[4, 0, 3, 7] # v4-v0-v3-v7 (left)
], dtype=np.int32)
if tri:
faces = mesh.triangulate(faces, vertices=vertices)
return vertices, faces
def camera_frustum(extrinsics: np.ndarray, intrinsics: np.ndarray, depth: float = 1.0) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""
Get x triangle mesh of camera frustum.
"""
assert extrinsics.shape == (4, 4) and intrinsics.shape == (3, 3)
vertices = transforms.unproject_cv(
np.array([[0, 0], [0, 0], [0, 1], [1, 1], [1, 0]], dtype=np.float32),
np.array([0] + [depth] * 4, dtype=np.float32),
extrinsics,
intrinsics
).astype(np.float32)
edges = np.array([
[0, 1], [0, 2], [0, 3], [0, 4],
[1, 2], [2, 3], [3, 4], [4, 1]
], dtype=np.int32)
faces = np.array([
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 1],
[1, 2, 3],
[1, 3, 4]
], dtype=np.int32)
return vertices, edges, faces
+133
View File
@@ -0,0 +1,133 @@
import importlib
import itertools
import torch
__modules_all__ = {
'mesh': [
'triangulate',
'compute_face_normal',
'compute_face_angles',
'compute_vertex_normal',
'compute_vertex_normal_weighted',
'remove_unreferenced_vertices',
'remove_corrupted_faces',
'merge_duplicate_vertices',
'subdivide_mesh_simple',
'compute_face_tbn',
'compute_vertex_tbn',
'laplacian',
'laplacian_smooth_mesh',
'taubin_smooth_mesh',
'laplacian_hc_smooth_mesh',
],
'nerf': [
'get_rays',
'get_image_rays',
'get_mipnerf_cones',
'volume_rendering',
'bin_sample',
'importance_sample',
'nerf_render_rays',
'mipnerf_render_rays',
'nerf_render_view',
'mipnerf_render_view',
'InstantNGP',
],
'utils': [
'sliding_window_1d',
'sliding_window_2d',
'sliding_window_nd',
'image_uv',
'image_pixel_center',
'image_mesh',
'chessboard',
'depth_edge',
'depth_aliasing',
'image_mesh_from_depth',
'point_to_normal',
'depth_to_normal',
'masked_min',
'masked_max',
'bounding_rect'
],
'transforms': [
'perspective',
'perspective_from_fov',
'perspective_from_fov_xy',
'intrinsics_from_focal_center',
'intrinsics_from_fov',
'intrinsics_from_fov_xy',
'view_look_at',
'extrinsics_look_at',
'perspective_to_intrinsics',
'intrinsics_to_perspective',
'extrinsics_to_view',
'view_to_extrinsics',
'normalize_intrinsics',
'crop_intrinsics',
'pixel_to_uv',
'pixel_to_ndc',
'uv_to_pixel',
'project_depth',
'depth_buffer_to_linear',
'project_gl',
'project_cv',
'unproject_gl',
'unproject_cv',
'skew_symmetric',
'rotation_matrix_from_vectors',
'euler_axis_angle_rotation',
'euler_angles_to_matrix',
'matrix_to_euler_angles',
'matrix_to_quaternion',
'quaternion_to_matrix',
'matrix_to_axis_angle',
'axis_angle_to_matrix',
'axis_angle_to_quaternion',
'quaternion_to_axis_angle',
'slerp',
'interpolate_extrinsics',
'interpolate_view',
'extrinsics_to_essential',
'to4x4',
'rotation_matrix_2d',
'rotate_2d',
'translate_2d',
'scale_2d',
'apply_2d',
],
'rasterization': [
'RastContext',
'rasterize_triangle_faces',
'warp_image_by_depth',
'warp_image_by_forward_flow',
],
}
__all__ = list(itertools.chain(*__modules_all__.values()))
def __getattr__(name):
try:
return globals()[name]
except KeyError:
pass
try:
module_name = next(m for m in __modules_all__ if name in __modules_all__[m])
except StopIteration:
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
module = importlib.import_module(f'.{module_name}', __name__)
for key in __modules_all__[module_name]:
globals()[key] = getattr(module, key)
return globals()[name]
if __name__ == '__main__':
from .transforms import *
from .mesh import *
from .utils import *
from .nerf import *
from .rasterization import *
+102
View File
@@ -0,0 +1,102 @@
# decorator
import torch
from numbers import Number
import inspect
from functools import wraps
def get_device(args, kwargs):
device = None
for arg in (list(args) + list(kwargs.values())):
if isinstance(arg, torch.Tensor):
if device is None:
device = arg.device
elif device != arg.device:
raise ValueError("All tensors must be on the same device.")
return device
def get_args_order(func, args, kwargs):
"""
Get the order of the arguments of a function.
"""
names = inspect.getfullargspec(func).args
names_idx = {name: i for i, name in enumerate(names)}
args_order = []
kwargs_order = {}
for name, arg in kwargs.items():
if name in names:
kwargs_order[name] = names_idx[name]
names.remove(name)
for i, arg in enumerate(args):
if i < len(names):
args_order.append(names_idx[names[i]])
return args_order, kwargs_order
def broadcast_args(args, kwargs, args_dim, kwargs_dim):
spatial = []
for arg, arg_dim in zip(args + list(kwargs.values()), args_dim + list(kwargs_dim.values())):
if isinstance(arg, torch.Tensor) and arg_dim is not None:
arg_spatial = arg.shape[:arg.ndim-arg_dim]
if len(arg_spatial) > len(spatial):
spatial = [1] * (len(arg_spatial) - len(spatial)) + spatial
for j in range(len(arg_spatial)):
if spatial[-j] < arg_spatial[-j]:
if spatial[-j] == 1:
spatial[-j] = arg_spatial[-j]
else:
raise ValueError("Cannot broadcast arguments.")
for i, arg in enumerate(args):
if isinstance(arg, torch.Tensor) and args_dim[i] is not None:
args[i] = torch.broadcast_to(arg, [*spatial, *arg.shape[arg.ndim-args_dim[i]:]])
for key, arg in kwargs.items():
if isinstance(arg, torch.Tensor) and kwargs_dim[key] is not None:
kwargs[key] = torch.broadcast_to(arg, [*spatial, *arg.shape[arg.ndim-kwargs_dim[key]:]])
return args, kwargs, spatial
def batched(*dims):
"""
Decorator that allows a function to be called with batched arguments.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, device=torch.device('cpu'), **kwargs):
args = list(args)
# get arguments dimensions
args_order, kwargs_order = get_args_order(func, args, kwargs)
args_dim = [dims[i] for i in args_order]
kwargs_dim = {key: dims[i] for key, i in kwargs_order.items()}
# convert to torch tensor
device = get_device(args, kwargs) or device
for i, arg in enumerate(args):
if isinstance(arg, (Number, list, tuple)) and args_dim[i] is not None:
args[i] = torch.tensor(arg, device=device)
for key, arg in kwargs.items():
if isinstance(arg, (Number, list, tuple)) and kwargs_dim[key] is not None:
kwargs[key] = torch.tensor(arg, device=device)
# broadcast arguments
args, kwargs, spatial = broadcast_args(args, kwargs, args_dim, kwargs_dim)
for i, (arg, arg_dim) in enumerate(zip(args, args_dim)):
if isinstance(arg, torch.Tensor) and arg_dim is not None:
args[i] = arg.reshape([-1, *arg.shape[arg.ndim-arg_dim:]])
for key, arg in kwargs.items():
if isinstance(arg, torch.Tensor) and kwargs_dim[key] is not None:
kwargs[key] = arg.reshape([-1, *arg.shape[arg.ndim-kwargs_dim[key]:]])
# call function
results = func(*args, **kwargs)
type_results = type(results)
results = list(results) if isinstance(results, (tuple, list)) else [results]
# restore spatial dimensions
for i, result in enumerate(results):
results[i] = result.reshape([*spatial, *result.shape[1:]])
if type_results == tuple:
results = tuple(results)
elif type_results == list:
results = list(results)
else:
results = results[0]
return results
return wrapper
return decorator
+401
View File
@@ -0,0 +1,401 @@
import torch
import torch.nn.functional as F
from typing import *
from ._helpers import batched
__all__ = [
'triangulate',
'compute_face_normal',
'compute_face_angles',
'compute_vertex_normal',
'compute_vertex_normal_weighted',
'remove_unreferenced_vertices',
'remove_corrupted_faces',
'merge_duplicate_vertices',
'subdivide_mesh_simple',
'compute_face_tbn',
'compute_vertex_tbn',
'laplacian',
'laplacian_smooth_mesh',
'taubin_smooth_mesh',
'laplacian_hc_smooth_mesh',
]
def triangulate(
faces: torch.Tensor,
vertices: torch.Tensor = None,
backslash: bool = None
) -> torch.Tensor:
"""
Triangulate a polygonal mesh.
Args:
faces (torch.Tensor): [..., L, P] polygonal faces
vertices (torch.Tensor, optional): [..., N, 3] 3-dimensional vertices.
If given, the triangulation is performed according to the distance
between vertices. Defaults to None.
backslash (torch.Tensor, optional): [..., L] boolean array indicating
how to triangulate the quad faces. Defaults to None.
Returns:
(torch.Tensor): [L * (P - 2), 3] triangular faces
"""
if faces.shape[-1] == 3:
return faces
P = faces.shape[-1]
if vertices is not None:
assert faces.shape[-1] == 4, "now only support quad mesh"
if backslash is None:
faces_idx = faces.long()
backslash = torch.norm(vertices[faces_idx[..., 0]] - vertices[faces_idx[..., 2]], p=2, dim=-1) < \
torch.norm(vertices[faces_idx[..., 1]] - vertices[faces_idx[..., 3]], p=2, dim=-1)
if backslash is None:
loop_indice = torch.stack([
torch.zeros(P - 2, dtype=int),
torch.arange(1, P - 1, 1, dtype=int),
torch.arange(2, P, 1, dtype=int)
], axis=1)
return faces[:, loop_indice].reshape(-1, 3)
else:
assert faces.shape[-1] == 4, "now only support quad mesh"
if isinstance(backslash, bool):
if backslash:
faces = faces[:, [0, 1, 2, 0, 2, 3]].reshape(-1, 3)
else:
faces = faces[:, [0, 1, 3, 3, 1, 2]].reshape(-1, 3)
else:
faces = torch.where(
backslash[:, None],
faces[:, [0, 1, 2, 0, 2, 3]],
faces[:, [0, 1, 3, 3, 1, 2]]
).reshape(-1, 3)
return faces
@batched(2, None)
def compute_face_normal(
vertices: torch.Tensor,
faces: torch.Tensor
) -> torch.Tensor:
"""
Compute face normals of a triangular mesh
Args:
vertices (torch.Tensor): [..., N, 3] 3-dimensional vertices
faces (torch.Tensor): [..., T, 3] triangular face indices
Returns:
normals (torch.Tensor): [..., T, 3] face normals
"""
N = vertices.shape[0]
index = torch.arange(N)[:, None]
normal = torch.cross(
vertices[index, faces[..., 1].long()] - vertices[index, faces[..., 0].long()],
vertices[index, faces[..., 2].long()] - vertices[index, faces[..., 0].long()],
dim=-1
)
return F.normalize(normal, p=2, dim=-1)
@batched(2, None)
def compute_face_angles(
vertices: torch.Tensor,
faces: torch.Tensor
) -> torch.Tensor:
"""
Compute face angles of a triangular mesh
Args:
vertices (torch.Tensor): [..., N, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
Returns:
angles (torch.Tensor): [..., T, 3] face angles
"""
face_angles = []
for i in range(3):
edge1 = torch.index_select(vertices, dim=-2, index=faces[:, (i + 1) % 3]) - torch.index_select(vertices, dim=-2, index=faces[:, i])
edge2 = torch.index_select(vertices, dim=-2, index=faces[:, (i + 2) % 3]) - torch.index_select(vertices, dim=-2, index=faces[:, i])
face_angle = torch.arccos(torch.sum(F.normalize(edge1, p=2, dim=-1) * F.normalize(edge2, p=2, dim=-1), dim=-1))
face_angles.append(face_angle)
face_angles = torch.stack(face_angles, dim=-1)
return face_angles
@batched(2, None, 2)
def compute_vertex_normal(
vertices: torch.Tensor,
faces: torch.Tensor,
face_normal: torch.Tensor = None
) -> torch.Tensor:
"""
Compute vertex normals of a triangular mesh by averaging neightboring face normals
Args:
vertices (torch.Tensor): [..., N, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
face_normal (torch.Tensor, optional): [..., T, 3] face normals.
None to compute face normals from vertices and faces. Defaults to None.
Returns:
normals (torch.Tensor): [..., N, 3] vertex normals
"""
N = vertices.shape[0]
assert faces.shape[-1] == 3, "Only support triangular mesh"
if face_normal is None:
face_normal = compute_face_normal(vertices, faces)
face_normal = face_normal[:, :, None, :].expand(-1, -1, 3, -1).flatten(-3, -2)
faces = faces.flatten()
vertex_normal = torch.index_put(torch.zeros_like(vertices), (torch.arange(N)[:, None], faces[None, :]), face_normal, accumulate=True)
vertex_normal = F.normalize(vertex_normal, p=2, dim=-1)
return vertex_normal
@batched(2, None, 2)
def compute_vertex_normal_weighted(
vertices: torch.Tensor,
faces: torch.Tensor,
face_normal: torch.Tensor = None
) -> torch.Tensor:
"""
Compute vertex normals of a triangular mesh by weighted sum of neightboring face normals
according to the angles
Args:
vertices (torch.Tensor): [..., N, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
face_normal (torch.Tensor, optional): [..., T, 3] face normals.
None to compute face normals from vertices and faces. Defaults to None.
Returns:
normals (torch.Tensor): [..., N, 3] vertex normals
"""
N = vertices.shape[0]
if face_normal is None:
face_normal = compute_face_normal(vertices, faces)
face_angle = compute_face_angles(vertices, faces)
face_normal = face_normal[:, :, None, :].expand(-1, -1, 3, -1) * face_angle[..., None]
vertex_normal = torch.index_put(torch.zeros_like(vertices), (torch.arange(N)[:, None], faces.view(N, -1)), face_normal.view(N, -1, 3), accumulate=True)
vertex_normal = F.normalize(vertex_normal, p=2, dim=-1)
return vertex_normal
def remove_unreferenced_vertices(
faces: torch.Tensor,
*vertice_attrs,
return_indices: bool = False
) -> Tuple[torch.Tensor, ...]:
"""
Remove unreferenced vertices of a mesh.
Unreferenced vertices are removed, and the face indices are updated accordingly.
Args:
faces (torch.Tensor): [T, P] face indices
*vertice_attrs: vertex attributes
Returns:
faces (torch.Tensor): [T, P] face indices
*vertice_attrs: vertex attributes
indices (torch.Tensor, optional): [N] indices of vertices that are kept. Defaults to None.
"""
P = faces.shape[-1]
fewer_indices, inv_map = torch.unique(faces, return_inverse=True)
faces = inv_map.to(torch.int32).reshape(-1, P)
ret = [faces]
for attr in vertice_attrs:
ret.append(attr[fewer_indices])
if return_indices:
ret.append(fewer_indices)
return tuple(ret)
def remove_corrupted_faces(
faces: torch.Tensor
) -> torch.Tensor:
"""
Remove corrupted faces (faces with duplicated vertices)
Args:
faces (torch.Tensor): [T, 3] triangular face indices
Returns:
torch.Tensor: [T_, 3] triangular face indices
"""
corrupted = (faces[:, 0] == faces[:, 1]) | (faces[:, 1] == faces[:, 2]) | (faces[:, 2] == faces[:, 0])
return faces[~corrupted]
def merge_duplicate_vertices(
vertices: torch.Tensor,
faces: torch.Tensor,
tol: float = 1e-6
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Merge duplicate vertices of a triangular mesh.
Duplicate vertices are merged by selecte one of them, and the face indices are updated accordingly.
Args:
vertices (torch.Tensor): [N, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
tol (float, optional): tolerance for merging. Defaults to 1e-6.
Returns:
vertices (torch.Tensor): [N_, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
"""
vertices_round = torch.round(vertices / tol)
uni, uni_inv = torch.unique(vertices_round, dim=0, return_inverse=True)
uni[uni_inv] = vertices
faces = uni_inv[faces]
return uni, faces
def subdivide_mesh_simple(vertices: torch.Tensor, faces: torch.Tensor, n: int = 1) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Subdivide a triangular mesh by splitting each triangle into 4 smaller triangles.
NOTE: All original vertices are kept, and new vertices are appended to the end of the vertex list.
Args:
vertices (torch.Tensor): [N, 3] 3-dimensional vertices
faces (torch.Tensor): [T, 3] triangular face indices
n (int, optional): number of subdivisions. Defaults to 1.
Returns:
vertices (torch.Tensor): [N_, 3] subdivided 3-dimensional vertices
faces (torch.Tensor): [4 * T, 3] subdivided triangular face indices
"""
for _ in range(n):
edges = torch.stack([faces[:, [0, 1]], faces[:, [1, 2]], faces[:, [2, 0]]], dim=0)
edges = torch.sort(edges, dim=2)
uni_edges, uni_inv = torch.unique(edges, return_inverse=True, dim=0)
midpoints = (vertices[uni_edges[:, 0]] + vertices[uni_edges[:, 1]]) / 2
n_vertices = vertices.shape[0]
vertices = torch.cat([vertices, midpoints], dim=0)
faces = torch.cat([
torch.stack([faces[:, 0], n_vertices + uni_inv[0], n_vertices + uni_inv[2]], axis=1),
torch.stack([faces[:, 1], n_vertices + uni_inv[1], n_vertices + uni_inv[0]], axis=1),
torch.stack([faces[:, 2], n_vertices + uni_inv[2], n_vertices + uni_inv[1]], axis=1),
torch.stack([n_vertices + uni_inv[0], n_vertices + uni_inv[1], n_vertices + uni_inv[2]], axis=1),
], dim=0)
return vertices, faces
def compute_face_tbn(pos: torch.Tensor, faces_pos: torch.Tensor, uv: torch.Tensor, faces_uv: torch.Tensor, eps: float = 1e-7) -> torch.Tensor:
"""compute TBN matrix for each face
Args:
pos (torch.Tensor): shape (..., N_pos, 3), positions
faces_pos (torch.Tensor): shape(T, 3)
uv (torch.Tensor): shape (..., N_uv, 3) uv coordinates,
faces_uv (torch.Tensor): shape(T, 3)
Returns:
torch.Tensor: (..., T, 3, 3) TBN matrix for each face. Note TBN vectors are normalized but not necessarily orthognal
"""
e01 = torch.index_select(pos, dim=-2, index=faces_pos[:, 1]) - torch.index_select(pos, dim=-2, index=faces_pos[:, 0])
e02 = torch.index_select(pos, dim=-2, index=faces_pos[:, 2]) - torch.index_select(pos, dim=-2, index=faces_pos[:, 0])
uv01 = torch.index_select(uv, dim=-2, index=faces_uv[:, 1]) - torch.index_select(uv, dim=-2, index=faces_uv[:, 0])
uv02 = torch.index_select(uv, dim=-2, index=faces_uv[:, 2]) - torch.index_select(uv, dim=-2, index=faces_uv[:, 0])
normal = torch.cross(e01, e02)
tangent_bitangent = torch.stack([e01, e02], dim=-1) @ torch.inverse(torch.stack([uv01, uv02], dim=-1))
tbn = torch.cat([tangent_bitangent, normal.unsqueeze(-1)], dim=-1)
tbn = tbn / (torch.norm(tbn, p=2, dim=-2, keepdim=True) + eps)
return tbn
def compute_vertex_tbn(faces_topo: torch.Tensor, pos: torch.Tensor, faces_pos: torch.Tensor, uv: torch.Tensor, faces_uv: torch.Tensor) -> torch.Tensor:
"""compute TBN matrix for each face
Args:
faces_topo (torch.Tensor): (T, 3), face indice of topology
pos (torch.Tensor): shape (..., N_pos, 3), positions
faces_pos (torch.Tensor): shape(T, 3)
uv (torch.Tensor): shape (..., N_uv, 3) uv coordinates,
faces_uv (torch.Tensor): shape(T, 3)
Returns:
torch.Tensor: (..., V, 3, 3) TBN matrix for each face. Note TBN vectors are normalized but not necessarily orthognal
"""
n_vertices = faces_topo.max().item() + 1
n_tri = faces_topo.shape[-2]
batch_shape = pos.shape[:-2]
face_tbn = compute_face_tbn(pos, faces_pos, uv, faces_uv) # (..., T, 3, 3)
face_tbn = face_tbn[..., :, None, :, :].repeat(*[1] * len(batch_shape), 1, 3, 1, 1).view(*batch_shape, n_tri * 3, 3, 3) # (..., T * 3, 3, 3)
vertex_tbn = torch.index_add(torch.zeros(*batch_shape, n_vertices, 3, 3).to(face_tbn), dim=-3, index=faces_topo.view(-1), source=face_tbn)
vertex_tbn = vertex_tbn / (torch.norm(vertex_tbn, p=2, dim=-2, keepdim=True) + 1e-7)
return vertex_tbn
def laplacian(vertices: torch.Tensor, faces: torch.Tensor, weight: str = 'uniform') -> torch.Tensor:
"""Laplacian smooth with cotangent weights
Args:
vertices (torch.Tensor): shape (..., N, 3)
faces (torch.Tensor): shape (T, 3)
weight (str): 'uniform' or 'cotangent'
"""
sum_verts = torch.zeros_like(vertices) # (..., N, 3)
sum_weights = torch.zeros(*vertices.shape[:-1]).to(vertices) # (..., N)
face_verts = torch.index_select(vertices, -2, faces.view(-1)).view(*vertices.shape[:-2], *faces.shape, vertices.shape[-1]) # (..., T, 3)
if weight == 'cotangent':
for i in range(3):
e1 = face_verts[..., (i + 1) % 3, :] - face_verts[..., i, :]
e2 = face_verts[..., (i + 2) % 3, :] - face_verts[..., i, :]
cot_angle = (e1 * e2).sum(dim=-1) / torch.cross(e1, e2, dim=-1).norm(p=2, dim=-1) # (..., T, 3)
sum_verts = torch.index_add(sum_verts, -2, faces[:, (i + 1) % 3], face_verts[..., (i + 2) % 3, :] * cot_angle[..., None])
sum_weights = torch.index_add(sum_weights, -1, faces[:, (i + 1) % 3], cot_angle)
sum_verts = torch.index_add(sum_verts, -2, faces[:, (i + 2) % 3], face_verts[..., (i + 1) % 3, :] * cot_angle[..., None])
sum_weights = torch.index_add(sum_weights, -1, faces[:, (i + 2) % 3], cot_angle)
elif weight == 'uniform':
for i in range(3):
sum_verts = torch.index_add(sum_verts, -2, faces[:, i], face_verts[..., (i + 1) % 3, :])
sum_weights = torch.index_add(sum_weights, -1, faces[:, i], torch.ones_like(face_verts[..., i, 0]))
else:
raise NotImplementedError
return sum_verts / (sum_weights[..., None] + 1e-7)
def laplacian_smooth_mesh(vertices: torch.Tensor, faces: torch.Tensor, weight: str = 'uniform', times: int = 5) -> torch.Tensor:
"""Laplacian smooth with cotangent weights
Args:
vertices (torch.Tensor): shape (..., N, 3)
faces (torch.Tensor): shape (T, 3)
weight (str): 'uniform' or 'cotangent'
"""
for _ in range(times):
vertices = laplacian(vertices, faces, weight)
return vertices
def taubin_smooth_mesh(vertices: torch.Tensor, faces: torch.Tensor, lambda_: float = 0.5, mu_: float = -0.51) -> torch.Tensor:
"""Taubin smooth mesh
Args:
vertices (torch.Tensor): _description_
faces (torch.Tensor): _description_
lambda_ (float, optional): _description_. Defaults to 0.5.
mu_ (float, optional): _description_. Defaults to -0.51.
Returns:
torch.Tensor: _description_
"""
pt = vertices + lambda_ * laplacian_smooth_mesh(vertices, faces)
p = pt + mu_ * laplacian_smooth_mesh(pt, faces)
return p
def laplacian_hc_smooth_mesh(vertices: torch.Tensor, faces: torch.Tensor, times: int = 5, alpha: float = 0.5, beta: float = 0.5, weight: str = 'uniform'):
"""HC algorithm from Improved Laplacian Smoothing of Noisy Surface Meshes by J.Vollmer et al.
"""
p = vertices
for i in range(times):
q = p
p = laplacian_smooth_mesh(vertices, faces, weight)
b = p - (alpha * vertices + (1 - alpha) * q)
p = p - (beta * b + (1 - beta) * laplacian_smooth_mesh(b, faces, weight)) * 0.8
return p
+749
View File
@@ -0,0 +1,749 @@
from typing import *
from numbers import Number
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import Tensor
from .utils import image_uv
__all__ = [
'get_rays',
'get_image_rays',
'get_mipnerf_cones',
'volume_rendering',
'bin_sample',
'importance_sample',
'nerf_render_rays',
'mipnerf_render_rays',
'nerf_render_view',
'mipnerf_render_view',
'InstantNGP',
]
def get_rays(extrinsics: Tensor, intrinsics: Tensor, uv: Tensor) -> Tuple[Tensor, Tensor]:
"""
Args:
extrinsics: (..., 4, 4) extrinsics matrices.
intrinsics: (..., 3, 3) intrinsics matrices.
uv: (..., n_rays, 2) uv coordinates of the rays.
Returns:
rays_o: (..., 1, 3) ray origins
rays_d: (..., n_rays, 3) ray directions.
NOTE: ray directions are NOT normalized. They actuallys makes rays_o + rays_d * z = world coordinates, where z is the depth.
"""
uvz = torch.cat([uv, torch.ones_like(uv[..., :1])], dim=-1).to(extrinsics) # (n_batch, n_views, n_rays, 3)
with torch.cuda.amp.autocast(enabled=False):
inv_transformation = (intrinsics @ extrinsics[..., :3, :3]).inverse()
inv_extrinsics = extrinsics.inverse()
rays_d = uvz @ inv_transformation.transpose(-1, -2)
rays_o = inv_extrinsics[..., None, :3, 3] # (n_batch, n_views, 1, 3)
return rays_o, rays_d
def get_image_rays(extrinsics: Tensor, intrinsics: Tensor, width: int, height: int) -> Tuple[Tensor, Tensor]:
"""
Args:
extrinsics: (..., 4, 4) extrinsics matrices.
intrinsics: (..., 3, 3) intrinsics matrices.
width: width of the image.
height: height of the image.
Returns:
rays_o: (..., 1, 1, 3) ray origins
rays_d: (..., height, width, 3) ray directions.
NOTE: ray directions are NOT normalized. They actuallys makes rays_o + rays_d * z = world coordinates, where z is the depth.
"""
uv = image_uv(height, width).to(extrinsics).flatten(0, 1)
rays_o, rays_d = get_rays(extrinsics, intrinsics, uv)
rays_o = rays_o.unflatten(-2, (1, 1))
rays_d = rays_d.unflatten(-2, (height, width))
return rays_o, rays_d
def get_mipnerf_cones(rays_o: Tensor, rays_d: Tensor, z_vals: Tensor, pixel_width: Tensor) -> Tuple[Tensor, Tensor]:
"""
Args:
rays_o: (..., n_rays, 3) ray origins
rays_d: (..., n_rays, 3) ray directions.
z_vals: (..., n_rays, n_samples) z values.
pixel_width: (...) pixel width. = 1 / (normalized focal length * width)
Returns:
mu: (..., n_rays, n_samples, 3) cone mu.
sigma: (..., n_rays, n_samples, 3, 3) cone sigma.
"""
t_mu = (z_vals[..., 1:] + z_vals[..., :-1]).mul_(0.5)
t_delta = (z_vals[..., 1:] - z_vals[..., :-1]).mul_(0.5)
t_mu_square = t_mu.square()
t_delta_square = t_delta.square()
t_delta_quad = t_delta_square.square()
mu_t = t_mu + 2.0 * t_mu * t_delta_square / (3.0 * t_mu_square + t_delta_square)
sigma_t = t_delta_square / 3.0 - (4.0 / 15.0) * t_delta_quad / (3.0 * t_mu_square + t_delta_square).square() * (12.0 * t_mu_square - t_delta_square)
sigma_r = (pixel_width[..., None, None].square() / 3.0) * (t_mu_square / 4.0 + (5.0 / 12.0) * t_delta_square - (4.0 / 15.0) * t_delta_quad / (3.0 * t_mu_square + t_delta_square))
points_mu = rays_o[:, :, :, None, :] + rays_d[:, :, :, None, :] * mu_t[..., None]
d_dt = rays_d[..., :, None] * rays_d[..., None, :] # (..., n_rays, 3, 3)
points_sigma = sigma_t[..., None, None] * d_dt[..., None, :, :] + sigma_r[..., None, None] * (torch.eye(3).to(rays_o) - d_dt[..., None, :, :])
return points_mu, points_sigma
def get_pixel_width(intrinsics: Tensor, width: int, height: int) -> Tensor:
"""
Args:
intrinsics: (..., 3, 3) intrinsics matrices.
width: width of the image.
height: height of the image.
Returns:
pixel_width: (...) pixel width. = 1 / (normalized focal length * width)
"""
assert width == height, "Currently, only square images are supported."
pixel_width = torch.reciprocal((intrinsics[..., 0, 0] * intrinsics[..., 1, 1]).sqrt() * width)
return pixel_width
def volume_rendering(color: Tensor, sigma: Tensor, z_vals: Tensor, ray_length: Tensor, rgb: bool = True, depth: bool = True) -> Tuple[Tensor, Tensor, Tensor]:
"""
Given color, sigma and z_vals (linear depth of the sampling points), render the volume.
NOTE: By default, color and sigma should have one less sample than z_vals, in correspondence with the average value in intervals.
If queried color are aligned with z_vals, we use trapezoidal rule to calculate the average values in intervals.
Args:
color: (..., n_samples or n_samples - 1, 3) color values.
sigma: (..., n_samples or n_samples - 1) density values.
z_vals: (..., n_samples) z values.
ray_length: (...) length of the ray
Returns:
rgb: (..., 3) rendered color values.
depth: (...) rendered depth values.
weights (..., n_samples) weights.
"""
dists = (z_vals[..., 1:] - z_vals[..., :-1]) * ray_length[..., None]
if color.shape[-2] == z_vals.shape[-1]:
color = (color[..., 1:, :] + color[..., :-1, :]).mul_(0.5)
sigma = (sigma[..., 1:] + sigma[..., :-1]).mul_(0.5)
sigma_delta = sigma * dists
transparancy = (-torch.cat([torch.zeros_like(sigma_delta[..., :1]), sigma_delta[..., :-1]], dim=-1).cumsum(dim=-1)).exp_() # First cumsum then exp for numerical stability
alpha = 1.0 - (-sigma_delta).exp_()
weights = alpha * transparancy
if rgb:
rgb = torch.sum(weights[..., None] * color, dim=-2) if rgb else None
if depth:
z_vals = (z_vals[..., 1:] + z_vals[..., :-1]).mul_(0.5)
depth = torch.sum(weights * z_vals, dim=-1) / weights.sum(dim=-1).clamp_min_(1e-8) if depth else None
return rgb, depth, weights
def neus_volume_rendering(color: Tensor, sdf: Tensor, s: torch.Tensor, z_vals: Tensor = None, rgb: bool = True, depth: bool = True) -> Tuple[Tensor, Tensor, Tensor]:
"""
Given color, sdf values and z_vals (linear depth of the sampling points), do volume rendering. (NeuS)
Args:
color: (..., n_samples or n_samples - 1, 3) color values.
sdf: (..., n_samples) sdf values.
s: (..., n_samples) S values of S-density function in NeuS. The standard deviation of such S-density distribution is 1 / s.
z_vals: (..., n_samples) z values.
ray_length: (...) length of the ray
Returns:
rgb: (..., 3) rendered color values.
depth: (...) rendered depth values.
weights (..., n_samples) weights.
"""
if color.shape[-2] == z_vals.shape[-1]:
color = (color[..., 1:, :] + color[..., :-1, :]).mul_(0.5)
sigmoid_sdf = torch.sigmoid(s * sdf)
alpha = F.relu(1 - sigmoid_sdf[..., :-1] / sigmoid_sdf[..., :-1])
transparancy = torch.cumprod(torch.cat([torch.ones_like(alpha[..., :1]), alpha], dim=-1), dim=-1)
weights = alpha * transparancy
if rgb:
rgb = torch.sum(weights[..., None] * color, dim=-2) if rgb else None
if depth:
z_vals = (z_vals[..., 1:] + z_vals[..., :-1]).mul_(0.5)
depth = torch.sum(weights * z_vals, dim=-1) / weights.sum(dim=-1).clamp_min_(1e-8) if depth else None
return rgb, depth, weights
def bin_sample(size: Union[torch.Size, Tuple[int, ...]], n_samples: int, min_value: Number, max_value: Number, spacing: Literal['linear', 'inverse_linear'], dtype: torch.dtype = None, device: torch.device = None) -> Tensor:
"""
Uniformly (or uniformly in inverse space) sample z values in `n_samples` bins in range [min_value, max_value].
Args:
size: size of the rays
n_samples: number of samples to be sampled, also the number of bins
min_value: minimum value of the range
max_value: maximum value of the range
space: 'linear' or 'inverse_linear'. If 'inverse_linear', the sampling is uniform in inverse space.
Returns:
z_rand: (*size, n_samples) sampled z values, sorted in ascending order.
"""
if spacing == 'linear':
pass
elif spacing == 'inverse_linear':
min_value = 1.0 / min_value
max_value = 1.0 / max_value
bin_length = (max_value - min_value) / n_samples
z_rand = (torch.rand(*size, n_samples, device=device, dtype=dtype) - 0.5) * bin_length + torch.linspace(min_value + bin_length * 0.5, max_value - bin_length * 0.5, n_samples, device=device, dtype=dtype)
if spacing == 'inverse_linear':
z_rand = 1.0 / z_rand
return z_rand
def importance_sample(z_vals: Tensor, weights: Tensor, n_samples: int) -> Tuple[Tensor, Tensor]:
"""
Importance sample z values.
NOTE: By default, weights should have one less sample than z_vals, in correspondence with the intervals.
If weights has the same number of samples as z_vals, we use trapezoidal rule to calculate the average weights in intervals.
Args:
z_vals: (..., n_rays, n_input_samples) z values, sorted in ascending order.
weights: (..., n_rays, n_input_samples or n_input_samples - 1) weights.
n_samples: number of output samples for importance sampling.
Returns:
z_importance: (..., n_rays, n_samples) importance sampled z values, unsorted.
"""
if weights.shape[-1] == z_vals.shape[-1]:
weights = (weights[..., 1:] + weights[..., :-1]).mul_(0.5)
weights = weights / torch.sum(weights, dim=-1, keepdim=True) # (..., n_rays, n_input_samples - 1)
bins_a, bins_b = z_vals[..., :-1], z_vals[..., 1:]
pdf = weights / torch.sum(weights, dim=-1, keepdim=True) # (..., n_rays, n_input_samples - 1)
cdf = torch.cumsum(pdf, dim=-1)
u = torch.rand(*z_vals.shape[:-1], n_samples, device=z_vals.device, dtype=z_vals.dtype)
inds = torch.searchsorted(cdf, u, right=True).clamp(0, cdf.shape[-1] - 1) # (..., n_rays, n_samples)
bins_a = torch.gather(bins_a, dim=-1, index=inds)
bins_b = torch.gather(bins_b, dim=-1, index=inds)
z_importance = bins_a + (bins_b - bins_a) * torch.rand_like(u)
return z_importance
def nerf_render_rays(
nerf: Union[Callable[[Tensor, Tensor], Tuple[Tensor, Tensor]], Tuple[Callable[[Tensor], Tuple[Tensor, Tensor]], Callable[[Tensor], Tuple[Tensor, Tensor]]]],
rays_o: Tensor, rays_d: Tensor,
*,
return_dict: bool = False,
n_coarse: int = 64, n_fine: int = 64,
near: float = 0.1, far: float = 100.0,
z_spacing: Literal['linear', 'inverse_linear'] = 'linear',
):
"""
NeRF rendering of rays. Note that it supports arbitrary batch dimensions (denoted as `...`)
Args:
nerf: nerf model, which takes (points, directions) as input and returns (color, density) as output.
If nerf is a tuple, it should be (nerf_coarse, nerf_fine), where nerf_coarse and nerf_fine are two nerf models for coarse and fine stages respectively.
nerf args:
points: (..., n_rays, n_samples, 3)
directions: (..., n_rays, n_samples, 3)
nerf returns:
color: (..., n_rays, n_samples, 3) color values.
density: (..., n_rays, n_samples) density values.
rays_o: (..., n_rays, 3) ray origins
rays_d: (..., n_rays, 3) ray directions.
pixel_width: (..., n_rays) pixel width. How to compute? pixel_width = 1 / (normalized focal length * width)
Returns
if return_dict is False, return rendered rgb and depth for short cut. (If there are separate coarse and fine results, return fine results)
rgb: (..., n_rays, 3) rendered color values.
depth: (..., n_rays) rendered depth values.
else, return a dict. If `n_fine == 0` or `nerf` is a single model, the dict only contains coarse results:
```
{'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..}
```
If there are two models for coarse and fine stages, the dict contains both coarse and fine results:
```
{
"coarse": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..},
"fine": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..}
}
```
"""
if isinstance(nerf, tuple):
nerf_coarse, nerf_fine = nerf
else:
nerf_coarse = nerf_fine = nerf
# 1. Coarse: bin sampling
z_coarse = bin_sample(rays_d.shape[:-1], n_coarse, near, far, device=rays_o.device, dtype=rays_o.dtype, spacing=z_spacing) # (n_batch, n_views, n_rays, n_samples)
points_coarse = rays_o[..., None, :] + rays_d[..., None, :] * z_coarse[..., None] # (n_batch, n_views, n_rays, n_samples, 3)
ray_length = rays_d.norm(dim=-1)
# Query color and density
color_coarse, density_coarse = nerf_coarse(points_coarse, rays_d[..., None, :].expand_as(points_coarse)) # (n_batch, n_views, n_rays, n_samples, 3), (n_batch, n_views, n_rays, n_samples)
# Volume rendering
with torch.no_grad():
rgb_coarse, depth_coarse, weights = volume_rendering(color_coarse, density_coarse, z_coarse, ray_length) # (n_batch, n_views, n_rays, 3), (n_batch, n_views, n_rays, 1), (n_batch, n_views, n_rays, n_samples)
if n_fine == 0:
if return_dict:
return {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights, 'z_vals': z_coarse, 'color': color_coarse, 'density': density_coarse}
else:
return rgb_coarse, depth_coarse
# 2. Fine: Importance sampling
if nerf_coarse is nerf_fine:
# If coarse and fine stages share the same model, the points of coarse stage can be reused,
# and we only need to query the importance samples of fine stage.
z_fine = importance_sample(z_coarse, weights, n_fine)
points_fine = rays_o[..., None, :] + rays_d[..., None, :] * z_fine[..., None]
color_fine, density_fine = nerf_fine(points_fine, rays_d[..., None, :].expand_as(points_fine))
# Merge & volume rendering
z_vals = torch.cat([z_coarse, z_fine], dim=-1)
color = torch.cat([color_coarse, color_fine], dim=-2)
density = torch.cat([density_coarse, density_fine], dim=-1)
z_vals, sort_inds = torch.sort(z_vals, dim=-1)
color = torch.gather(color, dim=-2, index=sort_inds[..., None].expand_as(color))
density = torch.gather(density, dim=-1, index=sort_inds)
rgb, depth, weights = volume_rendering(color, density, z_vals, ray_length)
if return_dict:
return {'rgb': rgb, 'depth': depth, 'weights': weights, 'z_vals': z_vals, 'color': color, 'density': density}
else:
return rgb, depth
else:
# If coarse and fine stages use different models, we need to query the importance samples of both stages.
z_fine = importance_sample(z_coarse, weights, n_fine)
z_vals = torch.cat([z_coarse, z_fine], dim=-1)
points = rays_o[..., None, :] + rays_d[..., None, :] * z_vals[..., None]
color, density = nerf_fine(points)
rgb, depth, weights = volume_rendering(color, density, z_vals, ray_length)
if return_dict:
return {
'coarse': {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights, 'z_vals': z_coarse, 'color': color_coarse, 'density': density_coarse},
'fine': {'rgb': rgb, 'depth': depth, 'weights': weights, 'z_vals': z_vals, 'color': color, 'density': density}
}
else:
return rgb, depth
def mipnerf_render_rays(
mipnerf: Callable[[Tensor, Tensor, Tensor], Tuple[Tensor, Tensor]],
rays_o: Tensor, rays_d: Tensor, pixel_width: Tensor,
*,
return_dict: bool = False,
n_coarse: int = 64, n_fine: int = 64, uniform_ratio: float = 0.4,
near: float = 0.1, far: float = 100.0,
z_spacing: Literal['linear', 'inverse_linear'] = 'linear',
) -> Union[Tuple[Tensor, Tensor], Dict[str, Tensor]]:
"""
MipNeRF rendering.
Args:
mipnerf: mipnerf model, which takes (points_mu, points_sigma) as input and returns (color, density) as output.
mipnerf args:
points_mu: (..., n_rays, n_samples, 3) cone mu.
points_sigma: (..., n_rays, n_samples, 3, 3) cone sigma.
directions: (..., n_rays, n_samples, 3)
mipnerf returns:
color: (..., n_rays, n_samples, 3) color values.
density: (..., n_rays, n_samples) density values.
rays_o: (..., n_rays, 3) ray origins
rays_d: (..., n_rays, 3) ray directions.
pixel_width: (..., n_rays) pixel width. How to compute? pixel_width = 1 / (normalized focal length * width)
Returns
if return_dict is False, return rendered results only: (If `n_fine == 0`, return coarse results, otherwise return fine results)
rgb: (..., n_rays, 3) rendered color values.
depth: (..., n_rays) rendered depth values.
else, return a dict. If `n_fine == 0`, the dict only contains coarse results:
```
{'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..}
```
If n_fine > 0, the dict contains both coarse and fine results :
```
{
"coarse": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..},
"fine": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..}
}
```
"""
# 1. Coarse: bin sampling
z_coarse = bin_sample(rays_d.shape[:-1], n_coarse, near, far, spacing=z_spacing, device=rays_o.device, dtype=rays_o.dtype)
points_mu_coarse, points_sigma_coarse = get_mipnerf_cones(rays_o, rays_d, z_coarse, pixel_width)
ray_length = rays_d.norm(dim=-1)
# Query color and density
color_coarse, density_coarse = mipnerf(points_mu_coarse, points_sigma_coarse, rays_d[..., None, :].expand_as(points_mu_coarse)) # (n_batch, n_views, n_rays, n_samples, 3), (n_batch, n_views, n_rays, n_samples)
# Volume rendering
rgb_coarse, depth_coarse, weights_coarse = volume_rendering(color_coarse, density_coarse, z_coarse, ray_length) # (n_batch, n_views, n_rays, 3), (n_batch, n_views, n_rays, 1), (n_batch, n_views, n_rays, n_samples)
if n_fine == 0:
if return_dict:
return {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights_coarse, 'z_vals': z_coarse, 'color': color_coarse, 'density': density_coarse}
else:
return rgb_coarse, depth_coarse
# 2. Fine: Importance sampling. (NOTE: coarse stages and fine stages always share the same model, but coarse stage points can not be reused)
with torch.no_grad():
weights_coarse = (1.0 - uniform_ratio) * weights_coarse + uniform_ratio / weights_coarse.shape[-1]
z_fine = importance_sample(z_coarse, weights_coarse, n_fine)
z_fine, _ = torch.sort(z_fine, dim=-2)
points_mu_fine, points_sigma_fine = get_mipnerf_cones(rays_o, rays_d, z_fine, pixel_width)
color_fine, density_fine = mipnerf(points_mu_fine, points_sigma_fine, rays_d[..., None, :].expand_as(points_mu_fine))
# Volume rendering
rgb_fine, depth_fine, weights_fine = volume_rendering(color_fine, density_fine, z_fine, ray_length)
if return_dict:
return {
'coarse': {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights_coarse, 'z_vals': z_coarse, 'color': color_coarse, 'density': density_coarse},
'fine': {'rgb': rgb_fine, 'depth': depth_fine, 'weights': weights_fine, 'z_vals': z_fine, 'color': color_fine, 'density': density_fine}
}
else:
return rgb_fine, depth_fine
def neus_render_rays(
neus: Callable[[Tensor, Tensor], Tuple[Tensor, Tensor]],
s: Union[Number, Tensor],
rays_o: Tensor, rays_d: Tensor,
*,
compute_normal: bool = True,
return_dict: bool = False,
n_coarse: int = 64, n_fine: int = 64,
near: float = 0.1, far: float = 100.0,
z_spacing: Literal['linear', 'inverse_linear'] = 'linear',
):
"""
TODO
NeuS rendering of rays. Note that it supports arbitrary batch dimensions (denoted as `...`)
Args:
neus: neus model, which takes (points, directions) as input and returns (color, density) as output.
nerf args:
points: (..., n_rays, n_samples, 3)
directions: (..., n_rays, n_samples, 3)
nerf returns:
color: (..., n_rays, n_samples, 3) color values.
density: (..., n_rays, n_samples) density values.
rays_o: (..., n_rays, 3) ray origins
rays_d: (..., n_rays, 3) ray directions.
pixel_width: (..., n_rays) pixel width. How to compute? pixel_width = 1 / (normalized focal length * width)
Returns
if return_dict is False, return rendered results only: (If `n_fine == 0`, return coarse results, otherwise return fine results)
rgb: (..., n_rays, 3) rendered color values.
depth: (..., n_rays) rendered depth values.
else, return a dict. If `n_fine == 0`, the dict only contains coarse results:
```
{'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'sdf': ..., 'normal': ...}
```
If n_fine > 0, the dict contains both coarse and fine results:
```
{
"coarse": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..},
"fine": {'rgb': .., 'depth': .., 'weights': .., 'z_vals': .., 'color': .., 'density': ..}
}
```
"""
# 1. Coarse: bin sampling
z_coarse = bin_sample(rays_d.shape[:-1], n_coarse, near, far, device=rays_o.device, dtype=rays_o.dtype, spacing=z_spacing) # (n_batch, n_views, n_rays, n_samples)
points_coarse = rays_o[..., None, :] + rays_d[..., None, :] * z_coarse[..., None] # (n_batch, n_views, n_rays, n_samples, 3)
# Query color and density
color_coarse, sdf_coarse = neus(points_coarse, rays_d[..., None, :].expand_as(points_coarse)) # (n_batch, n_views, n_rays, n_samples, 3), (n_batch, n_views, n_rays, n_samples)
# Volume rendering
with torch.no_grad():
rgb_coarse, depth_coarse, weights = neus_volume_rendering(color_coarse, sdf_coarse, s, z_coarse) # (n_batch, n_views, n_rays, 3), (n_batch, n_views, n_rays, 1), (n_batch, n_views, n_rays, n_samples)
if n_fine == 0:
if return_dict:
return {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights, 'z_vals': z_coarse, 'color': color_coarse, 'sdf': sdf_coarse}
else:
return rgb_coarse, depth_coarse
# If coarse and fine stages share the same model, the points of coarse stage can be reused,
# and we only need to query the importance samples of fine stage.
z_fine = importance_sample(z_coarse, weights, n_fine)
points_fine = rays_o[..., None, :] + rays_d[..., None, :] * z_fine[..., None]
color_fine, sdf_fine = neus(points_fine, rays_d[..., None, :].expand_as(points_fine))
# Merge & volume rendering
z_vals = torch.cat([z_coarse, z_fine], dim=-1)
color = torch.cat([color_coarse, color_fine], dim=-2)
sdf = torch.cat([sdf_coarse, sdf_fine], dim=-1)
z_vals, sort_inds = torch.sort(z_vals, dim=-1)
color = torch.gather(color, dim=-2, index=sort_inds[..., None].expand_as(color))
sdf = torch.gather(sdf, dim=-1, index=sort_inds)
rgb, depth, weights = neus_volume_rendering(color, sdf, s, z_vals)
if return_dict:
return {
'coarse': {'rgb': rgb_coarse, 'depth': depth_coarse, 'weights': weights, 'z_vals': z_coarse, 'color': color_coarse, 'sdf': sdf_coarse},
'fine': {'rgb': rgb, 'depth': depth, 'weights': weights, 'z_vals': z_vals, 'color': color, 'sdf': sdf}
}
else:
return rgb, depth
def nerf_render_view(
nerf: Tensor,
extrinsics: Tensor,
intrinsics: Tensor,
width: int,
height: int,
*,
patchify: bool = False,
patch_size: Tuple[int, int] = (64, 64),
**options: Dict[str, Any]
) -> Tuple[Tensor, Tensor]:
"""
NeRF rendering of views. Note that it supports arbitrary batch dimensions (denoted as `...`)
Args:
extrinsics: (..., 4, 4) extrinsics matrice of the rendered views
intrinsics (optional): (..., 3, 3) intrinsics matrice of the rendered views.
width (optional): image width of the rendered views.
height (optional): image height of the rendered views.
patchify (optional): If the image is too large, render it patch by patch
**options: rendering options.
Returns:
rgb: (..., channels, height, width) rendered color values.
depth: (..., height, width) rendered depth values.
"""
if patchify:
# Patchified rendering
max_patch_width, max_patch_height = patch_size
n_rows, n_columns = math.ceil(height / max_patch_height), math.ceil(width / max_patch_width)
rgb_rows, depth_rows = [], []
for i_row in range(n_rows):
rgb_row, depth_row = [], []
for i_column in range(n_columns):
patch_shape = patch_height, patch_width = min(max_patch_height, height - i_row * max_patch_height), min(max_patch_width, width - i_column * max_patch_width)
uv = image_uv(height, width, i_column * max_patch_width, i_row * max_patch_height, i_column * max_patch_width + patch_width, i_row * max_patch_height + patch_height).to(extrinsics)
uv = uv.flatten(0, 1) # (patch_height * patch_width, 2)
ray_o_, ray_d_ = get_rays(extrinsics, intrinsics, uv)
rgb_, depth_ = nerf_render_rays(nerf, ray_o_, ray_d_, **options, return_dict=False)
rgb_ = rgb_.transpose(-1, -2).unflatten(-1, patch_shape) # (..., 3, patch_height, patch_width)
depth_ = depth_.unflatten(-1, patch_shape) # (..., patch_height, patch_width)
rgb_row.append(rgb_)
depth_row.append(depth_)
rgb_rows.append(torch.cat(rgb_row, dim=-1))
depth_rows.append(torch.cat(depth_row, dim=-1))
rgb = torch.cat(rgb_rows, dim=-2)
depth = torch.cat(depth_rows, dim=-2)
return rgb, depth
else:
# Full rendering
uv = image_uv(height, width).to(extrinsics)
uv = uv.flatten(0, 1) # (height * width, 2)
ray_o_, ray_d_ = get_rays(extrinsics, intrinsics, uv)
rgb, depth = nerf_render_rays(nerf, ray_o_, ray_d_, **options, return_dict=False)
rgb = rgb.transpose(-1, -2).unflatten(-1, (height, width)) # (..., 3, height, width)
depth = depth.unflatten(-1, (height, width)) # (..., height, width)
return rgb, depth
def mipnerf_render_view(
mipnerf: Tensor,
extrinsics: Tensor,
intrinsics: Tensor,
width: int,
height: int,
*,
patchify: bool = False,
patch_size: Tuple[int, int] = (64, 64),
**options: Dict[str, Any]
) -> Tuple[Tensor, Tensor]:
"""
MipNeRF rendering of views. Note that it supports arbitrary batch dimensions (denoted as `...`)
Args:
extrinsics: (..., 4, 4) extrinsics matrice of the rendered views
intrinsics (optional): (..., 3, 3) intrinsics matrice of the rendered views.
width (optional): image width of the rendered views.
height (optional): image height of the rendered views.
patchify (optional): If the image is too large, render it patch by patch
**options: rendering options.
Returns:
rgb: (..., 3, height, width) rendered color values.
depth: (..., height, width) rendered depth values.
"""
pixel_width = get_pixel_width(intrinsics, width, height)
if patchify:
# Patchified rendering
max_patch_width, max_patch_height = patch_size
n_rows, n_columns = math.ceil(height / max_patch_height), math.ceil(width / max_patch_width)
rgb_rows, depth_rows = [], []
for i_row in range(n_rows):
rgb_row, depth_row = [], []
for i_column in range(n_columns):
patch_shape = patch_height, patch_width = min(max_patch_height, height - i_row * max_patch_height), min(max_patch_width, width - i_column * max_patch_width)
uv = image_uv(height, width, i_column * max_patch_width, i_row * max_patch_height, i_column * max_patch_width + patch_width, i_row * max_patch_height + patch_height).to(extrinsics)
uv = uv.flatten(0, 1) # (patch_height * patch_width, 2)
ray_o_, ray_d_ = get_rays(extrinsics, intrinsics, uv)
rgb_, depth_ = mipnerf_render_rays(mipnerf, ray_o_, ray_d_, pixel_width, **options)
rgb_ = rgb_.transpose(-1, -2).unflatten(-1, patch_shape) # (..., 3, patch_height, patch_width)
depth_ = depth_.unflatten(-1, patch_shape) # (..., patch_height, patch_width)
rgb_row.append(rgb_)
depth_row.append(depth_)
rgb_rows.append(torch.cat(rgb_row, dim=-1))
depth_rows.append(torch.cat(depth_row, dim=-1))
rgb = torch.cat(rgb_rows, dim=-2)
depth = torch.cat(depth_rows, dim=-2)
return rgb, depth
else:
# Full rendering
uv = image_uv(height, width).to(extrinsics)
uv = uv.flatten(0, 1) # (height * width, 2)
ray_o_, ray_d_ = get_rays(extrinsics, intrinsics, uv)
rgb, depth = mipnerf_render_rays(mipnerf, ray_o_, ray_d_, pixel_width, **options)
rgb = rgb.transpose(-1, -2).unflatten(-1, (height, width)) # (..., 3, height, width)
depth = depth.unflatten(-1, (height, width)) # (..., height, width)
return rgb, depth
class InstantNGP(nn.Module):
"""
An implementation of InstantNGP, Müller et. al., https://nvlabs.github.io/instant-ngp/.
Requires `tinycudann` package.
Install it by:
```
pip install git+https://github.com/NVlabs/tiny-cuda-nn/#subdirectory=bindings/torch
```
"""
def __init__(self,
view_dependent: bool = True,
base_resolution: int = 16,
finest_resolution: int = 2048,
n_levels: int = 16,
num_layers_density: int = 2,
hidden_dim_density: int = 64,
num_layers_color: int = 3,
hidden_dim_color: int = 64,
log2_hashmap_size: int = 19,
bound: float = 1.0,
color_channels: int = 3,
):
super().__init__()
import tinycudann
N_FEATURES_PER_LEVEL = 2
GEO_FEAT_DIM = 15
self.bound = bound
self.color_channels = color_channels
# density network
self.num_layers_density = num_layers_density
self.hidden_dim_density = hidden_dim_density
per_level_scale = (finest_resolution / base_resolution) ** (1 / (n_levels - 1))
self.encoder = tinycudann.Encoding(
n_input_dims=3,
encoding_config={
"otype": "HashGrid",
"n_levels": n_levels,
"n_features_per_level": N_FEATURES_PER_LEVEL,
"log2_hashmap_size": log2_hashmap_size,
"base_resolution": base_resolution,
"per_level_scale": per_level_scale,
},
)
self.density_net = tinycudann.Network(
n_input_dims=N_FEATURES_PER_LEVEL * n_levels,
n_output_dims=1 + GEO_FEAT_DIM,
network_config={
"otype": "FullyFusedMLP",
"activation": "ReLU",
"output_activation": "None",
"n_neurons": hidden_dim_density,
"n_hidden_layers": num_layers_density - 1,
},
)
# color network
self.num_layers_color = num_layers_color
self.hidden_dim_color = hidden_dim_color
self.view_dependent = view_dependent
if view_dependent:
self.encoder_dir = tinycudann.Encoding(
n_input_dims=3,
encoding_config={
"otype": "SphericalHarmonics",
"degree": 4,
},
)
self.in_dim_color = self.encoder_dir.n_output_dims + GEO_FEAT_DIM
else:
self.in_dim_color = GEO_FEAT_DIM
self.color_net = tinycudann.Network(
n_input_dims=self.in_dim_color,
n_output_dims=color_channels,
network_config={
"otype": "FullyFusedMLP",
"activation": "ReLU",
"output_activation": "None",
"n_neurons": hidden_dim_color,
"n_hidden_layers": num_layers_color - 1,
},
)
def forward(self, x: torch.Tensor, d: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Args:
x: (..., 3) points
d: (..., 3) directions
Returns:
color: (..., 3) color values.
density: (..., 1) density values.
"""
batch_shape = x.shape[:-1]
x, d = x.reshape(-1, 3), d.reshape(-1, 3)
# density
x = (x + self.bound) / (2 * self.bound) # to [0, 1]
x = self.encoder(x)
density, geo_feat = self.density_net(x).split([1, 15], dim=-1)
density = F.softplus(density).squeeze(-1)
# color
if self.view_dependent:
d = (F.normalize(d, dim=-1) + 1) / 2 # tcnn SH encoding requires inputs to be in [0, 1]
d = self.encoder_dir(d)
h = torch.cat([d, geo_feat], dim=-1)
else:
h = geo_feat
color = self.color_net(h)
return color.reshape(*batch_shape, self.color_channels), density.reshape(*batch_shape)
+362
View File
@@ -0,0 +1,362 @@
from typing import *
import torch
import nvdiffrast.torch as dr
from . import utils, transforms, mesh
from ._helpers import batched
__all__ = [
'RastContext',
'rasterize_triangle_faces',
'warp_image_by_depth',
'warp_image_by_forward_flow',
]
class RastContext:
"""
Create a rasterization context. Nothing but a wrapper of nvdiffrast.torch.RasterizeCudaContext or nvdiffrast.torch.RasterizeGLContext.
"""
def __init__(self, nvd_ctx: Union[dr.RasterizeCudaContext, dr.RasterizeGLContext] = None, *, backend: Literal['cuda', 'gl'] = 'gl', device: Union[str, torch.device] = None):
import nvdiffrast.torch as dr
if nvd_ctx is not None:
self.nvd_ctx = nvd_ctx
return
if backend == 'gl':
self.nvd_ctx = dr.RasterizeGLContext(device=device)
elif backend == 'cuda':
self.nvd_ctx = dr.RasterizeCudaContext(device=device)
else:
raise ValueError(f'Unknown backend: {backend}')
def rasterize_triangle_faces(
ctx: RastContext,
vertices: torch.Tensor,
faces: torch.Tensor,
attr: torch.Tensor,
width: int,
height: int,
model: torch.Tensor = None,
view: torch.Tensor = None,
projection: torch.Tensor = None,
antialiasing: Union[bool, List[int]] = True,
diff_attrs: Union[None, List[int]] = None,
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
"""
Rasterize a mesh with vertex attributes.
Args:
ctx (GLContext): rasterizer context
vertices (np.ndarray): (B, N, 2 or 3 or 4)
faces (torch.Tensor): (T, 3)
attr (torch.Tensor): (B, N, C)
width (int): width of the output image
height (int): height of the output image
model (torch.Tensor, optional): ([B,] 4, 4) model matrix. Defaults to None (identity).
view (torch.Tensor, optional): ([B,] 4, 4) view matrix. Defaults to None (identity).
projection (torch.Tensor, optional): ([B,] 4, 4) projection matrix. Defaults to None (identity).
antialiasing (Union[bool, List[int]], optional): whether to perform antialiasing. Defaults to True. If a list of indices is provided, only those channels will be antialiased.
diff_attrs (Union[None, List[int]], optional): indices of attributes to compute screen-space derivatives. Defaults to None.
Returns:
image: (torch.Tensor): (B, C, H, W)
depth: (torch.Tensor): (B, H, W) screen space depth, ranging from 0 (near) to 1. (far)
NOTE: Empty pixels will have depth 1., i.e. far plane.
"""
assert vertices.ndim == 3
assert faces.ndim == 2
if vertices.shape[-1] == 2:
vertices = torch.cat([vertices, torch.zeros_like(vertices[..., :1]), torch.ones_like(vertices[..., :1])], dim=-1)
elif vertices.shape[-1] == 3:
vertices = torch.cat([vertices, torch.ones_like(vertices[..., :1])], dim=-1)
elif vertices.shape[-1] == 4:
pass
else:
raise ValueError(f'Wrong shape of vertices: {vertices.shape}')
mvp = projection if projection is not None else torch.eye(4).to(vertices)
if view is not None:
mvp = mvp @ view
if model is not None:
mvp = mvp @ model
pos_clip = vertices @ mvp.transpose(-1, -2)
faces = faces.contiguous()
attr = attr.contiguous()
rast_out, rast_db = dr.rasterize(ctx.nvd_ctx, pos_clip, faces, resolution=[height, width], grad_db=True)
image, image_dr = dr.interpolate(attr, rast_out, faces, rast_db, diff_attrs=diff_attrs)
if antialiasing == True:
image = dr.antialias(image, rast_out, pos_clip, faces)
elif isinstance(antialiasing, list):
aa_image = dr.antialias(image[..., antialiasing], rast_out, pos_clip, faces)
image[..., antialiasing] = aa_image
image = image.flip(1).permute(0, 3, 1, 2)
depth = rast_out[..., 2].flip(1)
depth = (depth * 0.5 + 0.5) * (depth > 0).float() + (depth == 0).float()
if diff_attrs is not None:
image_dr = image_dr.flip(1).permute(0, 3, 1, 2)
return image, depth, image_dr
return image, depth
def texture(
ctx: RastContext,
uv: torch.Tensor,
uv_da: torch.Tensor,
texture: torch.Tensor,
) -> torch.Tensor:
dr.texture(ctx.nvd_ctx, uv, texture)
def warp_image_by_depth(
ctx: RastContext,
depth: torch.FloatTensor,
image: torch.FloatTensor = None,
mask: torch.BoolTensor = None,
width: int = None,
height: int = None,
*,
extrinsics_src: torch.FloatTensor = None,
extrinsics_tgt: torch.FloatTensor = None,
intrinsics_src: torch.FloatTensor = None,
intrinsics_tgt: torch.FloatTensor = None,
near: float = 0.1,
far: float = 100.0,
antialiasing: bool = True,
backslash: bool = False,
padding: int = 0,
return_uv: bool = False,
return_dr: bool = False,
) -> Tuple[torch.FloatTensor, torch.FloatTensor, torch.BoolTensor, Optional[torch.FloatTensor], Optional[torch.FloatTensor]]:
"""
Warp image by depth.
NOTE: if batch size is 1, image mesh will be triangulated aware of the depth, yielding less distorted results.
Otherwise, image mesh will be triangulated simply for batch rendering.
Args:
ctx (Union[dr.RasterizeCudaContext, dr.RasterizeGLContext]): rasterization context
depth (torch.Tensor): (B, H, W) linear depth
image (torch.Tensor): (B, C, H, W). None to use image space uv. Defaults to None.
width (int, optional): width of the output image. None to use the same as depth. Defaults to None.
height (int, optional): height of the output image. Defaults the same as depth..
extrinsics_src (torch.Tensor, optional): (B, 4, 4) extrinsics matrix for source. None to use identity. Defaults to None.
extrinsics_tgt (torch.Tensor, optional): (B, 4, 4) extrinsics matrix for target. None to use identity. Defaults to None.
intrinsics_src (torch.Tensor, optional): (B, 3, 3) intrinsics matrix for source. None to use the same as target. Defaults to None.
intrinsics_tgt (torch.Tensor, optional): (B, 3, 3) intrinsics matrix for target. None to use the same as source. Defaults to None.
near (float, optional): near plane. Defaults to 0.1.
far (float, optional): far plane. Defaults to 100.0.
antialiasing (bool, optional): whether to perform antialiasing. Defaults to True.
backslash (bool, optional): whether to use backslash triangulation. Defaults to False.
padding (int, optional): padding of the image. Defaults to 0.
return_uv (bool, optional): whether to return the uv. Defaults to False.
return_dr (bool, optional): whether to return the image-space derivatives of uv. Defaults to False.
Returns:
image: (torch.FloatTensor): (B, C, H, W) rendered image
depth: (torch.FloatTensor): (B, H, W) linear depth, ranging from 0 to inf
mask: (torch.BoolTensor): (B, H, W) mask of valid pixels
uv: (torch.FloatTensor): (B, 2, H, W) image-space uv
dr: (torch.FloatTensor): (B, 4, H, W) image-space derivatives of uv
"""
assert depth.ndim == 3
batch_size = depth.shape[0]
if width is None:
width = depth.shape[-1]
if height is None:
height = depth.shape[-2]
if image is not None:
assert image.shape[-2:] == depth.shape[-2:], f'Shape of image {image.shape} does not match shape of depth {depth.shape}'
if extrinsics_src is None:
extrinsics_src = torch.eye(4).to(depth)
if extrinsics_tgt is None:
extrinsics_tgt = torch.eye(4).to(depth)
if intrinsics_src is None:
intrinsics_src = intrinsics_tgt
if intrinsics_tgt is None:
intrinsics_tgt = intrinsics_src
assert all(x is not None for x in [extrinsics_src, extrinsics_tgt, intrinsics_src, intrinsics_tgt]), "Make sure you have provided all the necessary camera parameters."
view_tgt = transforms.extrinsics_to_view(extrinsics_tgt)
perspective_tgt = transforms.intrinsics_to_perspective(intrinsics_tgt, near=near, far=far)
if padding > 0:
uv, faces = utils.image_mesh(width=width+2, height=height+2)
uv = (uv - 1 / (width + 2)) * ((width + 2) / width)
uv_ = uv.clone().reshape(height+2, width+2, 2)
uv_[0, :, 1] -= padding / height
uv_[-1, :, 1] += padding / height
uv_[:, 0, 0] -= padding / width
uv_[:, -1, 0] += padding / width
uv_ = uv_.reshape(-1, 2)
depth = torch.nn.functional.pad(depth, [1, 1, 1, 1], mode='replicate')
if image is not None:
image = torch.nn.functional.pad(image, [1, 1, 1, 1], mode='replicate')
uv, uv_, faces = uv.to(depth.device), uv_.to(depth.device), faces.to(depth.device)
pts = transforms.unproject_cv(
uv_,
depth.flatten(-2, -1),
extrinsics_src,
intrinsics_src,
)
else:
uv, faces = utils.image_mesh(width=depth.shape[-1], height=depth.shape[-2])
if mask is not None:
depth = torch.where(mask, depth, torch.tensor(far, dtype=depth.dtype, device=depth.device))
uv, faces = uv.to(depth.device), faces.to(depth.device)
pts = transforms.unproject_cv(
uv,
depth.flatten(-2, -1),
extrinsics_src,
intrinsics_src,
)
# triangulate
if batch_size == 1:
faces = mesh.triangulate(faces, vertices=pts[0])
else:
faces = mesh.triangulate(faces, backslash=backslash)
# rasterize attributes
diff_attrs = None
if image is not None:
attr = image.permute(0, 2, 3, 1).flatten(1, 2)
if return_dr or return_uv:
if return_dr:
diff_attrs = [image.shape[1], image.shape[1]+1]
if return_uv and antialiasing:
antialiasing = list(range(image.shape[1]))
attr = torch.cat([attr, uv.expand(batch_size, -1, -1)], dim=-1)
else:
attr = uv.expand(batch_size, -1, -1)
if antialiasing:
print("\033[93mWarning: you are performing antialiasing on uv. This may cause artifacts.\033[0m")
if return_uv:
return_uv = False
print("\033[93mWarning: image is None, return_uv is ignored.\033[0m")
if return_dr:
diff_attrs = [0, 1]
if mask is not None:
attr = torch.cat([attr, mask.float().flatten(1, 2).unsqueeze(-1)], dim=-1)
rast = rasterize_triangle_faces(
ctx,
pts,
faces,
attr,
width,
height,
view=view_tgt,
perspective=perspective_tgt,
antialiasing=antialiasing,
diff_attrs=diff_attrs,
)
if return_dr:
output_image, screen_depth, output_dr = rast
else:
output_image, screen_depth = rast
output_mask = screen_depth < 1.0
if mask is not None:
output_image, rast_mask = output_image[..., :-1, :, :], output_image[..., -1, :, :]
output_mask &= (rast_mask > 0.9999).reshape(-1, height, width)
if (return_dr or return_uv) and image is not None:
output_image, output_uv = output_image[..., :-2, :, :], output_image[..., -2:, :, :]
output_depth = transforms.depth_buffer_to_linear(screen_depth, near=near, far=far) * output_mask
output_image = output_image * output_mask.unsqueeze(1)
outs = [output_image, output_depth, output_mask]
if return_uv:
outs.append(output_uv)
if return_dr:
outs.append(output_dr)
return tuple(outs)
def warp_image_by_forward_flow(
ctx: RastContext,
image: torch.FloatTensor,
flow: torch.FloatTensor,
depth: torch.FloatTensor = None,
*,
antialiasing: bool = True,
backslash: bool = False,
) -> Tuple[torch.FloatTensor, torch.BoolTensor]:
"""
Warp image by forward flow.
NOTE: if batch size is 1, image mesh will be triangulated aware of the depth, yielding less distorted results.
Otherwise, image mesh will be triangulated simply for batch rendering.
Args:
ctx (Union[dr.RasterizeCudaContext, dr.RasterizeGLContext]): rasterization context
image (torch.Tensor): (B, C, H, W) image
flow (torch.Tensor): (B, 2, H, W) forward flow
depth (torch.Tensor, optional): (B, H, W) linear depth. If None, will use the same for all pixels. Defaults to None.
antialiasing (bool, optional): whether to perform antialiasing. Defaults to True.
backslash (bool, optional): whether to use backslash triangulation. Defaults to False.
Returns:
image: (torch.FloatTensor): (B, C, H, W) rendered image
mask: (torch.BoolTensor): (B, H, W) mask of valid pixels
"""
assert image.ndim == 4, f'Wrong shape of image: {image.shape}'
batch_size, _, height, width = image.shape
if depth is None:
depth = torch.ones_like(flow[:, 0])
extrinsics = torch.eye(4).to(image)
fov = torch.deg2rad(torch.tensor([45.0], device=image.device))
intrinsics = transforms.intrinsics_from_fov(fov, width, height, normalize=True)[0]
view = transforms.extrinsics_to_view(extrinsics)
perspective = transforms.intrinsics_to_perspective(intrinsics, near=0.1, far=100)
uv, faces = utils.image_mesh(width=width, height=height)
uv, faces = uv.to(image.device), faces.to(image.device)
uv = uv + flow.permute(0, 2, 3, 1).flatten(1, 2)
pts = transforms.unproject_cv(
uv,
depth.flatten(-2, -1),
extrinsics,
intrinsics,
)
# triangulate
if batch_size == 1:
faces = mesh.triangulate(faces, vertices=pts[0])
else:
faces = mesh.triangulate(faces, backslash=backslash)
# rasterize attributes
attr = image.permute(0, 2, 3, 1).flatten(1, 2)
rast = rasterize_triangle_faces(
ctx,
pts,
faces,
attr,
width,
height,
view=view,
perspective=perspective,
antialiasing=antialiasing,
)
output_image, screen_depth = rast
output_mask = screen_depth < 1.0
output_image = output_image * output_mask.unsqueeze(1)
outs = [output_image, output_mask]
return tuple(outs)
File diff suppressed because it is too large Load Diff
+351
View File
@@ -0,0 +1,351 @@
from typing import *
import torch
import torch.nn.functional as F
from . import transforms
from . import mesh
from ._helpers import batched
__all__ = [
'sliding_window_1d',
'sliding_window_2d',
'sliding_window_nd',
'image_uv',
'image_pixel_center',
'image_mesh',
'chessboard',
'depth_edge',
'depth_aliasing',
'image_mesh_from_depth',
'point_to_normal',
'depth_to_normal',
'masked_min',
'masked_max',
'bounding_rect'
]
def sliding_window_1d(x: torch.Tensor, window_size: int, stride: int = 1, dim: int = -1) -> torch.Tensor:
"""
Sliding window view of the input tensor. The dimension of the sliding window is appended to the end of the input tensor's shape.
NOTE: Since Pytorch has `unfold` function, 1D sliding window view is just a wrapper of it.
"""
return x.unfold(dim, window_size, stride)
def sliding_window_nd(x: torch.Tensor, window_size: Tuple[int, ...], stride: Tuple[int, ...], dim: Tuple[int, ...]) -> torch.Tensor:
dim = [dim[i] % x.ndim for i in range(len(dim))]
assert len(window_size) == len(stride) == len(dim)
for i in range(len(window_size)):
x = sliding_window_1d(x, window_size[i], stride[i], dim[i])
return x
def sliding_window_2d(x: torch.Tensor, window_size: Union[int, Tuple[int, int]], stride: Union[int, Tuple[int, int]], dim: Union[int, Tuple[int, int]] = (-2, -1)) -> torch.Tensor:
if isinstance(window_size, int):
window_size = (window_size, window_size)
if isinstance(stride, int):
stride = (stride, stride)
return sliding_window_nd(x, window_size, stride, dim)
def image_uv(height: int, width: int, left: int = None, top: int = None, right: int = None, bottom: int = None, device: torch.device = None, dtype: torch.dtype = None) -> torch.Tensor:
"""
Get image space UV grid, ranging in [0, 1].
>>> image_uv(10, 10):
[[[0.05, 0.05], [0.15, 0.05], ..., [0.95, 0.05]],
[[0.05, 0.15], [0.15, 0.15], ..., [0.95, 0.15]],
... ... ...
[[0.05, 0.95], [0.15, 0.95], ..., [0.95, 0.95]]]
Args:
width (int): image width
height (int): image height
Returns:
np.ndarray: shape (height, width, 2)
"""
if left is None: left = 0
if top is None: top = 0
if right is None: right = width
if bottom is None: bottom = height
u = torch.linspace((left + 0.5) / width, (right - 0.5) / width, right - left, device=device, dtype=dtype)
v = torch.linspace((top + 0.5) / height, (bottom - 0.5) / height, bottom - top, device=device, dtype=dtype)
u, v = torch.meshgrid(u, v, indexing='xy')
uv = torch.stack([u, v], dim=-1)
return uv
def image_pixel_center(
height: int,
width: int,
left: int = None,
top: int = None,
right: int = None,
bottom: int = None,
dtype: torch.dtype = None,
device: torch.device = None
) -> torch.Tensor:
"""
Get image pixel center coordinates, ranging in [0, width] and [0, height].
`image[i, j]` has pixel center coordinates `(j + 0.5, i + 0.5)`.
>>> image_pixel_center(10, 10):
[[[0.5, 0.5], [1.5, 0.5], ..., [9.5, 0.5]],
[[0.5, 1.5], [1.5, 1.5], ..., [9.5, 1.5]],
... ... ...
[[0.5, 9.5], [1.5, 9.5], ..., [9.5, 9.5]]]
Args:
width (int): image width
height (int): image height
Returns:
np.ndarray: shape (height, width, 2)
"""
if left is None: left = 0
if top is None: top = 0
if right is None: right = width
if bottom is None: bottom = height
u = torch.linspace(left + 0.5, right - 0.5, right - left, dtype=dtype, device=device)
v = torch.linspace(top + 0.5, bottom - 0.5, bottom - top, dtype=dtype, device=device)
u, v = torch.meshgrid(u, v, indexing='xy')
return torch.stack([u, v], dim=2)
def image_mesh(height: int, width: int, mask: torch.Tensor = None, device: torch.device = None, dtype: torch.dtype = None) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Get a quad mesh regarding image pixel uv coordinates as vertices and image grid as faces.
Args:
width (int): image width
height (int): image height
mask (np.ndarray, optional): binary mask of shape (height, width), dtype=bool. Defaults to None.
Returns:
uv (np.ndarray): uv corresponding to pixels as described in image_uv()
faces (np.ndarray): quad faces connecting neighboring pixels
indices (np.ndarray, optional): indices of vertices in the original mesh
"""
if device is None and mask is not None:
device = mask.device
if mask is not None:
assert mask.shape[0] == height and mask.shape[1] == width
assert mask.dtype == torch.bool
uv = image_uv(height, width, device=device, dtype=dtype).reshape((-1, 2))
row_faces = torch.stack([
torch.arange(0, width - 1, dtype=torch.int32, device=device),
torch.arange(width, 2 * width - 1, dtype=torch.int32, device=device),
torch.arange(1 + width, 2 * width, dtype=torch.int32, device=device),
torch.arange(1, width, dtype=torch.int32, device=device)
], dim=1)
faces = (torch.arange(0, (height - 1) * width, width, device=device, dtype=torch.int32)[:, None, None] + row_faces[None, :, :]).reshape((-1, 4))
if mask is not None:
quad_mask = (mask[:-1, :-1] & mask[1:, :-1] & mask[1:, 1:] & mask[:-1, 1:]).ravel()
faces = faces[quad_mask]
faces, uv, indices = mesh.remove_unreferenced_vertices(faces, uv, return_indices=True)
return uv, faces, indices
return uv, faces
def depth_edge(depth: torch.Tensor, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: torch.Tensor = None) -> torch.BoolTensor:
"""
Compute the edge mask of a depth map. The edge is defined as the pixels whose neighbors have a large difference in depth.
Args:
depth (torch.Tensor): shape (..., height, width), linear depth map
atol (float): absolute tolerance
rtol (float): relative tolerance
Returns:
edge (torch.Tensor): shape (..., height, width) of dtype torch.bool
"""
shape = depth.shape
depth = depth.reshape(-1, 1, *shape[-2:])
if mask is not None:
mask = mask.reshape(-1, 1, *shape[-2:])
if mask is None:
diff = (F.max_pool2d(depth, kernel_size, stride=1, padding=kernel_size // 2) + F.max_pool2d(-depth, kernel_size, stride=1, padding=kernel_size // 2))
else:
diff = (F.max_pool2d(torch.where(mask, depth, -torch.inf), kernel_size, stride=1, padding=kernel_size // 2) + F.max_pool2d(torch.where(mask, -depth, -torch.inf), kernel_size, stride=1, padding=kernel_size // 2))
edge = torch.zeros_like(depth, dtype=torch.bool)
if atol is not None:
edge |= diff > atol
if rtol is not None:
edge |= (diff / depth).nan_to_num_() > rtol
edge = edge.reshape(*shape)
return edge
def depth_aliasing(depth: torch.Tensor, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: torch.Tensor = None) -> torch.BoolTensor:
"""
Compute the map that indicates the aliasing of a depth map. The aliasing is defined as the pixels which neither close to the maximum nor the minimum of its neighbors.
Args:
depth (torch.Tensor): shape (..., height, width), linear depth map
atol (float): absolute tolerance
rtol (float): relative tolerance
Returns:
edge (torch.Tensor): shape (..., height, width) of dtype torch.bool
"""
shape = depth.shape
depth = depth.reshape(-1, 1, *shape[-2:])
if mask is not None:
mask = mask.reshape(-1, 1, *shape[-2:])
if mask is None:
diff_max = F.max_pool2d(depth, kernel_size, stride=1, padding=kernel_size // 2) - depth
diff_min = F.max_pool2d(-depth, kernel_size, stride=1, padding=kernel_size // 2) + depth
else:
diff_max = F.max_pool2d(torch.where(mask, depth, -torch.inf), kernel_size, stride=1, padding=kernel_size // 2) - depth
diff_min = F.max_pool2d(torch.where(mask, -depth, -torch.inf), kernel_size, stride=1, padding=kernel_size // 2) + depth
diff = torch.minimum(diff_max, diff_min)
edge = torch.zeros_like(depth, dtype=torch.bool)
if atol is not None:
edge |= diff > atol
if rtol is not None:
edge |= (diff / depth).nan_to_num_() > rtol
edge = edge.reshape(*shape)
return edge
def image_mesh_from_depth(
depth: torch.Tensor,
extrinsics: torch.Tensor = None,
intrinsics: torch.Tensor = None
) -> Tuple[torch.Tensor, torch.Tensor]:
height, width = depth.shape
uv, faces = image_mesh(height, width)
faces = faces.reshape(-1, 4)
depth = depth.reshape(-1)
pts = transforms.unproject_cv(image_uv, depth, extrinsics, intrinsics)
faces = mesh.triangulate(faces, vertices=pts)
return pts, faces
@batched(3, 2, 2)
def point_to_normal(point: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
"""
Calculate normal map from point map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
Args:
point (torch.Tensor): shape (..., height, width, 3), point map
Returns:
normal (torch.Tensor): shape (..., height, width, 3), normal map.
"""
has_mask = mask is not None
if mask is None:
mask = torch.ones_like(point[..., 0], dtype=torch.bool)
mask = F.pad(mask, (1, 1, 1, 1), mode='constant', value=0)
pts = F.pad(point.permute(0, 3, 1, 2), (1, 1, 1, 1), mode='constant', value=1).permute(0, 2, 3, 1)
up = pts[:, :-2, 1:-1, :] - pts[:, 1:-1, 1:-1, :]
left = pts[:, 1:-1, :-2, :] - pts[:, 1:-1, 1:-1, :]
down = pts[:, 2:, 1:-1, :] - pts[:, 1:-1, 1:-1, :]
right = pts[:, 1:-1, 2:, :] - pts[:, 1:-1, 1:-1, :]
normal = torch.stack([
torch.cross(up, left, dim=-1),
torch.cross(left, down, dim=-1),
torch.cross(down, right, dim=-1),
torch.cross(right, up, dim=-1),
])
normal = F.normalize(normal, dim=-1)
valid = torch.stack([
mask[:, :-2, 1:-1] & mask[:, 1:-1, :-2],
mask[:, 1:-1, :-2] & mask[:, 2:, 1:-1],
mask[:, 2:, 1:-1] & mask[:, 1:-1, 2:],
mask[:, 1:-1, 2:] & mask[:, :-2, 1:-1],
]) & mask[None, :, 1:-1, 1:-1]
normal = (normal * valid[..., None]).sum(dim=0)
normal = F.normalize(normal, dim=-1)
if has_mask:
return normal, valid.any(dim=0)
else:
return normal
@batched(2, 2, 2)
def depth_to_normal(depth: torch.Tensor, intrinsics: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
"""
Calculate normal map from depth map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
Args:
depth (torch.Tensor): shape (..., height, width), linear depth map
intrinsics (torch.Tensor): shape (..., 3, 3), intrinsics matrix
Returns:
normal (torch.Tensor): shape (..., 3, height, width), normal map.
"""
has_mask = mask is not None
height, width = depth.shape[-2:]
if mask is None:
mask = torch.ones_like(depth, dtype=torch.bool)
mask = F.pad(mask, (1, 1, 1, 1), mode='constant', value=0)
uv = image_uv(*depth.shape[-2:]).unsqueeze(0).to(depth)
pts = transforms.unproject_cv(uv.reshape(-1, 2), depth.flatten(-2), intrinsics=intrinsics, extrinsics=None).unflatten(-2, (height, width))
return point_to_normal(pts, mask)
def masked_min(input: torch.Tensor, mask: torch.BoolTensor, dim: int = None, keepdim: bool = False) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
"""Similar to torch.min, but with mask
"""
if dim is None:
return torch.where(mask, input, torch.tensor(torch.inf, dtype=input.dtype, device=input.device)).min()
else:
return torch.where(mask, input, torch.tensor(torch.inf, dtype=input.dtype, device=input.device)).min(dim=dim, keepdim=keepdim)
def masked_max(input: torch.Tensor, mask: torch.BoolTensor, dim: int = None, keepdim: bool = False) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
"""Similar to torch.max, but with mask
"""
if dim is None:
return torch.where(mask, input, torch.tensor(-torch.inf, dtype=input.dtype, device=input.device)).max()
else:
return torch.where(mask, input, torch.tensor(-torch.inf, dtype=input.dtype, device=input.device)).max(dim=dim, keepdim=keepdim)
def bounding_rect(mask: torch.BoolTensor):
"""get bounding rectangle of a mask
Args:
mask (torch.Tensor): shape (..., height, width), mask
Returns:
rect (torch.Tensor): shape (..., 4), bounding rectangle (left, top, right, bottom)
"""
height, width = mask.shape[-2:]
mask = mask.flatten(-2).unsqueeze(-1)
uv = image_uv(height, width).to(mask.device).reshape(-1, 2)
left_top = masked_min(uv, mask, dim=-2)[0]
right_bottom = masked_max(uv, mask, dim=-2)[0]
return torch.cat([left_top, right_bottom], dim=-1)
def chessboard(width: int, height: int, grid_size: int, color_a: torch.Tensor, color_b: torch.Tensor) -> torch.Tensor:
"""get a chessboard image
Args:
width (int): image width
height (int): image height
grid_size (int): size of chessboard grid
color_a (torch.Tensor): shape (chanenls,), color of the grid at the top-left corner
color_b (torch.Tensor): shape (chanenls,), color in complementary grids
Returns:
image (torch.Tensor): shape (height, width, channels), chessboard image
"""
x = torch.div(torch.arange(width), grid_size, rounding_mode='floor')
y = torch.div(torch.arange(height), grid_size, rounding_mode='floor')
mask = ((x[None, :] + y[:, None]) % 2).to(color_a)
image = (1 - mask[..., None]) * color_a + mask[..., None] * color_b
return image