Spaces:
Running
Running
aknapitsch user
commited on
Commit
·
630b56b
1
Parent(s):
9f367f7
cleanup files
Browse files
mapanything/utils/hf_utils/moge_utils.py
DELETED
|
@@ -1,639 +0,0 @@
|
|
| 1 |
-
import numpy as np
|
| 2 |
-
from typing import *
|
| 3 |
-
from numbers import Number
|
| 4 |
-
import warnings
|
| 5 |
-
import functools
|
| 6 |
-
|
| 7 |
-
from ._helpers import batched
|
| 8 |
-
from . import transforms
|
| 9 |
-
from . import mesh
|
| 10 |
-
|
| 11 |
-
__all__ = [
|
| 12 |
-
'sliding_window_1d',
|
| 13 |
-
'sliding_window_nd',
|
| 14 |
-
'sliding_window_2d',
|
| 15 |
-
'max_pool_1d',
|
| 16 |
-
'max_pool_2d',
|
| 17 |
-
'max_pool_nd',
|
| 18 |
-
'depth_edge',
|
| 19 |
-
'normals_edge',
|
| 20 |
-
'depth_aliasing',
|
| 21 |
-
'interpolate',
|
| 22 |
-
'image_scrcoord',
|
| 23 |
-
'image_uv',
|
| 24 |
-
'image_pixel_center',
|
| 25 |
-
'image_pixel',
|
| 26 |
-
'image_mesh',
|
| 27 |
-
'image_mesh_from_depth',
|
| 28 |
-
'points_to_normals',
|
| 29 |
-
'points_to_normals',
|
| 30 |
-
'chessboard',
|
| 31 |
-
'cube',
|
| 32 |
-
'icosahedron',
|
| 33 |
-
'square',
|
| 34 |
-
'camera_frustum',
|
| 35 |
-
'to4x4'
|
| 36 |
-
]
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def no_runtime_warnings(fn):
|
| 40 |
-
"""
|
| 41 |
-
Disable runtime warnings in numpy.
|
| 42 |
-
"""
|
| 43 |
-
@functools.wraps(fn)
|
| 44 |
-
def wrapper(*args, **kwargs):
|
| 45 |
-
with warnings.catch_warnings():
|
| 46 |
-
warnings.simplefilter("ignore")
|
| 47 |
-
return fn(*args, **kwargs)
|
| 48 |
-
return wrapper
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def sliding_window_1d(x: np.ndarray, window_size: int, stride: int, axis: int = -1):
|
| 52 |
-
"""
|
| 53 |
-
Return x view of the input array with x sliding window of the given kernel size and stride.
|
| 54 |
-
The sliding window is performed over the given axis, and the window dimension is append to the end of the output array's shape.
|
| 55 |
-
|
| 56 |
-
Args:
|
| 57 |
-
x (np.ndarray): input array with shape (..., axis_size, ...)
|
| 58 |
-
kernel_size (int): size of the sliding window
|
| 59 |
-
stride (int): stride of the sliding window
|
| 60 |
-
axis (int): axis to perform sliding window over
|
| 61 |
-
|
| 62 |
-
Returns:
|
| 63 |
-
a_sliding (np.ndarray): view of the input array with shape (..., n_windows, ..., kernel_size), where n_windows = (axis_size - kernel_size + 1) // stride
|
| 64 |
-
"""
|
| 65 |
-
assert x.shape[axis] >= window_size, f"kernel_size ({window_size}) is larger than axis_size ({x.shape[axis]})"
|
| 66 |
-
axis = axis % x.ndim
|
| 67 |
-
shape = (*x.shape[:axis], (x.shape[axis] - window_size + 1) // stride, *x.shape[axis + 1:], window_size)
|
| 68 |
-
strides = (*x.strides[:axis], stride * x.strides[axis], *x.strides[axis + 1:], x.strides[axis])
|
| 69 |
-
x_sliding = np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)
|
| 70 |
-
return x_sliding
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def sliding_window_nd(x: np.ndarray, window_size: Tuple[int,...], stride: Tuple[int,...], axis: Tuple[int,...]) -> np.ndarray:
|
| 74 |
-
axis = [axis[i] % x.ndim for i in range(len(axis))]
|
| 75 |
-
for i in range(len(axis)):
|
| 76 |
-
x = sliding_window_1d(x, window_size[i], stride[i], axis[i])
|
| 77 |
-
return x
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
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:
|
| 81 |
-
if isinstance(window_size, int):
|
| 82 |
-
window_size = (window_size, window_size)
|
| 83 |
-
if isinstance(stride, int):
|
| 84 |
-
stride = (stride, stride)
|
| 85 |
-
return sliding_window_nd(x, window_size, stride, axis)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def max_pool_1d(x: np.ndarray, kernel_size: int, stride: int, padding: int = 0, axis: int = -1):
|
| 89 |
-
axis = axis % x.ndim
|
| 90 |
-
if padding > 0:
|
| 91 |
-
fill_value = np.nan if x.dtype.kind == 'f' else np.iinfo(x.dtype).min
|
| 92 |
-
padding_arr = np.full((*x.shape[:axis], padding, *x.shape[axis + 1:]), fill_value=fill_value, dtype=x.dtype)
|
| 93 |
-
x = np.concatenate([padding_arr, x, padding_arr], axis=axis)
|
| 94 |
-
a_sliding = sliding_window_1d(x, kernel_size, stride, axis)
|
| 95 |
-
max_pool = np.nanmax(a_sliding, axis=-1)
|
| 96 |
-
return max_pool
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def max_pool_nd(x: np.ndarray, kernel_size: Tuple[int,...], stride: Tuple[int,...], padding: Tuple[int,...], axis: Tuple[int,...]) -> np.ndarray:
|
| 100 |
-
for i in range(len(axis)):
|
| 101 |
-
x = max_pool_1d(x, kernel_size[i], stride[i], padding[i], axis[i])
|
| 102 |
-
return x
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
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)):
|
| 106 |
-
if isinstance(kernel_size, Number):
|
| 107 |
-
kernel_size = (kernel_size, kernel_size)
|
| 108 |
-
if isinstance(stride, Number):
|
| 109 |
-
stride = (stride, stride)
|
| 110 |
-
if isinstance(padding, Number):
|
| 111 |
-
padding = (padding, padding)
|
| 112 |
-
axis = tuple(axis)
|
| 113 |
-
return max_pool_nd(x, kernel_size, stride, padding, axis)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
@no_runtime_warnings
|
| 117 |
-
def depth_edge(depth: np.ndarray, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: np.ndarray = None) -> np.ndarray:
|
| 118 |
-
"""
|
| 119 |
-
Compute the edge mask from depth map. The edge is defined as the pixels whose neighbors have large difference in depth.
|
| 120 |
-
|
| 121 |
-
Args:
|
| 122 |
-
depth (np.ndarray): shape (..., height, width), linear depth map
|
| 123 |
-
atol (float): absolute tolerance
|
| 124 |
-
rtol (float): relative tolerance
|
| 125 |
-
|
| 126 |
-
Returns:
|
| 127 |
-
edge (np.ndarray): shape (..., height, width) of dtype torch.bool
|
| 128 |
-
"""
|
| 129 |
-
if mask is None:
|
| 130 |
-
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))
|
| 131 |
-
else:
|
| 132 |
-
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))
|
| 133 |
-
|
| 134 |
-
edge = np.zeros_like(depth, dtype=bool)
|
| 135 |
-
if atol is not None:
|
| 136 |
-
edge |= diff > atol
|
| 137 |
-
|
| 138 |
-
with warnings.catch_warnings():
|
| 139 |
-
warnings.simplefilter("ignore", category=RuntimeWarning)
|
| 140 |
-
if rtol is not None:
|
| 141 |
-
edge |= diff / depth > rtol
|
| 142 |
-
return edge
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
@no_runtime_warnings
|
| 146 |
-
def depth_aliasing(depth: np.ndarray, atol: float = None, rtol: float = None, kernel_size: int = 3, mask: np.ndarray = None) -> np.ndarray:
|
| 147 |
-
"""
|
| 148 |
-
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.
|
| 149 |
-
Args:
|
| 150 |
-
depth (np.ndarray): shape (..., height, width), linear depth map
|
| 151 |
-
atol (float): absolute tolerance
|
| 152 |
-
rtol (float): relative tolerance
|
| 153 |
-
|
| 154 |
-
Returns:
|
| 155 |
-
edge (np.ndarray): shape (..., height, width) of dtype torch.bool
|
| 156 |
-
"""
|
| 157 |
-
if mask is None:
|
| 158 |
-
diff_max = max_pool_2d(depth, kernel_size, stride=1, padding=kernel_size // 2) - depth
|
| 159 |
-
diff_min = max_pool_2d(-depth, kernel_size, stride=1, padding=kernel_size // 2) + depth
|
| 160 |
-
else:
|
| 161 |
-
diff_max = max_pool_2d(np.where(mask, depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2) - depth
|
| 162 |
-
diff_min = max_pool_2d(np.where(mask, -depth, -np.inf), kernel_size, stride=1, padding=kernel_size // 2) + depth
|
| 163 |
-
diff = np.minimum(diff_max, diff_min)
|
| 164 |
-
|
| 165 |
-
edge = np.zeros_like(depth, dtype=bool)
|
| 166 |
-
if atol is not None:
|
| 167 |
-
edge |= diff > atol
|
| 168 |
-
if rtol is not None:
|
| 169 |
-
edge |= diff / depth > rtol
|
| 170 |
-
return edge
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
@no_runtime_warnings
|
| 174 |
-
def normals_edge(normals: np.ndarray, tol: float, kernel_size: int = 3, mask: np.ndarray = None) -> np.ndarray:
|
| 175 |
-
"""
|
| 176 |
-
Compute the edge mask from normal map.
|
| 177 |
-
|
| 178 |
-
Args:
|
| 179 |
-
normal (np.ndarray): shape (..., height, width, 3), normal map
|
| 180 |
-
tol (float): tolerance in degrees
|
| 181 |
-
|
| 182 |
-
Returns:
|
| 183 |
-
edge (np.ndarray): shape (..., height, width) of dtype torch.bool
|
| 184 |
-
"""
|
| 185 |
-
assert normals.ndim >= 3 and normals.shape[-1] == 3, "normal should be of shape (..., height, width, 3)"
|
| 186 |
-
normals = normals / (np.linalg.norm(normals, axis=-1, keepdims=True) + 1e-12)
|
| 187 |
-
|
| 188 |
-
padding = kernel_size // 2
|
| 189 |
-
normals_window = sliding_window_2d(
|
| 190 |
-
np.pad(normals, (*([(0, 0)] * (normals.ndim - 3)), (padding, padding), (padding, padding), (0, 0)), mode='edge'),
|
| 191 |
-
window_size=kernel_size,
|
| 192 |
-
stride=1,
|
| 193 |
-
axis=(-3, -2)
|
| 194 |
-
)
|
| 195 |
-
if mask is None:
|
| 196 |
-
angle_diff = np.acos((normals[..., None, None] * normals_window).sum(axis=-3)).max(axis=(-2, -1))
|
| 197 |
-
else:
|
| 198 |
-
mask_window = sliding_window_2d(
|
| 199 |
-
np.pad(mask, (*([(0, 0)] * (mask.ndim - 3)), (padding, padding), (padding, padding)), mode='edge'),
|
| 200 |
-
window_size=kernel_size,
|
| 201 |
-
stride=1,
|
| 202 |
-
axis=(-3, -2)
|
| 203 |
-
)
|
| 204 |
-
angle_diff = np.where(mask_window, np.acos((normals[..., None, None] * normals_window).sum(axis=-3)), 0).max(axis=(-2, -1))
|
| 205 |
-
|
| 206 |
-
angle_diff = max_pool_2d(angle_diff, kernel_size, stride=1, padding=kernel_size // 2)
|
| 207 |
-
edge = angle_diff > np.deg2rad(tol)
|
| 208 |
-
return edge
|
| 209 |
-
|
| 210 |
-
@no_runtime_warnings
|
| 211 |
-
def points_to_normals(point: np.ndarray, mask: np.ndarray = None) -> np.ndarray:
|
| 212 |
-
"""
|
| 213 |
-
Calculate normal map from point map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
|
| 214 |
-
|
| 215 |
-
Args:
|
| 216 |
-
point (np.ndarray): shape (height, width, 3), point map
|
| 217 |
-
Returns:
|
| 218 |
-
normal (np.ndarray): shape (height, width, 3), normal map.
|
| 219 |
-
"""
|
| 220 |
-
height, width = point.shape[-3:-1]
|
| 221 |
-
has_mask = mask is not None
|
| 222 |
-
|
| 223 |
-
if mask is None:
|
| 224 |
-
mask = np.ones_like(point[..., 0], dtype=bool)
|
| 225 |
-
mask_pad = np.zeros((height + 2, width + 2), dtype=bool)
|
| 226 |
-
mask_pad[1:-1, 1:-1] = mask
|
| 227 |
-
mask = mask_pad
|
| 228 |
-
|
| 229 |
-
pts = np.zeros((height + 2, width + 2, 3), dtype=point.dtype)
|
| 230 |
-
pts[1:-1, 1:-1, :] = point
|
| 231 |
-
up = pts[:-2, 1:-1, :] - pts[1:-1, 1:-1, :]
|
| 232 |
-
left = pts[1:-1, :-2, :] - pts[1:-1, 1:-1, :]
|
| 233 |
-
down = pts[2:, 1:-1, :] - pts[1:-1, 1:-1, :]
|
| 234 |
-
right = pts[1:-1, 2:, :] - pts[1:-1, 1:-1, :]
|
| 235 |
-
normal = np.stack([
|
| 236 |
-
np.cross(up, left, axis=-1),
|
| 237 |
-
np.cross(left, down, axis=-1),
|
| 238 |
-
np.cross(down, right, axis=-1),
|
| 239 |
-
np.cross(right, up, axis=-1),
|
| 240 |
-
])
|
| 241 |
-
normal = normal / (np.linalg.norm(normal, axis=-1, keepdims=True) + 1e-12)
|
| 242 |
-
valid = np.stack([
|
| 243 |
-
mask[:-2, 1:-1] & mask[1:-1, :-2],
|
| 244 |
-
mask[1:-1, :-2] & mask[2:, 1:-1],
|
| 245 |
-
mask[2:, 1:-1] & mask[1:-1, 2:],
|
| 246 |
-
mask[1:-1, 2:] & mask[:-2, 1:-1],
|
| 247 |
-
]) & mask[None, 1:-1, 1:-1]
|
| 248 |
-
normal = (normal * valid[..., None]).sum(axis=0)
|
| 249 |
-
normal = normal / (np.linalg.norm(normal, axis=-1, keepdims=True) + 1e-12)
|
| 250 |
-
|
| 251 |
-
if has_mask:
|
| 252 |
-
normal_mask = valid.any(axis=0)
|
| 253 |
-
normal = np.where(normal_mask[..., None], normal, 0)
|
| 254 |
-
return normal, normal_mask
|
| 255 |
-
else:
|
| 256 |
-
return normal
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
def depth_to_normals(depth: np.ndarray, intrinsics: np.ndarray, mask: np.ndarray = None) -> np.ndarray:
|
| 260 |
-
"""
|
| 261 |
-
Calculate normal map from depth map. Value range is [-1, 1]. Normal direction in OpenGL identity camera's coordinate system.
|
| 262 |
-
|
| 263 |
-
Args:
|
| 264 |
-
depth (np.ndarray): shape (height, width), linear depth map
|
| 265 |
-
intrinsics (np.ndarray): shape (3, 3), intrinsics matrix
|
| 266 |
-
Returns:
|
| 267 |
-
normal (np.ndarray): shape (height, width, 3), normal map.
|
| 268 |
-
"""
|
| 269 |
-
has_mask = mask is not None
|
| 270 |
-
|
| 271 |
-
height, width = depth.shape[-2:]
|
| 272 |
-
if mask is None:
|
| 273 |
-
mask = np.ones_like(depth, dtype=bool)
|
| 274 |
-
|
| 275 |
-
uv = image_uv(width=width, height=height, dtype=np.float32)
|
| 276 |
-
pts = transforms.unproject_cv(uv, depth, intrinsics=intrinsics, extrinsics=None)
|
| 277 |
-
|
| 278 |
-
return points_to_normals(pts, mask)
|
| 279 |
-
|
| 280 |
-
def interpolate(bary: np.ndarray, tri_id: np.ndarray, attr: np.ndarray, faces: np.ndarray) -> np.ndarray:
|
| 281 |
-
"""Interpolate with given barycentric coordinates and triangle indices
|
| 282 |
-
|
| 283 |
-
Args:
|
| 284 |
-
bary (np.ndarray): shape (..., 3), barycentric coordinates
|
| 285 |
-
tri_id (np.ndarray): int array of shape (...), triangle indices
|
| 286 |
-
attr (np.ndarray): shape (N, M), vertices attributes
|
| 287 |
-
faces (np.ndarray): int array of shape (T, 3), face vertex indices
|
| 288 |
-
|
| 289 |
-
Returns:
|
| 290 |
-
np.ndarray: shape (..., M) interpolated result
|
| 291 |
-
"""
|
| 292 |
-
faces_ = np.concatenate([np.zeros((1, 3), dtype=faces.dtype), faces + 1], axis=0)
|
| 293 |
-
attr_ = np.concatenate([np.zeros((1, attr.shape[1]), dtype=attr.dtype), attr], axis=0)
|
| 294 |
-
return np.sum(bary[..., None] * attr_[faces_[tri_id + 1]], axis=-2)
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
def image_scrcoord(
|
| 298 |
-
width: int,
|
| 299 |
-
height: int,
|
| 300 |
-
) -> np.ndarray:
|
| 301 |
-
"""
|
| 302 |
-
Get OpenGL's screen space coordinates, ranging in [0, 1].
|
| 303 |
-
[0, 0] is the bottom-left corner of the image.
|
| 304 |
-
|
| 305 |
-
Args:
|
| 306 |
-
width (int): image width
|
| 307 |
-
height (int): image height
|
| 308 |
-
|
| 309 |
-
Returns:
|
| 310 |
-
(np.ndarray): shape (height, width, 2)
|
| 311 |
-
"""
|
| 312 |
-
x, y = np.meshgrid(
|
| 313 |
-
np.linspace(0.5 / width, 1 - 0.5 / width, width, dtype=np.float32),
|
| 314 |
-
np.linspace(1 - 0.5 / height, 0.5 / height, height, dtype=np.float32),
|
| 315 |
-
indexing='xy'
|
| 316 |
-
)
|
| 317 |
-
return np.stack([x, y], axis=2)
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
def image_uv(
|
| 321 |
-
height: int,
|
| 322 |
-
width: int,
|
| 323 |
-
left: int = None,
|
| 324 |
-
top: int = None,
|
| 325 |
-
right: int = None,
|
| 326 |
-
bottom: int = None,
|
| 327 |
-
dtype: np.dtype = np.float32
|
| 328 |
-
) -> np.ndarray:
|
| 329 |
-
"""
|
| 330 |
-
Get image space UV grid, ranging in [0, 1].
|
| 331 |
-
|
| 332 |
-
>>> image_uv(10, 10):
|
| 333 |
-
[[[0.05, 0.05], [0.15, 0.05], ..., [0.95, 0.05]],
|
| 334 |
-
[[0.05, 0.15], [0.15, 0.15], ..., [0.95, 0.15]],
|
| 335 |
-
... ... ...
|
| 336 |
-
[[0.05, 0.95], [0.15, 0.95], ..., [0.95, 0.95]]]
|
| 337 |
-
|
| 338 |
-
Args:
|
| 339 |
-
width (int): image width
|
| 340 |
-
height (int): image height
|
| 341 |
-
|
| 342 |
-
Returns:
|
| 343 |
-
np.ndarray: shape (height, width, 2)
|
| 344 |
-
"""
|
| 345 |
-
if left is None: left = 0
|
| 346 |
-
if top is None: top = 0
|
| 347 |
-
if right is None: right = width
|
| 348 |
-
if bottom is None: bottom = height
|
| 349 |
-
u = np.linspace((left + 0.5) / width, (right - 0.5) / width, right - left, dtype=dtype)
|
| 350 |
-
v = np.linspace((top + 0.5) / height, (bottom - 0.5) / height, bottom - top, dtype=dtype)
|
| 351 |
-
u, v = np.meshgrid(u, v, indexing='xy')
|
| 352 |
-
return np.stack([u, v], axis=2)
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
def image_pixel_center(
|
| 356 |
-
height: int,
|
| 357 |
-
width: int,
|
| 358 |
-
left: int = None,
|
| 359 |
-
top: int = None,
|
| 360 |
-
right: int = None,
|
| 361 |
-
bottom: int = None,
|
| 362 |
-
dtype: np.dtype = np.float32
|
| 363 |
-
) -> np.ndarray:
|
| 364 |
-
"""
|
| 365 |
-
Get image pixel center coordinates, ranging in [0, width] and [0, height].
|
| 366 |
-
`image[i, j]` has pixel center coordinates `(j + 0.5, i + 0.5)`.
|
| 367 |
-
|
| 368 |
-
>>> image_pixel_center(10, 10):
|
| 369 |
-
[[[0.5, 0.5], [1.5, 0.5], ..., [9.5, 0.5]],
|
| 370 |
-
[[0.5, 1.5], [1.5, 1.5], ..., [9.5, 1.5]],
|
| 371 |
-
... ... ...
|
| 372 |
-
[[0.5, 9.5], [1.5, 9.5], ..., [9.5, 9.5]]]
|
| 373 |
-
|
| 374 |
-
Args:
|
| 375 |
-
width (int): image width
|
| 376 |
-
height (int): image height
|
| 377 |
-
|
| 378 |
-
Returns:
|
| 379 |
-
np.ndarray: shape (height, width, 2)
|
| 380 |
-
"""
|
| 381 |
-
if left is None: left = 0
|
| 382 |
-
if top is None: top = 0
|
| 383 |
-
if right is None: right = width
|
| 384 |
-
if bottom is None: bottom = height
|
| 385 |
-
u = np.linspace(left + 0.5, right - 0.5, right - left, dtype=dtype)
|
| 386 |
-
v = np.linspace(top + 0.5, bottom - 0.5, bottom - top, dtype=dtype)
|
| 387 |
-
u, v = np.meshgrid(u, v, indexing='xy')
|
| 388 |
-
return np.stack([u, v], axis=2)
|
| 389 |
-
|
| 390 |
-
def image_pixel(
|
| 391 |
-
height: int,
|
| 392 |
-
width: int,
|
| 393 |
-
left: int = None,
|
| 394 |
-
top: int = None,
|
| 395 |
-
right: int = None,
|
| 396 |
-
bottom: int = None,
|
| 397 |
-
dtype: np.dtype = np.int32
|
| 398 |
-
) -> np.ndarray:
|
| 399 |
-
"""
|
| 400 |
-
Get image pixel coordinates grid, ranging in [0, width - 1] and [0, height - 1].
|
| 401 |
-
`image[i, j]` has pixel center coordinates `(j, i)`.
|
| 402 |
-
|
| 403 |
-
>>> image_pixel_center(10, 10):
|
| 404 |
-
[[[0, 0], [1, 0], ..., [9, 0]],
|
| 405 |
-
[[0, 1.5], [1, 1], ..., [9, 1]],
|
| 406 |
-
... ... ...
|
| 407 |
-
[[0, 9.5], [1, 9], ..., [9, 9 ]]]
|
| 408 |
-
|
| 409 |
-
Args:
|
| 410 |
-
width (int): image width
|
| 411 |
-
height (int): image height
|
| 412 |
-
|
| 413 |
-
Returns:
|
| 414 |
-
np.ndarray: shape (height, width, 2)
|
| 415 |
-
"""
|
| 416 |
-
if left is None: left = 0
|
| 417 |
-
if top is None: top = 0
|
| 418 |
-
if right is None: right = width
|
| 419 |
-
if bottom is None: bottom = height
|
| 420 |
-
u = np.arange(left, right, dtype=dtype)
|
| 421 |
-
v = np.arange(top, bottom, dtype=dtype)
|
| 422 |
-
u, v = np.meshgrid(u, v, indexing='xy')
|
| 423 |
-
return np.stack([u, v], axis=2)
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def image_mesh(
|
| 427 |
-
*image_attrs: np.ndarray,
|
| 428 |
-
mask: np.ndarray = None,
|
| 429 |
-
tri: bool = False,
|
| 430 |
-
return_indices: bool = False
|
| 431 |
-
) -> Tuple[np.ndarray, ...]:
|
| 432 |
-
"""
|
| 433 |
-
Get a mesh regarding image pixel uv coordinates as vertices and image grid as faces.
|
| 434 |
-
|
| 435 |
-
Args:
|
| 436 |
-
*image_attrs (np.ndarray): image attributes in shape (height, width, [channels])
|
| 437 |
-
mask (np.ndarray, optional): binary mask of shape (height, width), dtype=bool. Defaults to None.
|
| 438 |
-
|
| 439 |
-
Returns:
|
| 440 |
-
faces (np.ndarray): faces connecting neighboring pixels. shape (T, 4) if tri is False, else (T, 3)
|
| 441 |
-
*vertex_attrs (np.ndarray): vertex attributes in corresponding order with input image_attrs
|
| 442 |
-
indices (np.ndarray, optional): indices of vertices in the original mesh
|
| 443 |
-
"""
|
| 444 |
-
assert (len(image_attrs) > 0) or (mask is not None), "At least one of image_attrs or mask should be provided"
|
| 445 |
-
height, width = next(image_attrs).shape[:2] if mask is None else mask.shape
|
| 446 |
-
assert all(img.shape[:2] == (height, width) for img in image_attrs), "All image_attrs should have the same shape"
|
| 447 |
-
|
| 448 |
-
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)
|
| 449 |
-
faces = (np.arange(0, (height - 1) * width, width, dtype=np.int32)[:, None, None] + row_faces[None, :, :]).reshape((-1, 4))
|
| 450 |
-
if mask is None:
|
| 451 |
-
if tri:
|
| 452 |
-
faces = mesh.triangulate(faces)
|
| 453 |
-
ret = [faces, *(img.reshape(-1, *img.shape[2:]) for img in image_attrs)]
|
| 454 |
-
if return_indices:
|
| 455 |
-
ret.append(np.arange(height * width, dtype=np.int32))
|
| 456 |
-
return tuple(ret)
|
| 457 |
-
else:
|
| 458 |
-
quad_mask = (mask[:-1, :-1] & mask[1:, :-1] & mask[1:, 1:] & mask[:-1, 1:]).ravel()
|
| 459 |
-
faces = faces[quad_mask]
|
| 460 |
-
if tri:
|
| 461 |
-
faces = mesh.triangulate(faces)
|
| 462 |
-
return mesh.remove_unreferenced_vertices(
|
| 463 |
-
faces,
|
| 464 |
-
*(x.reshape(-1, *x.shape[2:]) for x in image_attrs),
|
| 465 |
-
return_indices=return_indices
|
| 466 |
-
)
|
| 467 |
-
|
| 468 |
-
def image_mesh_from_depth(
|
| 469 |
-
depth: np.ndarray,
|
| 470 |
-
extrinsics: np.ndarray = None,
|
| 471 |
-
intrinsics: np.ndarray = None,
|
| 472 |
-
*vertice_attrs: np.ndarray,
|
| 473 |
-
atol: float = None,
|
| 474 |
-
rtol: float = None,
|
| 475 |
-
remove_by_depth: bool = False,
|
| 476 |
-
return_uv: bool = False,
|
| 477 |
-
return_indices: bool = False
|
| 478 |
-
) -> Tuple[np.ndarray, ...]:
|
| 479 |
-
"""
|
| 480 |
-
Get x triangle mesh by lifting depth map to 3D.
|
| 481 |
-
|
| 482 |
-
Args:
|
| 483 |
-
depth (np.ndarray): [H, W] depth map
|
| 484 |
-
extrinsics (np.ndarray, optional): [4, 4] extrinsics matrix. Defaults to None.
|
| 485 |
-
intrinsics (np.ndarray, optional): [3, 3] intrinsics matrix. Defaults to None.
|
| 486 |
-
*vertice_attrs (np.ndarray): [H, W, C] vertex attributes. Defaults to None.
|
| 487 |
-
atol (float, optional): absolute tolerance. Defaults to None.
|
| 488 |
-
rtol (float, optional): relative tolerance. Defaults to None.
|
| 489 |
-
triangles with vertices having depth difference larger than atol + rtol * depth will be marked.
|
| 490 |
-
remove_by_depth (bool, optional): whether to remove triangles with large depth difference. Defaults to True.
|
| 491 |
-
return_uv (bool, optional): whether to return uv coordinates. Defaults to False.
|
| 492 |
-
return_indices (bool, optional): whether to return indices of vertices in the original mesh. Defaults to False.
|
| 493 |
-
|
| 494 |
-
Returns:
|
| 495 |
-
vertices (np.ndarray): [N, 3] vertices
|
| 496 |
-
faces (np.ndarray): [T, 3] faces
|
| 497 |
-
*vertice_attrs (np.ndarray): [N, C] vertex attributes
|
| 498 |
-
image_uv (np.ndarray, optional): [N, 2] uv coordinates
|
| 499 |
-
ref_indices (np.ndarray, optional): [N] indices of vertices in the original mesh
|
| 500 |
-
"""
|
| 501 |
-
height, width = depth.shape
|
| 502 |
-
image_uv, image_face = image_mesh(height, width)
|
| 503 |
-
depth = depth.reshape(-1)
|
| 504 |
-
pts = transforms.unproject_cv(image_uv, depth, extrinsics, intrinsics)
|
| 505 |
-
image_face = mesh.triangulate(image_face, vertices=pts)
|
| 506 |
-
ref_indices = None
|
| 507 |
-
ret = []
|
| 508 |
-
if atol is not None or rtol is not None:
|
| 509 |
-
atol = 0 if atol is None else atol
|
| 510 |
-
rtol = 0 if rtol is None else rtol
|
| 511 |
-
mean = depth[image_face].mean(axis=1)
|
| 512 |
-
diff = np.max(np.abs(depth[image_face] - depth[image_face[:, [1, 2, 0]]]), axis=1)
|
| 513 |
-
mask = (diff <= atol + rtol * mean)
|
| 514 |
-
image_face_ = image_face[mask]
|
| 515 |
-
image_face_, ref_indices = mesh.remove_unreferenced_vertices(image_face_, return_indices=True)
|
| 516 |
-
|
| 517 |
-
remove = remove_by_depth and ref_indices is not None
|
| 518 |
-
if remove:
|
| 519 |
-
pts = pts[ref_indices]
|
| 520 |
-
image_face = image_face_
|
| 521 |
-
ret += [pts, image_face]
|
| 522 |
-
for attr in vertice_attrs:
|
| 523 |
-
ret.append(attr.reshape(-1, attr.shape[-1]) if not remove else attr.reshape(-1, attr.shape[-1])[ref_indices])
|
| 524 |
-
if return_uv:
|
| 525 |
-
ret.append(image_uv if not remove else image_uv[ref_indices])
|
| 526 |
-
if return_indices and ref_indices is not None:
|
| 527 |
-
ret.append(ref_indices)
|
| 528 |
-
return tuple(ret)
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
def chessboard(width: int, height: int, grid_size: int, color_a: np.ndarray, color_b: np.ndarray) -> np.ndarray:
|
| 532 |
-
"""get x chessboard image
|
| 533 |
-
|
| 534 |
-
Args:
|
| 535 |
-
width (int): image width
|
| 536 |
-
height (int): image height
|
| 537 |
-
grid_size (int): size of chessboard grid
|
| 538 |
-
color_a (np.ndarray): color of the grid at the top-left corner
|
| 539 |
-
color_b (np.ndarray): color in complementary grid cells
|
| 540 |
-
|
| 541 |
-
Returns:
|
| 542 |
-
image (np.ndarray): shape (height, width, channels), chessboard image
|
| 543 |
-
"""
|
| 544 |
-
x = np.arange(width) // grid_size
|
| 545 |
-
y = np.arange(height) // grid_size
|
| 546 |
-
mask = (x[None, :] + y[:, None]) % 2
|
| 547 |
-
image = (1 - mask[..., None]) * color_a + mask[..., None] * color_b
|
| 548 |
-
return image
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
def square(tri: bool = False) -> Tuple[np.ndarray, np.ndarray]:
|
| 552 |
-
"""
|
| 553 |
-
Get a square mesh of area 1 centered at origin in the xy-plane.
|
| 554 |
-
|
| 555 |
-
### Returns
|
| 556 |
-
vertices (np.ndarray): shape (4, 3)
|
| 557 |
-
faces (np.ndarray): shape (1, 4)
|
| 558 |
-
"""
|
| 559 |
-
vertices = np.array([
|
| 560 |
-
[-0.5, 0.5, 0], [0.5, 0.5, 0], [0.5, -0.5, 0], [-0.5, -0.5, 0] # v0-v1-v2-v3
|
| 561 |
-
], dtype=np.float32)
|
| 562 |
-
if tri:
|
| 563 |
-
faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32)
|
| 564 |
-
else:
|
| 565 |
-
faces = np.array([[0, 1, 2, 3]], dtype=np.int32)
|
| 566 |
-
return vertices, faces
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
def cube(tri: bool = False) -> Tuple[np.ndarray, np.ndarray]:
|
| 570 |
-
"""
|
| 571 |
-
Get x cube mesh of size 1 centered at origin.
|
| 572 |
-
|
| 573 |
-
### Parameters
|
| 574 |
-
tri (bool, optional): return triangulated mesh. Defaults to False, which returns quad mesh.
|
| 575 |
-
|
| 576 |
-
### Returns
|
| 577 |
-
vertices (np.ndarray): shape (8, 3)
|
| 578 |
-
faces (np.ndarray): shape (12, 3)
|
| 579 |
-
"""
|
| 580 |
-
vertices = np.array([
|
| 581 |
-
[-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
|
| 582 |
-
[-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
|
| 583 |
-
], dtype=np.float32).reshape((-1, 3))
|
| 584 |
-
|
| 585 |
-
faces = np.array([
|
| 586 |
-
[0, 1, 2, 3], # v0-v1-v2-v3 (front)
|
| 587 |
-
[4, 5, 1, 0], # v4-v5-v1-v0 (top)
|
| 588 |
-
[3, 2, 6, 7], # v3-v2-v6-v7 (bottom)
|
| 589 |
-
[5, 4, 7, 6], # v5-v4-v7-v6 (back)
|
| 590 |
-
[1, 5, 6, 2], # v1-v5-v6-v2 (right)
|
| 591 |
-
[4, 0, 3, 7] # v4-v0-v3-v7 (left)
|
| 592 |
-
], dtype=np.int32)
|
| 593 |
-
|
| 594 |
-
if tri:
|
| 595 |
-
faces = mesh.triangulate(faces, vertices=vertices)
|
| 596 |
-
|
| 597 |
-
return vertices, faces
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
def camera_frustum(extrinsics: np.ndarray, intrinsics: np.ndarray, depth: float = 1.0) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 601 |
-
"""
|
| 602 |
-
Get x triangle mesh of camera frustum.
|
| 603 |
-
"""
|
| 604 |
-
assert extrinsics.shape == (4, 4) and intrinsics.shape == (3, 3)
|
| 605 |
-
vertices = transforms.unproject_cv(
|
| 606 |
-
np.array([[0, 0], [0, 0], [0, 1], [1, 1], [1, 0]], dtype=np.float32),
|
| 607 |
-
np.array([0] + [depth] * 4, dtype=np.float32),
|
| 608 |
-
extrinsics,
|
| 609 |
-
intrinsics
|
| 610 |
-
).astype(np.float32)
|
| 611 |
-
edges = np.array([
|
| 612 |
-
[0, 1], [0, 2], [0, 3], [0, 4],
|
| 613 |
-
[1, 2], [2, 3], [3, 4], [4, 1]
|
| 614 |
-
], dtype=np.int32)
|
| 615 |
-
faces = np.array([
|
| 616 |
-
[0, 1, 2],
|
| 617 |
-
[0, 2, 3],
|
| 618 |
-
[0, 3, 4],
|
| 619 |
-
[0, 4, 1],
|
| 620 |
-
[1, 2, 3],
|
| 621 |
-
[1, 3, 4]
|
| 622 |
-
], dtype=np.int32)
|
| 623 |
-
return vertices, edges, faces
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
def icosahedron():
|
| 627 |
-
A = (1 + 5 ** 0.5) / 2
|
| 628 |
-
vertices = np.array([
|
| 629 |
-
[0, 1, A], [0, -1, A], [0, 1, -A], [0, -1, -A],
|
| 630 |
-
[1, A, 0], [-1, A, 0], [1, -A, 0], [-1, -A, 0],
|
| 631 |
-
[A, 0, 1], [A, 0, -1], [-A, 0, 1], [-A, 0, -1]
|
| 632 |
-
], dtype=np.float32)
|
| 633 |
-
faces = np.array([
|
| 634 |
-
[0, 1, 8], [0, 8, 4], [0, 4, 5], [0, 5, 10], [0, 10, 1],
|
| 635 |
-
[3, 2, 9], [3, 9, 6], [3, 6, 7], [3, 7, 11], [3, 11, 2],
|
| 636 |
-
[1, 6, 8], [8, 9, 4], [4, 2, 5], [5, 11, 10], [10, 7, 1],
|
| 637 |
-
[2, 4, 9], [9, 8, 6], [6, 1, 7], [7, 10, 11], [11, 5, 2]
|
| 638 |
-
], dtype=np.int32)
|
| 639 |
-
return vertices, faces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|