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.
- gaussforge-0.4.2/.gitignore +19 -0
- gaussforge-0.4.2/CMakeLists.txt +84 -0
- gaussforge-0.4.2/PKG-INFO +132 -0
- gaussforge-0.4.2/README.md +107 -0
- gaussforge-0.4.2/example/.gitignore +5 -0
- gaussforge-0.4.2/example/test.py +150 -0
- gaussforge-0.4.2/example/tiny_gauss.ply +0 -0
- gaussforge-0.4.2/pyproject.toml +55 -0
- gaussforge-0.4.2/src/gaussforge/__init__.py +32 -0
- gaussforge-0.4.2/src/gaussforge/bindings.cpp +356 -0
|
@@ -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,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
|
+
}
|