gsvvcompressor 1.2.0__cp310-cp310-win_amd64.whl
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.
- gsvvcompressor/__init__.py +13 -0
- gsvvcompressor/__main__.py +243 -0
- gsvvcompressor/combinations/__init__.py +84 -0
- gsvvcompressor/combinations/registry.py +52 -0
- gsvvcompressor/combinations/vq_xyz_1mask.py +89 -0
- gsvvcompressor/combinations/vq_xyz_1mask_zstd.py +103 -0
- gsvvcompressor/combinations/vq_xyz_draco.py +468 -0
- gsvvcompressor/combinations/vq_xyz_draco_2pass.py +156 -0
- gsvvcompressor/combinations/vq_xyz_zstd.py +106 -0
- gsvvcompressor/compress/__init__.py +5 -0
- gsvvcompressor/compress/zstd.py +144 -0
- gsvvcompressor/decoder.py +155 -0
- gsvvcompressor/deserializer.py +42 -0
- gsvvcompressor/draco/__init__.py +34 -0
- gsvvcompressor/draco/draco_decoder.exe +0 -0
- gsvvcompressor/draco/draco_encoder.exe +0 -0
- gsvvcompressor/draco/dracoreduced3dgs.cp310-win_amd64.pyd +0 -0
- gsvvcompressor/draco/interface.py +339 -0
- gsvvcompressor/draco/serialize.py +235 -0
- gsvvcompressor/draco/twopass.py +359 -0
- gsvvcompressor/encoder.py +122 -0
- gsvvcompressor/interframe/__init__.py +11 -0
- gsvvcompressor/interframe/combine.py +271 -0
- gsvvcompressor/interframe/decoder.py +99 -0
- gsvvcompressor/interframe/encoder.py +92 -0
- gsvvcompressor/interframe/interface.py +221 -0
- gsvvcompressor/interframe/twopass.py +226 -0
- gsvvcompressor/io/__init__.py +31 -0
- gsvvcompressor/io/bytes.py +103 -0
- gsvvcompressor/io/config.py +78 -0
- gsvvcompressor/io/gaussian_model.py +127 -0
- gsvvcompressor/movecameras.py +33 -0
- gsvvcompressor/payload.py +34 -0
- gsvvcompressor/serializer.py +42 -0
- gsvvcompressor/vq/__init__.py +15 -0
- gsvvcompressor/vq/interface.py +324 -0
- gsvvcompressor/vq/singlemask.py +127 -0
- gsvvcompressor/vq/twopass.py +1 -0
- gsvvcompressor/xyz/__init__.py +26 -0
- gsvvcompressor/xyz/dense.py +39 -0
- gsvvcompressor/xyz/interface.py +382 -0
- gsvvcompressor/xyz/knn.py +141 -0
- gsvvcompressor/xyz/quant.py +143 -0
- gsvvcompressor/xyz/size.py +44 -0
- gsvvcompressor/xyz/twopass.py +1 -0
- gsvvcompressor-1.2.0.dist-info/METADATA +690 -0
- gsvvcompressor-1.2.0.dist-info/RECORD +50 -0
- gsvvcompressor-1.2.0.dist-info/WHEEL +5 -0
- gsvvcompressor-1.2.0.dist-info/licenses/LICENSE +21 -0
- gsvvcompressor-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Draco-capable interframe codec interface.
|
|
3
|
+
|
|
4
|
+
This module provides interfaces for interframe codecs that can output
|
|
5
|
+
Draco-compatible payloads for efficient compression.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Optional, Self
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from gaussian_splatting import GaussianModel
|
|
15
|
+
|
|
16
|
+
from ..payload import Payload
|
|
17
|
+
from ..interframe.interface import (
|
|
18
|
+
InterframeCodecInterface,
|
|
19
|
+
InterframeCodecContext,
|
|
20
|
+
InterframeEncoderInitConfig,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class DracoPayload(Payload):
|
|
26
|
+
"""
|
|
27
|
+
Payload containing data in Draco-compatible format.
|
|
28
|
+
|
|
29
|
+
This payload structure matches the format expected by Draco compression
|
|
30
|
+
for reduced 3DGS data. All arrays should be numpy arrays.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
positions: Point positions, shape (N, 3), dtype float32/float64
|
|
34
|
+
scales: Scale indices or values, shape (N, 1), dtype int32 for VQ indices
|
|
35
|
+
rotations: Rotation indices or values, shape (N, 2), dtype int32 for VQ indices
|
|
36
|
+
(rotation_re, rotation_im)
|
|
37
|
+
opacities: Opacity indices or values, shape (N, 1), dtype int32 for VQ indices
|
|
38
|
+
features_dc: DC feature indices or values, shape (N, 1), dtype int32 for VQ indices
|
|
39
|
+
features_rest: Rest feature indices or values, shape (N, 9),
|
|
40
|
+
dtype int32 for VQ indices (3 sh_degrees * 3)
|
|
41
|
+
extra: Optional additional payload for codec-specific data not covered by Draco format
|
|
42
|
+
"""
|
|
43
|
+
positions: np.ndarray # (N, 3) float
|
|
44
|
+
scales: np.ndarray # (N, 1) int32
|
|
45
|
+
rotations: np.ndarray # (N, 2) int32
|
|
46
|
+
opacities: np.ndarray # (N, 1) int32
|
|
47
|
+
features_dc: np.ndarray # (N, 1) int32
|
|
48
|
+
features_rest: np.ndarray # (N, 9) int32
|
|
49
|
+
extra: Optional[Payload] = None
|
|
50
|
+
|
|
51
|
+
def to(self, device) -> Self:
|
|
52
|
+
"""
|
|
53
|
+
Move the Payload to the specified device.
|
|
54
|
+
|
|
55
|
+
Since DracoPayload uses numpy arrays (CPU-only), only the extra
|
|
56
|
+
payload (if present) is moved to the target device.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
device: The target device (e.g., 'cpu', 'cuda', torch.device).
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A new DracoPayload with the extra payload moved to the target device.
|
|
63
|
+
"""
|
|
64
|
+
return DracoPayload(
|
|
65
|
+
positions=self.positions,
|
|
66
|
+
scales=self.scales,
|
|
67
|
+
rotations=self.rotations,
|
|
68
|
+
opacities=self.opacities,
|
|
69
|
+
features_dc=self.features_dc,
|
|
70
|
+
features_rest=self.features_rest,
|
|
71
|
+
extra=self.extra.to(device) if self.extra is not None else None,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class DracoInterframeCodecTranscodingInterface(ABC):
|
|
76
|
+
"""
|
|
77
|
+
Abstract interface for transcoding between internal payloads and DracoPayload.
|
|
78
|
+
|
|
79
|
+
This interface defines the conversion methods needed to transform codec-specific
|
|
80
|
+
payloads to/from DracoPayload format. Implementations of this interface handle
|
|
81
|
+
the data format conversion without implementing the actual encoding/decoding logic.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def interframe_payload_to_draco(self, payload: Payload) -> DracoPayload:
|
|
86
|
+
"""
|
|
87
|
+
Convert an interframe payload to a DracoPayload.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
payload: The internal payload from encode_interframe.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A DracoPayload containing the data in Draco-compatible format.
|
|
94
|
+
"""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def keyframe_payload_to_draco(self, payload: Payload) -> DracoPayload:
|
|
99
|
+
"""
|
|
100
|
+
Convert a keyframe payload to a DracoPayload.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
payload: The internal payload from encode_keyframe.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A DracoPayload containing the data in Draco-compatible format.
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def draco_to_interframe_payload(self, draco_payload: DracoPayload) -> Payload:
|
|
112
|
+
"""
|
|
113
|
+
Convert a DracoPayload to an interframe payload.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
draco_payload: The DracoPayload to convert.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
An internal payload suitable for decode_interframe.
|
|
120
|
+
"""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def draco_to_keyframe_payload(self, draco_payload: DracoPayload) -> Payload:
|
|
125
|
+
"""
|
|
126
|
+
Convert a DracoPayload to a keyframe payload.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
draco_payload: The DracoPayload to convert.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
An internal payload suitable for decode_keyframe.
|
|
133
|
+
"""
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class DracoInterframeCodecInterface(InterframeCodecInterface):
|
|
138
|
+
"""
|
|
139
|
+
Interframe codec that wraps another codec and transcodes to/from DracoPayload.
|
|
140
|
+
|
|
141
|
+
This class uses composition to combine an InterframeCodecInterface (for the
|
|
142
|
+
actual encoding/decoding logic) with a DracoInterframeCodecTranscodingInterface
|
|
143
|
+
(for payload format conversion).
|
|
144
|
+
|
|
145
|
+
The workflow is:
|
|
146
|
+
Encoding:
|
|
147
|
+
1. Call inner codec's encode method to get internal payload
|
|
148
|
+
2. Convert internal payload to DracoPayload via transcoding interface
|
|
149
|
+
|
|
150
|
+
Decoding:
|
|
151
|
+
1. Convert DracoPayload to internal payload via transcoding interface
|
|
152
|
+
2. Call inner codec's decode method with internal payload
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
codec: InterframeCodecInterface,
|
|
158
|
+
transcoder: DracoInterframeCodecTranscodingInterface,
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Initialize the DracoInterframeCodecInterface.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
codec: The inner codec that performs actual encoding/decoding.
|
|
165
|
+
transcoder: The transcoding interface for payload conversion.
|
|
166
|
+
"""
|
|
167
|
+
self._codec = codec
|
|
168
|
+
self._transcoder = transcoder
|
|
169
|
+
|
|
170
|
+
# =========================================================================
|
|
171
|
+
# Encode/decode methods - transcode then delegate
|
|
172
|
+
# =========================================================================
|
|
173
|
+
|
|
174
|
+
def decode_interframe(
|
|
175
|
+
self, payload: DracoPayload, prev_context: InterframeCodecContext
|
|
176
|
+
) -> InterframeCodecContext:
|
|
177
|
+
"""
|
|
178
|
+
Decode a DracoPayload to reconstruct the next frame's context.
|
|
179
|
+
|
|
180
|
+
This method first converts the DracoPayload to the internal format,
|
|
181
|
+
then delegates to the inner codec's decode_interframe.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
payload: The DracoPayload containing the delta data.
|
|
185
|
+
prev_context: The context of the previous frame.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The reconstructed context for the current frame.
|
|
189
|
+
"""
|
|
190
|
+
internal_payload = self._transcoder.draco_to_interframe_payload(payload)
|
|
191
|
+
return self._codec.decode_interframe(internal_payload, prev_context)
|
|
192
|
+
|
|
193
|
+
def encode_interframe(
|
|
194
|
+
self,
|
|
195
|
+
prev_context: InterframeCodecContext,
|
|
196
|
+
next_context: InterframeCodecContext,
|
|
197
|
+
) -> DracoPayload:
|
|
198
|
+
"""
|
|
199
|
+
Encode the difference between two consecutive frames as a DracoPayload.
|
|
200
|
+
|
|
201
|
+
This method delegates to the inner codec's encode_interframe,
|
|
202
|
+
then converts the result to a DracoPayload.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
prev_context: The context of the previous frame.
|
|
206
|
+
next_context: The context of the next frame.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
A DracoPayload containing the delta information.
|
|
210
|
+
"""
|
|
211
|
+
internal_payload = self._codec.encode_interframe(prev_context, next_context)
|
|
212
|
+
return self._transcoder.interframe_payload_to_draco(internal_payload)
|
|
213
|
+
|
|
214
|
+
def decode_keyframe(self, payload: DracoPayload) -> InterframeCodecContext:
|
|
215
|
+
"""
|
|
216
|
+
Decode a DracoPayload keyframe to create initial context.
|
|
217
|
+
|
|
218
|
+
This method first converts the DracoPayload to the internal format,
|
|
219
|
+
then delegates to the inner codec's decode_keyframe.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
payload: The DracoPayload containing full keyframe data.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
The context for the first/key frame.
|
|
226
|
+
"""
|
|
227
|
+
internal_payload = self._transcoder.draco_to_keyframe_payload(payload)
|
|
228
|
+
return self._codec.decode_keyframe(internal_payload)
|
|
229
|
+
|
|
230
|
+
def decode_keyframe_for_encode(
|
|
231
|
+
self, payload: DracoPayload, context: InterframeCodecContext
|
|
232
|
+
) -> InterframeCodecContext:
|
|
233
|
+
"""
|
|
234
|
+
Decode a DracoPayload keyframe during encoding to avoid error accumulation.
|
|
235
|
+
|
|
236
|
+
This method first converts the DracoPayload to the internal format,
|
|
237
|
+
then delegates to the inner codec's decode_keyframe_for_encode.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
payload: The DracoPayload that was just encoded.
|
|
241
|
+
context: The original context used for encoding this keyframe.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The reconstructed context as the decoder would produce it.
|
|
245
|
+
"""
|
|
246
|
+
internal_payload = self._transcoder.draco_to_keyframe_payload(payload)
|
|
247
|
+
return self._codec.decode_keyframe_for_encode(internal_payload, context)
|
|
248
|
+
|
|
249
|
+
def decode_interframe_for_encode(
|
|
250
|
+
self, payload: DracoPayload, prev_context: InterframeCodecContext
|
|
251
|
+
) -> InterframeCodecContext:
|
|
252
|
+
"""
|
|
253
|
+
Decode a DracoPayload interframe during encoding to avoid error accumulation.
|
|
254
|
+
|
|
255
|
+
This method first converts the DracoPayload to the internal format,
|
|
256
|
+
then delegates to the inner codec's decode_interframe_for_encode.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
payload: The DracoPayload that was just encoded.
|
|
260
|
+
prev_context: The previous frame's context (reconstructed version).
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The reconstructed context as the decoder would produce it.
|
|
264
|
+
"""
|
|
265
|
+
internal_payload = self._transcoder.draco_to_interframe_payload(payload)
|
|
266
|
+
return self._codec.decode_interframe_for_encode(internal_payload, prev_context)
|
|
267
|
+
|
|
268
|
+
def encode_keyframe(self, context: InterframeCodecContext) -> DracoPayload:
|
|
269
|
+
"""
|
|
270
|
+
Encode the first frame as a DracoPayload keyframe.
|
|
271
|
+
|
|
272
|
+
This method delegates to the inner codec's encode_keyframe,
|
|
273
|
+
then converts the result to a DracoPayload.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
context: The context of the first frame.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
A DracoPayload containing the full keyframe data.
|
|
280
|
+
"""
|
|
281
|
+
internal_payload = self._codec.encode_keyframe(context)
|
|
282
|
+
return self._transcoder.keyframe_payload_to_draco(internal_payload)
|
|
283
|
+
|
|
284
|
+
# =========================================================================
|
|
285
|
+
# Other methods - delegate directly to inner codec
|
|
286
|
+
# =========================================================================
|
|
287
|
+
|
|
288
|
+
def keyframe_to_context(
|
|
289
|
+
self, frame: GaussianModel, init_config: InterframeEncoderInitConfig
|
|
290
|
+
) -> InterframeCodecContext:
|
|
291
|
+
"""
|
|
292
|
+
Convert a keyframe to a Context.
|
|
293
|
+
|
|
294
|
+
Delegates directly to the inner codec.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
frame: The GaussianModel frame to convert.
|
|
298
|
+
init_config: Encoder initialization configuration.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
The corresponding Context representation.
|
|
302
|
+
"""
|
|
303
|
+
return self._codec.keyframe_to_context(frame, init_config)
|
|
304
|
+
|
|
305
|
+
def interframe_to_context(
|
|
306
|
+
self,
|
|
307
|
+
frame: GaussianModel,
|
|
308
|
+
prev_context: InterframeCodecContext,
|
|
309
|
+
) -> InterframeCodecContext:
|
|
310
|
+
"""
|
|
311
|
+
Convert a frame to a Context using the previous context as reference.
|
|
312
|
+
|
|
313
|
+
Delegates directly to the inner codec.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
frame: The GaussianModel frame to convert.
|
|
317
|
+
prev_context: The context from the previous frame.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
The corresponding Context representation.
|
|
321
|
+
"""
|
|
322
|
+
return self._codec.interframe_to_context(frame, prev_context)
|
|
323
|
+
|
|
324
|
+
def context_to_frame(
|
|
325
|
+
self, context: InterframeCodecContext, frame: GaussianModel
|
|
326
|
+
) -> GaussianModel:
|
|
327
|
+
"""
|
|
328
|
+
Convert a Context back to a frame.
|
|
329
|
+
|
|
330
|
+
Delegates directly to the inner codec.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
context: The Context to convert.
|
|
334
|
+
frame: An empty GaussianModel or one from previous pipeline steps.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The modified GaussianModel with the frame data.
|
|
338
|
+
"""
|
|
339
|
+
return self._codec.context_to_frame(context, frame)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Draco-based streaming serialization and deserialization.
|
|
3
|
+
|
|
4
|
+
Uses dracoreduced3dgs encoding + cloudpickle for extra data + length-prefix
|
|
5
|
+
framing + zstd compression for efficient streaming of DracoPayload objects.
|
|
6
|
+
|
|
7
|
+
WARNING: cloudpickle uses pickle under the hood. Do NOT use with untrusted
|
|
8
|
+
data sources. Only use with trusted data (local files, same-process, etc.).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import struct
|
|
12
|
+
from typing import Iterator
|
|
13
|
+
|
|
14
|
+
import cloudpickle as pickle
|
|
15
|
+
import zstandard as zstd
|
|
16
|
+
|
|
17
|
+
from ..deserializer import AbstractDeserializer
|
|
18
|
+
from ..serializer import AbstractSerializer
|
|
19
|
+
from .interface import DracoPayload
|
|
20
|
+
|
|
21
|
+
from . import dracoreduced3dgs
|
|
22
|
+
|
|
23
|
+
# 4-byte big-endian length prefix (max 4GB per object)
|
|
24
|
+
_LEN = struct.Struct(">I")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DracoSerializer(AbstractSerializer):
|
|
28
|
+
"""
|
|
29
|
+
Streaming serializer using Draco encoding + cloudpickle + length-prefix framing + zstd.
|
|
30
|
+
|
|
31
|
+
Each DracoPayload is encoded as follows:
|
|
32
|
+
1. The main point cloud data is encoded using dracoreduced3dgs.encode
|
|
33
|
+
2. The extra field (if present) is pickled with cloudpickle
|
|
34
|
+
3. Both are combined with length-prefix framing:
|
|
35
|
+
[draco_len][draco_bytes][extra_len][extra_bytes]
|
|
36
|
+
(extra_len is 0 if no extra data)
|
|
37
|
+
4. The combined data is compressed incrementally using zstd streaming compression.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
zstd_level: int = 7,
|
|
43
|
+
draco_level: int = 0,
|
|
44
|
+
qp: int = 30,
|
|
45
|
+
qscale: int = 30,
|
|
46
|
+
qrotation: int = 30,
|
|
47
|
+
qopacity: int = 30,
|
|
48
|
+
qfeaturedc: int = 30,
|
|
49
|
+
qfeaturerest: int = 30,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Initialize the serializer.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
zstd_level: Zstd compression level (1-22). Default is 7.
|
|
56
|
+
draco_level: Draco compression level (0-10). Default is 0.
|
|
57
|
+
qp: Quantization bits for positions (0 to disable).
|
|
58
|
+
qscale: Quantization bits for scales (0 to disable).
|
|
59
|
+
qrotation: Quantization bits for rotations (0 to disable).
|
|
60
|
+
qopacity: Quantization bits for opacities (0 to disable).
|
|
61
|
+
qfeaturedc: Quantization bits for features_dc (0 to disable).
|
|
62
|
+
qfeaturerest: Quantization bits for features_rest (0 to disable).
|
|
63
|
+
"""
|
|
64
|
+
self._compressor = zstd.ZstdCompressor(level=zstd_level).compressobj()
|
|
65
|
+
self._draco_level = draco_level
|
|
66
|
+
self._qp = qp
|
|
67
|
+
self._qscale = qscale
|
|
68
|
+
self._qrotation = qrotation
|
|
69
|
+
self._qopacity = qopacity
|
|
70
|
+
self._qfeaturedc = qfeaturedc
|
|
71
|
+
self._qfeaturerest = qfeaturerest
|
|
72
|
+
|
|
73
|
+
def serialize_frame(self, payload: DracoPayload) -> Iterator[bytes]:
|
|
74
|
+
"""
|
|
75
|
+
Serialize and compress a DracoPayload object.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
payload: A DracoPayload instance to serialize.
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
Compressed byte chunks (may yield zero chunks if buffered).
|
|
82
|
+
"""
|
|
83
|
+
# Encode point cloud data using Draco
|
|
84
|
+
draco_encoded = dracoreduced3dgs.encode(
|
|
85
|
+
payload.positions,
|
|
86
|
+
payload.scales,
|
|
87
|
+
payload.rotations,
|
|
88
|
+
payload.opacities,
|
|
89
|
+
payload.features_dc,
|
|
90
|
+
payload.features_rest,
|
|
91
|
+
self._draco_level,
|
|
92
|
+
self._qp,
|
|
93
|
+
self._qscale,
|
|
94
|
+
self._qrotation,
|
|
95
|
+
self._qopacity,
|
|
96
|
+
self._qfeaturedc,
|
|
97
|
+
self._qfeaturerest,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Pickle the extra field if present
|
|
101
|
+
if payload.extra is not None:
|
|
102
|
+
extra_pickled = pickle.dumps(payload.extra, protocol=pickle.DEFAULT_PROTOCOL)
|
|
103
|
+
else:
|
|
104
|
+
extra_pickled = b""
|
|
105
|
+
|
|
106
|
+
# Combine with length-prefix framing:
|
|
107
|
+
# [draco_len][draco_bytes][extra_len][extra_bytes]
|
|
108
|
+
framed = (
|
|
109
|
+
_LEN.pack(len(draco_encoded))
|
|
110
|
+
+ draco_encoded
|
|
111
|
+
+ _LEN.pack(len(extra_pickled))
|
|
112
|
+
+ extra_pickled
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Compress incrementally
|
|
116
|
+
out = self._compressor.compress(framed)
|
|
117
|
+
if out:
|
|
118
|
+
yield out
|
|
119
|
+
|
|
120
|
+
def flush(self) -> Iterator[bytes]:
|
|
121
|
+
"""
|
|
122
|
+
Flush remaining compressed data.
|
|
123
|
+
|
|
124
|
+
Yields:
|
|
125
|
+
Final compressed byte chunks.
|
|
126
|
+
"""
|
|
127
|
+
tail = self._compressor.flush()
|
|
128
|
+
if tail:
|
|
129
|
+
yield tail
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DracoDeserializer(AbstractDeserializer):
|
|
133
|
+
"""
|
|
134
|
+
Streaming deserializer using zstd + length-prefix framing + Draco decoding + cloudpickle.
|
|
135
|
+
|
|
136
|
+
Decompresses incoming bytes incrementally, buffers until a complete
|
|
137
|
+
length-prefixed frame is available, then decodes and yields DracoPayloads.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self):
|
|
141
|
+
"""Initialize the deserializer."""
|
|
142
|
+
self._decompressor = zstd.ZstdDecompressor().decompressobj()
|
|
143
|
+
self._buffer = bytearray()
|
|
144
|
+
|
|
145
|
+
def deserialize_frame(self, data: bytes) -> Iterator[DracoPayload]:
|
|
146
|
+
"""
|
|
147
|
+
Decompress and deserialize bytes to DracoPayload objects.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
data: Compressed bytes to deserialize.
|
|
151
|
+
|
|
152
|
+
Yields:
|
|
153
|
+
Complete DracoPayload objects as they become available.
|
|
154
|
+
"""
|
|
155
|
+
# Decompress and add to buffer
|
|
156
|
+
decompressed = self._decompressor.decompress(data)
|
|
157
|
+
if decompressed:
|
|
158
|
+
self._buffer.extend(decompressed)
|
|
159
|
+
|
|
160
|
+
# Yield complete frames
|
|
161
|
+
yield from self._extract_payloads()
|
|
162
|
+
|
|
163
|
+
def flush(self) -> Iterator[DracoPayload]:
|
|
164
|
+
"""
|
|
165
|
+
Flush any remaining buffered data.
|
|
166
|
+
|
|
167
|
+
Yields:
|
|
168
|
+
Any remaining DracoPayload objects in the buffer.
|
|
169
|
+
"""
|
|
170
|
+
# Try to extract any remaining complete payloads
|
|
171
|
+
yield from self._extract_payloads()
|
|
172
|
+
|
|
173
|
+
# If there's leftover data, it's incomplete/corrupted
|
|
174
|
+
if self._buffer:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Incomplete data in buffer: {len(self._buffer)} bytes remaining"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _extract_payloads(self) -> Iterator[DracoPayload]:
|
|
180
|
+
"""
|
|
181
|
+
Extract complete payloads from the buffer.
|
|
182
|
+
|
|
183
|
+
Yields:
|
|
184
|
+
Complete DracoPayload objects.
|
|
185
|
+
"""
|
|
186
|
+
while True:
|
|
187
|
+
# Need at least draco length prefix
|
|
188
|
+
if len(self._buffer) < _LEN.size:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# Read draco length
|
|
192
|
+
(draco_len,) = _LEN.unpack(self._buffer[: _LEN.size])
|
|
193
|
+
|
|
194
|
+
# Check if we have complete draco data + extra length prefix
|
|
195
|
+
min_frame_size = _LEN.size + draco_len + _LEN.size
|
|
196
|
+
if len(self._buffer) < min_frame_size:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
# Read extra length
|
|
200
|
+
extra_len_offset = _LEN.size + draco_len
|
|
201
|
+
(extra_len,) = _LEN.unpack(
|
|
202
|
+
self._buffer[extra_len_offset: extra_len_offset + _LEN.size]
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Check if we have complete frame
|
|
206
|
+
total_frame_size = min_frame_size + extra_len
|
|
207
|
+
if len(self._buffer) < total_frame_size:
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# Extract draco bytes
|
|
211
|
+
draco_bytes = bytes(self._buffer[_LEN.size: _LEN.size + draco_len])
|
|
212
|
+
|
|
213
|
+
# Extract extra bytes
|
|
214
|
+
extra_offset = extra_len_offset + _LEN.size
|
|
215
|
+
extra_bytes = bytes(self._buffer[extra_offset: extra_offset + extra_len])
|
|
216
|
+
|
|
217
|
+
# Remove consumed data from buffer
|
|
218
|
+
del self._buffer[:total_frame_size]
|
|
219
|
+
|
|
220
|
+
# Decode draco data
|
|
221
|
+
pc = dracoreduced3dgs.decode(draco_bytes)
|
|
222
|
+
|
|
223
|
+
# Unpickle extra if present
|
|
224
|
+
extra = pickle.loads(extra_bytes) if extra_bytes else None
|
|
225
|
+
|
|
226
|
+
# Construct and yield DracoPayload
|
|
227
|
+
yield DracoPayload(
|
|
228
|
+
positions=pc.positions,
|
|
229
|
+
scales=pc.scales,
|
|
230
|
+
rotations=pc.rotations,
|
|
231
|
+
opacities=pc.opacities,
|
|
232
|
+
features_dc=pc.features_dc,
|
|
233
|
+
features_rest=pc.features_rest,
|
|
234
|
+
extra=extra,
|
|
235
|
+
)
|