Skip to content

Commit

Permalink
Implement experimental point cloud export API
Browse files Browse the repository at this point in the history
  • Loading branch information
johningve committed Dec 5, 2024
1 parent 7c396d2 commit f3d5f47
Show file tree
Hide file tree
Showing 10 changed files with 469 additions and 1 deletion.
1 change: 1 addition & 0 deletions modules/_zivid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
Frame,
FrameInfo,
PointCloud,
point_cloud_export,
Settings,
version,
Settings2D,
Expand Down
3 changes: 3 additions & 0 deletions modules/zivid/experimental/point_cloud_export/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Module for exporting point cloud data to various formats. This API may change in the future."""

from zivid.experimental.point_cloud_export._export_frame import export_frame
49 changes: 49 additions & 0 deletions modules/zivid/experimental/point_cloud_export/_export_frame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from zivid.frame import Frame
from zivid.experimental.point_cloud_export.file_format import ZDF, PLY, XYZ, PCD

import _zivid


def export_frame(frame, file_format):
"""Save frame to a file.
The file format is specified by the file_format argument. The file format can be ZDF, PLY, XYZ, or PCD.
If the format is PCD, this function stores the ordered point cloud with a header that indicates an unordered point
cloud. Since SDK 2.5, it is possible to export PCD with correct header by setting
`Configuration/APIBreakingBugFixes/FileFormats/PCD/UseOrganizedFormat` in Config.yml file. See
https://support.zivid.com/en/latest/reference-articles/point-cloud-structure-and-output-formats.html#organized-pcd-format.
Args:
frame: Frame to export.
file_format: File format specification.
Raises:
TypeError: If frame is not a Frame.
TypeError: If file_format is not a file format specification.
"""
if not isinstance(frame, Frame):
raise TypeError(
"Unsupported type for argument frame. Got {}, expected {}".format(
type(frame), Frame
)
)
if not any(
[
isinstance(file_format, ZDF),
isinstance(file_format, PLY),
isinstance(file_format, XYZ),
isinstance(file_format, PCD),
]
):
raise TypeError(
"Unsupported type for argument file_format. Got {}, expected {}".format(
type(file_format),
" or ".join([t.__name__ for t in [ZDF, PLY, XYZ, PCD]]),
)
)

format_impl_attr = f"_{type(file_format).__name__}__impl"
_zivid.point_cloud_export.export_frame(
frame._Frame__impl, getattr(file_format, format_impl_attr)
)
183 changes: 183 additions & 0 deletions modules/zivid/experimental/point_cloud_export/file_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import _zivid


class ColorSpace:
"""Color space for saving point cloud."""

linear_rgb = "linear_rgb"
srgb = "srgb"

@staticmethod
def valid_values():
"""Get valid values for color space."""
return [ColorSpace.linear_rgb, ColorSpace.srgb]

@classmethod
def _to_internal(cls, value):
if value == ColorSpace.linear_rgb:
return _zivid.point_cloud_export.ColorSpace.linear_rgb
if value == ColorSpace.srgb:
return _zivid.point_cloud_export.ColorSpace.srgb
raise ValueError(
"Invalid color space '{}'. Valid color spaces are: {}".format(
value, cls.valid_values()
)
)


class ZDF:
"""Specification for saving frame in ZDF (*.zdf) format."""

def __init__(self, file_name):
"""Create a ZDF file format specification with file name.
Args:
file_name: File name.
"""
if not isinstance(file_name, str):
raise TypeError(
"Unsupported type for argument file_name. Got {}, expected {}".format(
type(file_name), str
)
)
self.__impl = _zivid.point_cloud_export.file_format.ZDF(file_name)

def __str__(self):
return str(self.__impl)


class PLY:
"""Specification for saving frame in PLY (*.ply) format.
PLY is a file format developed at Stanford. To learn more about the PLY file format,
see https://paulbourke.net/dataformats/ply/.
"""

class Layout:
"""Layout for saving point cloud."""

ordered = "ordered"
unordered = "unordered"

@staticmethod
def valid_values():
"""Get valid values for layout."""
return [PLY.Layout.ordered, PLY.Layout.unordered]

@classmethod
def _to_internal(cls, value):
if value == PLY.Layout.ordered:
return _zivid.point_cloud_export.file_format.PLY.Layout.ordered
if value == PLY.Layout.unordered:
return _zivid.point_cloud_export.file_format.PLY.Layout.unordered
raise ValueError(
"Invalid layout '{}'. Valid layouts are: {}".format(
value, cls.valid_values()
)
)

def __init__(self, file_name, layout=Layout.ordered, color_space=ColorSpace.srgb):
"""Create a PLY file format specification with file name.
Args:
file_name: File name.
layout: Layout of point cloud. Default is ordered.
color_space: Color space of point cloud. Default is sRGB.
"""
if not isinstance(file_name, str):
raise TypeError(
"Unsupported type for argument file_name. Got {}, expected {}".format(
type(file_name), str
)
)
if not isinstance(layout, str):
raise TypeError(
"Unsupported type for argument layout. Got {}, expected {}".format(
type(layout), str
)
)
if not isinstance(color_space, str):
raise TypeError(
"Unsupported type for argument color_space. Got {}, expected {}".format(
type(color_space), str
)
)
self.__impl = _zivid.point_cloud_export.file_format.PLY(
file_name,
PLY.Layout._to_internal(layout),
ColorSpace._to_internal(color_space),
)

def __str__(self):
return str(self.__impl)


class XYZ:
"""Specification for saving frame in ASCII (*.xyz) format
ASCII characters are used to store cartesian coordinates of XYZ points and RGB color values.
"""

def __init__(self, file_name, color_space=ColorSpace.srgb):
"""Create a XYZ file format specification with file name.
Sets color space to linear RGB.
Args:
file_name: File name.
color_space: Color space of point cloud. Default is sRGB.
"""
if not isinstance(file_name, str):
raise TypeError(
"Unsupported type for argument file_name. Got {}, expected {}".format(
type(file_name), str
)
)
if not isinstance(color_space, str):
raise TypeError(
"Unsupported type for argument color_space. Got {}, expected {}".format(
type(color_space), str
)
)
self.__impl = _zivid.point_cloud_export.file_format.XYZ(
file_name, ColorSpace._to_internal(color_space)
)

def __str__(self):
return str(self.__impl)


class PCD:
"""Specification for saving frame in PCD (*.pcd) format.
PCD is a file format native to the Point Cloud Library (PCL). To learn more about
the PCD file format, see
https://pcl.readthedocs.io/projects/tutorials/en/latest/pcd_file_format.html#pcd-file-format.
"""

def __init__(self, file_name, color_space=ColorSpace.srgb):
"""Create a PCD file format specification with file name.
Args:
file_name: File name.
color_space: Color space of point cloud. Default is sRGB.
"""
if not isinstance(file_name, str):
raise TypeError(
"Unsupported type for argument file_name. Got {}, expected {}".format(
type(file_name), str
)
)
if not isinstance(color_space, str):
raise TypeError(
"Unsupported type for argument color_space. Got {}, expected {}".format(
type(color_space), str
)
)
self.__impl = _zivid.point_cloud_export.file_format.PCD(
file_name,
ColorSpace._to_internal(color_space),
)

def __str__(self):
return str(self.__impl)
8 changes: 7 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,13 @@ def _main():
author="Zivid AS",
author_email="[email protected]",
license="BSD 3-Clause",
packages=["zivid", "zivid._calibration", "zivid.experimental", "_zivid"],
packages=[
"zivid",
"zivid._calibration",
"zivid.experimental",
"zivid.experimental.point_cloud_export",
"_zivid",
],
package_dir={"": "modules"},
install_requires=["numpy"],
cmake_args=[
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ set(SOURCES
InfieldCorrection/InfieldCorrection.cpp
NodeType.cpp
PixelMapping.cpp
PointCloudExport.cpp
Projection.cpp
Presets.cpp
ReleasableArray2D.cpp
Expand Down
119 changes: 119 additions & 0 deletions src/PointCloudExport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#include <ZividPython/PointCloudExport.h>

#include <ZividPython/ReleasableFrame.h>

#include <utility>

namespace py = pybind11;

namespace
{
template<typename FileFormat>
auto wrapFileFormat(py::class_<FileFormat> pyClass)
{
return pyClass.def(py::init<const std::string &>(), py::arg("file_name"))
.def("to_string", &FileFormat::toString);
}

template<typename FileFormat>
auto exportFrame(const ZividPython::ReleasableFrame &frame, const FileFormat &fileFormat)
{
return Zivid::Experimental::PointCloudExport::exportFrame(frame.impl(), fileFormat);
}
} // namespace

namespace ZividPython
{
void wrapEnum(py::enum_<Zivid::Experimental::PointCloudExport::ColorSpace> pyEnum)
{
pyEnum.value("srgb", Zivid::Experimental::PointCloudExport::ColorSpace::sRGB)
.value("linear_rgb", Zivid::Experimental::PointCloudExport::ColorSpace::linearRGB)
.export_values();
}

void wrapClass(py::class_<Zivid::Experimental::PointCloudExport::FileFormat::ZDF> pyClass)
{
wrapFileFormat(std::move(pyClass));
}

void wrapClass(py::class_<Zivid::Experimental::PointCloudExport::FileFormat::PLY> pyClass)
{
using Layout = Zivid::Experimental::PointCloudExport::FileFormat::PLY::Layout;
ZIVID_PYTHON_WRAP_ENUM_CLASS(pyClass, Layout);

wrapFileFormat(std::move(pyClass))
.def(py::init<const std::string &, Layout, Zivid::Experimental::PointCloudExport::ColorSpace>(),
py::arg("file_name"),
py::arg("layout"),
py::arg("color_space"));
}

void wrapEnum(py::enum_<Zivid::Experimental::PointCloudExport::FileFormat::PLY::Layout> pyEnum)
{
pyEnum.value("ordered", Zivid::Experimental::PointCloudExport::FileFormat::PLY::Layout::ordered)
.value("unordered", Zivid::Experimental::PointCloudExport::FileFormat::PLY::Layout::unordered)
.export_values();
}

void wrapClass(py::class_<Zivid::Experimental::PointCloudExport::FileFormat::XYZ> pyClass)
{
wrapFileFormat(std::move(pyClass))
.def(py::init<const std::string &, Zivid::Experimental::PointCloudExport::ColorSpace>(),
py::arg("file_name"),
py::arg("color_space"));
}

void wrapClass(py::class_<Zivid::Experimental::PointCloudExport::FileFormat::PCD> pyClass)
{
wrapFileFormat(std::move(pyClass))
.def(py::init<const std::string &, Zivid::Experimental::PointCloudExport::ColorSpace>(),
py::arg("file_name"),
py::arg("color_space"));
}

namespace PointCloudExport
{
namespace FileFormat
{
void wrapAsSubmodule(py::module &dest)
{
using ZDF = Zivid::Experimental::PointCloudExport::FileFormat::ZDF;
ZIVID_PYTHON_WRAP_CLASS(dest, ZDF);

using PLY = Zivid::Experimental::PointCloudExport::FileFormat::PLY;
ZIVID_PYTHON_WRAP_CLASS(dest, PLY);

using XYZ = Zivid::Experimental::PointCloudExport::FileFormat::XYZ;
ZIVID_PYTHON_WRAP_CLASS(dest, XYZ);

using PCD = Zivid::Experimental::PointCloudExport::FileFormat::PCD;
ZIVID_PYTHON_WRAP_CLASS(dest, PCD);
}
} // namespace FileFormat

void wrapAsSubmodule(py::module &dest)
{
using ColorSpace = Zivid::Experimental::PointCloudExport::ColorSpace;
ZIVID_PYTHON_WRAP_ENUM_CLASS(dest, ColorSpace);

wrapNamespaceAsSubmodule(dest, FileFormat::wrapAsSubmodule, "FileFormat");

dest.def("export_frame",
py::overload_cast<const ReleasableFrame &,
const Zivid::Experimental::PointCloudExport::FileFormat::ZDF &>(
&exportFrame<Zivid::Experimental::PointCloudExport::FileFormat::ZDF>))
.def("export_frame",
py::overload_cast<const ReleasableFrame &,
const Zivid::Experimental::PointCloudExport::FileFormat::PLY &>(
&exportFrame<Zivid::Experimental::PointCloudExport::FileFormat::PLY>))
.def("export_frame",
py::overload_cast<const ReleasableFrame &,
const Zivid::Experimental::PointCloudExport::FileFormat::XYZ &>(
&exportFrame<Zivid::Experimental::PointCloudExport::FileFormat::XYZ>))
.def("export_frame",
py::overload_cast<const ReleasableFrame &,
const Zivid::Experimental::PointCloudExport::FileFormat::PCD &>(
&exportFrame<Zivid::Experimental::PointCloudExport::FileFormat::PCD>));
}
} // namespace PointCloudExport
} // namespace ZividPython
Loading

0 comments on commit f3d5f47

Please sign in to comment.