Skip to content

Commit 8893079

Browse files
lumurillojccruznadiegointeljoserochhikun03
authored
Gaussian Splat read, write and conversion to and from ply and splat files.
* Gaussian Splat read, write and conversion to and from ply and splat files. * IsGaussianSplat() function to check if a point cloud is a valid Gaussian Splat. * Unit tests for reading and writing ply and splat (read-write-read consistency). * Unit tests for converting ply to splat. * Jupyter notebook tutorial. References: - 3DGS files save to .ply format: https://github.com/graphdeco-inria/gaussian-splatting/blob/main/scene/gaussian_model.py - 3DGS files saved to .splat format: https://github.com/antimatter15/splat/blob/main/convert.py - Reference viewer: https://projects.markkellogg.org/threejs/demo_gaussian_splats_3d.php - 3DGS to point cloud reference: https://github.com/Lewis-Stuart-11/3DGS-to-PC/blob/main/gauss_to_pc.py 3DGS tensor point cloud representation: - pcd.point[‘positions’] (N,3) – (x,y,z) for each splat. - pcd.point[‘opacity’] – (N,) opacity - pcd.point[‘rot’] (N, 4) - quaternion rotation of Gaussian. - pcd.point[‘scale'] (N, 3) - x, y, z scales for Gaussian. - pcd.point[‘f_dc’] (N, 3) – DC components for RGB colors. - pcd.point[‘f_rest’] (N, Nc, 3) – SH coeffs for RGB colors. (Nc = 3, 8 or 15) --------- Co-authored-by: Juan Cruz <juan.cruz.naranjo@intel.com> Co-authored-by: Gomez Rodriguez, Diego <diego.gomez.rodriguez@intel.com> Co-authored-by: Jose Rojas Chaves <joserochh@gmail.com> Co-authored-by: ikun03 <ikunal03@gmail.com> Co-authored-by: Jose Rojas Chaves <jose.rojas.chaves@intel.com> Co-authored-by: Sameer Sheorey <sameer.sheorey@intel.com>
1 parent 8d06977 commit 8893079

8 files changed

Lines changed: 687 additions & 52 deletions

File tree

cpp/open3d/t/geometry/PointCloud.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,21 @@ core::Tensor PointCloud::ComputeMetrics(const PointCloud &pcd2,
13691369
metrics, params);
13701370
}
13711371

1372+
bool PointCloud::IsGaussianSplat() const {
1373+
auto num_points = GetPointPositions().GetLength();
1374+
bool have_all_attrs = HasPointAttr("opacity") && HasPointAttr("rot") &&
1375+
HasPointAttr("scale") && HasPointAttr("f_dc");
1376+
if (!have_all_attrs) { // not 3DGS, no messages.
1377+
return false;
1378+
}
1379+
// Existing but invalid attributes cause errors.
1380+
core::AssertTensorShape(GetPointAttr("opacity"), {num_points, 1});
1381+
core::AssertTensorShape(GetPointAttr("rot"), {num_points, 4});
1382+
core::AssertTensorShape(GetPointAttr("scale"), {num_points, 3});
1383+
core::AssertTensorShape(GetPointAttr("f_dc"), {num_points, 3});
1384+
// GaussianSplatGetSHOrder(); // TODO: Tests f_rest shape is valid.
1385+
return true;
1386+
}
13721387
} // namespace geometry
13731388
} // namespace t
13741389
} // namespace open3d

cpp/open3d/t/geometry/PointCloud.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,14 @@ class PointCloud : public Geometry, public DrawableGeometry {
738738
std::vector<Metric> metrics = {Metric::ChamferDistance},
739739
MetricParameters params = MetricParameters()) const;
740740

741+
/// Check if this point cloud has all the attributes required for a Gaussian
742+
/// Splat. This checks for the presence of scale, rot, opacity and f_dc
743+
/// attributes.
744+
/// \returns True if a valid 3DGS point cloud, else False.
745+
/// \throws If point cloud has 3DGS attributes, but they are invalid (wrong
746+
/// shape).
747+
bool IsGaussianSplat() const;
748+
741749
protected:
742750
core::Device device_ = core::Device("CPU:0");
743751
TensorMap point_attr_;

cpp/open3d/t/io/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ target_sources(tio PRIVATE
1313
file_format/FileJPG.cpp
1414
file_format/FilePCD.cpp
1515
file_format/FilePLY.cpp
16+
file_format/FileSPLAT.cpp
1617
file_format/FilePNG.cpp
1718
file_format/FilePTS.cpp
1819
file_format/FileTXT.cpp

cpp/open3d/t/io/PointCloudIO.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ static const std::unordered_map<
3535
{"xyzrgb", ReadPointCloudFromTXT},
3636
{"pcd", ReadPointCloudFromPCD},
3737
{"ply", ReadPointCloudFromPLY},
38+
{"splat", ReadPointCloudFromSPLAT},
3839
{"pts", ReadPointCloudFromPTS},
3940
};
4041

@@ -52,6 +53,7 @@ static const std::unordered_map<
5253
{"xyzrgb", WritePointCloudToTXT},
5354
{"pcd", WritePointCloudToPCD},
5455
{"ply", WritePointCloudToPLY},
56+
{"splat", WritePointCloudToSPLAT},
5557
{"pts", WritePointCloudToPTS},
5658
};
5759

cpp/open3d/t/io/PointCloudIO.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ bool WritePointCloudToPLY(const std::string &filename,
7474
const geometry::PointCloud &pointcloud,
7575
const WritePointCloudOption &params);
7676

77+
bool ReadPointCloudFromSPLAT(const std::string &filename,
78+
geometry::PointCloud &pointcloud,
79+
const ReadPointCloudOption &params);
80+
81+
bool WritePointCloudToSPLAT(const std::string &filename,
82+
const geometry::PointCloud &pointcloud,
83+
const WritePointCloudOption &params);
84+
7785
bool ReadPointCloudFromPTS(const std::string &filename,
7886
geometry::PointCloud &pointcloud,
7987
const ReadPointCloudOption &params);

cpp/open3d/t/io/file_format/FilePLY.cpp

Lines changed: 132 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ namespace open3d {
2222
namespace t {
2323
namespace io {
2424

25+
namespace {
26+
constexpr auto ROTATION_SUFFIX_INDEX = std::char_traits<char>::length("rot_");
27+
constexpr auto SCALE_SUFFIX_INDEX = std::char_traits<char>::length("scale_");
28+
constexpr auto F_DC_SUFFIX_INDEX = std::char_traits<char>::length("f_dc_");
29+
constexpr auto F_REST_SUFFIX_INDEX = std::char_traits<char>::length("f_rest_");
30+
2531
struct PLYReaderState {
2632
struct AttrState {
2733
std::string name_;
@@ -37,7 +43,7 @@ struct PLYReaderState {
3743
};
3844

3945
template <typename T>
40-
static int ReadAttributeCallback(p_ply_argument argument) {
46+
int ReadAttributeCallback(p_ply_argument argument) {
4147
PLYReaderState *state_ptr;
4248
long id;
4349
ply_get_argument_user_data(argument, reinterpret_cast<void **>(&state_ptr),
@@ -63,7 +69,7 @@ static int ReadAttributeCallback(p_ply_argument argument) {
6369

6470
// Some of these datatypes are supported by Tensor but are added here just
6571
// for completeness.
66-
static std::string GetDtypeString(e_ply_type type) {
72+
std::string GetDtypeString(e_ply_type type) {
6773
if (type == PLY_INT8) {
6874
return "int8";
6975
} else if (type == PLY_UINT8) {
@@ -103,7 +109,7 @@ static std::string GetDtypeString(e_ply_type type) {
103109
}
104110
}
105111

106-
static core::Dtype GetDtype(e_ply_type type) {
112+
core::Dtype GetDtype(e_ply_type type) {
107113
// PLY_LIST attribute is not supported.
108114
// Currently, we are not doing datatype conversions, so some of the ply
109115
// datatypes are not included.
@@ -123,7 +129,8 @@ static core::Dtype GetDtype(e_ply_type type) {
123129
}
124130
}
125131

126-
static std::tuple<std::string, int, int> GetNameStrideOffsetForAttribute(
132+
// Map ply attributes to Open3D point cloud attributes
133+
std::tuple<std::string, int, int> GetNameStrideOffsetForAttribute(
127134
const std::string &name) {
128135
// Positions attribute.
129136
if (name == "x") return std::make_tuple("positions", 3, 0);
@@ -140,10 +147,32 @@ static std::tuple<std::string, int, int> GetNameStrideOffsetForAttribute(
140147
if (name == "green") return std::make_tuple("colors", 3, 1);
141148
if (name == "blue") return std::make_tuple("colors", 3, 2);
142149

150+
// 3DGS SH DC component.
151+
if (name.rfind(std::string("f_dc") + "_", 0) == 0) {
152+
int offset = std::stoi(name.substr(F_DC_SUFFIX_INDEX));
153+
return std::make_tuple("f_dc", 3, offset);
154+
}
155+
// 3DGS SH higher order components. stride = 0 => set later
156+
if (name.rfind(std::string("f_rest") + "_", 0) == 0) {
157+
int offset = std::stoi(name.substr(F_REST_SUFFIX_INDEX));
158+
return std::make_tuple("f_rest", 0, offset);
159+
}
160+
// 3DGS Gaussian scale attribute.
161+
if (name.rfind(std::string("scale") + "_", 0) == 0) {
162+
int offset = std::stoi(name.substr(SCALE_SUFFIX_INDEX));
163+
return std::make_tuple("scale", 3, offset);
164+
}
165+
// 3DGS Gaussian rotation as a quaternion.
166+
if (name.rfind(std::string("rot") + "_", 0) == 0) {
167+
int offset = std::stoi(name.substr(ROTATION_SUFFIX_INDEX));
168+
return std::make_tuple("rot", 4, offset);
169+
}
143170
// Other attribute.
144171
return std::make_tuple(name, 1, 0);
145172
}
146173

174+
} // namespace
175+
147176
bool ReadPointCloudFromPLY(const std::string &filename,
148177
geometry::PointCloud &pointcloud,
149178
const open3d::io::ReadPointCloudOption &params) {
@@ -177,12 +206,17 @@ bool ReadPointCloudFromPLY(const std::string &filename,
177206
// No element with name "vertex".
178207
if (!element) {
179208
utility::LogWarning("Read PLY failed: no vertex attribute.");
209+
ply_close(ply_file);
180210
return false;
181211
}
182212

183213
std::unordered_map<std::string, bool> primary_attr_init = {
184-
{"positions", false}, {"normals", false}, {"colors", false}};
214+
{"positions", false}, {"normals", false}, {"colors", false},
215+
{"opacity", false}, {"rot", false}, {"scale", false},
216+
{"f_dc", false}, {"f_rest", false},
217+
};
185218

219+
int f_rest_count = 0;
186220
p_ply_property attribute = ply_get_next_property(element, nullptr);
187221

188222
while (attribute) {
@@ -210,39 +244,59 @@ bool ReadPointCloudFromPLY(const std::string &filename,
210244
"size of {} ({}).",
211245
name, size, element_name, element_size);
212246
}
213-
const std::string attr_name = std::string(name);
214247

215-
std::tie(attr_state->name_, attr_state->stride_,
216-
attr_state->offset_) =
248+
const std::string attr_name = std::string(name);
249+
auto [name, stride, offset] =
217250
GetNameStrideOffsetForAttribute(attr_name);
251+
if (name == "f_rest") {
252+
f_rest_count++;
253+
}
254+
attr_state->name_ = name;
255+
attr_state->stride_ = stride;
256+
attr_state->offset_ = offset;
218257

219-
if (primary_attr_init.count(attr_state->name_)) {
220-
if (primary_attr_init.at(attr_state->name_) == false) {
258+
if (primary_attr_init.count(name)) {
259+
if (primary_attr_init.at(name) == false) {
221260
pointcloud.SetPointAttr(
222-
attr_state->name_,
223-
core::Tensor::Empty(
224-
{element_size, attr_state->stride_},
225-
GetDtype(type)));
226-
primary_attr_init[attr_state->name_] = true;
261+
name, core::Tensor::Empty({element_size, stride},
262+
GetDtype(type)));
263+
primary_attr_init[name] = true;
227264
}
228265
} else {
229266
pointcloud.SetPointAttr(
230-
attr_state->name_,
231-
core::Tensor::Empty({element_size, attr_state->stride_},
232-
GetDtype(type)));
267+
name, core::Tensor::Empty({element_size, stride},
268+
GetDtype(type)));
233269
}
234-
235-
attr_state->data_ptr_ =
236-
pointcloud.GetPointAttr(attr_state->name_).GetDataPtr();
237-
270+
attr_state->data_ptr_ = pointcloud.GetPointAttr(name).GetDataPtr();
238271
attr_state->size_ = element_size;
239272
attr_state->current_size_ = 0;
240273
state.id_to_attr_state_.push_back(attr_state);
241274
}
242-
243275
attribute = ply_get_next_property(element, attribute);
244276
}
245277

278+
if (f_rest_count > 0) {
279+
if (f_rest_count % 3 != 0) {
280+
utility::LogWarning(
281+
"Read PLY failed: 3DGS f_rest attribute has {} elements "
282+
"per point, which is not divisible by 3.",
283+
f_rest_count);
284+
ply_close(ply_file);
285+
return false;
286+
}
287+
core::Tensor new_f_rest =
288+
core::Tensor::Empty({element_size, f_rest_count / 3, 3},
289+
core::Float32, pointcloud.GetDevice());
290+
pointcloud.SetPointAttr("f_rest", new_f_rest);
291+
for (auto &attr_state : state.id_to_attr_state_) {
292+
if (attr_state->name_ == "f_rest") {
293+
attr_state->data_ptr_ =
294+
pointcloud.GetPointAttr("f_rest").GetDataPtr();
295+
attr_state->stride_ = f_rest_count;
296+
}
297+
}
298+
}
299+
246300
utility::CountingProgressReporter reporter(params.update_progress);
247301
reporter.SetTotal(element_size);
248302
state.progress_bar_ = &reporter;
@@ -257,10 +311,15 @@ bool ReadPointCloudFromPLY(const std::string &filename,
257311
ply_close(ply_file);
258312
reporter.Finish();
259313

314+
if (pointcloud.IsGaussianSplat()) { // validates 3DGS, if present.
315+
utility::LogDebug("PLY file contains a Gaussian Splat.");
316+
}
317+
260318
return true;
261319
}
262320

263-
static e_ply_type GetPlyType(const core::Dtype &dtype) {
321+
namespace {
322+
e_ply_type GetPlyType(const core::Dtype &dtype) {
264323
if (dtype == core::UInt8) {
265324
return PLY_UCHAR;
266325
} else if (dtype == core::UInt16) {
@@ -290,6 +349,7 @@ struct AttributePtr {
290349
const void *data_ptr_;
291350
const int group_size_;
292351
};
352+
} // namespace
293353

294354
bool WritePointCloudToPLY(const std::string &filename,
295355
const geometry::PointCloud &pointcloud,
@@ -299,12 +359,19 @@ bool WritePointCloudToPLY(const std::string &filename,
299359
return false;
300360
}
301361

302-
geometry::TensorMap t_map(pointcloud.GetPointAttr().Contiguous());
362+
if (pointcloud.IsGaussianSplat()) { // validates 3DGS, if present.
363+
utility::LogDebug("Writing Gaussian Splat point cloud to PLY file.");
364+
}
365+
366+
geometry::TensorMap t_map =
367+
pointcloud.To(core::Device("CPU:0")).GetPointAttr().Contiguous();
303368

304369
long num_points =
305370
static_cast<long>(pointcloud.GetPointPositions().GetLength());
306371

307-
// Make sure all the attributes have same size.
372+
// Verify that standard attributes have length equal to num_points.
373+
// Extra attributes must have at least 2 dimensions: (num_points, channels,
374+
// ...).
308375
for (auto const &it : t_map) {
309376
if (it.first == "positions" || it.first == "normals" ||
310377
it.first == "colors") {
@@ -315,15 +382,20 @@ bool WritePointCloudToPLY(const std::string &filename,
315382
num_points, it.first, it.second.GetLength());
316383
return false;
317384
}
318-
} else if (it.second.GetShape() != core::SizeVector({num_points, 1})) {
319-
utility::LogWarning(
320-
"Write PLY failed. PointCloud contains {} attribute which "
321-
"is not supported by PLY IO. Only points, normals, colors "
322-
"and attributes with shape (num_points, 1) are supported. "
323-
"Expected shape: {} but got {}.",
324-
it.first, core::SizeVector({num_points, 1}).ToString(),
325-
it.second.GetShape().ToString());
326-
return false;
385+
} else {
386+
auto shape = it.second.GetShape();
387+
// Only tensors with shape (num_points, channels, ...) are
388+
// supported.
389+
if (shape.size() < 2 || shape[0] != num_points) {
390+
utility::LogWarning(
391+
"Write PLY failed. PointCloud contains {} attribute "
392+
"which is not supported by PLY IO. Only points, "
393+
"normals, colors and attributes with shape "
394+
"(num_points, channels, ...) are supported. Expected "
395+
"shape: {{{}, ...}} but got {}.",
396+
it.first, num_points, it.second.GetShape().ToString());
397+
return false;
398+
}
327399
}
328400
}
329401

@@ -375,16 +447,38 @@ bool WritePointCloudToPLY(const std::string &filename,
375447
pointColorType);
376448
}
377449

450+
// Process extra attributes.
451+
// Extra attributes are expected to be tensors with shape (num_points,
452+
// channels) or (num_points, C, D). For multi-channel attributes, the
453+
// channels are flattened, and each channel is written as a separate
454+
// property (e.g., "f_rest_0", "f_rest_1", ...).
378455
e_ply_type attributeType;
379456
for (auto const &it : t_map) {
380-
if (it.first != "positions" && it.first != "colors" &&
381-
it.first != "normals") {
457+
if (it.first == "positions" || it.first == "colors" ||
458+
it.first == "normals")
459+
continue;
460+
auto shape = it.second.GetShape();
461+
int group_size = 1;
462+
if (shape.size() == 2) {
463+
group_size = shape[1];
464+
} else if (shape.size() >= 3) {
465+
group_size = shape[1] * shape[2];
466+
}
467+
if (group_size == 1) {
382468
attribute_ptrs.emplace_back(it.second.GetDtype(),
383469
it.second.GetDataPtr(), 1);
384-
385470
attributeType = GetPlyType(it.second.GetDtype());
386471
ply_add_property(ply_file, it.first.c_str(), attributeType,
387472
attributeType, attributeType);
473+
} else {
474+
for (int ch = 0; ch < group_size; ch++) {
475+
std::string prop_name = it.first + "_" + std::to_string(ch);
476+
attributeType = GetPlyType(it.second.GetDtype());
477+
ply_add_property(ply_file, prop_name.c_str(), attributeType,
478+
attributeType, attributeType);
479+
}
480+
attribute_ptrs.emplace_back(it.second.GetDtype(),
481+
it.second.GetDataPtr(), group_size);
388482
}
389483
}
390484

0 commit comments

Comments
 (0)