Download tools/entropy_pack_point_cloud.py from topabaem/CC-4DGS: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/spaces/topabaem/CC-4DGS/resolve/main/tools/entropy_pack_point_cloud.py
- Command line
-
hf download hf://spaces/topabaem/CC-4DGS/tools/entropy_pack_point_cloud.py
-
curl -L -o entropy_pack_point_cloud.py https://huggingface.co/spaces/topabaem/CC-4DGS/resolve/main/tools/entropy_pack_point_cloud.py
16.8 kB
| #!/usr/bin/env python3 | |
| """ | |
| Entropy-coded variable-bit compressor for CC-4DGS point clouds. | |
| This prototype quantises coordinates/features/rest attributes, then | |
| encodes each dimension with canonical Huffman coding to produce a | |
| compact bitstream. Decompression reconstructs the PLY exactly as | |
| quantised (loss defined solely by the chosen bit depths). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import heapq | |
| from pathlib import Path | |
| from typing import Dict, Iterable, List, Tuple | |
| import numpy as np | |
| import subprocess | |
| import shutil | |
| import sys | |
| REPO_ROOT = Path(__file__).resolve().parents[1] | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| from tools.compress_point_cloud import ( | |
| _load_ply, | |
| _write_ply, | |
| _quantise_linear, | |
| _dequantise_linear, | |
| ) | |
| class BitWriter: | |
| def __init__(self) -> None: | |
| self.buffer = bytearray() | |
| self.current = 0 | |
| self.nbits = 0 | |
| def write(self, value: int, bits: int) -> None: | |
| for shift in range(bits - 1, -1, -1): | |
| bit = (value >> shift) & 1 | |
| self.current = (self.current << 1) | bit | |
| self.nbits += 1 | |
| if self.nbits == 8: | |
| self.buffer.append(self.current) | |
| self.current = 0 | |
| self.nbits = 0 | |
| def finish(self) -> Tuple[bytes, int]: | |
| pad = 0 | |
| if self.nbits: | |
| pad = 8 - self.nbits | |
| self.current <<= pad | |
| self.buffer.append(self.current) | |
| self.current = 0 | |
| self.nbits = 0 | |
| return bytes(self.buffer), pad | |
| class BitReader: | |
| def __init__(self, data: bytes) -> None: | |
| self.data = data | |
| self.byte_pos = 0 | |
| self.bit_pos = 0 | |
| def read_bit(self) -> int: | |
| if self.byte_pos >= len(self.data): | |
| raise EOFError("BitReader exhausted.") | |
| byte = self.data[self.byte_pos] | |
| bit = (byte >> (7 - self.bit_pos)) & 1 | |
| self.bit_pos += 1 | |
| if self.bit_pos == 8: | |
| self.bit_pos = 0 | |
| self.byte_pos += 1 | |
| return bit | |
| def _build_huffman_lengths(values: np.ndarray, alphabet_size: int) -> List[int]: | |
| counts = np.bincount(values, minlength=alphabet_size).astype(np.int64) | |
| heap: List[Tuple[int, int, int | Tuple[int, int]]] = [] | |
| counter = 0 | |
| for symbol, freq in enumerate(counts): | |
| if freq > 0: | |
| heapq.heappush(heap, (freq, counter, symbol)) | |
| counter += 1 | |
| if not heap: | |
| raise ValueError("Empty frequency table; cannot build Huffman codes.") | |
| if len(heap) == 1: | |
| freq, _, symbol = heap[0] | |
| lengths = [0] * alphabet_size | |
| lengths[symbol] = 1 | |
| return lengths | |
| while len(heap) > 1: | |
| freq1, _, node1 = heapq.heappop(heap) | |
| freq2, _, node2 = heapq.heappop(heap) | |
| heapq.heappush(heap, (freq1 + freq2, counter, (node1, node2))) | |
| counter += 1 | |
| root = heap[0][2] | |
| lengths = [0] * alphabet_size | |
| def assign(node: int | Tuple[int, int], depth: int) -> None: | |
| if isinstance(node, int): | |
| lengths[node] = depth | |
| return | |
| left, right = node | |
| assign(left, depth + 1) | |
| assign(right, depth + 1) | |
| assign(root, 0) | |
| return lengths | |
| class CanonicalHuffman: | |
| def __init__(self, lengths: List[int]) -> None: | |
| self.lengths = lengths | |
| self.code_map: Dict[int, Tuple[int, int]] = {} | |
| self.decode_tables: Dict[int, Dict[int, int]] = {} | |
| self.max_len = 0 | |
| self._build() | |
| def _build(self) -> None: | |
| entries = [(l, sym) for sym, l in enumerate(self.lengths) if l > 0] | |
| if not entries: | |
| raise ValueError("No symbols to encode.") | |
| entries.sort() | |
| code = 0 | |
| prev_len = entries[0][0] | |
| code <<= prev_len | |
| for length, symbol in entries: | |
| if length > prev_len: | |
| code <<= length - prev_len | |
| prev_len = length | |
| self.code_map[symbol] = (code, length) | |
| self.decode_tables.setdefault(length, {})[code] = symbol | |
| code += 1 | |
| self.max_len = prev_len | |
| def encode(self, values: np.ndarray) -> Tuple[bytes, int]: | |
| writer = BitWriter() | |
| for val in values: | |
| code, length = self.code_map[val] | |
| writer.write(code, length) | |
| return writer.finish() | |
| def decode(self, data: bytes, count: int) -> np.ndarray: | |
| reader = BitReader(data) | |
| out = np.empty(count, dtype=np.int64) | |
| for idx in range(count): | |
| code = 0 | |
| for length in range(1, self.max_len + 1): | |
| bit = reader.read_bit() | |
| code = (code << 1) | bit | |
| table = self.decode_tables.get(length) | |
| if table and code in table: | |
| out[idx] = table[code] | |
| break | |
| else: | |
| raise ValueError("Failed to decode symbol.") | |
| return out | |
| def _encode_dimension(values: np.ndarray, bits: int) -> Tuple[np.ndarray, np.ndarray, bytes, int]: | |
| alphabet_size = 1 << bits | |
| lengths = _build_huffman_lengths(values, alphabet_size) | |
| codec = CanonicalHuffman(lengths) | |
| stream, pad = codec.encode(values) | |
| return np.array(lengths, dtype=np.uint8), np.array([values.size], dtype=np.int64), stream, pad | |
| def _decode_dimension(stream: bytes, lengths: np.ndarray, count: int) -> np.ndarray: | |
| codec = CanonicalHuffman(lengths.tolist()) | |
| return codec.decode(stream, count).astype(np.int64) | |
| def _stack_fields(arrays: Dict[str, np.ndarray], names: Iterable[str]) -> np.ndarray: | |
| return np.stack([arrays[name] for name in names], axis=1) | |
| def compress_point_cloud( | |
| input_path: Path, | |
| output_path: Path, | |
| *, | |
| pos_bits: int, | |
| feature_bits: int, | |
| rest_bits: int, | |
| ) -> None: | |
| print(f"[entropy] Loading {input_path} ...") | |
| arrays, original_names = _load_ply(input_path) | |
| pos_names = ["x", "y", "z"] | |
| rest_names = sorted([n for n in arrays if n.startswith("f_rest_")], key=lambda x: int(x.split("_")[-1])) | |
| other_names = [n for n in original_names if n not in pos_names + rest_names] | |
| pos = _stack_fields(arrays, pos_names) | |
| others = _stack_fields(arrays, other_names) if other_names else None | |
| rest = _stack_fields(arrays, rest_names) if rest_names else None | |
| meta: Dict[str, object] = { | |
| "version": 1, | |
| "original_names": original_names, | |
| "pos_names": pos_names, | |
| "rest_names": rest_names, | |
| "other_names": other_names, | |
| "pos_bits": pos_bits, | |
| "feature_bits": feature_bits, | |
| "other_bits": feature_bits, | |
| "rest_bits": rest_bits, | |
| "count": int(pos.shape[0]), | |
| } | |
| compressed: Dict[str, np.ndarray] = {} | |
| print("[entropy] Quantising positions ...") | |
| pos_q, pos_min, pos_max = _quantise_linear(pos, pos_bits) | |
| compressed["pos_min"] = pos_min | |
| compressed["pos_max"] = pos_max | |
| for dim in range(pos_q.shape[1]): | |
| lengths, counts, stream, pad = _encode_dimension(pos_q[:, dim].astype(np.int64), pos_bits) | |
| compressed[f"pos_lengths_{dim}"] = lengths | |
| compressed[f"pos_counts_{dim}"] = counts | |
| compressed[f"pos_stream_{dim}"] = np.frombuffer(stream, dtype=np.uint8) | |
| compressed[f"pos_pad_{dim}"] = np.array([pad], dtype=np.uint8) | |
| if others is not None: | |
| print("[entropy] Quantising other attributes ...") | |
| other_q, other_min, other_max = _quantise_linear(others, feature_bits) | |
| compressed["other_min"] = other_min | |
| compressed["other_max"] = other_max | |
| for dim in range(other_q.shape[1]): | |
| lengths, counts, stream, pad = _encode_dimension(other_q[:, dim].astype(np.int64), feature_bits) | |
| compressed[f"other_lengths_{dim}"] = lengths | |
| compressed[f"other_counts_{dim}"] = counts | |
| compressed[f"other_stream_{dim}"] = np.frombuffer(stream, dtype=np.uint8) | |
| compressed[f"other_pad_{dim}"] = np.array([pad], dtype=np.uint8) | |
| else: | |
| compressed["other_min"] = np.empty(0, dtype=np.float32) | |
| compressed["other_max"] = np.empty(0, dtype=np.float32) | |
| if rest is not None: | |
| print("[entropy] Quantising f_rest* ...") | |
| rest_q, rest_min, rest_max = _quantise_linear(rest, rest_bits) | |
| compressed["rest_min"] = rest_min | |
| compressed["rest_max"] = rest_max | |
| for dim in range(rest_q.shape[1]): | |
| lengths, counts, stream, pad = _encode_dimension(rest_q[:, dim].astype(np.int64), rest_bits) | |
| compressed[f"rest_lengths_{dim}"] = lengths | |
| compressed[f"rest_counts_{dim}"] = counts | |
| compressed[f"rest_stream_{dim}"] = np.frombuffer(stream, dtype=np.uint8) | |
| compressed[f"rest_pad_{dim}"] = np.array([pad], dtype=np.uint8) | |
| else: | |
| compressed["rest_min"] = np.empty(0, dtype=np.float32) | |
| compressed["rest_max"] = np.empty(0, dtype=np.float32) | |
| compressed["meta"] = np.array(json.dumps(meta)) | |
| print(f"[entropy] Saving archive to {output_path} ...") | |
| np.savez_compressed(output_path, **compressed) | |
| print("[entropy] Compression done.") | |
| def _decode_group(prefix: str, meta: Dict[str, object], packed) -> np.ndarray | None: | |
| names: List[str] = meta[prefix + "_names"] | |
| if not names: | |
| return None | |
| bits = int(meta[f"{prefix}_bits"]) | |
| mins = packed[f"{prefix}_min"] | |
| maxs = packed[f"{prefix}_max"] | |
| decoded_cols = [] | |
| for dim in range(len(names)): | |
| lengths = packed[f"{prefix}_lengths_{dim}"] | |
| counts = int(packed[f"{prefix}_counts_{dim}"][0]) | |
| stream = bytes(packed[f"{prefix}_stream_{dim}"]) | |
| values = _decode_dimension(stream, lengths, counts).astype(np.int64) | |
| decoded = _dequantise_linear(values.reshape(-1, 1), mins[dim : dim + 1], maxs[dim : dim + 1], bits) | |
| decoded_cols.append(decoded[:, 0]) | |
| return np.stack(decoded_cols, axis=1) | |
| def decompress_point_cloud(input_path: Path, output_path: Path) -> None: | |
| print(f"[entropy] Loading archive {input_path} ...") | |
| packed = np.load(input_path, allow_pickle=False) | |
| meta = json.loads(str(packed["meta"])) | |
| arrays: Dict[str, np.ndarray] = {} | |
| pos = _decode_group("pos", meta, packed) | |
| for idx, name in enumerate(meta["pos_names"]): | |
| arrays[name] = pos[:, idx] | |
| others = _decode_group("other", meta, packed) | |
| if others is not None: | |
| for idx, name in enumerate(meta["other_names"]): | |
| arrays[name] = others[:, idx] | |
| rest = _decode_group("rest", meta, packed) | |
| if rest is not None: | |
| for idx, name in enumerate(meta["rest_names"]): | |
| arrays[name] = rest[:, idx] | |
| print(f"[entropy] Writing reconstructed PLY to {output_path} ...") | |
| _write_ply(output_path, arrays, meta["original_names"]) | |
| print("[entropy] Decompression done.") | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description="Entropy-coded point cloud compressor.") | |
| sub = parser.add_subparsers(dest="command", required=True) | |
| comp = sub.add_parser("compress") | |
| comp.add_argument("input", type=Path) | |
| comp.add_argument("output", type=Path) | |
| comp.add_argument("--pos-bits", type=int, default=16) | |
| comp.add_argument("--feature-bits", type=int, default=10) | |
| comp.add_argument("--rest-bits", type=int, default=12) | |
| comp.add_argument("--ram", action="store_true", help="After compression, render & metric the reconstructed PLY.") | |
| comp.add_argument("--model-path", type=Path) | |
| comp.add_argument("--dataset-path", type=Path) | |
| comp.add_argument("--config-path", type=Path) | |
| comp.add_argument("--iteration", type=int) | |
| comp.add_argument("--deformation-source", type=Path) | |
| comp.add_argument( | |
| "--keep-temp-ply", | |
| action="store_true", | |
| help="Keep the reconstructed PLY produced for --ram.", | |
| ) | |
| decomp = sub.add_parser("decompress") | |
| decomp.add_argument("input", type=Path) | |
| decomp.add_argument("output", type=Path) | |
| decomp.add_argument("--ram", action="store_true", help="After decompression, render & metric.") | |
| decomp.add_argument("--model-path", type=Path) | |
| decomp.add_argument("--dataset-path", type=Path) | |
| decomp.add_argument("--config-path", type=Path) | |
| decomp.add_argument("--iteration", type=int) | |
| decomp.add_argument("--deformation-source", type=Path) | |
| return parser | |
| def main() -> None: | |
| parser = build_parser() | |
| args = parser.parse_args() | |
| if args.command == "compress": | |
| input_path = args.input | |
| default_deform = None | |
| if input_path.is_dir(): | |
| ply_candidate = input_path / "point_cloud.ply" | |
| if not ply_candidate.exists(): | |
| parser.error(f"Iteration folder {input_path} is missing point_cloud.ply") | |
| input_path = ply_candidate | |
| possible_def = input_path.parent / "deformation.pth" | |
| if possible_def.exists(): | |
| default_deform = possible_def | |
| else: | |
| possible_def = input_path.parent / "deformation.pth" | |
| if possible_def.exists(): | |
| default_deform = possible_def | |
| compress_point_cloud( | |
| input_path, | |
| args.output, | |
| pos_bits=args.pos_bits, | |
| feature_bits=args.feature_bits, | |
| rest_bits=args.rest_bits, | |
| ) | |
| if args.ram: | |
| required = [args.model_path, args.dataset_path, args.config_path, args.iteration] | |
| if any(v is None for v in required): | |
| parser.error("--ram requires --model-path, --dataset-path, --config-path, --iteration") | |
| temp_ply = args.output.with_suffix(".ply") | |
| decompress_point_cloud(args.output, temp_ply) | |
| iter_dir = args.model_path / "point_cloud" / f"iteration_{args.iteration}" | |
| iter_dir.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(temp_ply, iter_dir / "point_cloud.ply") | |
| deform_src = args.deformation_source or default_deform or (input_path.parent / "deformation.pth") | |
| if not deform_src.exists(): | |
| raise FileNotFoundError(f"Missing deformation checkpoint: {deform_src}") | |
| shutil.copy2(deform_src, iter_dir / "deformation.pth") | |
| render_cmd = [ | |
| sys.executable, | |
| str(REPO_ROOT / "render.py"), | |
| "-s", | |
| str(args.dataset_path), | |
| "--configs", | |
| str(args.config_path), | |
| "--model_path", | |
| str(args.model_path), | |
| "--iteration", | |
| str(args.iteration), | |
| "--skip_train", | |
| "--skip_video", | |
| ] | |
| metrics_cmd = [ | |
| sys.executable, | |
| str(REPO_ROOT / "metrics.py"), | |
| "-m", | |
| str(args.model_path), | |
| ] | |
| subprocess.run(render_cmd, cwd=REPO_ROOT, check=True) | |
| subprocess.run(metrics_cmd, cwd=REPO_ROOT, check=True) | |
| if not args.keep_temp_ply: | |
| temp_ply.unlink(missing_ok=True) | |
| elif args.command == "decompress": | |
| if args.ram: | |
| required = [args.model_path, args.dataset_path, args.config_path, args.iteration] | |
| if any(v is None for v in required): | |
| parser.error("--ram requires --model-path, --dataset-path, --config-path, --iteration") | |
| iter_dir = args.model_path / "point_cloud" / f"iteration_{args.iteration}" | |
| iter_dir.mkdir(parents=True, exist_ok=True) | |
| target_ply = iter_dir / "point_cloud.ply" | |
| decompress_point_cloud(args.input, target_ply) | |
| if args.output.resolve() != target_ply.resolve(): | |
| shutil.copy2(target_ply, args.output) | |
| deform_src = args.deformation_source | |
| if deform_src and deform_src.exists(): | |
| shutil.copy2(deform_src, iter_dir / "deformation.pth") | |
| render_cmd = [ | |
| sys.executable, | |
| str(REPO_ROOT / "render.py"), | |
| "-s", | |
| str(args.dataset_path), | |
| "--configs", | |
| str(args.config_path), | |
| "--model_path", | |
| str(args.model_path), | |
| "--iteration", | |
| str(args.iteration), | |
| "--skip_train", | |
| "--skip_video", | |
| ] | |
| metrics_cmd = [ | |
| sys.executable, | |
| str(REPO_ROOT / "metrics.py"), | |
| "-m", | |
| str(args.model_path), | |
| ] | |
| subprocess.run(render_cmd, cwd=REPO_ROOT, check=True) | |
| subprocess.run(metrics_cmd, cwd=REPO_ROOT, check=True) | |
| else: | |
| decompress_point_cloud(args.input, args.output) | |
| if __name__ == "__main__": | |
| main() | |