@@ -22,6 +22,12 @@ namespace open3d {
2222namespace t {
2323namespace 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+
2531struct PLYReaderState {
2632 struct AttrState {
2733 std::string name_;
@@ -37,7 +43,7 @@ struct PLYReaderState {
3743};
3844
3945template <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+
147176bool ReadPointCloudFromPLY (const std::string &filename,
148177 geometry::PointCloud &pointcloud,
149178 const open3d::io::ReadPointCloudOption ¶ms) {
@@ -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
294354bool 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