ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
FilePLY.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include <FileSystem.h>
9 #include <Logging.h>
10 #include <ProgressReporters.h>
11 #include <rply.h>
12 
13 #include <vector>
14 
15 #include "cloudViewer/core/Dtype.h"
20 
21 namespace cloudViewer {
22 namespace t {
23 namespace io {
24 
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 
31 struct PLYReaderState {
32  struct AttrState {
33  std::string name_;
34  void *data_ptr_;
35  int stride_;
36  int offset_;
37  int64_t size_;
38  int64_t current_size_;
39  };
40  // Allow fast access of attr_state by index.
41  std::vector<std::shared_ptr<AttrState>> id_to_attr_state_;
42  utility::CountingProgressReporter *progress_bar_;
43 };
44 
45 template <typename T>
46 int ReadAttributeCallback(p_ply_argument argument) {
47  PLYReaderState *state_ptr;
48  long id;
49  ply_get_argument_user_data(argument, reinterpret_cast<void **>(&state_ptr),
50  &id);
51  std::shared_ptr<PLYReaderState::AttrState> &attr_state =
52  state_ptr->id_to_attr_state_[id];
53  if (attr_state->current_size_ >= attr_state->size_) {
54  return 0;
55  }
56 
57  T *data_ptr = static_cast<T *>(attr_state->data_ptr_);
58  const int64_t index = attr_state->stride_ * attr_state->current_size_ +
59  attr_state->offset_;
60  data_ptr[index] = static_cast<T>(ply_get_argument_value(argument));
61 
62  ++attr_state->current_size_;
63 
64  if (attr_state->offset_ == 0 && attr_state->current_size_ % 1000 == 0) {
65  state_ptr->progress_bar_->Update(attr_state->current_size_);
66  }
67  return 1;
68 }
69 
70 // Some of these datatypes are supported by Tensor but are added here just
71 // for completeness.
72 std::string GetDtypeString(e_ply_type type) {
73  if (type == PLY_INT8) {
74  return "int8";
75  } else if (type == PLY_UINT8) {
76  return "uint8";
77  } else if (type == PLY_INT16) {
78  return "int16";
79  } else if (type == PLY_UINT16) {
80  return "uint16";
81  } else if (type == PLY_INT32) {
82  return "int32";
83  } else if (type == PLY_UIN32) {
84  return "uint32";
85  } else if (type == PLY_FLOAT32) {
86  return "float32";
87  } else if (type == PLY_FLOAT64) {
88  return "float64";
89  } else if (type == PLY_CHAR) {
90  return "char";
91  } else if (type == PLY_UCHAR) {
92  return "uchar";
93  } else if (type == PLY_SHORT) {
94  return "short";
95  } else if (type == PLY_USHORT) {
96  return "ushort";
97  } else if (type == PLY_INT) {
98  return "int";
99  } else if (type == PLY_UINT) {
100  return "uint";
101  } else if (type == PLY_FLOAT) {
102  return "float";
103  } else if (type == PLY_DOUBLE) {
104  return "double";
105  } else if (type == PLY_LIST) {
106  return "list";
107  } else {
108  return "unknown";
109  }
110 }
111 
112 core::Dtype GetDtype(e_ply_type type) {
113  // PLY_LIST attribute is not supported.
114  // Currently, we are not doing datatype conversions, so some of the ply
115  // datatypes are not included.
116 
117  if (type == PLY_UINT8 || type == PLY_UCHAR) {
118  return core::UInt8;
119  } else if (type == PLY_UINT16) {
120  return core::UInt16;
121  } else if (type == PLY_INT32 || type == PLY_INT) {
122  return core::Int32;
123  } else if (type == PLY_FLOAT32 || type == PLY_FLOAT) {
124  return core::Float32;
125  } else if (type == PLY_FLOAT64 || type == PLY_DOUBLE) {
126  return core::Float64;
127  } else {
128  return core::Undefined;
129  }
130 }
131 
132 // Map ply attributes to CloudViewer point cloud attributes
133 std::tuple<std::string, int, int> GetNameStrideOffsetForAttribute(
134  const std::string &name) {
135  // Positions attribute.
136  if (name == "x") return std::make_tuple("positions", 3, 0);
137  if (name == "y") return std::make_tuple("positions", 3, 1);
138  if (name == "z") return std::make_tuple("positions", 3, 2);
139 
140  // Normals attribute.
141  if (name == "nx") return std::make_tuple("normals", 3, 0);
142  if (name == "ny") return std::make_tuple("normals", 3, 1);
143  if (name == "nz") return std::make_tuple("normals", 3, 2);
144 
145  // Colors attribute.
146  if (name == "red") return std::make_tuple("colors", 3, 0);
147  if (name == "green") return std::make_tuple("colors", 3, 1);
148  if (name == "blue") return std::make_tuple("colors", 3, 2);
149 
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  }
170  // Other attribute.
171  return std::make_tuple(name, 1, 0);
172 }
173 
174 } // namespace
175 
177  const std::string &filename,
178  geometry::PointCloud &pointcloud,
180  p_ply ply_file = ply_open(filename.c_str(), nullptr, 0, nullptr);
181  if (!ply_file) {
182  utility::LogWarning("Read PLY failed: unable to open file: {}.",
183  filename.c_str());
184  return false;
185  }
186  if (!ply_read_header(ply_file)) {
187  utility::LogWarning("Read PLY failed: unable to parse header.");
188  ply_close(ply_file);
189  return false;
190  }
191 
192  PLYReaderState state;
193 
194  const char *element_name;
195  long element_size = 0;
196  // Loop through ply elements and find "vertex".
197  p_ply_element element = ply_get_next_element(ply_file, nullptr);
198  while (element) {
199  ply_get_element_info(element, &element_name, &element_size);
200  if (std::string(element_name) == "vertex") {
201  break;
202  } else {
203  element = ply_get_next_element(ply_file, element);
204  }
205  }
206 
207  // No element with name "vertex".
208  if (!element) {
209  utility::LogWarning("Read PLY failed: no vertex attribute.");
210  ply_close(ply_file);
211  return false;
212  }
213 
214  std::unordered_map<std::string, bool> primary_attr_init = {
215  {"positions", false}, {"normals", false}, {"colors", false},
216  {"opacity", false}, {"rot", false}, {"scale", false},
217  {"f_dc", false}, {"f_rest", false},
218  };
219 
220  int f_rest_count = 0;
221  p_ply_property attribute = ply_get_next_property(element, nullptr);
222 
223  while (attribute) {
224  e_ply_type type;
225  const char *name;
226  ply_get_property_info(attribute, &name, &type, nullptr, nullptr);
227 
228  if (GetDtype(type) == core::Undefined) {
230  "Read PLY warning: skipping property \"{}\", unsupported "
231  "datatype \"{}\".",
232  name, GetDtypeString(type));
233  } else {
234  auto attr_state = std::make_shared<PLYReaderState::AttrState>();
235  long size = 0;
236  long id = static_cast<long>(state.id_to_attr_state_.size());
237  DISPATCH_DTYPE_TO_TEMPLATE(GetDtype(type), [&]() {
238  size = ply_set_read_cb(ply_file, element_name, name,
239  ReadAttributeCallback<scalar_t>, &state,
240  id);
241  });
242  if (size != element_size) {
244  "Total size of property {} ({}) is not equal to "
245  "size of {} ({}).",
246  name, size, element_name, element_size);
247  }
248 
249  const std::string attr_name = std::string(name);
250  auto [name, stride, offset] =
251  GetNameStrideOffsetForAttribute(attr_name);
252  if (name == "f_rest") {
253  f_rest_count++;
254  }
255  attr_state->name_ = name;
256  attr_state->stride_ = stride;
257  attr_state->offset_ = offset;
258 
259  if (primary_attr_init.count(name)) {
260  if (primary_attr_init.at(name) == false) {
261  pointcloud.SetPointAttr(
262  name, core::Tensor::Empty({element_size, stride},
263  GetDtype(type)));
264  primary_attr_init[name] = true;
265  }
266  } else {
267  pointcloud.SetPointAttr(
268  name, core::Tensor::Empty({element_size, stride},
269  GetDtype(type)));
270  }
271  attr_state->data_ptr_ = pointcloud.GetPointAttr(name).GetDataPtr();
272  attr_state->size_ = element_size;
273  attr_state->current_size_ = 0;
274  state.id_to_attr_state_.push_back(attr_state);
275  }
276  attribute = ply_get_next_property(element, attribute);
277  }
278 
279  if (f_rest_count > 0) {
280  if (f_rest_count % 3 != 0) {
282  "Read PLY failed: 3DGS f_rest attribute has {} elements "
283  "per point, which is not divisible by 3.",
284  f_rest_count);
285  ply_close(ply_file);
286  return false;
287  }
288  core::Tensor new_f_rest =
289  core::Tensor::Empty({element_size, f_rest_count / 3, 3},
290  core::Float32, pointcloud.GetDevice());
291  pointcloud.SetPointAttr("f_rest", new_f_rest);
292  for (auto &attr_state : state.id_to_attr_state_) {
293  if (attr_state->name_ == "f_rest") {
294  attr_state->data_ptr_ =
295  pointcloud.GetPointAttr("f_rest").GetDataPtr();
296  attr_state->stride_ = f_rest_count;
297  }
298  }
299  }
300 
301  utility::CountingProgressReporter reporter(params.update_progress);
302  reporter.SetTotal(element_size);
303  state.progress_bar_ = &reporter;
304 
305  if (!ply_read(ply_file)) {
306  utility::LogWarning("Read PLY failed: unable to read file: {}.",
307  filename);
308  ply_close(ply_file);
309  return false;
310  }
311 
312  ply_close(ply_file);
313  reporter.Finish();
314 
315  if (pointcloud.IsGaussianSplat()) { // validates 3DGS, if present.
316  utility::LogDebug("PLY file contains a Gaussian Splat.");
317  }
318 
319  return true;
320 }
321 
322 namespace {
323 e_ply_type GetPlyType(const core::Dtype &dtype) {
324  if (dtype == core::UInt8) {
325  return PLY_UCHAR;
326  } else if (dtype == core::UInt16) {
327  return PLY_UINT16;
328  } else if (dtype == core::Int32) {
329  return PLY_INT;
330  } else if (dtype == core::Float32) {
331  return PLY_FLOAT;
332  } else if (dtype == core::Float64) {
333  return PLY_DOUBLE;
334  } else {
336  "Data-type {} is not supported in WritePointCloudToPLY. "
337  "Supported data-types include UInt8, UInt16, Int32, Float32 "
338  "and Float64.",
339  dtype.ToString());
340  }
341 }
342 
343 struct AttributePtr {
344  AttributePtr(const core::Dtype &dtype,
345  const void *data_ptr,
346  const int &group_size)
347  : dtype_(dtype), data_ptr_(data_ptr), group_size_(group_size) {}
348 
349  const core::Dtype dtype_;
350  const void *data_ptr_;
351  const int group_size_;
352 };
353 } // namespace
354 
356  const std::string &filename,
357  const geometry::PointCloud &pointcloud,
359  if (pointcloud.IsEmpty()) {
360  utility::LogWarning("Write PLY failed: point cloud has 0 points.");
361  return false;
362  }
363 
364  if (pointcloud.IsGaussianSplat()) { // validates 3DGS, if present.
365  utility::LogDebug("Writing Gaussian Splat point cloud to PLY file.");
366  }
367 
368  geometry::TensorMap t_map =
369  pointcloud.To(core::Device("CPU:0")).GetPointAttr().Contiguous();
370 
371  long num_points =
372  static_cast<long>(pointcloud.GetPointPositions().GetLength());
373 
374  // Verify that standard attributes have length equal to num_points.
375  // Extra attributes must have at least 2 dimensions: (num_points, channels,
376  // ...).
377  for (auto const &it : t_map) {
378  if (it.first == "positions" || it.first == "normals" ||
379  it.first == "colors") {
380  if (it.second.GetLength() != num_points) {
382  "Write PLY failed: Points ({}) and {} ({}) have "
383  "different lengths.",
384  num_points, it.first, it.second.GetLength());
385  return false;
386  }
387  } else {
388  auto shape = it.second.GetShape();
389  // Only tensors with shape (num_points, channels, ...) are
390  // supported.
391  if (shape.size() < 2 || shape[0] != num_points) {
393  "Write PLY failed. PointCloud contains {} attribute "
394  "which is not supported by PLY IO. Only points, "
395  "normals, colors and attributes with shape "
396  "(num_points, channels, ...) are supported. Expected "
397  "shape: {{{}, ...}} but got {}.",
398  it.first, num_points, it.second.GetShape().ToString());
399  return false;
400  }
401  }
402  }
403 
404  p_ply ply_file =
405  ply_create(filename.c_str(),
406  bool(params.write_ascii) ? PLY_ASCII : PLY_LITTLE_ENDIAN,
407  NULL, 0, NULL);
408  if (!ply_file) {
409  utility::LogWarning("Write PLY failed: unable to open file: {}.",
410  filename);
411  return false;
412  }
413 
414  ply_add_comment(ply_file, "Created by CloudViewer");
415  ply_add_element(ply_file, "vertex", num_points);
416 
417  std::vector<AttributePtr> attribute_ptrs;
418  attribute_ptrs.emplace_back(t_map["positions"].GetDtype(),
419  t_map["positions"].GetDataPtr(), 3);
420 
421  e_ply_type pointType = GetPlyType(t_map["positions"].GetDtype());
422  ply_add_property(ply_file, "x", pointType, pointType, pointType);
423  ply_add_property(ply_file, "y", pointType, pointType, pointType);
424  ply_add_property(ply_file, "z", pointType, pointType, pointType);
425 
426  if (pointcloud.HasPointNormals()) {
427  attribute_ptrs.emplace_back(t_map["normals"].GetDtype(),
428  t_map["normals"].GetDataPtr(), 3);
429 
430  e_ply_type pointNormalsType = GetPlyType(t_map["normals"].GetDtype());
431  ply_add_property(ply_file, "nx", pointNormalsType, pointNormalsType,
432  pointNormalsType);
433  ply_add_property(ply_file, "ny", pointNormalsType, pointNormalsType,
434  pointNormalsType);
435  ply_add_property(ply_file, "nz", pointNormalsType, pointNormalsType,
436  pointNormalsType);
437  }
438 
439  if (pointcloud.HasPointColors()) {
440  attribute_ptrs.emplace_back(t_map["colors"].GetDtype(),
441  t_map["colors"].GetDataPtr(), 3);
442 
443  e_ply_type pointColorType = GetPlyType(t_map["colors"].GetDtype());
444  ply_add_property(ply_file, "red", pointColorType, pointColorType,
445  pointColorType);
446  ply_add_property(ply_file, "green", pointColorType, pointColorType,
447  pointColorType);
448  ply_add_property(ply_file, "blue", pointColorType, pointColorType,
449  pointColorType);
450  }
451 
452  // Process extra attributes.
453  // Extra attributes are expected to be tensors with shape (num_points,
454  // channels) or (num_points, C, D). For multi-channel attributes, the
455  // channels are flattened, and each channel is written as a separate
456  // property (e.g., "f_rest_0", "f_rest_1", ...).
457  e_ply_type attributeType;
458  for (auto const &it : t_map) {
459  if (it.first == "positions" || it.first == "colors" ||
460  it.first == "normals")
461  continue;
462  auto shape = it.second.GetShape();
463  int group_size = 1;
464  if (shape.size() == 2) {
465  group_size = shape[1];
466  } else if (shape.size() >= 3) {
467  group_size = shape[1] * shape[2];
468  }
469  if (group_size == 1) {
470  attribute_ptrs.emplace_back(it.second.GetDtype(),
471  it.second.GetDataPtr(), 1);
472  attributeType = GetPlyType(it.second.GetDtype());
473  ply_add_property(ply_file, it.first.c_str(), attributeType,
474  attributeType, attributeType);
475  } else {
476  for (int ch = 0; ch < group_size; ch++) {
477  std::string prop_name = it.first + "_" + std::to_string(ch);
478  attributeType = GetPlyType(it.second.GetDtype());
479  ply_add_property(ply_file, prop_name.c_str(), attributeType,
480  attributeType, attributeType);
481  }
482  attribute_ptrs.emplace_back(it.second.GetDtype(),
483  it.second.GetDataPtr(), group_size);
484  }
485  }
486 
487  if (!ply_write_header(ply_file)) {
488  utility::LogWarning("Write PLY failed: unable to write header.");
489  ply_close(ply_file);
490  return false;
491  }
492 
493  utility::CountingProgressReporter reporter(params.update_progress);
494  reporter.SetTotal(num_points);
495 
496  for (int64_t i = 0; i < num_points; i++) {
497  for (auto it : attribute_ptrs) {
498  DISPATCH_DTYPE_TO_TEMPLATE(it.dtype_, [&]() {
499  const scalar_t *data_ptr =
500  static_cast<const scalar_t *>(it.data_ptr_);
501  for (int idx_offset = it.group_size_ * i;
502  idx_offset < it.group_size_ * (i + 1); ++idx_offset) {
503  ply_write(ply_file, data_ptr[idx_offset]);
504  }
505  });
506  }
507 
508  if (i % 1000 == 0) {
509  reporter.Update(i);
510  }
511  }
512 
513  reporter.Finish();
514  ply_close(ply_file);
515  return true;
516 }
517 
518 } // namespace io
519 } // namespace t
520 } // namespace cloudViewer
std::string filename
#define DISPATCH_DTYPE_TO_TEMPLATE(DTYPE,...)
Definition: Dispatch.h:31
int size
std::string name
int offset
char type
int64_t current_size_
Definition: FilePLY.cpp:38
int offset_
Definition: FilePLY.cpp:36
const core::Dtype dtype_
Definition: FilePLY.cpp:349
std::string name_
Definition: FilePLY.cpp:33
int64_t size_
Definition: FilePLY.cpp:37
int stride_
Definition: FilePLY.cpp:35
void * data_ptr_
Definition: FilePLY.cpp:34
std::vector< std::shared_ptr< AttrState > > id_to_attr_state_
Definition: FilePLY.cpp:41
utility::CountingProgressReporter * progress_bar_
Definition: FilePLY.cpp:42
const int group_size_
Definition: FilePLY.cpp:351
#define PLY_LIST
Definition: Ply.h:82
#define PLY_UINT
Definition: Ply.h:67
#define PLY_ASCII
Definition: Ply.h:51
#define PLY_FLOAT
Definition: Ply.h:68
#define PLY_INT
Definition: Ply.h:64
#define PLY_DOUBLE
Definition: Ply.h:69
#define PLY_UCHAR
Definition: Ply.h:65
#define PLY_USHORT
Definition: Ply.h:66
#define PLY_SHORT
Definition: Ply.h:63
#define PLY_CHAR
Definition: Ply.h:62
cmdLineReadable * params[]
size_t stride
#define NULL
std::string ToString() const
Definition: Dtype.h:65
int64_t GetLength() const
Definition: Tensor.h:1125
static Tensor Empty(const SizeVector &shape, Dtype dtype, const Device &device=Device("CPU:0"))
Create a tensor with uninitialized values.
Definition: Tensor.cpp:400
A point cloud contains a list of 3D points.
Definition: PointCloud.h:82
core::Tensor & GetPointPositions()
Get the value of the "positions" attribute. Convenience function.
Definition: PointCloud.h:124
bool IsEmpty() const override
Returns !HasPointPositions().
Definition: PointCloud.h:257
PointCloud To(const core::Device &device, bool copy=false) const
Definition: PointCloud.cpp:112
core::Device GetDevice() const override
Returns the device attribute of this PointCloud.
Definition: PointCloud.h:421
const TensorMap & GetPointAttr() const
Getter for point_attr_ TensorMap. Used in Pybind.
Definition: PointCloud.h:111
void SetPointAttr(const std::string &key, const core::Tensor &value)
Definition: PointCloud.h:177
#define LogWarning(...)
Definition: Logging.h:72
#define LogError(...)
Definition: Logging.h:60
#define LogDebug(...)
Definition: Logging.h:90
__host__ __device__ float length(float2 v)
Definition: cutil_math.h:1162
const Dtype Undefined
Definition: Dtype.cpp:41
const Dtype UInt8
Definition: Dtype.cpp:48
const Dtype Float64
Definition: Dtype.cpp:43
const Dtype UInt16
Definition: Dtype.cpp:49
const Dtype Int32
Definition: Dtype.cpp:46
const Dtype Float32
Definition: Dtype.cpp:42
bool WritePointCloudToPLY(const std::string &filename, const geometry::PointCloud &pointcloud, const cloudViewer::io::WritePointCloudOption &params)
Definition: FilePLY.cpp:355
bool ReadPointCloudFromPLY(const std::string &filename, geometry::PointCloud &pointcloud, const cloudViewer::io::ReadPointCloudOption &params)
Definition: FilePLY.cpp:176
Generic file read and write utility for python interface.
std::string to_string(const T &n)
Definition: Common.h:20
Optional parameters to ReadPointCloud.
Definition: FileIO.h:39
Optional parameters to WritePointCloud.
Definition: FileIO.h:77