Spaces:
Running on Zero
Running on Zero
Delete files codecsplat_pipeline.py LICENSE-CodecSplat with huggingface_hub
Browse files- LICENSE-CodecSplat +0 -21
- codecsplat_pipeline.py +0 -338
LICENSE-CodecSplat
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
MIT License
|
| 2 |
-
|
| 3 |
-
Copyright (c) 2026 Pengpeng Yu
|
| 4 |
-
|
| 5 |
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
-
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
-
in the Software without restriction, including without limitation the rights
|
| 8 |
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
-
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
-
furnished to do so, subject to the following conditions:
|
| 11 |
-
|
| 12 |
-
The above copyright notice and this permission notice shall be included in all
|
| 13 |
-
copies or substantial portions of the Software.
|
| 14 |
-
|
| 15 |
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
-
SOFTWARE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
codecsplat_pipeline.py
DELETED
|
@@ -1,338 +0,0 @@
|
|
| 1 |
-
"""CodecSplat inference helpers for the Space.
|
| 2 |
-
|
| 3 |
-
Everything here mirrors the authors' evaluation path
|
| 4 |
-
(`scripts/test_dl3dv_view8_256x448.sh` -> `src/main.py` -> `ModelWrapper.test_step`):
|
| 5 |
-
the encoder runs the *complete* learned-codec path (encode context -> entropy-code the
|
| 6 |
-
2D latent features into a real bitstream -> decode the bitstream -> predict Gaussians),
|
| 7 |
-
and the gsplat decoder renders novel views, all under bf16 autocast like
|
| 8 |
-
`trainer.precision=bf16-mixed`.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import os
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
from types import SimpleNamespace
|
| 16 |
-
|
| 17 |
-
import numpy as np
|
| 18 |
-
import torch
|
| 19 |
-
from dacite import Config as DaciteConfig, from_dict
|
| 20 |
-
from einops import rearrange, repeat
|
| 21 |
-
from omegaconf import OmegaConf
|
| 22 |
-
from plyfile import PlyData, PlyElement
|
| 23 |
-
from scipy.spatial.transform import Rotation as R
|
| 24 |
-
from scipy.spatial.transform import Slerp
|
| 25 |
-
|
| 26 |
-
from src.dataset.shims.crop_shim import apply_crop_shim
|
| 27 |
-
from src.dataset.shims.patch_shim import apply_patch_shim
|
| 28 |
-
from src.misc.sh_rotation import rotate_sh
|
| 29 |
-
from src.model.encoder import get_encoder
|
| 30 |
-
from src.model.encoder.encoder_codecsplat import EncoderCodecSplatCfg
|
| 31 |
-
|
| 32 |
-
APP_DIR = Path(__file__).parent.resolve()
|
| 33 |
-
|
| 34 |
-
# ---- authors' DL3DV 8-view 256x448 evaluation preset ---------------------------------
|
| 35 |
-
IMAGE_SHAPE = (256, 448)
|
| 36 |
-
NEAR, FAR = 0.5, 200.0 # config/experiment/dl3dv.yaml
|
| 37 |
-
ENCODER_OVERRIDES = {
|
| 38 |
-
"num_depth_candidates": 128,
|
| 39 |
-
"shim_patch_size": 16,
|
| 40 |
-
"num_scales": 2,
|
| 41 |
-
"upsample_factor": 4,
|
| 42 |
-
"lowest_feature_resolution": 8,
|
| 43 |
-
"monodepth_vit_type": "vitb",
|
| 44 |
-
"enable_feature_codec": True,
|
| 45 |
-
"enable_depth_guided_mv_refiner": True,
|
| 46 |
-
"enable_post_vae_mv_refiner": True,
|
| 47 |
-
"codec_lmb_range": [16.0, 1024.0],
|
| 48 |
-
"supervise_intermediate_depth": False,
|
| 49 |
-
}
|
| 50 |
-
DECODER_OVERRIDES = {
|
| 51 |
-
"use_covariances": True,
|
| 52 |
-
"rasterize_mode": "antialiased",
|
| 53 |
-
"eps2d": 0.1,
|
| 54 |
-
"packed": True,
|
| 55 |
-
"scale_invariant": False,
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def build_encoder(checkpoint_path: str):
|
| 60 |
-
enc_cfg = OmegaConf.load(APP_DIR / "config/model/encoder/codecsplat.yaml")
|
| 61 |
-
enc_cfg = OmegaConf.merge(enc_cfg, ENCODER_OVERRIDES)
|
| 62 |
-
enc_cfg = from_dict(
|
| 63 |
-
EncoderCodecSplatCfg,
|
| 64 |
-
OmegaConf.to_container(enc_cfg, resolve=True),
|
| 65 |
-
config=DaciteConfig(cast=[tuple], strict=False),
|
| 66 |
-
)
|
| 67 |
-
encoder = get_encoder(enc_cfg)
|
| 68 |
-
|
| 69 |
-
ckpt = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
| 70 |
-
state = ckpt.get("state_dict", ckpt)
|
| 71 |
-
state = {k[len("encoder."):]: v for k, v in state.items() if k.startswith("encoder.")}
|
| 72 |
-
encoder.load_state_dict(state, strict=True)
|
| 73 |
-
# eval() also runs `compress_mode` -> builds the entropy-coder CDF tables (on CPU).
|
| 74 |
-
encoder.eval()
|
| 75 |
-
return encoder
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def build_decoder():
|
| 79 |
-
"""The authors' `model/decoder=splatting_cuda` decoder (Inria diff-gaussian-rasterization).
|
| 80 |
-
|
| 81 |
-
The evaluation scripts use the gsplat decoder; on ZeroGPU we use the repo's other
|
| 82 |
-
supported decoder because a prebuilt sm_120 wheel exists for it (gsplat would have to
|
| 83 |
-
be JIT-compiled against a CUDA toolkit at startup).
|
| 84 |
-
"""
|
| 85 |
-
from src.model.decoder.decoder_splatting_cuda import (
|
| 86 |
-
DecoderSplattingCUDA,
|
| 87 |
-
DecoderSplattingCUDACfg,
|
| 88 |
-
)
|
| 89 |
-
|
| 90 |
-
decoder = DecoderSplattingCUDA(
|
| 91 |
-
DecoderSplattingCUDACfg(name="splatting_cuda"),
|
| 92 |
-
SimpleNamespace(background_color=[0.0, 0.0, 0.0]),
|
| 93 |
-
)
|
| 94 |
-
return decoder.eval()
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
# ---- view selection ------------------------------------------------------------------
|
| 98 |
-
def farthest_point_sample(xyz: np.ndarray, npoint: int) -> np.ndarray:
|
| 99 |
-
n = len(xyz)
|
| 100 |
-
if npoint >= n:
|
| 101 |
-
return np.arange(n)
|
| 102 |
-
dist = np.full(n, 1e10)
|
| 103 |
-
farthest = int(np.argmax(((xyz - xyz.mean(0)) ** 2).sum(-1)))
|
| 104 |
-
out = []
|
| 105 |
-
for _ in range(npoint):
|
| 106 |
-
out.append(farthest)
|
| 107 |
-
d = ((xyz - xyz[farthest]) ** 2).sum(-1)
|
| 108 |
-
dist = np.minimum(dist, d)
|
| 109 |
-
farthest = int(np.argmax(dist))
|
| 110 |
-
return np.sort(np.array(out))
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def select_views(c2w: np.ndarray, num_context: int = 8, num_target: int = 8):
|
| 114 |
-
n = len(c2w)
|
| 115 |
-
ctx = farthest_point_sample(c2w[:, :3, 3], min(num_context, n))
|
| 116 |
-
rest = np.setdiff1d(np.arange(n), ctx)
|
| 117 |
-
if len(rest) > num_target:
|
| 118 |
-
rest = rest[np.linspace(0, len(rest) - 1, num_target).astype(int)]
|
| 119 |
-
return ctx, rest
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# ---- batch construction (same conventions as the dataset loaders) --------------------
|
| 123 |
-
def _load_images(paths):
|
| 124 |
-
from PIL import Image
|
| 125 |
-
|
| 126 |
-
imgs = []
|
| 127 |
-
for p in paths:
|
| 128 |
-
im = np.asarray(Image.open(p).convert("RGB"), dtype=np.float32) / 255.0
|
| 129 |
-
imgs.append(torch.from_numpy(im).permute(2, 0, 1))
|
| 130 |
-
return torch.stack(imgs)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def image_shape_for(h: int, w: int) -> tuple[int, int]:
|
| 134 |
-
# The checkpoint is trained at 256x448 (landscape). Portrait inputs use 448x256.
|
| 135 |
-
return IMAGE_SHAPE if w >= h else (IMAGE_SHAPE[1], IMAGE_SHAPE[0])
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def build_example(image_paths, c2w, K, ctx_idx, tgt_idx):
|
| 139 |
-
"""Build a single (un-batched) example, apply the authors' crop + patch shims."""
|
| 140 |
-
images = _load_images(image_paths) # [N, 3, H, W]
|
| 141 |
-
shape = image_shape_for(images.shape[-2], images.shape[-1])
|
| 142 |
-
c2w = torch.as_tensor(c2w, dtype=torch.float32)
|
| 143 |
-
K = torch.as_tensor(K, dtype=torch.float32)
|
| 144 |
-
|
| 145 |
-
# Express all poses relative to the middle context camera (rigid change of frame).
|
| 146 |
-
pivot = c2w[ctx_idx[len(ctx_idx) // 2]]
|
| 147 |
-
c2w = torch.linalg.inv(pivot)[None] @ c2w
|
| 148 |
-
|
| 149 |
-
def views(idx):
|
| 150 |
-
idx = torch.as_tensor(np.asarray(idx), dtype=torch.long)
|
| 151 |
-
return {
|
| 152 |
-
"extrinsics": c2w[idx],
|
| 153 |
-
"intrinsics": K[idx],
|
| 154 |
-
"image": images[idx],
|
| 155 |
-
"near": torch.full((len(idx),), NEAR),
|
| 156 |
-
"far": torch.full((len(idx),), FAR),
|
| 157 |
-
"index": idx,
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
example = {"context": views(ctx_idx), "target": views(tgt_idx), "scene": "user_scene"}
|
| 161 |
-
example = apply_crop_shim(example, shape)
|
| 162 |
-
batch = {
|
| 163 |
-
"context": {k: v[None] for k, v in example["context"].items()},
|
| 164 |
-
"target": {k: v[None] for k, v in example["target"].items()},
|
| 165 |
-
"scene": ["user_scene"],
|
| 166 |
-
}
|
| 167 |
-
# shim_patch_size * downscale_factor = 16 * 4
|
| 168 |
-
return apply_patch_shim(batch, patch_size=64)
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def to_device(batch, device):
|
| 172 |
-
out = {}
|
| 173 |
-
for k, v in batch.items():
|
| 174 |
-
if isinstance(v, dict):
|
| 175 |
-
out[k] = {kk: vv.to(device) for kk, vv in v.items()}
|
| 176 |
-
else:
|
| 177 |
-
out[k] = v
|
| 178 |
-
return out
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
# ---- smooth fly-through trajectory through the context cameras ----------------------
|
| 182 |
-
def order_cameras(c2w: torch.Tensor) -> list[int]:
|
| 183 |
-
"""Greedy nearest-neighbour ordering of cameras (start from an extreme one)."""
|
| 184 |
-
pos = c2w[:, :3, 3].cpu().numpy()
|
| 185 |
-
n = len(pos)
|
| 186 |
-
if n <= 2:
|
| 187 |
-
return list(range(n))
|
| 188 |
-
start = int(np.argmax(((pos - pos.mean(0)) ** 2).sum(-1)))
|
| 189 |
-
order, left = [start], set(range(n)) - {start}
|
| 190 |
-
while left:
|
| 191 |
-
last = pos[order[-1]]
|
| 192 |
-
nxt = min(left, key=lambda i: float(((pos[i] - last) ** 2).sum()))
|
| 193 |
-
order.append(nxt)
|
| 194 |
-
left.remove(nxt)
|
| 195 |
-
return order
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def interpolate_trajectory(c2w: torch.Tensor, K: torch.Tensor, frames_per_segment: int = 12):
|
| 199 |
-
order = order_cameras(c2w)
|
| 200 |
-
c2w = c2w[order].cpu().double().numpy()
|
| 201 |
-
K = K[order].cpu()
|
| 202 |
-
poses, intrs = [], []
|
| 203 |
-
for i in range(len(order) - 1):
|
| 204 |
-
rots = R.from_matrix(c2w[i : i + 2, :3, :3])
|
| 205 |
-
slerp = Slerp([0.0, 1.0], rots)
|
| 206 |
-
for t in np.linspace(0, 1, frames_per_segment, endpoint=False):
|
| 207 |
-
m = np.eye(4)
|
| 208 |
-
m[:3, :3] = slerp([t]).as_matrix()[0]
|
| 209 |
-
m[:3, 3] = (1 - t) * c2w[i, :3, 3] + t * c2w[i + 1, :3, 3]
|
| 210 |
-
poses.append(m)
|
| 211 |
-
intrs.append((1 - t) * K[i] + t * K[i + 1])
|
| 212 |
-
last = np.eye(4)
|
| 213 |
-
last[:3] = c2w[-1, :3]
|
| 214 |
-
poses.append(last)
|
| 215 |
-
intrs.append(K[-1])
|
| 216 |
-
poses = poses + poses[::-1][1:] # ping-pong loop
|
| 217 |
-
intrs = intrs + intrs[::-1][1:]
|
| 218 |
-
return (
|
| 219 |
-
torch.tensor(np.stack(poses), dtype=torch.float32),
|
| 220 |
-
torch.stack([torch.as_tensor(k, dtype=torch.float32) for k in intrs]),
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
# ---- core inference ------------------------------------------------------------------
|
| 225 |
-
@torch.no_grad()
|
| 226 |
-
def encode_decode(encoder, batch, lmb: float):
|
| 227 |
-
"""Full learned-codec round trip: context -> bitstream -> Gaussians."""
|
| 228 |
-
encoder.feature_codec.cfg.lmb = float(lmb)
|
| 229 |
-
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=batch["context"]["image"].is_cuda):
|
| 230 |
-
out = encoder(batch["context"], 0, visualization_dump={}, return_bitstream=True)
|
| 231 |
-
return out
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
@torch.no_grad()
|
| 235 |
-
def render(decoder, gaussians, extrinsics, intrinsics, hw, chunk: int = 16):
|
| 236 |
-
"""Render [V] views. `extrinsics`/`intrinsics` are [V,4,4]/[V,3,3] on device."""
|
| 237 |
-
v = extrinsics.shape[0]
|
| 238 |
-
near = torch.full((1, v), NEAR, device=extrinsics.device)
|
| 239 |
-
far = torch.full((1, v), FAR, device=extrinsics.device)
|
| 240 |
-
colors = []
|
| 241 |
-
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=extrinsics.is_cuda):
|
| 242 |
-
for s in range(0, v, chunk):
|
| 243 |
-
o = decoder.forward(
|
| 244 |
-
gaussians,
|
| 245 |
-
extrinsics[None, s : s + chunk],
|
| 246 |
-
intrinsics[None, s : s + chunk],
|
| 247 |
-
near[:, s : s + chunk],
|
| 248 |
-
far[:, s : s + chunk],
|
| 249 |
-
hw,
|
| 250 |
-
depth_mode=None,
|
| 251 |
-
)
|
| 252 |
-
colors.append(o.color[0].float().clamp(0, 1))
|
| 253 |
-
return torch.cat(colors, dim=0)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def psnr(gt: torch.Tensor, pred: torch.Tensor) -> float:
|
| 257 |
-
mse = ((gt - pred) ** 2).mean(dim=(1, 2, 3)).clamp_min(1e-10)
|
| 258 |
-
return float((-10 * torch.log10(mse)).mean())
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
# ---- Gaussian export -----------------------------------------------------------------
|
| 262 |
-
def world_rotations(rotations: torch.Tensor, context_c2w: torch.Tensor) -> torch.Tensor:
|
| 263 |
-
"""Per-pixel Gaussian rotations are predicted in each context camera's frame."""
|
| 264 |
-
v = context_c2w.shape[0]
|
| 265 |
-
n = rotations.shape[0]
|
| 266 |
-
c2w_rot = repeat(context_c2w[:, :3, :3], "v i j -> (v r) i j", r=n // v)
|
| 267 |
-
local = R.from_quat(rotations.cpu().double().numpy()).as_matrix()
|
| 268 |
-
world = c2w_rot.cpu().double().numpy() @ local
|
| 269 |
-
return torch.from_numpy(R.from_matrix(world).as_quat()).float()
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def write_ply(
|
| 273 |
-
path,
|
| 274 |
-
pivot_c2w: torch.Tensor,
|
| 275 |
-
means: torch.Tensor,
|
| 276 |
-
scales: torch.Tensor,
|
| 277 |
-
rotations_xyzw_world: torch.Tensor,
|
| 278 |
-
harmonics: torch.Tensor,
|
| 279 |
-
opacities: torch.Tensor,
|
| 280 |
-
):
|
| 281 |
-
"""Same output as the authors' `export_ply(..., align_to_view=True)`, vectorised."""
|
| 282 |
-
means = means.float().cpu()
|
| 283 |
-
scales = scales.float().cpu()
|
| 284 |
-
harmonics = harmonics.float().cpu()
|
| 285 |
-
opacities = opacities.float().cpu()
|
| 286 |
-
pivot = pivot_c2w.float().cpu()
|
| 287 |
-
|
| 288 |
-
view_rot = pivot[:3, :3].T
|
| 289 |
-
means = (means - pivot[:3, 3]) @ view_rot.T
|
| 290 |
-
harmonics = rotate_sh(harmonics, view_rot)
|
| 291 |
-
rot = R.from_quat(rotations_xyzw_world.double().numpy()).as_matrix()
|
| 292 |
-
rot = R.from_matrix(view_rot.double().numpy() @ rot).as_quat() # xyzw
|
| 293 |
-
rot = np.concatenate([rot[:, 3:4], rot[:, :3]], axis=1) # wxyz
|
| 294 |
-
|
| 295 |
-
dc = harmonics[..., 0].numpy()
|
| 296 |
-
rest = harmonics[..., 1:].flatten(start_dim=1).numpy()
|
| 297 |
-
logit_op = torch.logit(opacities.clamp(1e-6, 1 - 1e-6))[:, None].numpy()
|
| 298 |
-
log_scales = scales.clamp_min(1e-12).log().numpy()
|
| 299 |
-
m = means.numpy()
|
| 300 |
-
|
| 301 |
-
cols = [m, np.zeros_like(m), dc, rest, logit_op, log_scales, rot]
|
| 302 |
-
data = np.concatenate(cols, axis=1).astype(np.float32)
|
| 303 |
-
names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2"]
|
| 304 |
-
names += [f"f_rest_{i}" for i in range(rest.shape[1])]
|
| 305 |
-
names += ["opacity", "scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
|
| 306 |
-
arr = np.empty(data.shape[0], dtype=[(n, "f4") for n in names])
|
| 307 |
-
for i, n in enumerate(names):
|
| 308 |
-
arr[n] = data[:, i]
|
| 309 |
-
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 310 |
-
PlyData([PlyElement.describe(arr, "vertex")]).write(str(path))
|
| 311 |
-
return str(path)
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
def export_gaussians(g: dict, context_c2w: torch.Tensor, outdir: Path,
|
| 315 |
-
viewer_max: int = 600_000, viewer_min_opacity: float = 0.02):
|
| 316 |
-
"""Write a compact (DC colour, pruned), re-framed .ply of the Gaussians for the web viewer."""
|
| 317 |
-
pivot = context_c2w[len(context_c2w) // 2]
|
| 318 |
-
rot_world = world_rotations(g["rotations"], context_c2w)
|
| 319 |
-
|
| 320 |
-
keep = g["opacities"] > viewer_min_opacity
|
| 321 |
-
if int(keep.sum()) < 1000:
|
| 322 |
-
keep = torch.ones_like(keep)
|
| 323 |
-
idx = torch.nonzero(keep).squeeze(1)
|
| 324 |
-
if idx.numel() > viewer_max:
|
| 325 |
-
idx = idx[torch.randperm(idx.numel())[:viewer_max]]
|
| 326 |
-
means, scales = g["means"][idx], g["scales"][idx]
|
| 327 |
-
|
| 328 |
-
# Re-centre / re-scale (in the pivot camera frame) so the viewer's orbit camera frames it.
|
| 329 |
-
view_rot = pivot[:3, :3].T
|
| 330 |
-
local = (means - pivot[:3, 3]) @ view_rot.T
|
| 331 |
-
center = local.median(dim=0).values
|
| 332 |
-
radius = torch.quantile((local - center).norm(dim=-1).float(), 0.7)
|
| 333 |
-
s = float(3.0 / max(float(radius), 1e-6))
|
| 334 |
-
# shift in world coordinates so that the pivot-frame centre maps to the origin
|
| 335 |
-
means = (means - pivot[:3, 3] - center @ view_rot) * s + pivot[:3, 3]
|
| 336 |
-
viewer_path = write_ply(outdir / "codecsplat_viewer.ply", pivot, means, scales * s,
|
| 337 |
-
rot_world[idx], g["harmonics"][idx][..., :1], g["opacities"][idx])
|
| 338 |
-
return viewer_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|