gaussforge 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ # Build artifacts
2
+ build/
3
+ dist/
4
+ *.egg-info/
5
+ *.whl
6
+
7
+ # Compiled extensions
8
+ *.so
9
+ *.cpython-*.so
10
+
11
+ # Python cache
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+ .pytest_cache/
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
@@ -0,0 +1,84 @@
1
+ # Python binding CMakeLists.txt
2
+ # This file is included when building the Python package via scikit-build-core
3
+
4
+ cmake_minimum_required(VERSION 3.26)
5
+
6
+ project(gaussforge_python
7
+ VERSION 0.4.2
8
+ LANGUAGES CXX C
9
+ )
10
+
11
+ set(CMAKE_CXX_STANDARD 17)
12
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
13
+
14
+ # Find Python and nanobind
15
+ find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
16
+ find_package(nanobind CONFIG REQUIRED)
17
+
18
+ # Include parent project's dependencies
19
+ set(GAUSS_FORGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..")
20
+
21
+ # Configure version header
22
+ configure_file(
23
+ ${GAUSS_FORGE_ROOT}/include/gf/core/version.h.in
24
+ ${CMAKE_CURRENT_BINARY_DIR}/include/gf/core/version.h
25
+ @ONLY
26
+ )
27
+
28
+ # Include dependency configurations
29
+ include(FetchContent)
30
+ include(${GAUSS_FORGE_ROOT}/cmakes/zlib.cmake)
31
+ include(${GAUSS_FORGE_ROOT}/cmakes/sog.cmake)
32
+ include(${GAUSS_FORGE_ROOT}/cmakes/spz.cmake)
33
+
34
+ # Core library source files
35
+ set(GAUSS_FORGE_SOURCES
36
+ ${GAUSS_FORGE_ROOT}/src/core/model_info.cpp
37
+ ${GAUSS_FORGE_ROOT}/src/core/validate.cpp
38
+ ${GAUSS_FORGE_ROOT}/src/io/registry.cpp
39
+ ${GAUSS_FORGE_ROOT}/src/io/ply_auto.cpp
40
+ ${GAUSS_FORGE_ROOT}/src/io/ply_compressed_reader.cpp
41
+ ${GAUSS_FORGE_ROOT}/src/io/ply_compressed_writer.cpp
42
+ ${GAUSS_FORGE_ROOT}/src/io/ply_reader.cpp
43
+ ${GAUSS_FORGE_ROOT}/src/io/ply_writer.cpp
44
+ ${GAUSS_FORGE_ROOT}/src/io/splat_reader.cpp
45
+ ${GAUSS_FORGE_ROOT}/src/io/splat_writer.cpp
46
+ ${GAUSS_FORGE_ROOT}/src/io/ksplat_reader.cpp
47
+ ${GAUSS_FORGE_ROOT}/src/io/ksplat_writer.cpp
48
+ ${GAUSS_FORGE_ROOT}/src/io/spz_reader.cpp
49
+ ${GAUSS_FORGE_ROOT}/src/io/spz_writer.cpp
50
+ ${GAUSS_FORGE_ROOT}/src/io/sog_reader.cpp
51
+ ${GAUSS_FORGE_ROOT}/src/io/sog_writer.cpp
52
+ )
53
+
54
+ # Create static library for internal use
55
+ add_library(gauss_forge_py STATIC ${GAUSS_FORGE_SOURCES})
56
+ target_include_directories(gauss_forge_py PUBLIC
57
+ ${GAUSS_FORGE_ROOT}/include
58
+ ${CMAKE_CURRENT_BINARY_DIR}/include
59
+ )
60
+ target_link_libraries(gauss_forge_py PUBLIC
61
+ spz::spz
62
+ webp
63
+ nlohmann_json::nlohmann_json
64
+ ZLIB::ZLIB
65
+ )
66
+
67
+ # Create Python extension module
68
+ nanobind_add_module(
69
+ _core
70
+ STABLE_ABI
71
+ LTO
72
+ NB_STATIC
73
+ src/gaussforge/bindings.cpp
74
+ )
75
+ target_link_libraries(_core PRIVATE gauss_forge_py)
76
+ target_include_directories(_core PRIVATE
77
+ ${GAUSS_FORGE_ROOT}/include
78
+ ${CMAKE_CURRENT_BINARY_DIR}/include
79
+ )
80
+
81
+ # Set output directory for the extension module
82
+ set_target_properties(_core PROPERTIES
83
+ LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/gaussforge"
84
+ )
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: gaussforge
3
+ Version: 0.4.2
4
+ Summary: High-performance Gaussian Splatting format conversion library
5
+ Keywords: gaussian-splatting,3d,point-cloud,format-conversion,ply,splat
6
+ Author: tcodestudio
7
+ License-Expression: Apache-2.0
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Project-URL: Homepage, https://github.com/3dgscloud/GaussForge
21
+ Project-URL: Repository, https://github.com/3dgscloud/GaussForge.git
22
+ Project-URL: Issues, https://github.com/3dgscloud/GaussForge/issues
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ # GaussForge Python Binding
27
+
28
+ High-performance Gaussian Splatting format conversion library for Python.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install gaussforge
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ import gaussforge
40
+
41
+ # Create instance
42
+ gf = gaussforge.GaussForge()
43
+
44
+ # Read a PLY file
45
+ with open("model.ply", "rb") as f:
46
+ data = f.read()
47
+
48
+ result = gf.read(data, "ply")
49
+ if "error" not in result:
50
+ print(f"Loaded {result['data']['numPoints']} points")
51
+
52
+ # Convert to another format
53
+ converted = gf.convert(data, "ply", "splat")
54
+ if "error" not in converted:
55
+ with open("output.splat", "wb") as f:
56
+ f.write(converted["data"])
57
+ ```
58
+
59
+ ## Supported Formats
60
+
61
+ - `ply` - Standard PLY format
62
+ - `compressed.ply` - Compressed PLY format
63
+ - `splat` - Splat format
64
+ - `ksplat` - K-Splat format
65
+ - `spz` - SPZ compressed format
66
+ - `sog` - SOG format
67
+
68
+ ## API Reference
69
+
70
+ ### `GaussForge()`
71
+
72
+ Create a new GaussForge instance.
73
+
74
+ ### `read(data: bytes, format: str, strict: bool = False) -> dict`
75
+
76
+ Read Gaussian data from bytes.
77
+
78
+ - `data`: Raw file data as bytes
79
+ - `format`: Input format name
80
+ - `strict`: Enable strict validation (default: False)
81
+
82
+ Returns a dict with `data` key containing the parsed Gaussian data, or `error` key on failure.
83
+
84
+ ### `write(ir: dict, format: str, strict: bool = False) -> dict`
85
+
86
+ Write Gaussian IR to bytes.
87
+
88
+ - `ir`: Gaussian intermediate representation dict
89
+ - `format`: Output format name
90
+ - `strict`: Enable strict validation (default: False)
91
+
92
+ Returns a dict with `data` key containing the encoded bytes, or `error` key on failure.
93
+
94
+ ### `convert(data: bytes, in_format: str, out_format: str, strict: bool = False) -> dict`
95
+
96
+ Convert between formats directly.
97
+
98
+ - `data`: Input file data as bytes
99
+ - `in_format`: Input format name
100
+ - `out_format`: Output format name
101
+ - `strict`: Enable strict validation (default: False)
102
+
103
+ Returns a dict with `data` key containing the converted bytes, or `error` key on failure.
104
+
105
+ ### `get_model_info(data: bytes, format: str, file_size: int = 0) -> dict`
106
+
107
+ Get detailed model information.
108
+
109
+ - `data`: Raw file data as bytes
110
+ - `format`: Input format name
111
+ - `file_size`: Optional file size for reporting
112
+
113
+ Returns a dict with `data` key containing model info, or `error` key on failure.
114
+
115
+ ### `get_supported_formats() -> list[str]`
116
+
117
+ Get list of supported format names.
118
+
119
+ ### `get_version() -> str`
120
+
121
+ Get library version string.
122
+
123
+ ## Building from Source
124
+
125
+ ```bash
126
+ cd python
127
+ pip install -e .
128
+ ```
129
+
130
+ ## License
131
+
132
+ Apache-2.0
@@ -0,0 +1,107 @@
1
+ # GaussForge Python Binding
2
+
3
+ High-performance Gaussian Splatting format conversion library for Python.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install gaussforge
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import gaussforge
15
+
16
+ # Create instance
17
+ gf = gaussforge.GaussForge()
18
+
19
+ # Read a PLY file
20
+ with open("model.ply", "rb") as f:
21
+ data = f.read()
22
+
23
+ result = gf.read(data, "ply")
24
+ if "error" not in result:
25
+ print(f"Loaded {result['data']['numPoints']} points")
26
+
27
+ # Convert to another format
28
+ converted = gf.convert(data, "ply", "splat")
29
+ if "error" not in converted:
30
+ with open("output.splat", "wb") as f:
31
+ f.write(converted["data"])
32
+ ```
33
+
34
+ ## Supported Formats
35
+
36
+ - `ply` - Standard PLY format
37
+ - `compressed.ply` - Compressed PLY format
38
+ - `splat` - Splat format
39
+ - `ksplat` - K-Splat format
40
+ - `spz` - SPZ compressed format
41
+ - `sog` - SOG format
42
+
43
+ ## API Reference
44
+
45
+ ### `GaussForge()`
46
+
47
+ Create a new GaussForge instance.
48
+
49
+ ### `read(data: bytes, format: str, strict: bool = False) -> dict`
50
+
51
+ Read Gaussian data from bytes.
52
+
53
+ - `data`: Raw file data as bytes
54
+ - `format`: Input format name
55
+ - `strict`: Enable strict validation (default: False)
56
+
57
+ Returns a dict with `data` key containing the parsed Gaussian data, or `error` key on failure.
58
+
59
+ ### `write(ir: dict, format: str, strict: bool = False) -> dict`
60
+
61
+ Write Gaussian IR to bytes.
62
+
63
+ - `ir`: Gaussian intermediate representation dict
64
+ - `format`: Output format name
65
+ - `strict`: Enable strict validation (default: False)
66
+
67
+ Returns a dict with `data` key containing the encoded bytes, or `error` key on failure.
68
+
69
+ ### `convert(data: bytes, in_format: str, out_format: str, strict: bool = False) -> dict`
70
+
71
+ Convert between formats directly.
72
+
73
+ - `data`: Input file data as bytes
74
+ - `in_format`: Input format name
75
+ - `out_format`: Output format name
76
+ - `strict`: Enable strict validation (default: False)
77
+
78
+ Returns a dict with `data` key containing the converted bytes, or `error` key on failure.
79
+
80
+ ### `get_model_info(data: bytes, format: str, file_size: int = 0) -> dict`
81
+
82
+ Get detailed model information.
83
+
84
+ - `data`: Raw file data as bytes
85
+ - `format`: Input format name
86
+ - `file_size`: Optional file size for reporting
87
+
88
+ Returns a dict with `data` key containing model info, or `error` key on failure.
89
+
90
+ ### `get_supported_formats() -> list[str]`
91
+
92
+ Get list of supported format names.
93
+
94
+ ### `get_version() -> str`
95
+
96
+ Get library version string.
97
+
98
+ ## Building from Source
99
+
100
+ ```bash
101
+ cd python
102
+ pip install -e .
103
+ ```
104
+
105
+ ## License
106
+
107
+ Apache-2.0
@@ -0,0 +1,5 @@
1
+ output.*
2
+ __pycache__/
3
+ *.pyc
4
+ *.so
5
+ *.cpython-*.so
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Local test script - Test gaussforge package using tiny_gauss.ply
4
+
5
+ Usage:
6
+ 1. Build package: pip install -e ..
7
+ 2. Run test: python test.py
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ # Add parent directory to path for development testing
15
+ sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
16
+
17
+ from gaussforge import GaussForge, get_version
18
+
19
+
20
+ def main():
21
+ print("=" * 60)
22
+ print("Testing gaussforge Python package")
23
+ print("=" * 60)
24
+
25
+ test_file = Path(__file__).parent / "tiny_gauss.ply"
26
+
27
+ # Check if test file exists
28
+ if not test_file.exists():
29
+ print(f"Error: Test file does not exist: {test_file}")
30
+ sys.exit(1)
31
+
32
+ print(f"\nTest file: {test_file}")
33
+
34
+ try:
35
+ # 1. Initialize
36
+ print("\n1. Initializing GaussForge...")
37
+ gf = GaussForge()
38
+ print(" Initialization successful")
39
+
40
+ # 2. Print version
41
+ print("\n2. Getting version...")
42
+ version = gf.get_version()
43
+ print(f" Version: {version}")
44
+
45
+ # 3. Get supported formats
46
+ print("\n3. Getting supported formats...")
47
+ formats = gf.get_supported_formats()
48
+ print(f" Supported formats: {', '.join(formats)}")
49
+
50
+ # 4. Read test file
51
+ print("\n4. Reading test file...")
52
+ with open(test_file, "rb") as f:
53
+ input_data = f.read()
54
+ print(f" File size: {len(input_data)} bytes")
55
+
56
+ read_result = gf.read(input_data, "ply")
57
+ if "error" in read_result:
58
+ print(f" Read failed: {read_result['error']}")
59
+ sys.exit(1)
60
+
61
+ print(" Read successful")
62
+ data = read_result["data"]
63
+ print(f" Number of points: {data['numPoints']}")
64
+ print(f" SH degree: {data['meta']['shDegree']}")
65
+
66
+ # 5. Test model info
67
+ print("\n5. Testing model info...")
68
+ info_result = gf.get_model_info(input_data, "ply", len(input_data))
69
+ if "error" in info_result:
70
+ print(f" Model info failed: {info_result['error']}")
71
+ else:
72
+ info = info_result["data"]
73
+ print(" Model info retrieved")
74
+ print(" Basic info:")
75
+ print(f" - Points: {info['basic']['numPoints']}")
76
+ if "fileSize" in info["basic"]:
77
+ print(f" - File size: {info['basic']['fileSize']} bytes")
78
+ if "sourceFormat" in info["basic"]:
79
+ print(f" - Source format: {info['basic']['sourceFormat']}")
80
+
81
+ print(" Rendering:")
82
+ print(f" - SH degree: {info['rendering']['shDegree']}")
83
+ print(f" - Antialiased: {info['rendering']['antialiased']}")
84
+
85
+ if "bounds" in info:
86
+ print(" Bounds:")
87
+ print(f" - X: {info['bounds']['x']}")
88
+ print(f" - Y: {info['bounds']['y']}")
89
+ print(f" - Z: {info['bounds']['z']}")
90
+
91
+ if "scaleStats" in info:
92
+ print(" Scale stats:")
93
+ print(f" - Min: {info['scaleStats']['min']}")
94
+ print(f" - Max: {info['scaleStats']['max']}")
95
+ print(f" - Avg: {info['scaleStats']['avg']}")
96
+
97
+ if "alphaStats" in info:
98
+ print(" Alpha stats:")
99
+ print(f" - Min: {info['alphaStats']['min']}")
100
+ print(f" - Max: {info['alphaStats']['max']}")
101
+ print(f" - Avg: {info['alphaStats']['avg']}")
102
+
103
+ print(" Data sizes:")
104
+ for key, value in info["sizes"].items():
105
+ print(f" - {key}: {value}")
106
+
107
+ if "extraAttrs" in info:
108
+ print(" Extra attributes:")
109
+ for name, size in info["extraAttrs"].items():
110
+ print(f" - {name}: {size}")
111
+
112
+ # 6. Test format conversion
113
+ print("\n6. Testing format conversion...")
114
+ output_formats = ["splat", "ksplat", "spz", "ply", "compressed.ply", "sog"]
115
+
116
+ for out_format in output_formats:
117
+ if out_format not in formats:
118
+ print(f" Skipping {out_format} (not supported)")
119
+ continue
120
+
121
+ try:
122
+ convert_result = gf.convert(input_data, "ply", out_format)
123
+ if "error" in convert_result:
124
+ print(f" ply -> {out_format} failed: {convert_result['error']}")
125
+ else:
126
+ output_size = len(convert_result["data"])
127
+ print(f" ply -> {out_format}: {output_size} bytes")
128
+
129
+ # Save conversion result
130
+ output_file = Path(__file__).parent / f"output.{out_format}"
131
+ with open(output_file, "wb") as f:
132
+ f.write(convert_result["data"])
133
+ print(f" Saved to: {output_file}")
134
+ except Exception as e:
135
+ print(f" ply -> {out_format} failed: {e}")
136
+
137
+ print("\n" + "=" * 60)
138
+ print("All tests completed!")
139
+ print("=" * 60)
140
+
141
+ except Exception as e:
142
+ print(f"\nTest failed: {e}")
143
+ import traceback
144
+
145
+ traceback.print_exc()
146
+ sys.exit(1)
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
Binary file
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.10", "nanobind>=2.0.0"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "gaussforge"
7
+ version = "0.4.2"
8
+ description = "High-performance Gaussian Splatting format conversion library"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "tcodestudio" }]
13
+ keywords = [
14
+ "gaussian-splatting",
15
+ "3d",
16
+ "point-cloud",
17
+ "format-conversion",
18
+ "ply",
19
+ "splat",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "Intended Audience :: Science/Research",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Typing :: Typed",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/3dgscloud/GaussForge"
38
+ Repository = "https://github.com/3dgscloud/GaussForge.git"
39
+ Issues = "https://github.com/3dgscloud/GaussForge/issues"
40
+
41
+ [tool.scikit-build]
42
+ # Build configuration
43
+ minimum-version = "0.10"
44
+ build-dir = "build/{wheel_tag}"
45
+
46
+ # Include the C++ source from parent directory
47
+ wheel.packages = ["src/gaussforge"]
48
+ wheel.license-files = ["../LICENSE"]
49
+ sdist.include = ["../src", "../include", "../cmakes"]
50
+
51
+ # CMake configuration
52
+ cmake.version = ">=3.26"
53
+
54
+ [tool.scikit-build.cmake.define]
55
+ CMAKE_POSITION_INDEPENDENT_CODE = "ON"
@@ -0,0 +1,32 @@
1
+ """
2
+ GaussForge - High-performance Gaussian Splatting format conversion library.
3
+
4
+ This library provides efficient conversion between various Gaussian Splatting
5
+ formats including PLY, SPZ, SPLAT, KSPLAT, SOG, and compressed PLY.
6
+
7
+ Example:
8
+ import gaussforge
9
+
10
+ # Read a PLY file
11
+ with open("model.ply", "rb") as f:
12
+ data = f.read()
13
+
14
+ gf = gaussforge.GaussForge()
15
+ result = gf.read(data, "ply")
16
+
17
+ if "error" in result:
18
+ print(f"Error: {result['error']}")
19
+ else:
20
+ print(f"Loaded {result['data']['numPoints']} points")
21
+
22
+ # Convert to another format
23
+ converted = gf.convert(data, "ply", "splat")
24
+ if "error" not in converted:
25
+ with open("output.splat", "wb") as f:
26
+ f.write(converted["data"])
27
+ """
28
+
29
+ from gaussforge._core import GaussForge, get_version
30
+
31
+ __version__ = get_version()
32
+ __all__ = ["GaussForge", "get_version", "__version__"]
@@ -0,0 +1,356 @@
1
+ #include <nanobind/nanobind.h>
2
+ #include <nanobind/stl/string.h>
3
+ #include <nanobind/stl/vector.h>
4
+ #include <nanobind/stl/unordered_map.h>
5
+
6
+ #include <cstdint>
7
+ #include <memory>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ #include "gf/core/gauss_ir.h"
12
+ #include "gf/core/model_info.h"
13
+ #include "gf/core/validate.h"
14
+ #include "gf/core/version.h"
15
+ #include "gf/io/registry.h"
16
+
17
+ namespace nb = nanobind;
18
+
19
+ namespace {
20
+
21
+ /**
22
+ * Convert GaussianCloudIR to Python dict
23
+ * Returns bytes for float arrays (zero-copy friendly)
24
+ */
25
+ nb::dict gaussIRToPy(const gf::GaussianCloudIR &ir) {
26
+ nb::dict result;
27
+
28
+ result["numPoints"] = ir.numPoints;
29
+
30
+ // Convert float vectors to bytes for efficient transfer
31
+ auto floatVecToBytes = [](const std::vector<float> &vec) -> nb::bytes {
32
+ if (vec.empty())
33
+ return nb::bytes();
34
+ return nb::bytes(reinterpret_cast<const char *>(vec.data()),
35
+ vec.size() * sizeof(float));
36
+ };
37
+
38
+ result["positions"] = floatVecToBytes(ir.positions);
39
+ result["scales"] = floatVecToBytes(ir.scales);
40
+ result["rotations"] = floatVecToBytes(ir.rotations);
41
+ result["alphas"] = floatVecToBytes(ir.alphas);
42
+ result["colors"] = floatVecToBytes(ir.colors);
43
+ result["sh"] = floatVecToBytes(ir.sh);
44
+
45
+ // Handle extras
46
+ nb::dict extras;
47
+ for (const auto &pair : ir.extras) {
48
+ extras[pair.first.c_str()] = floatVecToBytes(pair.second);
49
+ }
50
+ result["extras"] = extras;
51
+
52
+ // Handle metadata
53
+ nb::dict meta;
54
+ meta["shDegree"] = ir.meta.shDegree;
55
+ meta["sourceFormat"] = ir.meta.sourceFormat;
56
+ result["meta"] = meta;
57
+
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * Convert Python dict to GaussianCloudIR
63
+ */
64
+ gf::GaussianCloudIR pyToGaussIR(nb::dict pyIR) {
65
+ gf::GaussianCloudIR ir;
66
+ ir.numPoints = nb::cast<int32_t>(pyIR["numPoints"]);
67
+
68
+ auto fill = [&](const char *key, std::vector<float> &dest) {
69
+ if (pyIR.contains(key)) {
70
+ nb::bytes data = nb::cast<nb::bytes>(pyIR[key]);
71
+ if (data.size() > 0) {
72
+ const float *ptr = reinterpret_cast<const float *>(data.c_str());
73
+ size_t count = data.size() / sizeof(float);
74
+ dest.assign(ptr, ptr + count);
75
+ }
76
+ }
77
+ };
78
+
79
+ fill("positions", ir.positions);
80
+ fill("scales", ir.scales);
81
+ fill("rotations", ir.rotations);
82
+ fill("alphas", ir.alphas);
83
+ fill("colors", ir.colors);
84
+ fill("sh", ir.sh);
85
+
86
+ if (pyIR.contains("extras")) {
87
+ nb::dict ex = nb::cast<nb::dict>(pyIR["extras"]);
88
+ for (auto item : ex) {
89
+ std::string key = nb::cast<std::string>(item.first);
90
+ nb::bytes data = nb::cast<nb::bytes>(item.second);
91
+ if (data.size() > 0) {
92
+ const float *ptr = reinterpret_cast<const float *>(data.c_str());
93
+ size_t count = data.size() / sizeof(float);
94
+ ir.extras[key].assign(ptr, ptr + count);
95
+ }
96
+ }
97
+ }
98
+
99
+ if (pyIR.contains("meta")) {
100
+ nb::dict m = nb::cast<nb::dict>(pyIR["meta"]);
101
+ if (m.contains("shDegree")) {
102
+ ir.meta.shDegree = nb::cast<int32_t>(m["shDegree"]);
103
+ }
104
+ if (m.contains("sourceFormat")) {
105
+ ir.meta.sourceFormat = nb::cast<std::string>(m["sourceFormat"]);
106
+ }
107
+ }
108
+
109
+ return ir;
110
+ }
111
+
112
+ /**
113
+ * Convert ModelInfo to Python dict
114
+ */
115
+ nb::dict modelInfoToPy(const gf::ModelInfo &info) {
116
+ nb::dict result;
117
+
118
+ // Basic info
119
+ nb::dict basic;
120
+ basic["numPoints"] = info.numPoints;
121
+ if (info.fileSize > 0)
122
+ basic["fileSize"] = static_cast<double>(info.fileSize);
123
+ if (!info.sourceFormat.empty())
124
+ basic["sourceFormat"] = info.sourceFormat;
125
+ result["basic"] = basic;
126
+
127
+ // Rendering properties
128
+ nb::dict rendering;
129
+ rendering["shDegree"] = info.shDegree;
130
+ rendering["antialiased"] = info.antialiased;
131
+ result["rendering"] = rendering;
132
+
133
+ // Metadata
134
+ nb::dict meta;
135
+ meta["handedness"] = gf::HandednessToString(info.handedness);
136
+ meta["upAxis"] = gf::UpAxisToString(info.upAxis);
137
+ meta["unit"] = gf::LengthUnitToString(info.unit);
138
+ meta["colorSpace"] = gf::ColorSpaceToString(info.colorSpace);
139
+ result["meta"] = meta;
140
+
141
+ // Geometry statistics
142
+ if (info.numPoints > 0) {
143
+ nb::dict bounds;
144
+ bounds["x"] = nb::make_tuple(info.bounds.minX, info.bounds.maxX);
145
+ bounds["y"] = nb::make_tuple(info.bounds.minY, info.bounds.maxY);
146
+ bounds["z"] = nb::make_tuple(info.bounds.minZ, info.bounds.maxZ);
147
+ result["bounds"] = bounds;
148
+ }
149
+
150
+ // Scale statistics
151
+ if (info.scaleStats.count > 0) {
152
+ nb::dict scaleStats;
153
+ scaleStats["min"] = info.scaleStats.min;
154
+ scaleStats["max"] = info.scaleStats.max;
155
+ scaleStats["avg"] = info.scaleStats.avg;
156
+ result["scaleStats"] = scaleStats;
157
+ }
158
+
159
+ // Alpha statistics
160
+ if (info.alphaStats.count > 0) {
161
+ nb::dict alphaStats;
162
+ alphaStats["min"] = info.alphaStats.min;
163
+ alphaStats["max"] = info.alphaStats.max;
164
+ alphaStats["avg"] = info.alphaStats.avg;
165
+ result["alphaStats"] = alphaStats;
166
+ }
167
+
168
+ // Data size breakdown
169
+ nb::dict sizes;
170
+ sizes["positions"] = gf::FormatBytes(info.positionsSize);
171
+ sizes["scales"] = gf::FormatBytes(info.scalesSize);
172
+ sizes["rotations"] = gf::FormatBytes(info.rotationsSize);
173
+ sizes["alphas"] = gf::FormatBytes(info.alphasSize);
174
+ sizes["colors"] = gf::FormatBytes(info.colorsSize);
175
+ sizes["sh"] = gf::FormatBytes(info.shSize);
176
+ sizes["total"] = gf::FormatBytes(info.totalSize);
177
+ result["sizes"] = sizes;
178
+
179
+ // Extra attributes
180
+ if (!info.extraAttrs.empty()) {
181
+ nb::dict extras;
182
+ for (const auto &[name, size] : info.extraAttrs) {
183
+ extras[name.c_str()] = gf::FormatBytes(size);
184
+ }
185
+ result["extraAttrs"] = extras;
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ } // namespace
192
+
193
+ /**
194
+ * GaussForge Python binding class
195
+ */
196
+ class GaussForgePy {
197
+ public:
198
+ GaussForgePy() : registry_(std::make_unique<gf::IORegistry>()) {}
199
+
200
+ nb::dict read(nb::bytes pyData, const std::string &format,
201
+ bool strict = false) {
202
+ try {
203
+ const uint8_t *data =
204
+ reinterpret_cast<const uint8_t *>(pyData.c_str());
205
+ size_t size = pyData.size();
206
+
207
+ auto *reader = registry_->ReaderForExt(format);
208
+ if (!reader)
209
+ return err("No reader for " + format);
210
+
211
+ auto ir_or = reader->Read(data, size, {strict});
212
+ if (!ir_or)
213
+ return err(ir_or.error().message);
214
+
215
+ auto validation = gf::ValidateBasic(ir_or.value(), strict);
216
+ if (!validation.message.empty() && strict)
217
+ return err(validation.message);
218
+
219
+ nb::dict res;
220
+ res["data"] = gaussIRToPy(ir_or.value());
221
+ if (!validation.message.empty())
222
+ res["warning"] = validation.message;
223
+ return res;
224
+ } catch (const std::exception &e) {
225
+ return err(e.what());
226
+ }
227
+ }
228
+
229
+ nb::dict write(nb::dict pyIR, const std::string &format,
230
+ bool strict = false) {
231
+ try {
232
+ auto *writer = registry_->WriterForExt(format);
233
+ if (!writer)
234
+ return err("No writer for " + format);
235
+
236
+ auto data_or = writer->Write(pyToGaussIR(pyIR), {strict});
237
+ if (!data_or)
238
+ return err(data_or.error().message);
239
+
240
+ nb::dict res;
241
+ res["data"] = nb::bytes(
242
+ reinterpret_cast<const char *>(data_or.value().data()),
243
+ data_or.value().size());
244
+ return res;
245
+ } catch (const std::exception &e) {
246
+ return err(e.what());
247
+ }
248
+ }
249
+
250
+ nb::dict convert(nb::bytes pyData, const std::string &inFormat,
251
+ const std::string &outFormat, bool strict = false) {
252
+ try {
253
+ auto *reader = registry_->ReaderForExt(inFormat);
254
+ auto *writer = registry_->WriterForExt(outFormat);
255
+ if (!reader || !writer)
256
+ return err("Format handler not found");
257
+
258
+ const uint8_t *data =
259
+ reinterpret_cast<const uint8_t *>(pyData.c_str());
260
+ size_t size = pyData.size();
261
+
262
+ auto ir_or = reader->Read(data, size, {strict});
263
+ if (!ir_or)
264
+ return err(ir_or.error().message);
265
+
266
+ auto out_or = writer->Write(ir_or.value(), {strict});
267
+ if (!out_or)
268
+ return err(out_or.error().message);
269
+
270
+ nb::dict res;
271
+ res["data"] = nb::bytes(
272
+ reinterpret_cast<const char *>(out_or.value().data()),
273
+ out_or.value().size());
274
+ return res;
275
+ } catch (const std::exception &e) {
276
+ return err(e.what());
277
+ }
278
+ }
279
+
280
+ nb::list getSupportedFormats() {
281
+ nb::list formats;
282
+ for (auto &s : {"ply", "compressed.ply", "splat", "ksplat", "spz", "sog"})
283
+ formats.append(s);
284
+ return formats;
285
+ }
286
+
287
+ nb::dict getModelInfo(nb::bytes pyData, const std::string &format,
288
+ size_t fileSize = 0) {
289
+ try {
290
+ const uint8_t *data =
291
+ reinterpret_cast<const uint8_t *>(pyData.c_str());
292
+ size_t size = pyData.size();
293
+
294
+ auto *reader = registry_->ReaderForExt(format);
295
+ if (!reader)
296
+ return err("No reader for " + format);
297
+
298
+ auto ir_or = reader->Read(data, size, {/*strict=*/false});
299
+ if (!ir_or)
300
+ return err(ir_or.error().message);
301
+
302
+ gf::ModelInfo info = gf::GetModelInfo(ir_or.value(), fileSize);
303
+
304
+ nb::dict res;
305
+ res["data"] = modelInfoToPy(info);
306
+ return res;
307
+ } catch (const std::exception &e) {
308
+ return err(e.what());
309
+ }
310
+ }
311
+
312
+ std::string getVersion() { return GAUSS_FORGE_VERSION_STRING; }
313
+
314
+ private:
315
+ std::unique_ptr<gf::IORegistry> registry_;
316
+
317
+ nb::dict err(const std::string &m) {
318
+ nb::dict e;
319
+ e["error"] = m;
320
+ return e;
321
+ }
322
+ };
323
+
324
+ NB_MODULE(_core, m) {
325
+ m.doc() = "GaussForge - High-performance Gaussian Splatting format "
326
+ "conversion library";
327
+
328
+ nb::class_<GaussForgePy>(
329
+ m, "GaussForge",
330
+ "Main class for Gaussian Splatting format conversion")
331
+ .def(nb::init<>(), "Create a new GaussForge instance")
332
+ .def("read", &GaussForgePy::read, nb::arg("data"), nb::arg("format"),
333
+ nb::arg("strict") = false,
334
+ "Read Gaussian data from bytes. Returns dict with 'data' or "
335
+ "'error'.")
336
+ .def("write", &GaussForgePy::write, nb::arg("ir"), nb::arg("format"),
337
+ nb::arg("strict") = false,
338
+ "Write Gaussian IR to bytes. Returns dict with 'data' or 'error'.")
339
+ .def("convert", &GaussForgePy::convert, nb::arg("data"),
340
+ nb::arg("in_format"), nb::arg("out_format"),
341
+ nb::arg("strict") = false,
342
+ "Convert between formats. Returns dict with 'data' or 'error'.")
343
+ .def("get_supported_formats", &GaussForgePy::getSupportedFormats,
344
+ "Get list of supported format names.")
345
+ .def("get_model_info", &GaussForgePy::getModelInfo, nb::arg("data"),
346
+ nb::arg("format"), nb::arg("file_size") = 0,
347
+ "Get detailed model information. Returns dict with 'data' or "
348
+ "'error'.")
349
+ .def("get_version", &GaussForgePy::getVersion,
350
+ "Get library version string.");
351
+
352
+ // Module-level convenience function
353
+ m.def(
354
+ "get_version", []() { return std::string(GAUSS_FORGE_VERSION_STRING); },
355
+ "Get library version string.");
356
+ }